diff --git a/.gitignore b/.gitignore index fde48b798..a1feaa08c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ python/src/xstudio.egg-info/ python/test/xstudio.log docs/conf.py python/src/xstudio/version.py +.vs/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ba40ec9b5..f5da236e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,39 @@ 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() + set(XSTUDIO_GLOBAL_VERSION "0.11.2" CACHE STRING "Version string") set(XSTUDIO_GLOBAL_NAME xStudio) project(${XSTUDIO_GLOBAL_NAME} VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) +cmake_policy(VERSION 3.26) + 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" ON) +option(BUILD_DOCS "Build xStudio documentation" OFF) 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) + +if(WIN32) + set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") + add_compile_options($<$:/MP>) + # 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") @@ -30,7 +49,9 @@ if (("${CMAKE_GENERATOR}" MATCHES "Makefiles" OR ("${CMAKE_GENERATOR}" MATCHES " endif() set(CXXOPTS_BUILD_TESTS OFF CACHE BOOL "Enable or disable cxxopts' tests") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -fmax-errors=5 -fdiagnostics-color=always") +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -fmax-errors=5 -fdiagnostics-color=always") +endif() if (${OPTIMIZE_FOR_NATIVE}) include(CheckCXXCompilerFlag) @@ -50,18 +71,27 @@ if (NOT ${GCC_MARCH_OVERRIDE} STREQUAL "") endif() endif() - -set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") +endif() set(TEST_RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/test_resource") set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) -set(CMAKE_CXX_STANDARD 17) +if(WIN32) + set(CMAKE_CXX_STANDARD 20) + add_compile_definitions($<$:_ITERATOR_DEBUG_LEVEL=0>) +else() + set(CMAKE_CXX_STANDARD 17) +endif() + set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_LINK_DEPENDS_NO_SHARED true) -set(CMAKE_THREAD_LIBS_INIT "-lpthread") +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(CMAKE_THREAD_LIBS_INIT "-lpthread") +endif() set(CMAKE_HAVE_THREADS_LIBRARY 1) set(CMAKE_USE_WIN32_THREADS_INIT 0) set(CMAKE_USE_PTHREADS_INIT 1) @@ -73,10 +103,14 @@ set(REPROC++ ON) set(OpenGL_GL_PREFERENCE GLVND) if (USE_SANITIZER STREQUAL "Address") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fsanitize=address") + if(MSVC) + target_compile_options( PUBLIC /fsanitize=address) + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fsanitize=address") + endif() elseif (USE_SANITIZER STREQUAL "Thread") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=thread") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread") @@ -123,7 +157,55 @@ if(ENABLE_CLANG_TIDY) endif() -find_package(nlohmann_json REQUIRED) +if(WIN32) + ADD_DEFINITIONS(-DNOMINMAX) + set(CMAKE_CXX_STANDARD 20) + add_compile_options(/permissive-) + + # Workaround for C++ 20+ comparisons in nlohmann json + # https://github.com/nlohmann/json/issues/3868#issuecomment-1563726354 + add_definitions(-DJSON_HAS_THREE_WAY_COMPARISON=OFF) + + 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 ) +endif() + +if(MSVC) + #Getenv complains, would be good to fix later but tired of seeing this for now. + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + +endif() + +# Add the necessary libraries from Vcpkg if Vcpkg integration is enabled +if(USE_VCPKG) + + 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 + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "PATH=${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3" python.exe -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 + 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") + +endif() + +find_package(nlohmann_json CONFIG REQUIRED) include(CTest) if(ENABLE_CLANG_FORMAT) @@ -136,7 +218,6 @@ if(ENABLE_CLANG_FORMAT) clangformat_setup(${FORMAT_ITEMS}) endif() - if(INSTALL_PYTHON_MODULE) add_subdirectory(python) endif() @@ -144,10 +225,24 @@ endif() 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) + install(DIRECTORY include/xstudio + DESTINATION include) + + INSTALL(DIRECTORY extern/ DESTINATION extern) + if(BUILD_DOCS) if(NOT INSTALL_PYTHON_MODULE) add_subdirectory(python) @@ -157,6 +252,33 @@ if(INSTALL_XSTUDIO) install(DIRECTORY share/docs/ DESTINATION share/xstudio/docs) endif () + include(CMakePackageConfigHelpers) + + 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(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 () -add_subdirectory("extern/reproc") +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 diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..fa832be0e --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,47 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "windows-base", + "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" + } + }, + { + "name": "Release", + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "RelWithDebInfo", + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "USE_SANITIZER": "address" + } + }, + { + "name": "Debug", + "hidden": true, + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_SANITIZER": "address" + } + } + ] +} diff --git a/NOTICE.TXT b/NOTICE.TXT index 7ddfab8fe..07b0f31b4 100644 --- a/NOTICE.TXT +++ b/NOTICE.TXT @@ -91,4 +91,31 @@ Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: -fonts at gnome dot org. \ No newline at end of file +fonts at gnome dot org. + + +zstr + +Located in extern/include/ + +The MIT License (MIT) + +Copyright (c) 2015 Matei David, Ontario Institute for Cancer Research + +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. \ No newline at end of file diff --git a/README.md b/README.md index 12b3f9067..124f666e4 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,26 @@ 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. -## Building xSTUDIO for MS Windows +## Building xSTUDIO -You can now build and run xSTUDIO on MS Windows. However, work towards full Windows compatibility is still in its final phase and the updates are therefore not yet merged into the main branch here. To access the Windows compatible codebase please follow [this link](https://github.com/mpkepic/xstudio/tree/windows). +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. -## Building xSTUDIO for Linux +We provide comprehensive build steps for 4 of the most popular distributions. -We provide comprehensive build steps for 3 of the most popular Linux distributions: +### Building xSTUDIO for Linux * [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) -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. +### Building xSTUDIO for Windows + +* [Windows](docs/build_guides/windows.md) + +### Building xSTUDIO for MacOS -## Building xSTUDIO for MacOS +MacOS compatibility is not yet available. Watch this space! -MacOS compatibility is not yet available but it is due in Q3 or Q4 2023. Watch this space! +### 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. diff --git a/cmake/macros.cmake b/cmake/macros.cmake index c57cf90fe..53a27b288 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -2,15 +2,18 @@ macro(default_compile_options name) target_compile_options(${name} # PRIVATE -fvisibility=hidden - PRIVATE $<$:-fno-omit-frame-pointer> + PRIVATE $<$,$>:-fno-omit-frame-pointer> + PRIVATE $<$,$>:/Oy> + PRIVATE $<$,$>:/showIncludes> # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> - PRIVATE $<$:-Wno-unused-function> + PRIVATE $<$,$>:-Wno-unused-function> + PRIVATE $<$,$>:-Wextra> + PRIVATE $<$,$>:-Wpedantic> + PRIVATE $<$,$>:/wd4100> # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> - PRIVATE $<$:-Wextra> - PRIVATE $<$:-Wpedantic> # PRIVATE ${GTEST_CFLAGS} ) @@ -21,13 +24,16 @@ macro(default_compile_options name) target_compile_definitions(${name} PUBLIC $<$:test_private=public> PUBLIC $<$>:test_private=private> - PRIVATE -D__linux__ + $<$:_GNU_SOURCE> # Define _GNU_SOURCE for Linux + $<$:__linux__> # Define __linux__ for Linux + $<$:_WIN32> # Define _WIN32 for Windows PRIVATE XSTUDIO_GLOBAL_VERSION=\"${XSTUDIO_GLOBAL_VERSION}\" PRIVATE XSTUDIO_GLOBAL_NAME=\"${XSTUDIO_GLOBAL_NAME}\" - PRIVATE PROJECT_VERSION=\"${PROJECT_VERSION}\" - PRIVATE BINARY_DIR=\"${CMAKE_BINARY_DIR}/bin\" + PUBLIC PROJECT_VERSION=\"${PROJECT_VERSION}\" + 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> ) endmacro() @@ -40,16 +46,16 @@ if (BUILD_TESTING) # 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} - PUBLIC cxx_std_17 + PUBLIC cxx_std_20 ) target_compile_definitions(${name} @@ -57,9 +63,9 @@ if (BUILD_TESTING) PUBLIC $<$>:test_private=private> PRIVATE XSTUDIO_GLOBAL_VERSION=\"${XSTUDIO_GLOBAL_VERSION}\" PRIVATE XSTUDIO_GLOBAL_NAME=\"${XSTUDIO_GLOBAL_NAME}\" - PRIVATE PROJECT_VERSION=\"${PROJECT_VERSION}\" + PUBLIC PROJECT_VERSION=\"${PROJECT_VERSION}\" PRIVATE SOURCE_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}\" - PRIVATE BINARY_DIR=\"${CMAKE_BINARY_DIR}/bin\" + PUBLIC BINARY_DIR=\"${CMAKE_BINARY_DIR}/bin\" PRIVATE TEST_RESOURCE=\"${TEST_RESOURCE}\" PRIVATE ROOT_DIR=\"${ROOT_DIR}\" PRIVATE $<$:XSTUDIO_DEBUG=1> @@ -72,7 +78,7 @@ macro(default_options_local 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} @@ -92,8 +98,13 @@ endmacro() macro(default_options name) default_options_local(${name}) - install(TARGETS ${name} + install(TARGETS ${name} EXPORT xstudio LIBRARY DESTINATION share/xstudio/lib) + target_include_directories(${name} INTERFACE + $ + $ + $ + $) endmacro() macro(default_options_static name) @@ -101,7 +112,7 @@ macro(default_options_static 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} @@ -141,6 +152,22 @@ macro(default_plugin_options name) ) install(TARGETS ${name} LIBRARY DESTINATION share/xstudio/plugin) + + 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 ) + # 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) + + #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" + ) + endif() + endmacro() if (BUILD_TESTING) @@ -191,7 +218,7 @@ macro(default_options_qt name) PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/lib" ) - install(TARGETS ${name} + install(TARGETS ${name} EXPORT xstudio LIBRARY DESTINATION share/xstudio/lib) endmacro() @@ -223,6 +250,7 @@ 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}) @@ -256,6 +284,15 @@ macro(create_component_with_alias NAME ALIASNAME VERSION DEPS) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + if(_WIN32) + set(CMAKE_CXX_VISIBILITY_PRESET hidden) + set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) + endif(_WIN32) + + # Generate export header + include(GenerateExportHeader) + generate_export_header(${PROJECT_NAME}) + endmacro() macro(create_component_static NAME VERSION DEPS STATICDEPS) @@ -356,11 +393,23 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) add_library(${ALIASNAME} ALIAS ${PROJECT_NAME}) default_options_qt(${PROJECT_NAME}) + # 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} ) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + 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} + # PUBLIC ${CMAKE_BINARY_DIR} # Include the build directory + #) + + endmacro() @@ -384,3 +433,8 @@ macro(build_studio_plugins STUDIO) 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 b0dc3bac4..8a5255727 100644 --- a/cmake/modules/FindCAF.cmake +++ b/cmake/modules/FindCAF.cmake @@ -152,4 +152,4 @@ if (CAF_test_FOUND AND NOT TARGET caf::test) set_target_properties(caf::test PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CAF_INCLUDE_DIR_TEST}" INTERFACE_LINK_LIBRARIES "caf::core") -endif () +endif () \ No newline at end of file diff --git a/cmake/modules/FindDbgHelp.cmake b/cmake/modules/FindDbgHelp.cmake new file mode 100644 index 000000000..022e46034 --- /dev/null +++ b/cmake/modules/FindDbgHelp.cmake @@ -0,0 +1,19 @@ +# 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/FindOpenEXR.cmake b/cmake/modules/FindOpenEXR.cmake index 84e0bebc2..40a7de668 100644 --- a/cmake/modules/FindOpenEXR.cmake +++ b/cmake/modules/FindOpenEXR.cmake @@ -130,7 +130,9 @@ if (CMAKE_USE_PTHREADS_INIT) endif () # Attempt to find OpenEXR with pkgconfig -find_package(PkgConfig) +if (UNIX AND NOT APPLE) + find_package(PkgConfig) +endif() if (PKG_CONFIG_FOUND) if (NOT Ilmbase_ROOT AND NOT ILMBASE_ROOT AND NOT DEFINED ENV{Ilmbase_ROOT} AND NOT DEFINED ENV{ILMBASE_ROOT}) diff --git a/cmake/modules/FindSphinx.cmake b/cmake/modules/FindSphinx.cmake index 13823090b..4825e1e14 100644 --- a/cmake/modules/FindSphinx.cmake +++ b/cmake/modules/FindSphinx.cmake @@ -1,11 +1,12 @@ -#Look for an executable called sphinx-build +# Look for an executable called sphinx-build find_program(SPHINX_EXECUTABLE NAMES sphinx-build + HINTS ${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3/Scripts DOC "Path to sphinx-build executable") include(FindPackageHandleStandardArgs) -#Handle standard arguments to find_package like REQUIRED and QUIET +# 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 diff --git a/cmake/modules/vcpkg.cmake b/cmake/modules/vcpkg.cmake new file mode 100644 index 000000000..daebd6d23 --- /dev/null +++ b/cmake/modules/vcpkg.cmake @@ -0,0 +1,615 @@ +# 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/docs/build_guides/media/images/Qt5_select_components.png b/docs/build_guides/media/images/Qt5_select_components.png new file mode 100644 index 000000000..c6c81078c Binary files /dev/null and b/docs/build_guides/media/images/Qt5_select_components.png differ diff --git a/docs/build_guides/media/images/setup_Qt5.png b/docs/build_guides/media/images/setup_Qt5.png new file mode 100644 index 000000000..71e1d6aae Binary files /dev/null and b/docs/build_guides/media/images/setup_Qt5.png differ diff --git a/docs/build_guides/media/images/setup_ffmpeg.png b/docs/build_guides/media/images/setup_ffmpeg.png new file mode 100644 index 000000000..a7fb0cf39 Binary files /dev/null and b/docs/build_guides/media/images/setup_ffmpeg.png differ diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md new file mode 100644 index 000000000..a578812f1 --- /dev/null +++ b/docs/build_guides/windows.md @@ -0,0 +1,49 @@ +# 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. + + +# Questions? + +Reach out on the ASWF Slack in the #open-review-initiative channel. diff --git a/docs/conf.py b/docs/conf.py index 34bf27d0f..811eeb66a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,9 +75,9 @@ # built documents. # # The short X.Y version. -version = '0.10.0' +version = '0.11.2' # The full version, including alpha/beta/rc tags. -release = '0.10.0' +release = '0.11.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/extern/include/QuickFuture b/extern/include/QuickFuture deleted file mode 120000 index a936bb7ab..000000000 --- a/extern/include/QuickFuture +++ /dev/null @@ -1 +0,0 @@ -../quickfuture/src/QuickFuture \ No newline at end of file diff --git a/extern/include/QuickFuture b/extern/include/QuickFuture new file mode 100644 index 000000000..a3f93128e --- /dev/null +++ b/extern/include/QuickFuture @@ -0,0 +1 @@ +#include "quickfuture.h" diff --git a/extern/include/cpp-httplib/httplib.h b/extern/include/cpp-httplib/httplib.h index 1dcb41bfd..0f86fbf61 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/gsl b/extern/include/gsl deleted file mode 120000 index 3420c7395..000000000 --- a/extern/include/gsl +++ /dev/null @@ -1 +0,0 @@ -../stduuid/include/gsl \ No newline at end of file diff --git a/extern/include/gsl/gsl b/extern/include/gsl/gsl new file mode 100644 index 000000000..55862ebdd --- /dev/null +++ b/extern/include/gsl/gsl @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_GSL_H +#define GSL_GSL_H + +#include // copy +#include // Ensures/Expects +#include // byte +#include // finally()/narrow()/narrow_cast()... +#include // multi_span, strided_span... +#include // owner, not_null +#include // span +#include // zstring, string_span, zstring_builder... + +#endif // GSL_GSL_H diff --git a/extern/include/gsl/gsl_algorithm b/extern/include/gsl/gsl_algorithm new file mode 100644 index 000000000..710792fbd --- /dev/null +++ b/extern/include/gsl/gsl_algorithm @@ -0,0 +1,63 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_ALGORITHM_H +#define GSL_ALGORITHM_H + +#include // for Expects +#include // for dynamic_extent, span + +#include // for copy_n +#include // for ptrdiff_t +#include // for is_assignable + +#ifdef _MSC_VER +#pragma warning(push) + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4996) // unsafe use of std::copy_n + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) +#endif // _MSC_VER + +namespace gsl +{ + +template +void copy(span src, span dest) +{ + static_assert(std::is_assignable::value, + "Elements of source span can not be assigned to elements of destination span"); + static_assert(SrcExtent == dynamic_extent || DestExtent == dynamic_extent || + (SrcExtent <= DestExtent), + "Source range is longer than target range"); + + Expects(dest.size() >= src.size()); + std::copy_n(src.data(), src.size(), dest.data()); +} + +} // namespace gsl + +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +#endif // GSL_ALGORITHM_H diff --git a/extern/include/gsl/gsl_assert b/extern/include/gsl/gsl_assert new file mode 100644 index 000000000..131fa8b15 --- /dev/null +++ b/extern/include/gsl/gsl_assert @@ -0,0 +1,145 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_CONTRACTS_H +#define GSL_CONTRACTS_H + +#include +#include // for logic_error + +// +// Temporary until MSVC STL supports no-exceptions mode. +// Currently terminate is a no-op in this mode, so we add termination behavior back +// +#if defined(_MSC_VER) && defined(_HAS_EXCEPTIONS) && !_HAS_EXCEPTIONS +#define GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND +#endif + +// +// There are three configuration options for this GSL implementation's behavior +// when pre/post conditions on the GSL types are violated: +// +// 1. GSL_TERMINATE_ON_CONTRACT_VIOLATION: std::terminate will be called (default) +// 2. GSL_THROW_ON_CONTRACT_VIOLATION: a gsl::fail_fast exception will be thrown +// 3. GSL_UNENFORCED_ON_CONTRACT_VIOLATION: nothing happens +// +#if !(defined(GSL_THROW_ON_CONTRACT_VIOLATION) || defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) || \ + defined(GSL_UNENFORCED_ON_CONTRACT_VIOLATION)) +#define GSL_TERMINATE_ON_CONTRACT_VIOLATION +#endif + +#define GSL_STRINGIFY_DETAIL(x) #x +#define GSL_STRINGIFY(x) GSL_STRINGIFY_DETAIL(x) + +#if defined(__clang__) || defined(__GNUC__) +#define GSL_LIKELY(x) __builtin_expect(!!(x), 1) +#define GSL_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else +#define GSL_LIKELY(x) (!!(x)) +#define GSL_UNLIKELY(x) (!!(x)) +#endif + +// +// GSL_ASSUME(cond) +// +// Tell the optimizer that the predicate cond must hold. It is unspecified +// whether or not cond is actually evaluated. +// +#ifdef _MSC_VER +#define GSL_ASSUME(cond) __assume(cond) +#elif defined(__GNUC__) +#define GSL_ASSUME(cond) ((cond) ? static_cast(0) : __builtin_unreachable()) +#else +#define GSL_ASSUME(cond) static_cast((cond) ? 0 : 0) +#endif + +// +// GSL.assert: assertions +// + +namespace gsl +{ +struct fail_fast : public std::logic_error +{ + explicit fail_fast(char const* const message) : std::logic_error(message) {} +}; + +namespace details +{ +#if defined(GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND) + + typedef void (__cdecl *terminate_handler)(); + + inline gsl::details::terminate_handler& get_terminate_handler() noexcept + { + static terminate_handler handler = &abort; + return handler; + } + +#endif + + [[noreturn]] inline void terminate() noexcept + { +#if defined(GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND) + (*gsl::details::get_terminate_handler())(); +#else + std::terminate(); +#endif + } + +#if defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) + + template + [[noreturn]] void throw_exception(Exception&&) + { + gsl::details::terminate(); + } + +#else + + template + [[noreturn]] void throw_exception(Exception&& exception) + { + throw std::forward(exception); + } + +#endif + +} // namespace details +} // namespace gsl + +#if defined(GSL_THROW_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) \ + (GSL_LIKELY(cond) ? static_cast(0) \ + : gsl::details::throw_exception(gsl::fail_fast( \ + "GSL: " type " failure at " __FILE__ ": " GSL_STRINGIFY(__LINE__)))) + +#elif defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) \ + (GSL_LIKELY(cond) ? static_cast(0) : gsl::details::terminate()) + +#elif defined(GSL_UNENFORCED_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) GSL_ASSUME(cond) + +#endif + +#define Expects(cond) GSL_CONTRACT_CHECK("Precondition", cond) +#define Ensures(cond) GSL_CONTRACT_CHECK("Postcondition", cond) + +#endif // GSL_CONTRACTS_H diff --git a/extern/include/gsl/gsl_byte b/extern/include/gsl/gsl_byte new file mode 100644 index 000000000..e8611733b --- /dev/null +++ b/extern/include/gsl/gsl_byte @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_BYTE_H +#define GSL_BYTE_H + +#include + +#ifdef _MSC_VER + +#pragma warning(push) + +// don't warn about function style casts in byte related operators +#pragma warning(disable : 26493) + +#ifndef GSL_USE_STD_BYTE +// this tests if we are under MSVC and the standard lib has std::byte and it is enabled +#if defined(_HAS_STD_BYTE) && _HAS_STD_BYTE + +#define GSL_USE_STD_BYTE 1 + +#else // defined(_HAS_STD_BYTE) && _HAS_STD_BYTE + +#define GSL_USE_STD_BYTE 0 + +#endif // defined(_HAS_STD_BYTE) && _HAS_STD_BYTE +#endif // GSL_USE_STD_BYTE + +#else // _MSC_VER + +#ifndef GSL_USE_STD_BYTE +// this tests if we are under GCC or Clang with enough -std:c++1z power to get us std::byte +#if defined(__cplusplus) && (__cplusplus >= 201703L) + +#define GSL_USE_STD_BYTE 1 +#include + +#else // defined(__cplusplus) && (__cplusplus >= 201703L) + +#define GSL_USE_STD_BYTE 0 + +#endif //defined(__cplusplus) && (__cplusplus >= 201703L) +#endif // GSL_USE_STD_BYTE + +#endif // _MSC_VER + +// Use __may_alias__ attribute on gcc and clang +#if defined __clang__ || (__GNUC__ > 5) +#define byte_may_alias __attribute__((__may_alias__)) +#else // defined __clang__ || defined __GNUC__ +#define byte_may_alias +#endif // defined __clang__ || defined __GNUC__ + +namespace gsl +{ +#if GSL_USE_STD_BYTE + + +using std::byte; +using std::to_integer; + +#else // GSL_USE_STD_BYTE + +// This is a simple definition for now that allows +// use of byte within span<> to be standards-compliant +enum class byte_may_alias byte : unsigned char +{ +}; + +template ::value>> +constexpr byte& operator<<=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte operator<<(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte& operator>>=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) >> shift); +} + +template ::value>> +constexpr byte operator>>(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) >> shift); +} + +constexpr byte& operator|=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) | static_cast(r)); +} + +constexpr byte operator|(byte l, byte r) noexcept +{ + return byte(static_cast(l) | static_cast(r)); +} + +constexpr byte& operator&=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) & static_cast(r)); +} + +constexpr byte operator&(byte l, byte r) noexcept +{ + return byte(static_cast(l) & static_cast(r)); +} + +constexpr byte& operator^=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator^(byte l, byte r) noexcept +{ + return byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator~(byte b) noexcept { return byte(~static_cast(b)); } + +template ::value>> +constexpr IntegerType to_integer(byte b) noexcept +{ + return static_cast(b); +} + +#endif // GSL_USE_STD_BYTE + +template +constexpr byte to_byte_impl(T t) noexcept +{ + static_assert( + E, "gsl::to_byte(t) must be provided an unsigned char, otherwise data loss may occur. " + "If you are calling to_byte with an integer contant use: gsl::to_byte() version."); + return static_cast(t); +} +template <> +constexpr byte to_byte_impl(unsigned char t) noexcept +{ + return byte(t); +} + +template +constexpr byte to_byte(T t) noexcept +{ + return to_byte_impl::value, T>(t); +} + +template +constexpr byte to_byte() noexcept +{ + static_assert(I >= 0 && I <= 255, + "gsl::byte only has 8 bits of storage, values must be in range 0-255"); + return static_cast(I); +} + +} // namespace gsl + +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +#endif // GSL_BYTE_H diff --git a/extern/include/gsl/gsl_util b/extern/include/gsl/gsl_util new file mode 100644 index 000000000..25f85020c --- /dev/null +++ b/extern/include/gsl/gsl_util @@ -0,0 +1,158 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_UTIL_H +#define GSL_UTIL_H + +#include // for Expects + +#include +#include // for ptrdiff_t, size_t +#include // for exception +#include // for initializer_list +#include // for is_signed, integral_constant +#include // for forward + +#if defined(_MSC_VER) + +#pragma warning(push) +#pragma warning(disable : 4127) // conditional expression is constant + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +namespace gsl +{ +// +// GSL.util: utilities +// + +// index type for all container indexes/subscripts/sizes +using index = std::ptrdiff_t; + +// final_action allows you to ensure something gets run at the end of a scope +template +class final_action +{ +public: + explicit final_action(F f) noexcept : f_(std::move(f)) {} + + final_action(final_action&& other) noexcept : f_(std::move(other.f_)), invoke_(other.invoke_) + { + other.invoke_ = false; + } + + final_action(const final_action&) = delete; + final_action& operator=(const final_action&) = delete; + final_action& operator=(final_action&&) = delete; + + ~final_action() noexcept + { + if (invoke_) f_(); + } + +private: + F f_; + bool invoke_ {true}; +}; + +// finally() - convenience function to generate a final_action +template + +final_action finally(const F& f) noexcept +{ + return final_action(f); +} + +template +final_action finally(F&& f) noexcept +{ + return final_action(std::forward(f)); +} + +// narrow_cast(): a searchable way to do narrowing casts of values +template +constexpr T narrow_cast(U&& u) noexcept +{ + return static_cast(std::forward(u)); +} + +struct narrowing_error : public std::exception +{ +}; + +namespace details +{ + template + struct is_same_signedness + : public std::integral_constant::value == std::is_signed::value> + { + }; +} + +// narrow() : a checked version of narrow_cast() that throws if the cast changed the value +template +T narrow(U u) +{ + T t = narrow_cast(u); + if (static_cast(t) != u) gsl::details::throw_exception(narrowing_error()); + if (!details::is_same_signedness::value && ((t < T{}) != (u < U{}))) + gsl::details::throw_exception(narrowing_error()); + return t; +} + +// +// at() - Bounds-checked way of accessing builtin arrays, std::array, std::vector +// +template +constexpr T& at(T (&arr)[N], const index i) +{ + Expects(i >= 0 && i < narrow_cast(N)); + return arr[static_cast(i)]; +} + +template +constexpr auto at(Cont& cont, const index i) -> decltype(cont[cont.size()]) +{ + Expects(i >= 0 && i < narrow_cast(cont.size())); + using size_type = decltype(cont.size()); + return cont[static_cast(i)]; +} + +template +constexpr T at(const std::initializer_list cont, const index i) +{ + Expects(i >= 0 && i < narrow_cast(cont.size())); + return *(cont.begin() + i); +} + +} // namespace gsl + +#if defined(_MSC_VER) +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 + +#pragma warning(pop) + +#endif // _MSC_VER + +#endif // GSL_UTIL_H diff --git a/extern/include/gsl/multi_span b/extern/include/gsl/multi_span new file mode 100644 index 000000000..9c0c27b33 --- /dev/null +++ b/extern/include/gsl/multi_span @@ -0,0 +1,2242 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_MULTI_SPAN_H +#define GSL_MULTI_SPAN_H + +#include // for Expects +#include // for byte +#include // for narrow_cast + +#include // for transform, lexicographical_compare +#include // for array +#include +#include // for ptrdiff_t, size_t, nullptr_t +#include // for PTRDIFF_MAX +#include // for divides, multiplies, minus, negate, plus +#include // for initializer_list +#include // for iterator, random_access_iterator_tag +#include // for numeric_limits +#include +#include +#include +#include // for basic_string +#include // for enable_if_t, remove_cv_t, is_same, is_co... +#include + +#ifdef _MSC_VER + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(push) +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4702) // unreachable code + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +// GCC 7 does not like the signed unsigned missmatch (size_t ptrdiff_t) +// While there is a conversion from signed to unsigned, it happens at +// compiletime, so the compiler wouldn't have to warn indiscriminently, but +// could check if the source value actually doesn't fit into the target type +// and only warn in those cases. +#if __GNUC__ > 6 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +#ifdef GSL_THROW_ON_CONTRACT_VIOLATION +#define GSL_NOEXCEPT /*noexcept*/ +#else +#define GSL_NOEXCEPT noexcept +#endif // GSL_THROW_ON_CONTRACT_VIOLATION + +namespace gsl +{ + +/* +** begin definitions of index and bounds +*/ +namespace details +{ + template + struct SizeTypeTraits + { + static const SizeType max_value = std::numeric_limits::max(); + }; + + template + class are_integral : public std::integral_constant + { + }; + + template + class are_integral + : public std::integral_constant::value && are_integral::value> + { + }; +} + +template +class multi_span_index final +{ + static_assert(Rank > 0, "Rank must be greater than 0!"); + + template + friend class multi_span_index; + +public: + static const std::size_t rank = Rank; + using value_type = std::ptrdiff_t; + using size_type = value_type; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_lvalue_reference_t>; + + constexpr multi_span_index() GSL_NOEXCEPT {} + + constexpr multi_span_index(const value_type (&values)[Rank]) GSL_NOEXCEPT + { + std::copy(values, values + Rank, elems); + } + + template ::value>> + constexpr multi_span_index(Ts... ds) GSL_NOEXCEPT : elems{narrow_cast(ds)...} + { + } + + constexpr multi_span_index(const multi_span_index& other) GSL_NOEXCEPT = default; + + constexpr multi_span_index& operator=(const multi_span_index& rhs) GSL_NOEXCEPT = default; + + // Preconditions: component_idx < rank + constexpr reference operator[](std::size_t component_idx) + { + Expects(component_idx < Rank); // Component index must be less than rank + return elems[component_idx]; + } + + // Preconditions: component_idx < rank + constexpr const_reference operator[](std::size_t component_idx) const GSL_NOEXCEPT + { + Expects(component_idx < Rank); // Component index must be less than rank + return elems[component_idx]; + } + + constexpr bool operator==(const multi_span_index& rhs) const GSL_NOEXCEPT + { + return std::equal(elems, elems + rank, rhs.elems); + } + + constexpr bool operator!=(const multi_span_index& rhs) const GSL_NOEXCEPT { return !(*this == rhs); } + + constexpr multi_span_index operator+() const GSL_NOEXCEPT { return *this; } + + constexpr multi_span_index operator-() const GSL_NOEXCEPT + { + multi_span_index ret = *this; + std::transform(ret, ret + rank, ret, std::negate{}); + return ret; + } + + constexpr multi_span_index operator+(const multi_span_index& rhs) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret += rhs; + return ret; + } + + constexpr multi_span_index operator-(const multi_span_index& rhs) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret -= rhs; + return ret; + } + + constexpr multi_span_index& operator+=(const multi_span_index& rhs) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, rhs.elems, elems, std::plus{}); + return *this; + } + + constexpr multi_span_index& operator-=(const multi_span_index& rhs) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, rhs.elems, elems, std::minus{}); + return *this; + } + + constexpr multi_span_index operator*(value_type v) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret *= v; + return ret; + } + + constexpr multi_span_index operator/(value_type v) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret /= v; + return ret; + } + + friend constexpr multi_span_index operator*(value_type v, const multi_span_index& rhs) GSL_NOEXCEPT + { + return rhs * v; + } + + constexpr multi_span_index& operator*=(value_type v) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, elems, + [v](value_type x) { return std::multiplies{}(x, v); }); + return *this; + } + + constexpr multi_span_index& operator/=(value_type v) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, elems, + [v](value_type x) { return std::divides{}(x, v); }); + return *this; + } + +private: + value_type elems[Rank] = {}; +}; + +#if !defined(_MSC_VER) || _MSC_VER >= 1910 + +struct static_bounds_dynamic_range_t +{ + template ::value>> + constexpr operator T() const GSL_NOEXCEPT + { + return narrow_cast(-1); + } +}; + +constexpr bool operator==(static_bounds_dynamic_range_t, static_bounds_dynamic_range_t) GSL_NOEXCEPT +{ + return true; +} + +constexpr bool operator!=(static_bounds_dynamic_range_t, static_bounds_dynamic_range_t) GSL_NOEXCEPT +{ + return false; +} + +template ::value>> +constexpr bool operator==(static_bounds_dynamic_range_t, T other) GSL_NOEXCEPT +{ + return narrow_cast(-1) == other; +} + +template ::value>> +constexpr bool operator==(T left, static_bounds_dynamic_range_t right) GSL_NOEXCEPT +{ + return right == left; +} + +template ::value>> +constexpr bool operator!=(static_bounds_dynamic_range_t, T other) GSL_NOEXCEPT +{ + return narrow_cast(-1) != other; +} + +template ::value>> +constexpr bool operator!=(T left, static_bounds_dynamic_range_t right) GSL_NOEXCEPT +{ + return right != left; +} + +constexpr static_bounds_dynamic_range_t dynamic_range{}; +#else +const std::ptrdiff_t dynamic_range = -1; +#endif + +struct generalized_mapping_tag +{ +}; +struct contiguous_mapping_tag : generalized_mapping_tag +{ +}; + +namespace details +{ + + template + struct LessThan + { + static const bool value = Left < Right; + }; + + template + struct BoundsRanges + { + using size_type = std::ptrdiff_t; + static const size_type Depth = 0; + static const size_type DynamicNum = 0; + static const size_type CurrentRange = 1; + static const size_type TotalSize = 1; + + // TODO : following signature is for work around VS bug + template + BoundsRanges(const OtherRange&, bool /* firstLevel */) + { + } + + BoundsRanges(const std::ptrdiff_t* const) {} + BoundsRanges() = default; + + template + void serialize(T&) const + { + } + + template + size_type linearize(const T&) const + { + return 0; + } + + template + size_type contains(const T&) const + { + return -1; + } + + size_type elementNum(std::size_t) const GSL_NOEXCEPT { return 0; } + + size_type totalSize() const GSL_NOEXCEPT { return TotalSize; } + + bool operator==(const BoundsRanges&) const GSL_NOEXCEPT { return true; } + }; + + template + struct BoundsRanges : BoundsRanges + { + using Base = BoundsRanges; + using size_type = std::ptrdiff_t; + static const std::size_t Depth = Base::Depth + 1; + static const std::size_t DynamicNum = Base::DynamicNum + 1; + static const size_type CurrentRange = dynamic_range; + static const size_type TotalSize = dynamic_range; + + private: + size_type m_bound; + + public: + BoundsRanges(const std::ptrdiff_t* const arr) + : Base(arr + 1), m_bound(*arr * this->Base::totalSize()) + { + Expects(0 <= *arr); + } + + BoundsRanges() : m_bound(0) {} + + template + BoundsRanges(const BoundsRanges& other, + bool /* firstLevel */ = true) + : Base(static_cast&>(other), false) + , m_bound(other.totalSize()) + { + } + + template + void serialize(T& arr) const + { + arr[Dim] = elementNum(); + this->Base::template serialize(arr); + } + + template + size_type linearize(const T& arr) const + { + const size_type index = this->Base::totalSize() * arr[Dim]; + Expects(index < m_bound); + return index + this->Base::template linearize(arr); + } + + template + size_type contains(const T& arr) const + { + const ptrdiff_t last = this->Base::template contains(arr); + if (last == -1) return -1; + const ptrdiff_t cur = this->Base::totalSize() * arr[Dim]; + return cur < m_bound ? cur + last : -1; + } + + size_type totalSize() const GSL_NOEXCEPT { return m_bound; } + + size_type elementNum() const GSL_NOEXCEPT { return totalSize() / this->Base::totalSize(); } + + size_type elementNum(std::size_t dim) const GSL_NOEXCEPT + { + if (dim > 0) + return this->Base::elementNum(dim - 1); + else + return elementNum(); + } + + bool operator==(const BoundsRanges& rhs) const GSL_NOEXCEPT + { + return m_bound == rhs.m_bound && + static_cast(*this) == static_cast(rhs); + } + }; + + template + struct BoundsRanges : BoundsRanges + { + using Base = BoundsRanges; + using size_type = std::ptrdiff_t; + static const std::size_t Depth = Base::Depth + 1; + static const std::size_t DynamicNum = Base::DynamicNum; + static const size_type CurrentRange = CurRange; + static const size_type TotalSize = + Base::TotalSize == dynamic_range ? dynamic_range : CurrentRange * Base::TotalSize; + + BoundsRanges(const std::ptrdiff_t* const arr) : Base(arr) {} + BoundsRanges() = default; + + template + BoundsRanges(const BoundsRanges& other, + bool firstLevel = true) + : Base(static_cast&>(other), false) + { + (void) firstLevel; + } + + template + void serialize(T& arr) const + { + arr[Dim] = elementNum(); + this->Base::template serialize(arr); + } + + template + size_type linearize(const T& arr) const + { + Expects(arr[Dim] >= 0 && arr[Dim] < CurrentRange); // Index is out of range + return this->Base::totalSize() * arr[Dim] + + this->Base::template linearize(arr); + } + + template + size_type contains(const T& arr) const + { + if (arr[Dim] >= CurrentRange) return -1; + const size_type last = this->Base::template contains(arr); + if (last == -1) return -1; + return this->Base::totalSize() * arr[Dim] + last; + } + + size_type totalSize() const GSL_NOEXCEPT { return CurrentRange * this->Base::totalSize(); } + + size_type elementNum() const GSL_NOEXCEPT { return CurrentRange; } + + size_type elementNum(std::size_t dim) const GSL_NOEXCEPT + { + if (dim > 0) + return this->Base::elementNum(dim - 1); + else + return elementNum(); + } + + bool operator==(const BoundsRanges& rhs) const GSL_NOEXCEPT + { + return static_cast(*this) == static_cast(rhs); + } + }; + + template + struct BoundsRangeConvertible + : public std::integral_constant= TargetType::TotalSize || + TargetType::TotalSize == dynamic_range || + SourceType::TotalSize == dynamic_range || + TargetType::TotalSize == 0)> + { + }; + + template + struct TypeListIndexer + { + const TypeChain& obj_; + TypeListIndexer(const TypeChain& obj) : obj_(obj) {} + + template + const TypeChain& getObj(std::true_type) + { + return obj_; + } + + template + auto getObj(std::false_type) + -> decltype(TypeListIndexer(static_cast(obj_)).template get()) + { + return TypeListIndexer(static_cast(obj_)).template get(); + } + + template + auto get() -> decltype(getObj(std::integral_constant())) + { + return getObj(std::integral_constant()); + } + }; + + template + TypeListIndexer createTypeListIndexer(const TypeChain& obj) + { + return TypeListIndexer(obj); + } + + template 1), + typename Ret = std::enable_if_t>> + constexpr Ret shift_left(const multi_span_index& other) GSL_NOEXCEPT + { + Ret ret{}; + for (std::size_t i = 0; i < Rank - 1; ++i) { + ret[i] = other[i + 1]; + } + return ret; + } +} + +template +class bounds_iterator; + +template +class static_bounds +{ +public: + static_bounds(const details::BoundsRanges&) {} +}; + +template +class static_bounds +{ + using MyRanges = details::BoundsRanges; + + MyRanges m_ranges; + constexpr static_bounds(const MyRanges& range) : m_ranges(range) {} + + template + friend class static_bounds; + +public: + static const std::size_t rank = MyRanges::Depth; + static const std::size_t dynamic_rank = MyRanges::DynamicNum; + static const std::ptrdiff_t static_size = MyRanges::TotalSize; + + using size_type = std::ptrdiff_t; + using index_type = multi_span_index; + using const_index_type = std::add_const_t; + using iterator = bounds_iterator; + using const_iterator = bounds_iterator; + using difference_type = std::ptrdiff_t; + using sliced_type = static_bounds; + using mapping_type = contiguous_mapping_tag; + + constexpr static_bounds(const static_bounds&) = default; + + template + struct BoundsRangeConvertible2; + + template > + static auto helpBoundsRangeConvertible(SourceType, TargetType, std::true_type) -> Ret; + + template + static auto helpBoundsRangeConvertible(SourceType, TargetType, ...) -> std::false_type; + + template + struct BoundsRangeConvertible2 + : decltype(helpBoundsRangeConvertible( + SourceType(), TargetType(), + std::integral_constant())) + { + }; + + template + struct BoundsRangeConvertible2 : std::true_type + { + }; + + template + struct BoundsRangeConvertible + : decltype(helpBoundsRangeConvertible( + SourceType(), TargetType(), + std::integral_constant::value || + TargetType::CurrentRange == dynamic_range || + SourceType::CurrentRange == dynamic_range)>())) + { + }; + + template + struct BoundsRangeConvertible : std::true_type + { + }; + + template , + details::BoundsRanges>::value>> + constexpr static_bounds(const static_bounds& other) : m_ranges(other.m_ranges) + { + Expects((MyRanges::DynamicNum == 0 && details::BoundsRanges::DynamicNum == 0) || + MyRanges::DynamicNum > 0 || other.m_ranges.totalSize() >= m_ranges.totalSize()); + } + + constexpr static_bounds(std::initializer_list il) + : m_ranges(il.begin()) + { + // Size of the initializer list must match the rank of the array + Expects((MyRanges::DynamicNum == 0 && il.size() == 1 && *il.begin() == static_size) || + MyRanges::DynamicNum == il.size()); + // Size of the range must be less than the max element of the size type + Expects(m_ranges.totalSize() <= PTRDIFF_MAX); + } + + constexpr static_bounds() = default; + + constexpr sliced_type slice() const GSL_NOEXCEPT + { + return sliced_type{static_cast&>(m_ranges)}; + } + + constexpr size_type stride() const GSL_NOEXCEPT { return rank > 1 ? slice().size() : 1; } + + constexpr size_type size() const GSL_NOEXCEPT { return m_ranges.totalSize(); } + + constexpr size_type total_size() const GSL_NOEXCEPT { return m_ranges.totalSize(); } + + constexpr size_type linearize(const index_type& idx) const { return m_ranges.linearize(idx); } + + constexpr bool contains(const index_type& idx) const GSL_NOEXCEPT + { + return m_ranges.contains(idx) != -1; + } + + constexpr size_type operator[](std::size_t idx) const GSL_NOEXCEPT + { + return m_ranges.elementNum(idx); + } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < rank, + "dimension should be less than rank (dimension count starts from 0)"); + return details::createTypeListIndexer(m_ranges).template get().elementNum(); + } + + template + constexpr size_type extent(IntType dim) const GSL_NOEXCEPT + { + static_assert(std::is_integral::value, + "Dimension parameter must be supplied as an integral type."); + auto real_dim = narrow_cast(dim); + Expects(real_dim < rank); + + return m_ranges.elementNum(real_dim); + } + + constexpr index_type index_bounds() const GSL_NOEXCEPT + { + size_type extents[rank] = {}; + m_ranges.serialize(extents); + return {extents}; + } + + template + constexpr bool operator==(const static_bounds& rhs) const GSL_NOEXCEPT + { + return this->size() == rhs.size(); + } + + template + constexpr bool operator!=(const static_bounds& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + + constexpr const_iterator begin() const GSL_NOEXCEPT + { + return const_iterator(*this, index_type{}); + } + + constexpr const_iterator end() const GSL_NOEXCEPT + { + return const_iterator(*this, this->index_bounds()); + } +}; + +template +class strided_bounds +{ + template + friend class strided_bounds; + +public: + static const std::size_t rank = Rank; + using value_type = std::ptrdiff_t; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_const_t; + using size_type = value_type; + using difference_type = value_type; + using index_type = multi_span_index; + using const_index_type = std::add_const_t; + using iterator = bounds_iterator; + using const_iterator = bounds_iterator; + static const value_type dynamic_rank = rank; + static const value_type static_size = dynamic_range; + using sliced_type = std::conditional_t, void>; + using mapping_type = generalized_mapping_tag; + + constexpr strided_bounds(const strided_bounds&) GSL_NOEXCEPT = default; + + constexpr strided_bounds& operator=(const strided_bounds&) GSL_NOEXCEPT = default; + + constexpr strided_bounds(const value_type (&values)[rank], index_type strides) + : m_extents(values), m_strides(std::move(strides)) + { + } + + constexpr strided_bounds(const index_type& extents, const index_type& strides) GSL_NOEXCEPT + : m_extents(extents), + m_strides(strides) + { + } + + constexpr index_type strides() const GSL_NOEXCEPT { return m_strides; } + + constexpr size_type total_size() const GSL_NOEXCEPT + { + size_type ret = 0; + for (std::size_t i = 0; i < rank; ++i) { + ret += (m_extents[i] - 1) * m_strides[i]; + } + return ret + 1; + } + + constexpr size_type size() const GSL_NOEXCEPT + { + size_type ret = 1; + for (std::size_t i = 0; i < rank; ++i) { + ret *= m_extents[i]; + } + return ret; + } + + constexpr bool contains(const index_type& idx) const GSL_NOEXCEPT + { + for (std::size_t i = 0; i < rank; ++i) { + if (idx[i] < 0 || idx[i] >= m_extents[i]) return false; + } + return true; + } + + constexpr size_type linearize(const index_type& idx) const GSL_NOEXCEPT + { + size_type ret = 0; + for (std::size_t i = 0; i < rank; i++) { + Expects(idx[i] < m_extents[i]); // index is out of bounds of the array + ret += idx[i] * m_strides[i]; + } + return ret; + } + + constexpr size_type stride() const GSL_NOEXCEPT { return m_strides[0]; } + + template 1), typename Ret = std::enable_if_t> + constexpr sliced_type slice() const + { + return {details::shift_left(m_extents), details::shift_left(m_strides)}; + } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "dimension should be less than rank (dimension count starts from 0)"); + return m_extents[Dim]; + } + + constexpr index_type index_bounds() const GSL_NOEXCEPT { return m_extents; } + constexpr const_iterator begin() const GSL_NOEXCEPT + { + return const_iterator{*this, index_type{}}; + } + + constexpr const_iterator end() const GSL_NOEXCEPT + { + return const_iterator{*this, index_bounds()}; + } + +private: + index_type m_extents; + index_type m_strides; +}; + +template +struct is_bounds : std::integral_constant +{ +}; +template +struct is_bounds> : std::integral_constant +{ +}; +template +struct is_bounds> : std::integral_constant +{ +}; + +template +class bounds_iterator +{ +public: + static const std::size_t rank = IndexType::rank; + using iterator_category = std::random_access_iterator_tag; + using value_type = IndexType; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + using index_type = value_type; + using index_size_type = typename IndexType::value_type; + template + explicit bounds_iterator(const Bounds& bnd, value_type curr) GSL_NOEXCEPT + : boundary_(bnd.index_bounds()), + curr_(std::move(curr)) + { + static_assert(is_bounds::value, "Bounds type must be provided"); + } + + constexpr reference operator*() const GSL_NOEXCEPT { return curr_; } + + constexpr pointer operator->() const GSL_NOEXCEPT { return &curr_; } + + constexpr bounds_iterator& operator++() GSL_NOEXCEPT + { + for (std::size_t i = rank; i-- > 0;) { + if (curr_[i] < boundary_[i] - 1) { + curr_[i]++; + return *this; + } + curr_[i] = 0; + } + // If we're here we've wrapped over - set to past-the-end. + curr_ = boundary_; + return *this; + } + + constexpr bounds_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + + constexpr bounds_iterator& operator--() GSL_NOEXCEPT + { + if (!less(curr_, boundary_)) { + // if at the past-the-end, set to last element + for (std::size_t i = 0; i < rank; ++i) { + curr_[i] = boundary_[i] - 1; + } + return *this; + } + for (std::size_t i = rank; i-- > 0;) { + if (curr_[i] >= 1) { + curr_[i]--; + return *this; + } + curr_[i] = boundary_[i] - 1; + } + // If we're here the preconditions were violated + // "pre: there exists s such that r == ++s" + Expects(false); + return *this; + } + + constexpr bounds_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + + constexpr bounds_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + bounds_iterator ret{*this}; + return ret += n; + } + + constexpr bounds_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + auto linear_idx = linearize(curr_) + n; + std::remove_const_t stride = 0; + stride[rank - 1] = 1; + for (std::size_t i = rank - 1; i-- > 0;) { + stride[i] = stride[i + 1] * boundary_[i + 1]; + } + for (std::size_t i = 0; i < rank; ++i) { + curr_[i] = linear_idx / stride[i]; + linear_idx = linear_idx % stride[i]; + } + // index is out of bounds of the array + Expects(!less(curr_, index_type{}) && !less(boundary_, curr_)); + return *this; + } + + constexpr bounds_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + bounds_iterator ret{*this}; + return ret -= n; + } + + constexpr bounds_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + + constexpr difference_type operator-(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return linearize(curr_) - linearize(rhs.curr_); + } + + constexpr value_type operator[](difference_type n) const GSL_NOEXCEPT { return *(*this + n); } + + constexpr bool operator==(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return curr_ == rhs.curr_; + } + + constexpr bool operator!=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + + constexpr bool operator<(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return less(curr_, rhs.curr_); + } + + constexpr bool operator<=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs < *this); + } + + constexpr bool operator>(const bounds_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + + constexpr bool operator>=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs > *this); + } + + void swap(bounds_iterator& rhs) GSL_NOEXCEPT + { + std::swap(boundary_, rhs.boundary_); + std::swap(curr_, rhs.curr_); + } + +private: + constexpr bool less(index_type& one, index_type& other) const GSL_NOEXCEPT + { + for (std::size_t i = 0; i < rank; ++i) { + if (one[i] < other[i]) return true; + } + return false; + } + + constexpr index_size_type linearize(const value_type& idx) const GSL_NOEXCEPT + { + // TODO: Smarter impl. + // Check if past-the-end + index_size_type multiplier = 1; + index_size_type res = 0; + if (!less(idx, boundary_)) { + res = 1; + for (std::size_t i = rank; i-- > 0;) { + res += (idx[i] - 1) * multiplier; + multiplier *= boundary_[i]; + } + } + else + { + for (std::size_t i = rank; i-- > 0;) { + res += idx[i] * multiplier; + multiplier *= boundary_[i]; + } + } + return res; + } + + value_type boundary_; + std::remove_const_t curr_; +}; + +template +bounds_iterator operator+(typename bounds_iterator::difference_type n, + const bounds_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +namespace details +{ + template + constexpr std::enable_if_t< + std::is_same::value, + typename Bounds::index_type> + make_stride(const Bounds& bnd) GSL_NOEXCEPT + { + return bnd.strides(); + } + + // Make a stride vector from bounds, assuming contiguous memory. + template + constexpr std::enable_if_t< + std::is_same::value, + typename Bounds::index_type> + make_stride(const Bounds& bnd) GSL_NOEXCEPT + { + auto extents = bnd.index_bounds(); + typename Bounds::size_type stride[Bounds::rank] = {}; + + stride[Bounds::rank - 1] = 1; + for (std::size_t i = 1; i < Bounds::rank; ++i) { + stride[Bounds::rank - i - 1] = stride[Bounds::rank - i] * extents[Bounds::rank - i]; + } + return {stride}; + } + + template + void verifyBoundsReshape(const BoundsSrc& src, const BoundsDest& dest) + { + static_assert(is_bounds::value && is_bounds::value, + "The src type and dest type must be bounds"); + static_assert(std::is_same::value, + "The source type must be a contiguous bounds"); + static_assert(BoundsDest::static_size == dynamic_range || + BoundsSrc::static_size == dynamic_range || + BoundsDest::static_size == BoundsSrc::static_size, + "The source bounds must have same size as dest bounds"); + Expects(src.size() == dest.size()); + } + +} // namespace details + +template +class contiguous_span_iterator; +template +class general_span_iterator; + +template +struct dim_t +{ + static const std::ptrdiff_t value = DimSize; +}; +template <> +struct dim_t +{ + static const std::ptrdiff_t value = dynamic_range; + const std::ptrdiff_t dvalue; + constexpr dim_t(std::ptrdiff_t size) GSL_NOEXCEPT : dvalue(size) {} +}; + +template = 0)>> +constexpr dim_t dim() GSL_NOEXCEPT +{ + return dim_t(); +} + +template > +constexpr dim_t dim(std::ptrdiff_t n) GSL_NOEXCEPT +{ + return dim_t<>(n); +} + +template +class multi_span; +template +class strided_span; + +namespace details +{ + template + struct SpanTypeTraits + { + using value_type = T; + using size_type = std::size_t; + }; + + template + struct SpanTypeTraits::type> + { + using value_type = typename Traits::span_traits::value_type; + using size_type = typename Traits::span_traits::size_type; + }; + + template + struct SpanArrayTraits + { + using type = multi_span; + using value_type = T; + using bounds_type = static_bounds; + using pointer = T*; + using reference = T&; + }; + template + struct SpanArrayTraits : SpanArrayTraits + { + }; + + template + BoundsType newBoundsHelperImpl(std::ptrdiff_t totalSize, std::true_type) // dynamic size + { + Expects(totalSize >= 0 && totalSize <= PTRDIFF_MAX); + return BoundsType{totalSize}; + } + template + BoundsType newBoundsHelperImpl(std::ptrdiff_t totalSize, std::false_type) // static size + { + Expects(BoundsType::static_size <= totalSize); + return {}; + } + template + BoundsType newBoundsHelper(std::ptrdiff_t totalSize) + { + static_assert(BoundsType::dynamic_rank <= 1, "dynamic rank must less or equal to 1"); + return newBoundsHelperImpl( + totalSize, std::integral_constant()); + } + + struct Sep + { + }; + + template + T static_as_multi_span_helper(Sep, Args... args) + { + return T{narrow_cast(args)...}; + } + template + std::enable_if_t< + !std::is_same>::value && !std::is_same::value, T> + static_as_multi_span_helper(Arg, Args... args) + { + return static_as_multi_span_helper(args...); + } + template + T static_as_multi_span_helper(dim_t val, Args... args) + { + return static_as_multi_span_helper(args..., val.dvalue); + } + + template + struct static_as_multi_span_static_bounds_helper + { + using type = static_bounds<(Dimensions::value)...>; + }; + + template + struct is_multi_span_oracle : std::false_type + { + }; + + template + struct is_multi_span_oracle> + : std::true_type + { + }; + + template + struct is_multi_span_oracle> : std::true_type + { + }; + + template + struct is_multi_span : is_multi_span_oracle> + { + }; +} + +template +class multi_span +{ + // TODO do we still need this? + template + friend class multi_span; + +public: + using bounds_type = static_bounds; + static const std::size_t Rank = bounds_type::rank; + using size_type = typename bounds_type::size_type; + using index_type = typename bounds_type::index_type; + using value_type = ValueType; + using const_value_type = std::add_const_t; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using iterator = contiguous_span_iterator; + using const_span = multi_span; + using const_iterator = contiguous_span_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + using sliced_type = + std::conditional_t>; + +private: + pointer data_; + bounds_type bounds_; + + friend iterator; + friend const_iterator; + +public: + // default constructor - same as constructing from nullptr_t + constexpr multi_span() GSL_NOEXCEPT : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "Default construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + } + + // construct from nullptr - get an empty multi_span + constexpr multi_span(std::nullptr_t) GSL_NOEXCEPT : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "nullptr_t construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + } + + // construct from nullptr with size of 0 (helps with template function calls) + template ::value>> + constexpr multi_span(std::nullptr_t, IntType size) GSL_NOEXCEPT + : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "nullptr_t construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + Expects(size == 0); + } + + // construct from a single element + constexpr multi_span(reference data) GSL_NOEXCEPT : multi_span(&data, bounds_type{1}) + { + static_assert(bounds_type::dynamic_rank > 0 || bounds_type::static_size == 0 || + bounds_type::static_size == 1, + "Construction from a single element only possible " + "for dynamic or fixed spans of length 0 or 1."); + } + + // prevent constructing from temporaries for single-elements + constexpr multi_span(value_type&&) = delete; + + // construct from pointer + length + constexpr multi_span(pointer ptr, size_type size) GSL_NOEXCEPT + : multi_span(ptr, bounds_type{size}) + { + } + + // construct from pointer + length - multidimensional + constexpr multi_span(pointer data, bounds_type bounds) GSL_NOEXCEPT : data_(data), + bounds_(std::move(bounds)) + { + Expects((bounds_.size() > 0 && data != nullptr) || bounds_.size() == 0); + } + + // construct from begin,end pointer pair + template ::value && + details::LessThan::value>> + constexpr multi_span(pointer begin, Ptr end) + : multi_span(begin, + details::newBoundsHelper(static_cast(end) - begin)) + { + Expects(begin != nullptr && end != nullptr && begin <= static_cast(end)); + } + + // construct from n-dimensions static array + template > + constexpr multi_span(T (&arr)[N]) + : multi_span(reinterpret_cast(arr), bounds_type{typename Helper::bounds_type{}}) + { + static_assert(std::is_convertible::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible::value, + "Cannot construct a multi_span from an array with fewer elements."); + } + + // construct from n-dimensions dynamic array (e.g. new int[m][4]) + // (precedence will be lower than the 1-dimension pointer) + template > + constexpr multi_span(T* const& data, size_type size) + : multi_span(reinterpret_cast(data), typename Helper::bounds_type{size}) + { + static_assert(std::is_convertible::value, + "Cannot convert from source type to target multi_span type."); + } + + // construct from std::array + template + constexpr multi_span(std::array& arr) + : multi_span(arr.data(), bounds_type{static_bounds{}}) + { + static_assert( + std::is_convertible(*)[]>::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible, bounds_type>::value, + "You cannot construct a multi_span from a std::array of smaller size."); + } + + // construct from const std::array + template + constexpr multi_span(const std::array& arr) + : multi_span(arr.data(), bounds_type{static_bounds{}}) + { + static_assert(std::is_convertible(*)[]>::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible, bounds_type>::value, + "You cannot construct a multi_span from a std::array of smaller size."); + } + + // prevent constructing from temporary std::array + template + constexpr multi_span(std::array&& arr) = delete; + + // construct from containers + // future: could use contiguous_iterator_traits to identify only contiguous containers + // type-requirements: container must have .size(), operator[] which are value_type compatible + template ::value && + std::is_convertible::value && + std::is_same().size(), + *std::declval().data())>, + DataType>::value>> + constexpr multi_span(Cont& cont) + : multi_span(static_cast(cont.data()), + details::newBoundsHelper(narrow_cast(cont.size()))) + { + } + + // prevent constructing from temporary containers + template ::value && + std::is_convertible::value && + std::is_same().size(), + *std::declval().data())>, + DataType>::value>> + explicit constexpr multi_span(Cont&& cont) = delete; + + // construct from a convertible multi_span + template , + typename = std::enable_if_t::value && + std::is_convertible::value>> + constexpr multi_span(multi_span other) GSL_NOEXCEPT + : data_(other.data_), + bounds_(other.bounds_) + { + } + + // trivial copy and move + constexpr multi_span(const multi_span&) = default; + constexpr multi_span(multi_span&&) = default; + + // trivial assignment + constexpr multi_span& operator=(const multi_span&) = default; + constexpr multi_span& operator=(multi_span&&) = default; + + // first() - extract the first Count elements into a new multi_span + template + constexpr multi_span first() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + Count <= bounds_type::static_size, + "Count is out of bounds."); + + Expects(bounds_type::static_size != dynamic_range || Count <= this->size()); + return {this->data(), Count}; + } + + // first() - extract the first count elements into a new multi_span + constexpr multi_span first(size_type count) const GSL_NOEXCEPT + { + Expects(count >= 0 && count <= this->size()); + return {this->data(), count}; + } + + // last() - extract the last Count elements into a new multi_span + template + constexpr multi_span last() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + Count <= bounds_type::static_size, + "Count is out of bounds."); + + Expects(bounds_type::static_size != dynamic_range || Count <= this->size()); + return {this->data() + this->size() - Count, Count}; + } + + // last() - extract the last count elements into a new multi_span + constexpr multi_span last(size_type count) const GSL_NOEXCEPT + { + Expects(count >= 0 && count <= this->size()); + return {this->data() + this->size() - count, count}; + } + + // subspan() - create a subview of Count elements starting at Offset + template + constexpr multi_span subspan() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(Offset >= 0, "Offset must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + ((Offset <= bounds_type::static_size) && + Count <= bounds_type::static_size - Offset), + "You must describe a sub-range within bounds of the multi_span."); + + Expects(bounds_type::static_size != dynamic_range || + (Offset <= this->size() && Count <= this->size() - Offset)); + return {this->data() + Offset, Count}; + } + + // subspan() - create a subview of count elements starting at offset + // supplying dynamic_range for count will consume all available elements from offset + constexpr multi_span + subspan(size_type offset, size_type count = dynamic_range) const GSL_NOEXCEPT + { + Expects((offset >= 0 && offset <= this->size()) && + (count == dynamic_range || (count <= this->size() - offset))); + return {this->data() + offset, count == dynamic_range ? this->length() - offset : count}; + } + + // section - creates a non-contiguous, strided multi_span from a contiguous one + constexpr strided_span section(index_type origin, + index_type extents) const GSL_NOEXCEPT + { + size_type size = this->bounds().total_size() - this->bounds().linearize(origin); + return {&this->operator[](origin), size, + strided_bounds{extents, details::make_stride(bounds())}}; + } + + // length of the multi_span in elements + constexpr size_type size() const GSL_NOEXCEPT { return bounds_.size(); } + + // length of the multi_span in elements + constexpr size_type length() const GSL_NOEXCEPT { return this->size(); } + + // length of the multi_span in bytes + constexpr size_type size_bytes() const GSL_NOEXCEPT + { + return narrow_cast(sizeof(value_type)) * this->size(); + } + + // length of the multi_span in bytes + constexpr size_type length_bytes() const GSL_NOEXCEPT { return this->size_bytes(); } + + constexpr bool empty() const GSL_NOEXCEPT { return this->size() == 0; } + + static constexpr std::size_t rank() { return Rank; } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "Dimension should be less than rank (dimension count starts from 0)."); + return bounds_.template extent(); + } + + template + constexpr size_type extent(IntType dim) const GSL_NOEXCEPT + { + return bounds_.extent(dim); + } + + constexpr bounds_type bounds() const GSL_NOEXCEPT { return bounds_; } + + constexpr pointer data() const GSL_NOEXCEPT { return data_; } + + template + constexpr reference operator()(FirstIndex idx) + { + return this->operator[](narrow_cast(idx)); + } + + template + constexpr reference operator()(FirstIndex firstIndex, OtherIndices... indices) + { + index_type idx = {narrow_cast(firstIndex), + narrow_cast(indices)...}; + return this->operator[](idx); + } + + constexpr reference operator[](const index_type& idx) const GSL_NOEXCEPT + { + return data_[bounds_.linearize(idx)]; + } + + template 1), typename Ret = std::enable_if_t> + constexpr Ret operator[](size_type idx) const GSL_NOEXCEPT + { + Expects(idx >= 0 && idx < bounds_.size()); // index is out of bounds of the array + const size_type ridx = idx * bounds_.stride(); + + // index is out of bounds of the underlying data + Expects(ridx < bounds_.total_size()); + return Ret{data_ + ridx, bounds_.slice()}; + } + + constexpr iterator begin() const GSL_NOEXCEPT { return iterator{this, true}; } + + constexpr iterator end() const GSL_NOEXCEPT { return iterator{this, false}; } + + constexpr const_iterator cbegin() const GSL_NOEXCEPT + { + return const_iterator{reinterpret_cast(this), true}; + } + + constexpr const_iterator cend() const GSL_NOEXCEPT + { + return const_iterator{reinterpret_cast(this), false}; + } + + constexpr reverse_iterator rbegin() const GSL_NOEXCEPT { return reverse_iterator{end()}; } + + constexpr reverse_iterator rend() const GSL_NOEXCEPT { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const GSL_NOEXCEPT + { + return const_reverse_iterator{cend()}; + } + + constexpr const_reverse_iterator crend() const GSL_NOEXCEPT + { + return const_reverse_iterator{cbegin()}; + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator==(const multi_span& other) const GSL_NOEXCEPT + { + return bounds_.size() == other.bounds_.size() && + (data_ == other.data_ || std::equal(this->begin(), this->end(), other.begin())); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator!=(const multi_span& other) const GSL_NOEXCEPT + { + return !(*this == other); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<(const multi_span& other) const GSL_NOEXCEPT + { + return std::lexicographical_compare(this->begin(), this->end(), other.begin(), other.end()); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<=(const multi_span& other) const GSL_NOEXCEPT + { + return !(other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>(const multi_span& other) const GSL_NOEXCEPT + { + return (other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>=(const multi_span& other) const GSL_NOEXCEPT + { + return !(*this < other); + } +}; + +// +// Free functions for manipulating spans +// + +// reshape a multi_span into a different dimensionality +// DimCount and Enabled here are workarounds for a bug in MSVC 2015 +template 0), typename = std::enable_if_t> +constexpr auto as_multi_span(SpanType s, Dimensions2... dims) + -> multi_span +{ + static_assert(details::is_multi_span::value, + "Variadic as_multi_span() is for reshaping existing spans."); + using BoundsType = + typename multi_span::bounds_type; + auto tobounds = details::static_as_multi_span_helper(dims..., details::Sep{}); + details::verifyBoundsReshape(s.bounds(), tobounds); + return {s.data(), tobounds}; +} + +// convert a multi_span to a multi_span +template +multi_span as_bytes(multi_span s) GSL_NOEXCEPT +{ + static_assert(std::is_trivial>::value, + "The value_type of multi_span must be a trivial type."); + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// convert a multi_span to a multi_span (a writeable byte multi_span) +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +multi_span as_writeable_bytes(multi_span s) GSL_NOEXCEPT +{ + static_assert(std::is_trivial>::value, + "The value_type of multi_span must be a trivial type."); + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// convert a multi_span to a multi_span +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +constexpr auto +as_multi_span(multi_span s) GSL_NOEXCEPT -> multi_span< + const U, static_cast( + multi_span::bounds_type::static_size != dynamic_range + ? (static_cast( + multi_span::bounds_type::static_size) / + sizeof(U)) + : dynamic_range)> +{ + using ConstByteSpan = multi_span; + static_assert( + std::is_trivial>::value && + (ConstByteSpan::bounds_type::static_size == dynamic_range || + ConstByteSpan::bounds_type::static_size % narrow_cast(sizeof(U)) == 0), + "Target type must be a trivial type and its size must match the byte array size"); + + Expects((s.size_bytes() % narrow_cast(sizeof(U))) == 0 && + (s.size_bytes() / narrow_cast(sizeof(U))) < PTRDIFF_MAX); + return {reinterpret_cast(s.data()), + s.size_bytes() / narrow_cast(sizeof(U))}; +} + +// convert a multi_span to a multi_span +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +constexpr auto as_multi_span(multi_span s) GSL_NOEXCEPT + -> multi_span( + multi_span::bounds_type::static_size != dynamic_range + ? static_cast( + multi_span::bounds_type::static_size) / + sizeof(U) + : dynamic_range)> +{ + using ByteSpan = multi_span; + static_assert( + std::is_trivial>::value && + (ByteSpan::bounds_type::static_size == dynamic_range || + ByteSpan::bounds_type::static_size % sizeof(U) == 0), + "Target type must be a trivial type and its size must match the byte array size"); + + Expects((s.size_bytes() % sizeof(U)) == 0); + return {reinterpret_cast(s.data()), + s.size_bytes() / narrow_cast(sizeof(U))}; +} + +template +constexpr auto as_multi_span(T* const& ptr, dim_t... args) + -> multi_span, Dimensions...> +{ + return {reinterpret_cast*>(ptr), + details::static_as_multi_span_helper>(args..., + details::Sep{})}; +} + +template +constexpr auto as_multi_span(T* arr, std::ptrdiff_t len) -> + typename details::SpanArrayTraits::type +{ + return {reinterpret_cast*>(arr), len}; +} + +template +constexpr auto as_multi_span(T (&arr)[N]) -> typename details::SpanArrayTraits::type +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(const std::array& arr) +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(const std::array&&) = delete; + +template +constexpr multi_span as_multi_span(std::array& arr) +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(T* begin, T* end) +{ + return {begin, end}; +} + +template +constexpr auto as_multi_span(Cont& arr) -> std::enable_if_t< + !details::is_multi_span>::value, + multi_span, dynamic_range>> +{ + Expects(arr.size() < PTRDIFF_MAX); + return {arr.data(), narrow_cast(arr.size())}; +} + +template +constexpr auto as_multi_span(Cont&& arr) -> std::enable_if_t< + !details::is_multi_span>::value, + multi_span, dynamic_range>> = delete; + +// from basic_string which doesn't have nonconst .data() member like other contiguous containers +template +constexpr auto as_multi_span(std::basic_string& str) + -> multi_span +{ + Expects(str.size() < PTRDIFF_MAX); + return {&str[0], narrow_cast(str.size())}; +} + +// strided_span is an extension that is not strictly part of the GSL at this time. +// It is kept here while the multidimensional interface is still being defined. +template +class strided_span +{ +public: + using bounds_type = strided_bounds; + using size_type = typename bounds_type::size_type; + using index_type = typename bounds_type::index_type; + using value_type = ValueType; + using const_value_type = std::add_const_t; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using iterator = general_span_iterator; + using const_strided_span = strided_span; + using const_iterator = general_span_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + using sliced_type = + std::conditional_t>; + +private: + pointer data_; + bounds_type bounds_; + + friend iterator; + friend const_iterator; + template + friend class strided_span; + +public: + // from raw data + constexpr strided_span(pointer ptr, size_type size, bounds_type bounds) + : data_(ptr), bounds_(std::move(bounds)) + { + Expects((bounds_.size() > 0 && ptr != nullptr) || bounds_.size() == 0); + // Bounds cross data boundaries + Expects(this->bounds().total_size() <= size); + (void) size; + } + + // from static array of size N + template + constexpr strided_span(value_type (&values)[N], bounds_type bounds) + : strided_span(values, N, std::move(bounds)) + { + } + + // from array view + template ::value, + typename = std::enable_if_t> + constexpr strided_span(multi_span av, bounds_type bounds) + : strided_span(av.data(), av.bounds().total_size(), std::move(bounds)) + { + } + + // convertible + template ::value>> + constexpr strided_span(const strided_span& other) + : data_(other.data_), bounds_(other.bounds_) + { + } + + // convert from bytes + template + constexpr strided_span< + typename std::enable_if::value, OtherValueType>::type, + Rank> + as_strided_span() const + { + static_assert((sizeof(OtherValueType) >= sizeof(value_type)) && + (sizeof(OtherValueType) % sizeof(value_type) == 0), + "OtherValueType should have a size to contain a multiple of ValueTypes"); + auto d = narrow_cast(sizeof(OtherValueType) / sizeof(value_type)); + + size_type size = this->bounds().total_size() / d; + return {const_cast(reinterpret_cast(this->data())), + size, + bounds_type{resize_extent(this->bounds().index_bounds(), d), + resize_stride(this->bounds().strides(), d)}}; + } + + constexpr strided_span section(index_type origin, index_type extents) const + { + size_type size = this->bounds().total_size() - this->bounds().linearize(origin); + return {&this->operator[](origin), size, + bounds_type{extents, details::make_stride(bounds())}}; + } + + constexpr reference operator[](const index_type& idx) const + { + return data_[bounds_.linearize(idx)]; + } + + template 1), typename Ret = std::enable_if_t> + constexpr Ret operator[](size_type idx) const + { + Expects(idx < bounds_.size()); // index is out of bounds of the array + const size_type ridx = idx * bounds_.stride(); + + // index is out of bounds of the underlying data + Expects(ridx < bounds_.total_size()); + return {data_ + ridx, bounds_.slice().total_size(), bounds_.slice()}; + } + + constexpr bounds_type bounds() const GSL_NOEXCEPT { return bounds_; } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "dimension should be less than Rank (dimension count starts from 0)"); + return bounds_.template extent(); + } + + constexpr size_type size() const GSL_NOEXCEPT { return bounds_.size(); } + + constexpr pointer data() const GSL_NOEXCEPT { return data_; } + + constexpr explicit operator bool() const GSL_NOEXCEPT { return data_ != nullptr; } + + constexpr iterator begin() const { return iterator{this, true}; } + + constexpr iterator end() const { return iterator{this, false}; } + + constexpr const_iterator cbegin() const + { + return const_iterator{reinterpret_cast(this), true}; + } + + constexpr const_iterator cend() const + { + return const_iterator{reinterpret_cast(this), false}; + } + + constexpr reverse_iterator rbegin() const { return reverse_iterator{end()}; } + + constexpr reverse_iterator rend() const { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{cend()}; } + + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{cbegin()}; } + + template , std::remove_cv_t>::value>> + constexpr bool + operator==(const strided_span& other) const GSL_NOEXCEPT + { + return bounds_.size() == other.bounds_.size() && + (data_ == other.data_ || std::equal(this->begin(), this->end(), other.begin())); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator!=(const strided_span& other) const GSL_NOEXCEPT + { + return !(*this == other); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<(const strided_span& other) const GSL_NOEXCEPT + { + return std::lexicographical_compare(this->begin(), this->end(), other.begin(), other.end()); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<=(const strided_span& other) const GSL_NOEXCEPT + { + return !(other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>(const strided_span& other) const GSL_NOEXCEPT + { + return (other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>=(const strided_span& other) const GSL_NOEXCEPT + { + return !(*this < other); + } + +private: + static index_type resize_extent(const index_type& extent, std::ptrdiff_t d) + { + // The last dimension of the array needs to contain a multiple of new type elements + Expects(extent[Rank - 1] >= d && (extent[Rank - 1] % d == 0)); + + index_type ret = extent; + ret[Rank - 1] /= d; + + return ret; + } + + template > + static index_type resize_stride(const index_type& strides, std::ptrdiff_t, void* = nullptr) + { + // Only strided arrays with regular strides can be resized + Expects(strides[Rank - 1] == 1); + + return strides; + } + + template 1), typename = std::enable_if_t> + static index_type resize_stride(const index_type& strides, std::ptrdiff_t d) + { + // Only strided arrays with regular strides can be resized + Expects(strides[Rank - 1] == 1); + // The strides must have contiguous chunks of + // memory that can contain a multiple of new type elements + Expects(strides[Rank - 2] >= d && (strides[Rank - 2] % d == 0)); + + for (std::size_t i = Rank - 1; i > 0; --i) { + // Only strided arrays with regular strides can be resized + Expects((strides[i - 1] >= strides[i]) && (strides[i - 1] % strides[i] == 0)); + } + + index_type ret = strides / d; + ret[Rank - 1] = 1; + + return ret; + } +}; + +template +class contiguous_span_iterator +{ +public: + using iterator_category = std::random_access_iterator_tag; + using value_type = typename Span::value_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + +private: + template + friend class multi_span; + + pointer data_; + const Span* m_validator; + void validateThis() const + { + // iterator is out of range of the array + Expects(data_ >= m_validator->data_ && data_ < m_validator->data_ + m_validator->size()); + } + contiguous_span_iterator(const Span* container, bool isbegin) + : data_(isbegin ? container->data_ : container->data_ + container->size()) + , m_validator(container) + { + } + +public: + reference operator*() const GSL_NOEXCEPT + { + validateThis(); + return *data_; + } + pointer operator->() const GSL_NOEXCEPT + { + validateThis(); + return data_; + } + contiguous_span_iterator& operator++() GSL_NOEXCEPT + { + ++data_; + return *this; + } + contiguous_span_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + contiguous_span_iterator& operator--() GSL_NOEXCEPT + { + --data_; + return *this; + } + contiguous_span_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + contiguous_span_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + contiguous_span_iterator ret{*this}; + return ret += n; + } + contiguous_span_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + data_ += n; + return *this; + } + contiguous_span_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + contiguous_span_iterator ret{*this}; + return ret -= n; + } + contiguous_span_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + difference_type operator-(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ - rhs.data_; + } + reference operator[](difference_type n) const GSL_NOEXCEPT { return *(*this + n); } + bool operator==(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ == rhs.data_; + } + bool operator!=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + bool operator<(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ < rhs.data_; + } + bool operator<=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs < *this); + } + bool operator>(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + bool operator>=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs > *this); + } + void swap(contiguous_span_iterator& rhs) GSL_NOEXCEPT + { + std::swap(data_, rhs.data_); + std::swap(m_validator, rhs.m_validator); + } +}; + +template +contiguous_span_iterator operator+(typename contiguous_span_iterator::difference_type n, + const contiguous_span_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +template +class general_span_iterator +{ +public: + using iterator_category = std::random_access_iterator_tag; + using value_type = typename Span::value_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + +private: + template + friend class strided_span; + + const Span* m_container; + typename Span::bounds_type::iterator m_itr; + general_span_iterator(const Span* container, bool isbegin) + : m_container(container) + , m_itr(isbegin ? m_container->bounds().begin() : m_container->bounds().end()) + { + } + +public: + reference operator*() GSL_NOEXCEPT { return (*m_container)[*m_itr]; } + pointer operator->() GSL_NOEXCEPT { return &(*m_container)[*m_itr]; } + general_span_iterator& operator++() GSL_NOEXCEPT + { + ++m_itr; + return *this; + } + general_span_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + general_span_iterator& operator--() GSL_NOEXCEPT + { + --m_itr; + return *this; + } + general_span_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + general_span_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + general_span_iterator ret{*this}; + return ret += n; + } + general_span_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + m_itr += n; + return *this; + } + general_span_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + general_span_iterator ret{*this}; + return ret -= n; + } + general_span_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + difference_type operator-(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr - rhs.m_itr; + } + value_type operator[](difference_type n) const GSL_NOEXCEPT { return (*m_container)[m_itr[n]]; } + + bool operator==(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr == rhs.m_itr; + } + bool operator!=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(*this == rhs); } + bool operator<(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr < rhs.m_itr; + } + bool operator<=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(rhs < *this); } + bool operator>(const general_span_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + bool operator>=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(rhs > *this); } + void swap(general_span_iterator& rhs) GSL_NOEXCEPT + { + std::swap(m_itr, rhs.m_itr); + std::swap(m_container, rhs.m_container); + } +}; + +template +general_span_iterator operator+(typename general_span_iterator::difference_type n, + const general_span_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +} // namespace gsl + +#undef GSL_NOEXCEPT + +#ifdef _MSC_VER +#if _MSC_VER < 1910 + +#undef constexpr +#pragma pop_macro("constexpr") +#endif // _MSC_VER < 1910 + +#pragma warning(pop) + +#endif // _MSC_VER + +#if __GNUC__ > 6 +#pragma GCC diagnostic pop +#endif // __GNUC__ > 6 + +#endif // GSL_MULTI_SPAN_H diff --git a/extern/include/gsl/pointers b/extern/include/gsl/pointers new file mode 100644 index 000000000..69499d6fe --- /dev/null +++ b/extern/include/gsl/pointers @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_POINTERS_H +#define GSL_POINTERS_H + +#include // for Ensures, Expects + +#include // for forward +#include // for ptrdiff_t, nullptr_t, ostream, size_t +#include // for shared_ptr, unique_ptr +#include // for hash +#include // for enable_if_t, is_convertible, is_assignable + +#if defined(_MSC_VER) && _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // defined(_MSC_VER) && _MSC_VER < 1910 + +namespace gsl +{ + +// +// GSL.owner: ownership pointers +// +using std::unique_ptr; +using std::shared_ptr; + +// +// owner +// +// owner is designed as a bridge for code that must deal directly with owning pointers for some reason +// +// T must be a pointer type +// - disallow construction from any type other than pointer type +// +template ::value>> +using owner = T; + +// +// not_null +// +// Restricts a pointer or smart pointer to only hold non-null values. +// +// Has zero size overhead over T. +// +// If T is a pointer (i.e. T == U*) then +// - allow construction from U* +// - disallow construction from nullptr_t +// - disallow default construction +// - ensure construction from null U* fails +// - allow implicit conversion to U* +// +template +class not_null +{ +public: + static_assert(std::is_assignable::value, "T cannot be assigned nullptr."); + + template ::value>> + constexpr explicit not_null(U&& u) : ptr_(std::forward(u)) + { + Expects(ptr_ != nullptr); + } + + template ::value>> + constexpr explicit not_null(T u) : ptr_(u) + { + Expects(ptr_ != nullptr); + } + + template ::value>> + constexpr not_null(const not_null& other) : not_null(other.get()) + { + } + + not_null(not_null&& other) = default; + not_null(const not_null& other) = default; + not_null& operator=(const not_null& other) = default; + + constexpr T get() const + { + Ensures(ptr_ != nullptr); + return ptr_; + } + + constexpr operator T() const { return get(); } + constexpr T operator->() const { return get(); } + constexpr decltype(auto) operator*() const { return *get(); } + + // prevents compilation when someone attempts to assign a null pointer constant + not_null(std::nullptr_t) = delete; + not_null& operator=(std::nullptr_t) = delete; + + // unwanted operators...pointers only point to single objects! + not_null& operator++() = delete; + not_null& operator--() = delete; + not_null operator++(int) = delete; + not_null operator--(int) = delete; + not_null& operator+=(std::ptrdiff_t) = delete; + not_null& operator-=(std::ptrdiff_t) = delete; + void operator[](std::ptrdiff_t) const = delete; + +private: + T ptr_; +}; + +template +std::ostream& operator<<(std::ostream& os, const not_null& val) +{ + os << val.get(); + return os; +} + +template +auto operator==(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() == rhs.get()) +{ + return lhs.get() == rhs.get(); +} + +template +auto operator!=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() != rhs.get()) +{ + return lhs.get() != rhs.get(); +} + +template +auto operator<(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() < rhs.get()) +{ + return lhs.get() < rhs.get(); +} + +template +auto operator<=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() <= rhs.get()) +{ + return lhs.get() <= rhs.get(); +} + +template +auto operator>(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() > rhs.get()) +{ + return lhs.get() > rhs.get(); +} + +template +auto operator>=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() >= rhs.get()) +{ + return lhs.get() >= rhs.get(); +} + +// more unwanted operators +template +std::ptrdiff_t operator-(const not_null&, const not_null&) = delete; +template +not_null operator-(const not_null&, std::ptrdiff_t) = delete; +template +not_null operator+(const not_null&, std::ptrdiff_t) = delete; +template +not_null operator+(std::ptrdiff_t, const not_null&) = delete; + +} // namespace gsl + +namespace std +{ +template +struct hash> +{ + std::size_t operator()(const gsl::not_null& value) const { return hash{}(value); } +}; + +} // namespace std + +#if defined(_MSC_VER) && _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // defined(_MSC_VER) && _MSC_VER < 1910 + +#endif // GSL_POINTERS_H diff --git a/extern/include/gsl/span b/extern/include/gsl/span new file mode 100644 index 000000000..2fa9cc556 --- /dev/null +++ b/extern/include/gsl/span @@ -0,0 +1,766 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_SPAN_H +#define GSL_SPAN_H + +#include // for Expects +#include // for byte +#include // for narrow_cast, narrow + +#include // for lexicographical_compare +#include // for array +#include // for ptrdiff_t, size_t, nullptr_t +#include // for reverse_iterator, distance, random_access_... +#include +#include +#include // for enable_if_t, declval, is_convertible, inte... +#include + +#ifdef _MSC_VER +#pragma warning(push) + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4702) // unreachable code + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ +#define GSL_USE_STATIC_CONSTEXPR_WORKAROUND + +#endif // _MSC_VER < 1910 +#else // _MSC_VER + +// See if we have enough C++17 power to use a static constexpr data member +// without needing an out-of-line definition +#if !(defined(__cplusplus) && (__cplusplus >= 201703L)) +#define GSL_USE_STATIC_CONSTEXPR_WORKAROUND +#endif // !(defined(__cplusplus) && (__cplusplus >= 201703L)) + +#endif // _MSC_VER + +// GCC 7 does not like the signed unsigned missmatch (size_t ptrdiff_t) +// While there is a conversion from signed to unsigned, it happens at +// compiletime, so the compiler wouldn't have to warn indiscriminently, but +// could check if the source value actually doesn't fit into the target type +// and only warn in those cases. +#if __GNUC__ > 6 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +namespace gsl +{ + +// [views.constants], constants +constexpr const std::ptrdiff_t dynamic_extent = -1; + +template +class span; + +// implementation details +namespace details +{ + template + struct is_span_oracle : std::false_type + { + }; + + template + struct is_span_oracle> : std::true_type + { + }; + + template + struct is_span : public is_span_oracle> + { + }; + + template + struct is_std_array_oracle : std::false_type + { + }; + + template + struct is_std_array_oracle> : std::true_type + { + }; + + template + struct is_std_array : public is_std_array_oracle> + { + }; + + template + struct is_allowed_extent_conversion + : public std::integral_constant + { + }; + + template + struct is_allowed_element_type_conversion + : public std::integral_constant::value> + { + }; + + template + class span_iterator + { + using element_type_ = typename Span::element_type; + + public: + +#ifdef _MSC_VER + // Tell Microsoft standard library that span_iterators are checked. + using _Unchecked_type = typename Span::pointer; +#endif + + using iterator_category = std::random_access_iterator_tag; + using value_type = std::remove_cv_t; + using difference_type = typename Span::index_type; + + using reference = std::conditional_t&; + using pointer = std::add_pointer_t; + + span_iterator() = default; + + constexpr span_iterator(const Span* span, typename Span::index_type idx) noexcept + : span_(span), index_(idx) + {} + + friend span_iterator; + template* = nullptr> + constexpr span_iterator(const span_iterator& other) noexcept + : span_iterator(other.span_, other.index_) + { + } + + constexpr reference operator*() const + { + Expects(index_ != span_->size()); + return *(span_->data() + index_); + } + + constexpr pointer operator->() const + { + Expects(index_ != span_->size()); + return span_->data() + index_; + } + + constexpr span_iterator& operator++() + { + Expects(0 <= index_ && index_ != span_->size()); + ++index_; + return *this; + } + + constexpr span_iterator operator++(int) + { + auto ret = *this; + ++(*this); + return ret; + } + + constexpr span_iterator& operator--() + { + Expects(index_ != 0 && index_ <= span_->size()); + --index_; + return *this; + } + + constexpr span_iterator operator--(int) + { + auto ret = *this; + --(*this); + return ret; + } + + constexpr span_iterator operator+(difference_type n) const + { + auto ret = *this; + return ret += n; + } + + friend constexpr span_iterator operator+(difference_type n, span_iterator const& rhs) + { + return rhs + n; + } + + constexpr span_iterator& operator+=(difference_type n) + { + Expects((index_ + n) >= 0 && (index_ + n) <= span_->size()); + index_ += n; + return *this; + } + + constexpr span_iterator operator-(difference_type n) const + { + auto ret = *this; + return ret -= n; + } + + constexpr span_iterator& operator-=(difference_type n) { return *this += -n; } + + constexpr difference_type operator-(span_iterator rhs) const + { + Expects(span_ == rhs.span_); + return index_ - rhs.index_; + } + + constexpr reference operator[](difference_type n) const + { + return *(*this + n); + } + + constexpr friend bool operator==(span_iterator lhs, + span_iterator rhs) noexcept + { + return lhs.span_ == rhs.span_ && lhs.index_ == rhs.index_; + } + + constexpr friend bool operator!=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(lhs == rhs); + } + + constexpr friend bool operator<(span_iterator lhs, + span_iterator rhs) noexcept + { + return lhs.index_ < rhs.index_; + } + + constexpr friend bool operator<=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(rhs < lhs); + } + + constexpr friend bool operator>(span_iterator lhs, + span_iterator rhs) noexcept + { + return rhs < lhs; + } + + constexpr friend bool operator>=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(rhs > lhs); + } + +#ifdef _MSC_VER + // MSVC++ iterator debugging support; allows STL algorithms in 15.8+ + // to unwrap span_iterator to a pointer type after a range check in STL + // algorithm calls + friend constexpr void _Verify_range(span_iterator lhs, + span_iterator rhs) noexcept + { // test that [lhs, rhs) forms a valid range inside an STL algorithm + Expects(lhs.span_ == rhs.span_ // range spans have to match + && lhs.index_ <= rhs.index_); // range must not be transposed + } + + constexpr void _Verify_offset(const difference_type n) const noexcept + { // test that the iterator *this + n is a valid range in an STL + // algorithm call + Expects((index_ + n) >= 0 && (index_ + n) <= span_->size()); + } + + constexpr pointer _Unwrapped() const noexcept + { // after seeking *this to a high water mark, or using one of the + // _Verify_xxx functions above, unwrap this span_iterator to a raw + // pointer + return span_->data() + index_; + } + + // Tell the STL that span_iterator should not be unwrapped if it can't + // validate in advance, even in release / optimized builds: +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const bool _Unwrap_when_unverified = false; +#else + static constexpr bool _Unwrap_when_unverified = false; +#endif + constexpr void _Seek_to(const pointer p) noexcept + { // adjust the position of *this to previously verified location p + // after _Unwrapped + index_ = p - span_->data(); + } +#endif + + protected: + const Span* span_ = nullptr; + std::ptrdiff_t index_ = 0; + }; + + template + class extent_type + { + public: + using index_type = std::ptrdiff_t; + + static_assert(Ext >= 0, "A fixed-size span must be >= 0 in size."); + + constexpr extent_type() noexcept {} + + template + constexpr extent_type(extent_type ext) + { + static_assert(Other == Ext || Other == dynamic_extent, + "Mismatch between fixed-size extent and size of initializing data."); + Expects(ext.size() == Ext); + } + + constexpr extent_type(index_type size) { Expects(size == Ext); } + + constexpr index_type size() const noexcept { return Ext; } + }; + + template <> + class extent_type + { + public: + using index_type = std::ptrdiff_t; + + template + explicit constexpr extent_type(extent_type ext) : size_(ext.size()) + { + } + + explicit constexpr extent_type(index_type size) : size_(size) { Expects(size >= 0); } + + constexpr index_type size() const noexcept { return size_; } + + private: + index_type size_; + }; + + template + struct calculate_subspan_type + { + using type = span; + }; +} // namespace details + +// [span], class template span +template +class span +{ +public: + // constants and types + using element_type = ElementType; + using value_type = std::remove_cv_t; + using index_type = std::ptrdiff_t; + using pointer = element_type*; + using reference = element_type&; + + using iterator = details::span_iterator, false>; + using const_iterator = details::span_iterator, true>; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + using size_type = index_type; + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const index_type extent { Extent }; +#else + static constexpr index_type extent { Extent }; +#endif + + // [span.cons], span constructors, copy, assignment, and destructor + template " SFINAE, + // since "std::enable_if_t" is ill-formed when Extent is greater than 0. + class = std::enable_if_t<(Dependent || Extent <= 0)>> + constexpr span() noexcept : storage_(nullptr, details::extent_type<0>()) + { + } + + constexpr span(pointer ptr, index_type count) : storage_(ptr, count) {} + + constexpr span(pointer firstElem, pointer lastElem) + : storage_(firstElem, std::distance(firstElem, lastElem)) + { + } + + template + constexpr span(element_type (&arr)[N]) noexcept + : storage_(KnownNotNull{&arr[0]}, details::extent_type()) + { + } + + template > + constexpr span(std::array& arr) noexcept + : storage_(&arr[0], details::extent_type()) + { + } + + template + constexpr span(const std::array, N>& arr) noexcept + : storage_(&arr[0], details::extent_type()) + { + } + + // NB: the SFINAE here uses .data() as a incomplete/imperfect proxy for the requirement + // on Container to be a contiguous sequence container. + template ::value && !details::is_std_array::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr span(Container& cont) : span(cont.data(), narrow(cont.size())) + { + } + + template ::value && !details::is_span::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr span(const Container& cont) : span(cont.data(), narrow(cont.size())) + { + } + + constexpr span(const span& other) noexcept = default; + + template < + class OtherElementType, std::ptrdiff_t OtherExtent, + class = std::enable_if_t< + details::is_allowed_extent_conversion::value && + details::is_allowed_element_type_conversion::value>> + constexpr span(const span& other) + : storage_(other.data(), details::extent_type(other.size())) + { + } + + ~span() noexcept = default; + constexpr span& operator=(const span& other) noexcept = default; + + // [span.sub], span subviews + template + constexpr span first() const + { + Expects(Count >= 0 && Count <= size()); + return {data(), Count}; + } + + template + constexpr span last() const + { + Expects(Count >= 0 && size() - Count >= 0); + return {data() + (size() - Count), Count}; + } + + template + constexpr auto subspan() const -> typename details::calculate_subspan_type::type + { + Expects((Offset >= 0 && size() - Offset >= 0) && + (Count == dynamic_extent || (Count >= 0 && Offset + Count <= size()))); + + return {data() + Offset, Count == dynamic_extent ? size() - Offset : Count}; + } + + constexpr span first(index_type count) const + { + Expects(count >= 0 && count <= size()); + return {data(), count}; + } + + constexpr span last(index_type count) const + { + return make_subspan(size() - count, dynamic_extent, subspan_selector{}); + } + + constexpr span subspan(index_type offset, + index_type count = dynamic_extent) const + { + return make_subspan(offset, count, subspan_selector{}); + } + + + // [span.obs], span observers + constexpr index_type size() const noexcept { return storage_.size(); } + constexpr index_type size_bytes() const noexcept + { + return size() * narrow_cast(sizeof(element_type)); + } + constexpr bool empty() const noexcept { return size() == 0; } + + // [span.elem], span element access + constexpr reference operator[](index_type idx) const + { + Expects(idx >= 0 && idx < storage_.size()); + return data()[idx]; + } + + constexpr reference at(index_type idx) const { return this->operator[](idx); } + constexpr reference operator()(index_type idx) const { return this->operator[](idx); } + constexpr pointer data() const noexcept { return storage_.data(); } + + // [span.iter], span iterator support + constexpr iterator begin() const noexcept { return {this, 0}; } + constexpr iterator end() const noexcept { return {this, size()}; } + + constexpr const_iterator cbegin() const noexcept { return {this, 0}; } + constexpr const_iterator cend() const noexcept { return {this, size()}; } + + constexpr reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; } + constexpr reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator{cend()}; } + constexpr const_reverse_iterator crend() const noexcept { return const_reverse_iterator{cbegin()}; } + +#ifdef _MSC_VER + // Tell MSVC how to unwrap spans in range-based-for + constexpr pointer _Unchecked_begin() const noexcept { return data(); } + constexpr pointer _Unchecked_end() const noexcept { return data() + size(); } +#endif // _MSC_VER + +private: + + // Needed to remove unnecessary null check in subspans + struct KnownNotNull + { + pointer p; + }; + + // this implementation detail class lets us take advantage of the + // empty base class optimization to pay for only storage of a single + // pointer in the case of fixed-size spans + template + class storage_type : public ExtentType + { + public: + // KnownNotNull parameter is needed to remove unnecessary null check + // in subspans and constructors from arrays + template + constexpr storage_type(KnownNotNull data, OtherExtentType ext) : ExtentType(ext), data_(data.p) + { + Expects(ExtentType::size() >= 0); + } + + + template + constexpr storage_type(pointer data, OtherExtentType ext) : ExtentType(ext), data_(data) + { + Expects(ExtentType::size() >= 0); + Expects(data || ExtentType::size() == 0); + } + + constexpr pointer data() const noexcept { return data_; } + + private: + pointer data_; + }; + + storage_type> storage_; + + // The rest is needed to remove unnecessary null check + // in subspans and constructors from arrays + constexpr span(KnownNotNull ptr, index_type count) : storage_(ptr, count) {} + + template + class subspan_selector {}; + + template + span make_subspan(index_type offset, + index_type count, + subspan_selector) const + { + span tmp(*this); + return tmp.subspan(offset, count); + } + + span make_subspan(index_type offset, + index_type count, + subspan_selector) const + { + Expects(offset >= 0 && size() - offset >= 0); + if (count == dynamic_extent) + { + return { KnownNotNull{ data() + offset }, size() - offset }; + } + + Expects(count >= 0 && size() - offset >= count); + return { KnownNotNull{ data() + offset }, count }; + } +}; + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) +template +constexpr const typename span::index_type span::extent; +#endif + + +// [span.comparison], span comparison operators +template +constexpr bool operator==(span l, + span r) +{ + return std::equal(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator!=(span l, + span r) +{ + return !(l == r); +} + +template +constexpr bool operator<(span l, + span r) +{ + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator<=(span l, + span r) +{ + return !(l > r); +} + +template +constexpr bool operator>(span l, + span r) +{ + return r < l; +} + +template +constexpr bool operator>=(span l, + span r) +{ + return !(l < r); +} + +namespace details +{ + // if we only supported compilers with good constexpr support then + // this pair of classes could collapse down to a constexpr function + + // we should use a narrow_cast<> to go to std::size_t, but older compilers may not see it as + // constexpr + // and so will fail compilation of the template + template + struct calculate_byte_size + : std::integral_constant(sizeof(ElementType) * + static_cast(Extent))> + { + }; + + template + struct calculate_byte_size + : std::integral_constant + { + }; +} + +// [span.objectrep], views of object representation +template +span::value> +as_bytes(span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +template ::value>> +span::value> +as_writeable_bytes(span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// +// make_span() - Utility functions for creating spans +// +template +constexpr span make_span(ElementType* ptr, typename span::index_type count) +{ + return span(ptr, count); +} + +template +constexpr span make_span(ElementType* firstElem, ElementType* lastElem) +{ + return span(firstElem, lastElem); +} + +template +constexpr span make_span(ElementType (&arr)[N]) noexcept +{ + return span(arr); +} + +template +constexpr span make_span(Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(const Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(Ptr& cont, std::ptrdiff_t count) +{ + return span(cont, count); +} + +template +constexpr span make_span(Ptr& cont) +{ + return span(cont); +} + +// Specialization of gsl::at for span +template +constexpr ElementType& at(span s, index i) +{ + // No bounds checking here because it is done in span::operator[] called below + return s[i]; +} + +} // namespace gsl + +#ifdef _MSC_VER +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 + +#pragma warning(pop) +#endif // _MSC_VER + +#if __GNUC__ > 6 +#pragma GCC diagnostic pop +#endif // __GNUC__ > 6 + +#endif // GSL_SPAN_H diff --git a/extern/include/gsl/string_span b/extern/include/gsl/string_span new file mode 100644 index 000000000..c08f24672 --- /dev/null +++ b/extern/include/gsl/string_span @@ -0,0 +1,730 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_STRING_SPAN_H +#define GSL_STRING_SPAN_H + +#include // for Ensures, Expects +#include // for narrow_cast +#include // for operator!=, operator==, dynamic_extent + +#include // for equal, lexicographical_compare +#include // for array +#include // for ptrdiff_t, size_t, nullptr_t +#include // for PTRDIFF_MAX +#include +#include // for basic_string, allocator, char_traits +#include // for declval, is_convertible, enable_if_t, add_... + +#ifdef _MSC_VER +#pragma warning(push) + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +// In order to test the library, we need it to throw exceptions that we can catch +#ifdef GSL_THROW_ON_CONTRACT_VIOLATION +#define GSL_NOEXCEPT /*noexcept*/ +#else +#define GSL_NOEXCEPT noexcept +#endif // GSL_THROW_ON_CONTRACT_VIOLATION + +namespace gsl +{ +// +// czstring and wzstring +// +// These are "tag" typedefs for C-style strings (i.e. null-terminated character arrays) +// that allow static analysis to help find bugs. +// +// There are no additional features/semantics that we can find a way to add inside the +// type system for these types that will not either incur significant runtime costs or +// (sometimes needlessly) break existing programs when introduced. +// + +template +using basic_zstring = CharT*; + +template +using czstring = basic_zstring; + +template +using cwzstring = basic_zstring; + +template +using cu16zstring = basic_zstring; + +template +using cu32zstring = basic_zstring; + +template +using zstring = basic_zstring; + +template +using wzstring = basic_zstring; + +template +using u16zstring = basic_zstring; + +template +using u32zstring = basic_zstring; + +namespace details +{ + template + std::ptrdiff_t string_length(const CharT* str, std::ptrdiff_t n) + { + if (str == nullptr || n <= 0) return 0; + + const span str_span{str, n}; + + std::ptrdiff_t len = 0; + while (len < n && str_span[len]) len++; + + return len; + } +} + +// +// ensure_sentinel() +// +// Provides a way to obtain an span from a contiguous sequence +// that ends with a (non-inclusive) sentinel value. +// +// Will fail-fast if sentinel cannot be found before max elements are examined. +// +template +span ensure_sentinel(T* seq, std::ptrdiff_t max = PTRDIFF_MAX) +{ + auto cur = seq; + while ((cur - seq) < max && *cur != Sentinel) ++cur; + Ensures(*cur == Sentinel); + return {seq, cur - seq}; +} + +// +// ensure_z - creates a span for a zero terminated strings. +// Will fail fast if a null-terminator cannot be found before +// the limit of size_type. +// +template +span ensure_z(CharT* const& sz, std::ptrdiff_t max = PTRDIFF_MAX) +{ + return ensure_sentinel(sz, max); +} + +template +span ensure_z(CharT (&sz)[N]) +{ + return ensure_z(&sz[0], static_cast(N)); +} + +template +span::type, dynamic_extent> +ensure_z(Cont& cont) +{ + return ensure_z(cont.data(), static_cast(cont.size())); +} + +template +class basic_string_span; + +namespace details +{ + template + struct is_basic_string_span_oracle : std::false_type + { + }; + + template + struct is_basic_string_span_oracle> : std::true_type + { + }; + + template + struct is_basic_string_span : is_basic_string_span_oracle> + { + }; +} + +// +// string_span and relatives +// +template +class basic_string_span +{ +public: + using element_type = CharT; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_lvalue_reference_t>; + using impl_type = span; + + using index_type = typename impl_type::index_type; + using iterator = typename impl_type::iterator; + using const_iterator = typename impl_type::const_iterator; + using reverse_iterator = typename impl_type::reverse_iterator; + using const_reverse_iterator = typename impl_type::const_reverse_iterator; + + // default (empty) + constexpr basic_string_span() GSL_NOEXCEPT = default; + + // copy + constexpr basic_string_span(const basic_string_span& other) GSL_NOEXCEPT = default; + + // assign + constexpr basic_string_span& operator=(const basic_string_span& other) GSL_NOEXCEPT = default; + + constexpr basic_string_span(pointer ptr, index_type length) : span_(ptr, length) {} + constexpr basic_string_span(pointer firstElem, pointer lastElem) : span_(firstElem, lastElem) {} + + // From static arrays - if 0-terminated, remove 0 from the view + // All other containers allow 0s within the length, so we do not remove them + template + constexpr basic_string_span(element_type (&arr)[N]) : span_(remove_z(arr)) + { + } + + template > + constexpr basic_string_span(std::array& arr) GSL_NOEXCEPT : span_(arr) + { + } + + template > + constexpr basic_string_span(const std::array& arr) GSL_NOEXCEPT + : span_(arr) + { + } + + // Container signature should work for basic_string after C++17 version exists + template + constexpr basic_string_span(std::basic_string& str) + : span_(&str[0], narrow_cast(str.length())) + { + } + + template + constexpr basic_string_span(const std::basic_string& str) + : span_(&str[0], str.length()) + { + } + + // from containers. Containers must have a pointer type and data() function signatures + template ::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr basic_string_span(Container& cont) : span_(cont) + { + } + + template ::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr basic_string_span(const Container& cont) : span_(cont) + { + } + + // from string_span + template < + class OtherValueType, std::ptrdiff_t OtherExtent, + class = std::enable_if_t::impl_type, impl_type>::value>> + constexpr basic_string_span(basic_string_span other) + : span_(other.data(), other.length()) + { + } + + template + constexpr basic_string_span first() const + { + return {span_.template first()}; + } + + constexpr basic_string_span first(index_type count) const + { + return {span_.first(count)}; + } + + template + constexpr basic_string_span last() const + { + return {span_.template last()}; + } + + constexpr basic_string_span last(index_type count) const + { + return {span_.last(count)}; + } + + template + constexpr basic_string_span subspan() const + { + return {span_.template subspan()}; + } + + constexpr basic_string_span + subspan(index_type offset, index_type count = dynamic_extent) const + { + return {span_.subspan(offset, count)}; + } + + constexpr reference operator[](index_type idx) const { return span_[idx]; } + constexpr reference operator()(index_type idx) const { return span_[idx]; } + + constexpr pointer data() const { return span_.data(); } + + constexpr index_type length() const GSL_NOEXCEPT { return span_.size(); } + constexpr index_type size() const GSL_NOEXCEPT { return span_.size(); } + constexpr index_type size_bytes() const GSL_NOEXCEPT { return span_.size_bytes(); } + constexpr index_type length_bytes() const GSL_NOEXCEPT { return span_.length_bytes(); } + constexpr bool empty() const GSL_NOEXCEPT { return size() == 0; } + + constexpr iterator begin() const GSL_NOEXCEPT { return span_.begin(); } + constexpr iterator end() const GSL_NOEXCEPT { return span_.end(); } + + constexpr const_iterator cbegin() const GSL_NOEXCEPT { return span_.cbegin(); } + constexpr const_iterator cend() const GSL_NOEXCEPT { return span_.cend(); } + + constexpr reverse_iterator rbegin() const GSL_NOEXCEPT { return span_.rbegin(); } + constexpr reverse_iterator rend() const GSL_NOEXCEPT { return span_.rend(); } + + constexpr const_reverse_iterator crbegin() const GSL_NOEXCEPT { return span_.crbegin(); } + constexpr const_reverse_iterator crend() const GSL_NOEXCEPT { return span_.crend(); } + +private: + static impl_type remove_z(pointer const& sz, std::ptrdiff_t max) + { + return {sz, details::string_length(sz, max)}; + } + + template + static impl_type remove_z(element_type (&sz)[N]) + { + return remove_z(&sz[0], narrow_cast(N)); + } + + impl_type span_; +}; + +template +using string_span = basic_string_span; + +template +using cstring_span = basic_string_span; + +template +using wstring_span = basic_string_span; + +template +using cwstring_span = basic_string_span; + +template +using u16string_span = basic_string_span; + +template +using cu16string_span = basic_string_span; + +template +using u32string_span = basic_string_span; + +template +using cu32string_span = basic_string_span; + +// +// to_string() allow (explicit) conversions from string_span to string +// + +template +std::basic_string::type> +to_string(basic_string_span view) +{ + return {view.data(), static_cast(view.length())}; +} + +template , + typename Allocator = std::allocator, typename gCharT, std::ptrdiff_t Extent> +std::basic_string to_basic_string(basic_string_span view) +{ + return {view.data(), static_cast(view.length())}; +} + +template +basic_string_span::value> +as_bytes(basic_string_span s) noexcept +{ + return { reinterpret_cast(s.data()), s.size_bytes() }; +} + +template ::value>> +basic_string_span::value> +as_writeable_bytes(basic_string_span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// zero-terminated string span, used to convert +// zero-terminated spans to legacy strings +template +class basic_zstring_span +{ +public: + using value_type = CharT; + using const_value_type = std::add_const_t; + + using pointer = std::add_pointer_t; + using const_pointer = std::add_pointer_t; + + using zstring_type = basic_zstring; + using const_zstring_type = basic_zstring; + + using impl_type = span; + using string_span_type = basic_string_span; + + constexpr basic_zstring_span(impl_type s) GSL_NOEXCEPT : span_(s) + { + // expects a zero-terminated span + Expects(s[s.size() - 1] == '\0'); + } + + // copy + constexpr basic_zstring_span(const basic_zstring_span& other) = default; + + // move + constexpr basic_zstring_span(basic_zstring_span&& other) = default; + + // assign + constexpr basic_zstring_span& operator=(const basic_zstring_span& other) = default; + + // move assign + constexpr basic_zstring_span& operator=(basic_zstring_span&& other) = default; + + constexpr bool empty() const GSL_NOEXCEPT { return span_.size() == 0; } + + constexpr string_span_type as_string_span() const GSL_NOEXCEPT + { + auto sz = span_.size(); + return { span_.data(), sz > 1 ? sz - 1 : 0 }; + } + constexpr string_span_type ensure_z() const GSL_NOEXCEPT { return gsl::ensure_z(span_); } + + constexpr const_zstring_type assume_z() const GSL_NOEXCEPT { return span_.data(); } + +private: + impl_type span_; +}; + +template +using zstring_span = basic_zstring_span; + +template +using wzstring_span = basic_zstring_span; + +template +using u16zstring_span = basic_zstring_span; + +template +using u32zstring_span = basic_zstring_span; + +template +using czstring_span = basic_zstring_span; + +template +using cwzstring_span = basic_zstring_span; + +template +using cu16zstring_span = basic_zstring_span; + +template +using cu32zstring_span = basic_zstring_span; + +// operator == +template ::value || + std::is_convertible>>::value>> +bool operator==(const gsl::basic_string_span& one, const T& other) GSL_NOEXCEPT +{ + const gsl::basic_string_span> tmp(other); + return std::equal(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template ::value && + std::is_convertible>>::value>> +bool operator==(const T& one, const gsl::basic_string_span& other) GSL_NOEXCEPT +{ + gsl::basic_string_span> tmp(one); + return std::equal(tmp.begin(), tmp.end(), other.begin(), other.end()); +} + +// operator != +template , Extent>>::value>> +bool operator!=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one == other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator!=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one == other); +} + +// operator< +template , Extent>>::value>> +bool operator<(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + const gsl::basic_string_span, Extent> tmp(other); + return std::lexicographical_compare(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator<(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(one); + return std::lexicographical_compare(tmp.begin(), tmp.end(), other.begin(), other.end()); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(other); + return std::lexicographical_compare(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(one); + return std::lexicographical_compare(tmp.begin(), tmp.end(), other.begin(), other.end()); +} +#endif + +// operator <= +template , Extent>>::value>> +bool operator<=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(other < one); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator<=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(other < one); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(other < one); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(other < one); +} +#endif + +// operator> +template , Extent>>::value>> +bool operator>(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return other < one; +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator>(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return other < one; +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return other < one; +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return other < one; +} +#endif + +// operator >= +template , Extent>>::value>> +bool operator>=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one < other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator>=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one < other); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one < other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one < other); +} +#endif +} // namespace gsl + +#undef GSL_NOEXCEPT + +#ifdef _MSC_VER +#pragma warning(pop) + +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +#endif // GSL_STRING_SPAN_H diff --git a/extern/include/qffuture.h b/extern/include/qffuture.h deleted file mode 120000 index 48b0cc47e..000000000 --- a/extern/include/qffuture.h +++ /dev/null @@ -1 +0,0 @@ -../quickfuture/src/qffuture.h \ No newline at end of file diff --git a/extern/include/qffuture.h b/extern/include/qffuture.h new file mode 100644 index 000000000..7253b49ec --- /dev/null +++ b/extern/include/qffuture.h @@ -0,0 +1,76 @@ +#ifndef QFFUTURE_H +#define QFFUTURE_H + +//NOLINTBEGIN + +#include +#include +#include +#include +#include "qfvariantwrapper.h" + +namespace QuickFuture { + +class Future : public QObject +{ + Q_OBJECT +public: + explicit Future(QObject *parent = 0); + + template + static void registerType() { + registerType(qRegisterMetaType >(), new VariantWrapper() ); + } + + template + static void registerType(std::function converter ) { + VariantWrapper* wrapper = new VariantWrapper(); + wrapper->converter = [=](void* data) { + return converter(*(T*) data); + }; + registerType(qRegisterMetaType >(), wrapper); + } + + QJSEngine *engine() const; + + void setEngine(QQmlEngine *engine); + +signals: + +public slots: + bool isFinished(const QVariant& future); + + bool isRunning(const QVariant& future); + + bool isCanceled(const QVariant& future); + + int progressValue(const QVariant& future); + + int progressMinimum(const QVariant& future); + + int progressMaximum(const QVariant& future); + + void onFinished(const QVariant& future, QJSValue func, QJSValue owner = QJSValue()); + + void onCanceled(const QVariant& future, QJSValue func, QJSValue owner = QJSValue()); + + void onProgressValueChanged(const QVariant& future, QJSValue func); + + QVariant result(const QVariant& future); + + QVariant results(const QVariant& future); + + QJSValue promise(QJSValue future); + + void sync(const QVariant& future, const QString& propertyInFuture, QObject* target, const QString& propertyInTarget = QString()); + +private: + static void registerType(int typeId, VariantWrapperBase* wrapper); + + QPointer m_engine; + QJSValue promiseCreator; +}; + +} +//NOLINTEND +#endif // QFFUTURE_H diff --git a/extern/include/qfvariantwrapper.h b/extern/include/qfvariantwrapper.h deleted file mode 120000 index 1338cd38b..000000000 --- a/extern/include/qfvariantwrapper.h +++ /dev/null @@ -1 +0,0 @@ -../quickfuture/src/qfvariantwrapper.h \ No newline at end of file diff --git a/extern/include/qfvariantwrapper.h b/extern/include/qfvariantwrapper.h new file mode 100644 index 000000000..1807bb3d8 --- /dev/null +++ b/extern/include/qfvariantwrapper.h @@ -0,0 +1,310 @@ +#ifndef QFVARIANTWRAPPER_H +#define QFVARIANTWRAPPER_H + +//NOLINTBEGIN + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QuickFuture { + + typedef std::function Converter; + + template + inline QJSValueList valueList(const QPointer& engine, const QFuture& future) { + QJSValue value; + if (future.resultCount() > 0) + value = engine->toScriptValue(future.result()); + return QJSValueList() << value; + } + + template <> + inline QJSValueList valueList(const QPointer& engine, const QFuture& future) { + Q_UNUSED(engine); + Q_UNUSED(future); + return QJSValueList(); + } + + template + inline void nextTick(F func) { + QObject tmp; + QObject::connect(&tmp, &QObject::destroyed, QCoreApplication::instance(), func, Qt::QueuedConnection); + } + + template + inline QVariant toVariant(const QFuture &future, Converter converter) { + if (!future.isResultReadyAt(0)) { + qWarning() << "Future.result(): The result is not ready!"; + return QVariant(); + } + + QVariant ret; + + if (converter != nullptr) { + T t = future.result(); + ret = converter(&t); + } else { + ret = QVariant::fromValue(future.result()); + } + + return ret; + } + + template <> + inline QVariant toVariant(const QFuture &future, Converter converter) { + Q_UNUSED(converter); + Q_UNUSED(future); + return QVariant(); + } + + template + inline QVariant toVariantList(const QFuture &future, Converter converter) { + if (future.resultCount() == 0) { + qWarning() << "Future.results(): The result is not ready!"; + return QVariant(); + } + + QVariantList ret; + + QList results = future.results(); + + if (converter != nullptr) { + + for (int i = 0 ; i < results.size() ;i++) { + T t = future.resultAt(i); + ret.append(converter(&t)); + } + + } else { + + for (int i = 0 ; i < results.size() ;i++) { + ret.append(QVariant::fromValue(future.resultAt(i))); + } + + } + + return ret; + } + + template <> + inline QVariant toVariantList(const QFuture &future, Converter converter) { + Q_UNUSED(converter); + Q_UNUSED(future); + return QVariant(); + } + + inline void printException(QJSValue value) { + QString message = QString("%1:%2: %3: %4") + .arg(value.property("fileName").toString()) + .arg(value.property("lineNumber").toString()) + .arg(value.property("name").toString()) + .arg(value.property("message").toString()); + qWarning() << message; + } + +class VariantWrapperBase { +public: + VariantWrapperBase() { + } + + virtual inline ~VariantWrapperBase() { + } + + virtual bool isPaused(const QVariant& v) = 0; + virtual bool isFinished(const QVariant& v) = 0; + virtual bool isRunning(const QVariant& v) = 0; + virtual bool isCanceled(const QVariant& v) = 0; + + virtual int progressValue(const QVariant& v) = 0; + + virtual int progressMinimum(const QVariant& v) = 0; + + virtual int progressMaximum(const QVariant& v) = 0; + + virtual QVariant result(const QVariant& v) = 0; + + virtual QVariant results(const QVariant& v) = 0; + + virtual void onFinished(QPointer engine, const QVariant& v, const QJSValue& func, QObject* owner) = 0; + + virtual void onCanceled(QPointer engine, const QVariant& v, const QJSValue& func, QObject* owner) = 0; + + virtual void onProgressValueChanged(QPointer engine, const QVariant& v, const QJSValue& func) = 0; + + virtual void sync(const QVariant &v, const QString &propertyInFuture, QObject *target, const QString &propertyInTarget) = 0; + + // Obtain the value of property by name + bool property(const QVariant& v, const QString& name) { + bool res = false; + if (name == "isFinished") { + res = isFinished(v); + } else if (name == "isRunning") { + res = isRunning(v); + } else if (name == "isPaused") { + res = isPaused(v); + } else { + qWarning().noquote() << QString("Future: Unknown property: %1").arg(name); + } + return res; + } + + Converter converter; +}; + +#define QF_WRAPPER_DECL_READ(type, method) \ + virtual type method(const QVariant& v) { \ + QFuture future = v.value >();\ + return future.method(); \ + } + +#define QF_WRAPPER_CHECK_CALLABLE(method, func) \ + if (!func.isCallable()) { \ + qWarning() << "Future." #method ": Callback is not callable"; \ + return; \ + } + +#define QF_WRAPPER_CONNECT(method, checker, watcherSignal) \ + virtual void method(QPointer engine, const QVariant& v, const QJSValue& func, QObject* owner) { \ + QPointer context = owner; \ + if (!func.isCallable()) { \ + qWarning() << "Future." #method ": Callback is not callable"; \ + return; \ + } \ + QFuture future = v.value>(); \ + auto listener = [=]() { \ + if (!engine.isNull()) { \ + QJSValue callback = func; \ + QJSValue ret = callback.call(QuickFuture::valueList(engine, future)); \ + if (ret.isError()) { \ + printException(ret); \ + } \ + } \ + };\ + if (future.checker()) { \ + QuickFuture::nextTick([=]() { \ + if (owner && context.isNull()) { \ + return;\ + } \ + listener(); \ + }); \ + } else { \ + QFutureWatcher *watcher = new QFutureWatcher(); \ + QObject::connect(watcher, &QFutureWatcherBase::watcherSignal, [=]() { \ + listener(); \ + delete watcher; \ + }); \ + watcher->setParent(owner); \ + watcher->setFuture(future); \ + } \ + } + +template +class VariantWrapper : public VariantWrapperBase { +public: + + QF_WRAPPER_DECL_READ(bool, isFinished) + + QF_WRAPPER_DECL_READ(bool, isRunning) + + QF_WRAPPER_DECL_READ(bool, isPaused) + + QF_WRAPPER_DECL_READ(bool, isCanceled) + + QF_WRAPPER_DECL_READ(int, progressValue) + + QF_WRAPPER_DECL_READ(int, progressMinimum) + + QF_WRAPPER_DECL_READ(int, progressMaximum) + + QF_WRAPPER_CONNECT(onFinished, isFinished, finished) + + QF_WRAPPER_CONNECT(onCanceled, isCanceled, canceled) + + QVariant result(const QVariant &future) { + QFuture f = future.value>(); + return QuickFuture::toVariant(f, converter); + } + + QVariant results(const QVariant &future) { + QFuture f = future.value>(); + return QuickFuture::toVariantList(f, converter); + } + + void onProgressValueChanged(QPointer engine, const QVariant &v, const QJSValue &func) { + if (!func.isCallable()) { + qWarning() << "Future.onProgressValueChanged: Callback is not callable"; + return; + } + + QFuture future = v.value>(); + QFutureWatcher *watcher = 0; + auto listener = [=](int value) { + if (!engine.isNull()) { + QJSValue callback = func; + QJSValueList args; + args << engine->toScriptValue(value); + QJSValue ret = callback.call(args); + if (ret.isError()) { + printException(ret); + } + } + }; + watcher = new QFutureWatcher(); + QObject::connect(watcher, &QFutureWatcherBase::progressValueChanged, listener); + QObject::connect(watcher, &QFutureWatcherBase::finished, [=](){ + watcher->disconnect(); + watcher->deleteLater(); + }); + watcher->setFuture(future); + } + + void sync(const QVariant &future, const QString &propertyInFuture, QObject *target, const QString &propertyInTarget) { + QPointer object = target; + QString pt = propertyInTarget; + if (pt.isEmpty()) { + pt = propertyInFuture; + } + + auto setProperty = [=]() { + if (object.isNull()) { + return; + } + bool value = property(future, propertyInFuture); + object->setProperty( pt.toUtf8().constData(), value); + }; + + setProperty(); + QFuture f = future.value >(); + + if (f.isFinished()) { + // No need to listen on an already finished future + return; + } + + QFutureWatcher *watcher = new QFutureWatcher(); + + QObject::connect(watcher, &QFutureWatcherBase::canceled, setProperty); + QObject::connect(watcher, &QFutureWatcherBase::paused, setProperty); + QObject::connect(watcher, &QFutureWatcherBase::resumed, setProperty); + QObject::connect(watcher, &QFutureWatcherBase::started, setProperty); + + QObject::connect(watcher, &QFutureWatcherBase::finished, [=]() { + setProperty(); + watcher->deleteLater(); + }); + + watcher->setFuture(f); + } +}; + +} // End of namespace + +//NOLINTEND +#endif // QFVARIANTWRAPPER_H diff --git a/extern/include/quickfuture.h b/extern/include/quickfuture.h deleted file mode 120000 index 7094ee369..000000000 --- a/extern/include/quickfuture.h +++ /dev/null @@ -1 +0,0 @@ -../quickfuture/src/quickfuture.h \ No newline at end of file diff --git a/extern/include/quickfuture.h b/extern/include/quickfuture.h new file mode 100644 index 000000000..516980d37 --- /dev/null +++ b/extern/include/quickfuture.h @@ -0,0 +1,33 @@ +#ifndef QUICKFUTURE_H +#define QUICKFUTURE_H + +#include +#include +#include "qffuture.h" + +namespace QuickFuture { + + template + static void registerType() { + Future::registerType(); + } + + template + static void registerType(std::function converter) { + Future::registerType(converter); + } + +} + +#ifdef QUICK_FUTURE_BUILD_PLUGIN +class QuickFutureQmlPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char *uri); +}; +#endif + +#endif // QUICKFUTURE_H diff --git a/extern/include/reproc++ b/extern/include/reproc++ deleted file mode 120000 index 8f7a50e1a..000000000 --- a/extern/include/reproc++ +++ /dev/null @@ -1 +0,0 @@ -../reproc/reproc++/include/reproc++ \ No newline at end of file diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp b/extern/include/reproc++/arguments.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp rename to extern/include/reproc++/arguments.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/detail/array.hpp b/extern/include/reproc++/detail/array.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/detail/array.hpp rename to extern/include/reproc++/detail/array.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp b/extern/include/reproc++/detail/type_traits.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp rename to extern/include/reproc++/detail/type_traits.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/drain.hpp b/extern/include/reproc++/drain.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/drain.hpp rename to extern/include/reproc++/drain.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/env.hpp b/extern/include/reproc++/env.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/env.hpp rename to extern/include/reproc++/env.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/export.hpp b/extern/include/reproc++/export.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/export.hpp rename to extern/include/reproc++/export.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/input.hpp b/extern/include/reproc++/input.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/input.hpp rename to extern/include/reproc++/input.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/reproc.hpp b/extern/include/reproc++/reproc.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/reproc.hpp rename to extern/include/reproc++/reproc.hpp diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/run.hpp b/extern/include/reproc++/run.hpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/include/reproc++/run.hpp rename to extern/include/reproc++/run.hpp diff --git a/extern/include/stduuid b/extern/include/stduuid deleted file mode 120000 index 090dbd22e..000000000 --- a/extern/include/stduuid +++ /dev/null @@ -1 +0,0 @@ -../stduuid/include/ \ No newline at end of file diff --git a/extern/include/stduuid/gsl/gsl b/extern/include/stduuid/gsl/gsl new file mode 100644 index 000000000..55862ebdd --- /dev/null +++ b/extern/include/stduuid/gsl/gsl @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_GSL_H +#define GSL_GSL_H + +#include // copy +#include // Ensures/Expects +#include // byte +#include // finally()/narrow()/narrow_cast()... +#include // multi_span, strided_span... +#include // owner, not_null +#include // span +#include // zstring, string_span, zstring_builder... + +#endif // GSL_GSL_H diff --git a/extern/include/stduuid/gsl/gsl_algorithm b/extern/include/stduuid/gsl/gsl_algorithm new file mode 100644 index 000000000..710792fbd --- /dev/null +++ b/extern/include/stduuid/gsl/gsl_algorithm @@ -0,0 +1,63 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_ALGORITHM_H +#define GSL_ALGORITHM_H + +#include // for Expects +#include // for dynamic_extent, span + +#include // for copy_n +#include // for ptrdiff_t +#include // for is_assignable + +#ifdef _MSC_VER +#pragma warning(push) + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4996) // unsafe use of std::copy_n + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) +#endif // _MSC_VER + +namespace gsl +{ + +template +void copy(span src, span dest) +{ + static_assert(std::is_assignable::value, + "Elements of source span can not be assigned to elements of destination span"); + static_assert(SrcExtent == dynamic_extent || DestExtent == dynamic_extent || + (SrcExtent <= DestExtent), + "Source range is longer than target range"); + + Expects(dest.size() >= src.size()); + std::copy_n(src.data(), src.size(), dest.data()); +} + +} // namespace gsl + +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +#endif // GSL_ALGORITHM_H diff --git a/extern/include/stduuid/gsl/gsl_assert b/extern/include/stduuid/gsl/gsl_assert new file mode 100644 index 000000000..131fa8b15 --- /dev/null +++ b/extern/include/stduuid/gsl/gsl_assert @@ -0,0 +1,145 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_CONTRACTS_H +#define GSL_CONTRACTS_H + +#include +#include // for logic_error + +// +// Temporary until MSVC STL supports no-exceptions mode. +// Currently terminate is a no-op in this mode, so we add termination behavior back +// +#if defined(_MSC_VER) && defined(_HAS_EXCEPTIONS) && !_HAS_EXCEPTIONS +#define GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND +#endif + +// +// There are three configuration options for this GSL implementation's behavior +// when pre/post conditions on the GSL types are violated: +// +// 1. GSL_TERMINATE_ON_CONTRACT_VIOLATION: std::terminate will be called (default) +// 2. GSL_THROW_ON_CONTRACT_VIOLATION: a gsl::fail_fast exception will be thrown +// 3. GSL_UNENFORCED_ON_CONTRACT_VIOLATION: nothing happens +// +#if !(defined(GSL_THROW_ON_CONTRACT_VIOLATION) || defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) || \ + defined(GSL_UNENFORCED_ON_CONTRACT_VIOLATION)) +#define GSL_TERMINATE_ON_CONTRACT_VIOLATION +#endif + +#define GSL_STRINGIFY_DETAIL(x) #x +#define GSL_STRINGIFY(x) GSL_STRINGIFY_DETAIL(x) + +#if defined(__clang__) || defined(__GNUC__) +#define GSL_LIKELY(x) __builtin_expect(!!(x), 1) +#define GSL_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else +#define GSL_LIKELY(x) (!!(x)) +#define GSL_UNLIKELY(x) (!!(x)) +#endif + +// +// GSL_ASSUME(cond) +// +// Tell the optimizer that the predicate cond must hold. It is unspecified +// whether or not cond is actually evaluated. +// +#ifdef _MSC_VER +#define GSL_ASSUME(cond) __assume(cond) +#elif defined(__GNUC__) +#define GSL_ASSUME(cond) ((cond) ? static_cast(0) : __builtin_unreachable()) +#else +#define GSL_ASSUME(cond) static_cast((cond) ? 0 : 0) +#endif + +// +// GSL.assert: assertions +// + +namespace gsl +{ +struct fail_fast : public std::logic_error +{ + explicit fail_fast(char const* const message) : std::logic_error(message) {} +}; + +namespace details +{ +#if defined(GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND) + + typedef void (__cdecl *terminate_handler)(); + + inline gsl::details::terminate_handler& get_terminate_handler() noexcept + { + static terminate_handler handler = &abort; + return handler; + } + +#endif + + [[noreturn]] inline void terminate() noexcept + { +#if defined(GSL_MSVC_USE_STL_NOEXCEPTION_WORKAROUND) + (*gsl::details::get_terminate_handler())(); +#else + std::terminate(); +#endif + } + +#if defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) + + template + [[noreturn]] void throw_exception(Exception&&) + { + gsl::details::terminate(); + } + +#else + + template + [[noreturn]] void throw_exception(Exception&& exception) + { + throw std::forward(exception); + } + +#endif + +} // namespace details +} // namespace gsl + +#if defined(GSL_THROW_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) \ + (GSL_LIKELY(cond) ? static_cast(0) \ + : gsl::details::throw_exception(gsl::fail_fast( \ + "GSL: " type " failure at " __FILE__ ": " GSL_STRINGIFY(__LINE__)))) + +#elif defined(GSL_TERMINATE_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) \ + (GSL_LIKELY(cond) ? static_cast(0) : gsl::details::terminate()) + +#elif defined(GSL_UNENFORCED_ON_CONTRACT_VIOLATION) + +#define GSL_CONTRACT_CHECK(type, cond) GSL_ASSUME(cond) + +#endif + +#define Expects(cond) GSL_CONTRACT_CHECK("Precondition", cond) +#define Ensures(cond) GSL_CONTRACT_CHECK("Postcondition", cond) + +#endif // GSL_CONTRACTS_H diff --git a/extern/include/stduuid/gsl/gsl_byte b/extern/include/stduuid/gsl/gsl_byte new file mode 100644 index 000000000..e8611733b --- /dev/null +++ b/extern/include/stduuid/gsl/gsl_byte @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_BYTE_H +#define GSL_BYTE_H + +#include + +#ifdef _MSC_VER + +#pragma warning(push) + +// don't warn about function style casts in byte related operators +#pragma warning(disable : 26493) + +#ifndef GSL_USE_STD_BYTE +// this tests if we are under MSVC and the standard lib has std::byte and it is enabled +#if defined(_HAS_STD_BYTE) && _HAS_STD_BYTE + +#define GSL_USE_STD_BYTE 1 + +#else // defined(_HAS_STD_BYTE) && _HAS_STD_BYTE + +#define GSL_USE_STD_BYTE 0 + +#endif // defined(_HAS_STD_BYTE) && _HAS_STD_BYTE +#endif // GSL_USE_STD_BYTE + +#else // _MSC_VER + +#ifndef GSL_USE_STD_BYTE +// this tests if we are under GCC or Clang with enough -std:c++1z power to get us std::byte +#if defined(__cplusplus) && (__cplusplus >= 201703L) + +#define GSL_USE_STD_BYTE 1 +#include + +#else // defined(__cplusplus) && (__cplusplus >= 201703L) + +#define GSL_USE_STD_BYTE 0 + +#endif //defined(__cplusplus) && (__cplusplus >= 201703L) +#endif // GSL_USE_STD_BYTE + +#endif // _MSC_VER + +// Use __may_alias__ attribute on gcc and clang +#if defined __clang__ || (__GNUC__ > 5) +#define byte_may_alias __attribute__((__may_alias__)) +#else // defined __clang__ || defined __GNUC__ +#define byte_may_alias +#endif // defined __clang__ || defined __GNUC__ + +namespace gsl +{ +#if GSL_USE_STD_BYTE + + +using std::byte; +using std::to_integer; + +#else // GSL_USE_STD_BYTE + +// This is a simple definition for now that allows +// use of byte within span<> to be standards-compliant +enum class byte_may_alias byte : unsigned char +{ +}; + +template ::value>> +constexpr byte& operator<<=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte operator<<(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte& operator>>=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) >> shift); +} + +template ::value>> +constexpr byte operator>>(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) >> shift); +} + +constexpr byte& operator|=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) | static_cast(r)); +} + +constexpr byte operator|(byte l, byte r) noexcept +{ + return byte(static_cast(l) | static_cast(r)); +} + +constexpr byte& operator&=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) & static_cast(r)); +} + +constexpr byte operator&(byte l, byte r) noexcept +{ + return byte(static_cast(l) & static_cast(r)); +} + +constexpr byte& operator^=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator^(byte l, byte r) noexcept +{ + return byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator~(byte b) noexcept { return byte(~static_cast(b)); } + +template ::value>> +constexpr IntegerType to_integer(byte b) noexcept +{ + return static_cast(b); +} + +#endif // GSL_USE_STD_BYTE + +template +constexpr byte to_byte_impl(T t) noexcept +{ + static_assert( + E, "gsl::to_byte(t) must be provided an unsigned char, otherwise data loss may occur. " + "If you are calling to_byte with an integer contant use: gsl::to_byte() version."); + return static_cast(t); +} +template <> +constexpr byte to_byte_impl(unsigned char t) noexcept +{ + return byte(t); +} + +template +constexpr byte to_byte(T t) noexcept +{ + return to_byte_impl::value, T>(t); +} + +template +constexpr byte to_byte() noexcept +{ + static_assert(I >= 0 && I <= 255, + "gsl::byte only has 8 bits of storage, values must be in range 0-255"); + return static_cast(I); +} + +} // namespace gsl + +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +#endif // GSL_BYTE_H diff --git a/extern/include/stduuid/gsl/gsl_util b/extern/include/stduuid/gsl/gsl_util new file mode 100644 index 000000000..25f85020c --- /dev/null +++ b/extern/include/stduuid/gsl/gsl_util @@ -0,0 +1,158 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_UTIL_H +#define GSL_UTIL_H + +#include // for Expects + +#include +#include // for ptrdiff_t, size_t +#include // for exception +#include // for initializer_list +#include // for is_signed, integral_constant +#include // for forward + +#if defined(_MSC_VER) + +#pragma warning(push) +#pragma warning(disable : 4127) // conditional expression is constant + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +namespace gsl +{ +// +// GSL.util: utilities +// + +// index type for all container indexes/subscripts/sizes +using index = std::ptrdiff_t; + +// final_action allows you to ensure something gets run at the end of a scope +template +class final_action +{ +public: + explicit final_action(F f) noexcept : f_(std::move(f)) {} + + final_action(final_action&& other) noexcept : f_(std::move(other.f_)), invoke_(other.invoke_) + { + other.invoke_ = false; + } + + final_action(const final_action&) = delete; + final_action& operator=(const final_action&) = delete; + final_action& operator=(final_action&&) = delete; + + ~final_action() noexcept + { + if (invoke_) f_(); + } + +private: + F f_; + bool invoke_ {true}; +}; + +// finally() - convenience function to generate a final_action +template + +final_action finally(const F& f) noexcept +{ + return final_action(f); +} + +template +final_action finally(F&& f) noexcept +{ + return final_action(std::forward(f)); +} + +// narrow_cast(): a searchable way to do narrowing casts of values +template +constexpr T narrow_cast(U&& u) noexcept +{ + return static_cast(std::forward(u)); +} + +struct narrowing_error : public std::exception +{ +}; + +namespace details +{ + template + struct is_same_signedness + : public std::integral_constant::value == std::is_signed::value> + { + }; +} + +// narrow() : a checked version of narrow_cast() that throws if the cast changed the value +template +T narrow(U u) +{ + T t = narrow_cast(u); + if (static_cast(t) != u) gsl::details::throw_exception(narrowing_error()); + if (!details::is_same_signedness::value && ((t < T{}) != (u < U{}))) + gsl::details::throw_exception(narrowing_error()); + return t; +} + +// +// at() - Bounds-checked way of accessing builtin arrays, std::array, std::vector +// +template +constexpr T& at(T (&arr)[N], const index i) +{ + Expects(i >= 0 && i < narrow_cast(N)); + return arr[static_cast(i)]; +} + +template +constexpr auto at(Cont& cont, const index i) -> decltype(cont[cont.size()]) +{ + Expects(i >= 0 && i < narrow_cast(cont.size())); + using size_type = decltype(cont.size()); + return cont[static_cast(i)]; +} + +template +constexpr T at(const std::initializer_list cont, const index i) +{ + Expects(i >= 0 && i < narrow_cast(cont.size())); + return *(cont.begin() + i); +} + +} // namespace gsl + +#if defined(_MSC_VER) +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 + +#pragma warning(pop) + +#endif // _MSC_VER + +#endif // GSL_UTIL_H diff --git a/extern/include/stduuid/gsl/multi_span b/extern/include/stduuid/gsl/multi_span new file mode 100644 index 000000000..9c0c27b33 --- /dev/null +++ b/extern/include/stduuid/gsl/multi_span @@ -0,0 +1,2242 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_MULTI_SPAN_H +#define GSL_MULTI_SPAN_H + +#include // for Expects +#include // for byte +#include // for narrow_cast + +#include // for transform, lexicographical_compare +#include // for array +#include +#include // for ptrdiff_t, size_t, nullptr_t +#include // for PTRDIFF_MAX +#include // for divides, multiplies, minus, negate, plus +#include // for initializer_list +#include // for iterator, random_access_iterator_tag +#include // for numeric_limits +#include +#include +#include +#include // for basic_string +#include // for enable_if_t, remove_cv_t, is_same, is_co... +#include + +#ifdef _MSC_VER + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(push) +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4702) // unreachable code + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +// GCC 7 does not like the signed unsigned missmatch (size_t ptrdiff_t) +// While there is a conversion from signed to unsigned, it happens at +// compiletime, so the compiler wouldn't have to warn indiscriminently, but +// could check if the source value actually doesn't fit into the target type +// and only warn in those cases. +#if __GNUC__ > 6 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +#ifdef GSL_THROW_ON_CONTRACT_VIOLATION +#define GSL_NOEXCEPT /*noexcept*/ +#else +#define GSL_NOEXCEPT noexcept +#endif // GSL_THROW_ON_CONTRACT_VIOLATION + +namespace gsl +{ + +/* +** begin definitions of index and bounds +*/ +namespace details +{ + template + struct SizeTypeTraits + { + static const SizeType max_value = std::numeric_limits::max(); + }; + + template + class are_integral : public std::integral_constant + { + }; + + template + class are_integral + : public std::integral_constant::value && are_integral::value> + { + }; +} + +template +class multi_span_index final +{ + static_assert(Rank > 0, "Rank must be greater than 0!"); + + template + friend class multi_span_index; + +public: + static const std::size_t rank = Rank; + using value_type = std::ptrdiff_t; + using size_type = value_type; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_lvalue_reference_t>; + + constexpr multi_span_index() GSL_NOEXCEPT {} + + constexpr multi_span_index(const value_type (&values)[Rank]) GSL_NOEXCEPT + { + std::copy(values, values + Rank, elems); + } + + template ::value>> + constexpr multi_span_index(Ts... ds) GSL_NOEXCEPT : elems{narrow_cast(ds)...} + { + } + + constexpr multi_span_index(const multi_span_index& other) GSL_NOEXCEPT = default; + + constexpr multi_span_index& operator=(const multi_span_index& rhs) GSL_NOEXCEPT = default; + + // Preconditions: component_idx < rank + constexpr reference operator[](std::size_t component_idx) + { + Expects(component_idx < Rank); // Component index must be less than rank + return elems[component_idx]; + } + + // Preconditions: component_idx < rank + constexpr const_reference operator[](std::size_t component_idx) const GSL_NOEXCEPT + { + Expects(component_idx < Rank); // Component index must be less than rank + return elems[component_idx]; + } + + constexpr bool operator==(const multi_span_index& rhs) const GSL_NOEXCEPT + { + return std::equal(elems, elems + rank, rhs.elems); + } + + constexpr bool operator!=(const multi_span_index& rhs) const GSL_NOEXCEPT { return !(*this == rhs); } + + constexpr multi_span_index operator+() const GSL_NOEXCEPT { return *this; } + + constexpr multi_span_index operator-() const GSL_NOEXCEPT + { + multi_span_index ret = *this; + std::transform(ret, ret + rank, ret, std::negate{}); + return ret; + } + + constexpr multi_span_index operator+(const multi_span_index& rhs) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret += rhs; + return ret; + } + + constexpr multi_span_index operator-(const multi_span_index& rhs) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret -= rhs; + return ret; + } + + constexpr multi_span_index& operator+=(const multi_span_index& rhs) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, rhs.elems, elems, std::plus{}); + return *this; + } + + constexpr multi_span_index& operator-=(const multi_span_index& rhs) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, rhs.elems, elems, std::minus{}); + return *this; + } + + constexpr multi_span_index operator*(value_type v) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret *= v; + return ret; + } + + constexpr multi_span_index operator/(value_type v) const GSL_NOEXCEPT + { + multi_span_index ret = *this; + ret /= v; + return ret; + } + + friend constexpr multi_span_index operator*(value_type v, const multi_span_index& rhs) GSL_NOEXCEPT + { + return rhs * v; + } + + constexpr multi_span_index& operator*=(value_type v) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, elems, + [v](value_type x) { return std::multiplies{}(x, v); }); + return *this; + } + + constexpr multi_span_index& operator/=(value_type v) GSL_NOEXCEPT + { + std::transform(elems, elems + rank, elems, + [v](value_type x) { return std::divides{}(x, v); }); + return *this; + } + +private: + value_type elems[Rank] = {}; +}; + +#if !defined(_MSC_VER) || _MSC_VER >= 1910 + +struct static_bounds_dynamic_range_t +{ + template ::value>> + constexpr operator T() const GSL_NOEXCEPT + { + return narrow_cast(-1); + } +}; + +constexpr bool operator==(static_bounds_dynamic_range_t, static_bounds_dynamic_range_t) GSL_NOEXCEPT +{ + return true; +} + +constexpr bool operator!=(static_bounds_dynamic_range_t, static_bounds_dynamic_range_t) GSL_NOEXCEPT +{ + return false; +} + +template ::value>> +constexpr bool operator==(static_bounds_dynamic_range_t, T other) GSL_NOEXCEPT +{ + return narrow_cast(-1) == other; +} + +template ::value>> +constexpr bool operator==(T left, static_bounds_dynamic_range_t right) GSL_NOEXCEPT +{ + return right == left; +} + +template ::value>> +constexpr bool operator!=(static_bounds_dynamic_range_t, T other) GSL_NOEXCEPT +{ + return narrow_cast(-1) != other; +} + +template ::value>> +constexpr bool operator!=(T left, static_bounds_dynamic_range_t right) GSL_NOEXCEPT +{ + return right != left; +} + +constexpr static_bounds_dynamic_range_t dynamic_range{}; +#else +const std::ptrdiff_t dynamic_range = -1; +#endif + +struct generalized_mapping_tag +{ +}; +struct contiguous_mapping_tag : generalized_mapping_tag +{ +}; + +namespace details +{ + + template + struct LessThan + { + static const bool value = Left < Right; + }; + + template + struct BoundsRanges + { + using size_type = std::ptrdiff_t; + static const size_type Depth = 0; + static const size_type DynamicNum = 0; + static const size_type CurrentRange = 1; + static const size_type TotalSize = 1; + + // TODO : following signature is for work around VS bug + template + BoundsRanges(const OtherRange&, bool /* firstLevel */) + { + } + + BoundsRanges(const std::ptrdiff_t* const) {} + BoundsRanges() = default; + + template + void serialize(T&) const + { + } + + template + size_type linearize(const T&) const + { + return 0; + } + + template + size_type contains(const T&) const + { + return -1; + } + + size_type elementNum(std::size_t) const GSL_NOEXCEPT { return 0; } + + size_type totalSize() const GSL_NOEXCEPT { return TotalSize; } + + bool operator==(const BoundsRanges&) const GSL_NOEXCEPT { return true; } + }; + + template + struct BoundsRanges : BoundsRanges + { + using Base = BoundsRanges; + using size_type = std::ptrdiff_t; + static const std::size_t Depth = Base::Depth + 1; + static const std::size_t DynamicNum = Base::DynamicNum + 1; + static const size_type CurrentRange = dynamic_range; + static const size_type TotalSize = dynamic_range; + + private: + size_type m_bound; + + public: + BoundsRanges(const std::ptrdiff_t* const arr) + : Base(arr + 1), m_bound(*arr * this->Base::totalSize()) + { + Expects(0 <= *arr); + } + + BoundsRanges() : m_bound(0) {} + + template + BoundsRanges(const BoundsRanges& other, + bool /* firstLevel */ = true) + : Base(static_cast&>(other), false) + , m_bound(other.totalSize()) + { + } + + template + void serialize(T& arr) const + { + arr[Dim] = elementNum(); + this->Base::template serialize(arr); + } + + template + size_type linearize(const T& arr) const + { + const size_type index = this->Base::totalSize() * arr[Dim]; + Expects(index < m_bound); + return index + this->Base::template linearize(arr); + } + + template + size_type contains(const T& arr) const + { + const ptrdiff_t last = this->Base::template contains(arr); + if (last == -1) return -1; + const ptrdiff_t cur = this->Base::totalSize() * arr[Dim]; + return cur < m_bound ? cur + last : -1; + } + + size_type totalSize() const GSL_NOEXCEPT { return m_bound; } + + size_type elementNum() const GSL_NOEXCEPT { return totalSize() / this->Base::totalSize(); } + + size_type elementNum(std::size_t dim) const GSL_NOEXCEPT + { + if (dim > 0) + return this->Base::elementNum(dim - 1); + else + return elementNum(); + } + + bool operator==(const BoundsRanges& rhs) const GSL_NOEXCEPT + { + return m_bound == rhs.m_bound && + static_cast(*this) == static_cast(rhs); + } + }; + + template + struct BoundsRanges : BoundsRanges + { + using Base = BoundsRanges; + using size_type = std::ptrdiff_t; + static const std::size_t Depth = Base::Depth + 1; + static const std::size_t DynamicNum = Base::DynamicNum; + static const size_type CurrentRange = CurRange; + static const size_type TotalSize = + Base::TotalSize == dynamic_range ? dynamic_range : CurrentRange * Base::TotalSize; + + BoundsRanges(const std::ptrdiff_t* const arr) : Base(arr) {} + BoundsRanges() = default; + + template + BoundsRanges(const BoundsRanges& other, + bool firstLevel = true) + : Base(static_cast&>(other), false) + { + (void) firstLevel; + } + + template + void serialize(T& arr) const + { + arr[Dim] = elementNum(); + this->Base::template serialize(arr); + } + + template + size_type linearize(const T& arr) const + { + Expects(arr[Dim] >= 0 && arr[Dim] < CurrentRange); // Index is out of range + return this->Base::totalSize() * arr[Dim] + + this->Base::template linearize(arr); + } + + template + size_type contains(const T& arr) const + { + if (arr[Dim] >= CurrentRange) return -1; + const size_type last = this->Base::template contains(arr); + if (last == -1) return -1; + return this->Base::totalSize() * arr[Dim] + last; + } + + size_type totalSize() const GSL_NOEXCEPT { return CurrentRange * this->Base::totalSize(); } + + size_type elementNum() const GSL_NOEXCEPT { return CurrentRange; } + + size_type elementNum(std::size_t dim) const GSL_NOEXCEPT + { + if (dim > 0) + return this->Base::elementNum(dim - 1); + else + return elementNum(); + } + + bool operator==(const BoundsRanges& rhs) const GSL_NOEXCEPT + { + return static_cast(*this) == static_cast(rhs); + } + }; + + template + struct BoundsRangeConvertible + : public std::integral_constant= TargetType::TotalSize || + TargetType::TotalSize == dynamic_range || + SourceType::TotalSize == dynamic_range || + TargetType::TotalSize == 0)> + { + }; + + template + struct TypeListIndexer + { + const TypeChain& obj_; + TypeListIndexer(const TypeChain& obj) : obj_(obj) {} + + template + const TypeChain& getObj(std::true_type) + { + return obj_; + } + + template + auto getObj(std::false_type) + -> decltype(TypeListIndexer(static_cast(obj_)).template get()) + { + return TypeListIndexer(static_cast(obj_)).template get(); + } + + template + auto get() -> decltype(getObj(std::integral_constant())) + { + return getObj(std::integral_constant()); + } + }; + + template + TypeListIndexer createTypeListIndexer(const TypeChain& obj) + { + return TypeListIndexer(obj); + } + + template 1), + typename Ret = std::enable_if_t>> + constexpr Ret shift_left(const multi_span_index& other) GSL_NOEXCEPT + { + Ret ret{}; + for (std::size_t i = 0; i < Rank - 1; ++i) { + ret[i] = other[i + 1]; + } + return ret; + } +} + +template +class bounds_iterator; + +template +class static_bounds +{ +public: + static_bounds(const details::BoundsRanges&) {} +}; + +template +class static_bounds +{ + using MyRanges = details::BoundsRanges; + + MyRanges m_ranges; + constexpr static_bounds(const MyRanges& range) : m_ranges(range) {} + + template + friend class static_bounds; + +public: + static const std::size_t rank = MyRanges::Depth; + static const std::size_t dynamic_rank = MyRanges::DynamicNum; + static const std::ptrdiff_t static_size = MyRanges::TotalSize; + + using size_type = std::ptrdiff_t; + using index_type = multi_span_index; + using const_index_type = std::add_const_t; + using iterator = bounds_iterator; + using const_iterator = bounds_iterator; + using difference_type = std::ptrdiff_t; + using sliced_type = static_bounds; + using mapping_type = contiguous_mapping_tag; + + constexpr static_bounds(const static_bounds&) = default; + + template + struct BoundsRangeConvertible2; + + template > + static auto helpBoundsRangeConvertible(SourceType, TargetType, std::true_type) -> Ret; + + template + static auto helpBoundsRangeConvertible(SourceType, TargetType, ...) -> std::false_type; + + template + struct BoundsRangeConvertible2 + : decltype(helpBoundsRangeConvertible( + SourceType(), TargetType(), + std::integral_constant())) + { + }; + + template + struct BoundsRangeConvertible2 : std::true_type + { + }; + + template + struct BoundsRangeConvertible + : decltype(helpBoundsRangeConvertible( + SourceType(), TargetType(), + std::integral_constant::value || + TargetType::CurrentRange == dynamic_range || + SourceType::CurrentRange == dynamic_range)>())) + { + }; + + template + struct BoundsRangeConvertible : std::true_type + { + }; + + template , + details::BoundsRanges>::value>> + constexpr static_bounds(const static_bounds& other) : m_ranges(other.m_ranges) + { + Expects((MyRanges::DynamicNum == 0 && details::BoundsRanges::DynamicNum == 0) || + MyRanges::DynamicNum > 0 || other.m_ranges.totalSize() >= m_ranges.totalSize()); + } + + constexpr static_bounds(std::initializer_list il) + : m_ranges(il.begin()) + { + // Size of the initializer list must match the rank of the array + Expects((MyRanges::DynamicNum == 0 && il.size() == 1 && *il.begin() == static_size) || + MyRanges::DynamicNum == il.size()); + // Size of the range must be less than the max element of the size type + Expects(m_ranges.totalSize() <= PTRDIFF_MAX); + } + + constexpr static_bounds() = default; + + constexpr sliced_type slice() const GSL_NOEXCEPT + { + return sliced_type{static_cast&>(m_ranges)}; + } + + constexpr size_type stride() const GSL_NOEXCEPT { return rank > 1 ? slice().size() : 1; } + + constexpr size_type size() const GSL_NOEXCEPT { return m_ranges.totalSize(); } + + constexpr size_type total_size() const GSL_NOEXCEPT { return m_ranges.totalSize(); } + + constexpr size_type linearize(const index_type& idx) const { return m_ranges.linearize(idx); } + + constexpr bool contains(const index_type& idx) const GSL_NOEXCEPT + { + return m_ranges.contains(idx) != -1; + } + + constexpr size_type operator[](std::size_t idx) const GSL_NOEXCEPT + { + return m_ranges.elementNum(idx); + } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < rank, + "dimension should be less than rank (dimension count starts from 0)"); + return details::createTypeListIndexer(m_ranges).template get().elementNum(); + } + + template + constexpr size_type extent(IntType dim) const GSL_NOEXCEPT + { + static_assert(std::is_integral::value, + "Dimension parameter must be supplied as an integral type."); + auto real_dim = narrow_cast(dim); + Expects(real_dim < rank); + + return m_ranges.elementNum(real_dim); + } + + constexpr index_type index_bounds() const GSL_NOEXCEPT + { + size_type extents[rank] = {}; + m_ranges.serialize(extents); + return {extents}; + } + + template + constexpr bool operator==(const static_bounds& rhs) const GSL_NOEXCEPT + { + return this->size() == rhs.size(); + } + + template + constexpr bool operator!=(const static_bounds& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + + constexpr const_iterator begin() const GSL_NOEXCEPT + { + return const_iterator(*this, index_type{}); + } + + constexpr const_iterator end() const GSL_NOEXCEPT + { + return const_iterator(*this, this->index_bounds()); + } +}; + +template +class strided_bounds +{ + template + friend class strided_bounds; + +public: + static const std::size_t rank = Rank; + using value_type = std::ptrdiff_t; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_const_t; + using size_type = value_type; + using difference_type = value_type; + using index_type = multi_span_index; + using const_index_type = std::add_const_t; + using iterator = bounds_iterator; + using const_iterator = bounds_iterator; + static const value_type dynamic_rank = rank; + static const value_type static_size = dynamic_range; + using sliced_type = std::conditional_t, void>; + using mapping_type = generalized_mapping_tag; + + constexpr strided_bounds(const strided_bounds&) GSL_NOEXCEPT = default; + + constexpr strided_bounds& operator=(const strided_bounds&) GSL_NOEXCEPT = default; + + constexpr strided_bounds(const value_type (&values)[rank], index_type strides) + : m_extents(values), m_strides(std::move(strides)) + { + } + + constexpr strided_bounds(const index_type& extents, const index_type& strides) GSL_NOEXCEPT + : m_extents(extents), + m_strides(strides) + { + } + + constexpr index_type strides() const GSL_NOEXCEPT { return m_strides; } + + constexpr size_type total_size() const GSL_NOEXCEPT + { + size_type ret = 0; + for (std::size_t i = 0; i < rank; ++i) { + ret += (m_extents[i] - 1) * m_strides[i]; + } + return ret + 1; + } + + constexpr size_type size() const GSL_NOEXCEPT + { + size_type ret = 1; + for (std::size_t i = 0; i < rank; ++i) { + ret *= m_extents[i]; + } + return ret; + } + + constexpr bool contains(const index_type& idx) const GSL_NOEXCEPT + { + for (std::size_t i = 0; i < rank; ++i) { + if (idx[i] < 0 || idx[i] >= m_extents[i]) return false; + } + return true; + } + + constexpr size_type linearize(const index_type& idx) const GSL_NOEXCEPT + { + size_type ret = 0; + for (std::size_t i = 0; i < rank; i++) { + Expects(idx[i] < m_extents[i]); // index is out of bounds of the array + ret += idx[i] * m_strides[i]; + } + return ret; + } + + constexpr size_type stride() const GSL_NOEXCEPT { return m_strides[0]; } + + template 1), typename Ret = std::enable_if_t> + constexpr sliced_type slice() const + { + return {details::shift_left(m_extents), details::shift_left(m_strides)}; + } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "dimension should be less than rank (dimension count starts from 0)"); + return m_extents[Dim]; + } + + constexpr index_type index_bounds() const GSL_NOEXCEPT { return m_extents; } + constexpr const_iterator begin() const GSL_NOEXCEPT + { + return const_iterator{*this, index_type{}}; + } + + constexpr const_iterator end() const GSL_NOEXCEPT + { + return const_iterator{*this, index_bounds()}; + } + +private: + index_type m_extents; + index_type m_strides; +}; + +template +struct is_bounds : std::integral_constant +{ +}; +template +struct is_bounds> : std::integral_constant +{ +}; +template +struct is_bounds> : std::integral_constant +{ +}; + +template +class bounds_iterator +{ +public: + static const std::size_t rank = IndexType::rank; + using iterator_category = std::random_access_iterator_tag; + using value_type = IndexType; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + using index_type = value_type; + using index_size_type = typename IndexType::value_type; + template + explicit bounds_iterator(const Bounds& bnd, value_type curr) GSL_NOEXCEPT + : boundary_(bnd.index_bounds()), + curr_(std::move(curr)) + { + static_assert(is_bounds::value, "Bounds type must be provided"); + } + + constexpr reference operator*() const GSL_NOEXCEPT { return curr_; } + + constexpr pointer operator->() const GSL_NOEXCEPT { return &curr_; } + + constexpr bounds_iterator& operator++() GSL_NOEXCEPT + { + for (std::size_t i = rank; i-- > 0;) { + if (curr_[i] < boundary_[i] - 1) { + curr_[i]++; + return *this; + } + curr_[i] = 0; + } + // If we're here we've wrapped over - set to past-the-end. + curr_ = boundary_; + return *this; + } + + constexpr bounds_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + + constexpr bounds_iterator& operator--() GSL_NOEXCEPT + { + if (!less(curr_, boundary_)) { + // if at the past-the-end, set to last element + for (std::size_t i = 0; i < rank; ++i) { + curr_[i] = boundary_[i] - 1; + } + return *this; + } + for (std::size_t i = rank; i-- > 0;) { + if (curr_[i] >= 1) { + curr_[i]--; + return *this; + } + curr_[i] = boundary_[i] - 1; + } + // If we're here the preconditions were violated + // "pre: there exists s such that r == ++s" + Expects(false); + return *this; + } + + constexpr bounds_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + + constexpr bounds_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + bounds_iterator ret{*this}; + return ret += n; + } + + constexpr bounds_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + auto linear_idx = linearize(curr_) + n; + std::remove_const_t stride = 0; + stride[rank - 1] = 1; + for (std::size_t i = rank - 1; i-- > 0;) { + stride[i] = stride[i + 1] * boundary_[i + 1]; + } + for (std::size_t i = 0; i < rank; ++i) { + curr_[i] = linear_idx / stride[i]; + linear_idx = linear_idx % stride[i]; + } + // index is out of bounds of the array + Expects(!less(curr_, index_type{}) && !less(boundary_, curr_)); + return *this; + } + + constexpr bounds_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + bounds_iterator ret{*this}; + return ret -= n; + } + + constexpr bounds_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + + constexpr difference_type operator-(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return linearize(curr_) - linearize(rhs.curr_); + } + + constexpr value_type operator[](difference_type n) const GSL_NOEXCEPT { return *(*this + n); } + + constexpr bool operator==(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return curr_ == rhs.curr_; + } + + constexpr bool operator!=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + + constexpr bool operator<(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return less(curr_, rhs.curr_); + } + + constexpr bool operator<=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs < *this); + } + + constexpr bool operator>(const bounds_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + + constexpr bool operator>=(const bounds_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs > *this); + } + + void swap(bounds_iterator& rhs) GSL_NOEXCEPT + { + std::swap(boundary_, rhs.boundary_); + std::swap(curr_, rhs.curr_); + } + +private: + constexpr bool less(index_type& one, index_type& other) const GSL_NOEXCEPT + { + for (std::size_t i = 0; i < rank; ++i) { + if (one[i] < other[i]) return true; + } + return false; + } + + constexpr index_size_type linearize(const value_type& idx) const GSL_NOEXCEPT + { + // TODO: Smarter impl. + // Check if past-the-end + index_size_type multiplier = 1; + index_size_type res = 0; + if (!less(idx, boundary_)) { + res = 1; + for (std::size_t i = rank; i-- > 0;) { + res += (idx[i] - 1) * multiplier; + multiplier *= boundary_[i]; + } + } + else + { + for (std::size_t i = rank; i-- > 0;) { + res += idx[i] * multiplier; + multiplier *= boundary_[i]; + } + } + return res; + } + + value_type boundary_; + std::remove_const_t curr_; +}; + +template +bounds_iterator operator+(typename bounds_iterator::difference_type n, + const bounds_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +namespace details +{ + template + constexpr std::enable_if_t< + std::is_same::value, + typename Bounds::index_type> + make_stride(const Bounds& bnd) GSL_NOEXCEPT + { + return bnd.strides(); + } + + // Make a stride vector from bounds, assuming contiguous memory. + template + constexpr std::enable_if_t< + std::is_same::value, + typename Bounds::index_type> + make_stride(const Bounds& bnd) GSL_NOEXCEPT + { + auto extents = bnd.index_bounds(); + typename Bounds::size_type stride[Bounds::rank] = {}; + + stride[Bounds::rank - 1] = 1; + for (std::size_t i = 1; i < Bounds::rank; ++i) { + stride[Bounds::rank - i - 1] = stride[Bounds::rank - i] * extents[Bounds::rank - i]; + } + return {stride}; + } + + template + void verifyBoundsReshape(const BoundsSrc& src, const BoundsDest& dest) + { + static_assert(is_bounds::value && is_bounds::value, + "The src type and dest type must be bounds"); + static_assert(std::is_same::value, + "The source type must be a contiguous bounds"); + static_assert(BoundsDest::static_size == dynamic_range || + BoundsSrc::static_size == dynamic_range || + BoundsDest::static_size == BoundsSrc::static_size, + "The source bounds must have same size as dest bounds"); + Expects(src.size() == dest.size()); + } + +} // namespace details + +template +class contiguous_span_iterator; +template +class general_span_iterator; + +template +struct dim_t +{ + static const std::ptrdiff_t value = DimSize; +}; +template <> +struct dim_t +{ + static const std::ptrdiff_t value = dynamic_range; + const std::ptrdiff_t dvalue; + constexpr dim_t(std::ptrdiff_t size) GSL_NOEXCEPT : dvalue(size) {} +}; + +template = 0)>> +constexpr dim_t dim() GSL_NOEXCEPT +{ + return dim_t(); +} + +template > +constexpr dim_t dim(std::ptrdiff_t n) GSL_NOEXCEPT +{ + return dim_t<>(n); +} + +template +class multi_span; +template +class strided_span; + +namespace details +{ + template + struct SpanTypeTraits + { + using value_type = T; + using size_type = std::size_t; + }; + + template + struct SpanTypeTraits::type> + { + using value_type = typename Traits::span_traits::value_type; + using size_type = typename Traits::span_traits::size_type; + }; + + template + struct SpanArrayTraits + { + using type = multi_span; + using value_type = T; + using bounds_type = static_bounds; + using pointer = T*; + using reference = T&; + }; + template + struct SpanArrayTraits : SpanArrayTraits + { + }; + + template + BoundsType newBoundsHelperImpl(std::ptrdiff_t totalSize, std::true_type) // dynamic size + { + Expects(totalSize >= 0 && totalSize <= PTRDIFF_MAX); + return BoundsType{totalSize}; + } + template + BoundsType newBoundsHelperImpl(std::ptrdiff_t totalSize, std::false_type) // static size + { + Expects(BoundsType::static_size <= totalSize); + return {}; + } + template + BoundsType newBoundsHelper(std::ptrdiff_t totalSize) + { + static_assert(BoundsType::dynamic_rank <= 1, "dynamic rank must less or equal to 1"); + return newBoundsHelperImpl( + totalSize, std::integral_constant()); + } + + struct Sep + { + }; + + template + T static_as_multi_span_helper(Sep, Args... args) + { + return T{narrow_cast(args)...}; + } + template + std::enable_if_t< + !std::is_same>::value && !std::is_same::value, T> + static_as_multi_span_helper(Arg, Args... args) + { + return static_as_multi_span_helper(args...); + } + template + T static_as_multi_span_helper(dim_t val, Args... args) + { + return static_as_multi_span_helper(args..., val.dvalue); + } + + template + struct static_as_multi_span_static_bounds_helper + { + using type = static_bounds<(Dimensions::value)...>; + }; + + template + struct is_multi_span_oracle : std::false_type + { + }; + + template + struct is_multi_span_oracle> + : std::true_type + { + }; + + template + struct is_multi_span_oracle> : std::true_type + { + }; + + template + struct is_multi_span : is_multi_span_oracle> + { + }; +} + +template +class multi_span +{ + // TODO do we still need this? + template + friend class multi_span; + +public: + using bounds_type = static_bounds; + static const std::size_t Rank = bounds_type::rank; + using size_type = typename bounds_type::size_type; + using index_type = typename bounds_type::index_type; + using value_type = ValueType; + using const_value_type = std::add_const_t; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using iterator = contiguous_span_iterator; + using const_span = multi_span; + using const_iterator = contiguous_span_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + using sliced_type = + std::conditional_t>; + +private: + pointer data_; + bounds_type bounds_; + + friend iterator; + friend const_iterator; + +public: + // default constructor - same as constructing from nullptr_t + constexpr multi_span() GSL_NOEXCEPT : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "Default construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + } + + // construct from nullptr - get an empty multi_span + constexpr multi_span(std::nullptr_t) GSL_NOEXCEPT : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "nullptr_t construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + } + + // construct from nullptr with size of 0 (helps with template function calls) + template ::value>> + constexpr multi_span(std::nullptr_t, IntType size) GSL_NOEXCEPT + : multi_span(nullptr, bounds_type{}) + { + static_assert(bounds_type::dynamic_rank != 0 || + (bounds_type::dynamic_rank == 0 && bounds_type::static_size == 0), + "nullptr_t construction of multi_span only possible " + "for dynamic or fixed, zero-length spans."); + Expects(size == 0); + } + + // construct from a single element + constexpr multi_span(reference data) GSL_NOEXCEPT : multi_span(&data, bounds_type{1}) + { + static_assert(bounds_type::dynamic_rank > 0 || bounds_type::static_size == 0 || + bounds_type::static_size == 1, + "Construction from a single element only possible " + "for dynamic or fixed spans of length 0 or 1."); + } + + // prevent constructing from temporaries for single-elements + constexpr multi_span(value_type&&) = delete; + + // construct from pointer + length + constexpr multi_span(pointer ptr, size_type size) GSL_NOEXCEPT + : multi_span(ptr, bounds_type{size}) + { + } + + // construct from pointer + length - multidimensional + constexpr multi_span(pointer data, bounds_type bounds) GSL_NOEXCEPT : data_(data), + bounds_(std::move(bounds)) + { + Expects((bounds_.size() > 0 && data != nullptr) || bounds_.size() == 0); + } + + // construct from begin,end pointer pair + template ::value && + details::LessThan::value>> + constexpr multi_span(pointer begin, Ptr end) + : multi_span(begin, + details::newBoundsHelper(static_cast(end) - begin)) + { + Expects(begin != nullptr && end != nullptr && begin <= static_cast(end)); + } + + // construct from n-dimensions static array + template > + constexpr multi_span(T (&arr)[N]) + : multi_span(reinterpret_cast(arr), bounds_type{typename Helper::bounds_type{}}) + { + static_assert(std::is_convertible::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible::value, + "Cannot construct a multi_span from an array with fewer elements."); + } + + // construct from n-dimensions dynamic array (e.g. new int[m][4]) + // (precedence will be lower than the 1-dimension pointer) + template > + constexpr multi_span(T* const& data, size_type size) + : multi_span(reinterpret_cast(data), typename Helper::bounds_type{size}) + { + static_assert(std::is_convertible::value, + "Cannot convert from source type to target multi_span type."); + } + + // construct from std::array + template + constexpr multi_span(std::array& arr) + : multi_span(arr.data(), bounds_type{static_bounds{}}) + { + static_assert( + std::is_convertible(*)[]>::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible, bounds_type>::value, + "You cannot construct a multi_span from a std::array of smaller size."); + } + + // construct from const std::array + template + constexpr multi_span(const std::array& arr) + : multi_span(arr.data(), bounds_type{static_bounds{}}) + { + static_assert(std::is_convertible(*)[]>::value, + "Cannot convert from source type to target multi_span type."); + static_assert(std::is_convertible, bounds_type>::value, + "You cannot construct a multi_span from a std::array of smaller size."); + } + + // prevent constructing from temporary std::array + template + constexpr multi_span(std::array&& arr) = delete; + + // construct from containers + // future: could use contiguous_iterator_traits to identify only contiguous containers + // type-requirements: container must have .size(), operator[] which are value_type compatible + template ::value && + std::is_convertible::value && + std::is_same().size(), + *std::declval().data())>, + DataType>::value>> + constexpr multi_span(Cont& cont) + : multi_span(static_cast(cont.data()), + details::newBoundsHelper(narrow_cast(cont.size()))) + { + } + + // prevent constructing from temporary containers + template ::value && + std::is_convertible::value && + std::is_same().size(), + *std::declval().data())>, + DataType>::value>> + explicit constexpr multi_span(Cont&& cont) = delete; + + // construct from a convertible multi_span + template , + typename = std::enable_if_t::value && + std::is_convertible::value>> + constexpr multi_span(multi_span other) GSL_NOEXCEPT + : data_(other.data_), + bounds_(other.bounds_) + { + } + + // trivial copy and move + constexpr multi_span(const multi_span&) = default; + constexpr multi_span(multi_span&&) = default; + + // trivial assignment + constexpr multi_span& operator=(const multi_span&) = default; + constexpr multi_span& operator=(multi_span&&) = default; + + // first() - extract the first Count elements into a new multi_span + template + constexpr multi_span first() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + Count <= bounds_type::static_size, + "Count is out of bounds."); + + Expects(bounds_type::static_size != dynamic_range || Count <= this->size()); + return {this->data(), Count}; + } + + // first() - extract the first count elements into a new multi_span + constexpr multi_span first(size_type count) const GSL_NOEXCEPT + { + Expects(count >= 0 && count <= this->size()); + return {this->data(), count}; + } + + // last() - extract the last Count elements into a new multi_span + template + constexpr multi_span last() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + Count <= bounds_type::static_size, + "Count is out of bounds."); + + Expects(bounds_type::static_size != dynamic_range || Count <= this->size()); + return {this->data() + this->size() - Count, Count}; + } + + // last() - extract the last count elements into a new multi_span + constexpr multi_span last(size_type count) const GSL_NOEXCEPT + { + Expects(count >= 0 && count <= this->size()); + return {this->data() + this->size() - count, count}; + } + + // subspan() - create a subview of Count elements starting at Offset + template + constexpr multi_span subspan() const GSL_NOEXCEPT + { + static_assert(Count >= 0, "Count must be >= 0."); + static_assert(Offset >= 0, "Offset must be >= 0."); + static_assert(bounds_type::static_size == dynamic_range || + ((Offset <= bounds_type::static_size) && + Count <= bounds_type::static_size - Offset), + "You must describe a sub-range within bounds of the multi_span."); + + Expects(bounds_type::static_size != dynamic_range || + (Offset <= this->size() && Count <= this->size() - Offset)); + return {this->data() + Offset, Count}; + } + + // subspan() - create a subview of count elements starting at offset + // supplying dynamic_range for count will consume all available elements from offset + constexpr multi_span + subspan(size_type offset, size_type count = dynamic_range) const GSL_NOEXCEPT + { + Expects((offset >= 0 && offset <= this->size()) && + (count == dynamic_range || (count <= this->size() - offset))); + return {this->data() + offset, count == dynamic_range ? this->length() - offset : count}; + } + + // section - creates a non-contiguous, strided multi_span from a contiguous one + constexpr strided_span section(index_type origin, + index_type extents) const GSL_NOEXCEPT + { + size_type size = this->bounds().total_size() - this->bounds().linearize(origin); + return {&this->operator[](origin), size, + strided_bounds{extents, details::make_stride(bounds())}}; + } + + // length of the multi_span in elements + constexpr size_type size() const GSL_NOEXCEPT { return bounds_.size(); } + + // length of the multi_span in elements + constexpr size_type length() const GSL_NOEXCEPT { return this->size(); } + + // length of the multi_span in bytes + constexpr size_type size_bytes() const GSL_NOEXCEPT + { + return narrow_cast(sizeof(value_type)) * this->size(); + } + + // length of the multi_span in bytes + constexpr size_type length_bytes() const GSL_NOEXCEPT { return this->size_bytes(); } + + constexpr bool empty() const GSL_NOEXCEPT { return this->size() == 0; } + + static constexpr std::size_t rank() { return Rank; } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "Dimension should be less than rank (dimension count starts from 0)."); + return bounds_.template extent(); + } + + template + constexpr size_type extent(IntType dim) const GSL_NOEXCEPT + { + return bounds_.extent(dim); + } + + constexpr bounds_type bounds() const GSL_NOEXCEPT { return bounds_; } + + constexpr pointer data() const GSL_NOEXCEPT { return data_; } + + template + constexpr reference operator()(FirstIndex idx) + { + return this->operator[](narrow_cast(idx)); + } + + template + constexpr reference operator()(FirstIndex firstIndex, OtherIndices... indices) + { + index_type idx = {narrow_cast(firstIndex), + narrow_cast(indices)...}; + return this->operator[](idx); + } + + constexpr reference operator[](const index_type& idx) const GSL_NOEXCEPT + { + return data_[bounds_.linearize(idx)]; + } + + template 1), typename Ret = std::enable_if_t> + constexpr Ret operator[](size_type idx) const GSL_NOEXCEPT + { + Expects(idx >= 0 && idx < bounds_.size()); // index is out of bounds of the array + const size_type ridx = idx * bounds_.stride(); + + // index is out of bounds of the underlying data + Expects(ridx < bounds_.total_size()); + return Ret{data_ + ridx, bounds_.slice()}; + } + + constexpr iterator begin() const GSL_NOEXCEPT { return iterator{this, true}; } + + constexpr iterator end() const GSL_NOEXCEPT { return iterator{this, false}; } + + constexpr const_iterator cbegin() const GSL_NOEXCEPT + { + return const_iterator{reinterpret_cast(this), true}; + } + + constexpr const_iterator cend() const GSL_NOEXCEPT + { + return const_iterator{reinterpret_cast(this), false}; + } + + constexpr reverse_iterator rbegin() const GSL_NOEXCEPT { return reverse_iterator{end()}; } + + constexpr reverse_iterator rend() const GSL_NOEXCEPT { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const GSL_NOEXCEPT + { + return const_reverse_iterator{cend()}; + } + + constexpr const_reverse_iterator crend() const GSL_NOEXCEPT + { + return const_reverse_iterator{cbegin()}; + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator==(const multi_span& other) const GSL_NOEXCEPT + { + return bounds_.size() == other.bounds_.size() && + (data_ == other.data_ || std::equal(this->begin(), this->end(), other.begin())); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator!=(const multi_span& other) const GSL_NOEXCEPT + { + return !(*this == other); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<(const multi_span& other) const GSL_NOEXCEPT + { + return std::lexicographical_compare(this->begin(), this->end(), other.begin(), other.end()); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<=(const multi_span& other) const GSL_NOEXCEPT + { + return !(other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>(const multi_span& other) const GSL_NOEXCEPT + { + return (other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>=(const multi_span& other) const GSL_NOEXCEPT + { + return !(*this < other); + } +}; + +// +// Free functions for manipulating spans +// + +// reshape a multi_span into a different dimensionality +// DimCount and Enabled here are workarounds for a bug in MSVC 2015 +template 0), typename = std::enable_if_t> +constexpr auto as_multi_span(SpanType s, Dimensions2... dims) + -> multi_span +{ + static_assert(details::is_multi_span::value, + "Variadic as_multi_span() is for reshaping existing spans."); + using BoundsType = + typename multi_span::bounds_type; + auto tobounds = details::static_as_multi_span_helper(dims..., details::Sep{}); + details::verifyBoundsReshape(s.bounds(), tobounds); + return {s.data(), tobounds}; +} + +// convert a multi_span to a multi_span +template +multi_span as_bytes(multi_span s) GSL_NOEXCEPT +{ + static_assert(std::is_trivial>::value, + "The value_type of multi_span must be a trivial type."); + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// convert a multi_span to a multi_span (a writeable byte multi_span) +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +multi_span as_writeable_bytes(multi_span s) GSL_NOEXCEPT +{ + static_assert(std::is_trivial>::value, + "The value_type of multi_span must be a trivial type."); + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// convert a multi_span to a multi_span +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +constexpr auto +as_multi_span(multi_span s) GSL_NOEXCEPT -> multi_span< + const U, static_cast( + multi_span::bounds_type::static_size != dynamic_range + ? (static_cast( + multi_span::bounds_type::static_size) / + sizeof(U)) + : dynamic_range)> +{ + using ConstByteSpan = multi_span; + static_assert( + std::is_trivial>::value && + (ConstByteSpan::bounds_type::static_size == dynamic_range || + ConstByteSpan::bounds_type::static_size % narrow_cast(sizeof(U)) == 0), + "Target type must be a trivial type and its size must match the byte array size"); + + Expects((s.size_bytes() % narrow_cast(sizeof(U))) == 0 && + (s.size_bytes() / narrow_cast(sizeof(U))) < PTRDIFF_MAX); + return {reinterpret_cast(s.data()), + s.size_bytes() / narrow_cast(sizeof(U))}; +} + +// convert a multi_span to a multi_span +// this is not currently a portable function that can be relied upon to work +// on all implementations. It should be considered an experimental extension +// to the standard GSL interface. +template +constexpr auto as_multi_span(multi_span s) GSL_NOEXCEPT + -> multi_span( + multi_span::bounds_type::static_size != dynamic_range + ? static_cast( + multi_span::bounds_type::static_size) / + sizeof(U) + : dynamic_range)> +{ + using ByteSpan = multi_span; + static_assert( + std::is_trivial>::value && + (ByteSpan::bounds_type::static_size == dynamic_range || + ByteSpan::bounds_type::static_size % sizeof(U) == 0), + "Target type must be a trivial type and its size must match the byte array size"); + + Expects((s.size_bytes() % sizeof(U)) == 0); + return {reinterpret_cast(s.data()), + s.size_bytes() / narrow_cast(sizeof(U))}; +} + +template +constexpr auto as_multi_span(T* const& ptr, dim_t... args) + -> multi_span, Dimensions...> +{ + return {reinterpret_cast*>(ptr), + details::static_as_multi_span_helper>(args..., + details::Sep{})}; +} + +template +constexpr auto as_multi_span(T* arr, std::ptrdiff_t len) -> + typename details::SpanArrayTraits::type +{ + return {reinterpret_cast*>(arr), len}; +} + +template +constexpr auto as_multi_span(T (&arr)[N]) -> typename details::SpanArrayTraits::type +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(const std::array& arr) +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(const std::array&&) = delete; + +template +constexpr multi_span as_multi_span(std::array& arr) +{ + return {arr}; +} + +template +constexpr multi_span as_multi_span(T* begin, T* end) +{ + return {begin, end}; +} + +template +constexpr auto as_multi_span(Cont& arr) -> std::enable_if_t< + !details::is_multi_span>::value, + multi_span, dynamic_range>> +{ + Expects(arr.size() < PTRDIFF_MAX); + return {arr.data(), narrow_cast(arr.size())}; +} + +template +constexpr auto as_multi_span(Cont&& arr) -> std::enable_if_t< + !details::is_multi_span>::value, + multi_span, dynamic_range>> = delete; + +// from basic_string which doesn't have nonconst .data() member like other contiguous containers +template +constexpr auto as_multi_span(std::basic_string& str) + -> multi_span +{ + Expects(str.size() < PTRDIFF_MAX); + return {&str[0], narrow_cast(str.size())}; +} + +// strided_span is an extension that is not strictly part of the GSL at this time. +// It is kept here while the multidimensional interface is still being defined. +template +class strided_span +{ +public: + using bounds_type = strided_bounds; + using size_type = typename bounds_type::size_type; + using index_type = typename bounds_type::index_type; + using value_type = ValueType; + using const_value_type = std::add_const_t; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using iterator = general_span_iterator; + using const_strided_span = strided_span; + using const_iterator = general_span_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + using sliced_type = + std::conditional_t>; + +private: + pointer data_; + bounds_type bounds_; + + friend iterator; + friend const_iterator; + template + friend class strided_span; + +public: + // from raw data + constexpr strided_span(pointer ptr, size_type size, bounds_type bounds) + : data_(ptr), bounds_(std::move(bounds)) + { + Expects((bounds_.size() > 0 && ptr != nullptr) || bounds_.size() == 0); + // Bounds cross data boundaries + Expects(this->bounds().total_size() <= size); + (void) size; + } + + // from static array of size N + template + constexpr strided_span(value_type (&values)[N], bounds_type bounds) + : strided_span(values, N, std::move(bounds)) + { + } + + // from array view + template ::value, + typename = std::enable_if_t> + constexpr strided_span(multi_span av, bounds_type bounds) + : strided_span(av.data(), av.bounds().total_size(), std::move(bounds)) + { + } + + // convertible + template ::value>> + constexpr strided_span(const strided_span& other) + : data_(other.data_), bounds_(other.bounds_) + { + } + + // convert from bytes + template + constexpr strided_span< + typename std::enable_if::value, OtherValueType>::type, + Rank> + as_strided_span() const + { + static_assert((sizeof(OtherValueType) >= sizeof(value_type)) && + (sizeof(OtherValueType) % sizeof(value_type) == 0), + "OtherValueType should have a size to contain a multiple of ValueTypes"); + auto d = narrow_cast(sizeof(OtherValueType) / sizeof(value_type)); + + size_type size = this->bounds().total_size() / d; + return {const_cast(reinterpret_cast(this->data())), + size, + bounds_type{resize_extent(this->bounds().index_bounds(), d), + resize_stride(this->bounds().strides(), d)}}; + } + + constexpr strided_span section(index_type origin, index_type extents) const + { + size_type size = this->bounds().total_size() - this->bounds().linearize(origin); + return {&this->operator[](origin), size, + bounds_type{extents, details::make_stride(bounds())}}; + } + + constexpr reference operator[](const index_type& idx) const + { + return data_[bounds_.linearize(idx)]; + } + + template 1), typename Ret = std::enable_if_t> + constexpr Ret operator[](size_type idx) const + { + Expects(idx < bounds_.size()); // index is out of bounds of the array + const size_type ridx = idx * bounds_.stride(); + + // index is out of bounds of the underlying data + Expects(ridx < bounds_.total_size()); + return {data_ + ridx, bounds_.slice().total_size(), bounds_.slice()}; + } + + constexpr bounds_type bounds() const GSL_NOEXCEPT { return bounds_; } + + template + constexpr size_type extent() const GSL_NOEXCEPT + { + static_assert(Dim < Rank, + "dimension should be less than Rank (dimension count starts from 0)"); + return bounds_.template extent(); + } + + constexpr size_type size() const GSL_NOEXCEPT { return bounds_.size(); } + + constexpr pointer data() const GSL_NOEXCEPT { return data_; } + + constexpr explicit operator bool() const GSL_NOEXCEPT { return data_ != nullptr; } + + constexpr iterator begin() const { return iterator{this, true}; } + + constexpr iterator end() const { return iterator{this, false}; } + + constexpr const_iterator cbegin() const + { + return const_iterator{reinterpret_cast(this), true}; + } + + constexpr const_iterator cend() const + { + return const_iterator{reinterpret_cast(this), false}; + } + + constexpr reverse_iterator rbegin() const { return reverse_iterator{end()}; } + + constexpr reverse_iterator rend() const { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const { return const_reverse_iterator{cend()}; } + + constexpr const_reverse_iterator crend() const { return const_reverse_iterator{cbegin()}; } + + template , std::remove_cv_t>::value>> + constexpr bool + operator==(const strided_span& other) const GSL_NOEXCEPT + { + return bounds_.size() == other.bounds_.size() && + (data_ == other.data_ || std::equal(this->begin(), this->end(), other.begin())); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator!=(const strided_span& other) const GSL_NOEXCEPT + { + return !(*this == other); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<(const strided_span& other) const GSL_NOEXCEPT + { + return std::lexicographical_compare(this->begin(), this->end(), other.begin(), other.end()); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator<=(const strided_span& other) const GSL_NOEXCEPT + { + return !(other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>(const strided_span& other) const GSL_NOEXCEPT + { + return (other < *this); + } + + template , std::remove_cv_t>::value>> + constexpr bool + operator>=(const strided_span& other) const GSL_NOEXCEPT + { + return !(*this < other); + } + +private: + static index_type resize_extent(const index_type& extent, std::ptrdiff_t d) + { + // The last dimension of the array needs to contain a multiple of new type elements + Expects(extent[Rank - 1] >= d && (extent[Rank - 1] % d == 0)); + + index_type ret = extent; + ret[Rank - 1] /= d; + + return ret; + } + + template > + static index_type resize_stride(const index_type& strides, std::ptrdiff_t, void* = nullptr) + { + // Only strided arrays with regular strides can be resized + Expects(strides[Rank - 1] == 1); + + return strides; + } + + template 1), typename = std::enable_if_t> + static index_type resize_stride(const index_type& strides, std::ptrdiff_t d) + { + // Only strided arrays with regular strides can be resized + Expects(strides[Rank - 1] == 1); + // The strides must have contiguous chunks of + // memory that can contain a multiple of new type elements + Expects(strides[Rank - 2] >= d && (strides[Rank - 2] % d == 0)); + + for (std::size_t i = Rank - 1; i > 0; --i) { + // Only strided arrays with regular strides can be resized + Expects((strides[i - 1] >= strides[i]) && (strides[i - 1] % strides[i] == 0)); + } + + index_type ret = strides / d; + ret[Rank - 1] = 1; + + return ret; + } +}; + +template +class contiguous_span_iterator +{ +public: + using iterator_category = std::random_access_iterator_tag; + using value_type = typename Span::value_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + +private: + template + friend class multi_span; + + pointer data_; + const Span* m_validator; + void validateThis() const + { + // iterator is out of range of the array + Expects(data_ >= m_validator->data_ && data_ < m_validator->data_ + m_validator->size()); + } + contiguous_span_iterator(const Span* container, bool isbegin) + : data_(isbegin ? container->data_ : container->data_ + container->size()) + , m_validator(container) + { + } + +public: + reference operator*() const GSL_NOEXCEPT + { + validateThis(); + return *data_; + } + pointer operator->() const GSL_NOEXCEPT + { + validateThis(); + return data_; + } + contiguous_span_iterator& operator++() GSL_NOEXCEPT + { + ++data_; + return *this; + } + contiguous_span_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + contiguous_span_iterator& operator--() GSL_NOEXCEPT + { + --data_; + return *this; + } + contiguous_span_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + contiguous_span_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + contiguous_span_iterator ret{*this}; + return ret += n; + } + contiguous_span_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + data_ += n; + return *this; + } + contiguous_span_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + contiguous_span_iterator ret{*this}; + return ret -= n; + } + contiguous_span_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + difference_type operator-(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ - rhs.data_; + } + reference operator[](difference_type n) const GSL_NOEXCEPT { return *(*this + n); } + bool operator==(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ == rhs.data_; + } + bool operator!=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(*this == rhs); + } + bool operator<(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_validator == rhs.m_validator); + return data_ < rhs.data_; + } + bool operator<=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs < *this); + } + bool operator>(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + bool operator>=(const contiguous_span_iterator& rhs) const GSL_NOEXCEPT + { + return !(rhs > *this); + } + void swap(contiguous_span_iterator& rhs) GSL_NOEXCEPT + { + std::swap(data_, rhs.data_); + std::swap(m_validator, rhs.m_validator); + } +}; + +template +contiguous_span_iterator operator+(typename contiguous_span_iterator::difference_type n, + const contiguous_span_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +template +class general_span_iterator +{ +public: + using iterator_category = std::random_access_iterator_tag; + using value_type = typename Span::value_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + +private: + template + friend class strided_span; + + const Span* m_container; + typename Span::bounds_type::iterator m_itr; + general_span_iterator(const Span* container, bool isbegin) + : m_container(container) + , m_itr(isbegin ? m_container->bounds().begin() : m_container->bounds().end()) + { + } + +public: + reference operator*() GSL_NOEXCEPT { return (*m_container)[*m_itr]; } + pointer operator->() GSL_NOEXCEPT { return &(*m_container)[*m_itr]; } + general_span_iterator& operator++() GSL_NOEXCEPT + { + ++m_itr; + return *this; + } + general_span_iterator operator++(int) GSL_NOEXCEPT + { + auto ret = *this; + ++(*this); + return ret; + } + general_span_iterator& operator--() GSL_NOEXCEPT + { + --m_itr; + return *this; + } + general_span_iterator operator--(int) GSL_NOEXCEPT + { + auto ret = *this; + --(*this); + return ret; + } + general_span_iterator operator+(difference_type n) const GSL_NOEXCEPT + { + general_span_iterator ret{*this}; + return ret += n; + } + general_span_iterator& operator+=(difference_type n) GSL_NOEXCEPT + { + m_itr += n; + return *this; + } + general_span_iterator operator-(difference_type n) const GSL_NOEXCEPT + { + general_span_iterator ret{*this}; + return ret -= n; + } + general_span_iterator& operator-=(difference_type n) GSL_NOEXCEPT { return *this += -n; } + difference_type operator-(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr - rhs.m_itr; + } + value_type operator[](difference_type n) const GSL_NOEXCEPT { return (*m_container)[m_itr[n]]; } + + bool operator==(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr == rhs.m_itr; + } + bool operator!=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(*this == rhs); } + bool operator<(const general_span_iterator& rhs) const GSL_NOEXCEPT + { + Expects(m_container == rhs.m_container); + return m_itr < rhs.m_itr; + } + bool operator<=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(rhs < *this); } + bool operator>(const general_span_iterator& rhs) const GSL_NOEXCEPT { return rhs < *this; } + bool operator>=(const general_span_iterator& rhs) const GSL_NOEXCEPT { return !(rhs > *this); } + void swap(general_span_iterator& rhs) GSL_NOEXCEPT + { + std::swap(m_itr, rhs.m_itr); + std::swap(m_container, rhs.m_container); + } +}; + +template +general_span_iterator operator+(typename general_span_iterator::difference_type n, + const general_span_iterator& rhs) GSL_NOEXCEPT +{ + return rhs + n; +} + +} // namespace gsl + +#undef GSL_NOEXCEPT + +#ifdef _MSC_VER +#if _MSC_VER < 1910 + +#undef constexpr +#pragma pop_macro("constexpr") +#endif // _MSC_VER < 1910 + +#pragma warning(pop) + +#endif // _MSC_VER + +#if __GNUC__ > 6 +#pragma GCC diagnostic pop +#endif // __GNUC__ > 6 + +#endif // GSL_MULTI_SPAN_H diff --git a/extern/include/stduuid/gsl/pointers b/extern/include/stduuid/gsl/pointers new file mode 100644 index 000000000..69499d6fe --- /dev/null +++ b/extern/include/stduuid/gsl/pointers @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_POINTERS_H +#define GSL_POINTERS_H + +#include // for Ensures, Expects + +#include // for forward +#include // for ptrdiff_t, nullptr_t, ostream, size_t +#include // for shared_ptr, unique_ptr +#include // for hash +#include // for enable_if_t, is_convertible, is_assignable + +#if defined(_MSC_VER) && _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // defined(_MSC_VER) && _MSC_VER < 1910 + +namespace gsl +{ + +// +// GSL.owner: ownership pointers +// +using std::unique_ptr; +using std::shared_ptr; + +// +// owner +// +// owner is designed as a bridge for code that must deal directly with owning pointers for some reason +// +// T must be a pointer type +// - disallow construction from any type other than pointer type +// +template ::value>> +using owner = T; + +// +// not_null +// +// Restricts a pointer or smart pointer to only hold non-null values. +// +// Has zero size overhead over T. +// +// If T is a pointer (i.e. T == U*) then +// - allow construction from U* +// - disallow construction from nullptr_t +// - disallow default construction +// - ensure construction from null U* fails +// - allow implicit conversion to U* +// +template +class not_null +{ +public: + static_assert(std::is_assignable::value, "T cannot be assigned nullptr."); + + template ::value>> + constexpr explicit not_null(U&& u) : ptr_(std::forward(u)) + { + Expects(ptr_ != nullptr); + } + + template ::value>> + constexpr explicit not_null(T u) : ptr_(u) + { + Expects(ptr_ != nullptr); + } + + template ::value>> + constexpr not_null(const not_null& other) : not_null(other.get()) + { + } + + not_null(not_null&& other) = default; + not_null(const not_null& other) = default; + not_null& operator=(const not_null& other) = default; + + constexpr T get() const + { + Ensures(ptr_ != nullptr); + return ptr_; + } + + constexpr operator T() const { return get(); } + constexpr T operator->() const { return get(); } + constexpr decltype(auto) operator*() const { return *get(); } + + // prevents compilation when someone attempts to assign a null pointer constant + not_null(std::nullptr_t) = delete; + not_null& operator=(std::nullptr_t) = delete; + + // unwanted operators...pointers only point to single objects! + not_null& operator++() = delete; + not_null& operator--() = delete; + not_null operator++(int) = delete; + not_null operator--(int) = delete; + not_null& operator+=(std::ptrdiff_t) = delete; + not_null& operator-=(std::ptrdiff_t) = delete; + void operator[](std::ptrdiff_t) const = delete; + +private: + T ptr_; +}; + +template +std::ostream& operator<<(std::ostream& os, const not_null& val) +{ + os << val.get(); + return os; +} + +template +auto operator==(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() == rhs.get()) +{ + return lhs.get() == rhs.get(); +} + +template +auto operator!=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() != rhs.get()) +{ + return lhs.get() != rhs.get(); +} + +template +auto operator<(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() < rhs.get()) +{ + return lhs.get() < rhs.get(); +} + +template +auto operator<=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() <= rhs.get()) +{ + return lhs.get() <= rhs.get(); +} + +template +auto operator>(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() > rhs.get()) +{ + return lhs.get() > rhs.get(); +} + +template +auto operator>=(const not_null& lhs, const not_null& rhs) -> decltype(lhs.get() >= rhs.get()) +{ + return lhs.get() >= rhs.get(); +} + +// more unwanted operators +template +std::ptrdiff_t operator-(const not_null&, const not_null&) = delete; +template +not_null operator-(const not_null&, std::ptrdiff_t) = delete; +template +not_null operator+(const not_null&, std::ptrdiff_t) = delete; +template +not_null operator+(std::ptrdiff_t, const not_null&) = delete; + +} // namespace gsl + +namespace std +{ +template +struct hash> +{ + std::size_t operator()(const gsl::not_null& value) const { return hash{}(value); } +}; + +} // namespace std + +#if defined(_MSC_VER) && _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // defined(_MSC_VER) && _MSC_VER < 1910 + +#endif // GSL_POINTERS_H diff --git a/extern/include/stduuid/gsl/span b/extern/include/stduuid/gsl/span new file mode 100644 index 000000000..2fa9cc556 --- /dev/null +++ b/extern/include/stduuid/gsl/span @@ -0,0 +1,766 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_SPAN_H +#define GSL_SPAN_H + +#include // for Expects +#include // for byte +#include // for narrow_cast, narrow + +#include // for lexicographical_compare +#include // for array +#include // for ptrdiff_t, size_t, nullptr_t +#include // for reverse_iterator, distance, random_access_... +#include +#include +#include // for enable_if_t, declval, is_convertible, inte... +#include + +#ifdef _MSC_VER +#pragma warning(push) + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4702) // unreachable code + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ +#define GSL_USE_STATIC_CONSTEXPR_WORKAROUND + +#endif // _MSC_VER < 1910 +#else // _MSC_VER + +// See if we have enough C++17 power to use a static constexpr data member +// without needing an out-of-line definition +#if !(defined(__cplusplus) && (__cplusplus >= 201703L)) +#define GSL_USE_STATIC_CONSTEXPR_WORKAROUND +#endif // !(defined(__cplusplus) && (__cplusplus >= 201703L)) + +#endif // _MSC_VER + +// GCC 7 does not like the signed unsigned missmatch (size_t ptrdiff_t) +// While there is a conversion from signed to unsigned, it happens at +// compiletime, so the compiler wouldn't have to warn indiscriminently, but +// could check if the source value actually doesn't fit into the target type +// and only warn in those cases. +#if __GNUC__ > 6 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +namespace gsl +{ + +// [views.constants], constants +constexpr const std::ptrdiff_t dynamic_extent = -1; + +template +class span; + +// implementation details +namespace details +{ + template + struct is_span_oracle : std::false_type + { + }; + + template + struct is_span_oracle> : std::true_type + { + }; + + template + struct is_span : public is_span_oracle> + { + }; + + template + struct is_std_array_oracle : std::false_type + { + }; + + template + struct is_std_array_oracle> : std::true_type + { + }; + + template + struct is_std_array : public is_std_array_oracle> + { + }; + + template + struct is_allowed_extent_conversion + : public std::integral_constant + { + }; + + template + struct is_allowed_element_type_conversion + : public std::integral_constant::value> + { + }; + + template + class span_iterator + { + using element_type_ = typename Span::element_type; + + public: + +#ifdef _MSC_VER + // Tell Microsoft standard library that span_iterators are checked. + using _Unchecked_type = typename Span::pointer; +#endif + + using iterator_category = std::random_access_iterator_tag; + using value_type = std::remove_cv_t; + using difference_type = typename Span::index_type; + + using reference = std::conditional_t&; + using pointer = std::add_pointer_t; + + span_iterator() = default; + + constexpr span_iterator(const Span* span, typename Span::index_type idx) noexcept + : span_(span), index_(idx) + {} + + friend span_iterator; + template* = nullptr> + constexpr span_iterator(const span_iterator& other) noexcept + : span_iterator(other.span_, other.index_) + { + } + + constexpr reference operator*() const + { + Expects(index_ != span_->size()); + return *(span_->data() + index_); + } + + constexpr pointer operator->() const + { + Expects(index_ != span_->size()); + return span_->data() + index_; + } + + constexpr span_iterator& operator++() + { + Expects(0 <= index_ && index_ != span_->size()); + ++index_; + return *this; + } + + constexpr span_iterator operator++(int) + { + auto ret = *this; + ++(*this); + return ret; + } + + constexpr span_iterator& operator--() + { + Expects(index_ != 0 && index_ <= span_->size()); + --index_; + return *this; + } + + constexpr span_iterator operator--(int) + { + auto ret = *this; + --(*this); + return ret; + } + + constexpr span_iterator operator+(difference_type n) const + { + auto ret = *this; + return ret += n; + } + + friend constexpr span_iterator operator+(difference_type n, span_iterator const& rhs) + { + return rhs + n; + } + + constexpr span_iterator& operator+=(difference_type n) + { + Expects((index_ + n) >= 0 && (index_ + n) <= span_->size()); + index_ += n; + return *this; + } + + constexpr span_iterator operator-(difference_type n) const + { + auto ret = *this; + return ret -= n; + } + + constexpr span_iterator& operator-=(difference_type n) { return *this += -n; } + + constexpr difference_type operator-(span_iterator rhs) const + { + Expects(span_ == rhs.span_); + return index_ - rhs.index_; + } + + constexpr reference operator[](difference_type n) const + { + return *(*this + n); + } + + constexpr friend bool operator==(span_iterator lhs, + span_iterator rhs) noexcept + { + return lhs.span_ == rhs.span_ && lhs.index_ == rhs.index_; + } + + constexpr friend bool operator!=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(lhs == rhs); + } + + constexpr friend bool operator<(span_iterator lhs, + span_iterator rhs) noexcept + { + return lhs.index_ < rhs.index_; + } + + constexpr friend bool operator<=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(rhs < lhs); + } + + constexpr friend bool operator>(span_iterator lhs, + span_iterator rhs) noexcept + { + return rhs < lhs; + } + + constexpr friend bool operator>=(span_iterator lhs, + span_iterator rhs) noexcept + { + return !(rhs > lhs); + } + +#ifdef _MSC_VER + // MSVC++ iterator debugging support; allows STL algorithms in 15.8+ + // to unwrap span_iterator to a pointer type after a range check in STL + // algorithm calls + friend constexpr void _Verify_range(span_iterator lhs, + span_iterator rhs) noexcept + { // test that [lhs, rhs) forms a valid range inside an STL algorithm + Expects(lhs.span_ == rhs.span_ // range spans have to match + && lhs.index_ <= rhs.index_); // range must not be transposed + } + + constexpr void _Verify_offset(const difference_type n) const noexcept + { // test that the iterator *this + n is a valid range in an STL + // algorithm call + Expects((index_ + n) >= 0 && (index_ + n) <= span_->size()); + } + + constexpr pointer _Unwrapped() const noexcept + { // after seeking *this to a high water mark, or using one of the + // _Verify_xxx functions above, unwrap this span_iterator to a raw + // pointer + return span_->data() + index_; + } + + // Tell the STL that span_iterator should not be unwrapped if it can't + // validate in advance, even in release / optimized builds: +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const bool _Unwrap_when_unverified = false; +#else + static constexpr bool _Unwrap_when_unverified = false; +#endif + constexpr void _Seek_to(const pointer p) noexcept + { // adjust the position of *this to previously verified location p + // after _Unwrapped + index_ = p - span_->data(); + } +#endif + + protected: + const Span* span_ = nullptr; + std::ptrdiff_t index_ = 0; + }; + + template + class extent_type + { + public: + using index_type = std::ptrdiff_t; + + static_assert(Ext >= 0, "A fixed-size span must be >= 0 in size."); + + constexpr extent_type() noexcept {} + + template + constexpr extent_type(extent_type ext) + { + static_assert(Other == Ext || Other == dynamic_extent, + "Mismatch between fixed-size extent and size of initializing data."); + Expects(ext.size() == Ext); + } + + constexpr extent_type(index_type size) { Expects(size == Ext); } + + constexpr index_type size() const noexcept { return Ext; } + }; + + template <> + class extent_type + { + public: + using index_type = std::ptrdiff_t; + + template + explicit constexpr extent_type(extent_type ext) : size_(ext.size()) + { + } + + explicit constexpr extent_type(index_type size) : size_(size) { Expects(size >= 0); } + + constexpr index_type size() const noexcept { return size_; } + + private: + index_type size_; + }; + + template + struct calculate_subspan_type + { + using type = span; + }; +} // namespace details + +// [span], class template span +template +class span +{ +public: + // constants and types + using element_type = ElementType; + using value_type = std::remove_cv_t; + using index_type = std::ptrdiff_t; + using pointer = element_type*; + using reference = element_type&; + + using iterator = details::span_iterator, false>; + using const_iterator = details::span_iterator, true>; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + using size_type = index_type; + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const index_type extent { Extent }; +#else + static constexpr index_type extent { Extent }; +#endif + + // [span.cons], span constructors, copy, assignment, and destructor + template " SFINAE, + // since "std::enable_if_t" is ill-formed when Extent is greater than 0. + class = std::enable_if_t<(Dependent || Extent <= 0)>> + constexpr span() noexcept : storage_(nullptr, details::extent_type<0>()) + { + } + + constexpr span(pointer ptr, index_type count) : storage_(ptr, count) {} + + constexpr span(pointer firstElem, pointer lastElem) + : storage_(firstElem, std::distance(firstElem, lastElem)) + { + } + + template + constexpr span(element_type (&arr)[N]) noexcept + : storage_(KnownNotNull{&arr[0]}, details::extent_type()) + { + } + + template > + constexpr span(std::array& arr) noexcept + : storage_(&arr[0], details::extent_type()) + { + } + + template + constexpr span(const std::array, N>& arr) noexcept + : storage_(&arr[0], details::extent_type()) + { + } + + // NB: the SFINAE here uses .data() as a incomplete/imperfect proxy for the requirement + // on Container to be a contiguous sequence container. + template ::value && !details::is_std_array::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr span(Container& cont) : span(cont.data(), narrow(cont.size())) + { + } + + template ::value && !details::is_span::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr span(const Container& cont) : span(cont.data(), narrow(cont.size())) + { + } + + constexpr span(const span& other) noexcept = default; + + template < + class OtherElementType, std::ptrdiff_t OtherExtent, + class = std::enable_if_t< + details::is_allowed_extent_conversion::value && + details::is_allowed_element_type_conversion::value>> + constexpr span(const span& other) + : storage_(other.data(), details::extent_type(other.size())) + { + } + + ~span() noexcept = default; + constexpr span& operator=(const span& other) noexcept = default; + + // [span.sub], span subviews + template + constexpr span first() const + { + Expects(Count >= 0 && Count <= size()); + return {data(), Count}; + } + + template + constexpr span last() const + { + Expects(Count >= 0 && size() - Count >= 0); + return {data() + (size() - Count), Count}; + } + + template + constexpr auto subspan() const -> typename details::calculate_subspan_type::type + { + Expects((Offset >= 0 && size() - Offset >= 0) && + (Count == dynamic_extent || (Count >= 0 && Offset + Count <= size()))); + + return {data() + Offset, Count == dynamic_extent ? size() - Offset : Count}; + } + + constexpr span first(index_type count) const + { + Expects(count >= 0 && count <= size()); + return {data(), count}; + } + + constexpr span last(index_type count) const + { + return make_subspan(size() - count, dynamic_extent, subspan_selector{}); + } + + constexpr span subspan(index_type offset, + index_type count = dynamic_extent) const + { + return make_subspan(offset, count, subspan_selector{}); + } + + + // [span.obs], span observers + constexpr index_type size() const noexcept { return storage_.size(); } + constexpr index_type size_bytes() const noexcept + { + return size() * narrow_cast(sizeof(element_type)); + } + constexpr bool empty() const noexcept { return size() == 0; } + + // [span.elem], span element access + constexpr reference operator[](index_type idx) const + { + Expects(idx >= 0 && idx < storage_.size()); + return data()[idx]; + } + + constexpr reference at(index_type idx) const { return this->operator[](idx); } + constexpr reference operator()(index_type idx) const { return this->operator[](idx); } + constexpr pointer data() const noexcept { return storage_.data(); } + + // [span.iter], span iterator support + constexpr iterator begin() const noexcept { return {this, 0}; } + constexpr iterator end() const noexcept { return {this, size()}; } + + constexpr const_iterator cbegin() const noexcept { return {this, 0}; } + constexpr const_iterator cend() const noexcept { return {this, size()}; } + + constexpr reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; } + constexpr reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; } + + constexpr const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator{cend()}; } + constexpr const_reverse_iterator crend() const noexcept { return const_reverse_iterator{cbegin()}; } + +#ifdef _MSC_VER + // Tell MSVC how to unwrap spans in range-based-for + constexpr pointer _Unchecked_begin() const noexcept { return data(); } + constexpr pointer _Unchecked_end() const noexcept { return data() + size(); } +#endif // _MSC_VER + +private: + + // Needed to remove unnecessary null check in subspans + struct KnownNotNull + { + pointer p; + }; + + // this implementation detail class lets us take advantage of the + // empty base class optimization to pay for only storage of a single + // pointer in the case of fixed-size spans + template + class storage_type : public ExtentType + { + public: + // KnownNotNull parameter is needed to remove unnecessary null check + // in subspans and constructors from arrays + template + constexpr storage_type(KnownNotNull data, OtherExtentType ext) : ExtentType(ext), data_(data.p) + { + Expects(ExtentType::size() >= 0); + } + + + template + constexpr storage_type(pointer data, OtherExtentType ext) : ExtentType(ext), data_(data) + { + Expects(ExtentType::size() >= 0); + Expects(data || ExtentType::size() == 0); + } + + constexpr pointer data() const noexcept { return data_; } + + private: + pointer data_; + }; + + storage_type> storage_; + + // The rest is needed to remove unnecessary null check + // in subspans and constructors from arrays + constexpr span(KnownNotNull ptr, index_type count) : storage_(ptr, count) {} + + template + class subspan_selector {}; + + template + span make_subspan(index_type offset, + index_type count, + subspan_selector) const + { + span tmp(*this); + return tmp.subspan(offset, count); + } + + span make_subspan(index_type offset, + index_type count, + subspan_selector) const + { + Expects(offset >= 0 && size() - offset >= 0); + if (count == dynamic_extent) + { + return { KnownNotNull{ data() + offset }, size() - offset }; + } + + Expects(count >= 0 && size() - offset >= count); + return { KnownNotNull{ data() + offset }, count }; + } +}; + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) +template +constexpr const typename span::index_type span::extent; +#endif + + +// [span.comparison], span comparison operators +template +constexpr bool operator==(span l, + span r) +{ + return std::equal(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator!=(span l, + span r) +{ + return !(l == r); +} + +template +constexpr bool operator<(span l, + span r) +{ + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator<=(span l, + span r) +{ + return !(l > r); +} + +template +constexpr bool operator>(span l, + span r) +{ + return r < l; +} + +template +constexpr bool operator>=(span l, + span r) +{ + return !(l < r); +} + +namespace details +{ + // if we only supported compilers with good constexpr support then + // this pair of classes could collapse down to a constexpr function + + // we should use a narrow_cast<> to go to std::size_t, but older compilers may not see it as + // constexpr + // and so will fail compilation of the template + template + struct calculate_byte_size + : std::integral_constant(sizeof(ElementType) * + static_cast(Extent))> + { + }; + + template + struct calculate_byte_size + : std::integral_constant + { + }; +} + +// [span.objectrep], views of object representation +template +span::value> +as_bytes(span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +template ::value>> +span::value> +as_writeable_bytes(span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// +// make_span() - Utility functions for creating spans +// +template +constexpr span make_span(ElementType* ptr, typename span::index_type count) +{ + return span(ptr, count); +} + +template +constexpr span make_span(ElementType* firstElem, ElementType* lastElem) +{ + return span(firstElem, lastElem); +} + +template +constexpr span make_span(ElementType (&arr)[N]) noexcept +{ + return span(arr); +} + +template +constexpr span make_span(Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(const Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(Ptr& cont, std::ptrdiff_t count) +{ + return span(cont, count); +} + +template +constexpr span make_span(Ptr& cont) +{ + return span(cont); +} + +// Specialization of gsl::at for span +template +constexpr ElementType& at(span s, index i) +{ + // No bounds checking here because it is done in span::operator[] called below + return s[i]; +} + +} // namespace gsl + +#ifdef _MSC_VER +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 + +#pragma warning(pop) +#endif // _MSC_VER + +#if __GNUC__ > 6 +#pragma GCC diagnostic pop +#endif // __GNUC__ > 6 + +#endif // GSL_SPAN_H diff --git a/extern/include/stduuid/gsl/string_span b/extern/include/stduuid/gsl/string_span new file mode 100644 index 000000000..c08f24672 --- /dev/null +++ b/extern/include/stduuid/gsl/string_span @@ -0,0 +1,730 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// 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. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_STRING_SPAN_H +#define GSL_STRING_SPAN_H + +#include // for Ensures, Expects +#include // for narrow_cast +#include // for operator!=, operator==, dynamic_extent + +#include // for equal, lexicographical_compare +#include // for array +#include // for ptrdiff_t, size_t, nullptr_t +#include // for PTRDIFF_MAX +#include +#include // for basic_string, allocator, char_traits +#include // for declval, is_convertible, enable_if_t, add_... + +#ifdef _MSC_VER +#pragma warning(push) + +// blanket turn off warnings from CppCoreCheck for now +// so people aren't annoyed by them when running the tool. +// more targeted suppressions will be added in a future update to the GSL +#pragma warning(disable : 26481 26482 26483 26485 26490 26491 26492 26493 26495) + +#if _MSC_VER < 1910 +#pragma push_macro("constexpr") +#define constexpr /*constexpr*/ + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +// In order to test the library, we need it to throw exceptions that we can catch +#ifdef GSL_THROW_ON_CONTRACT_VIOLATION +#define GSL_NOEXCEPT /*noexcept*/ +#else +#define GSL_NOEXCEPT noexcept +#endif // GSL_THROW_ON_CONTRACT_VIOLATION + +namespace gsl +{ +// +// czstring and wzstring +// +// These are "tag" typedefs for C-style strings (i.e. null-terminated character arrays) +// that allow static analysis to help find bugs. +// +// There are no additional features/semantics that we can find a way to add inside the +// type system for these types that will not either incur significant runtime costs or +// (sometimes needlessly) break existing programs when introduced. +// + +template +using basic_zstring = CharT*; + +template +using czstring = basic_zstring; + +template +using cwzstring = basic_zstring; + +template +using cu16zstring = basic_zstring; + +template +using cu32zstring = basic_zstring; + +template +using zstring = basic_zstring; + +template +using wzstring = basic_zstring; + +template +using u16zstring = basic_zstring; + +template +using u32zstring = basic_zstring; + +namespace details +{ + template + std::ptrdiff_t string_length(const CharT* str, std::ptrdiff_t n) + { + if (str == nullptr || n <= 0) return 0; + + const span str_span{str, n}; + + std::ptrdiff_t len = 0; + while (len < n && str_span[len]) len++; + + return len; + } +} + +// +// ensure_sentinel() +// +// Provides a way to obtain an span from a contiguous sequence +// that ends with a (non-inclusive) sentinel value. +// +// Will fail-fast if sentinel cannot be found before max elements are examined. +// +template +span ensure_sentinel(T* seq, std::ptrdiff_t max = PTRDIFF_MAX) +{ + auto cur = seq; + while ((cur - seq) < max && *cur != Sentinel) ++cur; + Ensures(*cur == Sentinel); + return {seq, cur - seq}; +} + +// +// ensure_z - creates a span for a zero terminated strings. +// Will fail fast if a null-terminator cannot be found before +// the limit of size_type. +// +template +span ensure_z(CharT* const& sz, std::ptrdiff_t max = PTRDIFF_MAX) +{ + return ensure_sentinel(sz, max); +} + +template +span ensure_z(CharT (&sz)[N]) +{ + return ensure_z(&sz[0], static_cast(N)); +} + +template +span::type, dynamic_extent> +ensure_z(Cont& cont) +{ + return ensure_z(cont.data(), static_cast(cont.size())); +} + +template +class basic_string_span; + +namespace details +{ + template + struct is_basic_string_span_oracle : std::false_type + { + }; + + template + struct is_basic_string_span_oracle> : std::true_type + { + }; + + template + struct is_basic_string_span : is_basic_string_span_oracle> + { + }; +} + +// +// string_span and relatives +// +template +class basic_string_span +{ +public: + using element_type = CharT; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_lvalue_reference_t>; + using impl_type = span; + + using index_type = typename impl_type::index_type; + using iterator = typename impl_type::iterator; + using const_iterator = typename impl_type::const_iterator; + using reverse_iterator = typename impl_type::reverse_iterator; + using const_reverse_iterator = typename impl_type::const_reverse_iterator; + + // default (empty) + constexpr basic_string_span() GSL_NOEXCEPT = default; + + // copy + constexpr basic_string_span(const basic_string_span& other) GSL_NOEXCEPT = default; + + // assign + constexpr basic_string_span& operator=(const basic_string_span& other) GSL_NOEXCEPT = default; + + constexpr basic_string_span(pointer ptr, index_type length) : span_(ptr, length) {} + constexpr basic_string_span(pointer firstElem, pointer lastElem) : span_(firstElem, lastElem) {} + + // From static arrays - if 0-terminated, remove 0 from the view + // All other containers allow 0s within the length, so we do not remove them + template + constexpr basic_string_span(element_type (&arr)[N]) : span_(remove_z(arr)) + { + } + + template > + constexpr basic_string_span(std::array& arr) GSL_NOEXCEPT : span_(arr) + { + } + + template > + constexpr basic_string_span(const std::array& arr) GSL_NOEXCEPT + : span_(arr) + { + } + + // Container signature should work for basic_string after C++17 version exists + template + constexpr basic_string_span(std::basic_string& str) + : span_(&str[0], narrow_cast(str.length())) + { + } + + template + constexpr basic_string_span(const std::basic_string& str) + : span_(&str[0], str.length()) + { + } + + // from containers. Containers must have a pointer type and data() function signatures + template ::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr basic_string_span(Container& cont) : span_(cont) + { + } + + template ::value && + std::is_convertible::value && + std::is_convertible().data())>::value>> + constexpr basic_string_span(const Container& cont) : span_(cont) + { + } + + // from string_span + template < + class OtherValueType, std::ptrdiff_t OtherExtent, + class = std::enable_if_t::impl_type, impl_type>::value>> + constexpr basic_string_span(basic_string_span other) + : span_(other.data(), other.length()) + { + } + + template + constexpr basic_string_span first() const + { + return {span_.template first()}; + } + + constexpr basic_string_span first(index_type count) const + { + return {span_.first(count)}; + } + + template + constexpr basic_string_span last() const + { + return {span_.template last()}; + } + + constexpr basic_string_span last(index_type count) const + { + return {span_.last(count)}; + } + + template + constexpr basic_string_span subspan() const + { + return {span_.template subspan()}; + } + + constexpr basic_string_span + subspan(index_type offset, index_type count = dynamic_extent) const + { + return {span_.subspan(offset, count)}; + } + + constexpr reference operator[](index_type idx) const { return span_[idx]; } + constexpr reference operator()(index_type idx) const { return span_[idx]; } + + constexpr pointer data() const { return span_.data(); } + + constexpr index_type length() const GSL_NOEXCEPT { return span_.size(); } + constexpr index_type size() const GSL_NOEXCEPT { return span_.size(); } + constexpr index_type size_bytes() const GSL_NOEXCEPT { return span_.size_bytes(); } + constexpr index_type length_bytes() const GSL_NOEXCEPT { return span_.length_bytes(); } + constexpr bool empty() const GSL_NOEXCEPT { return size() == 0; } + + constexpr iterator begin() const GSL_NOEXCEPT { return span_.begin(); } + constexpr iterator end() const GSL_NOEXCEPT { return span_.end(); } + + constexpr const_iterator cbegin() const GSL_NOEXCEPT { return span_.cbegin(); } + constexpr const_iterator cend() const GSL_NOEXCEPT { return span_.cend(); } + + constexpr reverse_iterator rbegin() const GSL_NOEXCEPT { return span_.rbegin(); } + constexpr reverse_iterator rend() const GSL_NOEXCEPT { return span_.rend(); } + + constexpr const_reverse_iterator crbegin() const GSL_NOEXCEPT { return span_.crbegin(); } + constexpr const_reverse_iterator crend() const GSL_NOEXCEPT { return span_.crend(); } + +private: + static impl_type remove_z(pointer const& sz, std::ptrdiff_t max) + { + return {sz, details::string_length(sz, max)}; + } + + template + static impl_type remove_z(element_type (&sz)[N]) + { + return remove_z(&sz[0], narrow_cast(N)); + } + + impl_type span_; +}; + +template +using string_span = basic_string_span; + +template +using cstring_span = basic_string_span; + +template +using wstring_span = basic_string_span; + +template +using cwstring_span = basic_string_span; + +template +using u16string_span = basic_string_span; + +template +using cu16string_span = basic_string_span; + +template +using u32string_span = basic_string_span; + +template +using cu32string_span = basic_string_span; + +// +// to_string() allow (explicit) conversions from string_span to string +// + +template +std::basic_string::type> +to_string(basic_string_span view) +{ + return {view.data(), static_cast(view.length())}; +} + +template , + typename Allocator = std::allocator, typename gCharT, std::ptrdiff_t Extent> +std::basic_string to_basic_string(basic_string_span view) +{ + return {view.data(), static_cast(view.length())}; +} + +template +basic_string_span::value> +as_bytes(basic_string_span s) noexcept +{ + return { reinterpret_cast(s.data()), s.size_bytes() }; +} + +template ::value>> +basic_string_span::value> +as_writeable_bytes(basic_string_span s) noexcept +{ + return {reinterpret_cast(s.data()), s.size_bytes()}; +} + +// zero-terminated string span, used to convert +// zero-terminated spans to legacy strings +template +class basic_zstring_span +{ +public: + using value_type = CharT; + using const_value_type = std::add_const_t; + + using pointer = std::add_pointer_t; + using const_pointer = std::add_pointer_t; + + using zstring_type = basic_zstring; + using const_zstring_type = basic_zstring; + + using impl_type = span; + using string_span_type = basic_string_span; + + constexpr basic_zstring_span(impl_type s) GSL_NOEXCEPT : span_(s) + { + // expects a zero-terminated span + Expects(s[s.size() - 1] == '\0'); + } + + // copy + constexpr basic_zstring_span(const basic_zstring_span& other) = default; + + // move + constexpr basic_zstring_span(basic_zstring_span&& other) = default; + + // assign + constexpr basic_zstring_span& operator=(const basic_zstring_span& other) = default; + + // move assign + constexpr basic_zstring_span& operator=(basic_zstring_span&& other) = default; + + constexpr bool empty() const GSL_NOEXCEPT { return span_.size() == 0; } + + constexpr string_span_type as_string_span() const GSL_NOEXCEPT + { + auto sz = span_.size(); + return { span_.data(), sz > 1 ? sz - 1 : 0 }; + } + constexpr string_span_type ensure_z() const GSL_NOEXCEPT { return gsl::ensure_z(span_); } + + constexpr const_zstring_type assume_z() const GSL_NOEXCEPT { return span_.data(); } + +private: + impl_type span_; +}; + +template +using zstring_span = basic_zstring_span; + +template +using wzstring_span = basic_zstring_span; + +template +using u16zstring_span = basic_zstring_span; + +template +using u32zstring_span = basic_zstring_span; + +template +using czstring_span = basic_zstring_span; + +template +using cwzstring_span = basic_zstring_span; + +template +using cu16zstring_span = basic_zstring_span; + +template +using cu32zstring_span = basic_zstring_span; + +// operator == +template ::value || + std::is_convertible>>::value>> +bool operator==(const gsl::basic_string_span& one, const T& other) GSL_NOEXCEPT +{ + const gsl::basic_string_span> tmp(other); + return std::equal(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template ::value && + std::is_convertible>>::value>> +bool operator==(const T& one, const gsl::basic_string_span& other) GSL_NOEXCEPT +{ + gsl::basic_string_span> tmp(one); + return std::equal(tmp.begin(), tmp.end(), other.begin(), other.end()); +} + +// operator != +template , Extent>>::value>> +bool operator!=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one == other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator!=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one == other); +} + +// operator< +template , Extent>>::value>> +bool operator<(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + const gsl::basic_string_span, Extent> tmp(other); + return std::lexicographical_compare(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator<(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(one); + return std::lexicographical_compare(tmp.begin(), tmp.end(), other.begin(), other.end()); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(other); + return std::lexicographical_compare(one.begin(), one.end(), tmp.begin(), tmp.end()); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + gsl::basic_string_span, Extent> tmp(one); + return std::lexicographical_compare(tmp.begin(), tmp.end(), other.begin(), other.end()); +} +#endif + +// operator <= +template , Extent>>::value>> +bool operator<=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(other < one); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator<=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(other < one); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(other < one); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator<=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(other < one); +} +#endif + +// operator> +template , Extent>>::value>> +bool operator>(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return other < one; +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator>(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return other < one; +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return other < one; +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return other < one; +} +#endif + +// operator >= +template , Extent>>::value>> +bool operator>=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one < other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename = std::enable_if_t< + std::is_convertible, Extent>>::value && + !gsl::details::is_basic_string_span::value>> +bool operator>=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one < other); +} + +#ifndef _MSC_VER + +// VS treats temp and const containers as convertible to basic_string_span, +// so the cases below are already covered by the previous operators + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>=(gsl::basic_string_span one, const T& other) GSL_NOEXCEPT +{ + return !(one < other); +} + +template < + typename CharT, std::ptrdiff_t Extent = gsl::dynamic_extent, typename T, + typename DataType = typename T::value_type, + typename = std::enable_if_t< + !gsl::details::is_span::value && !gsl::details::is_basic_string_span::value && + std::is_convertible::value && + std::is_same().size(), *std::declval().data())>, + DataType>::value>> +bool operator>=(const T& one, gsl::basic_string_span other) GSL_NOEXCEPT +{ + return !(one < other); +} +#endif +} // namespace gsl + +#undef GSL_NOEXCEPT + +#ifdef _MSC_VER +#pragma warning(pop) + +#if _MSC_VER < 1910 +#undef constexpr +#pragma pop_macro("constexpr") + +#endif // _MSC_VER < 1910 +#endif // _MSC_VER + +#endif // GSL_STRING_SPAN_H diff --git a/extern/include/stduuid/uuid.h b/extern/include/stduuid/uuid.h new file mode 100644 index 000000000..8e06b97e9 --- /dev/null +++ b/extern/include/stduuid/uuid.h @@ -0,0 +1,911 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#pragma comment(lib, "IPHLPAPI.lib") + +#elif defined(__linux__) || defined(__unix__) +#include +#elif defined(__APPLE__) +#include +#endif + +namespace uuids +{ + namespace detail + { + template + constexpr inline unsigned char hex2char(TChar const ch) + { + if (ch >= static_cast('0') && ch <= static_cast('9')) + return ch - static_cast('0'); + if (ch >= static_cast('a') && ch <= static_cast('f')) + return 10 + ch - static_cast('a'); + if (ch >= static_cast('A') && ch <= static_cast('F')) + return 10 + ch - static_cast('A'); + return 0; + } + + template + constexpr inline bool is_hex(TChar const ch) + { + return + (ch >= static_cast('0') && ch <= static_cast('9')) || + (ch >= static_cast('a') && ch <= static_cast('f')) || + (ch >= static_cast('A') && ch <= static_cast('F')); + } + + template + constexpr inline unsigned char hexpair2char(TChar const a, TChar const b) + { + return (hex2char(a) << 4) | hex2char(b); + } + + class sha1 + { + public: + using digest32_t = uint32_t[5]; + using digest8_t = uint8_t[20]; + + static constexpr unsigned int block_bytes = 64; + + inline static uint32_t left_rotate(uint32_t value, size_t const count) + { + return (value << count) ^ (value >> (32 - count)); + } + + sha1() { reset(); } + + void reset() + { + m_digest[0] = 0x67452301; + m_digest[1] = 0xEFCDAB89; + m_digest[2] = 0x98BADCFE; + m_digest[3] = 0x10325476; + m_digest[4] = 0xC3D2E1F0; + m_blockByteIndex = 0; + m_byteCount = 0; + } + + void process_byte(uint8_t octet) + { + this->m_block[this->m_blockByteIndex++] = octet; + ++this->m_byteCount; + if (m_blockByteIndex == block_bytes) + { + this->m_blockByteIndex = 0; + process_block(); + } + } + + void process_block(void const * const start, void const * const end) + { + const uint8_t* begin = static_cast(start); + const uint8_t* finish = static_cast(end); + while (begin != finish) + { + process_byte(*begin); + begin++; + } + } + + void process_bytes(void const * const data, size_t const len) + { + const uint8_t* block = static_cast(data); + process_block(block, block + len); + } + + uint32_t const * get_digest(digest32_t digest) + { + size_t const bitCount = this->m_byteCount * 8; + process_byte(0x80); + if (this->m_blockByteIndex > 56) { + while (m_blockByteIndex != 0) { + process_byte(0); + } + while (m_blockByteIndex < 56) { + process_byte(0); + } + } + else { + while (m_blockByteIndex < 56) { + process_byte(0); + } + } + process_byte(0); + process_byte(0); + process_byte(0); + process_byte(0); + process_byte(static_cast((bitCount >> 24) & 0xFF)); + process_byte(static_cast((bitCount >> 16) & 0xFF)); + process_byte(static_cast((bitCount >> 8) & 0xFF)); + process_byte(static_cast((bitCount) & 0xFF)); + + memcpy(digest, m_digest, 5 * sizeof(uint32_t)); + return digest; + } + + uint8_t const * get_digest_bytes(digest8_t digest) + { + digest32_t d32; + get_digest(d32); + size_t di = 0; + digest[di++] = ((d32[0] >> 24) & 0xFF); + digest[di++] = ((d32[0] >> 16) & 0xFF); + digest[di++] = ((d32[0] >> 8) & 0xFF); + digest[di++] = ((d32[0]) & 0xFF); + + digest[di++] = ((d32[1] >> 24) & 0xFF); + digest[di++] = ((d32[1] >> 16) & 0xFF); + digest[di++] = ((d32[1] >> 8) & 0xFF); + digest[di++] = ((d32[1]) & 0xFF); + + digest[di++] = ((d32[2] >> 24) & 0xFF); + digest[di++] = ((d32[2] >> 16) & 0xFF); + digest[di++] = ((d32[2] >> 8) & 0xFF); + digest[di++] = ((d32[2]) & 0xFF); + + digest[di++] = ((d32[3] >> 24) & 0xFF); + digest[di++] = ((d32[3] >> 16) & 0xFF); + digest[di++] = ((d32[3] >> 8) & 0xFF); + digest[di++] = ((d32[3]) & 0xFF); + + digest[di++] = ((d32[4] >> 24) & 0xFF); + digest[di++] = ((d32[4] >> 16) & 0xFF); + digest[di++] = ((d32[4] >> 8) & 0xFF); + digest[di++] = ((d32[4]) & 0xFF); + + return digest; + } + + private: + void process_block() + { + uint32_t w[80]; + for (size_t i = 0; i < 16; i++) { + w[i] = (m_block[i * 4 + 0] << 24); + w[i] |= (m_block[i * 4 + 1] << 16); + w[i] |= (m_block[i * 4 + 2] << 8); + w[i] |= (m_block[i * 4 + 3]); + } + for (size_t i = 16; i < 80; i++) { + w[i] = left_rotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1); + } + + uint32_t a = m_digest[0]; + uint32_t b = m_digest[1]; + uint32_t c = m_digest[2]; + uint32_t d = m_digest[3]; + uint32_t e = m_digest[4]; + + for (std::size_t i = 0; i < 80; ++i) + { + uint32_t f = 0; + uint32_t k = 0; + + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + uint32_t temp = left_rotate(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = left_rotate(b, 30); + b = a; + a = temp; + } + + m_digest[0] += a; + m_digest[1] += b; + m_digest[2] += c; + m_digest[3] += d; + m_digest[4] += e; + } + + private: + digest32_t m_digest; + uint8_t m_block[64]; + size_t m_blockByteIndex; + size_t m_byteCount; + }; + + static std::mt19937 clock_gen(std::random_device{}()); + static std::uniform_int_distribution clock_dis{ -32768, 32767 }; + static std::atomic_short clock_sequence = clock_dis(clock_gen); + } + + // -------------------------------------------------------------------------------------------------------------------------- + // UUID format https://tools.ietf.org/html/rfc4122 + // -------------------------------------------------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------------------------------------------------- + // Field NDR Data Type Octet # Note + // -------------------------------------------------------------------------------------------------------------------------- + // time_low unsigned long 0 - 3 The low field of the timestamp. + // time_mid unsigned short 4 - 5 The middle field of the timestamp. + // time_hi_and_version unsigned short 6 - 7 The high field of the timestamp multiplexed with the version number. + // clock_seq_hi_and_reserved unsigned small 8 The high field of the clock sequence multiplexed with the variant. + // clock_seq_low unsigned small 9 The low field of the clock sequence. + // node character 10 - 15 The spatially unique node identifier. + // -------------------------------------------------------------------------------------------------------------------------- + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | time_low | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | time_mid | time_hi_and_version | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |clk_seq_hi_res | clk_seq_low | node (0-1) | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | node (2-5) | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // -------------------------------------------------------------------------------------------------------------------------- + // enumerations + // -------------------------------------------------------------------------------------------------------------------------- + + // indicated by a bit pattern in octet 8, marked with N in xxxxxxxx-xxxx-xxxx-Nxxx-xxxxxxxxxxxx + enum class uuid_variant + { + // NCS backward compatibility (with the obsolete Apollo Network Computing System 1.5 UUID format) + // N bit pattern: 0xxx + // > the first 6 octets of the UUID are a 48-bit timestamp (the number of 4 microsecond units of time since 1 Jan 1980 UTC); + // > the next 2 octets are reserved; + // > the next octet is the "address family"; + // > the final 7 octets are a 56-bit host ID in the form specified by the address family + ncs, + + // RFC 4122/DCE 1.1 + // N bit pattern: 10xx + // > big-endian byte order + rfc, + + // Microsoft Corporation backward compatibility + // N bit pattern: 110x + // > little endian byte order + // > formely used in the Component Object Model (COM) library + microsoft, + + // reserved for possible future definition + // N bit pattern: 111x + reserved + }; + + // indicated by a bit pattern in octet 6, marked with M in xxxxxxxx-xxxx-Mxxx-xxxx-xxxxxxxxxxxx + enum class uuid_version + { + none = 0, // only possible for nil or invalid uuids + time_based = 1, // The time-based version specified in RFC 4122 + dce_security = 2, // DCE Security version, with embedded POSIX UIDs. + name_based_md5 = 3, // The name-based version specified in RFS 4122 with MD5 hashing + random_number_based = 4, // The randomly or pseudo-randomly generated version specified in RFS 4122 + name_based_sha1 = 5 // The name-based version specified in RFS 4122 with SHA1 hashing + }; + + // -------------------------------------------------------------------------------------------------------------------------- + // uuid class + // -------------------------------------------------------------------------------------------------------------------------- + class uuid + { + public: + using value_type = uint8_t; + + constexpr uuid() noexcept : data({}) {}; + + uuid(value_type(&arr)[16]) noexcept + { + std::copy(std::cbegin(arr), std::cend(arr), std::begin(data)); + } + + uuid(std::array const & arr) noexcept + { + std::copy(std::cbegin(arr), std::cend(arr), std::begin(data)); + } + + explicit uuid(gsl::span bytes) + { + std::copy(std::cbegin(bytes), std::cend(bytes), std::begin(data)); + } + + template + explicit uuid(ForwardIterator first, ForwardIterator last) + { + if (std::distance(first, last) == 16) + std::copy(first, last, std::begin(data)); + } + + constexpr uuid_variant variant() const noexcept + { + if ((data[8] & 0x80) == 0x00) + return uuid_variant::ncs; + else if ((data[8] & 0xC0) == 0x80) + return uuid_variant::rfc; + else if ((data[8] & 0xE0) == 0xC0) + return uuid_variant::microsoft; + else + return uuid_variant::reserved; + } + + constexpr uuid_version version() const noexcept + { + if ((data[6] & 0xF0) == 0x10) + return uuid_version::time_based; + else if ((data[6] & 0xF0) == 0x20) + return uuid_version::dce_security; + else if ((data[6] & 0xF0) == 0x30) + return uuid_version::name_based_md5; + else if ((data[6] & 0xF0) == 0x40) + return uuid_version::random_number_based; + else if ((data[6] & 0xF0) == 0x50) + return uuid_version::name_based_sha1; + else + return uuid_version::none; + } + + constexpr bool is_nil() const noexcept + { + for (size_t i = 0; i < data.size(); ++i) if (data[i] != 0) return false; + return true; + } + + void swap(uuid & other) noexcept + { + data.swap(other.data); + } + + inline gsl::span as_bytes() const + { + return gsl::span(reinterpret_cast(data.data()), 16); + } + + template + static bool is_valid_uuid(CharT const * str) noexcept + { + CharT digit = 0; + bool firstDigit = true; + int hasBraces = 0; + size_t index = 0; + size_t size = 0; + if constexpr(std::is_same_v) + size = strlen(str); + else + size = wcslen(str); + + if (str == nullptr || size == 0) + return false; + + if (str[0] == static_cast('{')) + hasBraces = 1; + if (hasBraces && str[size - 1] != static_cast('}')) + return false; + + for (size_t i = hasBraces; i < size - hasBraces; ++i) + { + if (str[i] == static_cast('-')) continue; + + if (index >= 16 || !detail::is_hex(str[i])) + { + return false; + } + + if (firstDigit) + { + firstDigit = false; + } + else + { + index++; + firstDigit = true; + } + } + + if (index < 16) + { + return false; + } + + return true; + } + + template, + class Allocator = std::allocator> + static bool is_valid_uuid(std::basic_string const & str) noexcept + { + return is_valid_uuid(str.c_str()); + } + + template + static std::optional from_string(CharT const * str) noexcept + { + CharT digit = 0; + bool firstDigit = true; + int hasBraces = 0; + size_t index = 0; + size_t size = 0; + if constexpr(std::is_same_v) + size = strlen(str); + else + size = wcslen(str); + + std::array data{ { 0 } }; + + if (str == nullptr || size == 0) return {}; + + if (str[0] == static_cast('{')) + hasBraces = 1; + if (hasBraces && str[size - 1] != static_cast('}')) + return {}; + + for (size_t i = hasBraces; i < size - hasBraces; ++i) + { + if (str[i] == static_cast('-')) continue; + + if (index >= 16 || !detail::is_hex(str[i])) + { + return {}; + } + + if (firstDigit) + { + digit = str[i]; + firstDigit = false; + } + else + { + data[index++] = detail::hexpair2char(digit, str[i]); + firstDigit = true; + } + } + + if (index < 16) + { + return {}; + } + + return uuid{ std::cbegin(data), std::cend(data) }; + } + + template, + class Allocator = std::allocator> + static std::optional from_string(std::basic_string const & str) noexcept + { + return from_string(str.c_str()); + } + + private: + std::array data{ { 0 } }; + + friend bool operator==(uuid const & lhs, uuid const & rhs) noexcept; + friend bool operator<(uuid const & lhs, uuid const & rhs) noexcept; + + template + friend std::basic_ostream & operator<<(std::basic_ostream &s, uuid const & id); + }; + + // -------------------------------------------------------------------------------------------------------------------------- + // operators and non-member functions + // -------------------------------------------------------------------------------------------------------------------------- + + inline bool operator== (uuid const& lhs, uuid const& rhs) noexcept + { + return lhs.data == rhs.data; + } + + inline bool operator!= (uuid const& lhs, uuid const& rhs) noexcept + { + return !(lhs == rhs); + } + + inline bool operator< (uuid const& lhs, uuid const& rhs) noexcept + { + return lhs.data < rhs.data; + } + + template + std::basic_ostream & operator<<(std::basic_ostream &s, uuid const & id) + { + return s << std::hex << std::setfill(static_cast('0')) + << std::setw(2) << (int)id.data[0] + << std::setw(2) << (int)id.data[1] + << std::setw(2) << (int)id.data[2] + << std::setw(2) << (int)id.data[3] + << '-' + << std::setw(2) << (int)id.data[4] + << std::setw(2) << (int)id.data[5] + << '-' + << std::setw(2) << (int)id.data[6] + << std::setw(2) << (int)id.data[7] + << '-' + << std::setw(2) << (int)id.data[8] + << std::setw(2) << (int)id.data[9] + << '-' + << std::setw(2) << (int)id.data[10] + << std::setw(2) << (int)id.data[11] + << std::setw(2) << (int)id.data[12] + << std::setw(2) << (int)id.data[13] + << std::setw(2) << (int)id.data[14] + << std::setw(2) << (int)id.data[15]; + } + + template, + class Allocator = std::allocator> + inline std::basic_string to_string(uuid const & id) + { + std::basic_stringstream sstr; + sstr << id; + return sstr.str(); + } + + inline void swap(uuids::uuid & lhs, uuids::uuid & rhs) noexcept + { + lhs.swap(rhs); + } + + // -------------------------------------------------------------------------------------------------------------------------- + // namespace IDs that could be used for generating name-based uuids + // -------------------------------------------------------------------------------------------------------------------------- + + // Name string is a fully-qualified domain name + static uuid uuid_namespace_dns{ {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is a URL + static uuid uuid_namespace_url{ {0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is an ISO OID (See https://oidref.com/, https://en.wikipedia.org/wiki/Object_identifier) + static uuid uuid_namespace_oid{ {0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is an X.500 DN, in DER or a text output format (See https://en.wikipedia.org/wiki/X.500, https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One) + static uuid uuid_namespace_x500{ {0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // -------------------------------------------------------------------------------------------------------------------------- + // uuid generators + // -------------------------------------------------------------------------------------------------------------------------- + + class uuid_system_generator + { + public: + using result_type = uuid; + + uuid operator()() + { +#ifdef _WIN32 + + GUID newId; + ::CoCreateGuid(&newId); + + std::array bytes = + { { + (unsigned char)((newId.Data1 >> 24) & 0xFF), + (unsigned char)((newId.Data1 >> 16) & 0xFF), + (unsigned char)((newId.Data1 >> 8) & 0xFF), + (unsigned char)((newId.Data1) & 0xFF), + + (unsigned char)((newId.Data2 >> 8) & 0xFF), + (unsigned char)((newId.Data2) & 0xFF), + + (unsigned char)((newId.Data3 >> 8) & 0xFF), + (unsigned char)((newId.Data3) & 0xFF), + + newId.Data4[0], + newId.Data4[1], + newId.Data4[2], + newId.Data4[3], + newId.Data4[4], + newId.Data4[5], + newId.Data4[6], + newId.Data4[7] + } }; + + return uuid{ std::begin(bytes), std::end(bytes) }; + +#elif defined(__linux__) || defined(__unix__) + + uuid_t id; + uuid_generate(id); + + std::array bytes = + { { + id[0], + id[1], + id[2], + id[3], + id[4], + id[5], + id[6], + id[7], + id[8], + id[9], + id[10], + id[11], + id[12], + id[13], + id[14], + id[15] + } }; + + return uuid{ std::begin(bytes), std::end(bytes) }; + +#elif defined(__APPLE__) + auto newId = CFUUIDCreate(NULL); + auto bytes = CFUUIDGetUUIDBytes(newId); + CFRelease(newId); + + std::array arrbytes = + { { + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15 + } }; + return uuid{ std::begin(arrbytes), std::end(arrbytes) }; +#elif + return uuid{}; +#endif + } + }; + + template + class basic_uuid_random_generator + { + public: + using engine_type = UniformRandomNumberGenerator; + + explicit basic_uuid_random_generator(engine_type& gen) : + generator(&gen, [](auto) {}) {} + explicit basic_uuid_random_generator(engine_type* gen) : + generator(gen, [](auto) {}) {} + + uuid operator()() + { + uint8_t bytes[16]; + for (int i = 0; i < 16; i += 4) + *reinterpret_cast(bytes + i) = distribution(*generator); + + // variant must be 10xxxxxx + bytes[8] &= 0xBF; + bytes[8] |= 0x80; + + // version must be 0100xxxx + bytes[6] &= 0x4F; + bytes[6] |= 0x40; + + return uuid{std::begin(bytes), std::end(bytes)}; + } + + private: + std::uniform_int_distribution distribution; + std::shared_ptr generator; + }; + + using uuid_random_generator = basic_uuid_random_generator; + + class uuid_name_generator + { + public: + explicit uuid_name_generator(uuid const& namespace_uuid) noexcept + : nsuuid(namespace_uuid) + {} + + template + uuid operator()(CharT const * name) + { + size_t size = 0; + if constexpr (std::is_same_v) + size = strlen(name); + else + size = wcslen(name); + + reset(); + process_characters(name, size); + return make_uuid(); + } + + template, + class Allocator = std::allocator> + uuid operator()(std::basic_string const & name) + { + reset(); + process_characters(name.data(), name.size()); + return make_uuid(); + } + + private: + void reset() + { + hasher.reset(); + std::byte bytes[16]; + auto nsbytes = nsuuid.as_bytes(); + std::copy(std::cbegin(nsbytes), std::cend(nsbytes), bytes); + hasher.process_bytes(bytes, 16); + } + + template ::value>> + void process_characters(char_type const * const characters, size_t const count) + { + for (size_t i = 0; i < count; i++) + { + uint32_t c = characters[i]; + hasher.process_byte(static_cast((c >> 0) & 0xFF)); + hasher.process_byte(static_cast((c >> 8) & 0xFF)); + hasher.process_byte(static_cast((c >> 16) & 0xFF)); + hasher.process_byte(static_cast((c >> 24) & 0xFF)); + } + } + + void process_characters(const char * const characters, size_t const count) + { + hasher.process_bytes(characters, count); + } + + uuid make_uuid() + { + detail::sha1::digest8_t digest; + hasher.get_digest_bytes(digest); + + // variant must be 0b10xxxxxx + digest[8] &= 0xBF; + digest[8] |= 0x80; + + // version must be 0b0101xxxx + digest[6] &= 0x5F; + digest[6] |= 0x50; + + return uuid{ digest, digest + 16 }; + } + + private: + uuid nsuuid; + detail::sha1 hasher; + }; + + // !!! DO NOT USE THIS IN PRODUCTION + // this implementation is unreliable for good uuids + class uuid_time_generator + { + using mac_address = std::array; + + std::optional device_address; + + bool get_mac_address() + { + if (device_address.has_value()) + { + return true; + } + +#ifdef _WIN32 + DWORD len = 0; + auto ret = GetAdaptersInfo(nullptr, &len); + if (ret != ERROR_BUFFER_OVERFLOW) return false; + std::vector buf(len); + auto pips = reinterpret_cast(&buf.front()); + ret = GetAdaptersInfo(pips, &len); + if (ret != ERROR_SUCCESS) return false; + mac_address addr; + std::copy(pips->Address, pips->Address + 6, std::begin(addr)); + device_address = addr; +#endif + + return device_address.has_value(); + } + + long long get_time_intervals() + { + auto start = std::chrono::system_clock::from_time_t(-12219292800); + auto diff = std::chrono::system_clock::now() - start; + auto ns = std::chrono::duration_cast(diff).count(); + return ns / 100; + } + + public: + uuid_time_generator() + { + } + + uuid operator()() + { + if (get_mac_address()) + { + std::array data; + + auto tm = get_time_intervals(); + + short clock_seq = detail::clock_sequence++; + + clock_seq &= 0x3FFF; + + auto ptm = reinterpret_cast(&tm); + ptm[0] &= 0x0F; + + memcpy(&data[0], ptm + 4, 4); + memcpy(&data[4], ptm + 2, 2); + memcpy(&data[6], ptm, 2); + + memcpy(&data[8], reinterpret_cast(&clock_seq), 2); + + // variant must be 0b10xxxxxx + data[8] &= 0xBF; + data[8] |= 0x80; + + // version must be 0b0001xxxx + data[6] &= 0x5F; + data[6] |= 0x10; + + memcpy(&data[10], &device_address.value()[0], 6); + + return uuids::uuid{std::cbegin(data), std::cend(data)}; + } + + return {}; + } + }; +} + +namespace std +{ + template <> + struct hash + { + using argument_type = uuids::uuid; + using result_type = std::size_t; + + result_type operator()(argument_type const &uuid) const + { + std::hash hasher; + return static_cast(hasher(uuids::to_string(uuid))); + } + }; +} diff --git a/extern/include/strict_fstream.hpp b/extern/include/strict_fstream.hpp new file mode 100644 index 000000000..7d03ea664 --- /dev/null +++ b/extern/include/strict_fstream.hpp @@ -0,0 +1,237 @@ +#pragma once + +#include +#include +#include +#include +#include + +/** + * This namespace defines wrappers for std::ifstream, std::ofstream, and + * std::fstream objects. The wrappers perform the following steps: + * - check the open modes make sense + * - check that the call to open() is successful + * - (for input streams) check that the opened file is peek-able + * - turn on the badbit in the exception mask + */ +namespace strict_fstream +{ + +// Help people out a bit, it seems like this is a common recommenation since +// musl breaks all over the place. +#if defined(__NEED_size_t) && !defined(__MUSL__) +#warning "It seems to be recommended to patch in a define for __MUSL__ if you use musl globally: https://www.openwall.com/lists/musl/2013/02/10/5" +#define __MUSL__ +#endif + +// Workaround for broken musl implementation +// Since musl insists that they are perfectly compatible, ironically enough, +// they don't officially have a __musl__ or similar. But __NEED_size_t is defined in their +// relevant header (and not in working implementations), so we can use that. +#ifdef __MUSL__ +#warning "Working around broken strerror_r() implementation in musl, remove when musl is fixed" +#endif + +// Non-gnu variants of strerror_* don't necessarily null-terminate if +// truncating, so we have to do things manually. +inline std::string trim_to_null(const std::vector &buff) +{ + std::string ret(buff.begin(), buff.end()); + + const std::string::size_type pos = ret.find('\0'); + if (pos == std::string::npos) { + ret += " [...]"; // it has been truncated + } else { + ret.resize(pos); + } + return ret; +} + +/// Overload of error-reporting function, to enable use with VS and non-GNU +/// POSIX libc's +/// Ref: +/// - http://stackoverflow.com/a/901316/717706 +static std::string strerror() +{ + // Can't use std::string since we're pre-C++17 + std::vector buff(256, '\0'); + +#ifdef _WIN32 + // Since strerror_s might set errno itself, we need to store it. + const int err_num = errno; + if (strerror_s(buff.data(), buff.size(), err_num) != 0) { + return trim_to_null(buff); + } else { + return "Unknown error (" + std::to_string(err_num) + ")"; + } +#elif ((_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600 || defined(__APPLE__) || defined(__FreeBSD__)) && ! _GNU_SOURCE) || defined(__MUSL__) +// XSI-compliant strerror_r() + const int err_num = errno; // See above + if (strerror_r(err_num, buff.data(), buff.size()) == 0) { + return trim_to_null(buff); + } else { + return "Unknown error (" + std::to_string(err_num) + ")"; + } +#else +// GNU-specific strerror_r() + char * p = strerror_r(errno, &buff[0], buff.size()); + return std::string(p, std::strlen(p)); +#endif +} + +/// Exception class thrown by failed operations. +class Exception + : public std::exception +{ +public: + Exception(const std::string& msg) : _msg(msg) {} + const char * what() const noexcept { return _msg.c_str(); } +private: + std::string _msg; +}; // class Exception + +namespace detail +{ + +struct static_method_holder +{ + static std::string mode_to_string(std::ios_base::openmode mode) + { + static const int n_modes = 6; + static const std::ios_base::openmode mode_val_v[n_modes] = + { + std::ios_base::in, + std::ios_base::out, + std::ios_base::app, + std::ios_base::ate, + std::ios_base::trunc, + std::ios_base::binary + }; + + static const char * mode_name_v[n_modes] = + { + "in", + "out", + "app", + "ate", + "trunc", + "binary" + }; + std::string res; + for (int i = 0; i < n_modes; ++i) + { + if (mode & mode_val_v[i]) + { + res += (! res.empty()? "|" : ""); + res += mode_name_v[i]; + } + } + if (res.empty()) res = "none"; + return res; + } + static void check_mode(const std::string& filename, std::ios_base::openmode mode) + { + if ((mode & std::ios_base::trunc) && ! (mode & std::ios_base::out)) + { + throw Exception(std::string("strict_fstream: open('") + filename + "'): mode error: trunc and not out"); + } + else if ((mode & std::ios_base::app) && ! (mode & std::ios_base::out)) + { + throw Exception(std::string("strict_fstream: open('") + filename + "'): mode error: app and not out"); + } + else if ((mode & std::ios_base::trunc) && (mode & std::ios_base::app)) + { + throw Exception(std::string("strict_fstream: open('") + filename + "'): mode error: trunc and app"); + } + } + static void check_open(std::ios * s_p, const std::string& filename, std::ios_base::openmode mode) + { + if (s_p->fail()) + { + throw Exception(std::string("strict_fstream: open('") + + filename + "'," + mode_to_string(mode) + "): open failed: " + + strerror()); + } + } + static void check_peek(std::istream * is_p, const std::string& filename, std::ios_base::openmode mode) + { + bool peek_failed = true; + try + { + is_p->peek(); + peek_failed = is_p->fail(); + } + catch (const std::ios_base::failure &) {} + if (peek_failed) + { + throw Exception(std::string("strict_fstream: open('") + + filename + "'," + mode_to_string(mode) + "): peek failed: " + + strerror()); + } + is_p->clear(); + } +}; // struct static_method_holder + +} // namespace detail + +class ifstream + : public std::ifstream +{ +public: + ifstream() = default; + ifstream(const std::string& filename, std::ios_base::openmode mode = std::ios_base::in) + { + open(filename, mode); + } + void open(const std::string& filename, std::ios_base::openmode mode = std::ios_base::in) + { + mode |= std::ios_base::in; + exceptions(std::ios_base::badbit); + detail::static_method_holder::check_mode(filename, mode); + std::ifstream::open(filename, mode); + detail::static_method_holder::check_open(this, filename, mode); + detail::static_method_holder::check_peek(this, filename, mode); + } +}; // class ifstream + +class ofstream + : public std::ofstream +{ +public: + ofstream() = default; + ofstream(const std::string& filename, std::ios_base::openmode mode = std::ios_base::out) + { + open(filename, mode); + } + void open(const std::string& filename, std::ios_base::openmode mode = std::ios_base::out) + { + mode |= std::ios_base::out; + exceptions(std::ios_base::badbit); + detail::static_method_holder::check_mode(filename, mode); + std::ofstream::open(filename, mode); + detail::static_method_holder::check_open(this, filename, mode); + } +}; // class ofstream + +class fstream + : public std::fstream +{ +public: + fstream() = default; + fstream(const std::string& filename, std::ios_base::openmode mode = std::ios_base::in) + { + open(filename, mode); + } + void open(const std::string& filename, std::ios_base::openmode mode = std::ios_base::in) + { + if (! (mode & std::ios_base::out)) mode |= std::ios_base::in; + exceptions(std::ios_base::badbit); + detail::static_method_holder::check_mode(filename, mode); + std::fstream::open(filename, mode); + detail::static_method_holder::check_open(this, filename, mode); + detail::static_method_holder::check_peek(this, filename, mode); + } +}; // class fstream + +} // namespace strict_fstream + diff --git a/extern/include/zstr.hpp b/extern/include/zstr.hpp new file mode 100644 index 000000000..bd330ea11 --- /dev/null +++ b/extern/include/zstr.hpp @@ -0,0 +1,502 @@ +//--------------------------------------------------------- +// Copyright 2015 Ontario Institute for Cancer Research +// Written by Matei David (matei@cs.toronto.edu) +//--------------------------------------------------------- + +// Reference: +// http://stackoverflow.com/questions/14086417/how-to-write-custom-input-stream-in-c + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "strict_fstream.hpp" + +#if defined(__GNUC__) && !defined(__clang__) +#if (__GNUC__ > 5) || (__GNUC__ == 5 && __GNUC_MINOR__>0) +#define CAN_MOVE_IOSTREAM +#endif +#else +#define CAN_MOVE_IOSTREAM +#endif + +namespace zstr +{ + +static const std::size_t default_buff_size = static_cast(1 << 20); + +/// Exception class thrown by failed zlib operations. +class Exception + : public std::ios_base::failure +{ +public: + static std::string error_to_message(z_stream * zstrm_p, int ret) + { + std::string msg = "zlib: "; + switch (ret) + { + case Z_STREAM_ERROR: + msg += "Z_STREAM_ERROR: "; + break; + case Z_DATA_ERROR: + msg += "Z_DATA_ERROR: "; + break; + case Z_MEM_ERROR: + msg += "Z_MEM_ERROR: "; + break; + case Z_VERSION_ERROR: + msg += "Z_VERSION_ERROR: "; + break; + case Z_BUF_ERROR: + msg += "Z_BUF_ERROR: "; + break; + default: + std::ostringstream oss; + oss << ret; + msg += "[" + oss.str() + "]: "; + break; + } + if (zstrm_p->msg) { + msg += zstrm_p->msg; + } + msg += " (" + "next_in: " + + std::to_string(uintptr_t(zstrm_p->next_in)) + + ", avail_in: " + + std::to_string(uintptr_t(zstrm_p->avail_in)) + + ", next_out: " + + std::to_string(uintptr_t(zstrm_p->next_out)) + + ", avail_out: " + + std::to_string(uintptr_t(zstrm_p->avail_out)) + + ")"; + return msg; + } + + Exception(z_stream * zstrm_p, int ret) + : std::ios_base::failure(error_to_message(zstrm_p, ret)) + { + } +}; // class Exception + +namespace detail +{ + +class z_stream_wrapper + : public z_stream +{ +public: + z_stream_wrapper(bool _is_input, int _level, int _window_bits) + : is_input(_is_input) + { + this->zalloc = nullptr;//Z_NULL + this->zfree = nullptr;//Z_NULL + this->opaque = nullptr;//Z_NULL + int ret; + if (is_input) + { + this->avail_in = 0; + this->next_in = nullptr;//Z_NULL + ret = inflateInit2(this, _window_bits ? _window_bits : 15+32); + } + else + { + ret = deflateInit2(this, _level, Z_DEFLATED, _window_bits ? _window_bits : 15+16, 8, Z_DEFAULT_STRATEGY); + } + if (ret != Z_OK) throw Exception(this, ret); + } + ~z_stream_wrapper() + { + if (is_input) + { + inflateEnd(this); + } + else + { + deflateEnd(this); + } + } +private: + bool is_input; +}; // class z_stream_wrapper + +} // namespace detail + +class istreambuf + : public std::streambuf +{ +public: + istreambuf(std::streambuf * _sbuf_p, + std::size_t _buff_size = default_buff_size, bool _auto_detect = true, int _window_bits = 0) + : sbuf_p(_sbuf_p), + in_buff(), + in_buff_start(nullptr), + in_buff_end(nullptr), + out_buff(), + zstrm_p(nullptr), + buff_size(_buff_size), + auto_detect(_auto_detect), + auto_detect_run(false), + is_text(false), + window_bits(_window_bits) + { + assert(sbuf_p); + in_buff = std::unique_ptr(new char[buff_size]); + in_buff_start = in_buff.get(); + in_buff_end = in_buff.get(); + out_buff = std::unique_ptr(new char[buff_size]); + setg(out_buff.get(), out_buff.get(), out_buff.get()); + } + + istreambuf(const istreambuf &) = delete; + istreambuf & operator = (const istreambuf &) = delete; + + pos_type seekoff(off_type off, std::ios_base::seekdir dir, + std::ios_base::openmode which) override + { + if (off != 0 || dir != std::ios_base::cur) { + return std::streambuf::seekoff(off, dir, which); + } + + if (!zstrm_p) { + return 0; + } + + return static_cast(zstrm_p->total_out - static_cast(in_avail())); + } + + std::streambuf::int_type underflow() override + { + if (this->gptr() == this->egptr()) + { + // pointers for free region in output buffer + char * out_buff_free_start = out_buff.get(); + int tries = 0; + do + { + if (++tries > 1000) { + throw std::ios_base::failure("Failed to fill buffer after 1000 tries"); + } + + // read more input if none available + if (in_buff_start == in_buff_end) + { + // empty input buffer: refill from the start + in_buff_start = in_buff.get(); + std::streamsize sz = sbuf_p->sgetn(in_buff.get(), static_cast(buff_size)); + in_buff_end = in_buff_start + sz; + if (in_buff_end == in_buff_start) break; // end of input + } + // auto detect if the stream contains text or deflate data + if (auto_detect && ! auto_detect_run) + { + auto_detect_run = true; + unsigned char b0 = *reinterpret_cast< unsigned char * >(in_buff_start); + unsigned char b1 = *reinterpret_cast< unsigned char * >(in_buff_start + 1); + // Ref: + // http://en.wikipedia.org/wiki/Gzip + // http://stackoverflow.com/questions/9050260/what-does-a-zlib-header-look-like + is_text = ! (in_buff_start + 2 <= in_buff_end + && ((b0 == 0x1F && b1 == 0x8B) // gzip header + || (b0 == 0x78 && (b1 == 0x01 // zlib header + || b1 == 0x9C + || b1 == 0xDA)))); + } + if (is_text) + { + // simply swap in_buff and out_buff, and adjust pointers + assert(in_buff_start == in_buff.get()); + std::swap(in_buff, out_buff); + out_buff_free_start = in_buff_end; + in_buff_start = in_buff.get(); + in_buff_end = in_buff.get(); + } + else + { + // run inflate() on input + if (! zstrm_p) zstrm_p = std::unique_ptr(new detail::z_stream_wrapper(true, Z_DEFAULT_COMPRESSION, window_bits)); + zstrm_p->next_in = reinterpret_cast< decltype(zstrm_p->next_in) >(in_buff_start); + zstrm_p->avail_in = uint32_t(in_buff_end - in_buff_start); + zstrm_p->next_out = reinterpret_cast< decltype(zstrm_p->next_out) >(out_buff_free_start); + zstrm_p->avail_out = uint32_t((out_buff.get() + buff_size) - out_buff_free_start); + int ret = inflate(zstrm_p.get(), Z_NO_FLUSH); + // process return code + if (ret != Z_OK && ret != Z_STREAM_END) throw Exception(zstrm_p.get(), ret); + // update in&out pointers following inflate() + in_buff_start = reinterpret_cast< decltype(in_buff_start) >(zstrm_p->next_in); + in_buff_end = in_buff_start + zstrm_p->avail_in; + out_buff_free_start = reinterpret_cast< decltype(out_buff_free_start) >(zstrm_p->next_out); + assert(out_buff_free_start + zstrm_p->avail_out == out_buff.get() + buff_size); + + if (ret == Z_STREAM_END) { + // if stream ended, deallocate inflator + zstrm_p.reset(); + } + } + } while (out_buff_free_start == out_buff.get()); + // 2 exit conditions: + // - end of input: there might or might not be output available + // - out_buff_free_start != out_buff: output available + this->setg(out_buff.get(), out_buff.get(), out_buff_free_start); + } + return this->gptr() == this->egptr() + ? traits_type::eof() + : traits_type::to_int_type(*this->gptr()); + } +private: + std::streambuf * sbuf_p; + std::unique_ptr in_buff; + char * in_buff_start; + char * in_buff_end; + std::unique_ptr out_buff; + std::unique_ptr zstrm_p; + std::size_t buff_size; + bool auto_detect; + bool auto_detect_run; + bool is_text; + int window_bits; + +}; // class istreambuf + +class ostreambuf + : public std::streambuf +{ +public: + ostreambuf(std::streambuf * _sbuf_p, + std::size_t _buff_size = default_buff_size, int _level = Z_DEFAULT_COMPRESSION, int _window_bits = 0) + : sbuf_p(_sbuf_p), + in_buff(), + out_buff(), + zstrm_p(new detail::z_stream_wrapper(false, _level, _window_bits)), + buff_size(_buff_size) + { + assert(sbuf_p); + in_buff = std::unique_ptr(new char[buff_size]); + out_buff = std::unique_ptr(new char[buff_size]); + setp(in_buff.get(), in_buff.get() + buff_size); + } + + ostreambuf(const ostreambuf &) = delete; + ostreambuf & operator = (const ostreambuf &) = delete; + + int deflate_loop(int flush) + { + while (true) + { + zstrm_p->next_out = reinterpret_cast< decltype(zstrm_p->next_out) >(out_buff.get()); + zstrm_p->avail_out = uint32_t(buff_size); + int ret = deflate(zstrm_p.get(), flush); + if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR) { + failed = true; + throw Exception(zstrm_p.get(), ret); + } + std::streamsize sz = sbuf_p->sputn(out_buff.get(), reinterpret_cast< decltype(out_buff.get()) >(zstrm_p->next_out) - out_buff.get()); + if (sz != reinterpret_cast< decltype(out_buff.get()) >(zstrm_p->next_out) - out_buff.get()) + { + // there was an error in the sink stream + return -1; + } + if (ret == Z_STREAM_END || ret == Z_BUF_ERROR || sz == 0) + { + break; + } + } + return 0; + } + + virtual ~ostreambuf() + { + // flush the zlib stream + // + // NOTE: Errors here (sync() return value not 0) are ignored, because we + // cannot throw in a destructor. This mirrors the behaviour of + // std::basic_filebuf::~basic_filebuf(). To see an exception on error, + // close the ofstream with an explicit call to close(), and do not rely + // on the implicit call in the destructor. + // + if (!failed) try { + sync(); + } catch (...) {} + } + std::streambuf::int_type overflow(std::streambuf::int_type c = traits_type::eof()) override + { + zstrm_p->next_in = reinterpret_cast< decltype(zstrm_p->next_in) >(pbase()); + zstrm_p->avail_in = uint32_t(pptr() - pbase()); + while (zstrm_p->avail_in > 0) + { + int r = deflate_loop(Z_NO_FLUSH); + if (r != 0) + { + setp(nullptr, nullptr); + return traits_type::eof(); + } + } + setp(in_buff.get(), in_buff.get() + buff_size); + return traits_type::eq_int_type(c, traits_type::eof()) ? traits_type::eof() : sputc(char_type(c)); + } + int sync() override + { + // first, call overflow to clear in_buff + overflow(); + if (! pptr()) return -1; + // then, call deflate asking to finish the zlib stream + zstrm_p->next_in = nullptr; + zstrm_p->avail_in = 0; + if (deflate_loop(Z_FINISH) != 0) return -1; + deflateReset(zstrm_p.get()); + return 0; + } +private: + std::streambuf * sbuf_p = nullptr; + std::unique_ptr in_buff; + std::unique_ptr out_buff; + std::unique_ptr zstrm_p; + std::size_t buff_size; + bool failed = false; + +}; // class ostreambuf + +class istream + : public std::istream +{ +public: + istream(std::istream & is, + std::size_t _buff_size = default_buff_size, bool _auto_detect = true, int _window_bits = 0) + : std::istream(new istreambuf(is.rdbuf(), _buff_size, _auto_detect, _window_bits)) + { + exceptions(std::ios_base::badbit); + } + explicit istream(std::streambuf * sbuf_p) + : std::istream(new istreambuf(sbuf_p)) + { + exceptions(std::ios_base::badbit); + } + virtual ~istream() + { + delete rdbuf(); + } +}; // class istream + +class ostream + : public std::ostream +{ +public: + ostream(std::ostream & os, + std::size_t _buff_size = default_buff_size, int _level = Z_DEFAULT_COMPRESSION, int _window_bits = 0) + : std::ostream(new ostreambuf(os.rdbuf(), _buff_size, _level, _window_bits)) + { + exceptions(std::ios_base::badbit); + } + explicit ostream(std::streambuf * sbuf_p) + : std::ostream(new ostreambuf(sbuf_p)) + { + exceptions(std::ios_base::badbit); + } + virtual ~ostream() + { + delete rdbuf(); + } +}; // class ostream + +namespace detail +{ + +template < typename FStream_Type > +struct strict_fstream_holder +{ + strict_fstream_holder(const std::string& filename, std::ios_base::openmode mode = std::ios_base::in) + : _fs(filename, mode) + {} + strict_fstream_holder() = default; + FStream_Type _fs {}; +}; // class strict_fstream_holder + +} // namespace detail + +class ifstream + : private detail::strict_fstream_holder< strict_fstream::ifstream >, + public std::istream +{ +public: + explicit ifstream(const std::string filename, std::ios_base::openmode mode = std::ios_base::in, size_t buff_size = default_buff_size) + : detail::strict_fstream_holder< strict_fstream::ifstream >(filename, mode), + std::istream(new istreambuf(_fs.rdbuf(), buff_size)) + { + exceptions(std::ios_base::badbit); + } + explicit ifstream(): detail::strict_fstream_holder< strict_fstream::ifstream >(), std::istream(new istreambuf(_fs.rdbuf())){} + void close() { + _fs.close(); + } + #ifdef CAN_MOVE_IOSTREAM + void open(const std::string filename, std::ios_base::openmode mode = std::ios_base::in) { + _fs.open(filename, mode); + std::istream::operator=(std::istream(new istreambuf(_fs.rdbuf()))); + } + #endif + bool is_open() const { + return _fs.is_open(); + } + virtual ~ifstream() + { + if (_fs.is_open()) close(); + if (rdbuf()) delete rdbuf(); + } + + /// Return the position within the compressed file (wrapped filestream) + std::streampos compressed_tellg() + { + return _fs.tellg(); + } +}; // class ifstream + +class ofstream + : private detail::strict_fstream_holder< strict_fstream::ofstream >, + public std::ostream +{ +public: + explicit ofstream(const std::string filename, std::ios_base::openmode mode = std::ios_base::out, + int level = Z_DEFAULT_COMPRESSION, size_t buff_size = default_buff_size) + : detail::strict_fstream_holder< strict_fstream::ofstream >(filename, mode | std::ios_base::binary), + std::ostream(new ostreambuf(_fs.rdbuf(), buff_size, level)) + { + exceptions(std::ios_base::badbit); + } + explicit ofstream(): detail::strict_fstream_holder< strict_fstream::ofstream >(), std::ostream(new ostreambuf(_fs.rdbuf())){} + void close() { + std::ostream::flush(); + _fs.close(); + } + #ifdef CAN_MOVE_IOSTREAM + void open(const std::string filename, std::ios_base::openmode mode = std::ios_base::out, int level = Z_DEFAULT_COMPRESSION) { + flush(); + _fs.open(filename, mode | std::ios_base::binary); + std::ostream::operator=(std::ostream(new ostreambuf(_fs.rdbuf(), default_buff_size, level))); + } + #endif + bool is_open() const { + return _fs.is_open(); + } + ofstream& flush() { + std::ostream::flush(); + _fs.flush(); + return *this; + } + virtual ~ofstream() + { + if (_fs.is_open()) close(); + if (rdbuf()) delete rdbuf(); + } + + // Return the position within the compressed file (wrapped filestream) + std::streampos compressed_tellp() + { + return _fs.tellp(); + } +}; // class ofstream + +} // namespace zstr + diff --git a/extern/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt new file mode 100644 index 000000000..6bc949b57 --- /dev/null +++ b/extern/quickfuture/CMakeLists.txt @@ -0,0 +1,55 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 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_include_directories(quickfuture PUBLIC src) + +set(QML_FILES + src/qmldir + 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() + + set_target_properties(quickfuture + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture" + ) + + 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) + + # 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) + + 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) + +endif() diff --git a/extern/quickfuture/buildlib/buildlib.pro b/extern/quickfuture/buildlib/buildlib.pro index ec8baf484..952cba9e9 100644 --- a/extern/quickfuture/buildlib/buildlib.pro +++ b/extern/quickfuture/buildlib/buildlib.pro @@ -3,7 +3,7 @@ TEMPLATE = lib CONFIG += plugin isEmpty(SHARED): SHARED = "false" -isEmpty(PLUGIN): PLUGIN = "false" #Install as a QML PLugin +isEmpty(PLUGIN): PLUGIN = "true" #Install as a QML PLugin DEFAULT_INSTALL_ROOT = $$[QT_INSTALL_LIBS] diff --git a/extern/quickfuture/src/qffuture.cpp b/extern/quickfuture/src/qffuture.cpp index 016737db5..fd61522ab 100644 --- a/extern/quickfuture/src/qffuture.cpp +++ b/extern/quickfuture/src/qffuture.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include "qffuture.h" #include "quickfuture.h" @@ -14,6 +16,11 @@ Q_DECLARE_METATYPE(QFuture) Q_DECLARE_METATYPE(QFuture) Q_DECLARE_METATYPE(QFuture) Q_DECLARE_METATYPE(QFuture) +Q_DECLARE_METATYPE(QUrl) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QFuture) +Q_DECLARE_METATYPE(QFuture>) +Q_DECLARE_METATYPE(QFuture) namespace QuickFuture { @@ -263,6 +270,11 @@ static void init() { Future::registerType(); Future::registerType(); Future::registerType(); + + Future::registerType(); + Future::registerType(); + Future::registerType>(); + } #ifndef QUICK_FUTURE_BUILD_PLUGIN diff --git a/extern/quickfuture/src/qmldir b/extern/quickfuture/src/qmldir index 846764825..debb46229 100644 --- a/extern/quickfuture/src/qmldir +++ b/extern/quickfuture/src/qmldir @@ -1,3 +1,3 @@ module QuickFuture -plugin quickfutureqmlplugin +plugin quickfuture typeinfo quickfuture.qmltypes diff --git a/extern/quickpromise/CMakeLists.txt b/extern/quickpromise/CMakeLists.txt index 1e7e5b9eb..2ed083cd7 100644 --- a/extern/quickpromise/CMakeLists.txt +++ b/extern/quickpromise/CMakeLists.txt @@ -1,24 +1,67 @@ -# -# To build it with cmake, you should register qml types explicitly by calling registerQuickFluxQmlTypes() in your main.cpp -# See examples/middleware for example -# +cmake_minimum_required(VERSION 3.14) -cmake_minimum_required(VERSION 3.0.0) -project(quickpromise) +project(quickpromise LANGUAGES CXX) -set(INCLUDE - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/ -) +set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qppromise.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qptimer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/qml/quickpromise.qrc -) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(INSTALL_ROOT ${CMAKE_INSTALL_PREFIX}) -set(HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qppromise.h - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qptimer.h - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/QuickPromise +find_package(Qt5 COMPONENTS Core Quick REQUIRED) + +if(NOT DEFINED STATIC) + set(STATIC OFF) +endif() + +set(QML_FILES + qml/QuickPromise/promise.js + qml/QuickPromise/qmldir + qml/QuickPromise/Promise.qml + qml/QuickPromise/PromiseTimer.qml ) +# Equivalent to QML_IMPORT_PATH +set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${PROJECT_SOURCE_DIR}/qml) + +# Approximates RESOURCES +qt5_add_resources(RESOURCES ${PROJECT_SOURCE_DIR}/qml/quickpromise.qrc) + +# Add the library +if(${STATIC}) + add_library(quickpromise STATIC) +else() + add_library(quickpromise SHARED) +endif() + +target_link_libraries(quickpromise PRIVATE Qt5::Core Qt5::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) +else() + install(TARGETS quickpromise DESTINATION ${INSTALL_ROOT}) + install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/xstudio/plugin/qml/QuickPromise) +endif() + +# QuickPromise needs to be installed somwhere on the QML include search +# paths. xstudio will add /plugin/qml to the +# search paths. Although quickpromise is installed as required when +# 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) +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) +add_dependencies(quickpromise COPY_PROMISE_QML) \ No newline at end of file diff --git a/extern/reproc b/extern/reproc deleted file mode 120000 index c6f6cca2a..000000000 --- a/extern/reproc +++ /dev/null @@ -1 +0,0 @@ -reproc-14.2.4 \ No newline at end of file diff --git a/extern/reproc-14.2.4/.clang-format b/extern/reproc/.clang-format similarity index 100% rename from extern/reproc-14.2.4/.clang-format rename to extern/reproc/.clang-format diff --git a/extern/reproc-14.2.4/.clang-tidy b/extern/reproc/.clang-tidy similarity index 100% rename from extern/reproc-14.2.4/.clang-tidy rename to extern/reproc/.clang-tidy diff --git a/extern/reproc-14.2.4/.editorconfig b/extern/reproc/.editorconfig similarity index 100% rename from extern/reproc-14.2.4/.editorconfig rename to extern/reproc/.editorconfig diff --git a/extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml b/extern/reproc/.github/workflows/codeql-analysis.yml similarity index 100% rename from extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml rename to extern/reproc/.github/workflows/codeql-analysis.yml diff --git a/extern/reproc-14.2.4/.github/workflows/main.yml b/extern/reproc/.github/workflows/main.yml similarity index 100% rename from extern/reproc-14.2.4/.github/workflows/main.yml rename to extern/reproc/.github/workflows/main.yml diff --git a/extern/reproc-14.2.4/.github/workflows/vsenv.ps1 b/extern/reproc/.github/workflows/vsenv.ps1 similarity index 100% rename from extern/reproc-14.2.4/.github/workflows/vsenv.ps1 rename to extern/reproc/.github/workflows/vsenv.ps1 diff --git a/extern/reproc-14.2.4/CHANGELOG.md b/extern/reproc/CHANGELOG.md similarity index 100% rename from extern/reproc-14.2.4/CHANGELOG.md rename to extern/reproc/CHANGELOG.md diff --git a/extern/reproc-14.2.4/CMakeLists.txt b/extern/reproc/CMakeLists.txt similarity index 100% rename from extern/reproc-14.2.4/CMakeLists.txt rename to extern/reproc/CMakeLists.txt diff --git a/extern/reproc-14.2.4/LICENSE b/extern/reproc/LICENSE similarity index 100% rename from extern/reproc-14.2.4/LICENSE rename to extern/reproc/LICENSE diff --git a/extern/reproc-14.2.4/README.md b/extern/reproc/README.md similarity index 100% rename from extern/reproc-14.2.4/README.md rename to extern/reproc/README.md diff --git a/extern/reproc-14.2.4/cmake/reproc.cmake b/extern/reproc/cmake/reproc.cmake similarity index 100% rename from extern/reproc-14.2.4/cmake/reproc.cmake rename to extern/reproc/cmake/reproc.cmake diff --git a/extern/reproc-14.2.4/reproc++/CMakeLists.txt b/extern/reproc/reproc++/CMakeLists.txt similarity index 100% rename from extern/reproc-14.2.4/reproc++/CMakeLists.txt rename to extern/reproc/reproc++/CMakeLists.txt diff --git a/extern/reproc-14.2.4/reproc++/examples/background.cpp b/extern/reproc/reproc++/examples/background.cpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/examples/background.cpp rename to extern/reproc/reproc++/examples/background.cpp diff --git a/extern/reproc-14.2.4/reproc++/examples/drain.cpp b/extern/reproc/reproc++/examples/drain.cpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/examples/drain.cpp rename to extern/reproc/reproc++/examples/drain.cpp diff --git a/extern/reproc-14.2.4/reproc++/examples/forward.cpp b/extern/reproc/reproc++/examples/forward.cpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/examples/forward.cpp rename to extern/reproc/reproc++/examples/forward.cpp diff --git a/extern/reproc-14.2.4/reproc++/examples/run.cpp b/extern/reproc/reproc++/examples/run.cpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/examples/run.cpp rename to extern/reproc/reproc++/examples/run.cpp diff --git a/extern/reproc/reproc++/include/reproc++/arguments.hpp b/extern/reproc/reproc++/include/reproc++/arguments.hpp new file mode 100644 index 000000000..c542fafc0 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/arguments.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +namespace reproc { + +class arguments : public detail::array { +public: + arguments(const char *const *argv) // NOLINT + : detail::array(argv, false) + {} + + /*! + `Arguments` must be iterable as a sequence of strings. Examples of types that + satisfy this requirement are `std::vector` and + `std::array`. + + `arguments` has the same restrictions as `argv` in `reproc_start` except + that it should not end with `NULL` (`start` allocates a new array which + includes the missing `NULL` value). + */ + template > + arguments(const Arguments &arguments) // NOLINT + : detail::array(from(arguments), true) + {} + +private: + template + static const char *const *from(const Arguments &arguments); +}; + +template +const char *const *arguments::from(const Arguments &arguments) +{ + using size_type = typename Arguments::value_type::size_type; + + const char **argv = new const char *[arguments.size() + 1]; + std::size_t current = 0; + + for (const auto &argument : arguments) { + char *string = new char[argument.size() + 1]; + + argv[current++] = string; + + for (size_type i = 0; i < argument.size(); i++) { + *string++ = argument[i]; + } + + *string = '\0'; + } + + argv[current] = nullptr; + + return argv; +} + +} diff --git a/extern/reproc/reproc++/include/reproc++/detail/array.hpp b/extern/reproc/reproc++/include/reproc++/detail/array.hpp new file mode 100644 index 000000000..a4081471a --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/detail/array.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +namespace reproc { +namespace detail { + +class array { + const char *const *data_; + bool owned_; + +public: + array(const char *const *data, bool owned) noexcept + : data_(data), owned_(owned) + {} + + array(array &&other) noexcept : data_(other.data_), owned_(other.owned_) + { + other.data_ = nullptr; + other.owned_ = false; + } + + array &operator=(array &&other) noexcept + { + if (&other != this) { + data_ = other.data_; + owned_ = other.owned_; + other.data_ = nullptr; + other.owned_ = false; + } + + return *this; + } + + ~array() noexcept + { + if (owned_) { + for (size_t i = 0; data_[i] != nullptr; i++) { + delete[] data_[i]; + } + + delete[] data_; + } + } + + const char *const *data() const noexcept + { + return data_; + } +}; + +} +} diff --git a/extern/reproc/reproc++/include/reproc++/detail/type_traits.hpp b/extern/reproc/reproc++/include/reproc++/detail/type_traits.hpp new file mode 100644 index 000000000..553f12755 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/detail/type_traits.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace reproc { +namespace detail { + +template +using enable_if = typename std::enable_if::type; + +template +using is_char_array = std::is_convertible; + +template +using enable_if_not_char_array = enable_if::value>; + +} +} diff --git a/extern/reproc/reproc++/include/reproc++/drain.hpp b/extern/reproc/reproc++/include/reproc++/drain.hpp new file mode 100644 index 000000000..90ad2efa9 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/drain.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include + +#include + +namespace reproc { + +/*! +`reproc_drain` but takes lambdas as sinks. Return an error code from a sink to +break out of `drain` early. `out` and `err` expect the following signature: + +```c++ +std::error_code sink(stream stream, const uint8_t *buffer, size_t size); +``` +*/ +template +std::error_code drain(process &process, Out &&out, Err &&err) +{ + static constexpr uint8_t initial = 0; + std::error_code ec; + + // A single call to `read` might contain multiple messages. By always calling + // both sinks once with no data before reading, we give them the chance to + // process all previous output before reading from the child process again. + + ec = out(stream::in, &initial, 0); + if (ec) { + return ec; + } + + ec = err(stream::in, &initial, 0); + if (ec) { + return ec; + } + + static constexpr size_t BUFFER_SIZE = 4096; + uint8_t buffer[BUFFER_SIZE] = {}; + + for (;;) { + int events = 0; + std::tie(events, ec) = process.poll(event::out | event::err, infinite); + if (ec) { + ec = ec == error::broken_pipe ? std::error_code() : ec; + break; + } + + if (events & event::deadline) { + ec = std::make_error_code(std::errc::timed_out); + break; + } + + stream stream = events & event::out ? stream::out : stream::err; + + size_t bytes_read = 0; + std::tie(bytes_read, ec) = process.read(stream, buffer, BUFFER_SIZE); + if (ec && ec != error::broken_pipe) { + break; + } + + bytes_read = ec == error::broken_pipe ? 0 : bytes_read; + + // This used to be `auto &sink = stream == stream::out ? out : err;` but + // that doesn't actually work if `out` and `err` are not the same type. + if (stream == stream::out) { + ec = out(stream, buffer, bytes_read); + } else { + ec = err(stream, buffer, bytes_read); + } + + if (ec) { + break; + } + } + + return ec; +} + +namespace sink { + +/*! Reads all output into `string`. */ +class string { + std::string &string_; + +public: + explicit string(std::string &string) noexcept : string_(string) {} + + std::error_code operator()(stream stream, const uint8_t *buffer, size_t size) + { + (void) stream; + string_.append(reinterpret_cast(buffer), size); + return {}; + } +}; + +/*! Forwards all output to `ostream`. */ +class ostream { + std::ostream &ostream_; + +public: + explicit ostream(std::ostream &ostream) noexcept : ostream_(ostream) {} + + std::error_code operator()(stream stream, const uint8_t *buffer, size_t size) + { + (void) stream; + ostream_.write(reinterpret_cast(buffer), + static_cast(size)); + return {}; + } +}; + +/*! Discards all output. */ +class discard { +public: + std::error_code + operator()(stream stream, const uint8_t *buffer, size_t size) const noexcept + { + (void) stream; + (void) buffer; + (void) size; + + return {}; + } +}; + +constexpr discard null = discard(); + +namespace thread_safe { + +/*! `sink::string` but locks the given mutex before invoking the sink. */ +class string { + sink::string sink_; + std::mutex &mutex_; + +public: + string(std::string &string, std::mutex &mutex) noexcept + : sink_(string), mutex_(mutex) + {} + + std::error_code operator()(stream stream, const uint8_t *buffer, size_t size) + { + std::lock_guard lock(mutex_); + return sink_(stream, buffer, size); + } +}; + +} + +} +} diff --git a/extern/reproc/reproc++/include/reproc++/env.hpp b/extern/reproc/reproc++/include/reproc++/env.hpp new file mode 100644 index 000000000..144f41dc9 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/env.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +namespace reproc { + +class env : public detail::array { +public: + enum type { + extend, + empty, + }; + + env(const char *const *envp = nullptr) // NOLINT + : detail::array(envp, false) + {} + + /*! + `Env` must be iterable as a sequence of string pairs. Examples of + types that satisfy this requirement are `std::vector>` and `std::map`. + + The pairs in `env` represent the extra environment variables of the child + process and are converted to the right format before being passed as the + environment to `reproc_start` via the `env.extra` field of `reproc_options`. + */ + template > + env(const Env &env) // NOLINT + : detail::array(from(env), true) + {} + +private: + template + static const char *const *from(const Env &env); +}; + +template +const char *const *env::from(const Env &env) +{ + using name_size_type = typename Env::value_type::first_type::size_type; + using value_size_type = typename Env::value_type::second_type::size_type; + + const char **envp = new const char *[env.size() + 1]; + std::size_t current = 0; + + for (const auto &entry : env) { + const auto &name = entry.first; + const auto &value = entry.second; + + // We add 2 to the size to reserve space for the '=' sign and the NUL + // terminator at the end of the string. + char *string = new char[name.size() + value.size() + 2]; + + envp[current++] = string; + + for (name_size_type i = 0; i < name.size(); i++) { + *string++ = name[i]; + } + + *string++ = '='; + + for (value_size_type i = 0; i < value.size(); i++) { + *string++ = value[i]; + } + + *string = '\0'; + } + + envp[current] = nullptr; + + return envp; +} + +} diff --git a/extern/reproc/reproc++/include/reproc++/export.hpp b/extern/reproc/reproc++/include/reproc++/export.hpp new file mode 100644 index 000000000..3eb0af0e6 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/export.hpp @@ -0,0 +1,21 @@ +#pragma once + +#ifndef REPROCXX_EXPORT + #ifdef _WIN32 + #ifdef REPROCXX_SHARED + #ifdef REPROCXX_BUILDING + #define REPROCXX_EXPORT __declspec(dllexport) + #else + #define REPROCXX_EXPORT __declspec(dllimport) + #endif + #else + #define REPROCXX_EXPORT + #endif + #else + #ifdef REPROCXX_BUILDING + #define REPROCXX_EXPORT __attribute__((visibility("default"))) + #else + #define REPROCXX_EXPORT + #endif + #endif +#endif diff --git a/extern/reproc/reproc++/include/reproc++/input.hpp b/extern/reproc/reproc++/include/reproc++/input.hpp new file mode 100644 index 000000000..e69049d26 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/input.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace reproc { + +class input { + const uint8_t *data_ = nullptr; + size_t size_ = 0; + +public: + input() = default; + + input(const uint8_t *data, size_t size) : data_(data), size_(size) {} + + /*! Implicitly convert from string literals. */ + template + input(const char (&data)[N]) // NOLINT + : data_(reinterpret_cast(data)), size_(N) + {} + + input(const input &other) = default; + input &operator=(const input &) = default; + + const uint8_t *data() const noexcept + { + return data_; + } + + size_t size() const noexcept + { + return size_; + } +}; + +} diff --git a/extern/reproc/reproc++/include/reproc++/reproc.hpp b/extern/reproc/reproc++/include/reproc++/reproc.hpp new file mode 100644 index 000000000..ab6f1394a --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/reproc.hpp @@ -0,0 +1,223 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Forward declare `reproc_t` so we don't have to include reproc.h in the +// header. +struct reproc_t; + +/*! The `reproc` namespace wraps all reproc++ declarations. `process` wraps +reproc's API inside a C++ class. To avoid exposing reproc's API when using +reproc++ all structs, enums and constants of reproc have a replacement in +reproc++. Only differences in behaviour compared to reproc are documented. Refer +to reproc.h and the examples for general information on how to use reproc. */ +namespace reproc { + +/*! Conversion from reproc `errno` constants to `std::errc` constants: +https://en.cppreference.com/w/cpp/error/errc */ +using error = std::errc; + +namespace signal { + +REPROCXX_EXPORT extern const int kill; +REPROCXX_EXPORT extern const int terminate; + +} + +/*! Timeout values are passed as `reproc::milliseconds` instead of `int` in +reproc++. */ +using milliseconds = std::chrono::duration; + +REPROCXX_EXPORT extern const milliseconds infinite; +REPROCXX_EXPORT extern const milliseconds deadline; + +enum class stop { + noop, + wait, + terminate, + kill, +}; + +struct stop_action { + stop action; + milliseconds timeout; +}; + +struct stop_actions { + stop_action first; + stop_action second; + stop_action third; +}; + +#if defined(_WIN32) +using handle = void *; +#else +using handle = int; +#endif + +struct redirect { + enum type { + default_, // Unfortunately, both `default` and `auto` are keywords. + pipe, + parent, + discard, + // stdout would conflict with a macro on Windows. + stdout_, + // Unfortunately, class members and nested enum members can't have the same + // name. + handle_, + file_, + path_, + }; + + enum type type; + reproc::handle handle; + FILE *file; + const char *path; +}; + +struct options { + struct { + env::type behavior; + /*! Implicitly converts from any STL container of string pairs to the + environment format expected by `reproc_start`. */ + class env extra; + } env = {}; + + const char *working_directory = nullptr; + + struct { + redirect in; + redirect out; + redirect err; + bool parent; + bool discard; + FILE *file; + const char *path; + } redirect = {}; + + struct stop_actions stop = {}; + reproc::milliseconds timeout = reproc::milliseconds(0); + reproc::milliseconds deadline = reproc::milliseconds(0); + /*! Implicitly converts from string literals to the pointer size pair expected + by `reproc_start`. */ + class input input; + bool nonblocking = false; + + /*! Make a shallow copy of `options`. */ + static options clone(const options &other) + { + struct options clone; + clone.env.behavior = other.env.behavior; + // Make sure we make a shallow copy of `environment`. + clone.env.extra = other.env.extra.data(); + clone.working_directory = other.working_directory; + clone.redirect = other.redirect; + clone.stop = other.stop; + clone.timeout = other.timeout; + clone.deadline = other.deadline; + clone.input = other.input; + + return clone; + } +}; + +enum class stream { + in, + out, + err, +}; + +class process; + +namespace event { + +enum { + in = 1 << 0, + out = 1 << 1, + err = 1 << 2, + exit = 1 << 3, + deadline = 1 << 4, +}; + +struct source { + class process &process; + int interests; + int events; +}; + +} + +REPROCXX_EXPORT std::error_code poll(event::source *sources, + size_t num_sources, + milliseconds timeout = infinite); + +/*! Improves on reproc's API by adding RAII and changing the API of some +functions to be more idiomatic C++. */ +class process { + +public: + REPROCXX_EXPORT process(); + REPROCXX_EXPORT ~process() noexcept; + + // Enforce unique ownership of child processes. + REPROCXX_EXPORT process(process &&other) noexcept; + REPROCXX_EXPORT process &operator=(process &&other) noexcept; + + /*! `reproc_start` but implicitly converts from STL containers to the + arguments format expected by `reproc_start`. */ + REPROCXX_EXPORT std::error_code start(const arguments &arguments, + const options &options = {}) noexcept; + + REPROCXX_EXPORT std::pair pid() noexcept; + + /*! Sets the `fork` option in `reproc_options` and calls `start`. Returns + `true` in the child process and `false` in the parent process. */ + REPROCXX_EXPORT std::pair + fork(const options &options = {}) noexcept; + + /*! Shorthand for `reproc::poll` that only polls this process. Returns a pair + of (events, error). */ + REPROCXX_EXPORT std::pair + poll(int interests, milliseconds timeout = infinite); + + /*! `reproc_read` but returns a pair of (bytes read, error). */ + REPROCXX_EXPORT std::pair + read(stream stream, uint8_t *buffer, size_t size) noexcept; + + /*! reproc_write` but returns a pair of (bytes_written, error). */ + REPROCXX_EXPORT std::pair + write(const uint8_t *buffer, size_t size) noexcept; + + REPROCXX_EXPORT std::error_code close(stream stream) noexcept; + + /*! `reproc_wait` but returns a pair of (status, error). */ + REPROCXX_EXPORT std::pair + wait(milliseconds timeout) noexcept; + + REPROCXX_EXPORT std::error_code terminate() noexcept; + + REPROCXX_EXPORT std::error_code kill() noexcept; + + /*! `reproc_stop` but returns a pair of (status, error). */ + REPROCXX_EXPORT std::pair + stop(stop_actions stop) noexcept; + +private: + REPROCXX_EXPORT friend std::error_code + poll(event::source *sources, size_t num_sources, milliseconds timeout); + + std::unique_ptr impl_; +}; + +} diff --git a/extern/reproc/reproc++/include/reproc++/run.hpp b/extern/reproc/reproc++/include/reproc++/run.hpp new file mode 100644 index 000000000..196121f72 --- /dev/null +++ b/extern/reproc/reproc++/include/reproc++/run.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +namespace reproc { + +template +std::pair +run(const arguments &arguments, const options &options, Out &&out, Err &&err) +{ + process process; + std::error_code ec; + + ec = process.start(arguments, options); + if (ec) { + return { -1, ec }; + } + + ec = drain(process, std::forward(out), std::forward(err)); + if (ec) { + return { -1, ec }; + } + + return process.stop(options.stop); +} + +inline std::pair run(const arguments &arguments, + const options &options = {}) +{ + struct options modified = options::clone(options); + + if (!options.redirect.discard && options.redirect.file == nullptr && + options.redirect.path == nullptr) { + modified.redirect.parent = true; + } + + return run(arguments, modified, sink::null, sink::null); +} + +} diff --git a/extern/reproc-14.2.4/reproc++/reproc++-config.cmake.in b/extern/reproc/reproc++/reproc++-config.cmake.in similarity index 100% rename from extern/reproc-14.2.4/reproc++/reproc++-config.cmake.in rename to extern/reproc/reproc++/reproc++-config.cmake.in diff --git a/extern/reproc-14.2.4/reproc++/reproc++.pc.in b/extern/reproc/reproc++/reproc++.pc.in similarity index 100% rename from extern/reproc-14.2.4/reproc++/reproc++.pc.in rename to extern/reproc/reproc++/reproc++.pc.in diff --git a/extern/reproc-14.2.4/reproc++/src/reproc.cpp b/extern/reproc/reproc++/src/reproc.cpp similarity index 100% rename from extern/reproc-14.2.4/reproc++/src/reproc.cpp rename to extern/reproc/reproc++/src/reproc.cpp diff --git a/extern/reproc-14.2.4/reproc/CMakeLists.txt b/extern/reproc/reproc/CMakeLists.txt similarity index 100% rename from extern/reproc-14.2.4/reproc/CMakeLists.txt rename to extern/reproc/reproc/CMakeLists.txt diff --git a/extern/reproc-14.2.4/reproc/examples/drain.c b/extern/reproc/reproc/examples/drain.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/drain.c rename to extern/reproc/reproc/examples/drain.c diff --git a/extern/reproc-14.2.4/reproc/examples/env.c b/extern/reproc/reproc/examples/env.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/env.c rename to extern/reproc/reproc/examples/env.c diff --git a/extern/reproc-14.2.4/reproc/examples/parent.c b/extern/reproc/reproc/examples/parent.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/parent.c rename to extern/reproc/reproc/examples/parent.c diff --git a/extern/reproc-14.2.4/reproc/examples/path.c b/extern/reproc/reproc/examples/path.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/path.c rename to extern/reproc/reproc/examples/path.c diff --git a/extern/reproc-14.2.4/reproc/examples/poll.c b/extern/reproc/reproc/examples/poll.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/poll.c rename to extern/reproc/reproc/examples/poll.c diff --git a/extern/reproc-14.2.4/reproc/examples/read.c b/extern/reproc/reproc/examples/read.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/read.c rename to extern/reproc/reproc/examples/read.c diff --git a/extern/reproc-14.2.4/reproc/examples/run.c b/extern/reproc/reproc/examples/run.c similarity index 100% rename from extern/reproc-14.2.4/reproc/examples/run.c rename to extern/reproc/reproc/examples/run.c diff --git a/extern/reproc-14.2.4/reproc/include/reproc/drain.h b/extern/reproc/reproc/include/reproc/drain.h similarity index 100% rename from extern/reproc-14.2.4/reproc/include/reproc/drain.h rename to extern/reproc/reproc/include/reproc/drain.h diff --git a/extern/reproc-14.2.4/reproc/include/reproc/export.h b/extern/reproc/reproc/include/reproc/export.h similarity index 100% rename from extern/reproc-14.2.4/reproc/include/reproc/export.h rename to extern/reproc/reproc/include/reproc/export.h diff --git a/extern/reproc-14.2.4/reproc/include/reproc/reproc.h b/extern/reproc/reproc/include/reproc/reproc.h similarity index 100% rename from extern/reproc-14.2.4/reproc/include/reproc/reproc.h rename to extern/reproc/reproc/include/reproc/reproc.h diff --git a/extern/reproc-14.2.4/reproc/include/reproc/run.h b/extern/reproc/reproc/include/reproc/run.h similarity index 100% rename from extern/reproc-14.2.4/reproc/include/reproc/run.h rename to extern/reproc/reproc/include/reproc/run.h diff --git a/extern/reproc-14.2.4/reproc/reproc-config.cmake.in b/extern/reproc/reproc/reproc-config.cmake.in similarity index 100% rename from extern/reproc-14.2.4/reproc/reproc-config.cmake.in rename to extern/reproc/reproc/reproc-config.cmake.in diff --git a/extern/reproc-14.2.4/reproc/reproc.pc.in b/extern/reproc/reproc/reproc.pc.in similarity index 100% rename from extern/reproc-14.2.4/reproc/reproc.pc.in rename to extern/reproc/reproc/reproc.pc.in diff --git a/extern/reproc-14.2.4/reproc/resources/argv.c b/extern/reproc/reproc/resources/argv.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/argv.c rename to extern/reproc/reproc/resources/argv.c diff --git a/extern/reproc-14.2.4/reproc/resources/deadline.c b/extern/reproc/reproc/resources/deadline.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/deadline.c rename to extern/reproc/reproc/resources/deadline.c diff --git a/extern/reproc-14.2.4/reproc/resources/env.c b/extern/reproc/reproc/resources/env.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/env.c rename to extern/reproc/reproc/resources/env.c diff --git a/extern/reproc-14.2.4/reproc/resources/io.c b/extern/reproc/reproc/resources/io.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/io.c rename to extern/reproc/reproc/resources/io.c diff --git a/extern/reproc-14.2.4/reproc/resources/overflow.c b/extern/reproc/reproc/resources/overflow.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/overflow.c rename to extern/reproc/reproc/resources/overflow.c diff --git a/extern/reproc-14.2.4/reproc/resources/path.c b/extern/reproc/reproc/resources/path.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/path.c rename to extern/reproc/reproc/resources/path.c diff --git a/extern/reproc-14.2.4/reproc/resources/pid.c b/extern/reproc/reproc/resources/pid.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/pid.c rename to extern/reproc/reproc/resources/pid.c diff --git a/extern/reproc-14.2.4/reproc/resources/sleep.h b/extern/reproc/reproc/resources/sleep.h similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/sleep.h rename to extern/reproc/reproc/resources/sleep.h diff --git a/extern/reproc-14.2.4/reproc/resources/stop.c b/extern/reproc/reproc/resources/stop.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/stop.c rename to extern/reproc/reproc/resources/stop.c diff --git a/extern/reproc-14.2.4/reproc/resources/working-directory.c b/extern/reproc/reproc/resources/working-directory.c similarity index 100% rename from extern/reproc-14.2.4/reproc/resources/working-directory.c rename to extern/reproc/reproc/resources/working-directory.c diff --git a/extern/reproc-14.2.4/reproc/src/clock.h b/extern/reproc/reproc/src/clock.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/clock.h rename to extern/reproc/reproc/src/clock.h diff --git a/extern/reproc-14.2.4/reproc/src/clock.posix.c b/extern/reproc/reproc/src/clock.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/clock.posix.c rename to extern/reproc/reproc/src/clock.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/clock.windows.c b/extern/reproc/reproc/src/clock.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/clock.windows.c rename to extern/reproc/reproc/src/clock.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/drain.c b/extern/reproc/reproc/src/drain.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/drain.c rename to extern/reproc/reproc/src/drain.c diff --git a/extern/reproc-14.2.4/reproc/src/error.h b/extern/reproc/reproc/src/error.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/error.h rename to extern/reproc/reproc/src/error.h diff --git a/extern/reproc-14.2.4/reproc/src/error.posix.c b/extern/reproc/reproc/src/error.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/error.posix.c rename to extern/reproc/reproc/src/error.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/error.windows.c b/extern/reproc/reproc/src/error.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/error.windows.c rename to extern/reproc/reproc/src/error.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/handle.h b/extern/reproc/reproc/src/handle.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/handle.h rename to extern/reproc/reproc/src/handle.h diff --git a/extern/reproc-14.2.4/reproc/src/handle.posix.c b/extern/reproc/reproc/src/handle.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/handle.posix.c rename to extern/reproc/reproc/src/handle.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/handle.windows.c b/extern/reproc/reproc/src/handle.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/handle.windows.c rename to extern/reproc/reproc/src/handle.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/init.h b/extern/reproc/reproc/src/init.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/init.h rename to extern/reproc/reproc/src/init.h diff --git a/extern/reproc-14.2.4/reproc/src/init.posix.c b/extern/reproc/reproc/src/init.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/init.posix.c rename to extern/reproc/reproc/src/init.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/init.windows.c b/extern/reproc/reproc/src/init.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/init.windows.c rename to extern/reproc/reproc/src/init.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/macro.h b/extern/reproc/reproc/src/macro.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/macro.h rename to extern/reproc/reproc/src/macro.h diff --git a/extern/reproc-14.2.4/reproc/src/options.c b/extern/reproc/reproc/src/options.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/options.c rename to extern/reproc/reproc/src/options.c diff --git a/extern/reproc-14.2.4/reproc/src/options.h b/extern/reproc/reproc/src/options.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/options.h rename to extern/reproc/reproc/src/options.h diff --git a/extern/reproc-14.2.4/reproc/src/pipe.h b/extern/reproc/reproc/src/pipe.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/pipe.h rename to extern/reproc/reproc/src/pipe.h diff --git a/extern/reproc-14.2.4/reproc/src/pipe.posix.c b/extern/reproc/reproc/src/pipe.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/pipe.posix.c rename to extern/reproc/reproc/src/pipe.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/pipe.windows.c b/extern/reproc/reproc/src/pipe.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/pipe.windows.c rename to extern/reproc/reproc/src/pipe.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/process.h b/extern/reproc/reproc/src/process.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/process.h rename to extern/reproc/reproc/src/process.h diff --git a/extern/reproc-14.2.4/reproc/src/process.posix.c b/extern/reproc/reproc/src/process.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/process.posix.c rename to extern/reproc/reproc/src/process.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/process.windows.c b/extern/reproc/reproc/src/process.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/process.windows.c rename to extern/reproc/reproc/src/process.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/redirect.c b/extern/reproc/reproc/src/redirect.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/redirect.c rename to extern/reproc/reproc/src/redirect.c diff --git a/extern/reproc-14.2.4/reproc/src/redirect.h b/extern/reproc/reproc/src/redirect.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/redirect.h rename to extern/reproc/reproc/src/redirect.h diff --git a/extern/reproc-14.2.4/reproc/src/redirect.posix.c b/extern/reproc/reproc/src/redirect.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/redirect.posix.c rename to extern/reproc/reproc/src/redirect.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/redirect.windows.c b/extern/reproc/reproc/src/redirect.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/redirect.windows.c rename to extern/reproc/reproc/src/redirect.windows.c diff --git a/extern/reproc-14.2.4/reproc/src/reproc.c b/extern/reproc/reproc/src/reproc.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/reproc.c rename to extern/reproc/reproc/src/reproc.c diff --git a/extern/reproc-14.2.4/reproc/src/run.c b/extern/reproc/reproc/src/run.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/run.c rename to extern/reproc/reproc/src/run.c diff --git a/extern/reproc-14.2.4/reproc/src/strv.c b/extern/reproc/reproc/src/strv.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/strv.c rename to extern/reproc/reproc/src/strv.c diff --git a/extern/reproc-14.2.4/reproc/src/strv.h b/extern/reproc/reproc/src/strv.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/strv.h rename to extern/reproc/reproc/src/strv.h diff --git a/extern/reproc-14.2.4/reproc/src/utf.h b/extern/reproc/reproc/src/utf.h similarity index 100% rename from extern/reproc-14.2.4/reproc/src/utf.h rename to extern/reproc/reproc/src/utf.h diff --git a/extern/reproc-14.2.4/reproc/src/utf.posix.c b/extern/reproc/reproc/src/utf.posix.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/utf.posix.c rename to extern/reproc/reproc/src/utf.posix.c diff --git a/extern/reproc-14.2.4/reproc/src/utf.windows.c b/extern/reproc/reproc/src/utf.windows.c similarity index 100% rename from extern/reproc-14.2.4/reproc/src/utf.windows.c rename to extern/reproc/reproc/src/utf.windows.c diff --git a/extern/reproc-14.2.4/reproc/test/argv.c b/extern/reproc/reproc/test/argv.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/argv.c rename to extern/reproc/reproc/test/argv.c diff --git a/extern/reproc-14.2.4/reproc/test/assert.h b/extern/reproc/reproc/test/assert.h similarity index 100% rename from extern/reproc-14.2.4/reproc/test/assert.h rename to extern/reproc/reproc/test/assert.h diff --git a/extern/reproc-14.2.4/reproc/test/deadline.c b/extern/reproc/reproc/test/deadline.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/deadline.c rename to extern/reproc/reproc/test/deadline.c diff --git a/extern/reproc-14.2.4/reproc/test/env.c b/extern/reproc/reproc/test/env.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/env.c rename to extern/reproc/reproc/test/env.c diff --git a/extern/reproc-14.2.4/reproc/test/fork.c b/extern/reproc/reproc/test/fork.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/fork.c rename to extern/reproc/reproc/test/fork.c diff --git a/extern/reproc-14.2.4/reproc/test/io.c b/extern/reproc/reproc/test/io.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/io.c rename to extern/reproc/reproc/test/io.c diff --git a/extern/reproc-14.2.4/reproc/test/overflow.c b/extern/reproc/reproc/test/overflow.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/overflow.c rename to extern/reproc/reproc/test/overflow.c diff --git a/extern/reproc-14.2.4/reproc/test/path.c b/extern/reproc/reproc/test/path.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/path.c rename to extern/reproc/reproc/test/path.c diff --git a/extern/reproc-14.2.4/reproc/test/pid.c b/extern/reproc/reproc/test/pid.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/pid.c rename to extern/reproc/reproc/test/pid.c diff --git a/extern/reproc-14.2.4/reproc/test/stop.c b/extern/reproc/reproc/test/stop.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/stop.c rename to extern/reproc/reproc/test/stop.c diff --git a/extern/reproc-14.2.4/reproc/test/working-directory.c b/extern/reproc/reproc/test/working-directory.c similarity index 100% rename from extern/reproc-14.2.4/reproc/test/working-directory.c rename to extern/reproc/reproc/test/working-directory.c diff --git a/extern/stduuid/include/uuid.h b/extern/stduuid/include/uuid.h index 34f59e5f8..fb5875762 100644 --- a/extern/stduuid/include/uuid.h +++ b/extern/stduuid/include/uuid.h @@ -20,8 +20,9 @@ #ifdef _WIN32 #include -#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include #include diff --git a/include/xstudio/atoms.hpp b/include/xstudio/atoms.hpp index b2d174984..04de419cd 100644 --- a/include/xstudio/atoms.hpp +++ b/include/xstudio/atoms.hpp @@ -26,6 +26,7 @@ const std::string audio_cache_registry{"AUDIOCACHE"}; const std::string audio_output_registry{"AUDIO_OUTPUT"}; 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"}; @@ -33,16 +34,15 @@ const std::string global_playhead_events_actor{"GLOBALPLAYHEADEVENTS"}; const std::string global_store_registry{"GLOBALSTORE"}; const std::string image_cache_registry{"IMAGECACHE"}; const std::string keyboard_events{"KEYBOARDEVENTS"}; -const std::string main_viewport_registry{"MAIN_VIEWPORT"}; 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 player_ui_registry{"PLAYERUI"}; const std::string plugin_manager_registry{"PLUGINMNGR"}; const std::string scanner_registry{"SCANNER"}; const std::string studio_registry{"STUDIO"}; +const std::string studio_ui_registry{"STUDIOUI"}; const std::string sync_gateway_manager_registry{"SYNCGATEMAN"}; const std::string sync_gateway_registry{"SYNCGATE"}; const std::string thumbnail_manager_registry{"THUMBNAIL"}; @@ -51,6 +51,8 @@ const std::string global_ui_model_data_registry{"GLOBALUIMODELDATA"}; namespace bookmark { class AnnotationBase; struct BookmarkDetail; + struct BookmarkAndAnnotation; + typedef std::shared_ptr BookmarkAndAnnotationPtr; } // namespace bookmark namespace event { @@ -103,6 +105,11 @@ namespace media { typedef std::vector MediaKeyVector; } // namespace media +namespace conform { + struct ConformRequest; + struct ConformReply; +} // namespace conform + namespace media_reader { class AudioBuffer; class AudioBufPtr; @@ -126,7 +133,7 @@ namespace ui { class PointerEvent; struct Signature; namespace viewport { - struct GPUShader; + class GPUShader; typedef std::shared_ptr GPUShaderPtr; } // namespace viewport @@ -138,6 +145,7 @@ namespace plugin_manager { namespace plugin { class ViewportOverlayRenderer; + class GPUPreDrawHook; } // namespace plugin namespace module { @@ -156,7 +164,6 @@ using namespace caf; caf::init_global_meta_objects(); \ caf::init_global_meta_objects(); - CAF_ALLOW_UNSAFE_MESSAGE_TYPE(httplib::Headers) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(httplib::Params) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(httplib::Response) @@ -168,6 +175,8 @@ CAF_ALLOW_UNSAFE_MESSAGE_TYPE(std::shared_ptr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(std::shared_ptr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(std::shared_ptr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(std::shared_ptr) +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) @@ -181,8 +190,10 @@ CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::ui::Hotkey) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::ui::viewport::GPUShaderPtr) // clang-format off +// offset first_custom_type_id by first custom qt event +#define FIRST_CUSTOM_ID (first_custom_type_id + 1000 + 100) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, first_custom_type_id) +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (httplib::Headers)) CAF_ADD_TYPE_ID(xstudio_simple_types, (httplib::Params)) @@ -192,18 +203,19 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, first_custom_type_id) CAF_ADD_TYPE_ID(xstudio_simple_types, (Imath::V2i)) CAF_ADD_TYPE_ID(xstudio_simple_types, (timebase::flicks)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::bookmark::BookmarkDetail)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::colour_pipeline::ColourPipelineDataPtr)) + 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::media_error)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaDetail)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaKey)) - 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::MediaStatus)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaType)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::StreamDetail)) @@ -216,7 +228,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, first_custom_type_id) 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_manager::PluginType)) 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)) @@ -231,6 +242,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, first_custom_type_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::GPUShaderPtr)) 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)) @@ -248,36 +260,36 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, first_custom_type_id) 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)) - - // **************** add new entries here ****************** - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GPUShaderPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::ImageFormat)) CAF_END_TYPE_ID_BLOCK(xstudio_simple_types) - -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, xstudio_simple_types_last_type_id + 100) +// xstudio_simple_types_last_type_id +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::array)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::list)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::map>)) 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::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::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::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::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, (xstudio::utility::UuidActor)) - 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::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::pair)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::set)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::shared_ptr)) @@ -290,10 +302,10 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, xstudio_simple_types_last_type_id CAF_ADD_TYPE_ID(xstudio_complex_types, (std::shared_ptr)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple, std::optional>)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple, xstudio::utility::Uuid>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple, xstudio::utility::Uuid>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) 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)) @@ -302,28 +314,27 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, xstudio_simple_types_last_type_id 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, (xstudio::utility::UuidActorVector)) 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)) - 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)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) @@ -331,14 +342,18 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, xstudio_simple_types_last_type_id 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, (xstudio::utility::UuidActor)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (xstudio::utility::UuidActorVector)) - // **************** add new entries here ****************** - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) + 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, (std::set)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::shared_ptr)) CAF_END_TYPE_ID_BLOCK(xstudio_complex_types) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, xstudio_complex_types_last_type_id+100) +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::broadcast, broadcast_down_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::broadcast, join_broadcast_atom) @@ -347,10 +362,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, xstudio_complex_types_last_type CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, busy_atom) 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) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_playhead_events_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_store_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_thumbnail_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_plugin_manager_atom) @@ -378,6 +395,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, xstudio_complex_types_last_type CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, add_attribute_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, attribute_deleted_event_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, attribute_role_data_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, attribute_uuids_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, attribute_value_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, change_attribute_event_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, change_attribute_request_atom) @@ -388,11 +406,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, xstudio_complex_types_last_type 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_ui_focus_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) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, grab_ui_focus_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, join_module_attr_events_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, leave_module_attr_events_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, link_module_atom) 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) @@ -423,16 +442,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, xstudio_complex_types_last_type CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, uuid_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, version_atom) - // **************** add new entries here ****************** - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_playhead_events_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, attribute_uuids_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_actor_from_registry_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, link_module_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, remove_attribute_atom) CAF_END_TYPE_ID_BLOCK(xstudio_framework_atoms) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, xstudio_framework_atoms_last_type_id+100) +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, FIRST_CUSTOM_ID + (200 * 3)) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::data_source, get_data_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::data_source, post_data_atom) @@ -470,6 +485,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, xstudio_framework_atoms_last_type_ CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_authentication_source_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_create_entity_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_credential_atom) + CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_delete_entity_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_entity_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_entity_filter_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_entity_search_atom) @@ -490,11 +506,13 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, xstudio_framework_atoms_last_type_ CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::shotgun_client, shotgun_attachment_atom) // **************** 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_END_TYPE_ID_BLOCK(xstudio_plugin_atoms) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, xstudio_plugin_atoms_last_type_id+100) +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::bookmark, add_annotation_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::bookmark, add_bookmark_atom) @@ -511,8 +529,10 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, xstudio_plugin_atoms_last_type_id CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, acquire_media_detail_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, add_media_source_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, add_media_stream_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, checksum_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, current_media_source_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, current_media_stream_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, decompose_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, get_edit_list_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, get_media_details_atom) //DEPRECATED CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, get_media_pointer_atom) @@ -525,6 +545,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, xstudio_plugin_atoms_last_type_id CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, invalidate_cache_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, media_reference_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, media_status_atom) + 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_metadata, get_metadata_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, add_media_atom) @@ -586,29 +608,39 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, xstudio_plugin_atoms_last_type_id CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, available_range_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, duration_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, erase_item_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, erase_item_at_frame_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, insert_item_at_frame_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, insert_item_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_name_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, link_media_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, move_item_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, move_item_at_frame_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, remove_item_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, remove_item_at_frame_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, split_item_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, split_item_at_frame_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, trimmed_range_atom) - // **************** add new entries here ****************** - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_name_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, checksum_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, relink_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, decompose_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, rescan_atom) + 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_END_TYPE_ID_BLOCK(xstudio_session_atoms) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, xstudio_session_atoms_last_type_id+100) +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::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) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, display_colour_transform_hash_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, get_colour_pipe_data_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, get_colour_pipe_params_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_pipeline_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, get_thumbnail_colour_pipeline_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, pixel_info_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, set_colour_pipe_params_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_cache, cached_frames_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_cache, count_atom) @@ -660,12 +692,14 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, xstudio_session_atoms_last_type_ CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, jump_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, key_child_playhead_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, key_playhead_index_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, last_frame_media_pointer_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, logical_frame_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, logical_frame_to_flicks_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, loop_atom) 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_logical_frame_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_source_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, monitored_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, overflow_mode_atom) @@ -691,19 +725,11 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, xstudio_session_atoms_last_type_ CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, velocity_multiplier_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, viewport_events_group_atom) - // **************** add new entries here ****************** - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, last_frame_media_pointer_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, display_colour_transform_hash_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, pixel_info_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_logical_frame_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, get_thumbnail_colour_pipeline_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, connect_to_viewport_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_operation_uniforms_atom) - + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_frame_atom) CAF_END_TYPE_ID_BLOCK(xstudio_playback_atoms) -CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, xstudio_playback_atoms_last_type_id+100) +CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, FIRST_CUSTOM_ID + (200 * 6)) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, show_buffer_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::fps_monitor, connect_to_playhead_atom) @@ -711,17 +737,27 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, xstudio_playback_atoms_last_type_id+10 CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::fps_monitor, framebuffer_swapped_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::fps_monitor, update_actual_fps_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, all_keys_up_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, hotkey_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, hotkey_event_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, key_down_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, key_pressed_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, key_released_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, key_up_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, mouse_event_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, pressed_keys_changed_atom) + 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, register_hotkey_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, hotkey_event_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, 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, fit_mode_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, other_viewport_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, overlay_render_function_atom) @@ -734,18 +770,17 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, xstudio_playback_atoms_last_type_id+10 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) - // **************** add new entries here ****************** - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, enable_hud_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, hotkey_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, model_data_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, set_node_data_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, insert_rows_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, remove_rows_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, remove_node_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, insert_or_update_menu_node_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, video_output_actor_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, aux_shader_uniforms_atom) CAF_END_TYPE_ID_BLOCK(xstudio_ui_atoms) diff --git a/include/xstudio/audio/audio_output.hpp b/include/xstudio/audio/audio_output.hpp index eb5c11aa4..70338b38b 100644 --- a/include/xstudio/audio/audio_output.hpp +++ b/include/xstudio/audio/audio_output.hpp @@ -17,22 +17,16 @@ namespace xstudio::audio { * required */ -class AudioOutputControl : public module::Module { +class AudioOutputControl { public: /** * @brief Constructor * */ - AudioOutputControl(const utility::JsonStore &prefs = utility::JsonStore()); + AudioOutputControl(const utility::JsonStore &prefs = utility::JsonStore()) {} - /** - * @brief Destructor - * - * @details Closes the connection to the audio device by deleting the - * output device object - */ - ~AudioOutputControl() override = default; + ~AudioOutputControl() = default; /** * @brief Use steady clock combined with soundcard latency to fill a @@ -47,20 +41,15 @@ class AudioOutputControl : public module::Module { const int num_channels, const int sample_rate); - /** - * @brief Set the audio volume in range 0-1 - */ - void set_volume(const float v) { volume_->set_value(v); } - /** * @brief The audio volume (range is 0-1) */ - [[nodiscard]] float volume() const { return volume_->value(); } + [[nodiscard]] float volume() const { return volume_; } /** * @brief The audio volume muted */ - [[nodiscard]] bool muted() const { return muted_->value(); } + [[nodiscard]] bool muted() const { return muted_; } /** * @brief Queue audio buffer for streaming to the soundcard @@ -78,6 +67,21 @@ class AudioOutputControl : public module::Module { enum Fade { NoFade = 0, DoFadeHead = 1, DoFadeTail = 2, DoFadeHeadAndTail = 3 }; + /** + * @brief Sets volume etc - these settings come from the global audio output + * module. + */ + void set_attrs( + const float volume, + const bool muted, + const bool audio_repitch, + const bool audio_scrubbing) { + volume_ = volume; + muted_ = muted; + audio_repitch_ = audio_repitch; + audio_scrubbing_ = audio_scrubbing; + } + private: media_reader::AudioBufPtr pick_audio_buffer(const utility::clock::time_point &tp, const bool drop_old_buffers); @@ -87,8 +91,6 @@ class AudioOutputControl : public module::Module { const media_reader::AudioBufPtr &next_buf, const media_reader::AudioBufPtr &previous_buf_); - utility::JsonStore prefs_; - std::map sample_data_; media_reader::AudioBufPtr current_buf_; media_reader::AudioBufPtr previous_buf_; @@ -97,13 +99,9 @@ class AudioOutputControl : public module::Module { int fade_in_out_ = {NoFade}; - module::IntegerAttribute *audio_delay_millisecs_; - module::BooleanAttribute *audio_repitch_; - module::BooleanAttribute *audio_scrubbing_; - - const utility::JsonStore params_; - - module::FloatAttribute *volume_; - module::BooleanAttribute *muted_; + bool audio_repitch_ = {false}; + bool audio_scrubbing_ = {false}; + float volume_ = {100.0f}; + bool muted_ = {false}; }; } // namespace xstudio::audio diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index bf139806e..0ff730b0b 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -8,57 +8,141 @@ namespace xstudio::audio { +template class AudioOutputDeviceActor : public caf::event_based_actor { public: - AudioOutputDeviceActor( - caf::actor_config &cfg, - caf::actor_addr audio_playback_manager, - const std::string name = "AudioOutputDeviceActor"); + AudioOutputDeviceActor(caf::actor_config &cfg, caf::actor samples_actor) + : caf::event_based_actor(cfg), + playing_(false), + waiting_for_samples_(false), + audio_samples_actor_(samples_actor) { + + // spdlog::info("Created {} {}", "AudioOutputDeviceActor", OutputClassType::name()); + // utility::print_on_exit(this, OutputClassType::name()); + + 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( + + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](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 & /*j*/) { + // TODO: restart soundcard connection with new prefs + if (output_device_) { + output_device_->initialize_sound_card(); + } + }, + [=](utility::event_atom, playhead::play_atom, const bool is_playing) { + if (!is_playing && output_device_) { + // this stops the loop pushing samples to the 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_) + 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, + 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()) + .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); + } + }, + [=](caf::error &err) mutable { waiting_for_samples_ = false; }); + } + + ); + } + + 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_; } - const char *name() const override { return NAME.c_str(); } - - private: - void open_output_device(const utility::JsonStore &prefs); + const char *name() const override { return name_.c_str(); } + protected: std::unique_ptr output_device_; - inline static const std::string NAME = "AudioOutputDeviceActor"; - + private: caf::behavior behavior_; std::string name_; bool playing_; - caf::actor_addr audio_playback_manager_; + caf::actor audio_samples_actor_; bool waiting_for_samples_; }; - -class AudioOutputControlActor : public caf::event_based_actor, AudioOutputControl { +template +class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { public: - AudioOutputControlActor( - caf::actor_config &cfg, const std::string name = "AudioOutputControlActor"); - ~AudioOutputControlActor() override = default; + AudioOutputActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { init(); } - caf::behavior make_behavior() override { return message_handler().or_else(behavior_); } + ~AudioOutputActor() override = default; - void on_exit() override; - const char *name() const override { return NAME.c_str(); } + caf::behavior make_behavior() override { return behavior_; } private: caf::actor audio_output_device_; - inline static const std::string NAME = "AudioOutputControlActor"; void init(); void get_audio_buffer(caf::actor media_actor, const utility::Uuid uuid, const int source_frame); caf::behavior behavior_; - std::string name_; const utility::JsonStore params_; bool playing_ = {false}; int video_frame_ = {0}; @@ -66,4 +150,99 @@ class AudioOutputControlActor : public caf::event_based_actor, AudioOutputContro utility::Uuid uuid_ = {utility::Uuid::generate()}; utility::Uuid sub_playhead_uuid_; }; + +/* Singleton class that receives audio sample buffers from the current +playhead during playback. It re-broadcasts these samples to any AudioOutputActor +that has been instanced. */ +class GlobalAudioOutputActor : public caf::event_based_actor, module::Module { + + public: + GlobalAudioOutputActor(caf::actor_config &cfg); + ~GlobalAudioOutputActor() override = default; + + void on_exit() override; + + void attribute_changed(const utility::Uuid &attr_uuid, const int role); + + caf::behavior make_behavior() override { + return behavior_.or_else(module::Module::message_handler()); + } + + private: + caf::actor event_group_; + caf::message_handler behavior_; + module::BooleanAttribute *audio_repitch_; + module::BooleanAttribute *audio_scrubbing_; + module::FloatAttribute *volume_; + module::BooleanAttribute *muted_; +}; + +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 d19f4b50f..c0b86f8b1 100644 --- a/include/xstudio/audio/audio_output_device.hpp +++ b/include/xstudio/audio/audio_output_device.hpp @@ -28,6 +28,13 @@ class AudioOutputDevice { */ virtual ~AudioOutputDevice() = default; + /** + * @brief Configure the sound card. + * + * @details Should be called any time the sound card should be set up or changed + */ + virtual void initialize_sound_card() = 0; + /** * @brief Open the connection to the sounding device * diff --git a/src/audio/src/linux_audio_output_device.hpp b/include/xstudio/audio/linux_audio_output_device.hpp similarity index 92% rename from src/audio/src/linux_audio_output_device.hpp rename to include/xstudio/audio/linux_audio_output_device.hpp index be6463a30..2ff0005d1 100644 --- a/src/audio/src/linux_audio_output_device.hpp +++ b/include/xstudio/audio/linux_audio_output_device.hpp @@ -37,7 +37,11 @@ namespace audio { [[nodiscard]] SampleFormat sample_format() const override { return sample_format_; } + static std::string name() { return "LinuxAudioOutputDevice"; } + private: + void initialize_sound_card() override {} + long sample_rate_ = {44100}; int num_channels_ = {2}; long buffer_size_ = {2048}; diff --git a/include/xstudio/audio/windows_audio_output_device.hpp b/include/xstudio/audio/windows_audio_output_device.hpp new file mode 100644 index 000000000..ec06390b7 --- /dev/null +++ b/include/xstudio/audio/windows_audio_output_device.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "xstudio/audio/audio_output_device.hpp" +#include "xstudio/utility/json_store.hpp" + + +namespace xstudio { +namespace audio { + + /** + * @brief WindowsAudioOutputDevice class, low level interface with audio output + * + * @details + * See header for AudioOutputDevice + */ + class WindowsAudioOutputDevice : public AudioOutputDevice { + public: + WindowsAudioOutputDevice(const utility::JsonStore &prefs); + + ~WindowsAudioOutputDevice() override; + + void connect_to_soundcard() override; + + void disconnect_from_soundcard() override; + + long desired_samples() override; + + void 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 sample_rate_ = {48000}; + int num_channels_ = {2}; + long buffer_size_ = {2048}; + SampleFormat sample_format_ = {SampleFormat::INT16}; + CComPtr audio_client_; + CComPtr render_client_; + CComPtr render_clock_adjustment_; + + const utility::JsonStore config_; + const utility::JsonStore prefs_; + + void initialize_sound_card(); + + HRESULT initializeAudioClient( + const std::string &sound_card = "", long sample_rate = 48000, int num_channels = 2); + }; +} // namespace audio +} // namespace xstudio diff --git a/include/xstudio/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index ba7b2307b..8dbaff21d 100644 --- a/include/xstudio/bookmark/bookmark.hpp +++ b/include/xstudio/bookmark/bookmark.hpp @@ -29,10 +29,14 @@ namespace bookmark { virtual utility::JsonStore serialise(utility::Uuid &plugin_uuid) const { return store_; } - const utility::JsonStore store_; utility::Uuid bookmark_uuid_; + + private: + utility::JsonStore store_; }; + typedef std::shared_ptr AnnotationBasePtr; + class Note { public: Note() = default; @@ -84,6 +88,7 @@ namespace bookmark { std::optional owner_; std::optional enabled_; std::optional has_focus_; + std::optional visible_; std::optional start_; std::optional duration_; @@ -96,6 +101,7 @@ namespace bookmark { std::optional has_note_; std::optional has_annotation_; + std::optional media_reference_; std::optional media_flag_; @@ -108,6 +114,7 @@ namespace bookmark { 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("hasa", x.has_annotation_), @@ -230,7 +237,11 @@ 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); } @@ -277,6 +288,7 @@ namespace bookmark { auto enabled() const { return enabled_; } auto has_focus() const { return has_focus_; } + auto visible() const { return visible_; } auto start() const { return start_; } auto duration() const { return duration_; } @@ -284,6 +296,7 @@ namespace bookmark { void set_owner(const utility::Uuid owner) { owner_ = owner; } void set_enabled(const bool enabled = true) { enabled_ = enabled; } + void set_visible(const bool visible = true) { visible_ = visible; } void set_has_focus(const bool has_focus = true) { has_focus_ = has_focus; } void set_start(const timebase::flicks start = timebase::k_flicks_low) { start_ = start; @@ -301,6 +314,7 @@ namespace bookmark { utility::Uuid owner_; bool enabled_{true}; bool has_focus_{false}; + bool visible_{true}; timebase::flicks start_{timebase::k_flicks_low}; timebase::flicks duration_{timebase::k_flicks_max}; @@ -308,6 +322,22 @@ namespace bookmark { std::shared_ptr annotation_{nullptr}; }; + /* This struct is used by Playhead classes as a convenient way to maintain + a record of bookmarks, attached annotation data and a logical frame range*/ + struct BookmarkAndAnnotation { + + BookmarkDetail detail_; + std::shared_ptr annotation_; + int start_frame_ = -1; + int end_frame_ = -1; + }; + + // BookmarkAndAnnotationPtrs can be shared across different parts of the + // application (for example the SubPlayhead, ImageBufPtr, AnnotationsTool) + // and therefore it must be const data + typedef std::shared_ptr BookmarkAndAnnotationPtr; + typedef std::vector BookmarkAndAnnotations; + // not sure if we want this... class Bookmarks : public utility::Container { public: diff --git a/include/xstudio/colour_pipeline/colour_operation.hpp b/include/xstudio/colour_pipeline/colour_operation.hpp index 367a96370..8a99c4db7 100644 --- a/include/xstudio/colour_pipeline/colour_operation.hpp +++ b/include/xstudio/colour_pipeline/colour_operation.hpp @@ -13,7 +13,7 @@ namespace colour_pipeline { // Base class for implementing colour operation plugins. A colour operation // transforms an RGBA value by defining glsl fragment shader that // includes a function with the following signature (exactly): - // vec4 colour_transform_op(vec4 rgba); + // vec4 colour_transform_op(vec4 rgba, vec2 image_pos); // // The function operates on the linear RGBA fragment colour before it is // passed to the display shader that applies the display LUT, for example. @@ -39,12 +39,18 @@ namespace colour_pipeline { or appropriate high number. */ [[nodiscard]] virtual float ordering() const = 0; - /* For the given MediaSource, return the colour operation data - which - includes (for OpenGL viewport) a required glsl shader and optional LUT - data. Typically this result will be static for all sources but there - is the possibility to have data that depends on properties (like - metadata) of the media_source if required.*/ - virtual ColourOperationDataPtr data( + /* For the given image, return the colour operation data to be applied + to the linearised RGB pixel values of that image. + The which data would include (for OpenGL viewport) a required glsl + shader which implements the colour transform maths of your colour + operator, with optional LUT data that may also be used to apply a + colour operation. + Typically this result will be static for all sources but there is the + possibility to have data that depends on properties (like metadata) of + the media_source if required. It is up to the plugin write to make this + call efficient and have cacheing of shader data where that might + be appropriate.*/ + virtual ColourOperationDataPtr colour_op_graphics_data( utility::UuidActor &media_source, const utility::JsonStore &media_source_colour_metadata) = 0; @@ -56,14 +62,13 @@ namespace colour_pipeline { virtual void onscreen_media_source_changed( const utility::UuidActor &media_source, const utility::JsonStore &colour_params) {} - /* For the given MediaSource, update key/value pairs in the - uniforms_dict json dictionary - keys should match the names of - uniforms in your shader and values should match the type of your uniform. + /* 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 + shader at draw-time - keys should match the names of uniforms in your + shader and values should match the type of your uniform. For vec3 types etc us Imath::V3f for example.*/ - virtual void update_shader_uniforms( - utility::JsonStore &uniforms_dict, - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) = 0; + virtual utility::JsonStore + update_shader_uniforms(const media_reader::ImageBufPtr &image) = 0; /* Call this function with custom metadata to be merged into the colour metadata of the current on screen source */ diff --git a/include/xstudio/colour_pipeline/colour_pipeline.hpp b/include/xstudio/colour_pipeline/colour_pipeline.hpp index 91858e1c0..f00898e31 100644 --- a/include/xstudio/colour_pipeline/colour_pipeline.hpp +++ b/include/xstudio/colour_pipeline/colour_pipeline.hpp @@ -3,6 +3,7 @@ #pragma once #include "colour_lut.hpp" +#include "colour_texture.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/module/module.hpp" @@ -25,10 +26,14 @@ namespace colour_pipeline { struct ColourOperationData { ColourOperationData() = default; ColourOperationData(const ColourOperationData &o) = default; + ColourOperationData(const utility::Uuid &uuid, const std::string name) + : uuid_(uuid), name_(name) {} 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; @@ -39,6 +44,9 @@ namespace colour_pipeline { struct ColourPipelineData { + ColourPipelineData() = default; + ColourPipelineData(const ColourPipelineData &o) = default; + std::string cache_id_; void add_operation(const ColourOperationDataPtr &op) { @@ -52,6 +60,23 @@ namespace colour_pipeline { ordered_colour_operations_.insert(p, op); } + void overwrite_operation_data(const ColourOperationDataPtr &op) { + for (auto &op_data : ordered_colour_operations_) { + if (op_data->uuid_ == op->uuid_) { + op_data = op; + break; + } + } + } + + ColourOperationDataPtr get_operation_data(const utility::Uuid &uuid) { + for (auto &op_data : ordered_colour_operations_) { + if (op_data->uuid_ == uuid) + return op_data; + } + return ColourOperationDataPtr(); + } + // user_data can be used by colour pipeline plugin to add any data specific // to a media source that the colour pipeline wants xstudio to cache and // pass back into the colour pipeline when re-evaluating shader, LUTs and @@ -79,7 +104,7 @@ namespace colour_pipeline { ColourPipeline(caf::actor_config &cfg, const utility::JsonStore &init_settings); - ~ColourPipeline() override = default; + 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 @@ -103,12 +128,6 @@ namespace colour_pipeline { const utility::Uuid &source_uuid, const utility::JsonStore &media_source_colour_metadata) = 0; - /* When the ColourPipeline is instanced by the parent Viewport this - method will be called. It gives an opportunity to query the name of - the viewport toolbar, for example, which is unqique for each viewport */ - virtual void connect_to_viewport( - caf::actor viewport, const std::string viewport_name, const int viewport_index) = 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 */ @@ -131,10 +150,13 @@ namespace colour_pipeline { const utility::Uuid &source_uuid, const utility::JsonStore &media_source_colour_metadata) = 0; - virtual void update_shader_uniforms( - utility::JsonStore &uniforms, - const utility::Uuid &source_uuid, - std::any &user_data) = 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 + shader at draw-time - keys should match the names of uniforms in your + shader and values should match the type of your uniform. + For vec3 types etc us Imath::V3f for example.*/ + virtual utility::JsonStore + update_shader_uniforms(const media_reader::ImageBufPtr &image, std::any &user_data) = 0; virtual void media_source_changed( const utility::Uuid &source_uuid, @@ -183,6 +205,9 @@ namespace colour_pipeline { virtual std::string 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); + void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; caf::message_handler message_handler_extensions() override; @@ -190,7 +215,6 @@ namespace colour_pipeline { bool is_worker() const { return is_worker_; } utility::Uuid uuid_; - std::string viewport_name_; private: bool make_colour_pipe_data_from_cached_data( @@ -222,6 +246,8 @@ namespace colour_pipeline { const std::string &linearise_transform_cache_id, const std::string &display_transform_cache_id); + void load_colour_op_plugins(); + std::map>> in_flight_requests_; std::map> cache_keys_cache_; @@ -232,10 +258,12 @@ namespace colour_pipeline { caf::actor pixel_probe_worker_; caf::actor cache_; std::vector workers_; - bool is_worker_ = false; + bool is_worker_ = false; + bool colour_ops_loaded_ = false; - void load_colour_op_plugins(); std::vector colour_op_plugins_; + std::vector, int>> + hook_requests_; }; } // namespace colour_pipeline diff --git a/include/xstudio/colour_pipeline/colour_pipeline_actor.hpp b/include/xstudio/colour_pipeline/colour_pipeline_actor.hpp index 1c36e4428..e7fdb2cc8 100644 --- a/include/xstudio/colour_pipeline/colour_pipeline_actor.hpp +++ b/include/xstudio/colour_pipeline/colour_pipeline_actor.hpp @@ -22,7 +22,7 @@ namespace colour_pipeline { class GlobalColourPipelineActor : public caf::event_based_actor, public module::Module { public: GlobalColourPipelineActor(caf::actor_config &cfg); - ~GlobalColourPipelineActor() override = default; + virtual ~GlobalColourPipelineActor(); caf::behavior make_behavior() override; @@ -48,7 +48,7 @@ namespace colour_pipeline { std::vector colour_pipe_plugin_details_; std::string default_plugin_name_; utility::JsonStore prefs_jsn_; - caf::actor viewport0_colour_pipeline_; + std::map colour_piplines_; }; diff --git a/include/xstudio/colour_pipeline/colour_texture.hpp b/include/xstudio/colour_pipeline/colour_texture.hpp new file mode 100644 index 000000000..082753ac0 --- /dev/null +++ b/include/xstudio/colour_pipeline/colour_texture.hpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace xstudio { + +namespace colour_pipeline { + + // Quick and dirty placeholder for OpenGL texture descriptor + // representing an already existing and created texture. + // Probably should use some structure directly from core xStudio + // if already available or create otherwise. + + enum ColourTextureTarget { + TEXTURE_2D, + }; + + struct ColourTexture { + std::string name; + ColourTextureTarget target; + unsigned int id; + }; + +} // namespace colour_pipeline +} // namespace xstudio diff --git a/include/xstudio/conform/conform_manager_actor.hpp b/include/xstudio/conform/conform_manager_actor.hpp new file mode 100644 index 000000000..7c2a059b6 --- /dev/null +++ b/include/xstudio/conform/conform_manager_actor.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/media_reader/media_reader.hpp" + +namespace xstudio::conform { +class ConformWorkerActor : public caf::event_based_actor { + public: + ConformWorkerActor(caf::actor_config &cfg); + ~ConformWorkerActor() override = default; + + caf::behavior make_behavior() override { return behavior_; } + const char *name() const override { return NAME.c_str(); } + + private: + inline static const std::string NAME = "ConformWorkerActor"; + caf::behavior behavior_; +}; + +class ConformManagerActor : public caf::event_based_actor { + public: + ConformManagerActor( + caf::actor_config &cfg, const utility::Uuid uuid = utility::Uuid::generate()); + ~ConformManagerActor() override = default; + + caf::behavior make_behavior() override { return behavior_; } + void on_exit() override; + const char *name() const override { return NAME.c_str(); } + + private: + inline static const std::string NAME = "ConformManagerActor"; + caf::behavior behavior_; + utility::Uuid uuid_; + caf::actor event_group_; + std::vector tasks_; +}; + +} // namespace xstudio::conform diff --git a/include/xstudio/conform/conformer.hpp b/include/xstudio/conform/conformer.hpp new file mode 100644 index 000000000..07ab65975 --- /dev/null +++ b/include/xstudio/conform/conformer.hpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +// #include +// #include +// #include +// #include +// #include +// #include +// #include + +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/plugin_manager/plugin_factory.hpp" +#include "xstudio/utility/media_reference.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/utility/helpers.hpp" + +namespace xstudio { +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; + + struct ConformRequest { + ConformRequest( + const utility::UuidActor playlist, + const utility::JsonStore playlist_json, + const std::vector items) + : playlist_(std::move(playlist)), + playlist_json_(std::move(playlist_json)), + items_(std::move(items)) {} + ConformRequest() = default; + ~ConformRequest() = default; + + utility::UuidActor playlist_; + utility::JsonStore playlist_json_; + std::vector< // request item + ConformRequestItem> + items_; + + 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("items", x.items_)); + } + }; + + typedef std::tuple< + bool, // exists in playlist + utility::MediaReference, // media json + utility::UuidActor // reference to media actor + > + ConformReplyItem; + + struct ConformReply { + ConformReply() = default; + ~ConformReply() = default; + std::vector>> items_; + + template friend bool inspect(Inspector &f, ConformReply &x) { + return f.object(x).fields(f.field("items", x.items_)); + } + }; + + class Conformer { + public: + Conformer(const utility::JsonStore &prefs = utility::JsonStore()); + 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 std::vector conform_tasks(); + }; + + template + class ConformPlugin : public plugin_manager::PluginFactoryTemplate { + public: + ConformPlugin( + utility::Uuid uuid, + std::string name = "", + std::string author = "", + std::string description = "", + semver::version version = semver::version("0.0.0")) + : plugin_manager::PluginFactoryTemplate( + uuid, + name, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM), + false, + author, + description, + version) {} + ~ConformPlugin() override = default; + }; + + template class ConformPluginActor : public caf::event_based_actor { + + public: + ConformPluginActor( + caf::actor_config &cfg, const utility::JsonStore &jsn = utility::JsonStore()) + : caf::event_based_actor(cfg), conform_(jsn) { + + spdlog::debug("Created ConformPluginActor"); + utility::print_on_exit(this, "ConformPluginActor"); + + { + auto prefs = global_store::GlobalStoreHelper(system()); + utility::JsonStore js; + utility::join_broadcast(this, prefs.get_group(js)); + conform_.update_preferences(js); + } + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](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); + }, + + [=](conform_tasks_atom) -> std::vector { + return conform_.conform_tasks(); + }, + + [=](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) { + try { + conform_.update_preferences(js); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }); + } + + caf::behavior make_behavior() override { return behavior_; } + + private: + caf::behavior behavior_; + T conform_; + }; + +} // namespace conform +} // namespace xstudio diff --git a/include/xstudio/data_source/data_source.hpp b/include/xstudio/data_source/data_source.hpp index dfa6e6b4c..10599236b 100644 --- a/include/xstudio/data_source/data_source.hpp +++ b/include/xstudio/data_source/data_source.hpp @@ -61,7 +61,7 @@ namespace data_source { : plugin_manager::PluginFactoryTemplate( uuid, name, - plugin_manager::PluginType::PT_DATA_SOURCE, + plugin_manager::PluginFlags::PF_DATA_SOURCE, true, author, description, diff --git a/include/xstudio/embedded_python/embedded_python.hpp b/include/xstudio/embedded_python/embedded_python.hpp index e4a81cdd2..83e06f247 100644 --- a/include/xstudio/embedded_python/embedded_python.hpp +++ b/include/xstudio/embedded_python/embedded_python.hpp @@ -66,7 +66,7 @@ namespace embedded_python { EmbeddedPythonActor *parent_; - static EmbeddedPython *s_instance_; + inline static EmbeddedPython *s_instance_ = nullptr; std::set sessions_; bool inited_{false}; bool setup_{false}; diff --git a/include/xstudio/event/event.hpp b/include/xstudio/event/event.hpp index 08d23e090..306e25e14 100644 --- a/include/xstudio/event/event.hpp +++ b/include/xstudio/event/event.hpp @@ -48,7 +48,7 @@ namespace event { float percentage = 100.0; auto range = progress_maximum_ - progress_minimum_; if (range > 0) { - auto div = 100.0 / static_cast(range); + auto div = 100.0f / static_cast(range); percentage = div * static_cast(progress_ - progress_minimum_); } diff --git a/include/xstudio/global_store/global_store.hpp b/include/xstudio/global_store/global_store.hpp index b433a4c11..1fea8d83b 100644 --- a/include/xstudio/global_store/global_store.hpp +++ b/include/xstudio/global_store/global_store.hpp @@ -75,7 +75,7 @@ namespace global_store { void from_json(const nlohmann::json &j, GlobalStoreDef &gsd); static const std::vector PreferenceContexts{ - "NEW_SESSION", "APPLICATION", "QML_UI"}; + "NEW_SESSION", "APPLICATION", "QML_UI", "PLUGIN"}; static const GlobalStoreDef gsd_hello{"/hello", "goodbye", "string", "Says goodbye"}; // static const GlobalStoreDef gsd_beast{"/beast", 666, "Number of the beast"}; // static const GlobalStoreDef gsd_happy{"/happy", true, "Am I happy"}; @@ -233,6 +233,15 @@ namespace global_store { JsonStoreHelper::set(value, path + "/value", 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( + const std::string &path, + const utility::JsonStore &default_, + const bool async = true, + const bool broacast_change = true, + const std::string &context = "APPLICATION"); + void set(const GlobalStoreDef &gsd, const bool async = true); bool save(const std::string &context); }; diff --git a/include/xstudio/history/history.hpp b/include/xstudio/history/history.hpp index 89353c897..4002ac838 100644 --- a/include/xstudio/history/history.hpp +++ b/include/xstudio/history/history.hpp @@ -77,6 +77,8 @@ namespace history { std::optional undo() { return undo_redo_.undo(); } std::optional redo() { return undo_redo_.redo(); } + std::optional peek_undo() { return undo_redo_.peek_undo(); } + std::optional peek_redo() { return undo_redo_.peek_redo(); } std::optional undo(const K &key) { return undo_redo_.undo(key); } std::optional redo(const K &key) { return undo_redo_.redo(key); } void clear() { undo_redo_.clear(); } diff --git a/include/xstudio/history/history_actor.hpp b/include/xstudio/history/history_actor.hpp index 1ed969515..886681bb9 100644 --- a/include/xstudio/history/history_actor.hpp +++ b/include/xstudio/history/history_actor.hpp @@ -6,6 +6,7 @@ #include "xstudio/atoms.hpp" #include "xstudio/history/history.hpp" +#include "xstudio/utility/chrono.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/uuid.hpp" @@ -212,6 +213,128 @@ namespace history { }); } + 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()), + + [=](plugin_manager::enable_atom) -> bool { return base_.enabled(); }, + + [=](plugin_manager::enable_atom, const bool enabled) -> bool { + base_.set_enabled(enabled); + return true; + }, + + [=](utility::clear_atom) -> bool { + base_.clear(); + return true; + }, + + [=](media_cache::count_atom) -> int { return static_cast(base_.count()); }, + + [=](media_cache::count_atom, const int count) -> bool { + base_.set_max_count(static_cast(count)); + return true; + }, + + [=](undo_atom) -> result { + auto i = base_.undo(); + if (i) + return *i; + return make_error(xstudio_error::error, "No history"); + }, + + [=](undo_atom, const utility::sys_time_duration &duration) + -> result> { + auto peek = base_.peek_undo(); + + if (peek) { + auto result = std::vector(); + while (true) { + auto next_peek = base_.peek_undo(); + if (next_peek and *next_peek >= (*peek) - duration) { + peek = next_peek; + auto i = base_.undo(); + if (i) { + result.push_back(*i); + } + } else + break; + } + return result; + } + + return make_error(xstudio_error::error, "No history"); + }, + + + [=](redo_atom, const utility::sys_time_duration &duration) + -> result> { + auto peek = base_.peek_redo(); + + if (peek) { + auto result = std::vector(); + while (true) { + auto next_peek = base_.peek_redo(); + if (next_peek and *next_peek >= (*peek) - duration) { + peek = next_peek; + auto i = base_.redo(); + if (i) { + result.push_back(*i); + } + } else + break; + } + return result; + } + + return make_error(xstudio_error::error, "No history"); + }, + + + [=](redo_atom) -> result { + auto i = base_.redo(); + if (i) + return *i; + return make_error(xstudio_error::error, "No history"); + }, + + [=](undo_atom, const utility::sys_time_point &key) -> result { + auto i = base_.undo(key); + if (i) + return *i; + return make_error(xstudio_error::error, "No history"); + }, + + [=](redo_atom, const utility::sys_time_point &key) -> result { + auto i = base_.redo(key); + if (i) + return *i; + return make_error(xstudio_error::error, "No history"); + }, + + [=](log_atom, + const utility::sys_time_point &key, + const utility::JsonStore &value) -> bool { + if (base_.enabled()) { + base_.push(key, value); + return true; + } + return false; + }, + + [=](utility::serialise_atom) -> utility::JsonStore { + utility::JsonStore jsn; + jsn["base"] = base_.serialise(); + return jsn; + }); + } + } // namespace history } // namespace xstudio diff --git a/include/xstudio/media/media.hpp b/include/xstudio/media/media.hpp index 63fc51fea..f03e2c9cf 100644 --- a/include/xstudio/media/media.hpp +++ b/include/xstudio/media/media.hpp @@ -65,16 +65,24 @@ namespace media { utility::FrameRateDuration duration = utility::FrameRateDuration(), std::string name = "Main", const MediaType media_type = MT_IMAGE, - std::string key_format = "{0}@{1}/{2}") + std::string key_format = "{0}@{1}/{2}", + Imath::V2i resolution = Imath::V2i(0, 0), + float pixel_aspect = 1.0f, + int index = -1) : duration_(std::move(duration)), name_(std::move(name)), media_type_(media_type), - key_format_(std::move(key_format)) {} + key_format_(std::move(key_format)), + resolution_(resolution), + pixel_aspect_(pixel_aspect), + index_(index) {} virtual ~StreamDetail() = default; bool operator==(const StreamDetail &other) const { return ( duration_ == other.duration_ and name_ == other.name_ and - media_type_ == other.media_type_ and key_format_ == other.key_format_); + media_type_ == other.media_type_ and key_format_ == other.key_format_ and + resolution_ == other.resolution_ and pixel_aspect_ == other.pixel_aspect_ and + index_ == other.index_); } template friend bool inspect(Inspector &f, StreamDetail &x) { @@ -82,7 +90,10 @@ namespace media { f.field("dur", x.duration_), f.field("name", x.name_), f.field("mt", x.media_type_), - f.field("kf", x.key_format_)); + f.field("kf", x.key_format_), + f.field("res", x.resolution_), + f.field("pa", x.pixel_aspect_), + f.field("idx", x.index_)); } friend std::string to_string(const StreamDetail &value); @@ -90,6 +101,9 @@ namespace media { std::string name_; MediaType media_type_; std::string key_format_; + Imath::V2i resolution_; + float pixel_aspect_; + int index_; }; inline std::string to_string(const StreamDetail &v) { @@ -372,9 +386,14 @@ namespace media { [[nodiscard]] std::pair checksum() const { return std::make_pair(checksum_, size_); } - void checksum(const std::pair &checksum) { - checksum_ = checksum.first; - size_ = checksum.second; + [[nodiscard]] bool checksum(const std::pair &checksum) { + auto changed = false; + if (checksum_ != checksum.first or size_ != checksum.second) { + checksum_ = checksum.first; + size_ = checksum.second; + changed = true; + } + return changed; } private: @@ -393,25 +412,24 @@ namespace media { class MediaStream : public utility::Container { public: MediaStream(const utility::JsonStore &jsn); - MediaStream( - const std::string &name, - utility::FrameRateDuration duration = utility::FrameRateDuration(), - const MediaType media_type = MT_IMAGE, - std::string key_format = "{0}@{1}/{2}"); + MediaStream(const StreamDetail &detail); ~MediaStream() override = default; [[nodiscard]] utility::JsonStore serialise() const override; - [[nodiscard]] std::string key_format() const { return key_format_; } - void set_key_format(const std::string &key_format) { key_format_ = key_format; } + [[nodiscard]] std::string key_format() const { return detail_.key_format_; } + void set_key_format(const std::string &key_format) { detail_.key_format_ = key_format; } + void set_detail(const StreamDetail &detail) { + detail_ = detail; + detail_.name_ = name(); + } - [[nodiscard]] MediaType media_type() const { return media_type_; } - [[nodiscard]] utility::FrameRateDuration duration() const { return duration_; } + [[nodiscard]] MediaType media_type() const { return detail_.media_type_; } + [[nodiscard]] utility::FrameRateDuration duration() const { return detail_.duration_; } + const StreamDetail &detail() const { return detail_; } private: - utility::FrameRateDuration duration_; - std::string key_format_; - MediaType media_type_; + StreamDetail detail_; }; inline std::shared_ptr make_blank_frame(const MediaType media_type) { diff --git a/include/xstudio/media/media_actor.hpp b/include/xstudio/media/media_actor.hpp index c9fffeb93..7fe34ac0b 100644 --- a/include/xstudio/media/media_actor.hpp +++ b/include/xstudio/media/media_actor.hpp @@ -97,6 +97,7 @@ namespace media { private: void update_media_status(); + void update_media_detail(); void acquire_detail(const utility::FrameRate &rate, caf::typed_response_promise rp); @@ -132,11 +133,8 @@ namespace media { public: MediaStreamActor( caf::actor_config &cfg, - const std::string &name, - const utility::FrameRateDuration &duration = utility::FrameRateDuration(), - const MediaType media_type = MT_IMAGE, - const std::string &key_format = "{0}@{1}/{2}", - const utility::Uuid &uuid = utility::Uuid()); + const StreamDetail &detail, + const utility::Uuid &uuid = utility::Uuid()); MediaStreamActor(caf::actor_config &cfg, const utility::JsonStore &jsn); ~MediaStreamActor() override = default; diff --git a/include/xstudio/media_hook/media_hook.hpp b/include/xstudio/media_hook/media_hook.hpp index dab879234..fb54b55c5 100644 --- a/include/xstudio/media_hook/media_hook.hpp +++ b/include/xstudio/media_hook/media_hook.hpp @@ -74,7 +74,7 @@ namespace media_hook { : plugin_manager::PluginFactoryTemplate( uuid, name, - plugin_manager::PluginType::PT_MEDIA_HOOK, + plugin_manager::PluginFlags::PF_MEDIA_HOOK, false, author, description, diff --git a/include/xstudio/media_metadata/media_metadata.hpp b/include/xstudio/media_metadata/media_metadata.hpp index 5e7d6fd8c..753fce702 100644 --- a/include/xstudio/media_metadata/media_metadata.hpp +++ b/include/xstudio/media_metadata/media_metadata.hpp @@ -69,7 +69,7 @@ namespace media_metadata { : plugin_manager::PluginFactoryTemplate( uuid, name, - plugin_manager::PluginType::PT_MEDIA_METADATA, + plugin_manager::PluginFlags::PF_MEDIA_METADATA, false, author, description, diff --git a/include/xstudio/media_reader/buffer.hpp b/include/xstudio/media_reader/buffer.hpp index ee9387ce3..7bd8b0c5e 100644 --- a/include/xstudio/media_reader/buffer.hpp +++ b/include/xstudio/media_reader/buffer.hpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#undef NO_ERROR #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/blind_data.hpp" @@ -63,10 +64,19 @@ namespace media_reader { } struct BufferData { - BufferData(byte *d) { data_.reset(d); } - BufferData(size_t sz) { data_.reset(new (std::align_val_t(1024)) byte[sz]); } - std::unique_ptr data_{ - nullptr}; // using long long which should get result byte alignment + struct BufferDeleter { + void operator()(byte *ptr) const { + operator delete[](ptr, std::align_val_t(1024)); + } + }; + + BufferData(byte *d) : data_(d, BufferDeleter()) {} + BufferData(size_t sz) : data_(nullptr, BufferDeleter()) { + byte *ptr = static_cast(operator new[](sz, std::align_val_t(1024))); + data_.reset(ptr); + } + + std::unique_ptr data_; }; typedef std::shared_ptr BufferDataPtr; @@ -80,4 +90,4 @@ namespace media_reader { }; } // namespace media_reader -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/media_reader/enums.hpp b/include/xstudio/media_reader/enums.hpp index 09fb750be..8429bd569 100644 --- a/include/xstudio/media_reader/enums.hpp +++ b/include/xstudio/media_reader/enums.hpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#undef NO_ERROR namespace xstudio { namespace media_reader { diff --git a/include/xstudio/media_reader/image_buffer.hpp b/include/xstudio/media_reader/image_buffer.hpp index df0698ea4..cc7d6349c 100644 --- a/include/xstudio/media_reader/image_buffer.hpp +++ b/include/xstudio/media_reader/image_buffer.hpp @@ -105,7 +105,8 @@ namespace media_reader { when_to_display_(o.when_to_display_), plugin_blind_data_(o.plugin_blind_data_), tts_(o.tts_), - frame_id_(o.frame_id_) {} + frame_id_(o.frame_id_), + bookmarks_(o.bookmarks_) {} ImageBufPtr &operator=(const ImageBufPtr &o) { Base &b = static_cast(*this); @@ -116,15 +117,34 @@ namespace media_reader { plugin_blind_data_ = o.plugin_blind_data_; tts_ = o.tts_; frame_id_ = o.frame_id_; + bookmarks_ = o.bookmarks_; return *this; } ~ImageBufPtr() = default; bool operator==(const ImageBufPtr &o) const { - return this->get() == o.get() && - colour_pipe_data_->cache_id_ == o.colour_pipe_data_->cache_id_ && - tts_ == o.tts_ && colour_pipe_uniforms_ == o.colour_pipe_uniforms_; + if (this->get() != o.get()) { + return false; + } + + if (colour_pipe_data_ && o.colour_pipe_data_) { + if (colour_pipe_data_->cache_id_ != o.colour_pipe_data_->cache_id_) { + return false; + } + } else if (colour_pipe_data_ || o.colour_pipe_data_) { + return false; + } + + if (tts_ != o.tts_) { + return false; + } + + if (colour_pipe_uniforms_ != o.colour_pipe_uniforms_) { + return false; + } + + return true; } bool operator<(const ImageBufPtr &o) const { return tts_ < o.tts_; } @@ -140,31 +160,57 @@ namespace media_reader { utility::time_point when_to_display_; + // TODO: drop add_plugin_blind_data when all plugins are using + // 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] = 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; } [[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; + return p->second.first; + 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 plugin_blind_data_; + std::map< + utility::Uuid, + std::pair> + plugin_blind_data_; [[nodiscard]] const timebase::flicks &timeline_timestamp() const { return tts_; } void set_timline_timestamp(const timebase::flicks tts) { tts_ = tts; } + [[nodiscard]] const bookmark::BookmarkAndAnnotations &bookmarks() const { + return bookmarks_; + } + void set_bookmarks(const bookmark::BookmarkAndAnnotations &bookmarks) { + bookmarks_ = bookmarks; + } + [[nodiscard]] const media::AVFrameID &frame_id() const { return frame_id_; } void set_frame_id(const media::AVFrameID &id) { frame_id_ = id; } private: timebase::flicks tts_ = timebase::flicks{0}; media::AVFrameID frame_id_; + bookmark::BookmarkAndAnnotations bookmarks_; }; } // namespace media_reader -} // namespace xstudio \ No newline at end of file +} // 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 0741434b6..c3ce7a204 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 @@ -89,7 +89,6 @@ namespace media_reader { std::map media_detail_cache_age_; utility::Uuid uuid_; - caf::actor colour_pipe_manager_; std::vector plugins_; std::map plugins_map_; }; diff --git a/include/xstudio/media_reader/media_reader.hpp b/include/xstudio/media_reader/media_reader.hpp index 86967a997..c1278aa63 100644 --- a/include/xstudio/media_reader/media_reader.hpp +++ b/include/xstudio/media_reader/media_reader.hpp @@ -102,7 +102,7 @@ namespace media_reader { : plugin_manager::PluginFactoryTemplate( uuid, name, - plugin_manager::PluginType::PT_MEDIA_READER, + plugin_manager::PluginFlags::PF_MEDIA_READER, false, author, description, diff --git a/include/xstudio/media_reader/media_reader_actor.hpp b/include/xstudio/media_reader/media_reader_actor.hpp index 318a253ca..7c50f7b30 100644 --- a/include/xstudio/media_reader/media_reader_actor.hpp +++ b/include/xstudio/media_reader/media_reader_actor.hpp @@ -35,6 +35,8 @@ namespace media_reader { inline static const std::string NAME = "GlobalMediaReaderActor"; void prune_readers(); + bool prune_reader(const std::string &key); + std::optional check_cached_reader(const std::string &key, const bool preserve = true); caf::actor add_reader( diff --git a/include/xstudio/module/attribute.hpp b/include/xstudio/module/attribute.hpp index efb8e632b..929bfb429 100644 --- a/include/xstudio/module/attribute.hpp +++ b/include/xstudio/module/attribute.hpp @@ -74,7 +74,8 @@ namespace module { TextAlignment, TextContainerBox, Colour, - HotkeyUuid + HotkeyUuid, + UserData }; inline static const std::map role_names = { @@ -99,7 +100,7 @@ namespace module { {DefaultValue, "default_value"}, {AbbrValue, "short_value"}, {DisabledValue, "disabled_value"}, - {UuidRole, "uuid"}, + {UuidRole, "attr_uuid"}, {Groups, "groups"}, {MenuPaths, "menu_paths"}, {ToolbarPosition, "toolbar_position"}, @@ -113,7 +114,8 @@ namespace module { {TextAlignment, "text_alignment"}, {TextContainerBox, "text_alignment_box"}, {Colour, "attr_colour"}, - {HotkeyUuid, "hotkey_uuid"}}; + {HotkeyUuid, "hotkey_uuid"}, + {UserData, "user_data"}}; ~Attribute() = default; @@ -162,7 +164,7 @@ namespace module { void set_preference_path(const std::string &preference_path); - void expose_in_ui_attrs_group(const std::string &group_name); + void expose_in_ui_attrs_group(const std::string &group_name, bool expose = true); void set_tool_tip(const std::string &tool_tip); diff --git a/include/xstudio/module/attribute_role_data.hpp b/include/xstudio/module/attribute_role_data.hpp index 9d5964a94..94d7c0760 100644 --- a/include/xstudio/module/attribute_role_data.hpp +++ b/include/xstudio/module/attribute_role_data.hpp @@ -86,7 +86,7 @@ namespace module { template [[nodiscard]] const T get() const { try { return std::any_cast(data_); - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { spdlog::warn( "{} Attempt to get AttributeData with type {} as type {}", __PRETTY_FUNCTION__, diff --git a/include/xstudio/module/module.hpp b/include/xstudio/module/module.hpp index 7ec08332d..d538dd9e0 100644 --- a/include/xstudio/module/module.hpp +++ b/include/xstudio/module/module.hpp @@ -20,7 +20,7 @@ namespace module { protected: public: - Module(const std::string name); + Module(const std::string name, const utility::Uuid &uuid = utility::Uuid::generate()); virtual ~Module(); @@ -102,6 +102,8 @@ namespace module { [[nodiscard]] const std::string &name() const { return name_; } + [[nodiscard]] const utility::Uuid &uuid() const { return module_uuid_; } + virtual void deserialise(const nlohmann::json &json); void set_parent_actor_addr(caf::actor_addr addr); @@ -121,6 +123,8 @@ namespace module { const bool both_ways, const bool initial_push_sync); + void unlink_module(caf::actor other_module); + /* If this Module instance is linked to another Module instance, only attributes that have been registered with this function will be synced up between this module and the linked module(s). */ @@ -183,9 +187,6 @@ namespace module { // re-implement to receive callback when the on-screen media changes. To virtual void on_screen_media_changed(caf::actor media) {} - // re-implement to receive callback when the on-screen image changes. - virtual void on_screen_image(const media_reader::ImageBufPtr &) {} - // re-implement to receive callback when the on-screen media changes. virtual void on_screen_media_changed(caf::actor media, caf::actor media_source) {} @@ -195,6 +196,14 @@ namespace module { const std::vector> &bookmark_frame_ranges) {} + // re-implement to execute custom code when your module connects to a viewport. + // For example, exposing certain attributes in a particular named group + // of attributes for the UI layer (see Playhead.cpp) + virtual void connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect); + protected: /* Call this method with your StringChoiceAttribute to expose it in one of xSTUDIO's UI menus. The menu_path argument dictates which parent @@ -221,8 +230,16 @@ namespace module { const std::string top_level_menu, const std::string before = std::string{}); + void make_attribute_visible_in_viewport_toolbar( + Attribute *attr, const bool make_visible = true); + + void expose_attribute_in_model_data( + Attribute *attr, const std::string &model_name, const bool expose = true); + void redraw_viewport(); + virtual utility::JsonStore public_state_data(); + // re-implement this function and use it to add custom hotkeys virtual void register_hotkeys() {} @@ -249,9 +266,12 @@ namespace module { void disable_linking() { linking_disabled_ = true; } void enable_linking() { linking_disabled_ = false; } + std::vector attributes_; + 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_; @@ -264,11 +284,12 @@ namespace module { std::set partially_linked_modules_; std::set fully_linked_modules_; std::set linked_attrs_; + std::set attrs_in_toolbar_; + std::set connected_viewports_; - std::vector attributes_; - bool connected_to_ui_ = {false}; - bool linking_disabled_ = {false}; - utility::Uuid module_uuid_ = {utility::Uuid::generate()}; + bool connected_to_ui_ = {false}; + bool linking_disabled_ = {false}; + utility::Uuid module_uuid_; std::string name_; std::set attrs_waiting_to_update_prefs_; diff --git a/include/xstudio/playhead/playhead.hpp b/include/xstudio/playhead/playhead.hpp index 3c938842f..69bf68e23 100644 --- a/include/xstudio/playhead/playhead.hpp +++ b/include/xstudio/playhead/playhead.hpp @@ -41,7 +41,7 @@ namespace playhead { [[nodiscard]] bool playing() const { return playing_->value(); } [[nodiscard]] bool forward() const { return forward_->value(); } [[nodiscard]] AutoAlignMode auto_align_mode() const; - [[nodiscard]] LoopMode loop() const { return loop_; } + [[nodiscard]] int loop() const { return loop_mode_->value(); } [[nodiscard]] CompareMode compare_mode() const; [[nodiscard]] float velocity() const { return velocity_->value(); } [[nodiscard]] float velocity_multiplier() const { @@ -49,25 +49,25 @@ namespace playhead { } [[nodiscard]] utility::TimeSourceMode play_rate_mode() const { return play_rate_mode_; } [[nodiscard]] utility::FrameRate playhead_rate() const { return playhead_rate_; } - [[nodiscard]] utility::Uuid source() const { return source_uuid_; } [[nodiscard]] timebase::flicks loop_start() const { - return use_loop_range_ + return use_loop_range() ? loop_start_ : timebase::flicks(std::numeric_limits::lowest()); } [[nodiscard]] timebase::flicks loop_end() const { - return use_loop_range_ + return use_loop_range() ? loop_end_ : timebase::flicks(std::numeric_limits::max()); } - [[nodiscard]] bool use_loop_range() const { return use_loop_range_; } + [[nodiscard]] bool use_loop_range() const { return do_looping_->value(); } [[nodiscard]] timebase::flicks duration() const { return duration_; } [[nodiscard]] timebase::flicks effective_frame_period() const; 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_ = loop; } + void set_loop(const LoopMode loop = LM_LOOP) { loop_mode_->set_value(loop); } 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; } @@ -76,7 +76,6 @@ namespace playhead { velocity_multiplier_->set_value(velocity_multiplier); } void set_playhead_rate(const utility::FrameRate &rate) { playhead_rate_ = rate; } - void set_source(const utility::Uuid &uuid) { source_uuid_ = uuid; } void set_duration(const timebase::flicks duration); void set_compare_mode(const CompareMode mode); @@ -91,12 +90,14 @@ namespace playhead { void hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) override; + void connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect) override; + inline static const std::chrono::milliseconds playback_step_increment = std::chrono::milliseconds(5); - private: - void play_faster(const bool forwards); - inline static const std::vector> compare_mode_names = { {CM_STRING, "String", "Str", true}, @@ -106,6 +107,9 @@ namespace playhead { {CM_GRID, "Grid", "Grid", false}, {CM_OFF, "Off", "Off", true}}; + private: + void play_faster(const bool forwards); + inline static const std::vector< std::tuple> auto_align_mode_names = { @@ -113,16 +117,13 @@ namespace playhead { {AAM_ALIGN_FRAMES, "On", "On", true}, {AAM_ALIGN_TRIM, "On (Trim)", "Trim", true}}; - LoopMode loop_{LM_LOOP}; utility::TimeSourceMode play_rate_mode_{utility::TimeSourceMode::DYNAMIC}; utility::FrameRate playhead_rate_; - utility::Uuid source_uuid_; timebase::flicks position_; timebase::flicks duration_; timebase::flicks loop_start_; timebase::flicks loop_end_; - bool use_loop_range_{false}; utility::Uuid play_hotkey_; utility::Uuid play_forwards_hotkey_; @@ -153,6 +154,19 @@ namespace playhead { module::BooleanAttribute *restore_play_state_after_scrub_; module::IntegerAttribute *viewport_scrub_sensitivity_; + module::IntegerAttribute *loop_mode_; + module::IntegerAttribute *loop_start_frame_; + module::IntegerAttribute *loop_end_frame_; + module::IntegerAttribute *playhead_logical_frame_; + module::IntegerAttribute *playhead_media_logical_frame_; + module::IntegerAttribute *playhead_media_frame_; + module::IntegerAttribute *duration_frames_; + module::StringAttribute *current_source_frame_timecode_; + module::StringAttribute *current_media_uuid_; + module::StringAttribute *current_media_source_uuid_; + module::BooleanAttribute *do_looping_; + module::IntegerAttribute *audio_delay_millisecs_; + bool was_playing_when_scrub_started_ = {false}; }; } // namespace playhead diff --git a/include/xstudio/playhead/playhead_actor.hpp b/include/xstudio/playhead/playhead_actor.hpp index 405ce15bc..e90136ee4 100644 --- a/include/xstudio/playhead/playhead_actor.hpp +++ b/include/xstudio/playhead/playhead_actor.hpp @@ -67,7 +67,6 @@ namespace playhead { const media::MediaKeyVector &new_keys = media::MediaKeyVector(), const media::MediaKeyVector &remove_keys = media::MediaKeyVector()); void rebuild_cached_frames_status(); - void rebuild_bookmark_frames_ranges(); void select_media(const utility::UuidList &selection, caf::typed_response_promise &rp); void align_clip_frame_numbers(); @@ -112,7 +111,6 @@ namespace playhead { caf::actor image_cache_; caf::actor pre_reader_; caf::actor_addr playlist_selection_addr_; - utility::Uuid current_media_uuid_; utility::Uuid previous_source_uuid_; utility::Uuid current_source_uuid_; utility::Uuid key_playhead_uuid_; diff --git a/include/xstudio/playhead/playhead_global_events_actor.hpp b/include/xstudio/playhead/playhead_global_events_actor.hpp index 8bc20ac5a..e485d50e6 100644 --- a/include/xstudio/playhead/playhead_global_events_actor.hpp +++ b/include/xstudio/playhead/playhead_global_events_actor.hpp @@ -36,11 +36,11 @@ namespace playhead { caf::behavior make_behavior() override { return behavior_; } - protected: caf::behavior behavior_; caf::actor event_group_; caf::actor on_screen_playhead_; + std::map viewports_; }; } // namespace playhead } // namespace xstudio diff --git a/include/xstudio/playhead/sub_playhead.hpp b/include/xstudio/playhead/sub_playhead.hpp index 114bf66c5..76c145a1a 100644 --- a/include/xstudio/playhead/sub_playhead.hpp +++ b/include/xstudio/playhead/sub_playhead.hpp @@ -80,7 +80,6 @@ namespace playhead { std::shared_ptr get_frame( const timebase::flicks &time, - int &logical_frame, timebase::flicks &frame_period, timebase::flicks &timeline_pts); @@ -92,9 +91,22 @@ namespace playhead { void set_in_and_out_frames(); - void get_bookmark_ranges( - const std::vector &bookmark_details, - std::vector> &result); + typedef std::vector> BookmarkRanges; + + void extend_bookmark_frame( + const bookmark::BookmarkDetail &detail, + const int logical_playhead_frame, + BookmarkRanges &bookmark_ranges); + + void full_bookmarks_update(); + + void fetch_bookmark_annotations(BookmarkRanges bookmark_ranges); + + void add_annotations_data_to_frame(media_reader::ImageBufPtr &frame); + + void bookmark_deleted(const utility::Uuid &bookmark_uuid); + + void bookmark_changed(const utility::UuidActor bookmark); protected: int logical_frame_ = {0}; @@ -125,10 +137,12 @@ namespace playhead { utility::FrameRate override_frame_rate_; const media::MediaType media_type_; std::shared_ptr previous_frame_; + utility::UuidSet all_media_uuids_; - std::map timeline_logical_frame_pts_; media::FrameTimeMap full_timeline_frames_; media::FrameTimeMap::iterator in_frame_, out_frame_, first_frame_, last_frame_; + xstudio::bookmark::BookmarkAndAnnotations bookmarks_; + BookmarkRanges bookmark_ranges_; typedef std::pair ImageAndLut; @@ -136,4 +150,4 @@ namespace playhead { bool up_to_date_{false}; }; } // namespace playhead -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/plugin_manager/enums.hpp b/include/xstudio/plugin_manager/enums.hpp index 5a1860ca8..8a14c8335 100644 --- a/include/xstudio/plugin_manager/enums.hpp +++ b/include/xstudio/plugin_manager/enums.hpp @@ -4,17 +4,21 @@ namespace xstudio { namespace plugin_manager { typedef enum { - PT_CUSTOM = 1, - PT_MEDIA_READER, - PT_MEDIA_HOOK, - PT_MEDIA_METADATA, - PT_COLOUR_MANAGEMENT, - PT_COLOUR_OPERATION, - PT_DATA_SOURCE, - PT_VIEWPORT_OVERLAY, - PT_HEAD_UP_DISPLAY, - PT_UTILITY, - } PluginType; + PF_CUSTOM = 1 << 0, + PF_MEDIA_READER = 1 << 1, + PF_MEDIA_HOOK = 1 << 2, + PF_MEDIA_METADATA = 1 << 3, + PF_COLOUR_MANAGEMENT = 1 << 4, + 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, + } PluginFlags; -} + typedef unsigned int PluginType; + +} // namespace plugin_manager } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/plugin_manager/plugin_base.hpp b/include/xstudio/plugin_manager/plugin_base.hpp index a6a6543e9..3189e2ff2 100644 --- a/include/xstudio/plugin_manager/plugin_base.hpp +++ b/include/xstudio/plugin_manager/plugin_base.hpp @@ -13,6 +13,32 @@ namespace xstudio { namespace plugin { + class GPUPreDrawHook { + + public: + /* Plugins can provide this class to allow a way to execute any GPU + draw/compute functions *before* the viewport is drawn to the screen. + Note that 'image' is a non-const reference and as-such the colour + pipeline data object ptr that is a member of ImageBufPtr can be + overwritten with new data that the plugin (if it's a ColourOP) can + access at draw time (like LUTS & texture). Similiarly ViewportOverlay + plugins could use this to do pixel analysis and put the result into + texture data. This could be useful for doing waveform overlays, for + example. + + Note that plugins can add their own data to media via the bookmarks + system which will then be available here at draw time as metadata on + the ImageBufPtr we receive here. */ + + virtual void pre_viewport_draw_gpu_hook( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + xstudio::media_reader::ImageBufPtr &image) = 0; + }; + + typedef std::shared_ptr GPUPreDrawHookPtr; + class ViewportOverlayRenderer { public: @@ -29,7 +55,7 @@ namespace plugin { 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) {}; [[nodiscard]] virtual RenderPass preferred_render_pass() const { return AfterImage; } }; @@ -57,36 +83,44 @@ namespace plugin { caf::message_handler message_handler_; - virtual utility::BlindDataObjectPtr prepare_render_data( + 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 { + 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 std::string /*viewport_name*/, + const bool /*playhead_playing*/ + ) {} + virtual ViewportOverlayRendererPtr make_overlay_renderer(const int /*viewer_index*/) { return ViewportOverlayRendererPtr(); } - utility::Uuid create_bookmark_on_current_frame(bookmark::BookmarkDetail bmd); + // 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*/) { + return GPUPreDrawHookPtr(); + } // reimplement this function in an annotations plugin to return your // custom annotation class, based on bookmark::AnnotationBase base class. - virtual std::shared_ptr + virtual bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &anno_data) { - return std::shared_ptr(); + return bookmark::AnnotationBasePtr(); } - void push_annotation_to_bookmark(std::shared_ptr annotation); - - std::shared_ptr - fetch_annotation(const utility::Uuid &bookmark_uuid); - - std::map - clear_annotations_and_bookmarks(std::vector bookmark_ids); - - void restore_annotations_and_bookmarks( - const std::map &bookmarks_data); - /* Function signature for on screen frame change callback - reimplement to receive this event */ virtual void on_screen_frame_changed( @@ -97,13 +131,6 @@ namespace plugin { const utility::Timecode & // media frame timecode ) {} - /* Function signature for on screen annotation change - reimplement to - receive this event */ - virtual void on_screen_annotation_changed( - std::vector> // ptrs to annotation - // data - ) {} - /* Function signature for on screen annotation change - reimplement to receive this event */ virtual void on_screen_media_changed( @@ -120,19 +147,37 @@ namespace plugin { 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 current (on screen) frame + of for the entire duration for the media currently showing on the given named + viewport. */ + utility::Uuid create_bookmark_on_current_media( + const std::string &viewport_name, + const std::string &bookmark_subject, + const bookmark::BookmarkDetail &detail, + const bool bookmark_entire_duratio = false); + + + /* 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, + const bool annotation_is_empty); + + void update_bookmark_detail( + const utility::Uuid bookmark_id, const bookmark::BookmarkDetail &bmd); + + private: // re-implement to receive callback when the on-screen media changes. To void on_screen_media_changed(caf::actor media) override; void session_changed(caf::actor session); - void check_if_onscreen_bookmarks_have_changed( - const int media_frame, const bool force_update = false); - void current_viewed_playhead_changed(caf::actor_addr playhead_addr); - std::vector> bookmark_frame_ranges_; - utility::UuidList onscreen_bookmarks_; + void join_studio_events(); + int playhead_logical_frame_ = {-1}; caf::actor_addr active_viewport_playhead_; diff --git a/include/xstudio/plugin_manager/plugin_factory.hpp b/include/xstudio/plugin_manager/plugin_factory.hpp index c913d3cf6..0e479a93f 100644 --- a/include/xstudio/plugin_manager/plugin_factory.hpp +++ b/include/xstudio/plugin_manager/plugin_factory.hpp @@ -42,7 +42,7 @@ namespace plugin_manager { PluginFactoryTemplate( utility::Uuid uuid, std::string name = "", - PluginType type = PluginType::PT_CUSTOM, + PluginType type = PluginFlags::PF_CUSTOM, bool resident = false, std::string author = "", std::string description = "", @@ -84,6 +84,9 @@ namespace plugin_manager { semver::version version_; std::string ui_widget_string_; std::string ui_menu_string_; + + private: + caf::actor instance_; }; diff --git a/include/xstudio/plugin_manager/plugin_manager.hpp b/include/xstudio/plugin_manager/plugin_manager.hpp index 74c56001b..2b7e03a37 100644 --- a/include/xstudio/plugin_manager/plugin_manager.hpp +++ b/include/xstudio/plugin_manager/plugin_manager.hpp @@ -100,15 +100,13 @@ namespace plugin_manager { [[nodiscard]] caf::actor spawn( caf::blocking_actor &sys, const utility::Uuid &uuid, - const utility::JsonStore &json = utility::JsonStore(), - const bool singleton = false); + const utility::JsonStore &json = utility::JsonStore()); [[nodiscard]] std::string spawn_widget_ui(const utility::Uuid &uuid); [[nodiscard]] std::string spawn_menu_ui(const utility::Uuid &uuid); private: std::list plugin_paths_; std::map factories_; - std::map singletons_; }; } // namespace plugin_manager } // namespace xstudio diff --git a/include/xstudio/plugin_manager/plugin_utility.hpp b/include/xstudio/plugin_manager/plugin_utility.hpp index e1d1c697d..494f65f89 100644 --- a/include/xstudio/plugin_manager/plugin_utility.hpp +++ b/include/xstudio/plugin_manager/plugin_utility.hpp @@ -49,7 +49,7 @@ namespace plugin_manager { : plugin_manager::PluginFactoryTemplate( uuid, name, - plugin_manager::PluginType::PT_UTILITY, + plugin_manager::PluginFlags::PF_UTILITY, true, author, description, diff --git a/include/xstudio/shotgun_client/shotgun_client.hpp b/include/xstudio/shotgun_client/shotgun_client.hpp index 2f09ed941..0da8b3007 100644 --- a/include/xstudio/shotgun_client/shotgun_client.hpp +++ b/include/xstudio/shotgun_client/shotgun_client.hpp @@ -38,7 +38,7 @@ namespace shotgun_client { IS_NOT, LESS_THAN, GREATER_THAN, - IN, + IN_OPERATOR, NOT_IN, BETWEEN, NOT_BETWEEN, @@ -70,7 +70,7 @@ namespace shotgun_client { {ConditionalOperator::IS_NOT, "is_not"}, {ConditionalOperator::LESS_THAN, "less_than"}, {ConditionalOperator::GREATER_THAN, "greater_than"}, - {ConditionalOperator::IN, "in"}, + {ConditionalOperator::IN_OPERATOR, "in"}, {ConditionalOperator::NOT_IN, "not_in"}, {ConditionalOperator::BETWEEN, "between"}, {ConditionalOperator::NOT_BETWEEN, "not_between"}, @@ -431,7 +431,7 @@ namespace shotgun_client { } Field &in(const std::vector value) { - condition_ = ConditionalOperator::IN; + condition_ = ConditionalOperator::IN_OPERATOR; value_ = std::move(value); null_ = false; return *this; @@ -704,7 +704,7 @@ namespace shotgun_client { } Field &in(const std::vector value) { - condition_ = ConditionalOperator::IN; + condition_ = ConditionalOperator::IN_OPERATOR; value_ = std::move(value); null_ = false; return *this; @@ -1184,19 +1184,37 @@ namespace shotgun_client { RelationType(const utility::JsonStore &jsn) : Field(jsn) {} ~RelationType() override = default; - RelationType &is(const utility::JsonStore value) { + RelationType &is(const utility::JsonStore &value) { Field::is(value); return *this; } - RelationType &is_not(const utility::JsonStore value) { + RelationType &is_not(const utility::JsonStore &value) { Field::is_not(value); return *this; } - RelationType &in(const std::vector value) { + RelationType &name_is(const std::string &value) { + nlohmann::json jvalue; + jvalue = value; + Field::name_is(utility::JsonStore(jvalue)); + return *this; + } + RelationType &name_contains(const std::string &value) { + nlohmann::json jvalue; + jvalue = value; + Field::name_contains(utility::JsonStore(jvalue)); + return *this; + } + RelationType &name_not_contains(const std::string &value) { + nlohmann::json jvalue; + jvalue = value; + Field::name_not_contains(utility::JsonStore(jvalue)); + return *this; + } + RelationType &in(const std::vector &value) { Field::in(value); return *this; } - RelationType ¬_in(const std::vector value) { + RelationType ¬_in(const std::vector &value) { Field::not_in(value); return *this; } diff --git a/include/xstudio/studio/studio_actor.hpp b/include/xstudio/studio/studio_actor.hpp index 3946d7d64..8d693485c 100644 --- a/include/xstudio/studio/studio_actor.hpp +++ b/include/xstudio/studio/studio_actor.hpp @@ -24,6 +24,12 @@ namespace studio { caf::behavior behavior_; Studio base_; caf::actor session_; + + struct QuickviewRequest { + utility::UuidActorVector media_actors; + std::string compare_mode; + }; + std::vector quickview_requests_; }; } // namespace studio } // namespace xstudio diff --git a/include/xstudio/timeline/clip.hpp b/include/xstudio/timeline/clip.hpp index 5331eb327..6dc47b7c3 100644 --- a/include/xstudio/timeline/clip.hpp +++ b/include/xstudio/timeline/clip.hpp @@ -28,6 +28,8 @@ namespace timeline { ~Clip() override = default; [[nodiscard]] utility::JsonStore serialise() const override; + [[nodiscard]] Clip duplicate() const; + [[nodiscard]] const Item &item() const { return item_; } [[nodiscard]] Item &item() { return item_; } @@ -36,7 +38,12 @@ namespace timeline { } [[nodiscard]] const utility::Uuid &media_uuid() const { return media_uuid_; } - void set_media_uuid(const utility::Uuid &media_uuid) { media_uuid_ = media_uuid; } + void 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; + } private: Item item_; diff --git a/include/xstudio/timeline/clip_actor.hpp b/include/xstudio/timeline/clip_actor.hpp index 683a2149c..77eb682ea 100644 --- a/include/xstudio/timeline/clip_actor.hpp +++ b/include/xstudio/timeline/clip_actor.hpp @@ -14,6 +14,7 @@ namespace xstudio { namespace timeline { class ClipActor : public caf::event_based_actor { public: + ClipActor(caf::actor_config &cfg, const utility::JsonStore &jsn); ClipActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &item); ClipActor( caf::actor_config &cfg, diff --git a/include/xstudio/timeline/gap.hpp b/include/xstudio/timeline/gap.hpp index d84d8f9bd..9d3bfc10e 100644 --- a/include/xstudio/timeline/gap.hpp +++ b/include/xstudio/timeline/gap.hpp @@ -24,6 +24,7 @@ namespace timeline { ~Gap() override = default; [[nodiscard]] utility::JsonStore serialise() const override; + [[nodiscard]] Gap duplicate() const; [[nodiscard]] const Item &item() const { return item_; } [[nodiscard]] Item &item() { return item_; } diff --git a/include/xstudio/timeline/item.hpp b/include/xstudio/timeline/item.hpp index 691090637..5327659cb 100644 --- a/include/xstudio/timeline/item.hpp +++ b/include/xstudio/timeline/item.hpp @@ -25,12 +25,16 @@ namespace timeline { IT_REMOVE = 0x6L, IT_SPLICE = 0x7L, IT_NAME = 0x8L, + IT_FLAG = 0x9L, + IT_PROP = 0x10L, } ItemAction; class Item; using Items = std::list; + using ResolvedItem = std::tuple; + typedef std::function ItemEventFunc; class Item : private Items { @@ -88,7 +92,7 @@ namespace timeline { using Items::back; using Items::front; - // using Items::insert; + // these circumvent the event handler using Items::emplace_back; using Items::emplace_front; using Items::pop_back; @@ -117,11 +121,13 @@ namespace timeline { [[nodiscard]] std::optional available_duration() const; [[nodiscard]] std::optional active_duration() const; - [[nodiscard]] utility::FrameRate trimmed_start() const; [[nodiscard]] std::optional available_start() const; [[nodiscard]] std::optional active_start() const; + [[nodiscard]] utility::FrameRateDuration trimmed_frame_start() const { + return trimmed_range().frame_start(); + } [[nodiscard]] utility::FrameRateDuration trimmed_frame_duration() const { return trimmed_range().frame_duration(); } @@ -129,6 +135,15 @@ namespace timeline { [[nodiscard]] std::optional available_frame_duration() const; + [[nodiscard]] std::optional> + item_at_frame(const int frame) const; + + [[nodiscard]] std::optional item_at_index(const int index) const; + + [[nodiscard]] utility::FrameRange range_at_index(const int item_index) const; + [[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]] caf::actor_addr actor_addr() const { return uuid_addr_.second; } [[nodiscard]] caf::actor actor() const { return caf::actor_cast(uuid_addr_.second); @@ -138,6 +153,8 @@ namespace timeline { } [[nodiscard]] bool enabled() const { return enabled_; } [[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; @@ -153,8 +170,12 @@ namespace timeline { 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); utility::JsonStore set_name(const std::string &value); + utility::JsonStore set_flag(const std::string &value); + utility::JsonStore set_prop(const utility::JsonStore &value); void set_system(caf::actor_system *value) { the_system_ = value; } utility::JsonStore set_actor_addr(const caf::actor_addr &value); @@ -168,7 +189,8 @@ namespace timeline { Items::iterator position, const Item &val, const utility::JsonStore &blind = utility::JsonStore()); - utility::JsonStore erase(Items::iterator position); + utility::JsonStore + erase(Items::iterator position, const utility::JsonStore &blind = utility::JsonStore()); utility::JsonStore splice( Items::const_iterator pos, Items &other, @@ -183,6 +205,8 @@ namespace timeline { f.field("ava_rng", x.available_range_), f.field("enabled", x.enabled_), f.field("name", x.name_), + f.field("flag", x.flag_), + f.field("prop", x.prop_), f.field("has_av", x.has_available_range_), f.field("has_ac", x.has_active_range_), f.field("children", x.children())); @@ -193,12 +217,13 @@ 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 - name_ == other.name_; + flag_ == other.flag_ and prop_ == other.prop_ and name_ == other.name_; } - [[nodiscard]] std::optional> resolve_time( + [[nodiscard]] std::optional resolve_time( const utility::FrameRate &time, - const media::MediaType mt = media::MediaType::MT_IMAGE) const; + const media::MediaType mt = media::MediaType::MT_IMAGE, + const utility::UuidSet &focus = utility::UuidSet()) const; void undo(const utility::JsonStore &event); void redo(const utility::JsonStore &event); @@ -221,6 +246,8 @@ namespace timeline { void set_actor_addr_direct(const caf::actor_addr &value); void set_enabled_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); [[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; @@ -236,6 +263,8 @@ namespace timeline { bool has_active_range_{false}; bool enabled_{true}; std::string name_{}; + std::string flag_{}; + utility::JsonStore prop_{}; // not sure if this is safe.. caf::actor_system *the_system_{nullptr}; @@ -243,19 +272,27 @@ namespace timeline { bool recursive_bind_{false}; }; - inline Items::const_iterator find_item(const Items &items, const utility::Uuid &uuid) { + 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) { return obj.uuid() == uuid; }); + + // search children if (it == items.cend()) { for (const auto &i : items) { auto ii = find_item(i.children(), uuid); - if (ii != i.cend()) { - it = ii; + + if (ii) { + it = *ii; break; } } } + + if (it == items.cend()) + return {}; + return it; } @@ -296,17 +333,17 @@ namespace timeline { inline auto sum_trimmed_duration(const Items &items) { auto duration = utility::FrameRate(); - for (const auto &i : items) { + for (const auto &i : items) duration += i.trimmed_duration(); - } + return duration; } inline auto max_trimmed_duration(const Items &items) { auto duration = utility::FrameRate(); - for (const auto &i : items) { + for (const auto &i : items) duration = std::max(i.trimmed_duration(), duration); - } + return duration; } diff --git a/include/xstudio/timeline/stack.hpp b/include/xstudio/timeline/stack.hpp index d0afa04b6..1b40324a2 100644 --- a/include/xstudio/timeline/stack.hpp +++ b/include/xstudio/timeline/stack.hpp @@ -24,6 +24,7 @@ namespace timeline { ~Stack() override = default; [[nodiscard]] utility::JsonStore serialise() const override; + [[nodiscard]] Stack duplicate() const; [[nodiscard]] const Item &item() const { return item_; } [[nodiscard]] Item &item() { return item_; } diff --git a/include/xstudio/timeline/stack_actor.hpp b/include/xstudio/timeline/stack_actor.hpp index 110c8cc3c..5b70d4de1 100644 --- a/include/xstudio/timeline/stack_actor.hpp +++ b/include/xstudio/timeline/stack_actor.hpp @@ -11,6 +11,7 @@ namespace xstudio { namespace timeline { class StackActor : public caf::event_based_actor { public: + StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn); StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &item); StackActor( caf::actor_config &cfg, @@ -31,6 +32,27 @@ namespace timeline { caf::actor deserialise(const utility::JsonStore &value, const bool replace_item = false); void item_event_callback(const utility::JsonStore &event, Item &item); + void insert_items( + const int index, + const utility::UuidActorVector &uav, + caf::typed_response_promise rp); + + void remove_items( + const int index, + const int count, + caf::typed_response_promise< + std::pair>> rp); + + void erase_items( + const int index, + const int count, + caf::typed_response_promise rp); + + void move_items( + const int src_index, + const int count, + const int dst_index, + caf::typed_response_promise rp); private: caf::behavior behavior_; diff --git a/include/xstudio/timeline/timeline.hpp b/include/xstudio/timeline/timeline.hpp index a332a0be6..b7c39a897 100644 --- a/include/xstudio/timeline/timeline.hpp +++ b/include/xstudio/timeline/timeline.hpp @@ -14,6 +14,17 @@ namespace xstudio { namespace timeline { + static const std::set TIMELINE_TYPES( + {"Clip", + "Track", + "Video Track", + "Audio Track", + "Gap", + "Stack", + "TimelineItem", + "Timeline"}); + + class Timeline : public utility::Container { public: Timeline( @@ -25,6 +36,7 @@ namespace timeline { ~Timeline() override = default; [[nodiscard]] utility::JsonStore serialise() const override; + [[nodiscard]] Timeline duplicate() const; [[nodiscard]] const Item &item() const { return item_; } [[nodiscard]] Item &item() { return item_; } @@ -67,6 +79,13 @@ namespace timeline { return media_list_.contains(uuid); } + [[nodiscard]] utility::UuidSet &focus_list() { return focus_list_; } + [[nodiscard]] utility::UuidSet focus_list() const { return focus_list_; } + + void set_focus_list(const utility::UuidSet &list) { focus_list_ = list; } + void set_focus_list(const utility::UuidVector &list) { + focus_list_ = utility::UuidSet(list.begin(), list.end()); + } // [[nodiscard]] utility::UuidList tracks() const { return tracks_.uuids(); } // void insert_track( @@ -95,6 +114,7 @@ namespace timeline { private: Item item_; utility::UuidListContainer media_list_; + utility::UuidSet focus_list_; // utility::UuidListContainer tracks_; // utility::FrameRateDuration start_time_; diff --git a/include/xstudio/timeline/timeline_actor.hpp b/include/xstudio/timeline/timeline_actor.hpp index 4c86aadfb..0f57b7920 100644 --- a/include/xstudio/timeline/timeline_actor.hpp +++ b/include/xstudio/timeline/timeline_actor.hpp @@ -49,6 +49,22 @@ namespace timeline { const utility::Uuid &before_uuid = utility::Uuid()); bool remove_media(caf::actor actor, const utility::Uuid &uuid); + void insert_items( + const int index, + const utility::UuidActorVector &uav, + caf::typed_response_promise rp); + + void remove_items( + const int index, + const int count, + caf::typed_response_promise< + std::pair>> rp); + + void erase_items( + const int index, + const int count, + caf::typed_response_promise rp); + void sort_alphabetically(); void on_exit() override; diff --git a/include/xstudio/timeline/track.hpp b/include/xstudio/timeline/track.hpp index e7fb1889d..61e312fae 100644 --- a/include/xstudio/timeline/track.hpp +++ b/include/xstudio/timeline/track.hpp @@ -29,6 +29,7 @@ namespace timeline { [[nodiscard]] utility::JsonStore serialise() const override; + [[nodiscard]] Track duplicate() const; void set_media_type(const media::MediaType media_type); [[nodiscard]] media::MediaType media_type() const { return media_type_; } diff --git a/include/xstudio/timeline/track_actor.hpp b/include/xstudio/timeline/track_actor.hpp index ed1bb0a65..8b9083c8c 100644 --- a/include/xstudio/timeline/track_actor.hpp +++ b/include/xstudio/timeline/track_actor.hpp @@ -13,6 +13,7 @@ namespace xstudio { namespace timeline { class TrackActor : public caf::event_based_actor { public: + TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn); TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &item); TrackActor( caf::actor_config &cfg, @@ -36,6 +37,56 @@ namespace timeline { deserialise(const utility::JsonStore &value, const bool replace_item = false); void item_event_callback(const utility::JsonStore &event, Item &item); + void split_item( + const Items::const_iterator &item, + const int frame, + caf::typed_response_promise rp); + + void insert_items( + const int index, + const utility::UuidActorVector &uav, + caf::typed_response_promise rp); + + void insert_items_at_frame( + const int frame, + const utility::UuidActorVector &uav, + caf::typed_response_promise rp); + + void remove_items_at_frame( + const int frame, + const int duration, + caf::typed_response_promise< + std::pair>> rp); + + void remove_items( + const int index, + const int count, + caf::typed_response_promise< + std::pair>> rp); + + void erase_items_at_frame( + const int frame, + const int duration, + caf::typed_response_promise rp); + + void erase_items( + const int index, + const int count, + caf::typed_response_promise rp); + + void move_items( + const int src_index, + const int count, + const int dst_index, + caf::typed_response_promise rp); + + void move_items_at_frame( + const int frame, + const int duration, + const int dest_frame, + const bool insert, + caf::typed_response_promise rp); + private: caf::behavior behavior_; Track base_; diff --git a/include/xstudio/ui/canvas/canvas.hpp b/include/xstudio/ui/canvas/canvas.hpp new file mode 100644 index 000000000..5c82c4a9a --- /dev/null +++ b/include/xstudio/ui/canvas/canvas.hpp @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +#include "xstudio/ui/canvas/stroke.hpp" +#include "xstudio/ui/canvas/caption.hpp" +#include "xstudio/ui/canvas/handle.hpp" +#include "xstudio/utility/chrono.hpp" + + +namespace xstudio { +namespace ui { + + namespace opengl { + class OpenGLCanvasRenderer; + } + + namespace canvas { + + class Canvas; + + /* Class CanvasUndoRedo + + N.B. any subclass of this must access the Canvas passes into redo + and undo directly to its member data as a friend class, not via public + accessor methods. The reason is that redo and undo are excecuted by the + Canvas class itself *AFTER* it has acquired a unique_lock on its mutex. + As such, if the CanvasUndoRedo class tries to use a public method on the + Canvas during the undo or redo calls a deadlock will result. */ + class CanvasUndoRedo { + public: + virtual void redo(Canvas *) = 0; + virtual void undo(Canvas *) = 0; + }; + + typedef std::shared_ptr CanvasUndoRedoPtr; + + /* Class Canvas + + Note this class is thread safe EXCEPT for the begin()/end() iterators. + When looping over the iterators call 'read_lock()' first and 'read_unlock()' + afterwards. + */ + class Canvas { + + using Item = std::variant; + using ItemVec = std::vector; + + public: + Canvas() : uuid_(utility::Uuid::generate()) {} + Canvas(const Canvas &o) + : items_(o.items_), + 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_) {} + + bool operator==(const Canvas &o) const { + std::shared_lock l(mutex_); + return items_ == o.items_; + } + + 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_; + last_change_time_ = o.last_change_time_; + uuid_ = o.uuid_; + return *this; + } + + ItemVec::const_iterator begin() const { return items_.begin(); } + ItemVec::const_iterator end() const { return items_.end(); } + + // call this before using the above iterators + void read_lock() const { mutex_.lock_shared(); } + + // call this after using the above iterators + void read_unlock() const { mutex_.unlock_shared(); } + + bool empty() const { + std::shared_lock l(mutex_); + return items_.empty() && !current_item_; + } + size_t size() const { + std::shared_lock l(mutex_); + return items_.size(); + } + + void clear(const bool clear_history = false); + + void undo(); + void redo(); + + // Drawing interface follows start / update / end pattern. + // Calling end_draw() will append to the undo stack. + + // Stroke + + void start_stroke( + const utility::ColourTriplet &colour, + float thickness, + float softness, + float opacity); + void start_erase_stroke(float thickness); + void update_stroke(const Imath::V2f &pt); + // Delete the strokes when reaching 0 opacity. + bool fade_all_strokes(float opacity); + + // Shapes + + void + start_square(const utility::ColourTriplet &colour, float thickness, float opacity); + void update_square(const Imath::V2f &corner1, const Imath::V2f &corner2); + + void + start_circle(const utility::ColourTriplet &colour, float thickness, float opacity); + void update_circle(const Imath::V2f ¢er, float radius); + + void + start_arrow(const utility::ColourTriplet &colour, float thickness, float opacity); + void update_arrow(const Imath::V2f &start, const Imath::V2f &end); + + void + start_line(const utility::ColourTriplet &colour, float thickness, float opacity); + void update_line(const Imath::V2f &start, const Imath::V2f &end); + + // Text + + void start_caption( + const Imath::V2f &position, + const std::string &font_name, + float font_size, + const utility::ColourTriplet &colour, + float opacity, + float wrap_width, + Justification justification, + const utility::ColourTriplet &background_colour, + float background_opacity); + + std::string caption_text() const; + Imath::V2f caption_position() const; + float caption_width() const; + float caption_font_size() const; + utility::ColourTriplet caption_colour() const; + float caption_opacity() const; + std::string caption_font_name() const; + utility::ColourTriplet caption_background_colour() const; + float caption_background_opacity() const; + Imath::Box2f caption_bounding_box() const; + // Returns top and bottom position of the text cursor. + std::array caption_cursor_position() const; + Imath::V2f caption_cursor_bottom() const; + + void update_caption_text(const std::string &text); + void update_caption_position(const Imath::V2f &position); + void update_caption_width(float wrap_width); + void update_caption_font_size(float font_size); + void update_caption_colour(const utility::ColourTriplet &colour); + void update_caption_opacity(float opacity); + void update_caption_font_name(const std::string &font_name); + void update_caption_background_colour(const utility::ColourTriplet &colour); + void update_caption_background_opacity(float opacity); + + bool has_selected_caption() const; + + // Caption selection logic cover these cases: + // * Click on an existing caption: update the cursor position + // * Click on a different caption: select the caption + // * Click in an empty area: unselected the current caption + bool select_caption( + const Imath::V2f &pos, + const Imath::V2f &handle_size, + float viewport_pixel_scale); + + // Returns the hover status for the current selected caption. + // * Hovering on the caption area + // * Hovering on the caption handles (slightly outside the area) + // * Hovering anywhere else outside the caption + HandleHoverState hover_selected_caption_handle( + const Imath::V2f &pos, + const Imath::V2f &handle_size, + float viewport_pixel_scale) const; + + // Returns the bounding box the the caption under the cursor. + Imath::Box2f + hover_caption_bounding_box(const Imath::V2f &pos, float viewport_pixel_scale) const; + + void move_caption_cursor(int key); + + void delete_caption(); + + void end_draw(); + + void changed(); + + const utility::clock::time_point &last_change_time() const { + return last_change_time_; + } + + const utility::Uuid &uuid() const { return uuid_; } + + template bool has_current_item() const { + std::shared_lock l(mutex_); + return has_current_item_nolock(); + } + + template bool has_current_item_nolock() const { + return current_item_ && std::holds_alternative(current_item_.value()); + } + + template T get_current() const { + std::shared_lock l(mutex_); + return std::get(current_item_.value()); + } + + private: + void end_draw_no_lock(); + + HandleHoverState hover_selected_caption_handle_nolock( + const Imath::V2f &pos, + const Imath::V2f &handle_size, + float viewport_pixel_scale) const; + + template T ¤t_item() { + return std::get(current_item_.value()); + } + template const T ¤t_item() const { + return std::get(current_item_.value()); + } + + friend class UndoRedoAdd; + friend class UndoRedoDel; + friend class UndoRedoClear; + + friend void from_json(const nlohmann::json &j, Canvas &c); + friend void to_json(nlohmann::json &j, const Canvas &c); + + private: + utility::clock::time_point last_change_time_; + utility::Uuid uuid_; + + std::optional current_item_; + ItemVec items_; + + std::vector undo_stack_; + std::vector redo_stack_; + + std::string::const_iterator cursor_position_; + + mutable std::shared_mutex mutex_; + }; + + typedef std::shared_ptr CanvasPtr; + + void from_json(const nlohmann::json &j, Canvas &c); + void to_json(nlohmann::json &j, const Canvas &c); + + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/canvas/canvas_undo_redo.hpp b/include/xstudio/ui/canvas/canvas_undo_redo.hpp new file mode 100644 index 000000000..80dfd0a12 --- /dev/null +++ b/include/xstudio/ui/canvas/canvas_undo_redo.hpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/ui/canvas/canvas.hpp" + + +namespace xstudio { +namespace ui { + namespace canvas { + + class UndoRedoAdd : public CanvasUndoRedo { + + public: + UndoRedoAdd(const Canvas::Item &item) : item_(item) {} + + void redo(Canvas *) override; + void undo(Canvas *) override; + + Canvas::Item item_; + }; + + class UndoRedoDel : public CanvasUndoRedo { + + public: + UndoRedoDel(const Canvas::Item &item) : item_(item) {} + + void redo(Canvas *) override; + void undo(Canvas *) override; + + Canvas::Item item_; + }; + + class UndoRedoClear : public CanvasUndoRedo { + + public: + UndoRedoClear(const Canvas::ItemVec &items) : items_(items) {} + + void redo(Canvas *) override; + void undo(Canvas *) override; + + Canvas::ItemVec items_; + }; + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/canvas/caption.hpp b/include/xstudio/ui/canvas/caption.hpp new file mode 100644 index 000000000..080372b1c --- /dev/null +++ b/include/xstudio/ui/canvas/caption.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/utility/json_store.hpp" +#include "xstudio/ui/font.hpp" + + +namespace xstudio { +namespace ui { + namespace canvas { + + struct Caption { + + // JSON serialisation requires default constructible types + Caption() = default; + + 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, + const utility::ColourTriplet background_colour, + const float background_opacity); + + bool operator==(const Caption &o) const; + + void modify_text(const std::string &t, std::string::const_iterator &cursor); + + Imath::Box2f bounding_box() const; + + std::vector vertices() const; + + std::string hash() const; + + std::string text; + Imath::V2f position; + 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; + utility::ColourTriplet background_colour = { + utility::ColourTriplet(0.0f, 0.0f, 0.0f)}; + float background_opacity; + + private: + std::string caption_hash() const; + void update_vertices() const; + + mutable std::string hash_; + mutable Imath::Box2f bounding_box_; + mutable std::vector vertices_; + }; + + void from_json(const nlohmann::json &j, Caption &c); + void to_json(nlohmann::json &j, const Caption &c); + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio diff --git a/include/xstudio/ui/canvas/handle.hpp b/include/xstudio/ui/canvas/handle.hpp new file mode 100644 index 000000000..1dc96b8db --- /dev/null +++ b/include/xstudio/ui/canvas/handle.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace xstudio { +namespace ui { + namespace canvas { + + enum class HandleHoverState { + NotHovered, + HoveredInCaptionArea, + HoveredOnMoveHandle, + HoveredOnResizeHandle, + HoveredOnDeleteHandle + }; + + struct HandleState { + HandleHoverState hover_state{HandleHoverState::NotHovered}; + Imath::Box2f under_mouse_caption_bdb; + Imath::Box2f current_caption_bdb; + Imath::V2f handle_size{50.0f, 50.0f}; + std::array cursor_position = { + Imath::V2f(0.0f, 0.0f), Imath::V2f(0.0f, 0.0f)}; + bool cursor_blink_state{false}; + + bool operator==(const HandleState &o) const { + return ( + hover_state == o.hover_state && + under_mouse_caption_bdb == o.under_mouse_caption_bdb && + current_caption_bdb == o.current_caption_bdb && + handle_size == o.handle_size && cursor_position == o.cursor_position && + cursor_blink_state == o.cursor_blink_state); + } + + bool operator!=(const HandleState &o) const { return !(*this == o); } + }; + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio diff --git a/include/xstudio/ui/canvas/stroke.hpp b/include/xstudio/ui/canvas/stroke.hpp new file mode 100644 index 000000000..f2a8ca0dc --- /dev/null +++ b/include/xstudio/ui/canvas/stroke.hpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include "xstudio/utility/json_store.hpp" + +// 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 canvas { + + enum StrokeType { StrokeType_Pen, StrokeType_Erase }; + + struct Stroke { + + float opacity{1.0f}; + float thickness{0.0f}; + float softness{0.0f}; + utility::ColourTriplet colour; + StrokeType type{StrokeType_Pen}; + std::vector points; + + static Stroke + Pen(const utility::ColourTriplet &colour, + const float thickness, + const float softness, + const float opacity); + + static Stroke Erase(const float thickness); + + bool operator==(const Stroke &o) 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. + 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); + + void add_point(const Imath::V2f &pt); + + std::vector vertices() const; + + private: + mutable std::vector vertices_; + }; + + void from_json(const nlohmann::json &j, Stroke &s); + void to_json(nlohmann::json &j, const Stroke &s); + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/font.hpp b/include/xstudio/ui/font.hpp index ba499a06d..00bf9ef13 100644 --- a/include/xstudio/ui/font.hpp +++ b/include/xstudio/ui/font.hpp @@ -294,9 +294,14 @@ namespace ui { ~SDFBitmapFont() = default; + static std::map> available_fonts(); + + static std::shared_ptr font_by_name(const std::string &name); + protected: void generate_atlas(const std::string &font_path, const int glyph_pixel_size) override; }; + } // namespace ui } // namespace xstudio diff --git a/include/xstudio/ui/frontend_model/frontend_model_data.hpp b/include/xstudio/ui/frontend_model/frontend_model_data.hpp index 9f3f89667..ec1af22f6 100644 --- a/include/xstudio/ui/frontend_model/frontend_model_data.hpp +++ b/include/xstudio/ui/frontend_model/frontend_model_data.hpp @@ -9,7 +9,7 @@ namespace xstudio { namespace ui { - namespace frontend_model { + namespace ui_layouts_model { class WindowsAndPanelsModel : public JsonStoreActor { diff --git a/include/xstudio/ui/keyboard.hpp b/include/xstudio/ui/keyboard.hpp index e4ee969d3..f82ed4945 100644 --- a/include/xstudio/ui/keyboard.hpp +++ b/include/xstudio/ui/keyboard.hpp @@ -76,5 +76,137 @@ namespace ui { std::vector> 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"}}; + } // 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 9f5b26072..91f509932 100644 --- a/include/xstudio/ui/model_data/model_data_actor.hpp +++ b/include/xstudio/ui/model_data/model_data_actor.hpp @@ -52,6 +52,25 @@ namespace ui { const utility::JsonStore &data, const std::string &role = std::string()); + void set_data( + const std::string &model_name, + const utility::Uuid &item_uuid, + const std::string &role, + const utility::JsonStore &data, + caf::actor setter); + + void insert_attribute_data_into_model( + const std::string &model_name, + const utility::Uuid &attribute_uuid, + const utility::JsonStore &attribute_data, + const std::string &sort_role, + caf::actor client); + + void remove_attribute_data_from_model( + const std::string &model_name, + const utility::Uuid &attribute_uuid, + caf::actor client); + void register_model( const std::string &model_name, const utility::JsonStore &model_data, @@ -73,9 +92,13 @@ namespace ui { const std::string &model_name, const std::string &path, const int row, - int count); + int count, + caf::actor requester = caf::actor()); + + void push_to_prefs(const std::string &model_name, const bool actually_push = false); - void push_to_prefs(const std::string &model_name); + 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); @@ -87,6 +110,8 @@ namespace ui { void remove_node(const std::string &model_name, const utility::Uuid &model_item_id); + void broadcast_whole_model_data(const std::string &model_name); + struct ModelData { ModelData() = default; ModelData(const ModelData &o) = default; @@ -111,6 +136,7 @@ namespace ui { typedef std::shared_ptr ModelDataPtr; std::map models_; + std::set models_to_be_fully_broadcasted_; caf::behavior behavior_; }; diff --git a/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp new file mode 100644 index 000000000..a851e9f5a --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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/canvas/canvas.hpp" + + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLCanvasRenderer { + + public: + OpenGLCanvasRenderer(); + + void render_canvas( + const xstudio::ui::canvas::Canvas &canvas, + const xstudio::ui::canvas::HandleState &handle_state, + 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); + + private: + template + std::vector all_canvas_items(const xstudio::ui::canvas::Canvas &canvas) { + std::vector result; + canvas.read_lock(); + for (const auto &item : canvas) { + if (std::holds_alternative(item)) { + result.push_back(std::get(item)); + } + } + if (canvas.has_current_item_nolock()) { + result.push_back(std::move(canvas.get_current())); + } + canvas.read_unlock(); + return result; + } + + private: + std::unique_ptr stroke_renderer_; + std::unique_ptr caption_renderer_; + }; + + } // end namespace opengl +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_caption_renderer.hpp b/include/xstudio/ui/opengl/opengl_caption_renderer.hpp new file mode 100644 index 000000000..fee392445 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_caption_renderer.hpp @@ -0,0 +1,55 @@ +// 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/opengl/opengl_text_rendering.hpp" +#include "xstudio/ui/opengl/opengl_texthandle_renderer.hpp" +#include "xstudio/ui/canvas/caption.hpp" +#include "xstudio/ui/canvas/handle.hpp" + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLCaptionRenderer { + public: + ~OpenGLCaptionRenderer(); + + void render_captions( + const std::vector &captions, + const xstudio::ui::canvas::HandleState &handle_state, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx); + + private: + void init_gl(); + void cleanup_gl(); + + void render_background( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const utility::ColourTriplet &background_colour, + const float background_opacity, + const Imath::Box2f &bounding_box); + + typedef std::shared_ptr FontRenderer; + std::map text_renderers_; + std::unique_ptr texthandle_renderer_; + + std::unique_ptr bg_shader_; + GLuint bg_vertex_buffer_{0}; + GLuint bg_vertex_array_{0}; + }; + + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp new file mode 100644 index 000000000..1d09c7c91 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/ui/opengl/shader_program_base.hpp" + + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLOffscreenRenderer { + public: + explicit OpenGLOffscreenRenderer(GLint color_format); + OpenGLOffscreenRenderer(const OpenGLOffscreenRenderer &) = delete; + OpenGLOffscreenRenderer &operator=(const OpenGLOffscreenRenderer &) = delete; + ~OpenGLOffscreenRenderer(); + + void resize(const Imath::V2f &dims); + void begin(); + void end(); + + Imath::V2f dimensions() const { return fbo_dims_; } + unsigned int texture_handle() const { return tex_id_; } + GLenum texture_target() const { return tex_target_; } + + private: + void cleanup(); + + GLint color_format_{0}; + Imath::V2f fbo_dims_{0.0f, 0.0f}; + GLenum tex_target_{GL_TEXTURE_2D}; + unsigned int tex_id_{0}; + unsigned int rbo_id_{0}; + unsigned int fbo_id_{0}; + + std::array vp_state_; + }; + + using OpenGLOffscreenRendererPtr = std::unique_ptr; + + } // end namespace opengl +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp b/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp new file mode 100644 index 000000000..ed0721b8a --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp @@ -0,0 +1,45 @@ +// 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/stroke.hpp" + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLStrokeRenderer { + public: + ~OpenGLStrokeRenderer(); + + void render_strokes( + const std::vector &strokes, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx, + bool have_alpha_buffer); + + 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}; + + std::unique_ptr shader_; + GLuint ssbo_id_{0}; + GLuint ssbo_size_{0}; + std::size_t ssbo_data_hash_{0}; + }; + + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_texthandle_renderer.hpp b/include/xstudio/ui/opengl/opengl_texthandle_renderer.hpp new file mode 100644 index 000000000..680ff3155 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_texthandle_renderer.hpp @@ -0,0 +1,39 @@ +// 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/handle.hpp" + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLTextHandleRenderer { + public: + ~OpenGLTextHandleRenderer(); + + void render_handles( + const xstudio::ui::canvas::HandleState &handle_state, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx); + + private: + void init_gl(); + void cleanup_gl(); + + std::unique_ptr shader_; + GLuint handles_vertex_buffer_obj_{0}; + GLuint handles_vertex_array_{0}; + }; + + } // 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 2a475624c..123924a51 100644 --- a/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp @@ -27,6 +27,7 @@ namespace ui { ColourPipeLutCollection(const ColourPipeLutCollection &o); void upload_luts(const std::vector &luts); + void register_texture(const std::vector &textures); void bind_luts(GLShaderProgramPtr shader, int &tex_idx); @@ -36,6 +37,7 @@ namespace ui { typedef std::shared_ptr GLColourLutTexturePtr; std::map lut_textures_; std::vector active_luts_; + std::map active_textures_; }; class OpenGLViewportRenderer : public viewport::ViewportRenderer { @@ -62,7 +64,7 @@ namespace ui { const std::vector &operations); void - upload_image_and_colour_data(std::vector next_images); + upload_image_and_colour_data(std::vector &next_images); void bind_textures(); void release_textures(); void clear_viewport_area(const Imath::M44f &to_scene_matrix); diff --git a/include/xstudio/ui/opengl/shader_program_base.hpp b/include/xstudio/ui/opengl/shader_program_base.hpp index f215f2131..aef18fd1a 100644 --- a/include/xstudio/ui/opengl/shader_program_base.hpp +++ b/include/xstudio/ui/opengl/shader_program_base.hpp @@ -42,6 +42,8 @@ namespace ui { const std::vector &colour_op_shaders, const bool use_ssbo); + ~GLShaderProgram(); + void inject_colour_op_shader(const std::string &colour_op_shader); void compile(); @@ -63,6 +65,7 @@ namespace ui { std::map locations_; std::vector vertex_shaders_; std::vector fragment_shaders_; + std::vector shaders_; int colour_operation_index_ = {1}; }; } // namespace opengl diff --git a/include/xstudio/ui/opengl/texture.hpp b/include/xstudio/ui/opengl/texture.hpp index 027d17f0d..30b0671a4 100644 --- a/include/xstudio/ui/opengl/texture.hpp +++ b/include/xstudio/ui/opengl/texture.hpp @@ -10,7 +10,7 @@ #include "xstudio/colour_pipeline/colour_pipeline.hpp" #include "xstudio/utility/uuid.hpp" -//#define USE_SSBO +// #define USE_SSBO namespace xstudio { namespace ui { @@ -19,8 +19,8 @@ namespace ui { class GLBlindTex { public: - GLBlindTex() = default; - virtual ~GLBlindTex() = default; + GLBlindTex() = default; + ~GLBlindTex(); void release(); @@ -52,7 +52,7 @@ namespace ui { public: GLSsboTex(); - ~GLSsboTex() override; + virtual ~GLSsboTex(); void map_buffer_for_upload(media_reader::ImageBufPtr &frame) override; void start_pixel_upload() override; @@ -75,7 +75,7 @@ namespace ui { public: GLBlindRGBA8bitTex() = default; - ~GLBlindRGBA8bitTex() override; + virtual ~GLBlindRGBA8bitTex(); void map_buffer_for_upload(media_reader::ImageBufPtr &frame) override; void start_pixel_upload() override; @@ -131,6 +131,7 @@ namespace ui { public: GLColourLutTexture( const colour_pipeline::LUTDescriptor desc, const std::string texture_name); + virtual ~GLColourLutTexture(); void bind(int tex_index); void release(); diff --git a/include/xstudio/ui/qml/actor_object.hpp b/include/xstudio/ui/qml/actor_object.hpp index d896b9ba8..2c5f6e943 100644 --- a/include/xstudio/ui/qml/actor_object.hpp +++ b/include/xstudio/ui/qml/actor_object.hpp @@ -34,11 +34,13 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS +#include "xstudio/atoms.hpp" #include "xstudio/utility/logging.hpp" namespace caf::mixin { -template (QEvent::User + 31337)> +// QEvent::User == 1000 +template (FIRST_CUSTOM_ID)> class actor_object : public Base { public: /// A shared lockable. @@ -52,7 +54,13 @@ class actor_object : public Base { } }; - template actor_object(Ts &&...xs) : Base(std::forward(xs)...) { + // 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. + template < + typename... Ts, + std::enable_if_t<(std::is_move_constructible_v && ...), int> = 0> + actor_object(Ts &&...xs) : Base(std::forward(xs)...) { // nop } diff --git a/include/xstudio/ui/qml/bookmark_model_ui.hpp b/include/xstudio/ui/qml/bookmark_model_ui.hpp index 115ca829b..79216f9e5 100644 --- a/include/xstudio/ui/qml/bookmark_model_ui.hpp +++ b/include/xstudio/ui/qml/bookmark_model_ui.hpp @@ -13,6 +13,9 @@ CAF_PUSH_WARNINGS // #include CAF_POP_WARNINGS +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/bookmark_qml_export.h" + #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/bookmark/bookmark.hpp" @@ -20,7 +23,7 @@ CAF_POP_WARNINGS namespace xstudio::ui::qml { -class BookmarkCategoryModel : public JSONTreeModel { +class BOOKMARK_QML_EXPORT BookmarkCategoryModel : public JSONTreeModel { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) @@ -44,7 +47,7 @@ class BookmarkCategoryModel : public JSONTreeModel { }; -class BookmarkFilterModel : public QSortFilterProxyModel { +class BOOKMARK_QML_EXPORT BookmarkFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY( @@ -93,7 +96,7 @@ class BookmarkFilterModel : public QSortFilterProxyModel { }; -class BookmarkModel : public caf::mixin::actor_object { +class BOOKMARK_QML_EXPORT BookmarkModel : public caf::mixin::actor_object { Q_OBJECT Q_PROPERTY(QString bookmarkActorAddr READ bookmarkActorAddr WRITE setBookmarkActorAddr @@ -125,7 +128,8 @@ class BookmarkModel : public caf::mixin::actor_object { objectRole, startRole, durationRole, - durationFrameRole + durationFrameRole, + visibleRole }; using super = caf::mixin::actor_object; diff --git a/include/xstudio/ui/qml/caf_response_ui.hpp b/include/xstudio/ui/qml/caf_response_ui.hpp index 2df17805d..1546eeb73 100644 --- a/include/xstudio/ui/qml/caf_response_ui.hpp +++ b/include/xstudio/ui/qml/caf_response_ui.hpp @@ -20,6 +20,8 @@ class CafResponse : public QObject { signals: // Search value, search role, search hint, set role, set value void received(QVariant, int, QPersistentModelIndex, int, QString); + // Search value, search role, set role + void finished(QVariant, int, int); public: CafResponse( @@ -31,6 +33,17 @@ 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); + + private: void handleFinished(); diff --git a/include/xstudio/ui/qml/embedded_python_ui.hpp b/include/xstudio/ui/qml/embedded_python_ui.hpp index c1860e685..5166a46f6 100644 --- a/include/xstudio/ui/qml/embedded_python_ui.hpp +++ b/include/xstudio/ui/qml/embedded_python_ui.hpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/embedded_python_qml_export.h" + #include #include @@ -86,7 +90,7 @@ namespace ui { QList snippets_; }; - class EmbeddedPythonUI : public QMLActor { + class EMBEDDED_PYTHON_QML_EXPORT EmbeddedPythonUI : public QMLActor { Q_OBJECT Q_PROPERTY(bool waiting READ waiting NOTIFY waitingChanged) @@ -141,4 +145,4 @@ namespace ui { }; } // namespace qml } // namespace ui -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/ui/qml/event_ui.hpp b/include/xstudio/ui/qml/event_ui.hpp index ef231b525..caf5c9380 100644 --- a/include/xstudio/ui/qml/event_ui.hpp +++ b/include/xstudio/ui/qml/event_ui.hpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/event_qml_export.h" + #include #include @@ -18,7 +22,7 @@ namespace xstudio { namespace ui { namespace qml { - class EventUI : public QObject { + class EVENT_QML_EXPORT EventUI : public QObject { Q_OBJECT Q_PROPERTY(int progress READ progress NOTIFY progressChanged) @@ -73,7 +77,7 @@ namespace ui { event::Event event_; }; - class EventAttrs : public QQmlPropertyMap { + class EVENT_QML_EXPORT EventAttrs : public QQmlPropertyMap { Q_OBJECT @@ -83,7 +87,7 @@ namespace ui { void addEvent(const event::Event &); }; - class EventManagerUI : public QMLActor { + class EVENT_QML_EXPORT EventManagerUI : public QMLActor { Q_OBJECT diff --git a/include/xstudio/ui/qml/global_store_model_ui.hpp b/include/xstudio/ui/qml/global_store_model_ui.hpp index c0da10004..e09ba4c8d 100644 --- a/include/xstudio/ui/qml/global_store_model_ui.hpp +++ b/include/xstudio/ui/qml/global_store_model_ui.hpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/global_store_qml_export.h" + #include #include "xstudio/ui/qml/json_tree_model_ui.hpp" @@ -18,7 +22,8 @@ class GlobalStoreHelper; namespace xstudio::ui::qml { using namespace caf; -class GlobalStoreModel : public caf::mixin::actor_object { +class GLOBAL_STORE_QML_EXPORT GlobalStoreModel + : public caf::mixin::actor_object { Q_OBJECT Q_PROPERTY(bool autosave READ autosave WRITE setAutosave NOTIFY autosaveChanged) diff --git a/include/xstudio/ui/qml/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index 021ebab98..42c03f22a 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + #include #include #include @@ -12,6 +13,9 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/helper_qml_export.h" + CAF_PUSH_WARNINGS #include #include @@ -42,7 +46,42 @@ namespace ui { QVariant mapFromValue(const nlohmann::json &value); nlohmann::json mapFromValue(const QVariant &value); - class ModelProperty : public QObject { + class HELPER_QML_EXPORT ModelRowCount : public QObject { + Q_OBJECT + + Q_PROPERTY(QModelIndex index READ index WRITE setIndex NOTIFY indexChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + + public: + explicit ModelRowCount(QObject *parent = nullptr) : QObject(parent) {} + + [[nodiscard]] QModelIndex index() const { return index_; } + [[nodiscard]] int count() const { return count_; } + + Q_INVOKABLE void setIndex(const QModelIndex &index); + + signals: + void indexChanged(); + void countChanged(); + + private slots: + void inserted(const QModelIndex &parent, int first, int last); + void moved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow); + void removed(const QModelIndex &parent, int first, int last); + + private: + void setCount(const int count); + + QPersistentModelIndex index_; + int count_{0}; + }; + + class HELPER_QML_EXPORT ModelProperty : public QObject { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) @@ -84,7 +123,7 @@ namespace ui { QVariant value_; }; - class ModelPropertyTree : public JSONTreeModel { + class HELPER_QML_EXPORT ModelPropertyTree : public JSONTreeModel { Q_OBJECT Q_PROPERTY(QModelIndex index READ index WRITE setIndex NOTIFY indexChanged) @@ -123,7 +162,7 @@ namespace ui { }; - class ModelPropertyMap : public QObject { + class HELPER_QML_EXPORT ModelPropertyMap : public QObject { Q_OBJECT Q_PROPERTY(QQmlPropertyMap *values READ values NOTIFY valuesChanged) @@ -136,6 +175,7 @@ namespace ui { [[nodiscard]] QQmlPropertyMap *values() const { return values_; } Q_INVOKABLE void setIndex(const QModelIndex &index); + Q_INVOKABLE void dump(); signals: void indexChanged(); @@ -157,7 +197,7 @@ namespace ui { QQmlPropertyMap *values_{nullptr}; }; - class ModelNestedPropertyMap : public ModelPropertyMap { + class HELPER_QML_EXPORT ModelNestedPropertyMap : public ModelPropertyMap { Q_OBJECT public: @@ -173,7 +213,7 @@ namespace ui { QString default_role_ = {"defaultValueRole"}; }; - class CafSystemObject : public QObject { + class HELPER_QML_EXPORT CafSystemObject : public QObject { Q_OBJECT @@ -190,12 +230,14 @@ namespace ui { std::reference_wrapper system_ref_; }; - class QMLActor : public caf::mixin::actor_object { + 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) {} + explicit QMLActor(QObject *parent = nullptr); + + virtual ~QMLActor(); virtual void init(caf::actor_system &system) { super::init(system); } public: @@ -282,7 +324,7 @@ namespace ui { return jsn; } - class Helpers : public QObject { + class HELPER_QML_EXPORT Helpers : public QObject { Q_OBJECT public: @@ -394,12 +436,50 @@ namespace ui { s.select(i, i); return s; } + Q_INVOKABLE [[nodiscard]] bool itemSelectionContains( + const QItemSelection &selection, const QModelIndex &item) const { + return selection.contains(item); + } + + Q_INVOKABLE [[nodiscard]] QColor + saturate(const QColor &color, const double factor = 1.5) const { + double 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); + } + + Q_INVOKABLE [[nodiscard]] QColor + luminate(const QColor &color, const double factor = 1.5) const { + double 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); + } + + Q_INVOKABLE [[nodiscard]] QColor + alphate(const QColor &color, const double alpha) const { + auto result = color; + result.setAlphaF(std::max(0.0, std::min(1.0, alpha))); + return result; + } + + Q_INVOKABLE [[nodiscard]] QColor saturateLuminate( + const QColor &color, + const double sfactor = 1.0, + const double lfactor = 1.0) const { + double 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); + } private: QQmlEngine *engine_; }; - class CursorPosProvider : public QObject { + class HELPER_QML_EXPORT CursorPosProvider : public QObject { Q_OBJECT public: @@ -409,7 +489,7 @@ namespace ui { Q_INVOKABLE QPointF cursorPos() { return QCursor::pos(); } }; - class QMLUuid : public QObject { + class HELPER_QML_EXPORT QMLUuid : public QObject { Q_OBJECT Q_PROPERTY(QString asString READ asString WRITE setFromString NOTIFY changed) Q_PROPERTY(QUuid asQuuid READ asQuuid WRITE setFromQuuid NOTIFY changed) @@ -451,7 +531,7 @@ namespace ui { utility::Uuid uuid_; }; - class SemVer : public QObject { + class HELPER_QML_EXPORT SemVer : public QObject { Q_OBJECT Q_PROPERTY(QString version READ version WRITE setVersion NOTIFY versionChanged) Q_PROPERTY(uint major READ major WRITE setMajor NOTIFY versionChanged) @@ -497,7 +577,7 @@ namespace ui { semver::version version_; }; - class ClipboardProxy : public QObject { + class HELPER_QML_EXPORT ClipboardProxy : public QObject { Q_OBJECT Q_PROPERTY(QString text READ dataText WRITE setDataText NOTIFY dataChanged) Q_PROPERTY(QString selectionText READ selectionText WRITE setSelectionText NOTIFY @@ -520,7 +600,7 @@ namespace ui { void selectionChanged(); }; - class Plugin : public QObject { + class HELPER_QML_EXPORT Plugin : public QObject { Q_OBJECT Q_PROPERTY(QString qmlName READ qmlName NOTIFY qmlNameChanged) Q_PROPERTY( diff --git a/include/xstudio/ui/qml/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index cf15804ae..3c309f5c5 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/viewport_qml_export.h" + #include #include @@ -21,7 +24,8 @@ namespace utility { namespace ui { namespace qml { - class HotkeysUI : public caf::mixin::actor_object { + class VIEWPORT_QML_EXPORT HotkeysUI + : public caf::mixin::actor_object { Q_OBJECT @@ -63,7 +67,7 @@ namespace ui { }; - class HotkeyUI : public QMLActor { + class VIEWPORT_QML_EXPORT HotkeyUI : public QMLActor { Q_OBJECT @@ -149,7 +153,7 @@ namespace ui { utility::Uuid hotkey_uuid_; }; - class HotkeyReferenceUI : public QMLActor { + class VIEWPORT_QML_EXPORT HotkeyReferenceUI : public QMLActor { Q_OBJECT @@ -180,4 +184,4 @@ namespace ui { } // namespace qml } // namespace ui -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/ui/qml/json_tree_model_ui.hpp b/include/xstudio/ui/qml/json_tree_model_ui.hpp index 9eb84b40d..435758edf 100644 --- a/include/xstudio/ui/qml/json_tree_model_ui.hpp +++ b/include/xstudio/ui/qml/json_tree_model_ui.hpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + #include #include #include @@ -13,9 +14,11 @@ CAF_POP_WARNINGS #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/tree.hpp" +#include "helper_qml_export.h" + namespace xstudio::ui::qml { -class JSONTreeModel : public QAbstractItemModel { +class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { Q_OBJECT Q_PROPERTY(int count READ length NOTIFY lengthChanged) @@ -36,6 +39,8 @@ class JSONTreeModel : public QAbstractItemModel { JSONTreeModel(QObject *parent = nullptr); + [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 1; @@ -76,6 +81,8 @@ class JSONTreeModel : public QAbstractItemModel { bool insertRows(int row, int count, const QModelIndex &parent, const nlohmann::json &data); + Q_INVOKABLE QModelIndex invalidIndex() const { return QModelIndex(); } + Q_INVOKABLE int countExpandedChildren(const QModelIndex parent, const QModelIndexList &expanded); @@ -149,7 +156,7 @@ class JSONTreeModel : public QAbstractItemModel { nlohmann::json &indexToData(const QModelIndex &index); const nlohmann::json &indexToData(const QModelIndex &index) const; - nlohmann::json indexToFullData(const QModelIndex &index) const; + nlohmann::json indexToFullData(const QModelIndex &index, const int depth = -1) const; utility::JsonTree *indexToTree(const QModelIndex &index) const; nlohmann::json::json_pointer getIndexPath(const QModelIndex &index = QModelIndex()) const; @@ -170,7 +177,7 @@ class JSONTreeModel : public QAbstractItemModel { utility::JsonTree data_; }; -class JSONTreeFilterModel : public QSortFilterProxyModel { +class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(int length READ length NOTIFY lengthChanged) diff --git a/include/xstudio/ui/qml/log_ui.hpp b/include/xstudio/ui/qml/log_ui.hpp index 524b63d55..2b2b3082f 100644 --- a/include/xstudio/ui/qml/log_ui.hpp +++ b/include/xstudio/ui/qml/log_ui.hpp @@ -3,6 +3,9 @@ #include +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/log_qml_export.h" + #include "spdlog/common.h" #include "spdlog/details/log_msg.h" #include "spdlog/details/synchronous_factory.h" @@ -26,7 +29,7 @@ namespace ui { }; - class LogModel : public QAbstractListModel { + class LOG_QML_EXPORT LogModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QStringList logLevels READ logLevels NOTIFY logLevelsChanged) @@ -70,7 +73,7 @@ namespace ui { // err = SPDLOG_LEVEL_ERROR, critical = SPDLOG_LEVEL_CRITICAL, off = // SPDLOG_LEVEL_OFF - class LogFilterModel : public QSortFilterProxyModel { + class LOG_QML_EXPORT LogFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(int logLevel READ logLevel WRITE setLogLevel NOTIFY logLevelChanged) Q_PROPERTY(QString logLevelString READ logLevelString NOTIFY logLevelStringChanged) diff --git a/include/xstudio/ui/qml/model_data_ui.hpp b/include/xstudio/ui/qml/model_data_ui.hpp index 8678ae981..8b1bad14c 100644 --- a/include/xstudio/ui/qml/model_data_ui.hpp +++ b/include/xstudio/ui/qml/model_data_ui.hpp @@ -5,7 +5,7 @@ #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/ui/qml/tag_ui.hpp" CAF_PUSH_WARNINGS @@ -17,7 +17,7 @@ CAF_POP_WARNINGS namespace xstudio::ui::qml { using namespace caf; -class UIModelData : public caf::mixin::actor_object { +class HELPER_QML_EXPORT UIModelData : public caf::mixin::actor_object { Q_OBJECT @@ -61,6 +61,10 @@ class UIModelData : public caf::mixin::actor_object { Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + + Q_INVOKABLE bool + removeRowsSync(int row, int count, const QModelIndex &parent = QModelIndex()); + Q_INVOKABLE bool moveRows( const QModelIndex &sourceParent, int sourceRow, @@ -77,9 +81,10 @@ class UIModelData : public caf::mixin::actor_object { caf::actor central_models_data_actor_; std::string model_name_; std::string data_preference_path_; + bool foobarred_ = {false}; }; -class MenusModelData : public UIModelData { +class HELPER_QML_EXPORT MenusModelData : public UIModelData { Q_OBJECT @@ -87,7 +92,7 @@ class MenusModelData : public UIModelData { explicit MenusModelData(QObject *parent = nullptr); }; -class ViewsModelData : public UIModelData { +class HELPER_QML_EXPORT ViewsModelData : public UIModelData { Q_OBJECT @@ -98,18 +103,34 @@ class 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_path, QString view_name); + void register_view(QString qml_source, QString view_name); + + // 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 ReskinPanelsModel : public UIModelData { +class HELPER_QML_EXPORT ReskinPanelsModel : public UIModelData { Q_OBJECT public: explicit ReskinPanelsModel(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); +}; + +class HELPER_QML_EXPORT MediaListColumnsModel : public UIModelData { + + Q_OBJECT + + public: + explicit MediaListColumnsModel(QObject *parent = nullptr); }; -class MenuModelItem : public caf::mixin::actor_object { +class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object { Q_OBJECT @@ -173,7 +194,6 @@ class MenuModelItem : public caf::mixin::actor_object { } } void setIsChecked(const bool checked) { - std::cerr << "OIOI " << checked << " " << is_checked_ << "\n"; if (checked != is_checked_) { is_checked_ = checked; emit isCheckedChanged(); diff --git a/include/xstudio/ui/qml/module_data_ui.hpp b/include/xstudio/ui/qml/module_data_ui.hpp new file mode 100644 index 000000000..4986a59e1 --- /dev/null +++ b/include/xstudio/ui/qml/module_data_ui.hpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/ui/qml/model_data_ui.hpp" + + +CAF_PUSH_WARNINGS +#include +#include +#include +CAF_POP_WARNINGS + +namespace xstudio::ui::qml { +using namespace caf; + +class HELPER_QML_EXPORT ModulesModelData : public UIModelData { + + Q_OBJECT + + public: + explicit ModulesModelData(QObject *parent = nullptr); +}; + +} // 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 index 3548f56ed..60bf56e2f 100644 --- a/include/xstudio/ui/qml/module_menu_ui.hpp +++ b/include/xstudio/ui/qml/module_menu_ui.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/module_qml_export.h" + #include #include @@ -21,7 +24,7 @@ namespace ui { class ModuleAttrsToQMLShim; - class ModuleMenusModel : public QAbstractListModel { + class MODULE_QML_EXPORT ModuleMenusModel : public QAbstractListModel { Q_OBJECT @@ -56,6 +59,7 @@ namespace ui { 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); @@ -86,6 +90,8 @@ namespace ui { [[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); @@ -93,6 +99,7 @@ namespace ui { void num_submenusChanged(); void titleChanged(); void submenu_namesChanged(); + void emptyChanged(); public slots: diff --git a/include/xstudio/ui/qml/module_ui.hpp b/include/xstudio/ui/qml/module_ui.hpp index 6e7d536aa..793cea5fb 100644 --- a/include/xstudio/ui/qml/module_ui.hpp +++ b/include/xstudio/ui/qml/module_ui.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/module_qml_export.h" + #include #include @@ -21,7 +24,7 @@ namespace xstudio { namespace ui { namespace qml { - class ModuleAttrsDirect : public QQmlPropertyMap { + class MODULE_QML_EXPORT ModuleAttrsDirect : public QQmlPropertyMap { Q_OBJECT @@ -31,7 +34,7 @@ namespace ui { Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) ModuleAttrsDirect(QObject *parent = nullptr); - ~ModuleAttrsDirect() override = default; + virtual ~ModuleAttrsDirect(); void add_attributes_from_backend( const module::AttributeSet &attrs, const bool check_group = false); @@ -70,7 +73,7 @@ namespace ui { }; - class OrderedModuleAttrsModel : public QSortFilterProxyModel { + class MODULE_QML_EXPORT OrderedModuleAttrsModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QStringList attributesGroupNames READ attributesGroupNames WRITE @@ -89,7 +92,7 @@ namespace ui { }; - class ModuleAttrsModel : public QAbstractListModel { + class MODULE_QML_EXPORT ModuleAttrsModel : public QAbstractListModel { Q_OBJECT @@ -100,7 +103,7 @@ namespace ui { setattributesGroupNames NOTIFY attributesGroupNamesChanged) ModuleAttrsModel(QObject *parent = nullptr); - ~ModuleAttrsModel() override = default; + virtual ~ModuleAttrsModel(); [[nodiscard]] int rowCount() { return rowCount(QModelIndex()); } diff --git a/include/xstudio/ui/qml/playhead_ui.hpp b/include/xstudio/ui/qml/playhead_ui.hpp index 7602d1c09..82119c4c0 100644 --- a/include/xstudio/ui/qml/playhead_ui.hpp +++ b/include/xstudio/ui/qml/playhead_ui.hpp @@ -185,8 +185,6 @@ namespace ui { QString compareLayerName(); [[nodiscard]] QString name() const { return name_; } - [[nodiscard]] const utility::Uuid &sourceUuid() const { return source_uuid_; } - signals: void uuidChanged(); void frameChanged(); @@ -241,7 +239,6 @@ namespace ui { bool jumpToNextSource(); bool jumpToPreviousSource(); void jumpToSource(const QUuid media_uuid); - void setSourceUuid(const utility::Uuid uuid) { source_uuid_ = std::move(uuid); } void setFitMode(const QString mode); void connectToUI(); @@ -297,7 +294,6 @@ namespace ui { int source_offset_frames_; QVariant compare_mode_options_; - utility::Uuid source_uuid_; QList cache_detail_; QList bookmark_detail_ui_; std::vector> bookmark_detail_; diff --git a/include/xstudio/ui/qml/qml_viewport.hpp b/include/xstudio/ui/qml/qml_viewport.hpp index a168ce699..e0a8c2a90 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/viewport_qml_export.h" + #include CAF_PUSH_WARNINGS @@ -29,7 +32,7 @@ namespace ui { class QMLViewportRenderer; class PlayheadUI; - class QMLViewport : public QQuickItem { + class VIEWPORT_QML_EXPORT QMLViewport : public QQuickItem { Q_OBJECT Q_PROPERTY(float zoom READ zoom WRITE setZoom NOTIFY zoomChanged) @@ -40,8 +43,6 @@ namespace ui { Q_PROPERTY( QVector2D translate READ translate WRITE setTranslate NOTIFY translateChanged) Q_PROPERTY(QObject *playhead READ playhead NOTIFY playheadChanged) - Q_PROPERTY(QStringList colourUnderCursor READ colourUnderCursor NOTIFY - colourUnderCursorChanged) Q_PROPERTY(int mouseButtons READ mouseButtons NOTIFY mouseButtonsChanged) Q_PROPERTY(QPoint mouse READ mouse NOTIFY mouseChanged) Q_PROPERTY(int onScreenImageLogicalFrame READ onScreenImageLogicalFrame NOTIFY @@ -51,16 +52,17 @@ namespace ui { Q_PROPERTY(QSize imageResolution READ imageResolution NOTIFY imageResolutionChanged) Q_PROPERTY(bool enableShortcuts READ enableShortcuts NOTIFY enableShortcutsChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(bool isQuickViewer READ isQuickViewer WRITE setIsQuickViewer NOTIFY + isQuickViewerChanged) public: QMLViewport(QQuickItem *parent = nullptr); - ~QMLViewport() override = default; + virtual ~QMLViewport(); float zoom(); QString fpsExpression(); float scale(); QVector2D translate(); QObject *playhead(); - [[nodiscard]] QStringList colourUnderCursor() const { return colour_under_cursor; } [[nodiscard]] QString name() const; [[nodiscard]] int mouseButtons() const { return mouse_buttons; } [[nodiscard]] QPoint mouse() const { return mouse_position; } @@ -71,8 +73,13 @@ namespace ui { [[nodiscard]] bool noAlphaChannel() const { return no_alpha_channel_; } [[nodiscard]] bool enableShortcuts() const { return enable_shortcuts_; } void setPlayhead(caf::actor playhead); + QMLViewportRenderer *viewportActor() { return renderer_actor; } + void deleteRendererActor(); + bool isQuickViewer() const { return is_quick_viewer_; } protected: + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void hoverMoveEvent(QHoverEvent *event) override; @@ -89,6 +96,7 @@ namespace ui { void cleanup(); void setZoom(const float z); void revertFitZoomToPrevious(const bool ignoreOtherViewport = false); + void linkToViewport(QObject *other_viewport); void handleScreenChanged(QScreen *screen); void hideCursor(); @@ -97,12 +105,11 @@ namespace ui { QVector2D bboxCornerInViewport(const int min_x, const int min_y); void setScale(const float s); void setTranslate(const QVector2D &tr); - void setColourUnderCursor(const QVector3D &c); void setOnScreenImageLogicalFrame(const int frame_num); QRectF imageBoundaryInViewport(); void setFrameOutOfRange(bool frame_out_of_range); void setNoAlphaChannel(bool no_alpha_channel); - QString renderImageToFile( + void renderImageToFile( const QUrl filePath, const int format, const int compression, @@ -113,6 +120,7 @@ namespace ui { 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); private slots: @@ -125,7 +133,6 @@ namespace ui { void scaleChanged(float); void playheadChanged(QObject *); void translateChanged(QVector2D); - void colourUnderCursorChanged(); void mouseButtonsChanged(); void mouseChanged(); void onScreenImageLogicalFrameChanged(); @@ -136,6 +143,14 @@ namespace ui { void enableShortcutsChanged(); void doSnapshot(QString, QString, int, int, bool); void nameChanged(); + void quickViewSource(QStringList mediaActors, QString compareMode); + 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(); private: void releaseResources() override; @@ -152,7 +167,6 @@ namespace ui { bool connected_{false}; QCursor cursor_; bool cursor_hidden{false}; - QStringList colour_under_cursor{"--", "--", "--"}; int mouse_buttons = {0}; QPoint mouse_position; int on_screen_logical_frame_ = {0}; @@ -160,8 +174,9 @@ namespace ui { bool no_alpha_channel_ = {false}; bool enable_shortcuts_ = {true}; int viewport_index_ = {0}; + bool is_quick_viewer_ = {false}; }; } // namespace qml } // namespace ui -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/ui/qml/qml_viewport_renderer.hpp b/include/xstudio/ui/qml/qml_viewport_renderer.hpp index d3f95dbcf..b59d6c210 100644 --- a/include/xstudio/ui/qml/qml_viewport_renderer.hpp +++ b/include/xstudio/ui/qml/qml_viewport_renderer.hpp @@ -25,7 +25,7 @@ namespace ui { public: QMLViewportRenderer(QObject *owner, const int viewport_index); - ~QMLViewportRenderer() override = default; + virtual ~QMLViewportRenderer(); void setWindow(QQuickWindow *window); @@ -34,7 +34,8 @@ namespace ui { const QPointF topright, const QPointF bottomright, const QPointF bottomleft, - const QSize sceneSize); + const QSize sceneSize, + const float devicePixelRatio); void init_system(); void join_playhead(caf::actor group) { @@ -79,6 +80,18 @@ namespace ui { return QStringFromStd(viewport_renderer_->name()); } + void linkToViewport(QMLViewportRenderer *other_viewport); + + 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); + public slots: void init_renderer(); @@ -88,7 +101,7 @@ namespace ui { void frameSwapped(); float scale(); QVector2D translate(); - + void quickViewSource(QStringList mediaActors, QString compareMode); signals: void zoomChanged(float); @@ -101,17 +114,23 @@ namespace ui { void noAlphaChannelChanged(bool); 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); QQuickWindow *m_window; - std::shared_ptr viewport_renderer_; + 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; caf::actor playhead_group_; diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index 67e143506..108a849c9 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -1,11 +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 "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" CAF_PUSH_WARNINGS @@ -14,10 +17,14 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS +// namespace xstudio::timeline { +// class Item; +// } + namespace xstudio::ui::qml { using namespace caf; -class SessionModel : public caf::mixin::actor_object { +class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object { Q_OBJECT Q_PROPERTY(QString sessionActorAddr READ sessionActorAddr WRITE setSessionActorAddr NOTIFY @@ -28,33 +35,58 @@ class SessionModel : public caf::mixin::actor_object { Q_PROPERTY(QString bookmarkActorAddr READ bookmarkActorAddr NOTIFY bookmarkActorAddrChanged) Q_PROPERTY(QVariant playlists READ playlists NOTIFY playlistsChanged) + Q_PROPERTY(QStringList conformTasks READ conformTasks NOTIFY conformTasksChanged) public: enum Roles { - actorRole = JSONTreeModel::Roles::LASTROLE, + activeDurationRole = JSONTreeModel::Roles::LASTROLE, + activeStartRole, + actorRole, actorUuidRole, audioActorUuidRole, + availableDurationRole, + availableStartRole, bitDepthRole, busyRole, childrenRole, + clipMediaUuidRole, containerUuidRole, - flagRole, + 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, + uuidRole }; using super = caf::mixin::actor_object; @@ -74,6 +106,8 @@ class SessionModel : public caf::mixin::actor_object { Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + void fetchMore(const QModelIndex &parent) override; + Q_INVOKABLE bool removeRows(int row, int count, const bool deep, const QModelIndex &parent = QModelIndex()); @@ -85,7 +119,6 @@ class SessionModel : public caf::mixin::actor_object { Q_INVOKABLE QModelIndexList copyRows(const QModelIndexList &indexes, const int row, const QModelIndex &parent); - Q_INVOKABLE bool moveRows( const QModelIndex &sourceParent, int sourceRow, @@ -102,6 +135,34 @@ class SessionModel : public caf::mixin::actor_object { Q_INVOKABLE void mergeRows(const QModelIndexList &indexes, const QString &name = "Combined Playlist"); + // timeline operations + Q_INVOKABLE bool removeTimelineItems(const QModelIndexList &indexes); + Q_INVOKABLE QModelIndex getTimelineIndex(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex insertTimelineGap( + const int row, + const QModelIndex &parent, + const int frames = 24, + const double rate = 24.0, + const QString &name = "Gap"); + Q_INVOKABLE QModelIndex insertTimelineClip( + const int row, + const QModelIndex &parent, + const QModelIndex &mediaIndex, + const QString &name = "Clip"); + Q_INVOKABLE QModelIndex splitTimelineClip(const int frame, const QModelIndex &index); + + Q_INVOKABLE bool + removeTimelineItems(const QModelIndex &track_index, const int frame, const int duration); + + Q_INVOKABLE bool moveTimelineItem(const QModelIndex &index, const int distance); + Q_INVOKABLE bool moveRangeTimelineItems( + const QModelIndex &track_index, + const int frame, + const int duration, + const int dest, + const bool insert); + Q_INVOKABLE bool + alignTimelineItems(const QModelIndexList &indexes, const bool align_right = true); [[nodiscard]] QString sessionActorAddr() const { return session_actor_addr_; }; void setSessionActorAddr(const QString &addr); @@ -142,6 +203,10 @@ class SessionModel : public caf::mixin::actor_object { static nlohmann::json containerDetailToJson(const utility::ContainerDetail &detail, caf::actor_system &sys); + static nlohmann::json timelineItemToJson( + const timeline::Item &item, caf::actor_system &sys, const bool recurse = true); + + Q_INVOKABLE QString save(const QUrl &path, const QModelIndexList &selection = {}) { return saveFuture(path, selection).result(); } @@ -184,7 +249,7 @@ class SessionModel : public caf::mixin::actor_object { [[nodiscard]] QString bookmarkActorAddr() const { return bookmark_actor_addr_; }; [[nodiscard]] QVariant playlists() const; - + [[nodiscard]] QStringList conformTasks() const; Q_INVOKABLE void moveSelectionByIndex(const QModelIndex &index, const int offset); Q_INVOKABLE void @@ -212,6 +277,27 @@ class SessionModel : public caf::mixin::actor_object { Q_INVOKABLE void rescanMedia(const QModelIndexList &indexes); Q_INVOKABLE QModelIndex getPlaylistIndex(const QModelIndex &index) const; + Q_INVOKABLE QFuture undoFuture(const QModelIndex &index); + Q_INVOKABLE QFuture redoFuture(const QModelIndex &index); + + Q_INVOKABLE void + setTimelineFocus(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); + + public slots: + void updateMedia(); + signals: void bookmarkActorAddrChanged(); void sessionActorAddrChanged(); @@ -220,11 +306,12 @@ class SessionModel : public caf::mixin::actor_object { void tagsChanged(); void modifiedChanged(); void playlistsChanged(); + void conformTasksChanged(); void mediaSourceChanged(const QModelIndex &media, const QModelIndex &source, const int mode); public: - caf::actor_system &system() { return self()->home_system(); } + caf::actor_system &system() const { return self()->home_system(); } static nlohmann::json createEntry(const nlohmann::json &update = R"({})"_json); protected: @@ -253,6 +340,12 @@ class SessionModel : public caf::mixin::actor_object { bool isChildOf(const QModelIndex &parent, const QModelIndex &child) const; int depthOfChild(const QModelIndex &parent, const QModelIndex &child) const; + void triggerMediaStatusChange(const QModelIndex &index); + + void updateConformTasks(const std::vector &tasks); + + void updateErroredCount(const QModelIndex &media_index); + QModelIndexList insertRows( int row, int count, @@ -268,6 +361,8 @@ class SessionModel : public caf::mixin::actor_object { const int role, const QString &result); + void finishedDataSlot(const QVariant &search_value, const int search_role, const int role); + void receivedData( const nlohmann::json &search_value, const int search_role, @@ -280,15 +375,18 @@ class SessionModel : public caf::mixin::actor_object { const int search_role, const QPersistentModelIndex &search_hint, const QModelIndex &index, - const int role) const; + const int role, + const std::map &metadata_paths = std::map()) const; + void requestData( const QVariant &search_value, const int search_role, const QPersistentModelIndex &search_hint, const nlohmann::json &data, - const int role) const; + const int role, + const std::map &metadata_paths = std::map()) const; - caf::actor actorFromIndex(const QModelIndex &index, const bool try_parent = false); + caf::actor actorFromIndex(const QModelIndex &index, const bool try_parent = false) const; utility::Uuid actorUuidFromIndex(const QModelIndex &index, const bool try_parent = false); void processChildren(const nlohmann::json &result_json, const QModelIndex &index); @@ -300,6 +398,8 @@ class SessionModel : public caf::mixin::actor_object { QFuture> handleMediaIdDropFuture( const int proposedAction, const utility::JsonStore &drop, const QModelIndex &index); + QFuture> handleTimelineIdDropFuture( + const int proposedAction, const utility::JsonStore &drop, const QModelIndex &index); QFuture> handleContainerIdDropFuture( const int proposedAction, const utility::JsonStore &drop, const QModelIndex &index); QFuture> handleUriListDropFuture( @@ -307,9 +407,11 @@ class SessionModel : public caf::mixin::actor_object { QFuture> handleOtherDropFuture( const int proposedAction, const utility::JsonStore &drop, const QModelIndex &index); + void add_id_uuid_lookup(const utility::Uuid &uuid, const QModelIndex &index); void add_uuid_lookup(const utility::Uuid &uuid, const QModelIndex &index); void add_string_lookup(const std::string &str, const QModelIndex &index); void add_lookup(const utility::JsonTree &tree, const QModelIndex &index); + void item_event_callback(const utility::JsonStore &event, timeline::Item &item); private: QString session_actor_addr_; @@ -318,15 +420,25 @@ class SessionModel : public caf::mixin::actor_object { caf::actor session_actor_; TagManagerUI *tag_manager_{nullptr}; + caf::actor conform_actor_; + QStringList conform_tasks_; + utility::time_point saved_time_; utility::time_point last_changed_; - mutable std::set> in_flight_requests_; + mutable std::set in_flight_requests_; QThreadPool *request_handler_; - + std::map> id_uuid_lookup_; std::map> uuid_lookup_; std::map> string_lookup_; + + std::map timeline_lookup_; + + bool mediaStatusChangePending_{false}; + QPersistentModelIndex mediaStatusIndex_; + + std::map> metadata_sets_; }; } // 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 87210c431..2bacedabe 100644 --- a/include/xstudio/ui/qml/shotgun_provider_ui.hpp +++ b/include/xstudio/ui/qml/shotgun_provider_ui.hpp @@ -56,7 +56,7 @@ class ShotgunThumbnailReader : public ControllableJob system_.registry().get(thumbnail_manager_registry); if (not shotgun) - throw std::runtime_error("Shotgun not available"); + throw std::runtime_error("ShotGrid not available"); scoped_actor sys{system_}; @@ -118,6 +118,7 @@ class ShotgunThumbnailReader : public ControllableJob .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(); } diff --git a/include/xstudio/ui/qml/snapshot_model_ui.hpp b/include/xstudio/ui/qml/snapshot_model_ui.hpp new file mode 100644 index 000000000..3a171d52f --- /dev/null +++ b/include/xstudio/ui/qml/snapshot_model_ui.hpp @@ -0,0 +1,61 @@ +// 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 896f9e940..48e71eb66 100644 --- a/include/xstudio/ui/qml/studio_ui.hpp +++ b/include/xstudio/ui/qml/studio_ui.hpp @@ -11,6 +11,7 @@ CAF_PUSH_WARNINGS CAF_POP_WARNINGS #include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qt/offscreen_viewport.hpp" #include "xstudio/utility/uuid.hpp" namespace xstudio { @@ -28,7 +29,7 @@ namespace ui { public: explicit StudioUI(caf::actor_system &system, QObject *parent = nullptr); - ~StudioUI() override = default; + ~StudioUI(); Q_INVOKABLE bool clearImageCache(); @@ -57,11 +58,15 @@ namespace ui { void setSessionActorAddr(const QString &addr); signals: + void newSessionCreated(const QString &session_addr); void sessionLoaded(const QString &session_addr); void dataSourcesChanged(); void sessionRequest(const QUrl path, const QByteArray jsn); void sessionActorAddrChanged(); + void openQuickViewers(QStringList mediaActors, QString compareMode); + void showMessageBox( + QString messageTile, QString messageBody, bool closeButton, int timeoutSeconds); public slots: @@ -69,9 +74,13 @@ namespace ui { private: void init(caf::actor_system &system) override; void updateDataSources(); + void loadVideoOutputPlugins(); QList data_sources_; QString session_actor_addr_; + std::vector offscreen_viewports_; + std::vector video_output_plugins_; + xstudio::ui::qt::OffscreenViewport *snapshot_offscreen_viewport_ = nullptr; }; } // namespace qml } // namespace ui diff --git a/include/xstudio/ui/qml/tag_ui.hpp b/include/xstudio/ui/qml/tag_ui.hpp index 094a2fe6d..f8a159f54 100644 --- a/include/xstudio/ui/qml/tag_ui.hpp +++ b/include/xstudio/ui/qml/tag_ui.hpp @@ -2,6 +2,11 @@ #pragma once #pragma once +#pragma once + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/tag_qml_export.h" + #include #include @@ -85,7 +90,7 @@ namespace ui { void reset(); }; - class TagManagerUI : public QMLActor { + class TAG_QML_EXPORT TagManagerUI : public QMLActor { Q_OBJECT diff --git a/include/xstudio/ui/qml/thumbnail_provider_ui.hpp b/include/xstudio/ui/qml/thumbnail_provider_ui.hpp index 16cf07655..f3215a049 100644 --- a/include/xstudio/ui/qml/thumbnail_provider_ui.hpp +++ b/include/xstudio/ui/qml/thumbnail_provider_ui.hpp @@ -71,7 +71,6 @@ class ThumbnailReader : public ControllableJob> { AVFrameID mp; // super dirty... - for (auto i = 1; i < 5; i++) { try { mp = request_receive( @@ -167,7 +166,7 @@ class ThumbnailResponse : public QQuickImageResponse { // spdlog::warn("{}", StdFromQString(id)); if (bad_thumbs_.contains(id_) and bad_thumbs_[id_].secsTo(QDateTime::currentDateTime()) < 60 * 20) { - error_ = "Thumbnail does not exist."; + error_ = "Thumbnail does not exist 1."; emit finished(); } else { @@ -198,7 +197,8 @@ class ThumbnailResponse : public QQuickImageResponse { auto [i, e] = watcher_.result(); if (not e.isEmpty()) { - error_ = "Thumbnail does not exist."; + qDebug() << e; + error_ = "Thumbnail does not exist 2."; bad_thumbs_.insert(id_, QDateTime::currentDateTime()); } else { bad_thumbs_.remove(id_); @@ -228,7 +228,7 @@ class ThumbnailResponse : public QQuickImageResponse { } void handleFailed(QString error) { - error_ = "Thumbnail does not exist."; + error_ = "Thumbnail does not exist 3."; emit finished(); bad_thumbs_.insert(id_, QDateTime::currentDateTime()); } diff --git a/include/xstudio/ui/qt/offscreen_viewport.hpp b/include/xstudio/ui/qt/offscreen_viewport.hpp index a3756327e..7cb1ed5d8 100644 --- a/include/xstudio/ui/qt/offscreen_viewport.hpp +++ b/include/xstudio/ui/qt/offscreen_viewport.hpp @@ -7,7 +7,7 @@ #include #include #include -//#include +// #include #include #include #include @@ -22,55 +22,99 @@ namespace ui { class OffscreenViewport : public caf::mixin::actor_object { - // Q_OBJECT + Q_OBJECT using super = caf::mixin::actor_object; public: - OffscreenViewport(QObject *parent = nullptr); + OffscreenViewport(const std::string name); ~OffscreenViewport() override; // Direct rendering to an output file - void renderSnapshot( - caf::actor playhead, - const int width, - const int height, - const int compression, - const bool bakeColor, - const caf::uri path); + void + renderSnapshot(const int width, const int height, const caf::uri path = caf::uri()); + + void setPlayhead(const QString &playheadAddress); + + std::string name() { return viewport_renderer_->name(); } + + void stop(); + + public slots: - void moveToOwnThread(); + void autoDelete(); private: - thumbnail::ThumbnailBufferPtr - renderOffscreen(const int w, const int h, const media_reader::ImageBufPtr &image); + void receive_change_notification(viewport::Viewport::ChangeCallbackId id); + + thumbnail::ThumbnailBufferPtr renderOffscreen( + const int w, + const int h, + const media_reader::ImageBufPtr &image = media_reader::ImageBufPtr()); thumbnail::ThumbnailBufferPtr renderToThumbnail( - caf::actor playhead, const thumbnail::THUMBNAIL_FORMAT format, const int width, - const bool render_annotations, - const bool fit_to_annotations_outside_image); + const bool auto_scale, + const bool show_annotations); + + thumbnail::ThumbnailBufferPtr renderToThumbnail( + const thumbnail::THUMBNAIL_FORMAT format, const int width, const int height); + + void renderToImageBuffer( + const int w, + const int h, + media_reader::ImageBufPtr &image, + const viewport::ImageFormat format); void initGL(); - void exportToEXR(thumbnail::ThumbnailBufferPtr r, const caf::uri path); + void exportToEXR(const media_reader::ImageBufPtr &image, const caf::uri path); + + thumbnail::ThumbnailBufferPtr renderMediaFrameToThumbnail( + caf::actor media_actor, + const int media_frame, + const thumbnail::THUMBNAIL_FORMAT format, + const int width, + const bool auto_scale, + const bool show_annotations); void exportToCompressedFormat( - thumbnail::ThumbnailBufferPtr r, + const media_reader::ImageBufPtr &buf, const caf::uri path, - int compression, const std::string &ext); - media_reader::ImageBufPtr get_image_from_playhead(caf::actor playhead); + void setupTextureAndFrameBuffer( + const int width, const int height, const viewport::ImageFormat format); - std::shared_ptr viewport_renderer_; - QOpenGLContext *gl_context_ = {nullptr}; - QOffscreenSurface *surface_ = {nullptr}; - QThread *thread_ = {nullptr}; - caf::actor middleman_; + void make_conversion_lut(); + + thumbnail::ThumbnailBufferPtr + rgb96thumbFromHalfFloatImage(const media_reader::ImageBufPtr &image); + + ui::viewport::Viewport *viewport_renderer_ = nullptr; + QOpenGLContext *gl_context_ = {nullptr}; + QOffscreenSurface *surface_ = {nullptr}; + QThread *thread_ = {nullptr}; // TODO: will remove once everything done const char *formatSuffixes[4] = {"EXR", "JPG", "PNG", "TIFF"}; + + int tex_width_ = 0; + int tex_height_ = 0; + int pix_buf_size_ = 0; + GLuint texId_ = 0; + GLuint fboId_ = 0; + GLuint depth_texId_ = 0; + GLuint pixel_buffer_object_ = 0; + + int vid_out_width_ = 0; + int vid_out_height_ = 0; + viewport::ImageFormat vid_out_format_ = viewport::ImageFormat::RGBA_16; + caf::actor video_output_actor_; + std::vector output_buffers_; + std::vector half_to_int_32_lut_; + + caf::actor local_playhead_; }; } // namespace qt } // namespace ui diff --git a/include/xstudio/ui/viewport/enums.hpp b/include/xstudio/ui/viewport/enums.hpp index 48c7f47d6..f17304cc5 100644 --- a/include/xstudio/ui/viewport/enums.hpp +++ b/include/xstudio/ui/viewport/enums.hpp @@ -7,6 +7,7 @@ namespace ui { enum FitMode { Free, Width, Height, Fill, One2One, Best }; enum MirrorMode { Off, Flip, Flop, Both }; enum GraphicsAPI { None, OpenGL, Metal, Vulkan, DirectX }; + enum ImageFormat { RGBA_8, RGBA_10_10_10_2, RGBA_16, RGBA_16F, RGBA_32F }; } // namespace viewport } // namespace ui } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/viewport/keypress_monitor.hpp b/include/xstudio/ui/viewport/keypress_monitor.hpp index 4b32d052e..6702f62f9 100644 --- a/include/xstudio/ui/viewport/keypress_monitor.hpp +++ b/include/xstudio/ui/viewport/keypress_monitor.hpp @@ -31,7 +31,7 @@ namespace ui { caf::behavior behavior_; std::set held_keys_; std::map active_hotkeys_; - caf::actor actor_grabbing_all_mouse_input_; + std::set actor_grabbing_all_mouse_input_; caf::actor actor_grabbing_all_keyboard_input_; }; } // namespace keypress_monitor diff --git a/include/xstudio/ui/viewport/shader.hpp b/include/xstudio/ui/viewport/shader.hpp index e28e05400..38293dffa 100644 --- a/include/xstudio/ui/viewport/shader.hpp +++ b/include/xstudio/ui/viewport/shader.hpp @@ -10,14 +10,14 @@ namespace xstudio { namespace ui { namespace viewport { - /* Virtual base class for shader data. Subclass for OpenGL, Metal, + /* Base class for shader data. Subclass for OpenGL, Metal, Vulkan, DirectX etc. */ class GPUShader { public: GPUShader(utility::Uuid id, GraphicsAPI api) : shader_id_(id), graphics_api_(api) {} - [[nodiscard]] const utility::Uuid shader_id() const { return shader_id_; } + [[nodiscard]] utility::Uuid shader_id() const { return shader_id_; } [[nodiscard]] GraphicsAPI graphics_api() const { return graphics_api_; } private: diff --git a/include/xstudio/ui/viewport/viewport.hpp b/include/xstudio/ui/viewport/viewport.hpp index 611592325..384a78843 100644 --- a/include/xstudio/ui/viewport/viewport.hpp +++ b/include/xstudio/ui/viewport/viewport.hpp @@ -37,15 +37,16 @@ namespace ui { const utility::JsonStore &state_data, caf::actor parent_actor, const int viewport_index, - ViewportRendererPtr the_renderer); - ~Viewport() override; + ViewportRendererPtr the_renderer, + const std::string &name = std::string()); + virtual ~Viewport(); bool process_pointer_event(PointerEvent &); void set_pointer_event_viewport_coords(PointerEvent &pointer_event); void set_scale(const float scale); - void set_size(const float w, const float h); + void set_size(const float w, const float h, const float devicePixelRatio); void set_pan(const float x_pan, const float y_pan); void set_fit_mode(const FitMode md); void set_mirror_mode(const MirrorMode md); @@ -57,6 +58,15 @@ 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) @@ -65,7 +75,7 @@ namespace ui { * buttons to toggle the fit/zoom back to what it was before the last * interactino started. */ - void revert_fit_zoom_to_previous(); + void revert_fit_zoom_to_previous(const bool synced = false); /** * @brief Switch the mirror mode to Flop/Off @@ -128,7 +138,8 @@ namespace ui { const Imath::V2f topright, const Imath::V2f bottomright, const Imath::V2f bottomleft, - const Imath::V2i scene_size); + const Imath::V2i scene_size, + const float devicePixelRatio); /** * @brief Inform the viewport of the size of the image currently on screen to @@ -179,6 +190,8 @@ namespace ui { } [[nodiscard]] const std::string &toolbar_name() const { return toolbar_name_; } + [[nodiscard]] caf::actor colour_pipeline() { return colour_pipeline_; } + utility::JsonStore serialise() const override; /** @@ -247,16 +260,38 @@ namespace ui { 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); caf::actor fps_monitor() { return fps_monitor_; } - void framebuffer_swapped(); + void framebuffer_swapped(const utility::time_point swap_time); media_reader::ImageBufPtr get_image_from_playhead(caf::actor playhead); + media_reader::ImageBufPtr get_onscreen_image(); + + void set_aux_shader_uniforms( + const utility::JsonStore &j, const bool clear_and_overwrite = false); + protected: void register_hotkeys() override; @@ -285,7 +320,7 @@ namespace ui { */ void get_frames_for_display(std::vector &next_images); - void instance_overlay_plugins(const bool share_plugin_instances); + void instance_overlay_plugins(); private: @@ -314,6 +349,7 @@ namespace ui { Imath::M44f interact_start_inv_projection_matrix_; Imath::M44f viewport_to_canvas_; Imath::M44f fit_mode_matrix_; + float devicePixelRatio_ = {1.0}; Imath::V4f normalised_pointer_position() const; @@ -321,6 +357,9 @@ namespace ui { void get_colour_pipeline(); + void + quickview_media(std::vector &media_items, std::string compare_mode); + utility::JsonStore settings_; typedef std::function PointerInteractFunc; @@ -342,24 +381,26 @@ namespace ui { caf::actor fps_monitor_; caf::actor keypress_monitor_; caf::actor viewport_events_actor_; - caf::actor other_viewport_; + std::vector other_viewports_; caf::actor colour_pipeline_; caf::actor keyboard_events_actor_; + caf::actor quickview_playhead_; caf::actor_addr playhead_addr_; - caf::actor overlay_actor_; - 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 done_init_ = {false}; + int viewport_index_ = {0}; + bool playing_ = {false}; + bool playhead_pinned_ = {false}; std::set held_keys_; + utility::JsonStore aux_shader_uniforms_; + std::map overlay_plugin_instances_; std::map hud_plugin_instances_; diff --git a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp index 4d846bfe9..0136f351e 100644 --- a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp +++ b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp @@ -99,6 +99,8 @@ namespace ui { timebase::flicks predicted_playhead_position_at_next_video_refresh(); + double average_video_refresh_period() const; + bool playing_ = {false}; bool playing_forwards_ = {true}; diff --git a/include/xstudio/ui/viewport/viewport_renderer_base.hpp b/include/xstudio/ui/viewport/viewport_renderer_base.hpp index f21d38965..5d3c58dbf 100644 --- a/include/xstudio/ui/viewport/viewport_renderer_base.hpp +++ b/include/xstudio/ui/viewport/viewport_renderer_base.hpp @@ -61,11 +61,21 @@ namespace ui { */ 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; } + 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< @@ -97,8 +107,11 @@ namespace ui { 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/caf_helpers.hpp b/include/xstudio/utility/caf_helpers.hpp index ca85d7ea2..447ff961c 100644 --- a/include/xstudio/utility/caf_helpers.hpp +++ b/include/xstudio/utility/caf_helpers.hpp @@ -29,14 +29,18 @@ namespace utility { struct absolute_receive_timeout { public: - using ms = std::chrono::milliseconds; + using ms = std::chrono::milliseconds; +#ifdef _WIN32 + 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; - absolute_receive_timeout(const absolute_receive_timeout &) = default; + absolute_receive_timeout() = default; + absolute_receive_timeout(const absolute_receive_timeout &) = default; absolute_receive_timeout &operator=(const absolute_receive_timeout &) = default; [[nodiscard]] const clock_type::time_point &value() const { return x_; } diff --git a/include/xstudio/utility/chrono.hpp b/include/xstudio/utility/chrono.hpp index baccf99d1..68b4f3d45 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -17,15 +17,27 @@ namespace utility { using time_point = clock::time_point; using milliseconds = std::chrono::milliseconds; - using sysclock = std::chrono::system_clock; - using sys_time_point = sysclock::time_point; +#ifdef _WIN32 + using sysclock = std::chrono::high_resolution_clock; +#else + using sysclock = std::chrono::system_clock; +#endif + using sys_time_point = sysclock::time_point; + using sys_time_duration = sysclock::duration; inline std::string to_string(const sys_time_point &tp) { +#ifdef _WIN32 + std::stringstream ss; + // TODO: Ahead Fix + // ss << std::put_time(std::localtime(in_time_t), "%Y-%m-%d %X"); + return ss.str(); +#else auto in_time_t = std::chrono::system_clock::to_time_t(tp); std::stringstream ss; ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X"); return ss.str(); +#endif } // 2021-12-21T10:26:37Z utility::sys_time_point to_sys_time_point(const std::string &datetime); @@ -82,4 +94,4 @@ inline void to_json(json &j, const timebase::flicks &p) { j = json{{"count", p.c inline void from_json(const json &j, timebase::flicks &p) { p = timebase::flicks(j.at("count")); } -} // namespace std::chrono \ No newline at end of file +} // namespace std::chrono diff --git a/include/xstudio/utility/container.hpp b/include/xstudio/utility/container.hpp index 45e049ca3..09447be25 100644 --- a/include/xstudio/utility/container.hpp +++ b/include/xstudio/utility/container.hpp @@ -87,6 +87,8 @@ namespace utility { [[nodiscard]] virtual utility::JsonStore serialise() const; virtual void deserialise(const utility::JsonStore &); + [[nodiscard]] Container duplicate() const; + void send_changed( caf::actor grp, caf::event_based_actor *act, diff --git a/include/xstudio/utility/edit_list.hpp b/include/xstudio/utility/edit_list.hpp index e2f7af978..6b0f3c484 100644 --- a/include/xstudio/utility/edit_list.hpp +++ b/include/xstudio/utility/edit_list.hpp @@ -15,7 +15,7 @@ namespace utility { virtual ~EditList() = default; EditList &operator=(const EditList &) = default; - EditList &operator=(EditList &&) = default; + EditList &operator=(EditList &&) = default; void extend(const EditList &o); diff --git a/include/xstudio/utility/exports.hpp b/include/xstudio/utility/exports.hpp index 5390f4d6d..f8c8c6898 100644 --- a/include/xstudio/utility/exports.hpp +++ b/include/xstudio/utility/exports.hpp @@ -2,7 +2,7 @@ #pragma once #if defined _WIN32 || defined __CYGWIN__ -#ifdef BUILDING_DLL +#if defined BUILDING_DLL || defined _DLL #ifdef __GNUC__ #define DLL_PUBLIC __attribute__((dllexport)) #else diff --git a/include/xstudio/utility/file_system_item.hpp b/include/xstudio/utility/file_system_item.hpp new file mode 100644 index 000000000..a20202150 --- /dev/null +++ b/include/xstudio/utility/file_system_item.hpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +// container to handle sequences/mov files etc.. +#pragma once + +// #include +#ifdef _WIN32 +#else +#include +#endif +#include +#include +#include + +#include + +#include "xstudio/utility/json_store.hpp" + +namespace xstudio::utility { + +// typedef enum { +// FSA_INSERT = 0x1L, +// FSA_REMOVE = 0x2L, +// FSA_MOVE = 0x3L, +// FSA_UPDATE = 0x4L, + +// } FileSystemAction; + +typedef enum { + FSIT_NONE = 0x0L, + FSIT_ROOT = 0x1L, + FSIT_DIRECTORY = 0x2L, + FSIT_FILE = 0x3L, + +} FileSystemItemType; + +class FileSystemItem; +using FileSystemItems = std::list; + +// typedef std::function +// FileSystemItemEventFunc; +typedef std::function FileSystemItemIgnoreFunc; + +class FileSystemItem : private FileSystemItems { + public: + FileSystemItem() : FileSystemItems() {} + FileSystemItem(const fs::directory_entry &entry); + FileSystemItem( + const std::string name, const caf::uri path, FileSystemItemType type = FSIT_DIRECTORY) + : FileSystemItems(), + name_(std::move(name)), + path_(std::move(path)), + type_(std::move(type)) {} + + virtual ~FileSystemItem() = default; + + using FileSystemItems::empty; + using FileSystemItems::size; + + using FileSystemItems::begin; + using FileSystemItems::cbegin; + using FileSystemItems::cend; + using FileSystemItems::end; + + using FileSystemItems::crbegin; + using FileSystemItems::crend; + using FileSystemItems::rbegin; + using FileSystemItems::rend; + + using FileSystemItems::back; + using FileSystemItems::front; + + // these circumvent the handler.. + using FileSystemItems::clear; + using FileSystemItems::emplace_back; + using FileSystemItems::emplace_front; + using FileSystemItems::pop_back; + using FileSystemItems::pop_front; + using FileSystemItems::push_back; + using FileSystemItems::push_front; + using FileSystemItems::splice; + + FileSystemItems::iterator + insert(FileSystemItems::iterator position, const FileSystemItem &val); + + FileSystemItems::iterator erase(FileSystemItems::iterator position); + + [[nodiscard]] const FileSystemItems &children() const { return *this; } + [[nodiscard]] FileSystemItems &children() { return *this; } + + [[nodiscard]] std::string name() const { return name_; } + [[nodiscard]] caf::uri path() const { return path_; } + [[nodiscard]] fs::file_time_type last_write() const { return last_write_; } + [[nodiscard]] FileSystemItemType type() const { return type_; } + + void set_last_write(const fs::file_time_type &value = fs::file_time_type()) { + last_write_ = value; + } + + [[nodiscard]] std::optional + item_at_index(const int index) const; + + bool scan(const int depth = -1, const bool ignore_last_write = false); + + FileSystemItem *find_by_path(const caf::uri &path); + + utility::JsonStore dump() const; + + // void bind_event_func(FileSystemItemEventFunc fn); + void bind_ignore_entry_func(FileSystemItemIgnoreFunc fn); + + + private: + FileSystemItemType type_{FSIT_ROOT}; + std::string name_{}; + caf::uri path_{}; + + fs::file_time_type last_write_{}; + + // FileSystemItemEventFunc event_callback_{nullptr}; + FileSystemItemIgnoreFunc ignore_entry_callback_{nullptr}; +}; + +inline std::string to_string(const FileSystemItemType it) { + std::string str; + switch (it) { + case FSIT_NONE: + str = "None"; + break; + case FSIT_ROOT: + str = "ROOT"; + break; + case FSIT_DIRECTORY: + str = "DIRECTORY"; + break; + case FSIT_FILE: + str = "FILE"; + break; + } + return str; +} + +bool ignore_not_session(const fs::directory_entry &entry); + +} // namespace xstudio::utility \ No newline at end of file diff --git a/include/xstudio/utility/frame_rate.hpp b/include/xstudio/utility/frame_rate.hpp index b634e2c5a..98781359f 100644 --- a/include/xstudio/utility/frame_rate.hpp +++ b/include/xstudio/utility/frame_rate.hpp @@ -39,7 +39,7 @@ namespace utility { [[nodiscard]] timebase::flicks to_flicks() const { return *this; } FrameRate &operator=(const FrameRate &) = default; - FrameRate &operator=(FrameRate &&) = default; + FrameRate &operator=(FrameRate &&) = default; // Rational& operator+= (const Rational& other); // Rational& operator-= (const Rational& other); diff --git a/include/xstudio/utility/frame_rate_and_duration.hpp b/include/xstudio/utility/frame_rate_and_duration.hpp index 2c3f05860..25c3986df 100644 --- a/include/xstudio/utility/frame_rate_and_duration.hpp +++ b/include/xstudio/utility/frame_rate_and_duration.hpp @@ -27,7 +27,7 @@ namespace utility { // const int den) : rate_(num, den), count_(timebase_ * frames) {} FrameRateDuration &operator=(const FrameRateDuration &) = default; - FrameRateDuration &operator=(FrameRateDuration &&) = default; + FrameRateDuration &operator=(FrameRateDuration &&) = default; FrameRateDuration operator-(const FrameRateDuration &); diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 6640991ee..ea4535d0c 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -22,6 +22,10 @@ #include "xstudio/utility/string_helpers.hpp" #include "xstudio/caf_error.hpp" #include "xstudio/caf_utility/caf_setup.hpp" +#ifdef _WIN32 +#include +#include +#endif namespace xstudio { namespace utility { @@ -46,6 +50,7 @@ namespace utility { class ActorSystemSingleton { public: + static caf::actor_system &actor_system_ref(); static caf::actor_system &actor_system_ref(caf::actor_system &sys); private: @@ -63,6 +68,8 @@ namespace utility { const std::array supported_timeline_extensions{".OTIO", ".XML", ".EDL"}; + const std::array session_extensions{".XST", ".XSZ"}; + std::string actor_to_string(caf::actor_system &sys, const caf::actor &actor); caf::actor actor_from_string(caf::actor_system &sys, const std::string &str_addr); @@ -83,6 +90,17 @@ namespace utility { namespace fs = std::filesystem; + // Centralizing the Path to String conversions in case we run into encoding problems down + // the line. + inline std::string path_to_string(fs::path path) { +#ifdef _WIN32 + return path.string(); +#else + // Implicit cast works fine on Linux + return path; +#endif + } + inline bool check_create_path(const std::string &path) { bool create_path = true; @@ -177,27 +195,10 @@ namespace utility { spdlog::debug("{} created {}", name, to_string(hdl)); } - inline void print_on_exit(const caf::actor &hdl, const Container &cont) { - hdl->attach_functor([=](const caf::error &reason) { - spdlog::debug( - "{} {} {} exited: {}", - cont.type(), - cont.name(), - to_string(cont.uuid()), - to_string(reason)); - }); - } + void print_on_exit(const caf::actor &hdl, const Container &cont); - inline void print_on_exit( - const caf::actor &hdl, const std::string &name, const Uuid &uuid = utility::Uuid()) { - hdl->attach_functor([=](const caf::error &reason) { - spdlog::debug( - "{} {} exited: {}", - name, - uuid.is_null() ? "" : to_string(uuid), - to_string(reason)); - }); - } + 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); @@ -268,29 +269,83 @@ namespace utility { inline std::string xstudio_root(const std::string &append_path) { auto root = get_env("XSTUDIO_ROOT"); - std::string path = - (root ? (*root) + append_path : std::string(BINARY_DIR) + append_path); + + 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 path = (root ? (*root) + append_path : fallback_root + append_path); + return path; } inline std::string remote_session_path() { - auto root = get_env("HOME"); - std::string path = (root ? (*root) + "/.config/DNEG/xstudio/sessions" : ""); - return path; + const char *root; +#ifdef _WIN32 + root = std::getenv("USERPROFILE"); +#else + root = std::getenv("HOME"); +#endif + std::filesystem::path path; + if (root) { + path = std::filesystem::path(root) / ".config" / "DNEG" / "xstudio" / "sessions"; + } + + return path.string(); } inline std::string preference_path(const std::string &append_path = "") { - auto root = get_env("HOME"); - std::string path = - (root ? (*root) + "/.config/DNEG/xstudio/preferences/" + append_path : ""); - return path; + const char *root; +#ifdef _WIN32 + root = std::getenv("USERPROFILE"); +#else + root = std::getenv("HOME"); +#endif + std::filesystem::path path; + if (root) { + path = std::filesystem::path(root) / ".config" / "DNEG" / "xstudio" / "preferences"; + if (!append_path.empty()) { + path /= append_path; + } + } + + return path.string(); } inline std::string snippets_path(const std::string &append_path = "") { - auto root = get_env("HOME"); - std::string path = - (root ? (*root) + "/.config/DNEG/xstudio/snippets/" + append_path : ""); - return path; + const char *root; +#ifdef _WIN32 + root = std::getenv("USERPROFILE"); +#else + root = std::getenv("HOME"); +#endif + std::filesystem::path path; + if (root) { + path = std::filesystem::path(root) / ".config" / "DNEG" / "xstudio" / "snippets"; + if (!append_path.empty()) { + path /= append_path; + } + } + + return path.string(); } inline std::string preference_path_context(const std::string &context) { @@ -313,7 +368,7 @@ namespace utility { try { mtim = fs::last_write_time(path); } catch (const std::exception &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + spdlog::debug("{} {}", __PRETTY_FUNCTION__, err.what()); } return mtim; } @@ -322,19 +377,53 @@ namespace utility { return get_file_mtime(uri_to_posix_path(path)); } + inline std::string get_path_extension(const fs::path p) { + const std::string sp = p.string(); +#ifdef _WIN32 + std::string sanitized; + + try { + sanitized = fmt::format(sp, 0); + + } catch (...) { + // If we are here, the path likely doesn't have a format string. + sanitized = sp; + } + fs::path pth(sanitized); + std::string ext = pth.extension().string(); // Convert path extension to string + return ext; +#else + return p.extension().string(); +#endif + } + inline bool is_file_supported(const caf::uri &uri) { - fs::path p(uri_to_posix_path(uri)); - std::string ext = to_upper(p.extension()); + const std::string sp = uri_to_posix_path(uri); + std::string ext = to_upper(get_path_extension(fs::path(sp))); + for (const auto &i : supported_extensions) if (i == ext) return true; return false; } + inline bool is_session(const std::string &path) { + fs::path p(path); + std::string ext = to_upper(path_to_string(get_path_extension(p))); + for (const auto &i : session_extensions) + if (i == ext) + return true; + return false; + } + + inline bool is_session(const caf::uri &path) { return is_session(uri_to_posix_path(path)); } + inline bool is_timeline_supported(const caf::uri &uri) { fs::path p(uri_to_posix_path(uri)); - std::string ext = to_upper(p.extension()); + spdlog::error(p.string()); + std::string ext = to_upper(get_path_extension(p)); + for (const auto &i : supported_timeline_extensions) if (i == ext) return true; @@ -455,4 +544,4 @@ namespace utility { } } // namespace utility -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/utility/json_store.hpp b/include/xstudio/utility/json_store.hpp index 5eb976e93..cda5ddfe3 100644 --- a/include/xstudio/utility/json_store.hpp +++ b/include/xstudio/utility/json_store.hpp @@ -62,7 +62,7 @@ template struct adl_serializer> { vv++; // skip count for (int i = 0; i < 4; ++i) for (int k = 0; k < 4; ++k) - p[i][k] = (vv++).value().get(); + p[k][i] = (vv++).value().get(); } }; @@ -88,7 +88,7 @@ template struct adl_serializer> { vv++; // skip count for (int i = 0; i < 3; ++i) for (int k = 0; k < 3; ++k) - p[i][k] = (vv++).value().get(); + p[k][i] = (vv++).value().get(); } }; @@ -244,7 +244,7 @@ namespace utility { // [[nodiscard]] bool empty() const { return json_.empty(); } // void clear() { json_.clear(); } - [[nodiscard]] std::string dump(size_t pad = 0) const { + [[nodiscard]] std::string dump(int pad = 0) const { return nlohmann::json::dump( pad, ' ', false, nlohmann::detail::error_handler_t::replace); } @@ -286,6 +286,10 @@ namespace utility { namespace fs = std::filesystem; + JsonStore open_session(const caf::uri &path); + JsonStore open_session(const std::string &path); + + nlohmann::json sort_by(const nlohmann::json &jsn, const nlohmann::json::json_pointer &ptr); inline JsonStore merge_json_from_path(const std::string &path, JsonStore merged = JsonStore()) { @@ -301,8 +305,8 @@ namespace utility { i >> j; merged.merge(j); } - } catch (const std::exception &e) { - spdlog::warn("Preference path does not exist {}.", path); + } catch ([[maybe_unused]] const std::exception &e) { + spdlog::warn("Preference path does not exist {}. ({})", path); } return merged; } diff --git a/include/xstudio/utility/lock_file.hpp b/include/xstudio/utility/lock_file.hpp index 42d8832e0..115138718 100644 --- a/include/xstudio/utility/lock_file.hpp +++ b/include/xstudio/utility/lock_file.hpp @@ -1,6 +1,63 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#ifdef _WIN32 +using uid_t = DWORD; // Use DWORD type for user ID +using gid_t = DWORD; // Use DWORD type for group ID + +#include + +inline bool lstat(const std::string &path, struct stat *st) { + WIN32_FILE_ATTRIBUTE_DATA fileData; + if (GetFileAttributesExA(path.c_str(), GetFileExInfoStandard, &fileData)) { + // Fill the 'struct stat' with information from 'WIN32_FILE_ATTRIBUTE_DATA' + st->st_mode = fileData.dwFileAttributes; + // Set other members of 'struct stat' as needed + // ... + return true; + } + return false; +} + +inline uid_t getuid() { return static_cast(GetCurrentProcessId()); } + +struct passwd { + std::string pw_name; + std::string pw_passwd; + uid_t pw_uid; + gid_t pw_gid; + std::string pw_gecos; + std::string pw_dir; + std::string pw_shell; +}; + +inline struct passwd *getpwuid(uid_t uid) { + static struct passwd pw; + + // Get the username associated with the UID + wchar_t username[UNLEN + 1]; + DWORD usernameLen = UNLEN + 1; + if (GetUserNameW(username, &usernameLen)) { + pw.pw_name = std::to_string(uid); // Set the username as needed + pw.pw_passwd = ""; // Set the password as needed + pw.pw_uid = uid; + pw.pw_gid = 0; // Set the group ID as needed + pw.pw_gecos = ""; // Set the GECOS field as needed + pw.pw_dir = ""; // Set the home directory as needed + pw.pw_shell = ""; // Set the shell as needed + + return &pw; + } + + return nullptr; +} +#else +// For Linux or non-Windows platforms +using uid_t = uid_t; +using gid_t = gid_t; #include +#endif + #include #include @@ -43,11 +100,23 @@ namespace utility { [[nodiscard]] caf::uri source() const { return source_; } [[nodiscard]] caf::uri lock_file() const { // resolve path to source.. + +#ifdef _WIN32 + std::filesystem::path lpath(uri_to_posix_path(source_)); + + if (std::filesystem::exists(lpath) && std::filesystem::is_symlink(lpath)) + lpath = std::filesystem::canonical(lpath); + + std::string lpath_string = lpath.string(); + return posix_path_to_uri(lpath.concat(".lock").string()); +#else + // For other platforms, use the existing code auto lpath = uri_to_posix_path(source_); if (fs::exists(lpath) && fs::is_symlink(lpath)) lpath = fs::canonical(lpath); return posix_path_to_uri(lpath + ".lock"); +#endif } [[nodiscard]] bool locked() const { return locked_; } [[nodiscard]] bool owned() const { return owned_; } @@ -80,7 +149,11 @@ namespace utility { [[nodiscard]] bool unlock() { if (locked_ and owned_ and not borrowed_) { // unlock we no longer own file. +#ifdef _WIN32 + _unlink(uri_to_posix_path(lock_file()).c_str()); +#else unlink(uri_to_posix_path(lock_file()).c_str()); +#endif reset(); return true; } diff --git a/include/xstudio/utility/logging.hpp b/include/xstudio/utility/logging.hpp index 5b8893f08..7e26aefec 100644 --- a/include/xstudio/utility/logging.hpp +++ b/include/xstudio/utility/logging.hpp @@ -1,6 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#if defined(_WIN32) +#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__) +#define __PRETTY_FUNCTION__ __FUNCSIG__ +#endif +#endif /* \file logging.h Stop and start logging system diff --git a/include/xstudio/utility/remote_session_file.hpp b/include/xstudio/utility/remote_session_file.hpp index 202c35d0d..632b7ce85 100644 --- a/include/xstudio/utility/remote_session_file.hpp +++ b/include/xstudio/utility/remote_session_file.hpp @@ -1,13 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#ifdef _WIN32 +using pid_t = int; // Use Int type as pyconfig.h +#endif + #include #include #include #include +#ifdef __linux__ #include #include +#endif namespace xstudio { namespace utility { diff --git a/include/xstudio/utility/sequence.hpp b/include/xstudio/utility/sequence.hpp index 202b6ce1a..fcbc48a8b 100644 --- a/include/xstudio/utility/sequence.hpp +++ b/include/xstudio/utility/sequence.hpp @@ -2,6 +2,15 @@ // container to handle sequences/mov files etc.. #pragma once +#ifdef _WIN32 +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; +#endif + // #include // #include #include @@ -73,4 +82,4 @@ namespace utility { IgnoreSequenceFunc ignore_sequence = default_ignore_sequence); } // namespace utility -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/utility/serialise_headers.hpp b/include/xstudio/utility/serialise_headers.hpp index 80100af34..327835cf0 100644 --- a/include/xstudio/utility/serialise_headers.hpp +++ b/include/xstudio/utility/serialise_headers.hpp @@ -2,6 +2,7 @@ #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/media/media.hpp" diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index d6be083c4..ef21c8cbc 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -1,6 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#ifdef _WIN32 +#include +#endif + +#include #include #include #include @@ -11,7 +16,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #include @@ -176,6 +183,17 @@ namespace utility { return result; } + inline std::wstring to_upper(const std::wstring &str) { + static std::locale loc; + std::wstring result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::toupper(elem, loc); + + return result; + } + inline std::string to_upper(const std::string &str) { static std::locale loc; std::string result; @@ -187,6 +205,18 @@ namespace utility { return result; } + // TODO: Ahead to 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); + + return result; + } + inline std::optional get_env(const std::string &key) { const char *val = std::getenv(key.c_str()); if (val) @@ -196,7 +226,7 @@ namespace utility { inline std::string get_hostname() { std::array buffer; - if (not gethostname(buffer.data(), buffer.size())) { + if (not gethostname(buffer.data(), (int)buffer.size())) { return std::string(buffer.data()); } return std::string(); @@ -308,4 +338,4 @@ namespace utility { } // namespace utility -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/utility/tree.hpp b/include/xstudio/utility/tree.hpp index 0505c917b..5cd4771ed 100644 --- a/include/xstudio/utility/tree.hpp +++ b/include/xstudio/utility/tree.hpp @@ -219,14 +219,15 @@ namespace utility { return result; } - inline nlohmann::json tree_to_json(const JsonTree &node, const std::string &childname) { + inline nlohmann::json + tree_to_json(const JsonTree &node, const std::string &childname, const int depth = -1) { // unroll.. auto jsn = node.data(); - if (not node.empty()) { + if (depth and not node.empty()) { jsn[childname] = R"([])"_json; for (const auto &i : node) - jsn[childname].push_back(tree_to_json(i, childname)); + jsn[childname].push_back(tree_to_json(i, childname, depth - 1)); } return jsn; diff --git a/include/xstudio/utility/undo_redo.hpp b/include/xstudio/utility/undo_redo.hpp index 18b966fc1..3529c1942 100644 --- a/include/xstudio/utility/undo_redo.hpp +++ b/include/xstudio/utility/undo_redo.hpp @@ -99,6 +99,10 @@ namespace utility { std::optional redo(); std::optional undo(const K &key); std::optional redo(const K &key); + + std::optional peek_undo(); + std::optional peek_redo(); + void clear(); private: @@ -130,6 +134,22 @@ namespace utility { redo_.clear(); } + template std::optional UndoRedoMap::peek_undo() { + if (undo_.empty()) + return {}; + + auto it = undo_.rbegin(); + return it->first; + } + + template std::optional UndoRedoMap::peek_redo() { + if (redo_.empty()) + return {}; + + auto it = redo_.begin(); + return it->first; + } + template std::optional UndoRedoMap::undo() { if (undo_.empty()) return {}; diff --git a/include/xstudio/utility/uuid.hpp b/include/xstudio/utility/uuid.hpp index bf3b42620..2d25b7e14 100644 --- a/include/xstudio/utility/uuid.hpp +++ b/include/xstudio/utility/uuid.hpp @@ -138,6 +138,7 @@ namespace utility { using UuidActorAddrMap = std::map; using UuidList = std::list; using UuidVector = std::vector; + using UuidSet = std::set; /*! UuidActor diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 1e3dec3c4..8227751e7 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -55,6 +55,9 @@ if(INSTALL_PYTHON_MODULE) DESTINATION bin) endif() +if(WIN32) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio DESTINATION ${CMAKE_INSTALL_PREFIX}/python/ FILES_MATCHING PATTERN "*.py") +endif() # install(CODE "execute_process(COMMAND ${PYTHON} ${SETUP_PY} install)") diff --git a/python/src/xstudio/api/app.py b/python/src/xstudio/api/app.py index da34eda7d..038216dcc 100644 --- a/python/src/xstudio/api/app.py +++ b/python/src/xstudio/api/app.py @@ -1,11 +1,61 @@ # SPDX-License-Identifier: Apache-2.0 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 +from xstudio.core import viewport_playhead_atom, quickview_media_atom +from xstudio.core import UuidActorVec, UuidActor from xstudio.api.session import Session, Container 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): + """Create Viewport object. + + Args: + connection(Connection): Connection object + remote(actor): Remote actor object + + Kwargs: + uuid(Uuid): Uuid of remote actor. + """ + ModuleBase.__init__( + self, + connection, + connection.request_receive( + connection.remote(), + get_actor_from_registry_atom(), + "MAIN_VIEWPORT" + )[0] + ) + + def quickview(self, media_items, compare_mode="Off", position=(100,100), size=(1280,720)): + """Connect this playhead to the viewport. + + Args: + media_items(list(Media)): A list of Media objects to be shown in quickview + windows + compare_mode(str): Remote actor object + position(tuple(int,int)): X/Y Position of new window (default=(100,100)) + size(tuple(int,int)): X/Y Size of new window (default=(1280,720)) + + """ + + media_actors = UuidActorVec() + for m in media_items: + media_actors.push_back(UuidActor(m.uuid, m.remote)) + + self.connection.request_receive( + self.remote, + quickview_media_atom(), + media_actors, + compare_mode, + position[0], + position[1], + size[0], + size[1]) + class App(Container): """App access. """ def __init__(self, connection, remote, uuid=None): @@ -41,7 +91,7 @@ def viewport(self): Returns: viewport(ModuleBase): Viewport module.""" - return ModuleBase(self.connection, self.connection.request_receive(self.connection.remote(), get_actor_from_registry_atom(), "MAIN_VIEWPORT")[0]) + return Viewport(self.connection) @property def global_playhead_events(self): diff --git a/python/src/xstudio/api/module.py b/python/src/xstudio/api/module.py index fcd645cd5..f0b89b91e 100644 --- a/python/src/xstudio/api/module.py +++ b/python/src/xstudio/api/module.py @@ -6,7 +6,7 @@ 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 +from xstudio.core import AttributeRole, remove_attribute_atom from xstudio.api.auxiliary import ActorConnection from xstudio.core import JsonStore, Uuid from xstudio.api.auxiliary.helpers import get_event_group @@ -49,7 +49,7 @@ def __init__( parent_remote, add_attribute_atom(), attribute_name, - JsonStore(attribute_value), + attribute_value if type(attribute_value) == type(JsonStore()) else JsonStore(attribute_value), JsonStore(attribute_role_data) )[0] @@ -100,6 +100,20 @@ def set_value(self, value): self.uuid, JsonStore(value)) + 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) + class ModuleBase(ActorConnection): @@ -167,7 +181,8 @@ def add_attribute( self, attribute_name, attribute_value, - attribute_role_data={} + attribute_role_data={}, + preference_path=None ): """Add an attribute to your plugin class. Attributes provide a flexible way to store data and/or pass data between your plugin and the xStudio @@ -179,6 +194,9 @@ def add_attribute( attribute_value(int,float,bool,str, list(str)): The value of the 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 + the value restored next time xstudio starts up. """ new_attr = ModuleAttribute( @@ -190,8 +208,26 @@ def add_attribute( self.attrs_by_name_[attribute_name] = new_attr + if preference_path: + new_attr.set_role_data("preference_path", preference_path) + return new_attr + def remove_attribute( + self, + attribute_uuid + ): + """Remove (and delete) an attribute from your plugin class.. + + Args: + attribute_uuid(Uuid): Uuid of the attribute to be removed + """ + return self.connection.request_receive( + self.remote, + remove_attribute_atom(), + attribute_uuid + )[0] + def set_attribute(self, attr_name, value): """Set the value on the named attribute @@ -341,17 +377,23 @@ def message_handler(self, sender, req_id, message_content): if role == AttributeRole.Value: attr_uuid = str(message_content[2]) if len( message_content) > 2 else "" - if self.__attribute_changed: - self.__attribute_changed(attr_uuid) + 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]() + elif isinstance(atom, type(hotkey_event_atom())): hotkey_uuid = str(message_content[1]) if len( 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 "" if hotkey_uuid in self.hotkey_callbacks: - self.hotkey_callbacks[hotkey_uuid](activated) + self.hotkey_callbacks[hotkey_uuid](activated, context) + except Exception as err: print (err) print (traceback.format_exc()) diff --git a/python/src/xstudio/api/session/media/media.py b/python/src/xstudio/api/session/media/media.py index 3ae13e891..b64ff7d7c 100644 --- a/python/src/xstudio/api/session/media/media.py +++ b/python/src/xstudio/api/session/media/media.py @@ -2,7 +2,7 @@ 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 set_json_atom, JsonStore +from xstudio.core import set_json_atom, JsonStore, quickview_media_atom from xstudio.api.session.container import Container from xstudio.api.session.media.media_source import MediaSource @@ -231,4 +231,4 @@ def reflag(self, flag_colour, flag_string): Returns: success(bool): Returns result. """ - return self.connection.request_receive(self.remote, reflag_container_atom(), flag_colour, flag_string)[0] + return self.connection.request_receive(self.remote, reflag_container_atom(), flag_colour, flag_string)[0] \ No newline at end of file diff --git a/python/src/xstudio/api/session/playlist/playlist.py b/python/src/xstudio/api/session/playlist/playlist.py index a2f9f311f..98823d5e9 100644 --- a/python/src/xstudio/api/session/playlist/playlist.py +++ b/python/src/xstudio/api/session/playlist/playlist.py @@ -70,7 +70,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): diff --git a/python/src/xstudio/api/session/playlist/timeline/__init__.py b/python/src/xstudio/api/session/playlist/timeline/__init__.py index 99f5cd500..0f5a418f4 100644 --- a/python/src/xstudio/api/session/playlist/timeline/__init__.py +++ b/python/src/xstudio/api/session/playlist/timeline/__init__.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import UuidActor, Uuid, actor, item_atom, MediaType, ItemType, enable_atom +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.api.session.container import Container from xstudio.api.intrinsic import History @@ -155,6 +155,24 @@ def item_name(self, x): """ 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. diff --git a/python/src/xstudio/api/session/playlist/timeline/clip.py b/python/src/xstudio/api/session/playlist/timeline/clip.py index 5e76801d2..038ee4566 100644 --- a/python/src/xstudio/api/session/playlist/timeline/clip.py +++ b/python/src/xstudio/api/session/playlist/timeline/clip.py @@ -1,5 +1,5 @@ # 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 +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.media.media import Media @@ -60,6 +60,24 @@ def item_name(self, x): """ 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. diff --git a/python/src/xstudio/api/session/playlist/timeline/gap.py b/python/src/xstudio/api/session/playlist/timeline/gap.py index 9d87fb2cd..4d2975390 100644 --- a/python/src/xstudio/api/session/playlist/timeline/gap.py +++ b/python/src/xstudio/api/session/playlist/timeline/gap.py @@ -1,5 +1,5 @@ # 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 +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 class Gap(Container): @@ -64,6 +64,24 @@ def item_name(self, x): """ 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 [] diff --git a/python/src/xstudio/api/session/playlist/timeline/stack.py b/python/src/xstudio/api/session/playlist/timeline/stack.py index b972b86d2..20c87d5ad 100644 --- a/python/src/xstudio/api/session/playlist/timeline/stack.py +++ b/python/src/xstudio/api/session/playlist/timeline/stack.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import Uuid, actor, UuidActor, ItemType +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 @@ -49,6 +49,24 @@ def enabled(self, x): """ self.connection.request_receive(self.remote, enable_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 item_name(self): """Get name. diff --git a/python/src/xstudio/api/session/playlist/timeline/track.py b/python/src/xstudio/api/session/playlist/timeline/track.py index dc3e2f869..069b11d35 100644 --- a/python/src/xstudio/api/session/playlist/timeline/track.py +++ b/python/src/xstudio/api/session/playlist/timeline/track.py @@ -1,7 +1,7 @@ # 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 +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.api.session.playlist.timeline import create_item_container @@ -42,6 +42,24 @@ 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) + @property def item_name(self): """Get name. diff --git a/python/src/xstudio/connection/__init__.py b/python/src/xstudio/connection/__init__.py index 7b54d956f..eb8d80368 100644 --- a/python/src/xstudio/connection/__init__.py +++ b/python/src/xstudio/connection/__init__.py @@ -14,6 +14,7 @@ from xstudio.core import RemoteSessionManager, remote_session_path import uuid import time +import traceback import os from pathlib import Path from threading import Thread @@ -604,17 +605,22 @@ def load_plugin_from_path(self, path, plugin_name): Args: path (Path): Path to a directory on filesystem """ - import importlib.util import sys - - sys.path.insert(0, path) - spec = importlib.util.find_spec(plugin_name) - if spec is not None: - module = importlib.util.module_from_spec(spec) - sys.modules[plugin_name] = module - spec.loader.exec_module(module) - self.plugins[path + plugin_name] = module.create_plugin_instance(self) - else: - print ("Error loading plugin \"{0}\" from \"{0}\" - not python importable.".format( - path)) \ No newline at end of file + try: + sys.path.insert(0, path) + spec = importlib.util.find_spec(plugin_name) + if spec is not None: + module = importlib.util.module_from_spec(spec) + sys.modules[plugin_name] = module + 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)) + except Exception as e: + print ("Error loading plugin \"{0}\" from \"{1}\" - : {2}".format( + plugin_name, + path, + e)) + print (traceback.format_exc()) diff --git a/python/src/xstudio/demo/__init__.py b/python/src/xstudio/demo/__init__.py index fc3780144..0532c45cb 100644 --- a/python/src/xstudio/demo/__init__.py +++ b/python/src/xstudio/demo/__init__.py @@ -10,5 +10,6 @@ from xstudio.demo.make_playlists import make_playlists # from xstudio.demo.mask_plugin import mask_plugin from xstudio.demo.dump_shots import dump_shots +from xstudio.demo.dump_shots import dump_shots_gruff from xstudio.demo.dump_shots import render_all_annotations from xstudio.demo.clear_thumbnail_cache import clear_thumbnail_cache diff --git a/python/src/xstudio/demo/dump_shots.py b/python/src/xstudio/demo/dump_shots.py index 7fda2ec3a..0f9d40dde 100644 --- a/python/src/xstudio/demo/dump_shots.py +++ b/python/src/xstudio/demo/dump_shots.py @@ -28,6 +28,35 @@ def dump_shots(session): for i in sorted(shot_flags.keys()): print ("'"+i+"','"+ ",".join(shot_flags[i])+"'" ) +def dump_shots_gruff(session): + """Dump shot flag pairs. + + Args: + session (object): Session object. + """ + + media = session.get_media() + flag_shots = {} + + for i in media: + if i.flag_colour != "#00000000": + # has flag.. + # get metadata and see if shot is set. + meta = i.source_metadata + try: + shot = meta["metadata"]["external"]["DNeg"]["shot"] + if i.flag_text not in flag_shots: + flag_shots[i.flag_text] = set() + + flag_shots[i.flag_text].add(shot) + except: + pass + + for i in sorted(flag_shots.keys()): + print(i+":") + for ii in sorted(flag_shots[i]): + print (ii) + def render_all_annotations(session): """Render all annotations in the session as a sequence of (unordered) images diff --git a/python/src/xstudio/plugin/plugin_base.py b/python/src/xstudio/plugin/plugin_base.py index 33549e070..621a8d770 100644 --- a/python/src/xstudio/plugin/plugin_base.py +++ b/python/src/xstudio/plugin/plugin_base.py @@ -4,7 +4,7 @@ from xstudio.core import JsonStore, Uuid 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 +from xstudio.core import get_global_playhead_events_atom, show_message_box_atom import sys import os import traceback @@ -14,6 +14,10 @@ except: XStudioExtensions = None +def make_simple_string(string_in): + import re + return re.sub('[^0-9a-zA-Z]+', '_', string_in).lower() + class PluginBase(ModuleBase): """Base class for python plugins""" @@ -53,7 +57,8 @@ def add_attribute( self, attribute_name, attribute_value, - attribute_role_data={} + attribute_role_data={}, + register_as_preference=None ): """Add an attribute to your plugin class. Attributes provide a flexible way to store data and/or pass data between your plugin and the xStudio @@ -65,6 +70,10 @@ def add_attribute( attribute_value(int,float,bool,str, list(str)): The value of the attribute attribute_role_data(dict): Other role data of the attribute + register_as_preference(bool): If true the attribute value will be + recorded in the users preferences data when xstudio closes and + the value restored next time xstudio starts up. + """ if "qml_code" in attribute_role_data and self.qml_folder: @@ -75,7 +84,16 @@ def add_attribute( attribute_role_data["qml_code"] ) - return super().add_attribute(attribute_name, attribute_value, attribute_role_data) + preference_path = None + if register_as_preference: + preference_path = "/plugin/" + make_simple_string(self.name) + \ + "/" + make_simple_string(attribute_name) + + return super().add_attribute( + attribute_name, + attribute_value, + attribute_role_data, + preference_path=preference_path) def current_playhead(self): @@ -86,3 +104,28 @@ def current_playhead(self): return Playhead(self.connection, self.connection.request_receive( gphev, viewport_playhead_atom())[0]) + + def popup_message_box( + self, + message_title, + message_body, + close_button=True, + autohide_timeout_secs=0, + ): + """Pop-up a simple message box dialog in the xstudio GUI with only + a 'close' button + Args: + message_title(str): This goes in the title bar of the dialog + message_body(str): The body of the text + 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 + cp = self.connection.send( + app.remote, + show_message_box_atom(), + message_title, + message_body, + close_button, + int(autohide_timeout_secs) + ) \ No newline at end of file diff --git a/retired/playlist/src/playlist_ui.cpp b/retired/playlist/src/playlist_ui.cpp index 0908735fa..20bf07c55 100644 --- a/retired/playlist/src/playlist_ui.cpp +++ b/retired/playlist/src/playlist_ui.cpp @@ -386,7 +386,7 @@ void PlaylistUI::init(actor_system &system_) { }, [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - // spdlog::warn("media added, emit signal"); + spdlog::warn("media added, emit signal"); emit mediaAdded(QUuidFromUuid(ua.uuid())); }, diff --git a/retired/session_ui.cpp b/retired/session_ui.cpp index 028b69e43..f0640841d 100644 --- a/retired/session_ui.cpp +++ b/retired/session_ui.cpp @@ -962,7 +962,7 @@ QUuid SessionUI::duplicateContainer( void SessionUI::updateItemModel(const bool select_new_items, const bool reset) { // spdlog::stopwatch sw; - try { + try { scoped_actor sys{system()}; std::map uuid_actor; std::map hold; diff --git a/scripts/linting/license_stub_check b/scripts/linting/license_stub_check new file mode 100755 index 000000000..bee358fb4 --- /dev/null +++ b/scripts/linting/license_stub_check @@ -0,0 +1,59 @@ +#! /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 index 7402b484f..e1c8de6c5 100755 --- 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\ndodgy mothafucka {0}\n\n".format(lambda_contents)); + print ("\n\nResponse promise used in lambda not returning a value:\n {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 new file mode 100644 index 000000000..487937c1d --- /dev/null +++ b/scripts/qt_install/CMakeLists.txt @@ -0,0 +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 diff --git a/share/fonts/CMakeLists.txt b/share/fonts/CMakeLists.txt index 9d46cf2b9..30de99c03 100644 --- a/share/fonts/CMakeLists.txt +++ b/share/fonts/CMakeLists.txt @@ -20,6 +20,13 @@ add_custom_target( ${fonts} ) -install(FILES - ${fonts} - DESTINATION share/xstudio/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 diff --git a/share/preference/CMakeLists.txt b/share/preference/CMakeLists.txt index 964e695dd..c36ebee60 100644 --- a/share/preference/CMakeLists.txt +++ b/share/preference/CMakeLists.txt @@ -21,6 +21,13 @@ add_custom_target( ${prefs} ) -install(FILES - ${prefs} - DESTINATION share/xstudio/preference) +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_audio.json b/share/preference/core_audio.json index b4188be54..44f121039 100644 --- a/share/preference/core_audio.json +++ b/share/preference/core_audio.json @@ -46,9 +46,9 @@ "pulse_audio_prefs": { "sample_rate": { "path": "/core/audio/pulse_audio_prefs/sample_rate", - "default_value": 44100, + "default_value": 48000, "description": "Souncard sample rate", - "value": 44100, + "value": 48000, "minimum": 8000, "maximum": 96000, "datatype": "int", @@ -101,6 +101,66 @@ "value": "default", "datatype": "string", "context": ["APPLICATION"] + } + }, + "windows_audio_prefs": { + "sample_rate": { + "path": "/core/audio/windows_audio_prefs/sample_rate", + "default_value": 48000, + "description": "Souncard sample rate", + "value": 48000, + "minimum": 8000, + "maximum": 96000, + "datatype": "int", + "context": ["APPLICATION"] + }, + "buffer_size": { + "path": "/core/audio/windows_audio_prefs/buffer_size", + "default_value": 4096, + "description": "Souncard audio samples buffer size", + "value": 4096, + "minimum": 512, + "maximum": 16384, + "datatype": "int", + "context": ["APPLICATION"] + }, + "channels": { + "path": "/core/audio/windows_audio_prefs/channels", + "default_value": 2, + "description": "Souncard channels - currently limited to 2, but can be expanded to 5 for surround", + "value": 2, + "minimum": 2, + "maximum": 2, + "datatype": "int", + "context": ["APPLICATION"] + }, + "min_frames_for_available": { + "path": "/core/audio/windows_audio_prefs/min_frames_for_available", + "default_value": 128, + "description": "Souncard min number of samples before more samples must be written (underrun)", + "value": 128, + "minimum": 128, + "maximum": 1024, + "datatype": "int", + "context": ["APPLICATION"] + }, + "start_frames_threshold": { + "path": "/core/audio/windows_audio_prefs/start_frames_threshold", + "default_value": 0, + "description": "Souncard number of samples written before soundcard starts output", + "value": 0, + "minimum": 0, + "maximum": 512, + "datatype": "int", + "context": ["APPLICATION"] + }, + "sound_card": { + "path": "/core/audio/windows_audio_prefs/sound_card", + "default_value": "default", + "description": "Soundcard name", + "value": "default", + "datatype": "string", + "context": ["APPLICATION"] } } } diff --git a/share/preference/core_cache.json b/share/preference/core_cache.json index de374d3a5..8d9725b94 100644 --- a/share/preference/core_cache.json +++ b/share/preference/core_cache.json @@ -15,7 +15,7 @@ "path": "/core/image_cache/max_size", "default_value": 1024, "description": "Maximum total size of cache in megabytes.", - "value": 1024, + "value": 20480, "datatype": "int", "context": ["APPLICATION","SESSION"] } @@ -35,7 +35,7 @@ "path": "/core/audio_cache/max_size", "default_value": 512, "description": "Maximum total size of cache in megabytes.", - "value": 512, + "value": 2048, "datatype": "int", "context": ["APPLICATION"] } diff --git a/share/preference/core_conform.json b/share/preference/core_conform.json new file mode 100644 index 000000000..d27609c9c --- /dev/null +++ b/share/preference/core_conform.json @@ -0,0 +1,16 @@ +{ + "core": { + "conform":{ + "max_worker_count": { + "path": "/core/conform/max_worker_count", + "default_value": 10, + "description": "Maximum number conform workers.", + "value": 6, + "minimum": 1, + "maximum": 100, + "datatype": "int", + "context": ["APPLICATION"] + } + } + } +} \ No newline at end of file diff --git a/share/preference/core_plugin_manager.json b/share/preference/core_plugin_manager.json index d03df6b8b..84b1020f7 100644 --- a/share/preference/core_plugin_manager.json +++ b/share/preference/core_plugin_manager.json @@ -7,7 +7,13 @@ "description": "Enabled plugins.", "value": { "e4e1d569-2338-4e6e-b127-5a9688df161a": false, - "33201f8d-db32-4278-9c40-8c068372a304": false + "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 }, "datatype": "json", "context": ["APPLICATION"] diff --git a/share/preference/core_session.json b/share/preference/core_session.json index 3b06ec745..43896d03b 100644 --- a/share/preference/core_session.json +++ b/share/preference/core_session.json @@ -29,6 +29,22 @@ "datatype": "double", "context": ["NEW_SESSION"] }, + "compression": { + "path": "/core/session/compression", + "default_value": false, + "description": "Compress sessions.", + "value": false, + "datatype": "bool", + "context": ["APPLICATION"] + }, + "quickview_all_incoming_media": { + "path": "/core/session/quickview_all_incoming_media", + "default_value": false, + "description": "Launch a quickview window for incoming media sent over the CLI", + "value": false, + "datatype": "bool", + "context": ["APPLICATION"] + }, "media_flags": { "path": "/core/session/media_flags", "description": "Media flag names.", @@ -86,9 +102,9 @@ }, "path": { "path": "/core/session/autosave/path", - "default_value": "${HOME}/xStudio/autosave", + "default_value": "${USERPROFILE}/xStudio/autosave", "description": "Path to autosaves.", - "value": "${HOME}/xStudio/autosave", + "value": "${USERPROFILE}/xStudio/autosave", "datatype": "string", "context": ["APPLICATION"] } diff --git a/share/preference/core_snapshot.json b/share/preference/core_snapshot.json new file mode 100644 index 000000000..6f0c3aa45 --- /dev/null +++ b/share/preference/core_snapshot.json @@ -0,0 +1,14 @@ +{ + "core": { + "snapshot":{ + "paths": { + "path": "/core/snapshot/paths", + "default_value": [], + "description": "Snashot scan paths.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + } + } + } +} \ No newline at end of file diff --git a/share/preference/core_thumbnail.json b/share/preference/core_thumbnail.json index 9933787af..93a0c92a4 100644 --- a/share/preference/core_thumbnail.json +++ b/share/preference/core_thumbnail.json @@ -14,9 +14,9 @@ "disk_cache": { "path": { "path": "/core/thumbnail/disk_cache/path", - "default_value": "${HOME}/xStudio/thumbnails", + "default_value": "${USERPROFILE}/xStudio/thumbnails", "description": "Path to thumbnail cache.", - "value": "${HOME}/xStudio/thumbnails", + "value": "${USERPROFILE}/xStudio/thumbnails", "datatype": "string", "context": ["APPLICATION"] }, diff --git a/share/preference/plugin_annotations.json b/share/preference/plugin_annotations.json index 6cd779680..8bcfc7905 100644 --- a/share/preference/plugin_annotations.json +++ b/share/preference/plugin_annotations.json @@ -49,6 +49,22 @@ "datatype": "json", "context": ["APPLICATION"] }, + "text_bgr_colour": { + "path": "/plugin/annotations/text_bgr_colour", + "default_value": ["colour", 1, 0.0, 0.0, 0.0], + "description": "colour of text background", + "value": ["colour", 1, 0.0, 0.0, 0.0], + "datatype": "json", + "context": ["APPLICATION"] + }, + "text_bgr_opacity": { + "path": "/plugin/annotations/text_bgr_opacity", + "default_value": 0, + "description": "opacity of text background", + "value": 0, + "datatype": "int", + "context": ["APPLICATION"] + }, "display_mode": { "path": "/plugin/annotations/display_mode", "default_value": "Only When Paused", diff --git a/share/preference/plugin_colour_pipeline_ocio.json b/share/preference/plugin_colour_pipeline_ocio.json index 66840c30a..7b970f331 100644 --- a/share/preference/plugin_colour_pipeline_ocio.json +++ b/share/preference/plugin_colour_pipeline_ocio.json @@ -34,6 +34,14 @@ "datatype": "bool", "context": ["APPLICATION"] }, + "user_source_mode": { + "path": "/plugin/colour_pipeline/ocio/user_source_mode", + "default_value": true, + "description": "User source colour space mode (adjust from selected view if true).", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"] + }, "enable_gamma": { "path": "/plugin/colour_pipeline/ocio/enable_gamma", "default_value": true, diff --git a/share/preference/plugin_data_source_shotbrowser.json b/share/preference/plugin_data_source_shotbrowser.json new file mode 100644 index 000000000..2f9f841f2 --- /dev/null +++ b/share/preference/plugin_data_source_shotbrowser.json @@ -0,0 +1,2449 @@ +{ + "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 index b1d4e44d0..b78caa2e5 100644 --- a/share/preference/plugin_data_source_shotgun.json +++ b/share/preference/plugin_data_source_shotgun.json @@ -1,92 +1,155 @@ { - "ui": { - "qml": { - "shotgun_browser_settings": { - "path": "/ui/qml/shotgun_browser_settings", - "default_value": {}, - "description": "Prefs relating to window position.", - "value": { - "__ignore__": true, - "x": 100, - "y": 100, - "width": 700, - "height": 400, - "visibility": 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 }, - "datatype": "json", - "context": ["QML_UI"] - } - } - }, - "plugin": { - "data_source": { - "shotgun": { - "note_publish_settings": { - "path": "/plugin/data_source/shotgun/note_publish_settings", - "default_value": { - "__ignore__": true, - "defaultType": "", - "notifyCreator": true, - "combine": false, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "ignoreEmpty": false, - "skipAlreadyPublished": false + + "authentication": { + "client_id": { + "context": [ + "APPLICATION" + ], + "datatype": "string", + "default_value": "", + "description": "Client Id.", + "path": "/plugin/data_source/shotgun/authentication/client_id", + "value": "" }, - "description": "Prefs relating to note publishing.", - "value": { - "__ignore__": true, - "defaultType": "", - "notifyCreator": true, - "combine": false, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "ignoreEmpty": false, - "skipAlreadyPublished": false + "client_secret": { + "context": [ + "APPLICATION" + ], + "datatype": "string", + "default_value": "", + "description": "Client Secret.", + "path": "/plugin/data_source/shotgun/authentication/client_secret", + "value": "" }, - "datatype": "json", - "context": ["APPLICATION"] + "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}" + } }, - - "maximum_result_count": { - "path": "/plugin/data_source/shotgun/maximum_result_count", - "default_value": 1000, - "description": "Maximum results returned.", - "value": 1000, - "minimum": 50, - "maximum": 4999, - "datatype": "int", - "context": ["APPLICATION"] - }, - "global_filters": { - "shot": { - "path": "/plugin/data_source/shotgun/global_filters/shot", - "description": "Shot presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ + "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": "Shot Filter", + "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, - "term": "Preferred Visual", - "value": "movie_dneg" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "2500" - }, { "enabled": true, "livelink": false, @@ -101,93 +164,59 @@ }, { "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast/working" + "term": "Preferred Visual", + "value": "movie_dneg" }, { "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "scan" + "term": "Result Limit", + "value": "10" }, { "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" + "term": "Flag Media", + "value": "Orange" } ] } - ] - }, - "media_action": { - "path": "/plugin/data_source/shotgun/global_filters/media_action", - "description": "Media Action presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ + ], + "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": "Media Filter", + "name": "note 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, + "enabled": false, "term": "Result Limit", - "value": "10" - }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" + "value": "2500" } ] } - ] - }, - "playlist": { - "path": "/plugin/data_source/shotgun/global_filters/playlist", - "description": "playlist presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ + ], + "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", @@ -214,28 +243,17 @@ } ] } - ] - }, - "edit": { - "path": "/plugin/data_source/shotgun/global_filters/edit", - "description": "edit presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ - { - "name": "edit Filter", "expanded": false, - "queries": [] - } - ] - }, - "reference": { - "path": "/plugin/data_source/shotgun/global_filters/reference", - "description": "reference presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ + ], + "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", @@ -252,278 +270,294 @@ } ] } - ] - }, - "note": { - "path": "/plugin/data_source/shotgun/global_filters/note", - "description": "note presets.", - "datatype": "json", - "context": ["APPLICATION"], - "value": null, - "default_value": [ - { - "name": "note Filter", "expanded": false, - "queries": [ - { - "enabled": true, - "term": "Preferred Visual", - "value": "movie_dneg" - }, - { - "enabled": false, - "term": "Result Limit", - "value": "2500" - } - ] - } - ] - } - }, - "presets": { - "note_tree": { - "path": "/plugin/data_source/shotgun/presets/note_tree", - "value": null, - "type": "system", - "description": "Note Tree presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ - { - "expanded": false, - "type": "system", - "name": "All", - "queries": [ - { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "" - } - ] - } - ] + ], + "description": "reference presets.", + "path": "/plugin/data_source/shotgun/global_filters/reference", + "value": null }, - "shot_tree": { - "path": "/plugin/data_source/shotgun/presets/shot_tree", - "value": null, - "type": "system", - "description": "Shot Tree presets.", + "shot": { + "context": [ + "APPLICATION" + ], "datatype": "json", - "context": ["APPLICATION"], "default_value": [ - { - "expanded": false, - "name": "Latest", - "type": "system", - "queries": [ - { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - }, { "expanded": false, - "name": "Latest Client", - "type": "system", + "name": "Shot Filter", "queries": [ { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "097_tr_0140" + "enabled": false, + "term": "On Disk", + "value": "${DNSITEDATA_SHORT_NAME}" }, { "enabled": true, - "term": "Sent To Client", - "value": "True" + "term": "Preferred Visual", + "value": "movie_dneg" }, { "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - } - - ] - }, - "shot": { - "path": "/plugin/data_source/shotgun/presets/shot", - "value": null, - "description": "Shot presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ - { - "expanded": false, - "name": "Latest Dailies", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "3 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "My Dailies", - "queries": [ + "term": "Result Limit", + "value": "2500" + }, { "enabled": true, "livelink": false, - "term": "Author", - "value": "${USERFULLNAME}" + "term": "Twig Type", + "value": "render/out" }, { "enabled": true, - "term": "Lookback", - "value": "30 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Sent To Client", - "queries": [ - { - "enabled": true, - "term": "Sent To Client", - "value": "True" + "livelink": false, + "term": "Twig Type", + "value": "render/playblast" }, { "enabled": true, - "term": "Lookback", - "value": "7 Days" + "livelink": false, + "term": "Twig Type", + "value": "render/playblast/working" }, { - "enabled": false, - "term": "Latest Version", - "value": "True" + "enabled": true, + "livelink": false, + "term": "Twig Type", + "value": "scan" }, { - "enabled": false, - "term": "Production Status", - "value": "Final" + "enabled": true, + "livelink": false, + "term": "Twig Type", + "value": "data/clip/cut" }, { "enabled": false, - "term": "Production Status", - "value": "Final CBB" + "livelink": false, + "term": "Twig Type", + "value": "render/cg" }, { "enabled": false, - "term": "Production Status", - "value": "Final TC" + "livelink": false, + "term": "Twig Type", + "value": "render/element" } - ], - "type": "system" - }, + ] + } + ], + "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": "Prop Finals", - "type": "system", + "name": "Approved Cuts", "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" + "term": "Lookback", + "value": "1 Day" } - ] + ], + "type": "system" }, { "expanded": false, - "name": "All Finals", - "type": "system", + "name": "Trailers", "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" - }, + "term": "Lookback", + "value": "7 Days" + } + ], + "type": "system" + }, + { + "expanded": false, + "name": "All Cuts", + "queries": [ { "enabled": true, - "term": "Production Status", - "value": "Final TF" - }, - { - "enabled": false, - "term": "Latest Version", - "value": "True" + "term": "Lookback", + "value": "30 Days" } - ] + ], + "type": "system" } - ] - }, - "media_action": { - "path": "/plugin/data_source/shotgun/presets/media_action", - "value": null, - "description": "Shot presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ + ], + "description": "Edit presets.", + "path": "/plugin/data_source/shotgun/presets/edit", + "value": null + }, + "media_action": { + "context": [ + "APPLICATION" + ], + "datatype": "json", + "default_value": [ { - "type": "system", "expanded": false, "name": "Hero Scan", "queries": [ @@ -554,10 +588,10 @@ "term": "Latest Version", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Scan Retime/Repo", "queries": [ @@ -592,10 +626,10 @@ "term": "Latest Version", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Edit Ref", "queries": [ @@ -627,10 +661,10 @@ "term": "Order By", "value": "Created DESC" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Client Version", "queries": [ @@ -660,10 +694,10 @@ "term": "Result Limit", "value": "1" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Camera Track", "queries": [ @@ -690,10 +724,10 @@ "term": "Twig Name", "value": "autoslap" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Body Track", "queries": [ @@ -720,10 +754,10 @@ "term": "Twig Name", "value": "autoslap" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Layout", "queries": [ @@ -755,10 +789,10 @@ "term": "Filter", "value": "^P_" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Animation", "queries": [ @@ -811,10 +845,10 @@ "term": "Twig Type", "value": "render/playblast" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest Crowd", "queries": [ @@ -877,10 +911,10 @@ "term": "Twig Name", "value": "crowd_slap$" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Latest FX", "queries": [ @@ -913,12 +947,218 @@ "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" }, { - "type": "system", "expanded": false, - "name": "Latest CFX", + "name": "Next Version", "queries": [ { "enabled": true, @@ -928,63 +1168,43 @@ }, { "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" + "livelink": true, + "term": "Twig Name", + "value": "^O_00TS_0020_comp_repo$" }, { "enabled": true, - "livelink": false, + "livelink": true, "term": "Twig Type", "value": "render/playblast" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "cloth_slap" + "term": "On Disk", + "value": "${DNSITEDATA_SHORT_NAME}" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "creaturesculpt$" + "livelink": true, + "term": "Newer Version", + "value": "1" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "skin_slap" + "term": "Order By", + "value": "Version ASC" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "hair_slap" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "crowd_slap$" + "term": "Result Limit", + "value": "1" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, - "name": "Latest Environment", + "name": "Previous Version", "queries": [ { "enabled": true, @@ -994,49 +1214,43 @@ }, { "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" + "livelink": true, + "term": "Twig Name", + "value": "^O_00TS_0020_comp_repo$" }, { "enabled": true, - "term": "Latest Version", - "value": "True" + "livelink": true, + "term": "Twig Type", + "value": "render/playblast" }, { "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Environ" + "term": "On Disk", + "value": "${DNSITEDATA_SHORT_NAME}" }, { "enabled": true, - "term": "Result Limit", + "livelink": true, + "term": "Older Version", "value": "1" }, { "enabled": true, "term": "Order By", - "value": "Created DESC" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "slap$" + "value": "Version DESC" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "comp$" + "term": "Result Limit", + "value": "1" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, - "name": "Latest Lighting", + "name": "Latest Version", "queries": [ { "enabled": true, @@ -1046,232 +1260,184 @@ }, { "enabled": true, - "livelink": false, + "livelink": true, + "term": "Twig Name", + "value": "^O_00TS_0030_comp_skinny$" + }, + { + "enabled": true, + "livelink": true, "term": "Twig Type", - "value": "render/out" + "value": "render/playblast" }, { "enabled": true, - "term": "Latest Version", - "value": "True" + "term": "On Disk", + "value": "${DNSITEDATA_SHORT_NAME}" }, { "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Lighting" + "livelink": true, + "term": "Newer Version", + "value": "5" }, { "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "comp$" + "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": "Order By", - "value": "Created DESC" + "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": "Twig Name", - "value": "slap$" + "term": "Recipient", + "value": "${USERFULLNAME}" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, - "name": "Latest Comp", + "name": "Client Notes", "queries": [ { "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" + "term": "Lookback", + "value": "7 Days" }, { "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" + "term": "Note Type", + "value": "Client" }, { "enabled": true, - "term": "Latest Version", - "value": "True" + "term": "Note Type", + "value": "Director" }, { - "enabled": true, + "enabled": false, "livelink": false, - "term": "Pipeline Step", - "value": "Comp" - }, + "term": "Recipient", + "value": "${USERFULLNAME}" + } + ], + "type": "system" + }, + { + "expanded": false, + "name": "My Notes", + "queries": [ { "enabled": true, "livelink": false, - "term": "Twig Name", - "value": "comp$" + "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" } - ] - }, - { - "expanded": false, - "type": "system", - "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" - } - ] - }, - { - "expanded": false, - "name": "Previous Version", - "type": "system", - "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" + } + ], + "description": "Note presets.", + "path": "/plugin/data_source/shotgun/presets/note", + "value": null + }, + "note_tree": { + "context": [ + "APPLICATION" + ], + "datatype": "json", + "default_value": [ { - "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" + "expanded": false, + "name": "All", + "queries": [ + { + "dynamic": true, + "enabled": true, + "term": "Shot", + "value": "" + } + ], + "type": "system" } - - ] - }, - - "playlist": { - "path": "/plugin/data_source/shotgun/presets/playlist", - "value": null, - "description": "Playlist presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ + ], + "description": "Note Tree presets.", + "path": "/plugin/data_source/shotgun/presets/note_tree", + "type": "system", + "value": null + }, + "playlist": { + "context": [ + "APPLICATION" + ], + "datatype": "json", + "default_value": [ { - "type": "system", "expanded": false, "name": "Latest Playlists", "queries": [ @@ -1280,11 +1446,11 @@ "term": "Lookback", "value": "7 Days" } - ] + ], + "type": "system" }, { "expanded": false, - "type": "system", "name": "Dailies & Desk Reviews", "queries": [ { @@ -1302,10 +1468,10 @@ "term": "Lookback", "value": "7 Days" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Client Playlists", "queries": [ @@ -1329,10 +1495,10 @@ "term": "Lookback", "value": "30 Days" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Future Playlists", "queries": [ @@ -1346,10 +1512,10 @@ "term": "Disable Global", "value": "Has Contents" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Reference Playlists", "queries": [ @@ -1358,10 +1524,10 @@ "term": "Playlist Type", "value": "Reference Playlist" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "My Playlists", "queries": [ @@ -1371,21 +1537,22 @@ "term": "Author", "value": "${USERFULLNAME}" } - ] + ], + "type": "system" } - ] - }, - - "reference": { - "path": "/plugin/data_source/shotgun/presets/reference", - "value": null, - "description": "Reference presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ + ], + "description": "Playlist presets.", + "path": "/plugin/data_source/shotgun/presets/playlist", + "value": null + }, + "reference": { + "context": [ + "APPLICATION" + ], + "datatype": "json", + "default_value": [ { "expanded": false, - "type": "system", "name": "Concept Art", "queries": [ { @@ -1419,10 +1586,10 @@ "term": "Sent To Client", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Video Reference", "queries": [ @@ -1457,10 +1624,10 @@ "term": "Sent To Client", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Texture Reference", "queries": [ @@ -1495,10 +1662,10 @@ "term": "Sent To Client", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "On Set Reference", "queries": [ @@ -1533,10 +1700,10 @@ "term": "Sent To Client", "value": "True" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Witness Cameras", "queries": [ @@ -1552,10 +1719,10 @@ "term": "Twig Name", "value": "^WITVIDREF" } - ] + ], + "type": "system" }, { - "type": "system", "expanded": false, "name": "Set Drawings", "queries": [ @@ -1571,325 +1738,314 @@ "term": "Twig Name", "value": "^ARTSET" } - ] + ], + "type": "system" } - ] - }, - - "edit": { - "path": "/plugin/data_source/shotgun/presets/edit", - "value": null, - "description": "Edit presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ - { - "type": "system", - "name": "Approved Cuts", "expanded": false, - "queries": [ - {"term": "Lookback", "value": "1 Day", "enabled": true } - ] - }, - { - "type": "system", - "name": "Trailers", "expanded": false, - "queries": [ - {"term": "Lookback", "value": "7 Days", "enabled": true } - ] - }, - { - "type": "system", - "name": "All Cuts", "expanded": false, - "queries": [ - {"term": "Lookback", "value": "30 Days", "enabled": true } - ] - } - ] - }, - "note": { - "path": "/plugin/data_source/shotgun/presets/note", - "value": null, - "description": "Note presets.", - "datatype": "json", - "context": ["APPLICATION"], - "default_value": [ + ], + "description": "Reference presets.", + "path": "/plugin/data_source/shotgun/presets/reference", + "value": null + }, + "shot": { + "context": [ + "APPLICATION" + ], + "datatype": "json", + "default_value": [ { "expanded": false, - "type": "system", - "name": "Latest Notes", + "name": "Latest Dailies", "queries": [ { "enabled": true, "term": "Lookback", - "value": "7 Days" + "value": "3 Days" } - ] + ], + "type": "system" }, { "expanded": false, - "type": "system", - "name": "Supervisor Notes", + "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": true, - "term": "Note Type", - "value": "VFXSupe" + "enabled": false, + "term": "Latest Version", + "value": "True" }, { "enabled": false, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" + "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, - "type": "system", - "name": "Client Notes", + "name": "Prop Finals", "queries": [ { "enabled": true, - "term": "Lookback", - "value": "7 Days" + "term": "Production Status", + "value": "Proposed Final" }, { "enabled": true, - "term": "Note Type", - "value": "Client" + "term": "Production Status", + "value": "Proposed Final Pending" }, { "enabled": true, - "term": "Note Type", - "value": "Director" + "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, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" + "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": [ { - "type": "system", "expanded": false, - "name": "My Notes", + "name": "Latest", "queries": [ { + "dynamic": true, "enabled": true, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" + "term": "Shot", + "value": "" }, { - "enabled": false, - "term": "Note Type", - "value": "Director" + "enabled": true, + "term": "Latest Version", + "value": "True" }, { - "enabled": false, - "term": "Note Type", - "value": "Client" + "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": false, - "term": "Note Type", - "value": "VFXSupe" + "enabled": true, + "term": "Sent To Client", + "value": "True" }, { - "enabled": false, - "term": "Note Type", - "value": "Anim DIR" + "enabled": true, + "term": "Flag Media", + "value": "Orange" } - ] + ], + "type": "system" } - ] - } - }, - "server": { - "host": { - "path": "/plugin/data_source/shotgun/server/host", - "default_value": "shotgun", - "description": "Shotgun host.", - "value": "shotgun", - "datatype": "string", - "context": ["APPLICATION"] - }, - "port": { - "path": "/plugin/data_source/shotgun/server/port", - "default_value": 0, - "description": "Shotgun host port.", - "value": 0, - "datatype": "int", - "context": ["APPLICATION"] - }, - "protocol": { - "path": "/plugin/data_source/shotgun/server/protocol", - "default_value": "https", - "description": "Connection protocol.", - "value": "http", - "datatype": "string", - "context": ["APPLICATION"] - }, - "timeout": { - "path": "/plugin/data_source/shotgun/server/timeout", - "default_value": 120, - "description": "Connection timeout.", - "value": 120, - "datatype": "int", - "context": ["APPLICATION"] - } - }, - "project_id": { - "path": "/plugin/data_source/shotgun/project_id", - "default_value": -1, - "description": "Project id.", - "value": 329, - "datatype": "int", - "context": ["APPLICATION"] - }, - "location": { - "path": "/plugin/data_source/shotgun/location", - "default_value": "${DNSITEDATA_SHORT_NAME}", - "description": "Location.", - "value": "${DNSITEDATA_SHORT_NAME}", - "datatype": "string", - "context": ["APPLICATION"] + ], + "description": "Shot Tree presets.", + "path": "/plugin/data_source/shotgun/presets/shot_tree", + "type": "system", + "value": null + } }, - "context": { - "path": "/plugin/data_source/shotgun/context", - "default_value": "Playlists", - "description": "Default context.", - "value": "Playlists", - "datatype": "string", - "context": ["APPLICATION"] + "project_id": { + "context": [ + "APPLICATION" + ], + "datatype": "int", + "default_value": -1, + "description": "Project id.", + "path": "/plugin/data_source/shotgun/project_id", + "value": 329 }, - "pipestep": { - "path": "/plugin/data_source/shotgun/pipestep", - "default_value": [], - "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": "Prep"}, - {"name": "Previs"}, - {"name": "Retime Layout"}, - {"name": "Rig"}, - {"name": "Roto"}, - {"name": "Scan"}, - {"name": "Shot Sculpt"}, - {"name": "Skin"}, - {"name": "Sweatbox"}, - {"name": "TD"}, - {"name": "None"} + "project_presets": { + "context": [ + "APPLICATION" ], - "description": "Default pipesteps.", "datatype": "json", - "context": ["APPLICATION"] + "default_value": [], + "description": "Project presets.", + "path": "/plugin/data_source/shotgun/project_presets", + "value": null }, - "download": { - "size": { - "path": "/plugin/data_source/shotgun/download/size", - "default_value": 5, - "description": "Cache size in GBytes.", - "value": 5, + "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", - "context": ["APPLICATION"] + "default_value": 0, + "description": "Shotgun host port.", + "path": "/plugin/data_source/shotgun/server/port", + "value": 0 }, - "path": { - "path": "/plugin/data_source/shotgun/download/path", - "default_value": "${HOME}/xStudio/shotgun_cache", - "description": "Path to shotgun download cache.", - "value": "${TMPDIR}/${USER}/xStudio/shotgun_cache", + "protocol": { + "context": [ + "APPLICATION" + ], "datatype": "string", - "context": ["APPLICATION"] + "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 } - }, - "authentication": { - "refresh_token": { - "path": "/plugin/data_source/shotgun/authentication/refresh_token", - "default_value": "", - "description": "Authentication refresh token.", - "value": "", - "datatype": "string", - "context": ["APPLICATION"] - }, - - "grant_type": { - "path": "/plugin/data_source/shotgun/authentication/grant_type", - "default_value": "password", - "description": "Authentication method.", - "value": "password", - "datatype": "string", - "context": ["APPLICATION"] - }, - - "client_id": { - "path": "/plugin/data_source/shotgun/authentication/client_id", - "default_value": "", - "description": "Client Id.", - "value": "", - "datatype": "string", - "context": ["APPLICATION"] - }, - "client_secret": { - "path": "/plugin/data_source/shotgun/authentication/client_secret", - "default_value": "", - "description": "Client Secret.", - "value": "", - "datatype": "string", - "context": ["APPLICATION"] - }, - - "username": { - "path": "/plugin/data_source/shotgun/authentication/username", - "default_value": "${USER}", - "description": "Authentication Username.", - "value": "${USER}", - "datatype": "string", - "context": ["APPLICATION"] - }, - - "password": { - "path": "/plugin/data_source/shotgun/authentication/password", - "default_value": "", - "description": "Authentication password.", - "value": "", - "datatype": "string", - "context": ["APPLICATION"] - }, - - "session_token": { - "path": "/plugin/data_source/shotgun/authentication/session_token", - "default_value": "", - "description": "Authentication session_token.", - "value": "", - "datatype": "string", - "context": ["APPLICATION"] - } - } - } - } - } + } + } + } + }, + "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_grading.json b/share/preference/plugin_grading.json new file mode 100644 index 000000000..6d0d2b346 --- /dev/null +++ b/share/preference/plugin_grading.json @@ -0,0 +1,77 @@ +{ + "plugin": { + "grading": { + "grading_panel": { + "path": "/plugin/grading/grading_panel", + "default_value": "Basic", + "description": "Grading panel", + "value": "Basic", + "datatype": "string", + "context": ["APPLICATION"] + }, + "draw_pen_size": { + "path": "/plugin/grading/draw_pen_size", + "default_value": 10, + "description": "Thickness of scribble pen size", + "value": 150, + "datatype": "int", + "context": ["APPLICATION"] + }, + "erase_pen_size": { + "path": "/plugin/grading/erase_pen_size", + "default_value": 80, + "description": "Thickness of scribble pen size", + "value": 80, + "datatype": "int", + "context": ["APPLICATION"] + }, + "pen_opacity": { + "path": "/plugin/grading/pen_opacity", + "default_value": 100, + "description": "Opacity of scribble pen", + "value": 100, + "datatype": "int", + "context": ["APPLICATION"] + }, + "pen_softness": { + "path": "/plugin/grading/pen_softness", + "default_value": 100, + "description": "Softness of scribble pen", + "value": 100, + "datatype": "int", + "context": ["APPLICATION"] + }, + "pen_colour": { + "path": "/plugin/grading/pen_colour", + "default_value": ["colour", 1, 1.0, 1.0, 0.0], + "description": "colour of shape pen", + "value": ["colour", 1, 1.0, 1.0, 0.0], + "datatype": "json", + "context": ["APPLICATION"] + }, + "display_mode": { + "path": "/plugin/grading/display_mode", + "default_value": "Mask", + "description": "Control whether to show the mask being drawn or not", + "value": "Mask", + "datatype": "string", + "context": ["APPLICATION"] + }, + "toolbox_window_settings": { + "path": "/plugin/grading/toolbox_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"] + } + } + } +} diff --git a/share/preference/ui_qml.json b/share/preference/ui_qml.json index c06cad3f9..47e87f85f 100644 --- a/share/preference/ui_qml.json +++ b/share/preference/ui_qml.json @@ -461,80 +461,200 @@ "default_value": "{}", "description": "Windows layouts", "value": { - "children": [ + "children": [ + { + "window_name": "main_window", + "width": 800, + "height": 800, + "current_layout": 0, + "position_x": 100, + "position_y": 100, + "children": [ + { + "layout_name": "Review", + "enabled": true, + "children": [ + { + "split_horizontal": false, + "child_dividers": [0.5], + "children": [ + { + "split_horizontal": true, + "child_dividers": [0.5], + "children": [ + { + "current_tab": 2, + "children": [ + { "tab_view" : "Media" }, + { "tab_view" : "Timeline" }, + { "tab_view" : "Viewport" } + ] + }, + { + "current_tab": 0, + "children": [ + { "tab_view" : "Playlists" } + ] + } + ] + }, + { + "current_tab": 0, + "children": [ + { "tab_view" : "Timeline" } + ] + } + ] + } + ] + }, + { + "layout_name": "Present", + "enabled": true, + "children": [ + { + "split_horizontal": false, + "child_dividers": [0.5], + "children": [ + { + "split_horizontal": true, + "child_dividers": [0.5], + "children": [ + { + "current_tab": 2, + "children": [ + { "tab_view" : "Media" }, + { "tab_view" : "Timeline" }, + { "tab_view" : "Viewport" } + ] + }, + { + "current_tab": 0, + "children": [ + { "tab_view" : "Playlists" } + ] + } + ] + }, + { + "current_tab": 0, + "children": [ + { "tab_view" : "Timeline" } + ] + } + ] + } + ] + } + ] + } + ] + }, + "datatype": "json", + "context": ["QML_UI"] + }, + "media_list_columns_config": { + "path": "/ui/qml/media_list_columns_config", + "default_value": "{}", + "description": "Media List Columns Configuration", + "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 + }, { - "window_name": "main_window", - "width": 800, - "height": 800, - "position_x": 100, - "position_y": 100, - "children": [ - { - "split": "heightwise", - "fractional_position": 0.0, - "fractional_size": 1.0, - "children": [ - { - "split": "widthwise", - "fractional_position": 0.0, - "fractional_size": 0.7, - "children": [ - { - "panel_source_qml": "../views/timeline/XsTimeline.qml", - "fractional_position": 0.0, - "fractional_size": 0.5 - }, - { - "panel_source_qml": "../views/playlists/XsPlaylists.qml", - "fractional_position": 0.5, - "fractional_size": 0.5 - } - ] - }, - { - "panel_source_qml": "../views/media/XsMedialist.qml", - "fractional_position": 0.7, - "fractional_size": 0.3 - } - ] - } - ] + "title": "Author", + "metadata_path": "/metadata/shotgun/version/relationships/created_by/data/name", + "object": "Media", + "size": 120, + "data_type": "metadata", + "resizable": true, + "sortable": true }, { - "window_name": "second_window", - "children": [ - { - "split": "heightwise", - "fractional_position": 0.0, - "fractional_size": 1.0, - "children": [ - { - "split": "widthwise", - "fractional_position": 0.0, - "fractional_size": 0.7, - "children": [ - { - "panel_source_qml": "../views/timeline/XsTimeline.qml", - "fractional_position": 0.0, - "fractional_size": 0.5 - }, - { - "panel_source_qml": "../views/playlists/XsPlaylists.qml", - "fractional_position": 0.5, - "fractional_size": 0.5 - } - ] - }, - { - "panel_source_qml": "../views/media/XsMedialist.qml", - "fractional_position": 0.7, - "fractional_size": 0.3 - } - ] - } - ] + "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" } - ]}, "datatype": "json", "context": ["QML_UI"] diff --git a/share/snippets/CMakeLists.txt b/share/snippets/CMakeLists.txt index 706e81028..58ac8ac13 100644 --- a/share/snippets/CMakeLists.txt +++ b/share/snippets/CMakeLists.txt @@ -16,6 +16,13 @@ add_custom_target( ${snippets} ) -install(FILES - ${snippets} - DESTINATION share/xstudio/snippets) +if(WIN32) + install(FILES + ${snippets} + DESTINATION + ${CMAKE_INSTALL_PREFIX}/snippets) +else() + install(FILES + ${snippets} + DESTINATION share/xstudio/snippets) +endif() diff --git a/share/snippets/demo.json b/share/snippets/demo.json index cd4de62ea..4f22e228e 100644 --- a/share/snippets/demo.json +++ b/share/snippets/demo.json @@ -31,19 +31,25 @@ }, { "name": "Dump shots", - "description": "Dump shot maetadata with flag.", + "description": "Dump shot metadata with flag.", "menu": "Demo", "script": "from xstudio import demo; demo.dump_shots(XSTUDIO.api.session)" }, { - "name": "Remder all annotations", + "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": "Foo Noo", + "name": "Demo Mask Plugin", "description": "Foo Noo Poo Noo", "menu": "Demo", "script": "from xstudio import demo; demo.mask_plugin(XSTUDIO)" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 73cea7d4d..53f9178a6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ if(INSTALL_XSTUDIO) 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) @@ -28,6 +29,7 @@ if(INSTALL_XSTUDIO) 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) @@ -52,7 +54,7 @@ if(INSTALL_XSTUDIO) if(BUILD_GRADING_DEMO) add_src_and_test(demos/colour_op_plugins/source_grading_demo) - endif(BUILD_GRADING_DEMO) + endif(BUILD_GRADING_DEMO) endif () diff --git a/src/audio/src/CMakeLists.txt b/src/audio/src/CMakeLists.txt index 51a870ff5..153ca43e8 100644 --- a/src/audio/src/CMakeLists.txt +++ b/src/audio/src/CMakeLists.txt @@ -1,18 +1,24 @@ project(audio_output VERSION 0.1.0 LANGUAGES CXX) -find_package(ALSA REQUIRED) -find_package(PulseAudio REQUIRED) +if(WIN32) + # Additional Windows-specific configuration here. +elseif(APPLE) + # TODO: Apple-specific configuration here. +else() + find_package(ALSA REQUIRED) + find_package(PulseAudio REQUIRED) +endif() set(SOURCES - audio_output.cpp - audio_output_actor.cpp + audio_output.cpp + audio_output_actor.cpp ) -if (WIN32) - # TODO +if(WIN32) + list(APPEND SOURCES windows_audio_output_device.cpp) elseif(APPLE) - # TODO + # TODO: Apple-specific configuration here. else() - list(APPEND SOURCES linux_audio_output_device.cpp) + list(APPEND SOURCES linux_audio_output_device.cpp) endif() add_library(${PROJECT_NAME} SHARED ${SOURCES}) @@ -21,12 +27,19 @@ add_library(xstudio::audio_output ALIAS ${PROJECT_NAME}) default_options(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} - PUBLIC - xstudio::utility - xstudio::media_reader - caf::core - pulse - pulse-simple + PUBLIC + xstudio::utility + xstudio::media_reader + caf::core ) -set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) \ No newline at end of file +if(WIN32) + # Link against Windows Core Audio libraries. + target_link_libraries(${PROJECT_NAME} PUBLIC "avrt.lib" "mmdevapi.lib") +elseif(APPLE) + # TODO: Apple-specific audio libs +else() + target_link_libraries(${PROJECT_NAME} PUBLIC pulse pulse-simple) +endif() + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) diff --git a/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index d0a75f9e2..1b4d2a37f 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -122,37 +122,6 @@ template media_reader::AudioBufPtr super_simple_respeed_audio_buffer(const media_reader::AudioBufPtr in, const float velocity); -AudioOutputControl::AudioOutputControl(const utility::JsonStore &jsn) - : Module("AudioOutputControl"), prefs_(jsn) { - - - audio_delay_millisecs_ = - add_integer_attribute("Audio Delay Millisecs", "Audio Delay Millisecs", 0, -1000, 1000); - audio_delay_millisecs_->set_role_data( - module::Attribute::PreferencePath, "/core/audio/audio_latency_millisecs"); - - audio_repitch_ = add_boolean_attribute("Audio Repitch", "Audio Repitch", false); - audio_repitch_->set_role_data( - module::Attribute::PreferencePath, "/core/audio/audio_repitch"); - - audio_scrubbing_ = add_boolean_attribute("Audio Scrubbing", "Audio Scrubbing", false); - audio_repitch_->set_role_data( - module::Attribute::PreferencePath, "/core/audio/audio_scrubbing"); - - - 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::UuidRole, "d1545257-5540-4f2e-9c90-9012232fedb8"); - volume_->set_role_data(module::Attribute::Groups, nlohmann::json{"audio_output"}); - - muted_ = add_boolean_attribute("muted", "muted", false); - muted_->set_role_data(module::Attribute::UuidRole, "59b08f8c-8d86-433e-82f3-ee9c2bc7a27e"); - muted_->set_role_data(module::Attribute::Groups, nlohmann::json{"audio_output"}); - muted_->set_role_data(module::Attribute::PreferencePath, "/core/audio/muted"); -} - void AudioOutputControl::prepare_samples_for_soundcard( std::vector &v, const long num_samps_to_push, @@ -163,12 +132,14 @@ 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; @@ -182,8 +153,7 @@ void AudioOutputControl::prepare_samples_for_soundcard( // new buffer ready to be pushed into the soundcard buffer auto next_sample_play_time = utility::clock::now() + std::chrono::microseconds(microseconds_delay) + - std::chrono::microseconds((num_samps_pushed * 1000000) / sample_rate) - - std::chrono::milliseconds(audio_delay_millisecs_->value()); + std::chrono::microseconds((num_samps_pushed * 1000000) / sample_rate); current_buf_ = pick_audio_buffer(next_sample_play_time, true); @@ -203,11 +173,16 @@ void AudioOutputControl::prepare_samples_for_soundcard( current_buf_, next_buf, previous_buf_); } 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; } @@ -222,13 +197,16 @@ void AudioOutputControl::prepare_samples_for_soundcard( if (current_buf_pos_ == (long)current_buf_->num_samples()) { // current buf is exhausted + // spdlog::info("Current buffer is exhausted."); 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) { @@ -250,12 +228,35 @@ void AudioOutputControl::queue_samples_for_playing( const float velocity) { if (!playing) { - return; } playback_velocity_ = audio_repitch_ ? std::max(0.1f, velocity) : 1.0f; + /* + // 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(); + } + + 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; + } + + // 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; + } + } + */ + + for (const auto &a : audio_frames) { auto audio_frame = a; @@ -266,9 +267,17 @@ void AudioOutputControl::queue_samples_for_playing( 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()) + !audio_frame->num_samples()) { + + // spdlog::info("Audio frame skipped due to either being null, matching " + // "previous/current buffer or having no samples."); continue; + } + // 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()); // 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 @@ -281,16 +290,22 @@ void AudioOutputControl::queue_samples_for_playing( // have we already got these audio samples in our queue? If so erase and // add back in to update the key - for (auto p = sample_data_.begin(); p != sample_data_.end(); ++p) { - if (p->second->media_key() == audio_frame->media_key()) { - sample_data_.erase(p); - break; + 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; + } } } - if (audio_repitch_ && velocity != 1.0f) { - audio_frame = - super_simple_respeed_audio_buffer(audio_frame, fabs(velocity)); + + if (audio_repitch_ && playback_velocity_ != 1.0f) { + audio_frame = super_simple_respeed_audio_buffer( + audio_frame, fabs(playback_velocity_)); } if (!forwards) { @@ -491,7 +506,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( T *tt = ((T *)current_buf->buffer()) + current_buf_position * num_channels; if (fade_in_out & AudioOutputControl::DoFadeHead) { - while (current_buf_position < 32 && num_samples_to_copy && + while (current_buf_position < FADE_FUNC_SAMPS && num_samples_to_copy && current_buf_position < current_buf->num_samples()) { for (int chn = 0; chn < num_channels; ++chn) { @@ -522,7 +537,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 < 32 ? fade_coeffs[i] : 1.0f; + const float f = i < FADE_FUNC_SAMPS ? fade_coeffs[i] : 1.0f; for (int chn = 0; chn < num_channels; ++chn) { (*stream++) = T(round((*tt++) * f)); diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index f77a5a7e8..cfbdd1da7 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -13,160 +13,56 @@ // include for system (soundcard) audio output #ifdef __linux__ -#include "linux_audio_output_device.hpp" -#elif __APPLE__ -// TO DO -#elif _WIN32 -// TO DO +#include "xstudio/audio/linux_audio_output_device.hpp" +#endif +#ifdef _WIN32 +#include "xstudio/audio/windows_audio_output_device.hpp" #endif - using namespace caf; using namespace xstudio::audio; using namespace xstudio::utility; using namespace xstudio; -AudioOutputDeviceActor::AudioOutputDeviceActor( - caf::actor_config &cfg, caf::actor_addr audio_playback_manager, const std::string name) - : caf::event_based_actor(cfg), - name_(name), - playing_(false), - audio_playback_manager_(std::move(audio_playback_manager)), - waiting_for_samples_(false) { - - spdlog::debug("Created {} {}", NAME, name_); - print_on_exit(this, "AudioOutputDeviceActor"); - - try { - auto prefs = global_store::GlobalStoreHelper(system()); - JsonStore j; - join_broadcast(this, prefs.get_group(j)); - open_output_device(j); - } catch (...) { - open_output_device(JsonStore()); - } +GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) + : caf::event_based_actor(cfg), module::Module("GlobalAudioOutputActor") { - behavior_.assign( + audio_repitch_ = add_boolean_attribute("Audio Repitch", "Audio Repitch", false); + audio_repitch_->set_role_data( + module::Attribute::PreferencePath, "/core/audio/audio_repitch"); - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + audio_scrubbing_ = add_boolean_attribute("Audio Scrubbing", "Audio Scrubbing", false); + audio_scrubbing_->set_role_data( + module::Attribute::PreferencePath, "/core/audio/audio_scrubbing"); - [=](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 & /*j*/) { - // TODO: restart soundcard connection with new prefs - }, - [=](playhead::play_atom, const bool is_playing) { - if (!is_playing) { - // this stops the loop pushing samples to the soundcard - playing_ = false; - output_device_->disconnect_from_soundcard(); - } else if (is_playing && !playing_) { - // start loop - playing_ = true; - output_device_->connect_to_soundcard(); - anon_send(actor_cast(this), push_samples_atom_v); - } - }, - [=](push_samples_atom) { - // 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_) - return; - waiting_for_samples_ = true; - - const long num_samps_soundcard_wants = (long)output_device_->desired_samples(); - auto tt = utility::clock::now(); - request( - actor_cast(audio_playback_manager_), - infinite, - 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()) - .then( - [=](const std::vector &samples_to_play) mutable { - output_device_->push_samples( - (const void *)samples_to_play.data(), num_samps_soundcard_wants); - - waiting_for_samples_ = false; - - if (playing_) { - anon_send(actor_cast(this), push_samples_atom_v); - } - }, - [=](caf::error &err) mutable { waiting_for_samples_ = false; }); - } + 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"}); -void AudioOutputDeviceActor::open_output_device(const utility::JsonStore &prefs) { + muted_ = add_boolean_attribute("muted", "muted", false); + muted_->set_role_data(module::Attribute::Groups, nlohmann::json{"audio_output"}); + muted_->set_role_data(module::Attribute::PreferencePath, "/core/audio/muted"); - try { -#ifdef __linux__ - output_device_ = std::make_unique(prefs); -#elif __APPLE__ - // TO DO -#elif _WIN32 - // TO DO -#endif - } catch (std::exception &e) { - spdlog::debug( - "{} Failed to connect to an audio device: {}", __PRETTY_FUNCTION__, e.what()); - } -} - - -AudioOutputControlActor::AudioOutputControlActor(caf::actor_config &cfg, const std::string name) - : caf::event_based_actor(cfg), name_(name) { - init(); -} - -void AudioOutputControlActor::init() { - - spdlog::debug("Created {} {}", NAME, name_); - print_on_exit(this, "AudioOutputControlActor"); + spdlog::debug("Created GlobalAudioOutputActor"); + print_on_exit(this, "GlobalAudioOutputActor"); system().registry().put(audio_output_registry, this); - audio_output_device_ = spawn(actor_cast(this)); - link_to(audio_output_device_); + event_group_ = spawn(this); + link_to(event_group_); + set_parent_actor_addr(actor_cast(this)); behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](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 { + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, - prepare_samples_for_soundcard( - samples, num_samps_to_push, microseconds_delay, num_channels, sample_rate); - - } catch (std::exception &e) { + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - return caf::make_error(xstudio_error::error, e.what()); - } - return samples; - }, [=](playhead::play_atom, const bool is_playing) { - if (!is_playing) { - clear_queued_samples(); - } - anon_send(audio_output_device_, playhead::play_atom_v, is_playing); + send(event_group_, utility::event_atom_v, playhead::play_atom_v, is_playing); }, [=](playhead::sound_audio_atom, const std::vector &audio_buffers, @@ -174,17 +70,15 @@ void AudioOutputControlActor::init() { 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); - } + send( + event_group_, + utility::event_atom_v, + playhead::sound_audio_atom_v, + audio_buffers, + sub_playhead, + playing, + forwards, + velocity); } ); @@ -192,5 +86,19 @@ void AudioOutputControlActor::init() { connect_to_ui(); } +void GlobalAudioOutputActor::on_exit() { system().registry().erase(audio_output_registry); } -void AudioOutputControlActor::on_exit() { system().registry().erase(audio_output_registry); } +void GlobalAudioOutputActor::attribute_changed(const utility::Uuid &attr_uuid, const int role) { + + // update and audio output clients with volume, mute etc. + send( + event_group_, + utility::event_atom_v, + module::change_attribute_event_atom_v, + volume_->value(), + muted_->value(), + audio_repitch_->value(), + audio_scrubbing_->value()); + + Module::attribute_changed(attr_uuid, role); +} diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index c8acb4880..9dc3e7e0c 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -#include "linux_audio_output_device.hpp" +#include "xstudio/audio/linux_audio_output_device.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/logging.hpp" #include @@ -89,9 +89,10 @@ long LinuxAudioOutputDevice::latency_microseconds() { void 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 + // bitdepths if (playback_handle_ && - pa_simple_write(playback_handle_, sample_data, (size_t)num_samples * 2 * 2, &error) < - 0) { + 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()); diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp new file mode 100644 index 000000000..fb6771638 --- /dev/null +++ b/src/audio/src/windows_audio_output_device.cpp @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "xstudio/audio/windows_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; + + +WindowsAudioOutputDevice::WindowsAudioOutputDevice(const utility::JsonStore &prefs) + : AudioOutputDevice(), prefs_(prefs) {} + +WindowsAudioOutputDevice::~WindowsAudioOutputDevice() { disconnect_from_soundcard(); } + +void WindowsAudioOutputDevice::disconnect_from_soundcard() { return; } + +HRESULT WindowsAudioOutputDevice::initializeAudioClient( + const std::string &sound_card /* = L"" */, + long sample_rate /* = 48000 */, + int num_channels /* = 2 */) { + + CComPtr device_enumerator; + CComPtr audio_device; + HRESULT hr; + + // Create a device enumerator + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), + nullptr, + CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&device_enumerator)); + if (FAILED(hr)) { + return hr; + } + + // If sound_card is not provided, enumerate the devices and pick the first one + if (sound_card.empty() || sound_card == "default") { + hr = device_enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &audio_device); + } else { + // Get the audio-render device based on the provided sound_card + std::wstring sound_card_w = L""; + std::wstringstream combiner; + combiner << sound_card.c_str(); + sound_card_w = combiner.str(); + hr = device_enumerator->GetDevice(sound_card_w.c_str(), &audio_device); + } + + if (FAILED(hr)) { + return hr; + } + +#if false + // Print the device name + CComPtr property_store; + hr = audio_device->OpenPropertyStore(STGM_READ, &property_store); + if (SUCCEEDED(hr)) { + PROPVARIANT var_name; + PropVariantInit(&var_name); + + hr = property_store->GetValue(PKEY_Device_FriendlyName, &var_name); + if (SUCCEEDED(hr)) { + wprintf(L"Audio Device Name: %s\n", var_name.pwszVal); + PropVariantClear(&var_name); // always clear the PROPVARIANT to release any + // memory it might've allocated + } + } +#endif + + // Get an IAudioClient3 instance + hr = audio_device->Activate( + __uuidof(IAudioClient3), + CLSCTX_ALL, + nullptr, + reinterpret_cast(&audio_client_)); + if (FAILED(hr)) { + return hr; + } + + // Get the mix format from the audio client + WAVEFORMATEX *pMixFormat = NULL; + hr = audio_client_->GetMixFormat(&pMixFormat); + if (FAILED(hr)) { + spdlog::error("Failed to get mix format: HRESULT=0x{:08x}", hr); + return hr; + } + + + sample_rate_ = pMixFormat->nSamplesPerSec; +#if false + // Print the mix format details + spdlog::info("Mix Format Details:"); + spdlog::info("Format Tag: {}", pMixFormat->wFormatTag); + spdlog::info("Channels: {}", pMixFormat->nChannels); + spdlog::info("Sample Rate: {}", pMixFormat->nSamplesPerSec); + spdlog::info("Bits Per Sample: {}", pMixFormat->wBitsPerSample); + spdlog::info("Block Align: {}", pMixFormat->nBlockAlign); + spdlog::info("Average Bytes Per Second: {}", pMixFormat->nAvgBytesPerSec); + + if (pMixFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE && pMixFormat->cbSize >= 22) { + WAVEFORMATEXTENSIBLE *pExtensible = + reinterpret_cast(pMixFormat); + spdlog::info("Valid Bits Per Sample: {}", pExtensible->Samples.wValidBitsPerSample); + DWORD channel_mask = pExtensible->dwChannelMask; + spdlog::info("Channel Mask: {}", channel_mask); + // Add more fields if needed + + spdlog::info( + "Channel Layout:\nFL {} FLOC {} C {} FROC {} FR {} LFE {}\n\n TFL {} TFC {} TFR {}\n", + (int)(bool)(SPEAKER_FRONT_LEFT & channel_mask), + (int)(bool)(SPEAKER_FRONT_LEFT_OF_CENTER & channel_mask), + (int)(bool)(SPEAKER_FRONT_CENTER & channel_mask), + (int)(bool)(SPEAKER_FRONT_RIGHT_OF_CENTER & channel_mask), + (int)(bool)(SPEAKER_FRONT_RIGHT & channel_mask), + (int)(bool)(SPEAKER_LOW_FREQUENCY & channel_mask), + (int)(bool)(SPEAKER_TOP_FRONT_LEFT & channel_mask), + (int)(bool)(SPEAKER_TOP_FRONT_CENTER & channel_mask), + (int)(bool)(SPEAKER_TOP_FRONT_RIGHT & channel_mask) + + ); + } +#endif + + // Fetch the currently active shared mode format + WAVEFORMATEX *wavefmt = NULL; + UINT32 current_period = 0; + hr = audio_client_->GetCurrentSharedModeEnginePeriod( + (WAVEFORMATEX **)&wavefmt, ¤t_period); + if (FAILED(hr)) { + spdlog::error("Failed to get current shared mode engine period: HRESULT=0x{:08x}", hr); + CoTaskMemFree(pMixFormat); + return hr; + } + + // Fetch the minimum period supported by the current setup + UINT32 DP, FP, MINP, MAXP; + hr = audio_client_->GetSharedModeEnginePeriod(wavefmt, &DP, &FP, &MINP, &MAXP); + if (FAILED(hr)) { + spdlog::error("Failed to get shared mode engine period details: HRESULT=0x{:08x}", hr); + CoTaskMemFree(pMixFormat); + CoTaskMemFree(wavefmt); + return hr; + } + + // Initialize the audio client with the mix format + hr = audio_client_->Initialize( + AUDCLNT_SHAREMODE_SHARED, + 0, + MINP, + 0, + wavefmt, + nullptr // session GUID + ); + + // Free the mix format and wave format after usage + CoTaskMemFree(pMixFormat); + CoTaskMemFree(wavefmt); + + return hr; +} + + +void WindowsAudioOutputDevice::initialize_sound_card() { + sample_rate_ = 48000; // default values + num_channels_ = 2; + std::string sound_card("default"); + buffer_size_ = 2048; // Adjust to match your preferences + + // Replace with your method to get preference values + try { + sample_rate_ = + preference_value(prefs_, "/core/audio/windows_audio_prefs/sample_rate"); + buffer_size_ = + preference_value(prefs_, "/core/audio/windows_audio_prefs/buffer_size"); + num_channels_ = + preference_value(prefs_, "/core/audio/windows_audio_prefs/channels"); + sound_card = + preference_value(prefs_, "/core/audio/windows_audio_prefs/sound_card"); + } catch (std::exception &e) { + spdlog::warn("{} Failed to retrieve WASAPI prefs : {} ", __PRETTY_FUNCTION__, e.what()); + } + + HRESULT hr = initializeAudioClient(sound_card, sample_rate_, num_channels_); + if (FAILED(hr)) { + spdlog::error( + "{} Failed to initialize audio client: HRESULT=0x{:08x}", __PRETTY_FUNCTION__, hr); + return; // or handle the error as appropriate + } + + // 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 + } + + audio_client_->Start(); +} + +void WindowsAudioOutputDevice::connect_to_soundcard() { + // We are already playing ;-D +} + +long WindowsAudioOutputDevice::desired_samples() { + // Note: WASAPI works with a fixed buffer size, so this will return the same + // value for the duration of a playback session + UINT32 bufferSize = 0; // initialize to 0 + HRESULT hr = audio_client_->GetBufferSize(&bufferSize); + + if (FAILED(hr)) { + spdlog::error("Failed to get buffer size from WASAPI with HRESULT: 0x{:08x}", hr); + throw std::runtime_error("Failed to get buffer size"); + } + + UINT32 pad = 0; + hr = audio_client_->GetCurrentPadding(&pad); + if (FAILED(hr)) { + throw std::runtime_error("Failed to get current padding from WASAPI"); + } + + return bufferSize - pad; +} + +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 + 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 +} + +void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { + + int channel_count = num_channels_; + + if (num_samples < 0 || num_samples % channel_count != 0) { + spdlog::error( + "Invalid number of samples provided: {}. Expected a multiple of {}", + num_samples, + channel_count); + return; + } + + // Ensure we have a valid render_client_ + if (!render_client_) { + // spdlog::error("Invalid Render Client"); + return; // Exit if no render client is set + } + + // Retrieve the size (maximum capacity) of the endpoint buffer. + UINT32 buffer_framecount = 0; + HRESULT hr = audio_client_->GetBufferSize(&buffer_framecount); + if (FAILED(hr)) { + spdlog::error("Failed to get buffer size from WASAPI"); + return; + } + + + // Get the number of frames of padding in the endpoint buffer. + UINT32 pad = 0; + hr = audio_client_->GetCurrentPadding(&pad); + if (FAILED(hr)) { + spdlog::error("Failed to get current padding from WASAPI"); + return; + } + + // Calculate the number of frames we can safely write into the buffer without overflow. + long available_frames = buffer_framecount - pad; + long frames_to_write = num_samples / channel_count; + if (available_frames < frames_to_write) { + frames_to_write = available_frames; + } + + if (frames_to_write) { + + // Get a buffer from WASAPI for our audio data. + BYTE *buffer; + 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"); + } + + // Convert int16_t PCM data to float samples considering the interleaved format. + int16_t *pcmData = (int16_t *)sample_data; + float *floatBuffer = (float *)buffer; + const float maxInt16 = 32767.0f; + + long total_samples_to_process = frames_to_write * channel_count; + for (long i = 0; i < total_samples_to_process; i++) { + floatBuffer[i] = pcmData[i] / maxInt16; + } + + // Release the buffer back to WASAPI to play. + 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"); + } + 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)); + } +} diff --git a/src/bookmark/src/bookmark.cpp b/src/bookmark/src/bookmark.cpp index da047ebff..9468f766b 100644 --- a/src/bookmark/src/bookmark.cpp +++ b/src/bookmark/src/bookmark.cpp @@ -43,6 +43,7 @@ Bookmark::Bookmark(const JsonStore &jsn) duration_ = jsn.value("duration", timebase::k_flicks_max); enabled_ = jsn.value("enabled", true); owner_ = jsn.value("owner", utility::Uuid()); + visible_ = jsn.value("visible", true); // N.B. AnnotationBase creation requires caf api comms with plugins and // is handled by the bookmark actor @@ -75,6 +76,7 @@ JsonStore Bookmark::serialise() const { jsn["duration"] = duration_; jsn["enabled"] = enabled_; jsn["owner"] = owner_; + jsn["visible"] = visible_; return jsn; } @@ -92,6 +94,11 @@ bool Bookmark::update(const BookmarkDetail &detail) { changed = true; } + if (detail.visible_) { + visible_ = *(detail.visible_); + changed = true; + } + if (detail.start_) { start_ = *(detail.start_); changed = true; @@ -199,6 +206,7 @@ BookmarkDetail &BookmarkDetail::operator=(const Bookmark &other) { uuid_ = other.uuid(); enabled_ = other.enabled_; has_focus_ = other.has_focus_; + visible_ = other.visible_; start_ = other.start_; duration_ = other.duration_; diff --git a/src/bookmark/src/bookmark_actor.cpp b/src/bookmark/src/bookmark_actor.cpp index 96de7d748..d829a5aa4 100644 --- a/src/bookmark/src/bookmark_actor.cpp +++ b/src/bookmark/src/bookmark_actor.cpp @@ -256,11 +256,11 @@ void BookmarkActor::init() { return false; }, - [=](add_annotation_atom, std::shared_ptr anno) -> bool { + [=](add_annotation_atom, AnnotationBasePtr anno) -> bool { if (base_.annotation_ == anno) return false; base_.annotation_ = anno; - // send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); + send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); // base_.send_changed(event_group_, this); return true; }, @@ -270,7 +270,7 @@ void BookmarkActor::init() { return true; }, - [=](get_annotation_atom) -> std::shared_ptr { + [=](get_annotation_atom) -> AnnotationBasePtr { if (!base_.annotation_) { // if there is no annotation on this note, we return a temporary empty // annotation base that just does the job of carrying the bookmark uuid through @@ -284,6 +284,21 @@ void BookmarkActor::init() { return base_.annotation_; }, + [=](bookmark_detail_atom, get_annotation_atom) -> result { + auto rp = make_response_promise(); + request(caf::actor_cast(this), infinite, bookmark_detail_atom_v) + .then( + [=](const BookmarkDetail &detail) mutable { + auto data = new BookmarkAndAnnotation; + data->detail_ = detail; + data->annotation_ = base_.annotation_; + BookmarkAndAnnotationPtr ptr(data); + rp.deliver(ptr); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](utility::duplicate_atom) -> result { auto rp = make_response_promise(); @@ -356,13 +371,12 @@ void BookmarkActor::build_annotation_via_plugin(const utility::JsonStore &anno_d infinite, plugin_manager::spawn_plugin_atom_v, plugin_uuid, - utility::JsonStore(), - true) + utility::JsonStore()) .then( [=](caf::actor annotations_plugin) { request(annotations_plugin, infinite, build_annotation_atom_v, anno_data) .then( - [=](std::shared_ptr &anno) { + [=](AnnotationBasePtr &anno) { anno->bookmark_uuid_ = base_.uuid(); base_.annotation_ = anno; send( diff --git a/src/bookmark/src/bookmarks_actor.cpp b/src/bookmark/src/bookmarks_actor.cpp index a058235c1..e5675cb78 100644 --- a/src/bookmark/src/bookmarks_actor.cpp +++ b/src/bookmark/src/bookmarks_actor.cpp @@ -429,8 +429,34 @@ void BookmarksActor::init() { } return rp; }, + [=](bookmark_detail_atom, + const utility::UuidSet associated_uuids) -> result> { + if (bookmarks_.empty()) + return std::vector(); + + auto rp = make_response_promise>(); + + fan_out_request( + map_value_to_vec(bookmarks_), infinite, bookmark_detail_atom_v) + .then( + [=](const std::vector details) mutable { + std::vector results; + for (const auto &i : details) { + + if (associated_uuids.empty() or + associated_uuids.count((*(i.owner_)).uuid())) { + results.push_back(i); + } + } + + rp.deliver(results); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + + return rp; + }, - [=](bookmark_detail_atom, const std::vector &associated_uuids) + [=](bookmark_detail_atom, const std::vector associated_uuids) -> result> { if (bookmarks_.empty()) return std::vector(); diff --git a/src/broadcast/src/CMakeLists.txt b/src/broadcast/src/CMakeLists.txt index 61d5faa8f..6d88fcd14 100644 --- a/src/broadcast/src/CMakeLists.txt +++ b/src/broadcast/src/CMakeLists.txt @@ -1,4 +1,3 @@ - SET(LINK_DEPS xstudio::utility caf::core diff --git a/src/caf_utility/src/CMakeLists.txt b/src/caf_utility/src/CMakeLists.txt index e2a23d170..153647db0 100644 --- a/src/caf_utility/src/CMakeLists.txt +++ b/src/caf_utility/src/CMakeLists.txt @@ -1,7 +1,9 @@ +find_package(fmt REQUIRED) + SET(LINK_DEPS - xstudio::utility caf::io caf::core + fmt::fmt ) create_component(caf_utility 0.1.0 "${LINK_DEPS}") diff --git a/src/caf_utility/src/caf_setup.cpp b/src/caf_utility/src/caf_setup.cpp index e27c0bff3..3dbe0dadb 100644 --- a/src/caf_utility/src/caf_setup.cpp +++ b/src/caf_utility/src/caf_setup.cpp @@ -3,7 +3,6 @@ #include "xstudio/atoms.hpp" #include "xstudio/caf_utility/caf_setup.hpp" -#include "xstudio/utility/logging.hpp" using namespace xstudio; using namespace xstudio::caf_utility; diff --git a/src/colour_pipeline/src/colour_operation.cpp b/src/colour_pipeline/src/colour_operation.cpp index 8b92cb743..b247ef271 100644 --- a/src/colour_pipeline/src/colour_operation.cpp +++ b/src/colour_pipeline/src/colour_operation.cpp @@ -3,7 +3,7 @@ #include "xstudio/colour_pipeline/colour_operation.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/media/media.hpp" +#include "xstudio/media_reader/image_buffer.hpp" #include "xstudio/plugin_manager/plugin_base.hpp" using namespace xstudio::colour_pipeline; @@ -12,12 +12,10 @@ caf::message_handler ColourOpPlugin::message_handler_extensions() { return caf::message_handler( [=](colour_operation_uniforms_atom, - const media::AVFrameID &media_ptr) -> result { + const media_reader::ImageBufPtr &image) -> result { try { - utility::JsonStore r; - update_shader_uniforms(r, media_ptr.source_uuid_, media_ptr.params_); - return r; + return update_shader_uniforms(image); } catch (std::exception &e) { @@ -40,7 +38,8 @@ caf::message_handler ColourOpPlugin::message_handler_extensions() { [=](caf::actor media_actor) mutable { auto media = utility::UuidActor(media_ptr.media_uuid_, media_actor); - ColourOperationDataPtr result = data(media_source, media_ptr.params_); + ColourOperationDataPtr result = + colour_op_graphics_data(media_source, media_ptr.params_); rp.deliver(result); }, diff --git a/src/colour_pipeline/src/colour_pipeline.cpp b/src/colour_pipeline/src/colour_pipeline.cpp index f0751d63a..75d41569a 100644 --- a/src/colour_pipeline/src/colour_pipeline.cpp +++ b/src/colour_pipeline/src/colour_pipeline.cpp @@ -6,6 +6,7 @@ #include "xstudio/utility/logging.hpp" #include "xstudio/media/media.hpp" #include "xstudio/media_reader/pixel_info.hpp" +#include "xstudio/media_reader/image_buffer.hpp" using namespace xstudio::colour_pipeline; using namespace xstudio; @@ -23,11 +24,13 @@ ColourPipeline::ColourPipeline(caf::actor_config &cfg, const utility::JsonStore : StandardPlugin( cfg, init_settings.value("name", "ColourPipeline"), init_settings), uuid_(utility::Uuid::generate()), - viewport_name_(init_settings.value("viewport_name", "no viewport")), init_data_(init_settings) { 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), @@ -38,6 +41,8 @@ ColourPipeline::ColourPipeline(caf::actor_config &cfg, const utility::JsonStore } } +ColourPipeline::~ColourPipeline() {} + size_t ColourOperationData::size() const { size_t rt = 0; for (const auto &lut : luts_) { @@ -53,11 +58,17 @@ caf::message_handler ColourPipeline::message_handler_extensions() { j["is_worker"] = true; static int ct = 1; std::stringstream nm; - nm << Module::name() << "_" << viewport_name_ << "_Worker" << ct++; + nm << Module::name() << "_Worker" << ct++; j["name"] = nm.str(); auto worker = self_spawn(j); + link_to(worker); if (worker) { - link_to_module(worker, true, false, true); + link_to_module( + worker, + true, // link_all_attrs + false, // both_ways + true // initial_push_sync + ); workers_.push_back(worker); } return worker; @@ -260,16 +271,15 @@ caf::message_handler ColourPipeline::message_handler_extensions() { spdlog::debug("ColourPipelineActor exited: {}", to_string(reason)); }, [=](colour_operation_uniforms_atom atom, - const media::AVFrameID &media_ptr, - ColourPipelineDataPtr cpipe_data) -> result { + const media_reader::ImageBufPtr &image) -> result { auto rp = make_response_promise(); if (worker_pool_) { - rp.delegate(worker_pool_, atom, media_ptr, cpipe_data); + rp.delegate(worker_pool_, atom, image); } else { auto result = std::make_shared(); - for (const auto &op : cpipe_data->operations()) { - update_shader_uniforms(*result, media_ptr.source_uuid_, op->user_data_); + 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()); @@ -279,8 +289,7 @@ caf::message_handler ColourPipeline::message_handler_extensions() { } for (auto &colour_op_plugin : colour_op_plugins_) { - request( - colour_op_plugin, infinite, colour_operation_uniforms_atom_v, media_ptr) + request(colour_op_plugin, infinite, colour_operation_uniforms_atom_v, image) .then( [=](const utility::JsonStore &uniforms) mutable { result->merge(uniforms); @@ -375,9 +384,10 @@ caf::message_handler ColourPipeline::message_handler_extensions() { [=](connect_to_viewport_atom, caf::actor viewport_actor, const std::string &viewport_name, - const int viewport_index) { + const std::string &viewport_toolbar_name, + bool connect) { disable_linking(); - connect_to_viewport(viewport_actor, viewport_name, viewport_index); + connect_to_viewport(viewport_name, viewport_toolbar_name, connect); enable_linking(); connect_to_ui(); }, @@ -433,7 +443,25 @@ caf::message_handler ColourPipeline::message_handler_extensions() { } catch (...) { }*/ }, - [=](utility::serialise_atom) -> utility::JsonStore { return serialise(); }); + [=](utility::serialise_atom) -> utility::JsonStore { return serialise(); }, + [=](ui::viewport::pre_render_gpu_hook_atom, + const int viewer_index) -> 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 + // request. Due to asynchronous nature of the plugin loading + // (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); + } else { + // add to a queue of these requests pending a response + hook_requests_.push_back(std::make_pair(rp, viewer_index)); + // load_colour_op_plugins() will respond to these requests + // when all the plugins are loaded. + } + return rp; + }); } void ColourPipeline::attribute_changed(const utility::Uuid &attr_uuid, const int role) { @@ -523,6 +551,7 @@ void ColourPipeline::add_colour_op_plugin_data( .then( [=](ColourOperationDataPtr colour_op_data) mutable { result->add_operation(colour_op_data); + result->cache_id_ += colour_op_data->cache_id_; (*rcount)--; if (!(*rcount)) { @@ -564,33 +593,124 @@ void ColourPipeline::load_colour_op_plugins() { // ColourPipelineActor which is spawned byt the plugin_manager_registry... // If we did blocking request receive we would have a lock situation as the // plugin_manager_registry is busy spawning 'this'. + auto pm = system().registry().template get(plugin_manager_registry); request( - pm, infinite, utility::detail_atom_v, plugin_manager::PluginType::PT_COLOUR_OPERATION) + pm, + infinite, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_COLOUR_OPERATION)) .then( [=](const std::vector &colour_op_plugin_details) mutable { + auto count = std::make_shared(colour_op_plugin_details.size()); + + if (colour_op_plugin_details.empty()) { + colour_ops_loaded_ = true; + for (auto &hr : hook_requests_) { + make_pre_draw_gpu_hook(hr.first, hr.second); + } + hook_requests_.clear(); + } + for (const auto &pd : colour_op_plugin_details) { - // Note singleton flag - we only want one instance of a request( pm, infinite, plugin_manager::spawn_plugin_atom_v, pd.uuid_, - utility::JsonStore(), - true // this is the 'singleton' flag - ) + utility::JsonStore()) .then( [=](caf::actor colour_op_actor) mutable { anon_send(colour_op_actor, module::connect_to_ui_atom_v); colour_op_plugins_.push_back(colour_op_actor); + + // TODO: uncomment this when we've fixed colour grading + // singleton issue!! link_to(colour_op_actor); + (*count)--; + if (!(*count)) { + colour_ops_loaded_ = true; + for (auto &hr : hook_requests_) { + make_pre_draw_gpu_hook(hr.first, hr.second); + } + hook_requests_.clear(); + } }, [=](const caf::error &err) mutable { - // spdlog::warn( + for (auto &hr : hook_requests_) { + hr.first.deliver(err); + } + hook_requests_.clear(); }); } }, [=](const caf::error &err) mutable { - // spdlog::warn( + for (auto &hr : hook_requests_) { + hr.first.deliver(err); + } + hook_requests_.clear(); }); } + +/* We wrap any GPUPreDrawHooks in a single GPUPreDrawHook - so if multiple +colour ops have GPUPreDrawHooks then they appear as a single one to the +Viewport that executes our wrapper. Just makes life a bit easier than passing +a set of them back to the viewport, I suppose.*/ +class HookCollection : public plugin::GPUPreDrawHook { + public: + std::vector hooks_; + + void pre_viewport_draw_gpu_hook( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + xstudio::media_reader::ImageBufPtr &image) override { + for (auto &hook : hooks_) { + hook->pre_viewport_draw_gpu_hook( + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dpixel, + image); + } + } +}; + + +void ColourPipeline::make_pre_draw_gpu_hook( + caf::typed_response_promise rp, const int viewer_index) { + + // assumption: requests made in load_colour_op_plugins have finished + HookCollection *collection = new HookCollection(); + auto result = plugin::GPUPreDrawHookPtr(static_cast(collection)); + + if (colour_op_plugins_.empty()) { + rp.deliver(result); + return; + } + caf::scoped_actor sys(system()); + + // loop over the colour_op_plugins, requesting their GPUPreDrawHook class + // instances, gather them in our 'HookCollection' and when we have all + // the responses back we deliver. + 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) + .then( + [=](plugin::GPUPreDrawHookPtr &hook) mutable { + if (hook) { + // gather + collection->hooks_.push_back(hook); + } + (*count)--; + if (!(*count)) { + rp.deliver(result); + } + }, + [=](const caf::error &err) mutable { + rp.deliver(err); + (*count) = 0; + }); + } +} \ No newline at end of file diff --git a/src/colour_pipeline/src/colour_pipeline_actor.cpp b/src/colour_pipeline/src/colour_pipeline_actor.cpp index 5a1b0a82e..4a9e58b15 100644 --- a/src/colour_pipeline/src/colour_pipeline_actor.cpp +++ b/src/colour_pipeline/src/colour_pipeline_actor.cpp @@ -36,8 +36,19 @@ 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(); } + caf::behavior GlobalColourPipelineActor::make_behavior() { return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { @@ -52,8 +63,8 @@ caf::behavior GlobalColourPipelineActor::make_behavior() { }, [=](get_thumbnail_colour_pipeline_atom) -> result { auto rp = make_response_promise(); - if (viewport0_colour_pipeline_) { - rp.deliver(viewport0_colour_pipeline_); + if (colour_piplines_.find("viewport0") != colour_piplines_.end()) { + rp.deliver(colour_piplines_["viewport0"]); } else { request( caf::actor_cast(this), @@ -61,10 +72,7 @@ caf::behavior GlobalColourPipelineActor::make_behavior() { colour_pipeline_atom_v, "viewport0") .then( - [=](caf::actor colour_pipe) mutable { - viewport0_colour_pipeline_ = colour_pipe; - rp.deliver(viewport0_colour_pipeline_); - }, + [=](caf::actor colour_pipe) mutable { rp.deliver(colour_pipe); }, [=](caf::error &err) mutable { rp.deliver(err); }); } return rp; @@ -80,9 +88,9 @@ caf::behavior GlobalColourPipelineActor::make_behavior() { const media::AVFrameID &mptr, const thumbnail::ThumbnailBufferPtr &buf) -> result { auto rp = make_response_promise(); - if (viewport0_colour_pipeline_) { + if (colour_piplines_.find("viewport0") != colour_piplines_.end()) { rp.delegate( - viewport0_colour_pipeline_, + colour_piplines_["viewport0"], media_reader::process_thumbnail_atom_v, mptr, buf); @@ -113,7 +121,7 @@ void GlobalColourPipelineActor::load_colour_pipe_details() { *sys, pm, utility::detail_atom_v, - plugin_manager::PluginType::PT_COLOUR_MANAGEMENT); + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_COLOUR_MANAGEMENT)); for (const auto &pd : colour_pipe_plugin_details_) { if (pd.enabled_ && pd.name_ == default_plugin_name_) { @@ -146,24 +154,32 @@ void GlobalColourPipelineActor::make_colour_pipeline( } } + if (uuid.is_null()) { rp.deliver(make_error( xstudio_error::error, "create_colour_pipeline failed, invalid colour pipeline name.")); } else { + + const std::string viewport_name = jsn["viewport_name"]; + if (colour_piplines_.find(viewport_name) != colour_piplines_.end()) { + rp.deliver(colour_piplines_[viewport_name]); + return; + } + auto pm = system().registry().template get(plugin_manager_registry); request(pm, infinite, plugin_manager::spawn_plugin_atom_v, uuid, jsn) .await( [=](caf::actor colour_pipe) mutable { // link_to(colour_pipe); - if (jsn["viewport_name"] == "viewport0") { - if (viewport0_colour_pipeline_) { - rp.deliver(viewport0_colour_pipeline_); - } else { - viewport0_colour_pipeline_ = colour_pipe; - rp.deliver(colour_pipe); - } + if (colour_piplines_.find(viewport_name) != colour_piplines_.end()) { + // woopsie - colour pipeline already created while we were + // waiting the response here + rp.deliver(colour_piplines_[viewport_name]); + send_exit(colour_pipe, caf::exit_reason::user_shutdown); } else { + colour_piplines_[viewport_name] = colour_pipe; + monitor(colour_pipe); rp.deliver(colour_pipe); } }, diff --git a/src/conform/src/CMakeLists.txt b/src/conform/src/CMakeLists.txt new file mode 100644 index 000000000..cdf381bf0 --- /dev/null +++ b/src/conform/src/CMakeLists.txt @@ -0,0 +1,9 @@ +SET(LINK_DEPS + xstudio::utility + xstudio::broadcast + xstudio::json_store + xstudio::global_store + caf::core +) + +create_component(conform 0.1.0 "${LINK_DEPS}") diff --git a/src/conform/src/conform_manager_actor.cpp b/src/conform/src/conform_manager_actor.cpp new file mode 100644 index 000000000..8b307d96e --- /dev/null +++ b/src/conform/src/conform_manager_actor.cpp @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/conform/conformer.hpp" +#include "xstudio/conform/conform_manager_actor.hpp" +#include "xstudio/plugin_manager/plugin_factory.hpp" +#include "xstudio/plugin_manager/plugin_manager.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/logging.hpp" + +using namespace xstudio; +using namespace std::chrono_literals; +using namespace xstudio::utility; +using namespace xstudio::json_store; +using namespace xstudio::global_store; +using namespace xstudio::conform; +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); + } + } + } + + // distribute to all conformers. + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](conform_tasks_atom) -> result> { + if (not conformers.empty()) { + auto rp = make_response_promise>(); + fan_out_request(conformers, infinite, conform_tasks_atom_v) + .then( + [=](const std::vector> all_results) mutable { + // compile results.. + auto results = std::set(); + + for (const auto &i : all_results) { + for (const auto &j : i) { + results.insert(j); + } + } + + rp.deliver( + std::vector(results.begin(), results.end())); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + } + + return std::vector(); + }, + + [=](conform_atom, + const std::string &conform_task, + const utility::JsonStore &conform_detail, + const UuidActor &playlist, + const UuidActorVector &media) -> 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); }); + + 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()); + + 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++; + } + } + } + + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + } + + return ConformReply(); + }); +} + +ConformManagerActor::ConformManagerActor(caf::actor_config &cfg, const utility::Uuid uuid) + : caf::event_based_actor(cfg), uuid_(std::move(uuid)) { + size_t worker_count = 5; + spdlog::debug("Created ConformManagerActor."); + print_on_exit(this, "ConformManagerActor"); + + try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + join_broadcast(this, prefs.get_group(j)); + worker_count = preference_value(j, "/core/conform/max_worker_count"); + } catch (...) { + } + + 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, + [&] { return system().spawn(); }, + caf::actor_pool::round_robin()); + link_to(pool); + + system().registry().put(conform_registry, this); + + behavior_.assign( + make_get_event_group_handler(event_group_), + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](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); + }, + + [=](conform_atom, + const std::string &conform_task, + const utility::JsonStore &conform_detail, + const UuidActor &playlist, + const UuidActorVector &media) { + delegate(pool, conform_atom_v, conform_task, conform_detail, playlist, media); + }, + + [=](conform_tasks_atom) -> result> { + auto rp = make_response_promise>(); + + request(pool, infinite, conform_tasks_atom_v) + .then( + [=](const std::vector &result) mutable { + if (tasks_ != result) { + tasks_ = result; + send( + event_group_, + utility::event_atom_v, + conform_tasks_atom_v, + tasks_); + } + rp.deliver(tasks_); + }, + [=](const error &err) mutable { rp.deliver(err); }); + + 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 &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++; + } + } 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) + .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]); + } + }, + [=](const error &err) { + throw std::runtime_error( + "Failed to find pool " + to_string(err)); + }); + } + } catch (...) { + } + }); +} + +void ConformManagerActor::on_exit() { system().registry().erase(conform_registry); } diff --git a/src/conform/src/conformer.cpp b/src/conform/src/conformer.cpp new file mode 100644 index 000000000..19ef82ebd --- /dev/null +++ b/src/conform/src/conformer.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/conform/conformer.hpp" + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::conform; + +Conformer::Conformer(const utility::JsonStore &prefs) { update_preferences(prefs); } + +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(); +} diff --git a/src/conform/test/CMakeLists.txt b/src/conform/test/CMakeLists.txt new file mode 100644 index 000000000..6a68029e0 --- /dev/null +++ b/src/conform/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + xstudio::conform +) + +create_tests("${LINK_DEPS}") 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 5794671ba..66643618e 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 @@ -33,16 +33,13 @@ class GradingDemoColourOp : public ColourOpPlugin { float ordering() const override { return -100.0f; } - ColourOperationDataPtr data( + ColourOperationDataPtr colour_op_graphics_data( utility::UuidActor &media_source, const utility::JsonStore &media_source_colour_metadata) override { return op_data_; } - void update_shader_uniforms( - utility::JsonStore &uniforms_dict, - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; + utility::JsonStore update_shader_uniforms(const media_reader::ImageBufPtr &image) override; void attribute_changed( const utility::Uuid &attribute_uuid, const int /*role*/ @@ -104,7 +101,7 @@ GradingDemoColourOp::GradingDemoColourOp( blue_->expose_in_ui_attrs_group("grading_demo_controls"); blue_->set_role_data(module::Attribute::Colour, utility::ColourTriplet(0.0f, 0.0f, 1.0f)); - ColourOperationData *d = new ColourOperationData("Grade Demo OP"); + ColourOperationData *d = new ColourOperationData(PLUGIN_UUID, "Grade Demo OP"); d->shader_.reset(new ui::opengl::OpenGLShader(PLUGIN_UUID, glsl_shader_code)); op_data_.reset(d); @@ -122,14 +119,21 @@ void GradingDemoColourOp::update_shader_uniforms( utility::JsonStore &uniforms_dict, const utility::Uuid & /*source_uuid*/, const utility::JsonStore & /*media_source_colour_metadata*/ -) { +) {} + +utility::JsonStore +GradingDemoColourOp::update_shader_uniforms(const media_reader::ImageBufPtr &image) + // for this simple plugin, the effect is global so we don't depend on // the media - if (tool_is_active_->value()) - uniforms_dict["rgb_factor"] = - Imath::V3f(red_->value(), green_->value(), blue_->value()); - else - uniforms_dict["rgb_factor"] = Imath::V3f(1.0f, 1.0f, 1.0f); + utility::JsonStore uniforms_dict; +// for this simple plugin, the effect is global so we don't depend on +// the media +if (tool_is_active_->value()) + uniforms_dict["rgb_factor"] = Imath::V3f(red_->value(), green_->value(), blue_->value()); +else + uniforms_dict["rgb_factor"] = Imath::V3f(1.0f, 1.0f, 1.0f); +return uniforms_dict; } void GradingDemoColourOp::attribute_changed( 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 a4bffd032..b35e66609 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 @@ -33,16 +33,13 @@ class SolariseOp : public ColourOpPlugin { float ordering() const override { return 100.0f; } - ColourOperationDataPtr data( + ColourOperationDataPtr colour_op_graphics_data( utility::UuidActor &media_source, const utility::JsonStore &media_source_colour_metadata) override { return op_data_; } - void update_shader_uniforms( - utility::JsonStore &uniforms_dict, - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; + utility::JsonStore update_shader_uniforms(const media_reader::ImageBufPtr &image) override; module::FloatAttribute *gamma_; ColourOperationDataPtr op_data_; @@ -59,20 +56,20 @@ SolariseOp::SolariseOp(caf::actor_config &cfg, const utility::JsonStore &init_se gamma_->set_role_data(module::Attribute::DefaultValue, 1.0f); gamma_->set_role_data(module::Attribute::ToolTip, "Set the viewport gamma"); - ColourOperationData *d = new ColourOperationData("Grade and Saturation OP"); + ColourOperationData *d = new ColourOperationData(PLUGIN_UUID, "Grade and Saturation OP"); d->shader_.reset(new ui::opengl::OpenGLShader(PLUGIN_UUID, glsl_shader_code)); op_data_.reset(d); } -void SolariseOp::update_shader_uniforms( - utility::JsonStore &uniforms_dict, - const utility::Uuid & /*source_uuid*/, - const utility::JsonStore & /*media_source_colour_metadata*/ -) { +utility::JsonStore SolariseOp::update_shader_uniforms(const media_reader::ImageBufPtr &image) + // for this simple plugin, the effect is global so we don't depend on // the media - uniforms_dict["solarise"] = gamma_->value(); + utility::JsonStore rt; +rt["solarise"] = gamma_->value(); +return rt; } + } // namespace extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { diff --git a/src/demos/glx_minimal_demo/src/CMakeLists.txt b/src/demos/glx_minimal_demo/src/CMakeLists.txt index a8130e516..92069b1dd 100644 --- a/src/demos/glx_minimal_demo/src/CMakeLists.txt +++ b/src/demos/glx_minimal_demo/src/CMakeLists.txt @@ -5,7 +5,9 @@ set(SOURCES ) find_package(OpenGL REQUIRED) -find_package(X11 REQUIRED) +if(UNIX AND NOT APPLE) + find_package(X11 REQUIRED) +endif() find_package(GLEW REQUIRED) find_package(OpenSSL) find_package(ZLIB) diff --git a/src/demos/glx_minimal_demo/src/main.cpp b/src/demos/glx_minimal_demo/src/main.cpp index c23932321..dd849cb61 100644 --- a/src/demos/glx_minimal_demo/src/main.cpp +++ b/src/demos/glx_minimal_demo/src/main.cpp @@ -21,7 +21,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #include #include @@ -127,7 +129,7 @@ class GLXWindowViewportActor : public caf::event_based_actor { // this is crucial for video refresh sync, the viewport // needs to know when the image was put on the screen - viewport_renderer->framebuffer_swapped(); + viewport_renderer->framebuffer_swapped(utility::clock::now()); } glXMakeCurrent(display, 0, 0); // releases the context so that this function can be @@ -215,7 +217,8 @@ void GLXWindowViewportActor::resizeGL(int w, int h) { Imath::V2f(w, 0), Imath::V2f(w, h), Imath::V2f(0, h), - Imath::V2i(w, h)); + Imath::V2i(w, h), + 1.0f); } int main(int argc, char *argv[]) { @@ -575,4 +578,4 @@ void GLXWindowViewportActor::close() { XDestroyWindow(display, win); XFreeColormap(display, cmap); XCloseDisplay(display); -} \ No newline at end of file +} diff --git a/src/embedded_python/src/CMakeLists.txt b/src/embedded_python/src/CMakeLists.txt index b1819868c..88d406b4c 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) -find_package(pybind11 REQUIRED) # or `add_subdirectory(pybind11)` -find_package(spdlog REQUIRED) -find_package(fmt REQUIRED) +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(OpenTime REQUIRED) +#find_package(OpenTimelineIO REQUIRED) set(SOURCES embedded_python.cpp @@ -16,9 +16,15 @@ set(SOURCES 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 ) +endif() + +set_python_to_proper_build_type() + target_link_libraries(${PROJECT_NAME} PUBLIC caf::core @@ -27,8 +33,14 @@ target_link_libraries(${PROJECT_NAME} xstudio::utility xstudio::broadcast pybind11::embed - OTIO::opentime - OTIO::opentimelineio + #OTIO::opentime + #OTIO::opentimelineio ) 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) +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 8ff56e19a..d4df3741a 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -15,7 +15,7 @@ using namespace xstudio::utility; using namespace pybind11::literals; namespace py = pybind11; -EmbeddedPython *EmbeddedPython::s_instance_ = nullptr; +// EmbeddedPython *EmbeddedPython::s_instance_ = nullptr; EmbeddedPython::EmbeddedPython(const std::string &name, EmbeddedPythonActor *parent) : Container(name, "EmbeddedPython"), parent_(parent) { @@ -34,7 +34,6 @@ void EmbeddedPython::setup() { py::initialize_interpreter(); inited_ = true; } - if (Py_IsInitialized() and not setup_) { exec(R"( import xstudio diff --git a/src/embedded_python/src/embedded_python_actor.cpp b/src/embedded_python/src/embedded_python_actor.cpp index 78df1a626..d194fde59 100644 --- a/src/embedded_python/src/embedded_python_actor.cpp +++ b/src/embedded_python/src/embedded_python_actor.cpp @@ -7,7 +7,9 @@ #include #include +#ifdef BUILD_OTIO #include +#endif #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -24,10 +26,19 @@ using namespace nlohmann; using namespace caf; using namespace pybind11::literals; -namespace py = pybind11; +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) +#else +#pragma warning(push, hidden) +#endif + class PyStdErrOutStreamRedirect { public: PyStdErrOutStreamRedirect() { @@ -96,9 +107,11 @@ 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) { @@ -198,6 +211,8 @@ void EmbeddedPythonActor::act() { return result; }, +#ifdef BUILD_OTIO + // import otio file return as otio xml string. // if already native format should be quick.. [=](session::import_atom, const caf::uri &path) -> result { @@ -264,6 +279,8 @@ 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"); diff --git a/src/embedded_python/src/python310._pth b/src/embedded_python/src/python310._pth new file mode 100644 index 000000000..4c40f8206 --- /dev/null +++ b/src/embedded_python/src/python310._pth @@ -0,0 +1,6 @@ +python310.zip +. +../python +../python/site-packages + +import site diff --git a/src/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index 19750d546..4f3da75c4 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -8,14 +8,19 @@ set(SOURCES 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 ) +endif() + +set_python_to_proper_build_type() target_link_libraries(${PROJECT_NAME} PUBLIC xstudio::module xstudio::audio_output + xstudio::conform xstudio::embedded_python xstudio::event xstudio::global_store @@ -36,9 +41,12 @@ target_link_libraries(${PROJECT_NAME} xstudio::utility caf::core caf::io - asound PRIVATE pybind11::embed ) +if(UNIX AND NOT APPLE) + target_link_libraries(${PROJECT_NAME} PRIVATE asound) # Link against asound on Linux +endif() + set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index 57b671c3b..b53db7dbe 100644 --- a/src/global/src/global_actor.cpp +++ b/src/global/src/global_actor.cpp @@ -12,6 +12,7 @@ #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/colour_pipeline/colour_cache_actor.hpp" #include "xstudio/colour_pipeline/colour_pipeline_actor.hpp" +#include "xstudio/conform/conform_manager_actor.hpp" #include "xstudio/embedded_python/embedded_python_actor.hpp" #include "xstudio/global/global_actor.hpp" #include "xstudio/global_store/global_store.hpp" @@ -23,8 +24,8 @@ #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/studio/studio_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" @@ -32,6 +33,15 @@ #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 +#include "xstudio/audio/windows_audio_output_device.hpp" +#endif + using namespace caf; using namespace xstudio; using namespace xstudio::global; @@ -85,6 +95,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto sga = spawn(); auto sgma = spawn(); + auto ui_models = spawn(); auto pm = spawn(); auto colour = spawn(); auto gmma = spawn(); @@ -95,32 +106,45 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto gmha = spawn(); auto thumbnail = spawn(); auto keyboard_events = spawn(); - auto audio = spawn(); + auto audio = spawn(); auto phev = spawn(); auto pa = spawn("Python"); auto scanner = spawn(); - auto ui_models = spawn(); + auto conform = spawn(); + link_to(attr_evs); + link_to(audio); link_to(colour); - link_to(sga); - link_to(sgma); - link_to(pm); - link_to(gsa); - link_to(gmma); - link_to(gmra); - link_to(gica); + link_to(conform); link_to(gaca); link_to(gcca); + link_to(gica); link_to(gmha); + link_to(gmma); + link_to(gmra); + link_to(gsa); + link_to(keyboard_events); link_to(pa); + link_to(phev); + link_to(pm); link_to(scanner); - link_to(audio); + link_to(sga); + link_to(sgma); link_to(thumbnail); - link_to(attr_evs); - link_to(keyboard_events); - link_to(phev); link_to(ui_models); + + // 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); +#endif + python_enabled_ = false; connected_ = false; api_enabled_ = false; @@ -248,7 +272,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { // add timestamp+ext auto session_fullname = std::string(fmt::format( - "{}_{:%Y%m%d_%H%M%S}.xst", + "{}_{:%Y%m%d_%H%M%S}.xsz", session_name, fmt::localtime(std::time(nullptr)))); @@ -448,6 +472,8 @@ void GlobalActor::init(const utility::JsonStore &prefs) { delegate(studio_, _atom, path, js); }, + [=](bookmark::get_bookmark_atom atom) { delegate(studio_, atom); }, + [=](sync::get_sync_atom _atm) { delegate(sgma, _atm); }); } diff --git a/src/global_store/src/CMakeLists.txt b/src/global_store/src/CMakeLists.txt index 53d0a4bea..1d8c95e04 100644 --- a/src/global_store/src/CMakeLists.txt +++ b/src/global_store/src/CMakeLists.txt @@ -1,13 +1,16 @@ SET(LINK_DEPS xstudio::json_store - stdc++fs caf::core ) SET(STATIC_LINK_DEPS xstudio::json_store_static - stdc++fs caf::core ) +if(UNIX AND NOT APPLE) + list(APPEND LINK_DEPS stdc++fs) + list(APPEND STATIC_LINK_DEPS stdc++fs) +endif() + create_component_static(global_store 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") diff --git a/src/global_store/src/global_store.cpp b/src/global_store/src/global_store.cpp index 2dd3d5389..5b6d583d4 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -68,12 +68,11 @@ void xstudio::global_store::set_global_store_def( bool xstudio::global_store::preference_load_defaults( utility::JsonStore &js, const std::string &path) { - js.clear(); bool result = false; try { for (const auto &entry : fs::directory_iterator(path)) { if (not fs::is_regular_file(entry.status()) or - not(entry.path().extension() == ".json")) { + not(get_path_extension(entry.path()) == ".json")) { continue; } @@ -125,7 +124,7 @@ void load_from_list(const std::string &path, std::vector &overrides) { tmp = fs::canonical(rpath / tmp); } - if (fs::is_regular_file(tmp) and tmp.extension() == ".json") { + if (fs::is_regular_file(tmp) and get_path_extension(tmp) == ".json") { overrides.push_back(tmp); } else { spdlog::warn("Invalid pref entry {}", tmp.string()); @@ -140,6 +139,7 @@ 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; @@ -149,19 +149,24 @@ void load_override(utility::JsonStore &json, const fs::path &path) { // should be dict .. for (auto it : j.items()) { - // test for existence.. try { if (not ends_with(it.key(), "/value") and not ends_with(it.key(), "/locked")) { spdlog::warn("Property key is restricted {} {}", it.key(), path.string()); continue; } - // check it exists, with throw if not.. + // check it exists, with throw if not... unless it is a plugin preference, + // because plugins are loaded after the prefs are built and plugins + // can insert new preferences at runtime. nlohmann::json jj; + bool set_as_overridden = true; try { jj = json.get(it.key()); } catch (...) { - if (ends_with(it.key(), "/value")) { + + if (starts_with(it.key(), "/plugin")) { + set_as_overridden = false; + } else if (ends_with(it.key(), "/value")) { try { jj = json.get(it.value()["template_key"]); } catch (...) { @@ -186,13 +191,14 @@ void load_override(utility::JsonStore &json, const fs::path &path) { "Property overriden {} {} {}", it.key(), to_string(it.value()), path.string()); // tag it. set_preference_overridden_path(json, path.string(), property); - json.set(it.value(), property + "/overridden_value"); + if (set_as_overridden) + json.set(it.value(), property + "/overridden_value"); + } catch (const std::exception &err) { spdlog::warn("{} {} {}", err.what(), it.key(), to_string(it.value())); } } } - void xstudio::global_store::preference_load_overrides( utility::JsonStore &js, const std::vector &paths) { // we get a collection of JSONPOINTERS and values. @@ -208,9 +214,9 @@ void xstudio::global_store::preference_load_overrides( try { fs::path p(i); if (fs::is_regular_file(p)) { - if (p.extension() == ".json") + if (get_path_extension(p) == ".json") overrides.push_back(p); - else if (p.extension() == ".lst") + else if (get_path_extension(p) == ".lst") load_from_list(i, overrides); else throw std::runtime_error("Unrecognised extension"); @@ -218,7 +224,7 @@ void xstudio::global_store::preference_load_overrides( std::set tmp; for (const auto &entry : fs::directory_iterator(p)) { if (not fs::is_regular_file(entry.status()) or - not(entry.path().extension() == ".json")) { + not(get_path_extension(entry.path()) == ".json")) { continue; } tmp.insert(entry.path()); @@ -349,3 +355,34 @@ JsonStore GlobalStore::serialise() const { return jsn; } + +/*If a preference is found at path return the value. Otherwise build +a preference at path and return default.*/ +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 std::string &context) { + try { + + utility::JsonStore v = get(path); + if (!v.contains("overridden_value")) { + v["overridden_value"] = default_; + v["path"] = path; + v["context"] = std::vector({"APPLICATION"}); + JsonStoreHelper::set(v, path, async, broacast_change); + } + return v["value"]; + + } catch (...) { + + 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); + } + return default_; +} \ No newline at end of file diff --git a/src/http_client/src/http_client_actor.cpp b/src/http_client/src/http_client_actor.cpp index ad3155a18..12de88a35 100644 --- a/src/http_client/src/http_client_actor.cpp +++ b/src/http_client/src/http_client_actor.cpp @@ -271,12 +271,19 @@ HTTPWorker::HTTPWorker( cli.set_connection_timeout(connection_timeout, 0); cli.set_read_timeout(read_timeout, 0); cli.set_write_timeout(write_timeout, 0); + auto res = [&]() -> httplib::Result { if (content_type.empty()) return cli.Put(path.c_str(), headers, params); - return cli.Put(path.c_str(), headers, body, content_type.c_str()); + + if (params.empty()) + return cli.Put(path.c_str(), headers, body, content_type.c_str()); + + auto param_path = httplib::append_query_params(path, params); + return cli.Put(param_path.c_str(), headers, body, content_type.c_str()); }(); + if (res.error() != httplib::Error::Success) return make_error(hce::rest_error, get_error_string(res.error())); @@ -581,6 +588,17 @@ void HTTPClientActor::init() { content_type); }, + [=](http_put_atom atom, + const std::string &scheme_host_port, + const std::string &path, + const httplib::Headers &headers, + 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); + }, + [=](http_put_simple_atom atom, const std::string &scheme_host_port, const std::string &path) { @@ -626,5 +644,16 @@ void HTTPClientActor::init() { httplib::Params(), body, content_type); + }, + + [=](http_put_simple_atom atom, + const std::string &scheme_host_port, + const std::string &path, + const httplib::Headers &headers, + 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); }); } diff --git a/src/json_store/src/json_store_actor.cpp b/src/json_store/src/json_store_actor.cpp index 8df0aad8b..cc3e873fa 100644 --- a/src/json_store/src/json_store_actor.cpp +++ b/src/json_store/src/json_store_actor.cpp @@ -38,7 +38,8 @@ JsonStoreActor::JsonStoreActor( [=](get_json_atom, const std::string &path) -> caf::result { try { - return JsonStore(json_store_.get(path)); + 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()); @@ -51,20 +52,23 @@ JsonStoreActor::JsonStoreActor( }, [=](erase_json_atom, const std::string &path) -> bool { - auto result = json_store_.remove(path); + std::string p = path; + auto result = json_store_.remove(path); if (result) broadcast_change(); return result; }, [=](patch_atom, const JsonStore &json) -> bool { - json_store_ = json_store_.patch(json); + const JsonStore j = json; + json_store_ = json_store_.patch(j); broadcast_change(); return true; }, [=](merge_json_atom, const JsonStore &json) -> bool { - json_store_.merge(json); + const JsonStore j = json; + json_store_.merge(j); broadcast_change(); return true; }, @@ -72,12 +76,14 @@ JsonStoreActor::JsonStoreActor( [=](utility::serialise_atom) -> JsonStore { return json_store_; }, [=](set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(caf::actor_cast(this), atom, json, path, false); + std::string p = path; + delegate(caf::actor_cast(this), atom, json, p, false); }, [=](set_json_atom, const JsonStore &json) -> bool { // replace all - json_store_.set(json); + const JsonStore j = json; + json_store_.set(j); broadcast_change(); return true; }, @@ -85,13 +91,15 @@ JsonStoreActor::JsonStoreActor( [=](set_json_atom, const JsonStore &json, const std::string &path, const bool async) -> bool { // is it a subset + std::string p = path; + const JsonStore j = json; try { - json_store_.set(json, path); + json_store_.set(j, p); } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); return false; } - broadcast_change(json, path, async); + broadcast_change(j, p, async); return true; }, @@ -101,27 +109,29 @@ JsonStoreActor::JsonStoreActor( const bool async, const bool _broadcast_change) -> bool { // is it a subset + std::string p = path; + const JsonStore j = json; try { - json_store_.set(json, path); + json_store_.set(j, p); } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); return false; } if (_broadcast_change) - broadcast_change(json, path, async); + broadcast_change(j, p, async); return true; }, [=](subscribe_atom, const std::string &path, caf::actor _actor) -> caf::result { // delegate to reader, return promise ? - auto rp = make_response_promise(); + std::string p = path; + auto rp = make_response_promise(); this->request(_actor, caf::infinite, utility::get_group_atom_v) .then( - [&, path, _actor, rp]( - const std::pair &data) mutable { + [&, p, _actor, rp](const std::pair &data) mutable { const auto [grp, json] = data; actor_group_[actor_cast(_actor)] = grp; - group_path_[grp] = path; + group_path_[grp] = p; this->request(grp, caf::infinite, broadcast::join_broadcast_atom_v) .then( @@ -129,7 +139,7 @@ JsonStoreActor::JsonStoreActor( [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - json_store_.set(json, path); + json_store_.set(json, p); broadcast_change(); rp.deliver(true); }, @@ -180,6 +190,7 @@ caf::message_handler JsonStoreActor::default_event_handler() { void JsonStoreActor::broadcast_change( const JsonStore &change, const std::string &path, const bool async) { + std::string p = path; if (broadcast_delay_.count() and async) { if (not update_pending_) { delayed_anon_send(this, broadcast_delay_, jsonstore_change_atom_v); @@ -187,6 +198,6 @@ void JsonStoreActor::broadcast_change( } } else { // minor change, send now (DANGER MAYBE CAUSE ASYNC ISSUES) - send(broadcast_, update_atom_v, change, path, json_store_); + send(broadcast_, update_atom_v, change, p, json_store_); } } diff --git a/src/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index d0eded21e..097e41559 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -6,13 +6,18 @@ set(SOURCES ../../../../ui/qml/xstudio/qml.qrc ) +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) +#find_package(OpenTime REQUIRED) +#find_package(OpenTimelineIO REQUIRED) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) @@ -21,7 +26,12 @@ set(CMAKE_AUTORCC ON) add_executable(${PROJECT_NAME} ${SOURCES}) configure_file(.clang-tidy .clang-tidy) -configure_file(xstudio.sh.in xstudio.sh) + +if(WIN32) + configure_file(xstudio.bat.in xstudio.bat) +else() + configure_file(xstudio.sh.in xstudio.sh @ONLY) +endif() default_options(${PROJECT_NAME}) @@ -38,41 +48,66 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::qml::log xstudio::ui::qml::module xstudio::ui::qml::playhead - xstudio::ui::qml::quickfuture + 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::utility PUBLIC caf::core $<$:GLdispatch> Qt5::Gui + Qt5::Core + Qt5::Qml Qt5::Quick Qt5::Widgets OpenSSL::SSL ZLIB::ZLIB - OTIO::opentime - OTIO::opentimelineio + #OTIO::opentime + #OTIO::opentimelineio + quickfuture ) -set_target_properties(${PROJECT_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - OUTPUT_NAME "${PROJECT_NAME}.bin" - LINK_DEPENDS_NO_SHARED true -) +if(WIN32) + target_link_libraries(${PROJECT_NAME} PRIVATE dbghelp) +endif() + +if(WIN32) + set_target_properties(${PROJECT_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + OUTPUT_NAME "${PROJECT_NAME}" + 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} RUNTIME DESTINATION bin) -install(PROGRAMS - ${CMAKE_CURRENT_BINARY_DIR}/xstudio.sh - DESTINATION bin - RENAME xstudio) +if(WIN32) + install(PROGRAMS + ${CMAKE_CURRENT_BINARY_DIR}/xstudio.bat + DESTINATION bin + RENAME xstudio) +else() + install(PROGRAMS + ${CMAKE_CURRENT_BINARY_DIR}/xstudio.sh + DESTINATION bin + RENAME xstudio) -install(PROGRAMS - ${CMAKE_CURRENT_SOURCE_DIR}/xstudio_desktop_integration.sh - DESTINATION bin - RENAME xstudio_desktop_integration) + install(PROGRAMS + ${CMAKE_CURRENT_SOURCE_DIR}/xstudio_desktop_integration.sh + DESTINATION bin + RENAME xstudio_desktop_integration) +endif() diff --git a/src/launch/xstudio/src/xstudio.bat.in b/src/launch/xstudio/src/xstudio.bat.in new file mode 100644 index 000000000..d0a673051 --- /dev/null +++ b/src/launch/xstudio/src/xstudio.bat.in @@ -0,0 +1,26 @@ +@echo off + +setlocal + +rem Disable QML_IMPORT_TRACE (equivalent to export QML_IMPORT_TRACE=0 in bash) +set QML_IMPORT_TRACE=0 + +rem Check if XSTUDIO_ROOT environment variable is set +if "%XSTUDIO_ROOT%"=="" ( + rem Use bob world path if available + if not "%BOB_WORLD_SLOT_dneg_xstudio%"=="" ( + set "XSTUDIO_ROOT=%BOB_WORLD_SLOT_dneg_xstudio%\share\xstudio" + set "LD_LIBRARY_PATH=%XSTUDIO_ROOT%\lib;%LD_LIBRARY_PATH%" + ) else ( + set "XSTUDIO_ROOT=%CMAKE_INSTALL_PREFIX%\share\xstudio" + set "LD_LIBRARY_PATH=%XSTUDIO_ROOT%\lib;%LD_LIBRARY_PATH%" + ) +) + +rem Run xstudio_desktop_integration command +call xstudio_desktop_integration + +rem Run xstudio.bin with command line arguments +call xstudio.exe %* + +endlocal \ No newline at end of file diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index ede84c02f..76501dc46 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -5,12 +5,16 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #include #include #include +#ifdef __linux__ #include +#endif #ifndef CPPHTTPLIB_OPENSSL_SUPPORT @@ -66,21 +70,16 @@ CAF_POP_WARNINGS #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 "QuickFuture" - -Q_DECLARE_METATYPE(QUrl) -Q_DECLARE_METATYPE(QList) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture>) -Q_DECLARE_METATYPE(QFuture) +#include "xstudio/ui/qt/offscreen_viewport.hpp" //NOLINT using namespace std; using namespace caf; @@ -96,6 +95,81 @@ 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(); + } + }); + } +#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 + +void handler(int sig) { + void *stack[10]; + HANDLE process = GetCurrentProcess(); + SymInitialize(process, nullptr, TRUE); + + // Capture the call stack + WORD frames = CaptureStackBackTrace(0, 10, stack, nullptr); + + // Print out the frames to stderr + fprintf(stderr, "Error: signal %d:\n", sig); + for (int i = 0; i < frames; ++i) { + DWORD64 address = reinterpret_cast(stack[i]); + char symbolBuffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; + SYMBOL_INFO *symbol = reinterpret_cast(symbolBuffer); + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = MAX_SYM_NAME; + + if (SymFromAddr(process, address, nullptr, symbol)) { + fprintf(stderr, "%d: %s\n", i, symbol->Name); + } else { + fprintf(stderr, "%d: [Unknown Symbol]\n", i); + } + } + + SymCleanup(process); + exit(1); +} +#else void handler(int sig) { void *array[10]; size_t size; @@ -106,8 +180,10 @@ 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 void my_handler(int s) { spdlog::warn("Caught signal {}", s); @@ -139,8 +215,13 @@ 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 player = {parser, "player", "Player mode, minimal UI", {'p', "player"}}; + args::Flag quick_view = { + parser, + "quick-view", + "Open a quick-view for each supplied media item", + {'l', "quick-view"}}; std::unordered_map cmMapValues{ {"none", "Off"}, @@ -210,8 +291,11 @@ struct Launcher { Launcher(int argc, char **argv, actor_system &a_system) : system(a_system) { cli_args.parse_args(argc, argv); +#ifdef _WIN32 + _putenv_s("QML_IMPORT_TRACE", "0"); +#else setenv("QML_IMPORT_TRACE", "0", true); - +#endif signal(SIGSEGV, handler); start_logger( cli_args.debug.Matched() ? spdlog::level::debug : spdlog::level::info, @@ -224,6 +308,7 @@ struct Launcher { actions["headless"] = cli_args.headless.Matched(); actions["debug"] = cli_args.debug.Matched(); actions["player"] = cli_args.player.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(); @@ -277,7 +362,7 @@ struct Launcher { actions["set_play_rate"] = static_cast(args::get(cli_args.play_rate)); if (args::get(cli_args.media_paths).size() == 1 and - ends_with(args::get(cli_args.media_paths)[0], ".xst")) { + is_session(args::get(cli_args.media_paths)[0])) { actions["open_session"] = true; actions["open_session_path"] = args::get(cli_args.media_paths)[0]; } else { @@ -339,9 +424,8 @@ struct Launcher { // check for session file .. if (actions["open_session"]) { try { - JsonStore js; - std::ifstream i(actions["open_session_path"].get()); - i >> js; + JsonStore js = + utility::open_session(actions["open_session_path"].get()); if (actions["new_instance"]) { spdlog::stopwatch sw; @@ -397,7 +481,8 @@ struct Launcher { caf::actor playlist; - // Try default.. + // 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( @@ -428,7 +513,9 @@ struct Launcher { playlist, p.value(), not actions["new_instance"], - actions["compare"]); + actions["compare"], + actions["quick_view"]); + media_sent = true; } @@ -457,6 +544,17 @@ struct Launcher { "Failed to load application preferences {}", xstudio_root("/preference")); 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; } @@ -486,6 +584,7 @@ struct Launcher { { "headless": false, "new_instance": false, + "quick_view": false, "session_name": "", "open_session": false, "debug": false, @@ -555,27 +654,35 @@ struct Launcher { caf::actor playlist, const std::vector &media, const bool remote, - const std::string compare_mode) { + const std::string compare_mode, + const bool open_quick_view) { std::vector> uri_fl; std::vector files; auto media_rate = request_receive(*self, session, session::media_rate_atom_v); + UuidActorVector added_media; for (const auto &p : media) { if (utility::check_plugin_uri_request(p)) { // send to plugin manager.. auto uri = caf::make_uri(p); - if (uri) - self->anon_send( - plugin_manager, - data_source::use_data_atom_v, - *uri, - session, - playlist, - media_rate); - else { + if (uri) { + try { + added_media = request_receive( + *self, + plugin_manager, + data_source::use_data_atom_v, + *uri, + session, + playlist, + media_rate); + } catch (const std::exception &e) { + spdlog::error("Failed to load media '{}'", e.what()); + } + + } else { spdlog::warn("Invalid URI {}", p); } } else { @@ -591,7 +698,7 @@ struct Launcher { files.push_back(p); continue; } - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { } // add to scan list.. @@ -610,14 +717,12 @@ struct Launcher { uri_fl.insert(uri_fl.end(), file_items.begin(), file_items.end()); } - if (not compare_mode.empty()) { + if (not open_quick_view && not compare_mode.empty()) { // To set compare mode, we must have a playhead (which is where // compare mode setting is held) - // playlist can have multiple playheads ... but actually we never - // use this! (see PlaylistUI::createPlayhead()). The actual live - // playlist playhead should be the first in this list. + // get the playlist's playhead caf::actor playhead = request_receive(*self, playlist, playlist::get_playhead_atom_v) .actor(); @@ -633,7 +738,6 @@ struct Launcher { true); } - UuidActorVector added_media; for (const auto &i : uri_fl) { try { added_media.push_back(request_receive( @@ -655,7 +759,7 @@ struct Launcher { // get the actor that is responsible for selecting items from the playlist // for viewing - if (not compare_mode.empty()) { + if (not open_quick_view && not compare_mode.empty()) { auto playhead_selection_actor = request_receive(*self, playlist, playlist::selection_actor_atom_v); @@ -671,9 +775,20 @@ struct Launcher { } } - // finally, to ensure what we've added appears on screen we need to + // 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); + + + // 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); } caf::actor try_reuse_session() { @@ -758,6 +873,7 @@ int main(int argc, char **argv) { "Track"); { + try { // create the actor system @@ -779,13 +895,14 @@ int main(int argc, char **argv) { if (l.actions["headless"]) { system.await_actors_before_shutdown(true); - struct sigaction sigIntHandler; + // TODO: Ahead Fix + // struct sigaction sigIntHandler; - sigIntHandler.sa_handler = my_handler; - sigemptyset(&sigIntHandler.sa_mask); - sigIntHandler.sa_flags = 0; + // sigIntHandler.sa_handler = my_handler; + // sigemptyset(&sigIntHandler.sa_mask); + // sigIntHandler.sa_flags = 0; - sigaction(SIGINT, &sigIntHandler, nullptr); + // sigaction(SIGINT, &sigIntHandler, nullptr); while (not shutdown_xstudio) { // we should be able to shutdown via a API call.. @@ -867,6 +984,7 @@ int main(int argc, char **argv) { 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( @@ -876,9 +994,14 @@ int main(int argc, char **argv) { 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"); @@ -886,11 +1009,6 @@ int main(int argc, char **argv) { qRegisterMetaType("QQmlPropertyMap*"); - - QuickFuture::registerType(); - 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 @@ -930,6 +1048,14 @@ int main(int argc, char **argv) { 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")); + } + } + QObject::connect( &engine, &QQmlApplicationEngine::objectCreated, @@ -943,6 +1069,7 @@ int main(int argc, char **argv) { 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 @@ -968,11 +1095,20 @@ int main(int argc, char **argv) { 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(); + } catch (const std::exception &err) { spdlog::critical("{} {}", __PRETTY_FUNCTION__, err.what()); stop_logger(); std::exit(EXIT_FAILURE); } + + exit_timeout_killer.stop(); } stop_logger(); diff --git a/src/launch/xstudio/src/xstudio.sh.in b/src/launch/xstudio/src/xstudio.sh.in index 69be77d47..2c6957f0c 100755 --- a/src/launch/xstudio/src/xstudio.sh.in +++ b/src/launch/xstudio/src/xstudio.sh.in @@ -11,11 +11,14 @@ then export XSTUDIO_ROOT=$BOB_WORLD_SLOT_dneg_xstudio/share/xstudio 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:$LD_LIBRARY_PATH + 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 PYTHONPATH=@CMAKE_INSTALL_PREFIX@/lib/python:$PYTHONPATH fi fi xstudio_desktop_integration -exec xstudio.bin "$@" +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +exec ${SCRIPT_DIR}/xstudio.bin "$@" diff --git a/src/launch/xstudio/src/xstudio_desktop_integration.sh b/src/launch/xstudio/src/xstudio_desktop_integration.sh index 52be0677d..cae49a717 100755 --- a/src/launch/xstudio/src/xstudio_desktop_integration.sh +++ b/src/launch/xstudio/src/xstudio_desktop_integration.sh @@ -1,7 +1,7 @@ #!/bin/bash # if already installed. -grep -qs Version=1.4.0 ~/.local/share/applications/xstudio.desktop && exit 0 +grep -qs Version=1.6.0 ~/.local/share/applications/xstudio.desktop && exit 0 # Desktop file. mkdir -p ~/.local/share/applications @@ -11,7 +11,7 @@ mkdir -p ~/.local/share/icons cat < ~/.local/share/applications/xstudio.desktop [Desktop Entry] -Version=1.2.0 +Version=1.6.0 Type=Application Name=xStudio Exec=xstudio %U @@ -36,6 +36,11 @@ cat < ~/.local/share/mime/packages/xstudio.xml + + xStudio Project File (compressed) + + + EOF diff --git a/src/media/src/CMakeLists.txt b/src/media/src/CMakeLists.txt index 529c02087..cb6853881 100644 --- a/src/media/src/CMakeLists.txt +++ b/src/media/src/CMakeLists.txt @@ -1,7 +1,6 @@ SET(LINK_DEPS xstudio::json_store - xstudio::media xstudio::playhead xstudio::utility caf::core diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index fa06ab530..11f3997f4 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -172,7 +172,7 @@ void MediaActor::init() { try { rp.delegate(media_sources_.at(base_.current()), atom); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { rp.deliver(make_error(xstudio_error::error, "No MediaSources")); } @@ -237,7 +237,7 @@ void MediaActor::init() { auto rp = make_response_promise(); try { rp.delegate(media_sources_.at(base_.current()), atom, status); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { rp.deliver(make_error(xstudio_error::error, "No MediaSources")); } return rp; @@ -247,7 +247,7 @@ void MediaActor::init() { auto rp = make_response_promise(); try { rp.delegate(media_sources_.at(base_.current()), atom); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { rp.deliver(make_error(xstudio_error::error, "No MediaSources")); } @@ -332,7 +332,7 @@ void MediaActor::init() { auto rp = make_response_promise(); std::string ext = - ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); + ltrim_char(to_upper(get_path_extension(fs::path(uri_to_posix_path(uri)))), '.'); const auto source_uuid = Uuid::generate(); auto source = @@ -392,7 +392,7 @@ void MediaActor::init() { try { rp.delegate(media_sources_.at(base_.current()), atom); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { rp.deliver(make_error(xstudio_error::error, "No MediaSources")); } @@ -405,7 +405,7 @@ void MediaActor::init() { try { rp.delegate(media_sources_.at(base_.current()), atom, params); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { rp.deliver(make_error(xstudio_error::error, "No MediaSources")); } @@ -464,6 +464,9 @@ void MediaActor::init() { return result; }, + [=](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 { @@ -550,8 +553,55 @@ void MediaActor::init() { media_sources_.at(base_.current(media_type)), atom, media_type, logical_frame); return rp; }, - // const int num_frames, - // const int start_frame, + + [=](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, + 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); }); + return rp; + }, [=](get_media_pointers_atom atom, const MediaType media_type, @@ -570,11 +620,15 @@ void MediaActor::init() { override_rate) .then( [=](bool) mutable { - rp.delegate( + request( media_sources_.at(base_.current(media_type)), + infinite, atom, media_type, - ranges); + ranges) + .then( + [=](const media::AVFrameIDs &ids) mutable { rp.deliver(ids); }, + [=](error &err) mutable { rp.deliver(err); }); }, [=](error &err) mutable { rp.deliver(err); }); return rp; @@ -619,6 +673,16 @@ 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()); }, @@ -708,6 +772,28 @@ void MediaActor::init() { return rp; }, + [=](media::checksum_atom) -> 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(MT_IMAGE)), media::checksum_atom_v); + + return rp; + }, + + [=](media::checksum_atom, + const media::MediaType mt) -> 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(mt)), media::checksum_atom_v); + + return rp; + }, [=](get_media_source_names_atom, const media::MediaType mt) -> caf::result>> { @@ -814,6 +900,25 @@ void MediaActor::init() { return make_error(xstudio_error::error, "Invalid MediaSource Uuid"); }, + [=](json_store::get_json_atom atom, + const std::string &path, + bool try_source_actors) -> caf::result { + auto rp = make_response_promise(); + if (!try_source_actors) { + rp.delegate(caf::actor_cast(this), atom, utility::Uuid(), path); + } else { + request(json_store_, infinite, atom, path) + .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); + }); + } + return rp; + }, + [=](json_store::get_json_atom atom, const utility::Uuid &uuid, const std::string &path) -> caf::result { @@ -838,6 +943,15 @@ void MediaActor::init() { 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, const JsonStore &json, @@ -1351,25 +1465,25 @@ void MediaActor::auto_set_current_source(const media::MediaType media_type) { } }; - // first step, get info on the streams that each source can provide. Since - // the response to each request come in asynchonously we need a shared - // pointer to hold the results - auto sources_matching_media_type = std::make_shared>(); - auto response_count = std::make_shared(base_.media_sources().size()); + // 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 source_actor = media_sources_[source_uuid]; - request(source_actor, infinite, detail_atom_v, media_type) - .then([=](const std::vector stream_details) mutable { - if (stream_details.size()) - sources_matching_media_type->insert(source_uuid); - (*response_count)--; - if (!(*response_count)) { + try { + auto stream_details = request_receive>( + *sys, source_actor, detail_atom_v, media_type); - // we've gathered all our responses - auto_set_sources_mt(*sources_matching_media_type); - } - }); + if (stream_details.size()) + sources_matching_media_type.insert(source_uuid); + } catch (...) { + } } + + auto_set_sources_mt(sources_matching_media_type); } diff --git a/src/media/src/media_source_actor.cpp b/src/media/src/media_source_actor.cpp index 805c8eb35..f4dd3db09 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -52,6 +52,7 @@ MediaSourceActor::MediaSourceActor(caf::actor_config &cfg, const JsonStore &jsn) } link_to(json_store_); + bool re_aquire_detail = false; for (const auto &[key, value] : jsn["actors"].items()) { if (value["base"]["container"]["type"] == "MediaStream") { try { @@ -59,12 +60,22 @@ MediaSourceActor::MediaSourceActor(caf::actor_config &cfg, const JsonStore &jsn) system().spawn(static_cast(value)); link_to(media_streams_[Uuid(key)]); join_event_group(this, media_streams_[Uuid(key)]); + + // as of xSTUDIO v2 media detail has been extended to have + // reoslution and pixel aspect info. If we're reading from + // an older session file we need to update the media details + re_aquire_detail |= !value["base"].contains("resolution"); + } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } } + if (re_aquire_detail) { + update_media_detail(); + } + init(); } @@ -128,7 +139,6 @@ MediaSourceActor::MediaSourceActor( 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()); @@ -137,6 +147,48 @@ MediaSourceActor::MediaSourceActor( #include +void MediaSourceActor::update_media_detail() { + + // xstudio 2.0 extends 'StreamDetail' to include resolution and pixel + // aspect data ... here we therefore rescan for StreamDetail + try { + auto gmra = system().registry().template get(media_reader_registry); + if (!gmra) + throw std::runtime_error("No global media reader."); + 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 { + for (auto strm : media_streams_) { + + request(strm.second, infinite, get_stream_detail_atom_v) + .then( + [=](const StreamDetail &old_detail) { + for (const auto &stream_detail : md.streams_) { + if (stream_detail.name_ == old_detail.name_ && + stream_detail.media_type_ == + old_detail.media_type_) { + // update the media stream actor's details + send(strm.second, stream_detail); + } + } + }, + [=](const error &err) mutable { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](const error &err) mutable { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } catch (std::exception &e) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + void MediaSourceActor::acquire_detail( const utility::FrameRate &rate, caf::typed_response_promise rp) { @@ -179,8 +231,7 @@ void MediaSourceActor::acquire_detail( // HACK!!! auto uuid = utility::Uuid::generate(); - auto stream = spawn( - i.name_, i.duration_, i.media_type_, i.key_format_, uuid); + auto stream = spawn(i, uuid); link_to(stream); join_event_group(this, stream); media_streams_[uuid] = stream; @@ -387,10 +438,18 @@ void MediaSourceActor::init() { }, [=](current_media_stream_atom, const MediaType media_type) -> result { - if (media_streams_.count(base_.current(media_type))) - return UuidActor( - base_.current(media_type), media_streams_.at(base_.current(media_type))); - return result(make_error(xstudio_error::error, "No streams")); + auto rp = make_response_promise(); + request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) + .then( + [=](bool) mutable { + if (media_streams_.count(base_.current(media_type))) + rp.deliver(UuidActor( + base_.current(media_type), + media_streams_.at(base_.current(media_type)))); + rp.deliver(make_error(xstudio_error::error, "No streams")); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, [=](current_media_stream_atom, const MediaType media_type, const Uuid &uuid) -> bool { @@ -461,19 +520,26 @@ void MediaSourceActor::init() { [=](get_edit_list_atom, const MediaType media_type, const Uuid &uuid) -> result { - if (base_.current(media_type).is_null()) { - return make_error(xstudio_error::error, "No streams"); - } + auto rp = make_response_promise(); + request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) + .then( + [=](bool) mutable { + if (base_.current(media_type).is_null()) { + rp.deliver(make_error(xstudio_error::error, "No streams")); + } - if (uuid.is_null()) - return utility::EditList({EditListSection( - base_.uuid(), - base_.media_reference(base_.current(media_type)).duration(), - base_.media_reference(base_.current(media_type)).timecode())}); - return utility::EditList({EditListSection( - uuid, - base_.media_reference(base_.current(media_type)).duration(), - base_.media_reference(base_.current(media_type)).timecode())}); + 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())})); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, [=](get_media_pointer_atom, @@ -491,6 +557,8 @@ void MediaSourceActor::init() { get_stream_detail_atom_v) .then( [=](const StreamDetail &detail) mutable { + auto timecode = + base_.media_reference(base_.current(media_type)).timecode(); if (media_type == MT_IMAGE) { request( json_store_, @@ -521,9 +589,11 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), meta, - base_.current(MT_IMAGE), + base_.uuid(), parent_uuid_, media_type)); + results.back().timecode_ = timecode; + timecode = timecode + 1; } rp.deliver(results); @@ -558,6 +628,8 @@ void MediaSourceActor::init() { utility::Uuid(), parent_uuid_, media_type)); + results.back().timecode_ = timecode; + timecode = timecode + 1; } rp.deliver(results); @@ -583,9 +655,11 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), utility::JsonStore(), - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type)); + results.back().timecode_ = timecode; + timecode = timecode + 1; } rp.deliver(results); @@ -642,7 +716,7 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), meta, - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type)); }, @@ -678,7 +752,7 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), utility::JsonStore(), - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type)); } @@ -1163,12 +1237,39 @@ void MediaSourceActor::init() { }, [=](media::checksum_atom, const std::pair &checksum) { - return base_.checksum(checksum); + // 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()); + + // trigger re-eval of reader.. + request( + caf::actor_cast(this), + infinite, + get_media_pointer_atom_v, + MT_IMAGE, + static_cast(0)) + .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); + }, + [=](const error &err) {}); + } }, [=](media::rescan_atom atom) -> 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()) @@ -1182,11 +1283,30 @@ void MediaSourceActor::init() { 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()); } @@ -1291,6 +1411,8 @@ void MediaSourceActor::get_media_pointers_for_frames( [=](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; @@ -1323,7 +1445,7 @@ void MediaSourceActor::get_media_pointers_for_frames( base_.reader(), caf::actor_cast(this), meta, - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type); } else { @@ -1333,10 +1455,13 @@ void MediaSourceActor::get_media_pointers_for_frames( detail.key_format_, *_uri, frame, detail.name_); } + mptr.timecode_ = timecode; + mptr.playhead_logical_frame_ = logical_frame; + timecode = timecode + 1; result.emplace_back( std::shared_ptr( new media::AVFrameID(mptr))); - } catch (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { result.emplace_back( media::make_blank_frame(media_type)); } diff --git a/src/media/src/media_stream.cpp b/src/media/src/media_stream.cpp index 2e67dde17..a63ef42b6 100644 --- a/src/media/src/media_stream.cpp +++ b/src/media/src/media_stream.cpp @@ -8,28 +8,35 @@ using namespace xstudio::media; using namespace xstudio::utility; MediaStream::MediaStream(const JsonStore &jsn) - : utility::Container(static_cast(jsn["container"])), - duration_(jsn["duration"]), - key_format_(jsn["key_format"]), - media_type_(media_type_from_string(jsn["media_type"])) {} - -MediaStream::MediaStream( - const std::string &name, - utility::FrameRateDuration duration, - const MediaType media_type, - std::string key_format) - : utility::Container(name, "MediaStream"), - duration_(std::move(duration)), - key_format_(std::move(key_format)), - media_type_(media_type) {} + : 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")) + detail_.resolution_ = jsn["resolution"]; + if (jsn.contains("pixel_aspect") && jsn["pixel_aspect"].is_number()) + detail_.pixel_aspect_ = jsn["pixel_aspect"]; + if (jsn.contains("stream_index")) + detail_.index_ = jsn["stream_index"]; +} + +MediaStream::MediaStream(const StreamDetail &detail) + : utility::Container(detail.name_, "MediaStream"), detail_(detail) {} JsonStore MediaStream::serialise() const { JsonStore jsn; - jsn["container"] = Container::serialise(); - jsn["key_format"] = key_format_; - jsn["media_type"] = to_readable_string(media_type_); - jsn["duration"] = duration_; + jsn["container"] = Container::serialise(); + jsn["key_format"] = detail_.key_format_; + jsn["media_type"] = to_readable_string(detail_.media_type_); + jsn["duration"] = detail_.duration_; + jsn["resolution"] = detail_.resolution_; + jsn["pixel_aspect"] = detail_.pixel_aspect_; + 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 23bb56261..1d0e3f5c1 100644 --- a/src/media/src/media_stream_actor.cpp +++ b/src/media/src/media_stream_actor.cpp @@ -33,15 +33,8 @@ MediaStreamActor::MediaStreamActor(caf::actor_config &cfg, const JsonStore &jsn) } MediaStreamActor::MediaStreamActor( - caf::actor_config &cfg, - const std::string &name, - const utility::FrameRateDuration &duration, - const MediaType media_type, - const std::string &key_format, - const utility::Uuid &uuid - - ) - : caf::event_based_actor(cfg), base_(name, duration, media_type, key_format) { + caf::actor_config &cfg, const StreamDetail &detail, const utility::Uuid &uuid) + : caf::event_based_actor(cfg), base_(detail) { if (not uuid.is_null()) base_.set_uuid(uuid); @@ -78,10 +71,9 @@ void MediaStreamActor::init() { [=](get_media_type_atom) -> MediaType { return base_.media_type(); }, - [=](get_stream_detail_atom) -> StreamDetail { - return StreamDetail( - base_.duration(), base_.name(), base_.media_type(), base_.key_format()); - }, + [=](get_stream_detail_atom) -> StreamDetail { return base_.detail(); }, + + [=](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); @@ -90,8 +82,7 @@ void MediaStreamActor::init() { [=](utility::duplicate_atom) -> UuidActor { // clone ourself.. const auto uuid = utility::Uuid::generate(); - const auto actor = spawn( - base_.name(), base_.duration(), base_.media_type(), base_.key_format(), uuid); + const auto actor = spawn(base_.detail(), uuid); return UuidActor(uuid, actor); }, diff --git a/src/media_cache/src/media_cache_actor.cpp b/src/media_cache/src/media_cache_actor.cpp index 00e48aab7..bdd51ad06 100644 --- a/src/media_cache/src/media_cache_actor.cpp +++ b/src/media_cache/src/media_cache_actor.cpp @@ -35,10 +35,13 @@ 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) { - // spdlog::stopwatch sw; + // spdlog::stopwatch sw; +#ifdef _WIN32 + _heapmin(); +#else malloc_trim(64); +#endif // spdlog::warn("Release {:.3f}", sw); }); } diff --git a/src/media_hook/src/CMakeLists.txt b/src/media_hook/src/CMakeLists.txt index 705662a65..ca84febda 100644 --- a/src/media_hook/src/CMakeLists.txt +++ b/src/media_hook/src/CMakeLists.txt @@ -1,9 +1,12 @@ SET(LINK_DEPS xstudio::global_store caf::core - stdc++fs - dl + xstudio::media ) +if(UNIX) + list(APPEND LINK_DEPS stdc++fs dl) +endif() + create_component(media_hook 0.1.0 "${LINK_DEPS}") diff --git a/src/media_hook/src/media_hook_actor.cpp b/src/media_hook/src/media_hook_actor.cpp index c8189f14c..a5c44caa8 100644 --- a/src/media_hook/src/media_hook_actor.cpp +++ b/src/media_hook/src/media_hook_actor.cpp @@ -32,7 +32,10 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) 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::PT_MEDIA_HOOK); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_HOOK)); for (const auto &i : details) { if (i.enabled_) { @@ -91,11 +94,14 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) }, [=](get_media_hook_atom, caf::actor media_source) -> result { - if (hooks.empty()) - return true; - auto rp = make_response_promise(); + if (hooks.empty()) { + rp.deliver(true); + return rp; + } + + request(media_source, infinite, json_store::get_json_atom_v, "") .then( [=](const JsonStore &jsn) mutable { @@ -194,7 +200,10 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) // 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, utility::detail_atom_v, plugin_manager::PluginType::PT_MEDIA_HOOK) + pm, + infinite, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_HOOK)) .then( [=](const std::vector &details) mutable { utility::JsonStore result; @@ -216,7 +225,10 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) // 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, utility::detail_atom_v, plugin_manager::PluginType::PT_MEDIA_HOOK) + pm, + infinite, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_HOOK)) .then( [=](const std::vector &details) mutable { bool matched = true; diff --git a/src/media_metadata/src/CMakeLists.txt b/src/media_metadata/src/CMakeLists.txt index e38b84526..a468a0a68 100644 --- a/src/media_metadata/src/CMakeLists.txt +++ b/src/media_metadata/src/CMakeLists.txt @@ -1,8 +1,12 @@ SET(LINK_DEPS xstudio::global_store caf::core - stdc++fs - dl ) +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 +endif() + create_component(media_metadata 0.1.0 "${LINK_DEPS}") diff --git a/src/media_metadata/src/media_metadata.cpp b/src/media_metadata/src/media_metadata.cpp index c2f719f1a..abc13b7f5 100644 --- a/src/media_metadata/src/media_metadata.cpp +++ b/src/media_metadata/src/media_metadata.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // #include +#ifdef __linux__ #include +#endif #include #include diff --git a/src/media_metadata/src/media_metadata_actor.cpp b/src/media_metadata/src/media_metadata_actor.cpp index f0a9ff44b..21a543a70 100644 --- a/src/media_metadata/src/media_metadata_actor.cpp +++ b/src/media_metadata/src/media_metadata_actor.cpp @@ -27,7 +27,10 @@ MediaMetadataWorkerActor::MediaMetadataWorkerActor(caf::actor_config &cfg) { scoped_actor sys{system()}; auto details = request_receive>( - *sys, pm, utility::detail_atom_v, plugin_manager::PluginType::PT_MEDIA_METADATA); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_METADATA)); join_event_group(this, pm); @@ -48,7 +51,7 @@ MediaMetadataWorkerActor::MediaMetadataWorkerActor(caf::actor_config &cfg) utility::detail_atom, const std::vector &detail) { for (const auto &i : detail) { - if (i.type_ == plugin_manager::PluginType::PT_MEDIA_METADATA) { + if (i.type_ & plugin_manager::PluginFlags::PF_MEDIA_METADATA) { if (not i.enabled_ and name_plugin_.count(i.name_)) { // plugin has been disabled. auto plugin = name_plugin_[i.name_]; diff --git a/src/media_reader/src/CMakeLists.txt b/src/media_reader/src/CMakeLists.txt index 89f96045f..acd6c4501 100644 --- a/src/media_reader/src/CMakeLists.txt +++ b/src/media_reader/src/CMakeLists.txt @@ -4,9 +4,13 @@ SET(LINK_DEPS xstudio::global_store xstudio::broadcast caf::core - stdc++fs - dl ) +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 +endif() + create_component(media_reader 0.1.0 "${LINK_DEPS}") 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 39df52801..8963a2f6c 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 @@ -41,7 +41,10 @@ MediaDetailAndThumbnailReaderActor::MediaDetailAndThumbnailReaderActor( auto pm = system().registry().get(plugin_manager_registry); scoped_actor sys{system()}; auto details = request_receive>( - *sys, pm, utility::detail_atom_v, plugin_manager::PluginType::PT_MEDIA_READER); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_READER)); auto prefs = GlobalStoreHelper(system()); JsonStore js; @@ -58,8 +61,6 @@ MediaDetailAndThumbnailReaderActor::MediaDetailAndThumbnailReaderActor( } } - colour_pipe_manager_ = system().registry().get(colour_pipeline_registry); - behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, diff --git a/src/media_reader/src/media_reader.cpp b/src/media_reader/src/media_reader.cpp index ccb241ed5..dba3d4db1 100644 --- a/src/media_reader/src/media_reader.cpp +++ b/src/media_reader/src/media_reader.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // #include +#ifdef __linux__ #include +#endif #include #include diff --git a/src/media_reader/src/media_reader_actor.cpp b/src/media_reader/src/media_reader_actor.cpp index be8423ac6..6ad7c1d93 100644 --- a/src/media_reader/src/media_reader_actor.cpp +++ b/src/media_reader/src/media_reader_actor.cpp @@ -161,7 +161,10 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( 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::PT_MEDIA_READER); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_READER)); for (const auto &i : details) { if (i.enabled_) { @@ -236,6 +239,10 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( [=](const group_down_msg &) {}, + [=](retire_readers_atom, const media::AVFrameID &mptr) -> bool { + return prune_reader(reader_key(mptr.uri_, mptr.actor_addr_)); + }, + [=](get_image_atom, const media::AVFrameID &mptr, const bool @@ -594,6 +601,21 @@ GlobalMediaReaderActor::reader_key(const caf::uri &_uri, const caf::actor_addr & } +bool GlobalMediaReaderActor::prune_reader(const std::string &key) { + auto result = false; + auto it = readers_.find(key); + + if (it != std::end(readers_)) { + result = true; + unlink_from(it->second); + send_exit(it->second, caf::exit_reason::user_shutdown); + reader_access_.erase(it->first); + readers_.erase(it->first); + } + + return result; +} + void GlobalMediaReaderActor::prune_readers() { utility::time_point now = clock::now(); bool reaped = true; @@ -835,11 +857,6 @@ 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); - spdlog::warn( - "read_and_cache_image Failed to load buffer {} {} {}", - to_string(mptr->uri_), - mptr->key_, - to_string(err)); // we might still have more work to do so keep going continue_precacheing(); }); @@ -891,11 +908,6 @@ 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); - spdlog::warn( - "read_and_cache_audio Failed to load buffer {} {} {}", - to_string(mptr->uri_), - mptr->key_, - to_string(err)); // we might still have more work to do so keep going continue_precacheing(); }); @@ -939,6 +951,7 @@ void GlobalMediaReaderActor::mark_playhead_received_precache_result( void GlobalMediaReaderActor::send_error_to_source( const caf::actor_addr &addr, const caf::error &err) { if (addr) { + auto dest = caf::actor_cast(addr); if (dest and err.category() == caf::type_id_v) { media_error me; diff --git a/src/module/src/attribute.cpp b/src/module/src/attribute.cpp index d8fb3f665..3c53a38fe 100644 --- a/src/module/src/attribute.cpp +++ b/src/module/src/attribute.cpp @@ -114,10 +114,29 @@ void Attribute::set_preference_path(const std::string &preference_path) { set_role_data(PreferencePath, preference_path); } -void Attribute::expose_in_ui_attrs_group(const std::string &group_name) { - auto n = role_data_[Groups].get>(); - n.push_back(group_name); - set_role_data(Groups, n); +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"})); + return; + } + auto n = role_data_[Groups].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>(); + for (auto p = n.begin(); p != n.end(); ++p) { + if (*p == group_name) { + n.erase(p); + set_role_data(Groups, n); + return; + } + } + } } void Attribute::set_tool_tip(const std::string &tool_tip) { set_role_data(ToolTip, tool_tip); } diff --git a/src/module/src/module.cpp b/src/module/src/module.cpp index 4ecfb2485..1e6d9f576 100644 --- a/src/module/src/module.cpp +++ b/src/module/src/module.cpp @@ -4,6 +4,7 @@ #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/tree.hpp" #include "xstudio/module/module.hpp" #include "xstudio/ui/mouse.hpp" #include "xstudio/playhead/playhead.hpp" @@ -16,13 +17,24 @@ using namespace xstudio::utility; using namespace xstudio::module; using namespace xstudio; -Module::Module(const std::string name) : name_(std::move(name)) {} +namespace { +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); + } + }}; +} +} // namespace + +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(); - module_events_group_ = caf::actor(); } void Module::set_parent_actor_addr(caf::actor_addr addr) { @@ -52,7 +64,6 @@ void Module::set_parent_actor_addr(caf::actor_addr addr) { } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - // self()->link_to(module_events_group_); } // we can't add hotkeys until the parent actor has been set. Subclasses of @@ -74,6 +85,20 @@ void Module::set_parent_actor_addr(caf::actor_addr addr) { } 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) { @@ -101,22 +126,16 @@ 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()) { - try { - utility::request_receive( - *sys, - other_module, - change_attribute_value_atom_v, - attribute->get_role_data(Attribute::Title), - utility::JsonStore(attribute->role_data_as_json(Attribute::Value)), - true); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } + anon_send( + other_module, + change_attribute_value_atom_v, + attribute->get_role_data(Attribute::Title), + utility::JsonStore(attribute->role_data_as_json(Attribute::Value)), + true); } } } @@ -127,6 +146,25 @@ void Module::link_to_module( } } +void Module::unlink_module(caf::actor other_module) { + auto addr = caf::actor_cast(other_module); + auto p = std::find(fully_linked_modules_.begin(), fully_linked_modules_.end(), addr); + bool found_link = false; + if (p != fully_linked_modules_.end()) { + fully_linked_modules_.erase(p); + found_link = true; + } + p = std::find(partially_linked_modules_.begin(), partially_linked_modules_.end(), addr); + if (p != partially_linked_modules_.end()) { + partially_linked_modules_.erase(p); + found_link = true; + } + + if (found_link) { + anon_send(other_module, module::link_module_atom_v, self(), false); + } +} + FloatAttribute *Module::add_float_attribute( const std::string &title, const std::string &abbr_title, @@ -148,7 +186,8 @@ FloatAttribute *Module::add_float_attribute( fscrub_sensitivity)); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); + return rt; } @@ -162,7 +201,7 @@ StringChoiceAttribute *Module::add_string_choice_attribute( 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); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -170,7 +209,7 @@ 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); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -179,7 +218,7 @@ BooleanAttribute *Module::add_boolean_attribute( auto *rt(new BooleanAttribute(title, abbr_title, value)); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -188,7 +227,7 @@ StringAttribute *Module::add_string_attribute( auto rt = new StringAttribute(title, abbr_title, value); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -201,7 +240,7 @@ IntegerAttribute *Module::add_integer_attribute( auto rt = new IntegerAttribute(title, abbr_title, value, int_min, int_max); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -210,7 +249,7 @@ 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); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -221,7 +260,7 @@ ColourAttribute *Module::add_colour_attribute( auto rt = new ColourAttribute(title, abbr_title, value); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } @@ -231,21 +270,43 @@ Module::add_action_attribute(const std::string &title, const std::string &abbr_t auto rt = new ActionAttribute(title, abbr_title); rt->set_owner(this); - attributes_.emplace_back(static_cast(rt)); + add_attribute(static_cast(rt)); return rt; } bool Module::remove_attribute(const utility::Uuid &attribute_uuid) { - bool rt = false; - for (auto p = attributes_.begin(); p != attributes_.end(); p++) { - if ((*p)->uuid() == attribute_uuid) { - attributes_.erase(p); - anon_send(module_events_group_, attribute_deleted_event_atom_v, attribute_uuid); - rt = true; - break; + + auto attr = get_attribute(attribute_uuid); + if (attr) { + + auto central_models_data_actor = + 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); + for (const auto &group_name : groups) { + + anon_send( + central_models_data_actor, + ui::model_data::remove_rows_atom_v, + group_name, + attribute_uuid); + } + } + for (auto p = attributes_.begin(); p != attributes_.end(); p++) { + if ((*p)->uuid() == attribute_uuid) { + attributes_.erase(p); + break; + } } + } else { + throw std::runtime_error( + fmt::format( + "{}: No attribute with id {}", __PRETTY_FUNCTION__, to_string(attribute_uuid)) + .c_str()); } - return rt; + return true; } utility::JsonStore Module::serialise() const { @@ -324,6 +385,7 @@ caf::message_handler Module::message_handler() { const std::string &role_name, const utility::JsonStore &value) -> result { try { + for (const auto &p : attributes_) { if (p->uuid() == attr_uuid) { @@ -356,7 +418,6 @@ caf::message_handler Module::message_handler() { attribute_events_group_, broadcast::join_broadcast_atom_v, subscriber); - return r; } catch (std::exception &e) { @@ -570,6 +631,16 @@ caf::message_handler Module::message_handler() { } }, + [=](remove_attribute_atom, const utility::Uuid &uuid) -> result { + try { + remove_attribute(uuid); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + return true; + }, + + [=](attribute_uuids_atom) -> std::vector { std::vector rt; for (auto &attr : attributes_) { @@ -614,6 +685,12 @@ caf::message_handler Module::message_handler() { link_to_module(linkwith, all_attrs, both_ways, intial_push_sync); }, + [=](link_module_atom, caf::actor linkwith, bool unlink) { + if (unlink) { + unlink_module(linkwith); + } + }, + [=](connect_to_ui_atom) { connect_to_ui(); }, [=](disconnect_from_ui_atom) { disconnect_from_ui(); }, @@ -627,7 +704,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 (!pointer_event(e)) { + if (connected_viewports_.find(e.context()) != connected_viewports_.end() && + !pointer_event(e)) { // pointer event was not used } }, @@ -701,7 +779,8 @@ caf::message_handler Module::message_handler() { activated, context); - if (activated && connected_to_ui_) + if (activated && connected_to_ui_ && + connected_viewports_.find(context) != connected_viewports_.end()) hotkey_pressed(uuid, context); else hotkey_released(uuid, context); @@ -735,10 +814,13 @@ caf::message_handler Module::message_handler() { attrs_waiting_to_update_prefs_.clear(); }, [=](module::current_viewport_playhead_atom, caf::actor_addr) {}, - [=](utility::name_atom) -> std::string { return name(); }, - [=](utility::event_atom, playhead::show_atom, const media_reader::ImageBufPtr &buf) { - on_screen_image(buf); + [=](ui::viewport::connect_to_viewport_toolbar_atom, + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect) { + connect_to_viewport(viewport_name, viewport_toolbar_name, connect); }, + [=](utility::name_atom) -> std::string { return name(); }, [=](utility::event_atom, playhead::show_atom, caf::actor media, @@ -750,7 +832,32 @@ caf::message_handler Module::message_handler() { playhead::show_atom_v, media, media_source); - } + }, + [=](utility::event_atom, + xstudio::ui::model_data::set_node_data_atom, + const std::string &model_name, + const std::string &path, + const utility::JsonStore &data) {}, + [=](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) { + try { + Attribute *attr = get_attribute(uuid_role_data); + if (attr) { + attr->set_role_data(Attribute::role_index(role), data); + } + } 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) {} }); return h.or_else(playhead::PlayheadGlobalEventsActor::default_event_handler()); @@ -783,23 +890,109 @@ void Module::notify_change( role, value); + if (attr->has_role_data(Attribute::Groups)) { + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + auto groups = attr->get_role_data>(Attribute::Groups); + for (const auto &group_name : groups) { + anon_send( + central_models_data_actor, + ui::model_data::set_node_data_atom_v, + group_name, + attr->uuid(), + Attribute::role_name(role), + value, + self()); + } + } attribute_changed(attr_uuid, role, self_notify); } + if (role == Attribute::PreferencePath) { + + // looks like the preference path is being set on the attribute. Note + // we might get here before ser_parent_actor_addr' has been called so + // we don't have 'self()' which is why I use the ActorSystemSingleton + // to get to the caf system to get a GlobalStoreHelper + auto prefs = global_store::GlobalStoreHelper( + xstudio::utility::ActorSystemSingleton::actor_system_ref()); + + std::string pref_path; + try { + pref_path = attr->get_role_data(Attribute::PreferencePath); + attr->set_role_data( + Attribute::Value, + prefs.get_existing_or_create_new_preference( + pref_path, attr->role_data_as_json(Attribute::Value), true, false)); + + } catch (std::exception &e) { + + spdlog::warn("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); + } + } + + // if an attr has a PreferencePath this means its value will be stored and + // retrieved from the preferences system so the attribute value persists + // between sessions. So if you set Volume to level 8, next time you start + // xSTUDIO it is already at 8 for example. if (attr && attr->has_role_data(Attribute::PreferencePath) && self()) { if (!attrs_waiting_to_update_prefs_.size()) { - // if we haven't already queued up attrs to update in the prefs, - // order an update for 10 seconds time + + // In order to prevent rapid granular attr updates spamming the + // preference store when a user grabs a slider (like volume adjust, say) + // and interacts with it, we make a list of attrs that have changed + // and then do a periodic update to push the value to the prefs. + + // 'delayed_anon_send' is causing big problems with and Modules that + // have a parent actor that lives in the Qt layer (Viewport, for + // 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);*/ + + // To get around this problem we can do this shenannegans instead... + auto resender = self()->home_system().spawn(delayed_resend); delayed_anon_send( - self(), std::chrono::seconds(2), update_attribute_in_preferences_atom_v); + resender, + std::chrono::seconds(2), + update_attribute_in_preferences_atom_v, + parent_actor_addr_); } + attrs_waiting_to_update_prefs_.insert(attr_uuid); } } void Module::attribute_changed(const utility::Uuid &attr_uuid, const int role_id, bool notify) { + + module::Attribute *attr = get_attribute(attr_uuid); + + if (role_id == Attribute::Groups && 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); + + for (const auto &group_name : groups) { + anon_send( + central_models_data_actor, + ui::model_data::register_model_data_atom_v, + group_name, + utility::JsonStore(attr->as_json()), + attr_uuid, + Attribute::role_name(Attribute::ToolbarPosition), + self()); + } + } + // This is where the 'linking' mechanism is enacted. We send a change_attribute // message to linked modules. if (linking_disabled_ || role_id != Attribute::Value) { @@ -972,7 +1165,9 @@ void Module::listen_to_playhead_events(const bool listen) { void Module::connect_to_ui() { // if necessary, get the global module events actor and the associated events groups - if (!global_module_events_actor_ && self()) { + if ((!global_module_events_actor_ || !keypress_monitor_actor_ || + !keyboard_and_mouse_group_) && + self()) { try { @@ -996,28 +1191,35 @@ void Module::connect_to_ui() { } } - // Now join the events groups to 'connect' to events coming from the UI. - // (got to be a better way of doing this than these casts!?) - 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_); + try { + + // Now join the events groups to 'connect' to events coming from the UI. + // (got to be a better way of doing this than these casts!?) + auto a = caf::actor_cast(self()); + if (a) { + join_broadcast(a, keyboard_and_mouse_group_); + join_broadcast(a, ui_attribute_events_group_); } else { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - "Unable to cast parent actor for hotkey registration"); + 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( + "{} {}", + __PRETTY_FUNCTION__, + "Unable to cast parent actor for hotkey registration"); + } } + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } if (!connected_to_ui_) { connected_to_ui_ = true; connected_to_ui_changed(); + } else { + return; } register_hotkeys(); @@ -1025,9 +1227,38 @@ void Module::connect_to_ui() { 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 + // UI. When we update an attribute we notify the middleman about the change, + // and this is forwarded to the UI. Likewise, if the UI changes an attribute + // a message is sent to the middleman which is passed back to Module here so + // we update the actual attribute. + + 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, + group_name, + utility::JsonStore(a->as_json()), + a->uuid(), + Attribute::role_name(Attribute::ToolbarPosition), + self()); + } + } + } } void Module::disconnect_from_ui() { + if (!connected_to_ui_) return; @@ -1048,6 +1279,27 @@ void Module::disconnect_from_ui() { "Unable to cast parent actor for hotkey registration"); } } + + // 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, + group_name, + a->uuid(), + self()); + } + } + } } if (global_module_events_actor_) { @@ -1113,7 +1365,7 @@ void Module::update_attrs_from_preferences(const utility::JsonStore &entire_pref auto pref_value = global_store::preference_value( entire_prefs_dict, pref_path); - attr->set_role_data(Attribute::Value, pref_value, false); + attr->set_role_data(Attribute::Value, pref_value, true); } catch (std::exception &e) { spdlog::warn("{} failed to set preference {}", __PRETTY_FUNCTION__, e.what()); @@ -1166,11 +1418,72 @@ void Module::add_boolean_attr_to_menu( attr->set_role_data(module::Attribute::MenuPaths, nlohmann::json(menu_paths)); } +void Module::make_attribute_visible_in_viewport_toolbar( + Attribute *attr, const bool make_visible) { + if (make_visible) { + + attrs_in_toolbar_.insert(attr->uuid()); + + if (!self()) + return; + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + if (central_models_data_actor) { + + for (const auto &viewport_name : connected_viewports_) { + + std::string toolbar_name = viewport_name + "_toolbar"; + attr->expose_in_ui_attrs_group(toolbar_name, true); + + anon_send( + central_models_data_actor, + ui::model_data::register_model_data_atom_v, + toolbar_name, + utility::JsonStore(attr->as_json()), + attr->uuid(), + Attribute::role_name(Attribute::ToolbarPosition), + self()); + } + } + + } else { + + if (attrs_in_toolbar_.find(attr->uuid()) != attrs_in_toolbar_.end()) { + attrs_in_toolbar_.erase(attrs_in_toolbar_.find(attr->uuid())); + } + + if (!self()) + return; + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + if (central_models_data_actor) { + for (const auto &viewport_name : connected_viewports_) { + + 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, + toolbar_name, + attr->uuid(), + caf::actor()); + } + } + } +} void Module::redraw_viewport() { anon_send(module_events_group_, playhead::redraw_viewport_atom_v); } + Attribute *Module::add_attribute( const std::string &title, const utility::JsonStore &value, @@ -1229,8 +1542,12 @@ Attribute *Module::add_attribute( attr = static_cast( add_string_attribute(title, title, value.get())); - } else { + } else if (value.is_object() || value.is_null()) { + + attr = static_cast(add_json_attribute(title, nlohmann::json("{}"))); + attr->set_role_data(Attribute::Value, value); + } else { throw std::runtime_error("Unrecognised attribute value type"); } @@ -1244,3 +1561,68 @@ Attribute *Module::add_attribute( return attr; } + +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); + 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, + ui::model_data::register_model_data_atom_v, + model_name, + utility::JsonStore(attr->as_json()), + attr->uuid(), + Attribute::role_name(Attribute::ToolbarPosition), + self()); + } 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()); + } + } catch (std::exception &) { + } +} + +void Module::connect_to_viewport( + const std::string &viewport_name, const std::string &viewport_toolbar_name, bool connect) { + + 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)); + } + + for (const auto &toolbar_attr_id : attrs_in_toolbar_) { + Attribute *attr = get_attribute(toolbar_attr_id); + if (attr) { + expose_attribute_in_model_data(attr, viewport_toolbar_name, connect); + } + } +} + +void Module::add_attribute(Attribute *attr) { attributes_.emplace_back(attr); } + +utility::JsonStore Module::public_state_data() { + + // This is not called. Maybe we need live JsonTree data for the whole + // session at the backend to simplify frontend stuff for exposing data. + utility::JsonStore data; + data["name"] = name(); + data["children"] = nlohmann::json::array(); + for (auto &attr : attributes_) { + + data["children"].push_back(attr->as_json()); + } + // std::cerr << data.dump(2) << "\n"; + return data; +} diff --git a/src/playhead/src/edit_list_actor.cpp b/src/playhead/src/edit_list_actor.cpp index ac0f5292d..3196ab23c 100644 --- a/src/playhead/src/edit_list_actor.cpp +++ b/src/playhead/src/edit_list_actor.cpp @@ -277,6 +277,10 @@ EditListActor::EditListActor( } }, + [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { + // ignoring timeline events + }, + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }); } diff --git a/src/playhead/src/playhead.cpp b/src/playhead/src/playhead.cpp index feb5ef6ac..d34a722c2 100644 --- a/src/playhead/src/playhead.cpp +++ b/src/playhead/src/playhead.cpp @@ -14,9 +14,8 @@ using namespace xstudio::playhead; using namespace xstudio::utility; PlayheadBase::PlayheadBase(const std::string &name, const utility::Uuid uuid) - : Container(name, "PlayheadBase", std::move(uuid)), - Module(name), - + : Container(name, "PlayheadBase", uuid), + Module(name, uuid), playhead_rate_(timebase::k_flicks_24fps), position_(0), loop_start_(timebase::k_flicks_low), @@ -29,20 +28,18 @@ PlayheadBase::PlayheadBase(const std::string &name, const utility::Uuid uuid) PlayheadBase::PlayheadBase(const JsonStore &jsn) : Container(static_cast(jsn["container"])), Module("PlayheadBase"), - loop_(jsn["loop"]), 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"]) -// use_loop_range_(false) // use_loop_range_(jsn["use_loop_range"]) - forcing looprange off on // load, unwanted behaviour { - add_attributes(); if (jsn.find("module") != jsn.end()) { Module::deserialise(jsn["module"]); } + set_loop(jsn["loop"]); } void PlayheadBase::add_attributes() { @@ -63,7 +60,6 @@ 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); @@ -94,22 +90,8 @@ 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{"any_toolbar", "playhead"}); - velocity_->set_role_data( - module::Attribute::Groups, nlohmann::json{"any_toolbar", "playhead"}); - - source_ = add_qml_code_attribute( - "Src", - R"( - import xStudio 1.0 - XsSourceToolbarButton { - anchors.fill: parent - } - )"); - - source_->set_role_data( - module::Attribute::Groups, nlohmann::json{"any_toolbar", "playhead"}); + 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"}); @@ -118,12 +100,12 @@ void PlayheadBase::add_attributes() { 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"}); velocity_->set_role_data(module::Attribute::ToolbarPosition, 3.0f); compare_mode_->set_role_data(module::Attribute::ToolbarPosition, 9.0f); - source_->set_role_data(module::Attribute::ToolbarPosition, 12.0f); velocity_->set_role_data(module::Attribute::DefaultValue, 1.0f); @@ -156,6 +138,28 @@ void PlayheadBase::add_attributes() { 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_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", ""); + 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); + + // this attr tracks the global 'Audio Delay Millisecs' preference + audio_delay_millisecs_ = + add_integer_attribute("Audio Delay Millisecs", "Audio Delay Millisecs", 0, -1000, 1000); + audio_delay_millisecs_->set_role_data( + module::Attribute::PreferencePath, "/core/audio/audio_latency_millisecs"); } @@ -164,11 +168,11 @@ JsonStore PlayheadBase::serialise() const { jsn["container"] = Container::serialise(); jsn["position"] = position_.count(); - jsn["loop"] = loop_; + 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["use_loop_range"] = use_loop_range(); jsn["module"] = Module::serialise(); return jsn; @@ -188,13 +192,13 @@ PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { set_position(position_ - delta); } - const timebase::flicks in = use_loop_range_ and loop_start_ != timebase::k_flicks_low + const timebase::flicks in = use_loop_range() and loop_start_ != timebase::k_flicks_low ? loop_start_ : timebase::flicks(0); const timebase::flicks out = - use_loop_range_ and loop_end_ != timebase::k_flicks_max ? loop_end_ : duration_; + use_loop_range() and loop_end_ != timebase::k_flicks_max ? loop_end_ : duration_; - if (loop_ == LM_LOOP) { + if (loop() == LM_LOOP) { if (forward()) { if (position_ > out || position_ < in) { @@ -206,7 +210,7 @@ PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { } } - } else if (loop_ == LM_PING_PONG) { + } else if (loop() == LM_PING_PONG) { if (forward()) { if (position_ > out) { @@ -245,19 +249,152 @@ PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { return {}; } +timebase::flicks PlayheadBase::adjusted_position() const { + if (!playing()) + return position_; + + const timebase::flicks delta = std::chrono::duration_cast( + std::chrono::milliseconds(audio_delay_millisecs_->value())); + + const timebase::flicks in = use_loop_range() and loop_start_ != timebase::k_flicks_low + ? loop_start_ + : timebase::flicks(0); + const timebase::flicks out = + use_loop_range() and loop_end_ != timebase::k_flicks_max ? loop_end_ : duration_; + + // 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? + if (forward() && (position_ + delta) > out) { + + auto remainder = (position_ + delta) - out; + if (loop() == LM_LOOP) { + + while (remainder > (out - in)) { + remainder -= (out - in); + } + return in + remainder; + + } else if (loop() == LM_PING_PONG) { + + bool fwd = 0; + while (remainder > (out - in)) { + remainder -= (out - in); + fwd = !fwd; + } + if (fwd) { + return in + remainder; + } else { + return out - remainder; + } + + } else { + return out; + } + + } else if (forward() && (position_ + delta) < in) { + + auto remainder = in - (position_ + delta); + if (loop() == LM_LOOP) { + + while (remainder > (out - in)) { + remainder -= (out - in); + } + return out - remainder; + + } else if (loop() == LM_PING_PONG) { + + bool fwd = 0; + while (remainder > (out - in)) { + remainder -= (out - in); + fwd = !fwd; + } + if (fwd) { + return in + remainder; + } else { + return out - remainder; + } + + } else { + return out; + } + + } else if (!forward() && (position_ - delta) < in) { + + auto remainder = in - (position_ - delta); + if (loop() == LM_LOOP) { + + while (remainder > (out - in)) { + remainder -= (out - in); + } + return out - remainder; + + } else if (loop() == LM_PING_PONG) { + + bool fwd = true; + while (remainder > (out - in)) { + remainder -= (out - in); + fwd = !fwd; + } + if (fwd) { + return in + remainder; + } else { + return out - remainder; + } + + } else { + return out; + } + + } else if (!forward() && (position_ + delta) > out) { + + auto remainder = (position_ + delta) - out; + if (loop() == LM_LOOP) { + + while (remainder > (out - in)) { + remainder -= (out - in); + } + return in + remainder; + + } else if (loop() == LM_PING_PONG) { + + bool fwd = false; + while (remainder > (out - in)) { + remainder -= (out - in); + fwd = !fwd; + } + if (fwd) { + return in + remainder; + } else { + return out - remainder; + } + + } else { + return out; + } + + } else if (!forward()) { + return position_ - delta; + } + + return position_ + delta; +} + void PlayheadBase::set_playing(const bool play) { if (play != playing()) { // in play once mode, if the user wants to play again we set the // position back to the start to play through again - if (play && loop_ == LM_PLAY_ONCE) { + if (play && loop() == LM_PLAY_ONCE) { const timebase::flicks in = - use_loop_range_ and loop_start_ != timebase::k_flicks_low ? loop_start_ - : timebase::flicks(0); + use_loop_range() and loop_start_ != timebase::k_flicks_low + ? loop_start_ + : timebase::flicks(0); const timebase::flicks out = - use_loop_range_ and loop_end_ != timebase::k_flicks_max ? loop_end_ : duration_; + use_loop_range() and loop_end_ != timebase::k_flicks_max ? loop_end_ + : duration_; if (forward()) { if (position_ == out) @@ -286,7 +423,7 @@ timebase::flicks PlayheadBase::clamp_timepoint_to_loop_range(const timebase::fli const timebase::flicks out = loop_end(); auto rt = pos; - if (loop_ == LM_LOOP) { + if (loop() == LM_LOOP) { if (forward()) { if (pos > out || pos < in) { @@ -298,7 +435,7 @@ timebase::flicks PlayheadBase::clamp_timepoint_to_loop_range(const timebase::fli } } - } else if (loop_ == LM_PING_PONG) { + } else if (loop() == LM_PING_PONG) { if (forward()) { if (pos > out) { @@ -335,8 +472,8 @@ void PlayheadBase::set_position(const timebase::flicks p) { position_ = p; } bool PlayheadBase::set_use_loop_range(const bool use_loop_range) { bool position_changed = false; - if (use_loop_range_ != use_loop_range) { - use_loop_range_ = use_loop_range; + if (this->use_loop_range() != use_loop_range) { + do_looping_->set_value(use_loop_range); if (use_loop_range) { if (position_ < loop_start_) { set_position(loop_start_); @@ -358,7 +495,7 @@ bool PlayheadBase::set_loop_start(const timebase::flicks loop_start) { position_changed = true; } - if (use_loop_range_ && position_ < loop_start_) { + if (use_loop_range() && position_ < loop_start_) { set_position(loop_start_); position_changed = true; } @@ -374,7 +511,7 @@ bool PlayheadBase::set_loop_end(const timebase::flicks loop_end) { position_changed = true; } - if (use_loop_range_ && position_ > loop_end_) { + if (use_loop_range() && position_ > loop_end_) { set_position(loop_end_); position_changed = true; } @@ -497,7 +634,7 @@ 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) { if (hotkey_uuid == play_hotkey_) { forward_->set_value(true); @@ -515,4 +652,22 @@ void PlayheadBase::hotkey_pressed( } } -void PlayheadBase::set_duration(const timebase::flicks duration) { duration_ = duration; } \ No newline at end of file +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) { + + // this playhead needs to be connected (exposed) in a given toolbar + // attributes group, so that the compare, source and velocity attrs + // are visible in a particular viewport toolbar + // .. or, disconnected + expose_attribute_in_model_data( + image_source_, viewport_toolbar_name + "_image_source", connect); + 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); +} diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index 32cfbc83f..e37fccbd4 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -79,6 +79,20 @@ PlayheadActor::PlayheadActor( init(); set_parent_actor_addr(actor_cast(this)); connect_to_playlist_selection_actor(playlist_selection); + + // 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 + // the uuid of a module at the frontend we can get to any and all of its + // attribute data if/when we need to. For example, this is how we get to + // the Playhead attribute data in the frontend qml code ... the Playhead + // Uuid is published by the parent playlist/subset/timeline in the main + // SessionModel - we use this to connect to the model data of a given + // Playhead so we can talk to the Playhead of the 'current' timeline, subset, + // or playlist + for (auto &attr : attributes_) { + expose_attribute_in_model_data( + attr.get(), std::string("{") + to_string(Module::uuid()) + std::string("}"), true); + } } PlayheadActor::PlayheadActor( @@ -91,6 +105,12 @@ PlayheadActor::PlayheadActor( 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); + } } void PlayheadActor::init() { @@ -201,10 +221,6 @@ void PlayheadActor::init() { make_get_event_group_handler(event_group_), make_get_detail_handler(this, event_group_), - [=](utility::event_atom, - bookmark::bookmark_change_atom, - const utility::Uuid &bookmark_uuid) { rebuild_bookmark_frames_ranges(); }, - [=](actual_playback_rate_atom atom) { delegate(key_playhead_, atom); }, [=](clear_precache_requests_atom) -> result { @@ -333,7 +349,7 @@ void PlayheadActor::init() { [=](logical_frame_atom atom) { delegate(key_playhead_, atom); }, - [=](loop_atom) -> LoopMode { return loop(); }, + [=](loop_atom) -> LoopMode { return playhead::LoopMode(loop()); }, [=](loop_atom, const LoopMode loop) -> unit_t { set_loop(loop); @@ -352,7 +368,6 @@ void PlayheadActor::init() { // the cached frames display might need updating rebuild_cached_frames_status(); - rebuild_bookmark_frames_ranges(); } }, @@ -389,6 +404,26 @@ void PlayheadActor::init() { bookmark_frames_ranges_); }, + [=](utility::event_atom, + bookmark::get_bookmarks_atom, + const std::vector> + &bookmark_ranges) { + if (caf::actor_cast(current_sender()) == key_playhead_) { + bookmark_frames_ranges_ = bookmark_ranges; + send( + event_group_, + utility::event_atom_v, + bookmark::get_bookmarks_atom_v, + bookmark_frames_ranges_); + + send( + playhead_media_events_group_, + utility::event_atom_v, + bookmark::get_bookmarks_atom_v, + bookmark_frames_ranges_); + } + }, + [=](media_cache::keys_atom atom) { delegate(key_playhead_, atom); }, [=](play_atom) -> bool { return playing(); }, @@ -491,7 +526,9 @@ void PlayheadActor::init() { return clamped_estimated_playhead_position; }, - [=](media_logical_frame_atom) -> int { return media_logical_frame_; }, + [=](media_logical_frame_atom) -> int { return playhead_media_logical_frame_->value(); }, + + [=](media_frame_atom) -> int { return playhead_media_frame_->value(); }, [=](position_atom, actor child, @@ -505,7 +542,11 @@ void PlayheadActor::init() { // logical frame has changed if (child == key_playhead_) { - media_logical_frame_ = media_logical_frame; + playhead_logical_frame_->set_value(logical_frame, false); + playhead_media_logical_frame_->set_value(media_logical_frame, false); + current_source_frame_timecode_->set_value(to_string(tc), false); + playhead_media_frame_->set_value(media_frame, false); + send( playhead_media_events_group_, utility::event_atom_v, @@ -778,7 +819,6 @@ void PlayheadActor::init() { if (key_playhead) { rebuild_cached_frames_status(); - rebuild_bookmark_frames_ranges(); // this will trigger an update to the duration anon_send(this, duration_flicks_atom_v); @@ -810,6 +850,10 @@ void PlayheadActor::init() { } }, + [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &, bool) { + // timeline change event ... ignore as its taken care of by sub playhead + }, + [=](utility::event_atom, media_source_atom, caf::actor media_source_actor, @@ -819,12 +863,13 @@ void PlayheadActor::init() { const int /*media_frame*/) { if (sub_playhead == key_playhead_) { - if ((media_uuid != current_media_uuid_ or - source_uuid != current_source_uuid_) and + if ((to_string(media_uuid) != current_media_uuid_->value() or + to_string(source_uuid) != current_media_source_uuid_->value()) and media_source_actor) { - current_media_uuid_ = media_uuid; - previous_source_uuid_ = current_source_uuid_; - current_source_uuid_ = source_uuid; + 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) { @@ -872,16 +917,6 @@ void PlayheadActor::init() { new_source_list(source_list); }, - [=](ui::viewport::viewport_playhead_atom) { - auto main_vp = system().registry().template get(main_viewport_registry); - if (main_vp) { - anon_send( - main_vp, - ui::viewport::viewport_playhead_atom_v, - caf::actor_cast(this)); - } - }, - [=](source_atom, const std::vector &source_list) -> result { auto rp = make_response_promise(); @@ -912,7 +947,6 @@ void PlayheadActor::init() { [=](bookmark::get_bookmark_atom) -> std::vector> { - rebuild_bookmark_frames_ranges(); return bookmark_frames_ranges_; }, @@ -950,25 +984,15 @@ void PlayheadActor::init() { // controls creation and destruction of children [&](utility::event_atom, utility::change_atom) { - if (current_sender() == this) { - rebuild(); - } else { - - auto sender = caf::actor_cast(current_sender()); - if (sender) { - if (std::find(source_actors_.begin(), source_actors_.end(), sender) != - source_actors_.end()) { - // one of the sources has changed - we will do a rebuild in case its - // duration or timing has changed - rebuild(); - } - } - + if (current_sender() != this) { // change has bubbled up from a child playhead, force a redraw - current_media_uuid_ = utility::Uuid(); - current_source_uuid_ = utility::Uuid(); + 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); + } else { + send(this, jump_atom_v); } }, @@ -1037,8 +1061,11 @@ void PlayheadActor::connect_to_playlist_selection_actor(caf::actor playlist_sele infinite, playhead::get_selected_sources_atom_v) .then( - [=](const std::vector &selection) { - new_source_list(selection); + [=](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( @@ -1130,20 +1157,22 @@ void PlayheadActor::make_audio_child_playhead(const int source_index) { if (audio_playhead_) { unlink_from(audio_playhead_); send_exit(audio_playhead_, caf::exit_reason::user_shutdown); - unlink_from(audio_playhead_retimer_); + 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_STRING + 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_ ? audio_playhead_retimer_ : source_actors_[source_index], actor_cast(this), loop_start(), loop_end(), @@ -1152,7 +1181,8 @@ void PlayheadActor::make_audio_child_playhead(const int source_index) { media::MediaType::MT_AUDIO); link_to(audio_playhead_); - link_to(audio_playhead_retimer_); + if (audio_playhead_retimer_) + link_to(audio_playhead_retimer_); auto ap = audio_playhead_; request(audio_playhead_, infinite, get_event_group_atom_v) @@ -1172,6 +1202,9 @@ void PlayheadActor::make_audio_child_playhead(const int source_index) { void PlayheadActor::new_source_list(const std::vector &sl) { + 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) @@ -1189,34 +1222,7 @@ void PlayheadActor::new_source_list(const std::vector &sl) { // reset the loop range as we have new sources set_loop_start(timebase::k_flicks_low); set_loop_end(timebase::k_flicks_max); - - // here we join the event group of the new sources - we can look out for - // changes in the sources and 'do the needful' - if (source_actors_.size()) { - fan_out_request( - source_actors_, infinite, utility::get_event_group_atom_v) - .then( - [=](std::vector event_groups) mutable { - if (event_groups.size()) { - fan_out_request( - event_groups, infinite, broadcast::join_broadcast_atom_v) - .then( - [=](std::vector) { rebuild(); }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rebuild(); - }); - } else { - rebuild(); - } - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rebuild(); - }); - } else { - rebuild(); - } + rebuild(); } void PlayheadActor::rebuild() { @@ -1233,6 +1239,29 @@ void PlayheadActor::rebuild() { anon_send(this, show_atom_v, key_playhead_uuid_, ImageBufPtr(), true); + } else if (source_actors_.size() == 1 || compare_mode() == CM_OFF) { + + int count = 1; + for (auto source : source_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()) { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + "Trying to compare too many things, limiting to first ", + max_compare_sources_->value()); + break; + } + } + // 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) { auto foo = spawn("EditListActor", source_actors_, media::MT_IMAGE); @@ -1294,8 +1323,8 @@ void PlayheadActor::switch_key_playhead(int idx) { // got all the data it needs from its source request_receive(*sys, ph, source_atom_v); - if (request_receive(*sys, ph, media_source_atom_v, true) == - current_media_uuid_) { + if (to_string(request_receive( + *sys, ph, media_source_atom_v, true)) == current_media_uuid_->value()) { idx = i; break; } @@ -1314,9 +1343,20 @@ void PlayheadActor::switch_key_playhead(int idx) { if (idx >= 0 && idx < (int)sub_playheads_.size()) { key_playhead_ = sub_playheads_[idx]; + anon_send(key_playhead_, bookmark::get_bookmarks_atom_v); try { + // 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 '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); + auto source_actor = request_receive(*sys, key_playhead_, source_atom_v); make_audio_child_playhead(idx); @@ -1326,16 +1366,6 @@ void PlayheadActor::switch_key_playhead(int idx) { if (media_actor) current_media_changed(media_actor); - // 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(); - - // 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; - send(broadcast_, key_child_playhead_atom_v, uuid, switchpoint); - // 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); @@ -1344,7 +1374,8 @@ void PlayheadActor::switch_key_playhead(int idx) { // '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(current_media_uuid_); + 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(); } else { @@ -1361,7 +1392,6 @@ void PlayheadActor::switch_key_playhead(int idx) { notify_offset_changed(); update_playback_rate(); rebuild_cached_frames_status(); - rebuild_bookmark_frames_ranges(); restart_readahead_cacheing(compare_mode() != CM_OFF); }, [=](const caf::error &err) { @@ -1391,7 +1421,7 @@ void PlayheadActor::update_child_playhead_positions( anon_send( audio_playhead_, jump_atom_v, - position(), + adjusted_position(), forward(), velocity(), playing(), @@ -1450,6 +1480,7 @@ void PlayheadActor::notify_loop_end_changed() { .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); }, [=](const error &err) { @@ -1472,6 +1503,7 @@ void PlayheadActor::notify_loop_start_changed() { .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); }, [=](const error &err) { @@ -1529,7 +1561,8 @@ void PlayheadActor::update_duration(caf::typed_response_promiseset_value(duration); send(event_group_, utility::event_atom_v, duration_frames_atom_v, duration); }, [=](const error &err) { @@ -1613,7 +1646,6 @@ void PlayheadActor::update_playback_rate() { [=](const utility::FrameRate &rate) { if (rate != playhead_rate()) { set_playhead_rate(rate); - send(event_group_, utility::event_atom_v, playhead_rate_atom_v, rate); } send( fps_moniotor_group_, @@ -1621,62 +1653,17 @@ void PlayheadActor::update_playback_rate() { actual_playback_rate_atom_v, rate); }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } -} - -void PlayheadActor::rebuild_bookmark_frames_ranges() { - - try { - scoped_actor sys{system()}; - - auto global = system().registry().template get(global_registry); - - auto session = - utility::request_receive(*sys, global, session::session_atom_v); - auto bookmark = - utility::request_receive(*sys, session, bookmark::get_bookmark_atom_v); - - auto details = request_receive>( - *sys, bookmark, bookmark::bookmark_detail_atom_v, UuidVector()); - - auto ph = key_playhead_; - request(ph, infinite, bookmark::get_bookmarks_atom_v, details) - .then( - [=](const std::vector> - &bookmarked) { - // note we check the key playhead hasn't changed since this request was - // made! - if (bookmark_frames_ranges_ != bookmarked && ph == key_playhead_) { - bookmark_frames_ranges_ = bookmarked; - send( - event_group_, - utility::event_atom_v, - bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); - } - - // this group is subscribed to by the annotations tool, so we send - // a message in case annotations data has been updated since this - // message triggers a rebuild of the annotations data in the plugin + [=](const error &) { + // no media, fallback to 'default' playback rate send( - playhead_media_events_group_, + event_group_, utility::event_atom_v, - bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + playhead_rate_atom_v, + playhead_rate()); }); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } - void PlayheadActor::update_cached_frames_status( const media::MediaKeyVector &new_keys, const media::MediaKeyVector &remove_keys) { @@ -1940,9 +1927,7 @@ 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()) { - // send a change event to self - this will kick a rebuild of the timeline/child - // playheads including apply (or not apply) the auto alignment - send(this, utility::event_atom_v, utility::change_atom_v); + rebuild(); } else if (attr_uuid == velocity_->uuid()) { send(fps_moniotor_group_, utility::event_atom_v, velocity_atom_v, velocity()); } else if (attr_uuid == velocity_multiplier_->uuid()) { @@ -1982,6 +1967,11 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int send(fps_moniotor_group_, utility::event_atom_v, play_atom_v, playing()); 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()); } else { PlayheadBase::attribute_changed(attr_uuid, role); } @@ -2009,7 +1999,7 @@ void PlayheadActor::connected_to_ui_changed() { utility::event_atom_v, media_source_atom_v, media_actor, - current_source_uuid_); + utility::Uuid(current_media_source_uuid_->value())); }, [=](caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -2141,6 +2131,9 @@ void PlayheadActor::restart_readahead_cacheing( void PlayheadActor::switch_media_source( const std::string new_source_name, const media::MediaType mt) { + if (!key_playhead_) + return; + // 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 @@ -2167,12 +2160,14 @@ void PlayheadActor::switch_media_source( } }, [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + if (to_string(err) != "No frames") + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } }, [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + if (to_string(err) != "No frames") + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } @@ -2195,7 +2190,7 @@ void PlayheadActor::check_if_loop_range_makes_sense() { bool PlayheadActor::has_selection_changed() { if (source_actors_.size() == 1) { - return previous_source_uuid_ != current_source_uuid_; + return to_string(previous_source_uuid_) != current_media_source_uuid_->value(); } return static_cast(source_actors_.size()) != previous_selected_sources_count_; diff --git a/src/playhead/src/playhead_global_events_actor.cpp b/src/playhead/src/playhead_global_events_actor.cpp index 197cd89c5..955cbf4b2 100644 --- a/src/playhead/src/playhead_global_events_actor.cpp +++ b/src/playhead/src/playhead_global_events_actor.cpp @@ -62,7 +62,12 @@ void PlayheadGlobalEventsActor::init() { 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); + /*send( + event_group_, + utility::event_atom_v, + show_atom_v, + buf, + "viewport0");*/ }, [=](caf::error &) {}); } @@ -78,20 +83,56 @@ void PlayheadGlobalEventsActor::init() { 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 &) {}); + } monitor(playhead); } }, [=](show_atom, const media_reader::ImageBufPtr &buf) { - if (caf::actor_cast(current_sender()) == on_screen_playhead_) { - send(event_group_, utility::event_atom_v, show_atom_v, 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"); + }*/ }, [=](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); } }, - [=](ui::viewport::viewport_playhead_atom) -> caf::actor { - return on_screen_playhead_; + [=](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); + }, + [=](ui::viewport::viewport_atom, + const std::string viewport_name) -> result { + caf::actor r; + auto p = viewports_.find(viewport_name); + if (p != viewports_.end()) { + r = caf::actor_cast(p->second); + } + if (!r) + return make_error( + xstudio_error::error, fmt::format("No viewport named {}", viewport_name)); + return r; }); } \ 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 8b0b57ecc..e1deee3f5 100644 --- a/src/playhead/src/playhead_selection_actor.cpp +++ b/src/playhead/src/playhead_selection_actor.cpp @@ -214,12 +214,12 @@ void PlayheadSelectionActor::init() { return result(jsn); }, - [=](get_selected_sources_atom) -> std::vector { - std::vector result; + [=](get_selected_sources_atom) -> utility::UuidActorVector { + utility::UuidActorVector r; for (const auto &p : base_.items()) { - result.push_back(source_actors_[p]); + r.emplace_back(p, source_actors_[p]); } - return result; + return r; }, [=](utility::event_atom, playlist::move_media_atom, const UuidVector &, const Uuid &) { }, diff --git a/src/playhead/src/retime_actor.cpp b/src/playhead/src/retime_actor.cpp index 4265fc850..9821745f4 100644 --- a/src/playhead/src/retime_actor.cpp +++ b/src/playhead/src/retime_actor.cpp @@ -243,6 +243,11 @@ RetimeActor::RetimeActor( 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_; }); } diff --git a/src/playhead/src/sub_playhead.cpp b/src/playhead/src/sub_playhead.cpp index a4a60bd2e..73d7d7f38 100644 --- a/src/playhead/src/sub_playhead.cpp +++ b/src/playhead/src/sub_playhead.cpp @@ -53,7 +53,7 @@ void SubPlayhead::init() { // get global reader and steal mrm.. spdlog::debug("Created SubPlayhead {}", base_.name()); - print_on_exit(this, "SubPlayhead"); + // print_on_exit(this, "SubPlayhead"); try { @@ -112,6 +112,16 @@ void SubPlayhead::init() { default_exit_handler(a, m); }); + 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)); + + return message{}; + }); + behavior_.assign( base_.make_set_name_handler(event_group_, this), base_.make_get_name_handler(), @@ -123,7 +133,15 @@ void SubPlayhead::init() { make_get_event_group_handler(event_group_), base_.make_get_detail_handler(this, event_group_), - [=](actual_playback_rate_atom) { delegate(source_, rate_atom_v, logical_frame_); }, + [=](actual_playback_rate_atom) -> result { + auto rp = make_response_promise(); + request( + caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + .then( + [=](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()); @@ -143,6 +161,13 @@ void SubPlayhead::init() { return rp; }, + [=](utility::event_atom, + media::current_media_source_atom, + UuidActor &, + const media::MediaType) { + anon_send(this, source_atom_v); // 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 @@ -159,19 +184,56 @@ void SubPlayhead::init() { return rp; }, - [=](duration_flicks_atom atom) { - delegate(source_, atom, time_source_mode_, override_frame_rate_); + [=](duration_flicks_atom atom) -> result { + if (up_to_date_) { + if (full_timeline_frames_.size() < 2) { + return timebase::flicks(0); + } + return std::chrono::duration_cast( + full_timeline_frames_.rbegin()->first - + full_timeline_frames_.begin()->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) + .then( + [=](caf::actor) mutable { + if (full_timeline_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)); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, - [=](duration_frames_atom atom) { - // spdlog::warn("childplayhead delegate duration_frames_atom {}", - // to_string(source_)); - - delegate(source_, atom, time_source_mode_, override_frame_rate_); + [=](duration_frames_atom atom) -> result { + if (up_to_date_) { + return full_timeline_frames_.size() ? full_timeline_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) + .then( + [=](caf::actor) mutable { + rp.deliver( + full_timeline_frames_.size() ? full_timeline_frames_.size() - 1 + : 0); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, - [=](flicks_to_logical_frame_atom atom, timebase::flicks flicks) { - delegate(source_, atom, flicks, time_source_mode_, override_frame_rate_); + [=](flicks_to_logical_frame_atom atom, timebase::flicks flicks) -> int { + timebase::flicks frame_period, timeline_pts; + std::shared_ptr frame = + get_frame(flicks, frame_period, timeline_pts); + return frame ? frame->playhead_logical_frame_ : 0; }, [=](json_store::update_atom, @@ -310,7 +372,7 @@ void SubPlayhead::init() { } return *(full_timeline_frames_.begin()->second); } - return make_error(xstudio_error::error, "No Frames"); + return make_error(xstudio_error::error, "No frames"); }, [=](last_frame_media_pointer_atom) -> result { @@ -324,16 +386,59 @@ void SubPlayhead::init() { } return *(p->second); } - return make_error(xstudio_error::error, "No Frames"); + return make_error(xstudio_error::error, "No frames"); }, - [=](media_source_atom) -> caf::actor { - auto frame = full_timeline_frames_.lower_bound(position_flicks_); - caf::actor result; - if (frame != full_timeline_frames_.end() && frame->second) { - result = caf::actor_cast(frame->second->actor_addr_); + [=](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()) { + if (frame->second) { + return *(frame->second); + } else { + return make_error(xstudio_error::error, "No Frame"); + } + } else { + return make_error(xstudio_error::error, "No Frame"); + } } - return result; + // 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) + .then( + [=](caf::actor) mutable { + auto frame = full_timeline_frames_.lower_bound(position_flicks_); + if (full_timeline_frames_.size() && + frame != full_timeline_frames_.end()) { + rp.deliver(*(frame->second)); + } else { + rp.deliver(make_error(xstudio_error::error, "No Frame")); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + }, + + [=](media_source_atom) -> result { + // MediaSourceActor at current playhead position + + 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 + // the current frame + request(caf::actor_cast(this), infinite, source_atom_v) + .then( + [=](caf::actor) mutable { + auto frame = full_timeline_frames_.lower_bound(position_flicks_); + caf::actor result; + if (frame != full_timeline_frames_.end() && frame->second) { + result = caf::actor_cast(frame->second->actor_addr_); + } + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, [=](media_source_atom, @@ -344,6 +449,12 @@ void SubPlayhead::init() { request(caf::actor_cast(this), infinite, media_atom_v) .then( [=](caf::actor media_actor) mutable { + // no media ? + if (!media_actor) { + rp.deliver(false); + return; + } + // now get it to switched to the named MediaSource request(media_actor, infinite, media_source_atom_v, source_name, mt) .then( @@ -366,8 +477,26 @@ void SubPlayhead::init() { return rp; }, - [=](media_atom) { // gets the MediaActor from source_ - delegate(source_, media_atom_v, logical_frame_); + [=](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; }, [=](media_source_atom, bool) -> utility::Uuid { @@ -385,6 +514,11 @@ void SubPlayhead::init() { send(parent_, utility::event_atom_v, media::add_media_source_atom_v, uav); }, + [=](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_ + }, + [=](media_cache::keys_atom) -> media::MediaKeyVector { media::MediaKeyVector result; result.reserve(full_timeline_frames_.size()); @@ -396,20 +530,19 @@ void SubPlayhead::init() { return result; }, - [=](bookmark::get_bookmarks_atom, - const std::vector &bookmark_details) - -> std::vector> { - std::vector> r; - get_bookmark_ranges(bookmark_details, r); - return r; + [=](bookmark::get_bookmarks_atom) { + send( + parent_, + utility::event_atom_v, + bookmark::get_bookmarks_atom_v, + bookmark_ranges_); }, [=](buffer_atom) -> result { auto rp = make_response_promise(); - int logical_frame; timebase::flicks frame_period, timeline_pts; std::shared_ptr frame = - get_frame(position_flicks_, logical_frame, frame_period, timeline_pts); + get_frame(position_flicks_, frame_period, timeline_pts); if (!frame) { rp.deliver(ImageBufPtr()); @@ -428,6 +561,7 @@ void SubPlayhead::init() { image_buffer.when_to_display_ = utility::clock::now(); image_buffer.set_timline_timestamp(timeline_pts); image_buffer.set_frame_id(*(frame.get())); + add_annotations_data_to_frame(image_buffer); if (image_buffer) { image_buffer->params()["playhead_frame"] = @@ -450,9 +584,27 @@ void SubPlayhead::init() { const media::AVFrameID &mptr, const time_point &tp) { receive_image_from_cache(image_buffer, mptr, tp); }, - [=](playlist::get_media_uuid_atom atom) { delegate(source_, atom); }, + [=](playlist::get_media_uuid_atom) -> result { + auto rp = make_response_promise(); + request( + caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + .then( + [=](const media::AVFrameID &frameid) mutable { + rp.deliver(frameid.media_uuid_); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, - [=](rate_atom atom) { delegate(source_, atom, logical_frame_); }, + [=](rate_atom) -> result { + auto rp = make_response_promise(); + request( + caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + .then( + [=](const media::AVFrameID &frameid) mutable { rp.deliver(frameid.rate_); }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, [=](simple_loop_end_atom, const timebase::flicks flicks) { loop_out_point_ = flicks; @@ -521,7 +673,18 @@ void SubPlayhead::init() { [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &bookmark_uuid) { - send(parent_, event_atom_v, bookmark::bookmark_change_atom_v, bookmark_uuid); + // this comes from MediaActor, EditListActor or RetimeActor .. we + // just ignore it as we listen to bookmark events coming from the + // main BookmarkManager + }, + + [=](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 { @@ -562,9 +725,32 @@ void SubPlayhead::init() { // otherwise stop any pre cacheing precache_start_frame_ = std::numeric_limits::lowest(); } + }, + [=](utility::event_atom, + 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(); + }, + [=](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()); + } +} // move playhead to position void SubPlayhead::set_position( const timebase::flicks time, @@ -578,16 +764,13 @@ void SubPlayhead::set_position( playing_forwards_ = forwards; playback_velocity_ = velocity; - int logical_frame; timebase::flicks frame_period, timeline_pts; - std::shared_ptr frame = - get_frame(time, logical_frame, frame_period, timeline_pts); - + std::shared_ptr frame = get_frame(time, frame_period, timeline_pts); + int logical_frame = frame ? frame->playhead_logical_frame_ : 0; if (logical_frame_ != logical_frame || force_updates) { - const bool frame_changed = logical_frame_ != logical_frame; - logical_frame_ = logical_frame; + logical_frame_ = logical_frame; auto now = utility::clock::now(); @@ -732,6 +915,7 @@ void SubPlayhead::broadcast_image_frame( 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())); + add_annotations_data_to_frame(image_buffer); if (image_buffer) { image_buffer->params()["playhead_frame"] = @@ -788,7 +972,7 @@ void SubPlayhead::broadcast_audio_frame( const bool /*is_future_frame*/) { media::AVFrameIDsAndTimePoints future_frames; - get_lookahead_frame_pointers(future_frames, 20); + get_lookahead_frame_pointers(future_frames, 50); // now fetch audio samples for playback request( @@ -886,8 +1070,10 @@ void SubPlayhead::request_future_frames() { for (auto &imbuf : image_buffers) { imbuf.set_timline_timestamp(*(tp++)); std::shared_ptr av_idx = (idsp++)->second; - if (av_idx) + if (av_idx) { imbuf.set_frame_id(*(av_idx.get())); + add_annotations_data_to_frame(imbuf); + } } send( parent_, @@ -1002,6 +1188,7 @@ void SubPlayhead::receive_image_from_cache( image_buffer.set_timline_timestamp(position_flicks_); } image_buffer.set_frame_id(mptr); + add_annotations_data_to_frame(image_buffer); send( parent_, @@ -1043,14 +1230,42 @@ void SubPlayhead::get_full_timeline_frame_list(caf::typed_response_promisesecond) { + // 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_) { - timeline_logical_frame_pts_[f.first] = idx++; + // 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(); } set_in_and_out_frames(); + full_bookmarks_update(); + // our data has changed (full_timeline_frames_ describes most) // things that are important about the timeline, so send change // notification @@ -1068,7 +1283,6 @@ void SubPlayhead::get_full_timeline_frame_list(caf::typed_response_promise SubPlayhead::get_frame( const timebase::flicks &time, - int &logical_frame, timebase::flicks &frame_period, timebase::flicks &timeline_pts) { @@ -1086,9 +1300,8 @@ std::shared_ptr SubPlayhead::get_frame( // } if (full_timeline_frames_.size() < 2) { // and give the others values something valid ??? - frame_period = timebase::k_flicks_zero_seconds; - timeline_pts = timebase::k_flicks_zero_seconds; - logical_frame = 0; + frame_period = timebase::k_flicks_zero_seconds; + timeline_pts = timebase::k_flicks_zero_seconds; return std::shared_ptr(); } @@ -1108,12 +1321,6 @@ std::shared_ptr SubPlayhead::get_frame( frame_period = next_frame->first - frame->first; timeline_pts = frame->first; - auto lf = timeline_logical_frame_pts_.find(frame->first); - if (lf != timeline_logical_frame_pts_.end()) { - logical_frame = lf->second; - } else { - logical_frame = std::distance(full_timeline_frames_.begin(), frame); - } return frame->second; } @@ -1196,57 +1403,305 @@ void SubPlayhead::set_in_and_out_frames() { } } -void SubPlayhead::get_bookmark_ranges( - const std::vector &bookmark_details, - std::vector> &result) { - - // This needs some optimisation. At the moment we check the uuid of the media for every - // bookmark against the media uuid of every AVFrameID in 'full_timeline_frames_' - if - // there's a match we then check if the media frame of the AVFrameID is within the frame - // range of the bookmark. This is ok for shorter timelines and small numbers of bookmarks - // but if you have a lot of bookmarks and it's a long timeline this starts to take 10s of - // milliseconds - - std::map>> - bookmap; - std::map> timelinemap; - - // turn bookmarks into src lookup -> bookmarks range - for (const auto &i : bookmark_details) { - if (i.owner_ and i.media_reference_) { - auto uuid = (*(i.owner_)).uuid(); - bookmap[uuid].emplace_back( - std::make_tuple(i.uuid_, i.colour(), i.start_frame(), i.end_frame())); - } +void SubPlayhead::full_bookmarks_update() { + + // 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 + + if (all_media_uuids_.empty()) { + fetch_bookmark_annotations(BookmarkRanges()); } - int f = 0; + auto global = system().registry().template get(global_registry); + request(global, infinite, bookmark::get_bookmark_atom_v) + .then( + [=](caf::actor bookmarks_manager) { + // 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_) + .then( + [=](const std::vector &bookmark_details) { + // make a map of the bookmarks against the uuid of the media + // that owns the bookmark + std::map> + bookmarks; + + BookmarkRanges result; + + if (bookmark_details.empty()) { + fetch_bookmark_annotations(result); + }; + + for (const auto &i : bookmark_details) { + if (i.owner_ and i.media_reference_) { + bookmarks[(*(i.owner_)).uuid()].push_back(i); + } + } - for (const auto &i : full_timeline_frames_) { - if (i.second and bookmap.count(i.second->media_uuid_)) { - // matched = false; - // convert media frame into flick. - auto mf = i.second->frame_ - i.second->first_frame_; + utility::Uuid curr_media_uuid; + std::vector *curr_media_bookmarks = + nullptr; + + // WARNING!! This is potentially expensive for very long timelines + // ... Need to look for an optimisation + + // 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_) { + + // 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 (bookmarks.count(curr_media_uuid)) { + curr_media_bookmarks = &(bookmarks[curr_media_uuid]); + } else { + curr_media_bookmarks = nullptr; + } + + } else if (!f.second) { + curr_media_uuid = utility::Uuid(); + curr_media_bookmarks = nullptr; + } + + if (curr_media_bookmarks) { + + auto media_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) { + extend_bookmark_frame( + bookmark, logical_playhead_frame, result); + } + } + } + logical_playhead_frame++; + } - for (const auto &j : bookmap[i.second->media_uuid_]) { - const auto &[u, c, s, e] = j; + fetch_bookmark_annotations(result); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} - if (s <= mf and e >= mf) { - // has match - if (timelinemap.count(u)) { - std::get<2>(timelinemap[u]) = f; - // timelinemap[u].second = f; - } else { - timelinemap[u] = std::make_tuple(c, f, f); - } - } +void SubPlayhead::extend_bookmark_frame( + const bookmark::BookmarkDetail &detail, + const int logical_playhead_frame, + BookmarkRanges &bookmark_ranges) { + bool existing_entry_extended = false; + for (auto &bm_frame_range : bookmark_ranges) { + if (detail.uuid_ == std::get<0>(bm_frame_range)) { + if (std::get<3>(bm_frame_range) == (logical_playhead_frame - 1)) { + std::get<3>(bm_frame_range)++; + existing_entry_extended = true; + break; } } - f++; } + if (!existing_entry_extended) { + bookmark_ranges.emplace_back(std::make_tuple( + detail.uuid_, detail.colour(), logical_playhead_frame, logical_playhead_frame)); + } +} + +void SubPlayhead::fetch_bookmark_annotations(BookmarkRanges bookmark_ranges) { + if (!bookmark_ranges.size()) { + bookmark_ranges_.clear(); + bookmarks_.clear(); + send(parent_, utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_); + return; + } + utility::UuidList bookmark_ids; + for (const auto &p : bookmark_ranges) { + bookmark_ids.push_back(std::get<0>(p)); + } + + // 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) + .then( + [=](caf::actor bookmarks_manager) { + // get the bookmark actors for bookmarks that are in our timline + request( + bookmarks_manager, infinite, bookmark::get_bookmark_atom_v, bookmark_ids) + .then( + [=](const std::vector &bookmarks) { + // 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()); + + for (auto bookmark : bookmarks) { + + // now ask the bookmark actor for its detail and + // annotation data (if any) + request( + bookmark.actor(), + infinite, + bookmark::bookmark_detail_atom_v, + bookmark::get_annotation_atom_v) + .then( + [=](bookmark::BookmarkAndAnnotationPtr data) mutable { + for (const auto &p : bookmark_ranges) { + if (data->detail_.uuid_ == std::get<0>(p)) { + // set the frame ranges. Note this + // const_cast is safe because this shared + // ptr has not been shared with anyone yet. + auto d = const_cast< + bookmark::BookmarkAndAnnotation *>( + data.get()); + d->start_frame_ = std::get<2>(p); + d->end_frame_ = std::get<3>(p); + result->emplace_back(data); + } + } + + (*count)--; + if (!*count) { + + // sortf bookmarks by start frame - makes + // searching them faster + std::sort( + result->begin(), + result->end(), + [](const bookmark::BookmarkAndAnnotationPtr + &a, + const bookmark::BookmarkAndAnnotationPtr + &b) -> bool { + return a->start_frame_ < + b->start_frame_; + }); + + bookmarks_ = *result; + + // now ditch non-visible bookmarks + // (e.g. grades) from our ranges + bookmark_ranges_.clear(); + auto p = bookmark_ranges.begin(); + while (p != bookmark_ranges.end()) { + const auto uuid = std::get<0>(*p); + bool visible_bookmark = true; + for (const auto &b : bookmarks_) { + if (b->detail_.uuid_ == uuid && + !(b->detail_.visible_ && + *(b->detail_.visible_))) { + visible_bookmark = false; + break; + } + } + if (visible_bookmark) { + bookmark_ranges_.push_back(*p); + } + p++; + } + + // we've finished, ping the parent PlayheadActor + // with our new bookmark ranges + send( + parent_, + utility::event_atom_v, + bookmark::get_bookmarks_atom_v, + bookmark_ranges_); + } + }, + [=](const error &err) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + +void SubPlayhead::add_annotations_data_to_frame(ImageBufPtr &frame) { + + xstudio::bookmark::BookmarkAndAnnotations bookmarks; + int logical_frame = frame.frame_id().playhead_logical_frame_; + for (auto &p : bookmarks_) { + if (p->start_frame_ <= logical_frame && p->end_frame_ >= logical_frame) { + bookmarks.push_back(p); + } else if (p->start_frame_ > logical_frame) + break; + // bookmarks_ sorted by start frame so if this bookmark starts after + // logical_frame we can leave the loop + } + frame.set_bookmarks(bookmarks); +} + +void SubPlayhead::bookmark_deleted(const utility::Uuid &bookmark_uuid) { + + // update bookmark only if the removed bookmark is in our list... + auto p = bookmarks_.begin(); + while (p != bookmarks_.end()) { + if ((*p)->detail_.uuid_ == bookmark_uuid) { + p = bookmarks_.erase(p); + } else { + p++; + } + } + const size_t n = bookmark_ranges_.size(); + auto q = bookmark_ranges_.begin(); + while (q != bookmark_ranges_.end()) { + if (std::get<0>(*q) == bookmark_uuid) { + q = bookmark_ranges_.erase(q); + } else { + q++; + } + } + + if (n != bookmark_ranges_.size()) { + send(parent_, utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_); + } +} + +void SubPlayhead::bookmark_changed(const utility::UuidActor bookmark) { - for (const auto &i : timelinemap) { - const auto &[c, s, e] = i.second; - result.emplace_back(std::make_tuple(i.first, c, s, e)); + // if a bookmark has changed, and its a bookmar that is in our timleine, + // 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(); + return; + } } -} \ No newline at end of file + + // 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) + .then( + [=](const bookmark::BookmarkDetail &detail) { + if (detail.owner_) { + auto p = std::find( + all_media_uuids_.begin(), + all_media_uuids_.end(), + (*(detail.owner_)).uuid()); + if (p != all_media_uuids_.end()) { + full_bookmarks_update(); + } + } + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index fa6790da3..6e5634b40 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -76,8 +76,14 @@ 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)))), '.'); +#else std::string ext = ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); +#endif const auto source_uuid = Uuid::generate(); auto source = @@ -222,6 +228,7 @@ PlaylistActor::PlaylistActor( try { auto actor = system().spawn( static_cast(value), caf::actor_cast(this)); + container_[key] = actor; link_to(actor); join_event_group(this, actor); @@ -357,8 +364,13 @@ void PlaylistActor::init() { const utility::FrameRate &rate, const utility::UuidActor &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 const auto source_uuid = Uuid::generate(); auto source = @@ -415,8 +427,13 @@ 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 const auto source_uuid = Uuid::generate(); auto source = @@ -598,15 +615,17 @@ void PlaylistActor::init() { }, [=](add_media_atom, - std::vector media_actors, + 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 - auto source_count = std::make_shared(); - (*source_count) = media_actors.size(); - auto rp = make_response_promise(); + + std::vector media_actors = ma; + auto source_count = std::make_shared(); + (*source_count) = media_actors.size(); + auto rp = make_response_promise(); // add to lis first, then lazy update.. @@ -635,7 +654,6 @@ void PlaylistActor::init() { // 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! @@ -650,6 +668,7 @@ void PlaylistActor::init() { add_media_atom_v, i); } + send_content_changed_event(); } }, [=](error &err) mutable { @@ -658,7 +677,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); @@ -1919,7 +1938,8 @@ void PlaylistActor::add_media( open_media_reader(ua.actor()); }, [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn( + "{} {} {}", __PRETTY_FUNCTION__, to_string(err), to_string(ua.actor())); send_content_changed_event(); base_.send_changed(event_group_, this); rp.deliver(ua); @@ -2000,13 +2020,13 @@ void PlaylistActor::notify_tree(const utility::UuidTree & void PlaylistActor::duplicate_tree(utility::UuidTree &tree) { tree.value().set_name(tree.value().name() + " - copy"); - if (tree.value().type() == "ContainerDivider") { + auto type = tree.value().type(); + + if (type == "ContainerDivider") { tree.value().set_uuid(Uuid::generate()); - } else if (tree.value().type() == "ContainerGroup") { + } else if (type == "ContainerGroup") { tree.value().set_uuid(Uuid::generate()); - } else if ( - tree.value().type() == "Subset" || tree.value().type() == "ContactSheet" || - tree.value().type() == "Timeline") { + } else if (type == "Subset" || type == "ContactSheet" || type == "Timeline") { // 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.. // we need to insert the new playlist in to the session and update the UUID @@ -2014,6 +2034,14 @@ void PlaylistActor::duplicate_tree(utility::UuidTree &tre caf::scoped_actor sys(system()); auto result = request_receive( *sys, container_[tree.value().uuid()], duplicate_atom_v); + + if (type == "Timeline") + anon_send( + result.actor(), + playhead::source_atom_v, + caf::actor_cast(this), + UuidUuidMap()); + tree.value().set_uuid(result.uuid()); container_[result.uuid()] = result.actor(); link_to(result.actor()); diff --git a/src/plugin/colour_op/CMakeLists.txt b/src/plugin/colour_op/CMakeLists.txt new file mode 100644 index 000000000..d57e3272c --- /dev/null +++ b/src/plugin/colour_op/CMakeLists.txt @@ -0,0 +1,3 @@ +add_src_and_test(grading) + +build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/colour_op/grading/src/CMakeLists.txt b/src/plugin/colour_op/grading/src/CMakeLists.txt new file mode 100644 index 000000000..d739d8fef --- /dev/null +++ b/src/plugin/colour_op/grading/src/CMakeLists.txt @@ -0,0 +1,33 @@ +project(grading VERSION 0.1.0 LANGUAGES CXX) + +find_package(Imath) +find_package(OpenColorIO CONFIG) + +set(SOURCES + grading_data.cpp + grading_data_serialiser.cpp + grading_colour_op.cpp + grading.cpp + grading_mask_gl_renderer.cpp + serialisers/1.0/serialiser_1_pt_0.cpp +) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) +add_library(xstudio::colour_pipeline::grading ALIAS ${PROJECT_NAME}) +default_plugin_options(${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + xstudio::module + xstudio::plugin_manager + xstudio::colour_pipeline + xstudio::ui::opengl::viewport + Imath::Imath + OpenColorIO::OpenColorIO +) + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + +add_subdirectory(qml) \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading.cpp b/src/plugin/colour_op/grading/src/grading.cpp new file mode 100644 index 000000000..e370812e7 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading.cpp @@ -0,0 +1,1017 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/string_helpers.hpp" + +#include "grading.h" +#include "grading_mask_render_data.h" +#include "grading_mask_gl_renderer.h" +#include "grading_colour_op.hpp" + +using namespace xstudio; +using namespace xstudio::bookmark; +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) { + + module::QmlCodeAttribute *button = add_qml_code_attribute( + "MyCode", + R"( + import Grading 1.0 + GradingButton { + anchors.fill: parent + } + )"); + + button->expose_in_ui_attrs_group("media_tools_buttons_0"); + button->set_role_data(module::Attribute::ToolbarPosition, 500.0); + + // General + + 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"); + + 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 elements + + 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_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_bypass_ = add_boolean_attribute("drawing_bypass", "drawing_bypass", false); + grading_bypass_->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"); + + // 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)); + + // 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)); + + // 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)); + + // 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)); + + // Masking elements + + drawing_tool_ = add_string_choice_attribute( + "drawing_tool", + "drawing_tool", + utility::map_value_to_vec(drawing_tool_names_).front(), + 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"); + + 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"); + + erase_pen_size_ = add_integer_attribute("Erase Pen Size", "Erase Pen Size", 80, 1, 300); + erase_pen_size_->expose_in_ui_attrs_group("mask_tool_settings"); + erase_pen_size_->set_preference_path("/plugin/grading/erase_pen_size"); + + pen_colour_ = add_colour_attribute( + "Pen Colour", "Pen Colour", utility::ColourTriplet(0.5f, 0.4f, 1.0f)); + pen_colour_->expose_in_ui_attrs_group("mask_tool_settings"); + pen_colour_->set_preference_path("/plugin/grading/pen_colour"); + + pen_opacity_ = add_integer_attribute("Pen Opacity", "Pen Opacity", 100, 0, 100); + 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_->expose_in_ui_attrs_group("mask_tool_settings"); + pen_softness_->set_preference_path("/plugin/grading/pen_softness"); + + display_mode_attribute_ = add_string_choice_attribute( + "display_mode", + "display_mode", + utility::map_value_to_vec(display_mode_names_).front(), + utility::map_value_to_vec(display_mode_names_)); + 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(); + + // 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++; + } + } + }); +} + +utility::BlindDataObjectPtr GradingTool::prepare_overlay_data( + const media_reader::ImageBufPtr &image, const bool offscreen) 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 + // currently being edited by us. If so, we attach up-to-date data on + // the edited grade for display. + + if (!grading_data_.identity() && image) { + + bool we_are_editing_grade_on_this_image = false; + for (auto &bookmark : image.bookmarks()) { + if (bookmark->detail_.uuid_ == grading_data_.bookmark_uuid_) { + we_are_editing_grade_on_this_image = true; + break; + } + } + if (we_are_editing_grade_on_this_image) { + + auto render_data = std::make_shared(); + + // N.B. this means we copy the entirity of grading_data_ (it's strokes + // basically) on every redraw. Should be ok in the wider scheme of + // things but not exactly efficient. Another approach would be making + // GradingData thread safe (Canvas class already is) and share a + // 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; + } + } + return utility::BlindDataObjectPtr(); +} + +AnnotationBasePtr GradingTool::build_annotation(const utility::JsonStore &data) { + + return std::make_shared(data); +} + +void GradingTool::images_going_on_screen( + const std::vector &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()) { + + auto data = dynamic_cast(bookmark->annotation_.get()); + if (data && !grading_data) { + grading_data = data; + n++; + } else if (data) { + n++; + } + } + + 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(); + } + } + } +} + +void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { + + if (attribute_uuid == tool_is_active_->uuid()) { + + 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(); + } + + } 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(); + + } else { + release_mouse_focus(); + release_keyboard_focus(); + end_drawing(); + } + + refresh_current_layer_from_ui(); + + } else if (attribute_uuid == grading_action_->uuid() && grading_action_->value() != "") { + + if (grading_action_->value() == "Clear") { + + clear_cdl(); + refresh_current_layer_from_ui(); + + } else if (utility::starts_with(grading_action_->value(), "Save CDL ")) { + + std::size_t prefix_length = std::string("Save CDL ").size(); + std::string filepath = grading_action_->value().substr( + prefix_length, grading_action_->value().size() - prefix_length); + save_cdl(filepath); + + } else if (grading_action_->value() == "Prev Layer") { + + toggle_grade_layer(active_layer_ - 1); + + } else if (grading_action_->value() == "Next Layer") { + + toggle_grade_layer(active_layer_ + 1); + + } else if (grading_action_->value() == "Add Layer") { + + add_grade_layer(); + + } else if (grading_action_->value() == "Remove Layer") { + + delete_grade_layer(); + } + + grading_action_->set_value(""); + + } else if ( + + attribute_uuid == drawing_action_->uuid() && drawing_action_->value() != "") { + + if (drawing_action_->value() == "Clear") { + clear_mask(); + } else if (drawing_action_->value() == "Undo") { + undo(); + } else if (drawing_action_->value() == "Redo") { + redo(); + } + drawing_action_->set_value(""); + + } else if (attribute_uuid == drawing_tool_->uuid()) { + + if (tool_is_active_->value()) { + + if (drawing_tool_->value() == "None") { + release_mouse_focus(); + } else { + grab_mouse_focus(); + } + + end_drawing(); + release_keyboard_focus(); + } + + } else if (attribute_uuid == display_mode_attribute_->uuid()) { + + refresh_current_layer_from_ui(); + + } else if (attribute_uuid == grading_bypass_->uuid()) { + + for (auto &a : grading_colour_op_actors_) { + send(a, utility::event_atom_v, "bypass", grading_bypass_->value()); + } + + } 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); + + } else if ( + attribute_uuid == slope_master_->uuid() || + attribute_uuid == offset_master_->uuid() || + attribute_uuid == power_master_->uuid()) { + + 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); + } + + refresh_current_layer_from_ui(); + create_bookmark(); + save_bookmark(); + } + + redraw_viewport(); +} + +void GradingTool::register_hotkeys() { + + toggle_active_hotkey_ = register_hotkey( + int('G'), + 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"); + + undo_hotkey_ = register_hotkey( + int('Z'), + ui::ControlModifier, + "Undo (Annotation edit)", + "Undoes your last edits to an annotation"); + + redo_hotkey_ = register_hotkey( + int('Z'), + ui::ControlModifier | ui::ShiftModifier, + "Redo (Annotation edit)", + "Redoes your last undone edit on an annotation"); +} + +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()); + + } else if (hotkey_uuid == toggle_mask_hotkey_ && tool_is_active_->value()) { + + mask_is_active_->set_value(!mask_is_active_->value()); + + } else if (hotkey_uuid == undo_hotkey_ && tool_is_active_->value()) { + + undo(); + redraw_viewport(); + + } else if (hotkey_uuid == redo_hotkey_ && tool_is_active_->value()) { + + redo(); + redraw_viewport(); + } +} + +bool GradingTool::pointer_event(const ui::PointerEvent &e) { + + if (!tool_is_active_->value() || !mask_is_active_->value()) + return false; + + bool redraw = true; + + const Imath::V2f pointer_pos = e.position_in_viewport_coord_sys(); + + if (drawing_tool_->value() == "Draw" || drawing_tool_->value() == "Erase") { + + if (e.type() == ui::Signature::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) { + update_stroke(pointer_pos); + } else if (e.type() == ui::Signature::EventType::ButtonRelease) { + end_drawing(); + } + } else { + redraw = false; + } + + if (redraw) + redraw_viewport(); + + return false; +} + +void GradingTool::start_stroke(const Imath::V2f &point) { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + if (drawing_tool_->value() == "Draw") { + layer->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); + } + + update_stroke(point); +} + +void GradingTool::update_stroke(const Imath::V2f &point) { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + layer->mask().update_stroke(point); +} + +void GradingTool::end_drawing() { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + layer->mask().end_draw(); + save_bookmark(); +} + +void GradingTool::undo() { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + if (mask_is_active_->value()) { + + layer->mask().undo(); + } + + // TODO: Support undo / redo for grading +} + +void GradingTool::redo() { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + if (mask_is_active_->value()) { + + layer->mask().redo(); + } + + // TODO: Support undo / redo for grading +} + +void GradingTool::clear_mask() { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + layer->mask().clear(); +} + +void GradingTool::clear_cdl() { + + 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)); + + 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)); + + 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)); + + 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)); + + sat_->set_value(sat_->get_role_data(module::Attribute::DefaultValue)); +} + +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()}; + std::array offset{ + offset_red_->value() + offset_master_->value(), + offset_green_->value() + offset_master_->value(), + offset_blue_->value() + offset_master_->value()}; + std::array power{ + power_red_->value() * power_master_->value(), + power_green_->value() * power_master_->value(), + power_blue_->value() * power_master_->value()}; + + cdl->setSlope(slope.data()); + cdl->setOffset(offset.data()); + cdl->setPower(power.data()); + cdl->setSat(sat_->value()); + + OCIO::FormatMetadata &metadata = cdl->getFormatMetadata(); + metadata.setID("0"); + + OCIO::GroupTransformRcPtr grp = OCIO::GroupTransform::Create(); + grp->appendTransform(cdl); + + // Write to disk using OCIO + + std::string localpath = filepath; + localpath = utility::replace_once(localpath, "file://", ""); + + std::string format; + if (utility::ends_with(localpath, "cdl")) { + format = "ColorDecisionList"; + } else if (utility::ends_with(localpath, "cc")) { + format = "ColorCorrection"; + } else if (utility::ends_with(localpath, "ccc")) { + format = "ColorCorrectionCollection"; + } + + std::ofstream ofs(localpath); + if (ofs.is_open()) { + grp->write(OCIO::GetCurrentConfig(), format.c_str(), ofs); + } else { + spdlog::warn("Failed to create file: {}", localpath); + } +} + +void GradingTool::load_grade_layers(GradingData *grading_data) { + + // Load layer(s) + + grading_data_ = *grading_data; + active_layer_ = grading_data_.size() - 1; + + // Shader (re) construction + + + // Update UI + + 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); + + refresh_ui_from_current_layer(); +} + +void GradingTool::reset_grade_layers() { + + 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(); +} + +void GradingTool::add_grade_layer() { + + if (grading_data_.size() >= maximum_layers_) { + spdlog::warn("Maximum number of layers reached ({})", maximum_layers_); + return; + } + + // Add layer on top + + active_layer_ = grading_data_.size(); + grading_data_.push_layer(); + + // Update UI + + auto layer_name = std::string(fmt::format("Layer {}", active_layer_ + 1)); + + 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); + + refresh_ui_from_current_layer(); +} + +void GradingTool::toggle_grade_layer(size_t layer) { + + if (layer >= grading_data_.size() || layer < 0) { + spdlog::warn("Trying to toggle to non-existing layer {}", layer); + return; + } + + active_layer_ = layer; + + // Update UI + + auto layer_name = std::string(fmt::format("Layer {}", active_layer_ + 1)); + grading_layer_->set_value(layer_name, false); + + refresh_ui_from_current_layer(); +} + +void GradingTool::delete_grade_layer() { + + if (grading_data_.size() < 2) { + spdlog::warn("Can't delete base grade layer"); + return; + } + + // Delete top layer + + grading_data_.pop_layer(); + active_layer_ = grading_data_.size() - 1; + + // Update UI + + 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); + + refresh_ui_from_current_layer(); +} + +ui::viewport::LayerData *GradingTool::current_layer() { + + return grading_data_.layer(active_layer_); +} + +void GradingTool::refresh_current_layer_from_ui() { + + LayerData *layer = current_layer(); + if (!layer) { + return; + } + + 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; + } + + auto &grade = layer->grade(); + + 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); + + 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); + + 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); + + sat_->set_value(grade.sat, false); + + // mask_is_active_->set_value(layer->mask_active()); + display_mode_attribute_->set_value(layer->mask_editing() ? "Mask" : "Grade"); +} + +utility::Uuid GradingTool::current_bookmark() const { return grading_data_.bookmark_uuid_; } + +void GradingTool::create_bookmark() { + + if (current_bookmark().is_null()) { + + 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();*/ + + // Hides bookmark from timeline + bmd.colour_ = "transparent"; + bmd.visible_ = false; + + grading_data_.bookmark_uuid_ = StandardPlugin::create_bookmark_on_current_media( + "viewport0", "Grading Note", bmd, true); + grading_data_creation_frame_ = current_on_screen_frame_; + + // StandardPlugin::update_bookmark_detail(grading_data_.bookmark_uuid_, bmd); + } +} + +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(); + } + } +} + +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( + grading_colour_op, + utility::event_atom_v, + "bypass", + grading_bypass_->value()); + } + }}) + .or_else(StandardPlugin::message_handler_extensions()); +} + + +static std::vector> factories( + {std::make_shared>( + GradingTool::PLUGIN_UUID, + "GradingToolUI", + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + true, + "Remi Achard", + "Plugin providing interface for creating interactive grading notes with painted " + "masks.", + semver::version("0.0.0"), + "", + ""), + std::make_shared>( + GradingColourOperator::PLUGIN_UUID, + "GradingToolColourOp", + plugin_manager::PluginFlags::PF_COLOUR_OPERATION, + false, + "Remi Achard", + "Colour operator to apply CDL with optional painted masking in viewport.")}); + +#define PLUGIN_DECLARE_END() \ + extern "C" { \ + plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { \ + return new plugin_manager::PluginFactoryCollection( \ + std::vector>(factories)); \ + } \ + } + +PLUGIN_DECLARE_END() \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading.h b/src/plugin/colour_op/grading/src/grading.h new file mode 100644 index 000000000..6704d4d36 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include //NOLINT + +#include "xstudio/colour_pipeline/colour_operation.hpp" +#include "grading_data.h" +#include "grading_mask_gl_renderer.h" + +namespace OCIO = OCIO_NAMESPACE; + + +namespace xstudio::colour_pipeline { + +class GradingTool : public plugin::StandardPlugin { + public: + inline static const utility::Uuid PLUGIN_UUID = + utility::Uuid("5598e01e-c6bc-4cf9-80ff-74bb560df12a"); + + public: + 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; + + // Annotations (grading) + + bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &data) override; + + void images_going_on_screen( + const std::vector & images, + const std::string viewport_name, + const bool playhead_playing + ) 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; + + bool pointer_event(const ui::PointerEvent &e) override; + + // Playhead events + + protected: + + caf::message_handler message_handler_extensions() override; + + private: + void start_stroke(const Imath::V2f &point); + void update_stroke(const Imath::V2f &point); + void end_drawing(); + + void undo(); + void redo(); + + void clear_mask(); + void clear_cdl(); + 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; + void create_bookmark(); + void save_bookmark(); + + + private: + // General + module::BooleanAttribute *tool_is_active_ {nullptr}; + module::BooleanAttribute *mask_is_active_ {nullptr}; + module::StringAttribute *grading_action_ {nullptr}; + module::StringAttribute *drawing_action_ {nullptr}; + + // Grading + enum class GradingPanel { Basic, CDLSliders, CDLWheels }; + const std::map grading_panel_names_ = { + {GradingPanel::Basic, "Basic"}, + {GradingPanel::CDLSliders, "Sliders"}, + {GradingPanel::CDLWheels, "Wheels"} + }; + + 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}; + + // Drawing Mask + enum class DrawingTool { Draw, Erase, None }; + const std::map drawing_tool_names_ = { + {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}; + + enum DisplayMode { Mask, Grade }; + const std::map display_mode_names_ = { + {DisplayMode::Grade, "Grade"}, + {DisplayMode::Mask, "Mask"} + }; + 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 undo_hotkey_; + utility::Uuid redo_hotkey_; + + // Current media info (eg. for Bookmark creation) + bool playhead_is_playing_ {false}; + + // 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 new file mode 100644 index 000000000..cb45e9bdf --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_colour_op.cpp @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/string_helpers.hpp" + +#include "grading.h" +#include "grading_colour_op.hpp" +#include "grading_mask_render_data.h" +#include "grading_mask_gl_renderer.h" + +using namespace xstudio; +using namespace xstudio::bookmark; +using namespace xstudio::colour_pipeline; +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 + +uniform bool grade_tool_op; + +// LayerDeclarations +vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { + + vec4 image_col = rgba; + + if (grade_tool_op) { + return image_col; + } + + // LayerInvocations + return image_col; +} +)"; + +const char *layer_template = R"( +// Layer + +uniform sampler2D layer_mask; +uniform bool layer_mask_active; +uniform bool layer_mask_editing; +//OCIOTransform +vec4 apply_layer(vec4 image_col, vec2 image_pos) { + + 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); + } +} +)"; + +const char *layer_call = R"( + image_col = apply_layer(image_col, image_pos); +)"; + +// here's how it might look with uniform arrays and a single grading function: + +/*const char * temp_static_shader = R"( +#version 430 core + +uniform bool grade_tool_op; + +// Layer0 + +uniform sampler2D layer_mask[8]; +uniform bool layer_mask_active[8]; +uniform bool layer_mask_editing[8]; + +// Declaration of all variables + +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]; + +// Declaration of the OCIO shader function + +vec4 OCIOLayer0(vec4 inPixel, int layer_index) +{ + vec4 outColor = inPixel; + + // Add GradingPrimary 'linear' forward processing + + { + if (!ocio_layer_grading_primary_localBypass[layer_index]) + { + 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] + ); + } + } + + return outColor; +} + +vec4 apply_layer(vec4 image_col, 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); + + // 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); + } + // Output mask color pixels + else if (layer_mask_active[layer_index]) { + float mask_opacity = 0.5 * mask_alpha; + return vec4(mix(image_col.rgb, mask_color.rgb, mask_opacity), image_col.a); + } + else { + return OCIOGradeFunc(image_col, layer_index); + } +} + +vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { + + vec4 image_col = rgba; + + if (grade_tool_op) { + return image_col; + } + + // 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; +} +)";*/ + +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(); + } + } + + // 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; + } + } + + 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; +} + +} // anonymous namespace + +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) + .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)); + }, + [=](caf::error &err) mutable { + + }); +} + +caf::message_handler GradingColourOperator::message_handler_extensions() { + + // here's our handler for the messages coming from the GradintTool about + // the state of its 'bypass' attribute. + return caf::message_handler( + {[=](utility::event_atom, const std::string &desc, bool bypass) { + if (desc == "bypass") { + bypass_ = bypass; + } + }}) + .or_else(ColourOpPlugin::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_; +} + +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()) { + + auto data = dynamic_cast(bookmark->annotation_.get()); + if (data) { + + for (auto &layer : *data) { + + uniforms_dict[fmt::format("layer{}_mask_active", layer_id)] = + layer.mask_active(); + uniforms_dict[fmt::format("layer{}_mask_editing", layer_id)] = + layer.mask_editing(); + + update_dynamic_parameters( + shader_data_[layer_id].shader_desc, layer.grade()); + update_all_uniforms(shader_data_[layer_id].shader_desc, uniforms_dict); + layer_id++; + + 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() { + + /*if (grading_data->size() == shader_data_.size()) return; + + shader_data_.clear();*/ + + size_t layer_id = 0; + for (size_t layer_id = 0; layer_id < NUM_LAYERS; ++layer_id) { + + auto desc = setup_ocio_shader( + fmt::format("OCIOLayer{}", layer_id), fmt::format("ocio_layer{}_", layer_id)); + auto luts = setup_ocio_textures(desc); + + shader_data_.emplace_back(LayerShaderData{desc, luts}); + layer_id++; + } + + 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_; + + layer_id = 0; + for (auto &data : shader_data_) { + luts.insert(luts.end(), data.luts.begin(), data.luts.end()); + layer_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; +} + +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); +} + +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(); + + 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); + + return desc; +} + +std::vector +GradingColourOperator::setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader) { + + std::vector luts; + + // Process 3D LUTs + const unsigned max_texture_3D = shader->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->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->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(); + luts.push_back(xs_lut); + } + + // Process 1D LUTs + const unsigned max_texture_2D = shader->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->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->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(); + luts.push_back(xs_lut); + } + + 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 new file mode 100644 index 000000000..8ceb7eb47 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_colour_op.hpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include //NOLINT + +#include "xstudio/colour_pipeline/colour_operation.hpp" +#include "grading_data.h" +#include "grading_mask_gl_renderer.h" + +namespace OCIO = OCIO_NAMESPACE; + +namespace xstudio::colour_pipeline { + +class GradingColourOperator : public ColourOpPlugin { + + public: + inline static const utility::Uuid PLUGIN_UUID = + utility::Uuid("b78e2aff-4709-46a1-9db2-61260997d401"); + + public: + GradingColourOperator(caf::actor_config &cfg, const utility::JsonStore &init_settings); + ~GradingColourOperator() override = default; + + // Colour grading + + float ordering() const override { return -100.0f; } + + ColourOperationDataPtr colour_op_graphics_data( + utility::UuidActor &media_source, + const utility::JsonStore &media_source_colour_metadata) override; + + utility::JsonStore update_shader_uniforms(const media_reader::ImageBufPtr &image) override; + + plugin::GPUPreDrawHookPtr make_pre_draw_gpu_hook(const int /*viewer_index*/) override; + + protected: + caf::message_handler message_handler_extensions() override; + + private: + void build_shader_data(); + + void setup_colourop_shader(); + + OCIO::ConstGpuShaderDescRcPtr + setup_ocio_shader(const std::string &function_name, const std::string &resource_prefix); + + std::vector setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader); + + void update_dynamic_parameters( + OCIO::ConstGpuShaderDescRcPtr &shader, const ui::viewport::Grade &grade) const; + + void update_all_uniforms( + OCIO::ConstGpuShaderDescRcPtr &shader, utility::JsonStore &uniforms) const; + + 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_; + + bool bypass_ = {false}; +}; + +} // namespace xstudio::colour_pipeline \ 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 new file mode 100644 index 000000000..10cb7e6f3 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_data.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "grading_data.h" +#include "grading_data_serialiser.hpp" +#include "grading.h" + +using namespace xstudio::ui::viewport; +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("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_); +} + +void xstudio::ui::viewport::to_json(nlohmann::json &j, const LayerData &l) { + + j["grade"] = l.grade_; + j["mask_active"] = l.mask_active_; + j["mask_editing"] = l.mask_editing_; + j["mask"] = l.mask_; +} + + +GradingData::GradingData(const utility::JsonStore &s) : bookmark::AnnotationBase() { + + GradingDataSerialiser::deserialise(this, s); +} + +utility::JsonStore GradingData::serialise(utility::Uuid &plugin_uuid) const { + + plugin_uuid = colour_pipeline::GradingTool::PLUGIN_UUID; + 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) { + + if (idx >= 0 && idx < layers_.size()) { + return &layers_[idx]; + } + return nullptr; +} + +void GradingData::push_layer() { layers_.push_back(LayerData()); } + +void GradingData::pop_layer() { layers_.pop_back(); } + +void xstudio::ui::viewport::from_json(const nlohmann::json &j, GradingData &gd) { + + j.at("layers").get_to(gd.layers_); +} + +void xstudio::ui::viewport::to_json(nlohmann::json &j, const GradingData &gd) { + + j["layers"] = gd.layers_; +} diff --git a/src/plugin/colour_op/grading/src/grading_data.h b/src/plugin/colour_op/grading/src/grading_data.h new file mode 100644 index 000000000..013dd0478 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_data.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/bookmark/bookmark.hpp" +#include "xstudio/ui/canvas/canvas.hpp" + +namespace xstudio { +namespace ui { +namespace viewport { + + struct Grade { + std::array slope {1.0, 1.0, 1.0, 0.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}; + + bool operator==(const Grade &o) const { + return ( + slope == o.slope && + offset == o.offset && + power == o.power && + sat == o.sat + ); + } + }; + + void from_json(const nlohmann::json &j, Grade &g); + void to_json(nlohmann::json &j, const Grade &g); + + class LayerData { + public: + LayerData() = default; + + bool operator==(const LayerData &o) const { + return ( + grade_ == o.grade_ && + mask_active_ == o.mask_active_ && + mask_editing_ == o.mask_editing_ && + mask_ == o.mask_ + ); + } + + bool identity() const; + + Grade & grade() { return grade_; } + const Grade & grade() const { return grade_; } + + void set_mask_active(bool val) { mask_active_ = val; } + bool mask_active() const { return mask_active_; } + + void set_mask_editing(bool val) { mask_editing_ = val; } + bool mask_editing() const { return mask_editing_; } + + canvas::Canvas & mask() { return mask_; } + 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); + + 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; + +} // 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 new file mode 100644 index 000000000..139634709 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_data_serialiser.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "grading_data_serialiser.hpp" + +using namespace xstudio::ui::viewport; +using namespace xstudio; + +std::map> GradingDataSerialiser::serialisers; + +static const std::string GRADING_VERSION_KEY("Grading Serialiser Version"); + +utility::JsonStore GradingDataSerialiser::serialise(const GradingData *grading_data) { + + if (serialisers.empty()) { + throw std::runtime_error("No GradingData Serialisers registered."); + } + auto p = serialisers.rbegin(); + utility::JsonStore result; + result[GRADING_VERSION_KEY] = p->first; + result["Data"] = nlohmann::json(); + p->second->_serialise(grading_data, result["Data"]); + return result; +} + +void GradingDataSerialiser::deserialise( + GradingData *grading_data, const utility::JsonStore &data) { + + if (data.find(GRADING_VERSION_KEY) != data.end()) { + const int sver = data[GRADING_VERSION_KEY].get(); + if (serialisers.find(sver) != serialisers.end()) { + serialisers[sver]->_deserialise(grading_data, data["Data"]); + } else { + throw std::runtime_error("Unknown GradingData serialiser version."); + } + } else { + throw std::runtime_error("GradingDataSerialiser passed json data without \"GradingData " + "Serialiser Version\"."); + } +} + + +void GradingDataSerialiser::register_serialiser( + const unsigned char maj_ver, + const unsigned char minor_ver, + std::shared_ptr sptr) { + int fver = maj_ver << 8 + minor_ver; + assert(sptr); + if (serialisers.find(fver) != serialisers.end()) { + throw std::runtime_error("Attempt to register Annotation Serialiser with a used " + "version number that is already used."); + } + serialisers[fver] = sptr; +} diff --git a/src/plugin/colour_op/grading/src/grading_data_serialiser.hpp b/src/plugin/colour_op/grading/src/grading_data_serialiser.hpp new file mode 100644 index 000000000..323b141ed --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_data_serialiser.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "grading_data.h" + +namespace xstudio { +namespace ui { + namespace viewport { + + class GradingDataSerialiser { + + public: + GradingDataSerialiser() = default; + + static utility::JsonStore serialise(const GradingData *); + static void deserialise(GradingData *, const utility::JsonStore &); + + virtual void _serialise(const GradingData *, nlohmann::json &) const = 0; + virtual void _deserialise(GradingData *, const nlohmann::json &) = 0; + + static void register_serialiser( + const unsigned char maj_ver, + const unsigned char minor_ver, + std::shared_ptr sptr); + + private: + static std::map> serialisers; + }; + +#define RegisterGradingDataSerialiser(serialiser_class, v_maj, v_min) \ + class serialiser_class##_register_cls { \ + public: \ + serialiser_class##_register_cls() { \ + GradingDataSerialiser::register_serialiser( \ + v_maj, v_min, std::shared_ptr(new serialiser_class())); \ + } \ + }; \ + serialiser_class##_register_cls serialiser_class##_serialiser_register_rinst; + + } // 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_mask_gl_renderer.cpp b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp new file mode 100644 index 000000000..3ea9cbdf7 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#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" + +using namespace xstudio; +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::opengl; +using namespace xstudio::ui::viewport; + + +GradingMaskRenderer::GradingMaskRenderer() { + + canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); +} + +void GradingMaskRenderer::pre_viewport_draw_gpu_hook( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + 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 + // ImageBufPtr::bookmarks() - this is grading data that has been saved to + // a bookmark as annotation data. + // + // 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). + // + // 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 + // for the *same* grading note can also come up from the bookmark system + // but it will be slightly out-of-date vs. the blind data that is updated + // 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; + } + } + + for (auto &bookmark : image.bookmarks()) { + + 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 + } + } +} + +void GradingMaskRenderer::renderGradingDataMasks( + const GradingData *data, 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 + // operation - namely the shader code itself that implements the grading + // 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) + 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; + std::string cache_id_modifier; + for (auto &layer_data : *data) { + + // here the mask is rendered to a GL texture + render_layer(layer_data, render_layers_[layer_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), + colour_pipeline::ColourTextureTarget::TEXTURE_2D, + render_layers_[layer_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); + + layer_index++; + } + + // Again, colour_pipe_data_ is a shared ptr and we don't know who else might + // be holding/using this data so we make a deep copy. (This is not + // expensive as the contents of ColourPipelineData is only a vector + // to shared ptrs itself, we're not modifying elements of that vector though) + image.colour_pipe_data_.reset( + new colour_pipeline::ColourPipelineData(*(image.colour_pipe_data_))); + + // 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)); +} + +size_t GradingMaskRenderer::layer_count() const { return render_layers_.size(); } + +void GradingMaskRenderer::render_layer( + const LayerData &data, + RenderLayer &layer, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) { + + 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()); + layer.offscreen_renderer->begin(); + + if (data.mask().size()) { + glClearColor(0.0, 0.0, 0.0, 0.0); + glClearDepth(0.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + 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)); + + 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); + } 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); + } + + // note A1: This value is the 'viewport_du_dpixel; which tells us how + // many units of xstudio's viewport coordinate space are covered by a + // pixel. This is used to scale the 'softness' of strokes. In our case + // the number of pixels is the set to the canvas size - this is width + // 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(); + } +} \ 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 new file mode 100644 index 000000000..55e5c61c6 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/ui/opengl/opengl_offscreen_renderer.hpp" +#include "xstudio/ui/opengl/opengl_canvas_renderer.hpp" + + +namespace xstudio { +namespace ui { +namespace viewport { + + /* + The pre_viewport_draw_gpu_hook is called with the GL context of the + viewport in an active state. We draw the strokes of the grading mask + into a GL texture, and set the texture ID on the colour pipeline data + of the image that is passed in. When the image is drawn to the screen + our shader can sample the texture to mask the grade. + */ + + class GradingMaskRenderer : public plugin::GPUPreDrawHook { + + struct RenderLayer { + opengl::OpenGLOffscreenRendererPtr offscreen_renderer; + utility::clock::time_point last_canvas_change_time; + utility::Uuid last_canvas_uuid; + }; + + public: + + GradingMaskRenderer(); + + void pre_viewport_draw_gpu_hook( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + xstudio::media_reader::ImageBufPtr &image) override; + + private: + + size_t layer_count() const; + void add_layer(); + + void renderGradingDataMasks( + const GradingData *, + xstudio::media_reader::ImageBufPtr &image); + + void render_layer( + const LayerData& data, + RenderLayer& layer, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer); + + std::mutex immediate_data_gate_; + utility::BlindDataObjectPtr immediate_data_; + + std::vector render_layers_; + std::unique_ptr canvas_renderer_; + }; + + using GradingMaskRendererSPtr = std::shared_ptr; + +} // 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_mask_render_data.h b/src/plugin/colour_op/grading/src/grading_mask_render_data.h new file mode 100644 index 000000000..2a5df3986 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_mask_render_data.h @@ -0,0 +1,48 @@ +#pragma once + +#include "xstudio/utility/blind_data.hpp" + +#include "grading_data.h" + +namespace xstudio { +namespace ui { +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. + 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 +} // end namespace ui +} // namespace xstudio diff --git a/src/plugin/colour_op/grading/src/qml/.clang-tidy b/src/plugin/colour_op/grading/src/qml/.clang-tidy new file mode 100644 index 000000000..e4a0ac09c --- /dev/null +++ b/src/plugin/colour_op/grading/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/colour_op/grading/src/qml/CMakeLists.txt b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt new file mode 100644 index 000000000..5c50e0831 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..0d3dc27f5 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingButton.qml @@ -0,0 +1,67 @@ +// 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 new file mode 100644 index 000000000..9b25729a0 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingDialog.qml @@ -0,0 +1,502 @@ +// 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 new file mode 100644 index 000000000..bbcf892f2 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingHSlider.qml @@ -0,0 +1,217 @@ +// 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 new file mode 100644 index 000000000..b2baaf272 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderGroup.qml @@ -0,0 +1,217 @@ +// 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 new file mode 100644 index 000000000..26b3224bc --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderSimple.qml @@ -0,0 +1,40 @@ +// 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 new file mode 100644 index 000000000..5463d46f7 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingVSlider.qml @@ -0,0 +1,137 @@ +// 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 new file mode 100644 index 000000000..3ba0794b2 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingWheel.qml @@ -0,0 +1,545 @@ +// 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 new file mode 100644 index 000000000..a96396b12 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.1/qmldir @@ -0,0 +1,9 @@ +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/MaskTool.1/MaskDialog/MaskDialog.qml b/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml new file mode 100644 index 000000000..6af14496c --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml @@ -0,0 +1,1085 @@ +// 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 new file mode 100644 index 000000000..ebb6e1130 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/ToolSelector.qml @@ -0,0 +1,150 @@ +// 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 new file mode 100644 index 000000000..9cfe0d0cd --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/MaskTool.1/qmldir @@ -0,0 +1,3 @@ +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 new file mode 100644 index 000000000..3edf2a097 --- /dev/null +++ b/src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "grading_data_serialiser.hpp" +#include "grading_data.h" + +using namespace xstudio; +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::viewport; + + +class GradingDataSerialiser_1_pt_0 : public GradingDataSerialiser { + + public: + GradingDataSerialiser_1_pt_0() = default; + + void _serialise(const GradingData *, nlohmann::json &) const override; + void _deserialise(GradingData *, const nlohmann::json &) override; +}; + +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(); +} + +void GradingDataSerialiser_1_pt_0::_deserialise( + GradingData *grading_data, const nlohmann::json &d) { + + grading_data->layers() = d.template get>(); +} diff --git a/src/plugin/colour_op/grading/test/CMakeLists.txt b/src/plugin/colour_op/grading/test/CMakeLists.txt new file mode 100644 index 000000000..a73825679 --- /dev/null +++ b/src/plugin/colour_op/grading/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + 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 2ae01c3ec..8aa0216be 100644 --- a/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt +++ b/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(OpenColorIO CONFIG) +find_package(OpenColorIO) find_package(OpenEXR) find_package(Imath) find_package(GLEW REQUIRED) diff --git a/src/plugin/colour_pipeline/ocio/src/ocio.cpp b/src/plugin/colour_pipeline/ocio/src/ocio.cpp index d66d833cb..b3507f586 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio.cpp @@ -93,9 +93,18 @@ std::string OCIOColourPipeline::MediaParams::compute_hash() const { 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) { @@ -214,17 +223,6 @@ ColourOperationDataPtr OCIOColourPipeline::linear_to_display_op_data( std::string display_shader_src = replace_once( ShaderTemplates::OCIO_display, "//OCIODisplay", display_shader->getShaderText()); - // GradingPrimary implement the power function with mirrored behaviour for negatives - // (absolute value before pow then multiply by sign). We update the shader here to - // match ASC CDL clamping [0, 1] behaviour. - std::regex pattern( - R"((\w+)\.rgb = pow\( abs\(\w+\.rgb / (\w+_grading_primary_pivot)\), (\w+_grading_primary_contrast) \) \* sign\(\w+\.rgb\) \* \w+_grading_primary_pivot;)"); - - display_shader_src = std::regex_replace( - display_shader_src, - pattern, - "outColor.rgb = pow( clamp($1.rgb, 0.0, 1.0) / $2, $3 ) * $2;"); - data->shader_ = std::make_shared( utility::Uuid::generate(), display_shader_src); @@ -246,9 +244,10 @@ ColourOperationDataPtr OCIOColourPipeline::linear_to_display_op_data( return data; } -void OCIOColourPipeline::update_shader_uniforms( - utility::JsonStore &uniforms, const utility::Uuid &source_uuid, std::any &user_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") { @@ -280,12 +279,14 @@ void OCIOColourPipeline::update_shader_uniforms( // 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, source_uuid); + 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( @@ -337,6 +338,7 @@ thumbnail::ThumbnailBufferPtr OCIOColourPipeline::process_thumbnail( OCIO::AutoStride, OCIO::AutoStride); + OCIO::PackedImageDesc out_img( dst, thumb->width(), @@ -349,8 +351,8 @@ thumbnail::ThumbnailBufferPtr OCIOColourPipeline::process_thumbnail( 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()); } @@ -366,8 +368,9 @@ void OCIOColourPipeline::extend_pixel_info( try { - const MediaParams media_param = - get_media_params(frame_id.source_uuid_, frame_id.params_); + 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(); @@ -400,7 +403,7 @@ void OCIOColourPipeline::extend_pixel_info( OCIO::DynamicPropertyValue::AsDouble(property); gamma_prop->setValue(enable_gamma_->value() ? gamma_->value() : 1.0f); } - } catch (const OCIO::Exception &e) { + } catch ([[maybe_unused]] const OCIO::Exception &e) { // TODO: ColSci // Update when OCIO::CPUProcessor include hasDynamicProperty() } @@ -509,9 +512,39 @@ OCIOColourPipeline::MediaParams OCIOColourPipeline::get_media_params( return media_params_[source_uuid]; } -void OCIOColourPipeline::set_media_params( - const utility::Uuid &source_uuid, const MediaParams &new_media_param) const { - media_params_[source_uuid] = new_media_param; +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( @@ -845,8 +878,11 @@ OCIO::ConstProcessorRcPtr OCIOColourPipeline::make_display_processor( return ocio_config->getProcessor(context, group, OCIO::TRANSFORM_DIR_FORWARD); } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to construct OCIO processor: {}", e.what()); - spdlog::warn("OCIOColourPipeline: Defaulting to no-op processor"); + 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()); } } @@ -874,7 +910,7 @@ OCIO::ConstConfigRcPtr OCIOColourPipeline::make_dynamic_display_processor( auto primary = grading_primary_from_cdl(cdl_transform); updated_media_params.primary = primary; - set_media_params(media_param.source_uuid, updated_media_params); + set_media_params(updated_media_params); // Create a dynamic version of the look auto dynamic_config = config->createEditableCopy(); @@ -910,7 +946,7 @@ OCIO::ConstConfigRcPtr OCIOColourPipeline::make_dynamic_display_processor( return dynamic_config; - } catch (const OCIO::Exception &ex) { + } catch ([[maybe_unused]] const OCIO::Exception &ex) { group->appendTransform(display_transform( working_space(media_param), display, view, OCIO::TRANSFORM_DIR_FORWARD)); @@ -1005,8 +1041,8 @@ void OCIOColourPipeline::setup_textures( : 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; @@ -1111,7 +1147,7 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( PLUGIN_UUID, "OCIOColourPipeline", - plugin_manager::PluginType::PT_COLOUR_MANAGEMENT, + plugin_manager::PluginFlags::PF_COLOUR_MANAGEMENT, false, "xStudio", "OCIO (v2) Colour Pipeline", diff --git a/src/plugin/colour_pipeline/ocio/src/ocio.hpp b/src/plugin/colour_pipeline/ocio/src/ocio.hpp index 63236cd0b..3625024f6 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio.hpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio.hpp @@ -60,6 +60,8 @@ class OCIOColourPipeline : public ColourPipeline { 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( @@ -84,10 +86,8 @@ class OCIOColourPipeline : public ColourPipeline { const utility::JsonStore &media_source_colour_metadata) override; // Update colour pipeline shader dynamic parameters. - void update_shader_uniforms( - utility::JsonStore &uniforms, - const utility::Uuid &source_uuid, - std::any &user_data) override; + 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; @@ -108,9 +108,9 @@ class OCIOColourPipeline : public ColourPipeline { void register_hotkeys() override; void connect_to_viewport( - caf::actor viewport, - const std::string viewport_name, - const int viewport_index) override; + 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; @@ -129,11 +129,13 @@ class OCIOColourPipeline : public ColourPipeline { const utility::Uuid &source_uuid, const utility::JsonStore &colour_params = utility::JsonStore()) const; - void - set_media_params(const utility::Uuid &source_uuid, const MediaParams &media_param) 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; @@ -210,6 +212,8 @@ class OCIOColourPipeline : public ColourPipeline { 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); @@ -233,6 +237,7 @@ class OCIOColourPipeline : public ColourPipeline { module::BooleanAttribute *colour_bypass_; module::StringChoiceAttribute *preferred_view_; module::BooleanAttribute *global_view_; + module::BooleanAttribute *adjust_source_; module::BooleanAttribute *enable_gamma_; module::BooleanAttribute *enable_saturation_; @@ -249,7 +254,6 @@ class OCIOColourPipeline : public ColourPipeline { // Holds data on display screen option std::string monitor_name_; - std::string viewport_name_; // Pixel probe std::string last_pixel_probe_source_hash_; diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp index 675e642f2..220c7107e 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp @@ -54,7 +54,21 @@ void OCIOColourPipeline::media_source_changed( // Update the per media assigned view if (!global_view_->value()) { - view_->set_value(new_media_param.output_view); + // 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()); } } @@ -65,13 +79,17 @@ void OCIOColourPipeline::attribute_changed( if (attribute_uuid == display_->uuid()) { update_views(media_param.ocio_config); - } else if ( - attribute_uuid == view_->uuid() && !view_->value().empty() && !global_view_->value()) { - media_param.output_view = view_->value(); - set_media_params(current_source_uuid_, media_param); + } 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(current_source_uuid_, media_param); + set_media_params(media_param); } else if (attribute_uuid == colour_bypass_->uuid()) { update_bypass(display_, colour_bypass_->value()); } else if ( @@ -82,39 +100,19 @@ void OCIOColourPipeline::attribute_changed( } 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() && connected_to_ui()) { - - if (enable_gamma_->value()) { - gamma_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name_ + "_toolbar", "colour_pipe_attributes"}); + } else if (attribute_uuid == enable_gamma_->uuid()) { - } else { - gamma_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - } + make_attribute_visible_in_viewport_toolbar(gamma_, enable_gamma_->value()); - } else if (attribute_uuid == enable_saturation_->uuid() && connected_to_ui()) { - if (enable_saturation_->value()) { - saturation_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name_ + "_toolbar", "colour_pipe_attributes"}); + } else if (attribute_uuid == enable_saturation_->uuid()) { - } else { - saturation_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - } + make_attribute_visible_in_viewport_toolbar(saturation_, enable_saturation_->value()); } } void OCIOColourPipeline::hotkey_pressed( const utility::Uuid &hotkey_uuid, const std::string &context) { - // if the hotkey was pressed outside the viewport that owns this - // instance of the pipeline, skip it. - if (viewport_name_ != context) - 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); @@ -241,33 +239,14 @@ void OCIOColourPipeline::screen_changed( } void OCIOColourPipeline::connect_to_viewport( - caf::actor viewport, const std::string viewport_name, const int viewport_index) { + const std::string &viewport_name, const std::string &viewport_toolbar_name, bool connect) { - viewport_name_ = viewport_name; + Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect); - // make the 'display' button appear in the toolbar for the given viewport - display_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); - view_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); - channel_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); - exposure_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); - - if (enable_saturation_->value()) { - saturation_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); - } - if (enable_gamma_->value()) { - gamma_->set_role_data( - module::Attribute::Groups, - nlohmann::json{viewport_name + "_toolbar", "colour_pipe_attributes"}); + 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( @@ -277,7 +256,7 @@ void OCIOColourPipeline::connect_to_viewport( add_multichoice_attr_to_menu(channel_, viewport_name + "_context_menu_section1", "Channel"); - if (viewport_index == 0) { + if (viewport_name == "viewport0") { add_multichoice_attr_to_menu(view_, "Colour", "OCIO View"); @@ -289,6 +268,8 @@ void OCIOColourPipeline::connect_to_viewport( 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"); @@ -297,7 +278,7 @@ void OCIOColourPipeline::connect_to_viewport( add_boolean_attr_to_menu(enable_gamma_, "panels_menu|Toolbar"); - } else if (viewport_index == 1) { + } else if (viewport_name == "viewport1") { add_multichoice_attr_to_menu(display_, "Colour", "OCIO Pop-Out Viewer Display"); } @@ -428,8 +409,27 @@ void OCIOColourPipeline::setup_ui() { 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 @@ -437,22 +437,20 @@ void OCIOColourPipeline::setup_ui() { // 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() { - // don't register hotkeys again (for additional viewports) - if (viewport_name_ != "viewport0") - return; - for (const auto &hotkey_props : ui_text_.channel_hotkeys) { auto hotkey_id = register_hotkey( hotkey_props.key, @@ -536,11 +534,38 @@ void OCIOColourPipeline::populate_ui(const MediaParams &media_param) { 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. @@ -583,6 +608,20 @@ OCIOColourPipeline::parse_all_colourspaces(OCIO::ConstConfigRcPtr ocio_config) c 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()) diff --git a/src/plugin/colour_pipeline/ocio/src/shaders.hpp b/src/plugin/colour_pipeline/ocio/src/shaders.hpp index 085c86ecf..880104508 100644 --- a/src/plugin/colour_pipeline/ocio/src/shaders.hpp +++ b/src/plugin/colour_pipeline/ocio/src/shaders.hpp @@ -12,7 +12,7 @@ uniform float saturation; //OCIODisplay -vec4 colour_transform_op(vec4 rgba) +vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { rgba = OCIODisplay(rgba); @@ -44,7 +44,7 @@ vec4 colour_transform_op(vec4 rgba) //OCIOLinearise -vec4 colour_transform_op(vec4 rgba) +vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { return OCIOLinearise(rgba); } diff --git a/src/plugin/colour_pipeline/ocio/src/ui_text.hpp b/src/plugin/colour_pipeline/ocio/src/ui_text.hpp index 669f9b24c..eaedb5412 100644 --- a/src/plugin/colour_pipeline/ocio/src/ui_text.hpp +++ b/src/plugin/colour_pipeline/ocio/src/ui_text.hpp @@ -81,6 +81,8 @@ struct UiText { std::string PREF_VIEW = "Preferred View"; std::string VIEW_MODE = "Global View Control"; std::string GLOBAL_VIEW_SHORT = "Global View"; + std::string SOURCE_CS_MODE = "Auto adjust source"; + std::string SOURCE_CS_MODE_SHORT = "Adjust source"; std::string DEFAULT_VIEW = "Default"; @@ -145,6 +147,8 @@ struct UiText { std::string PREF_VIEW_TOOLTIP = "Set preferred view"; std::string GLOBAL_VIEW_TOOLTIP = "Enable global view to affect every loaded media when changing the OCIO view."; + std::string SOURCE_CS_MODE_TOOLTIP = + "Automatically use the most appropriate source colour space for the selected view."; std::vector OCIO_LOAD_ERROR = {"Error could not load OCIO config"}; }; diff --git a/src/plugin/conform/CMakeLists.txt b/src/plugin/conform/CMakeLists.txt new file mode 100644 index 000000000..868b3dbf1 --- /dev/null +++ b/src/plugin/conform/CMakeLists.txt @@ -0,0 +1 @@ +build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt b/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt new file mode 100644 index 000000000..2e8036d48 --- /dev/null +++ b/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..5114daff7 --- /dev/null +++ b/src/plugin/conform/dneg/shotgun/src/conform_shotgun.cpp @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 000000000..a73825679 --- /dev/null +++ b/src/plugin/conform/dneg/shotgun/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + caf::core +) + +create_tests("${LINK_DEPS}") 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 03f3c3cfd..4eed4fee4 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 @@ -21,11 +21,12 @@ using namespace std::chrono_literals; namespace fs = std::filesystem; -const auto GetShotFromIdJSON = R"({"shot_id": null, "operation": "GetShotFromId"})"_json; +const auto GetShotFromId = R"({"shot_id": null, "operation": "GetShotFromId"})"_json; 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 GetVersionIvyUuid = + R"({"operation": "VersionIvyUuid", "job":null, "ivy_uuid": null})"_json; class IvyMediaWorker : public caf::event_based_actor { public: @@ -641,12 +642,11 @@ void IvyMediaWorker::get_shotgun_version( }, [=](const error &err) mutable { // get from shotgun.. - request( - shotgun_actor, - infinite, - data_source::use_data_atom_v, - project, - stalk_dnuuid) + auto jsre = JsonStore(GetVersionIvyUuid); + jsre["ivy_uuid"] = to_string(stalk_dnuuid); + jsre["job"] = project; + + request(shotgun_actor, infinite, data_source::get_data_atom_v, jsre) .then( [=](const JsonStore &jsn) mutable { if (jsn.count("payload")) { @@ -711,7 +711,7 @@ void IvyMediaWorker::get_shotgun_shot( [=](const error &err) mutable { // get from shotgun.. try { - auto shotreq = JsonStore(GetShotFromIdJSON); + auto shotreq = JsonStore(GetShotFromId); shotreq["shot_id"] = shot_id; request(shotgun_actor, infinite, get_data_atom_v, shotreq) 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 index 199325d7b..3896e9335 100644 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.cpp +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include "data_source_shotgun.tcc" #include #include @@ -3398,7 +3399,6 @@ void ShotgunDataSourceActor::do_add_media_sources_from_ivy( }); } - extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { return new plugin_manager::PluginFactoryCollection( 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 index 4836a1cd0..35b6556f1 100644 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.hpp +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.hpp @@ -9,203 +9,15 @@ #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; -const auto UpdatePlaylistJSON = - R"({"entity":"Playlist", "relationship": "Version", "playlist_uuid": null})"_json; -const auto CreatePlaylistJSON = - R"({"entity":"Playlist", "playlist_uuid": null, "project_id": null, "code": null, "location": null, "playlist_type": "Dailies"})"_json; -const auto LoadPlaylistJSON = R"({"entity":"Playlist", "playlist_id": 0})"_json; -const auto GetPlaylistValidMediaJSON = - R"({"playlist_uuid": null, "operation": "MediaCount"})"_json; -const auto GetPlaylistLinkMediaJSON = - R"({"playlist_uuid": null, "operation": "LinkMedia"})"_json; - -const auto DownloadMediaJSON = R"({"media_uuid": null, "operation": "DownloadMedia"})"_json; - -const auto GetVersionIvyUuidJSON = - R"({"job":null, "ivy_uuid": null, "operation": "VersionFromIvy"})"_json; -const auto GetShotFromIdJSON = R"({"shot_id": null, "operation": "GetShotFromId"})"_json; -const auto RefreshPlaylistJSON = - R"({"entity":"Playlist", "relationship": "Version", "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 PreparePlaylistNotesJSON = 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 CreatePlaylistNotesJSON = - R"({"entity":"Note", "playlist_uuid": null, "payload": []})"_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", - "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 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"); - 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(); -}; - class BuildPlaylistMediaJob; template class ShotgunDataSourceActor : public caf::event_based_actor { @@ -232,6 +44,26 @@ template class ShotgunDataSourceActor : public caf::event_based_act 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, @@ -286,6 +118,32 @@ template class ShotgunDataSourceActor : public caf::event_based_act 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_; @@ -302,6 +160,8 @@ template class ShotgunDataSourceActor : public caf::event_based_act 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 new file mode 100644 index 000000000..d5f87bf20 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.tcc @@ -0,0 +1,1188 @@ + +#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 new file mode 100644 index 000000000..7d4ec2fb8 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_action.tcc @@ -0,0 +1,292 @@ +// 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 new file mode 100644 index 000000000..137a7ab53 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.cpp @@ -0,0 +1,179 @@ +// 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 new file mode 100644 index 000000000..9ca44a1fe --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.hpp @@ -0,0 +1,88 @@ +// 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 new file mode 100644 index 000000000..637c16fa9 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_definitions.hpp @@ -0,0 +1,391 @@ +// 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 new file mode 100644 index 000000000..d0648d03c --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_get_actions.tcc @@ -0,0 +1,787 @@ +// 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 new file mode 100644 index 000000000..969e1f8be --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_post_actions.tcc @@ -0,0 +1,498 @@ +// 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 new file mode 100644 index 000000000..d2c67ca05 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_put_actions.tcc @@ -0,0 +1,157 @@ +// 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 new file mode 100644 index 000000000..701256d29 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.cpp @@ -0,0 +1,229 @@ +// 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 new file mode 100644 index 000000000..dcce88255 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.hpp @@ -0,0 +1,72 @@ +// 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 new file mode 100644 index 000000000..4a8dd4bc6 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp @@ -0,0 +1,259 @@ +// 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 new file mode 100644 index 000000000..a95ec5ab4 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.hpp @@ -0,0 +1,135 @@ +// 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 index 4154bde1a..b3217bcc6 100644 --- a/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt +++ b/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt @@ -9,9 +9,10 @@ configure_file(.clang-tidy .clang-tidy) # find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL QUIET) # QT5_ADD_RESOURCES(PROTOTYPE_RCS) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic") -set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") - +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) @@ -21,6 +22,9 @@ QT5_WRAP_CPP(SHOTGUN_MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/shotgun_model 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} ) 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 index 6008c0a91..d28f85867 100644 --- 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 @@ -38,6 +38,7 @@ DelegateChoice { id: mArea anchors.fill: parent hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { searchResultsDiv.itemClicked(mouse, index, isSelected) 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 index c8696b03b..365592add 100644 --- 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 @@ -68,7 +68,7 @@ DelegateChoice { anchors.fill: parent anchors.margins: framePadding rows: 2 - columns: 7 + columns: 8 rowSpacing: itemSpacing Rectangle{ id: indicators @@ -212,50 +212,11 @@ DelegateChoice { } } - // XsButton{ id: versionsButton - // Layout.preferredWidth: pipeStatusDisplay.width - // Layout.preferredHeight: parent.height - // Layout.rowSpan: 2 - - // text: "History" - // 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: { - // if(roleValue=="Reference") currentCategory = "Versions" - // rightDiv.popupMenuAction("Related Versions", index) //createPresetType("Live Versions") - // } - // } - - // XsButton{ id: allVersionsButton - // Layout.preferredWidth: pipeStatusDisplay.width - // Layout.preferredHeight: parent.height - // Layout.rowSpan: 2 - - // text: "Latest" - // 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("Latest Versions", index) - // } - XsTextButton{ id: nameDisplay text: " "+nameRole isClickable: false onTextClicked: createPreset("Twig Name", twigNameRole) - Layout.columnSpan: 3 + Layout.columnSpan: stepDisplay.visible? 3 : 5 Layout.alignment: Qt.AlignLeft Layout.fillWidth: true @@ -272,6 +233,7 @@ DelegateChoice { } XsTextButton{ id: stepDisplay + visible: text != "" text: pipelineStepRole ? pipelineStepRole : "" isClickable: false textDiv.font.pixelSize: fontSize*1.2 @@ -285,7 +247,7 @@ DelegateChoice { Layout.minimumWidth: 65 Layout.preferredWidth: 70 Layout.maximumWidth: 92 - Layout.columnSpan: 1 + Layout.columnSpan: 2 ToolTip.text: text ToolTip.visible: hovered && textDiv.truncated @@ -314,7 +276,7 @@ DelegateChoice { model: siteModel //["chn","lon","mtl","mum","syd",van"] XsButton{ id: onDiskDisplay - property bool onDisk: { + property int onDisk: { if(index==0) onSiteChn else if(index==1) onSiteLon else if(index==2) onSiteMtl @@ -331,18 +293,18 @@ DelegateChoice { focus: false enabled: false borderWidth: 0 - bgColorNormal: onDisk ? siteColour : palette.base + 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:"chn"; siteColour:"#508f00"} + ListElement{siteName:"lon"; siteColour:"#2b7ffc"} + ListElement{siteName:"mtl"; siteColour:"#979700"} ListElement{siteName:"mum"; siteColour:"#ef9526"} - ListElement{siteName:"syd"; siteColour:"#008a46"} //"#7f082f"} - ListElement{siteName:"van"; siteColour:"#7a1a39"} //"#7f082f"} + ListElement{siteName:"syd"; siteColour:"#008a46"} + ListElement{siteName:"van"; siteColour:"#7a1a39"} } } @@ -417,6 +379,23 @@ DelegateChoice { 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 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 index 8e6e61be2..be88207bf 100644 --- 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 @@ -253,7 +253,7 @@ DelegateChoice { text: " "+nameRole isClickable: false onTextClicked: createPreset("Twig Name", twigNameRole) - Layout.columnSpan: 3 + Layout.columnSpan: stepDisplay.visible? 3 : 4 Layout.alignment: Qt.AlignLeft Layout.fillWidth: true @@ -270,6 +270,7 @@ DelegateChoice { } XsTextButton{ id: stepDisplay + visible: text != "" text: pipelineStepRole ? pipelineStepRole : "" isClickable: false textDiv.font.pixelSize: fontSize*1.2 @@ -312,7 +313,7 @@ DelegateChoice { model: siteModel //["chn","lon","mtl","mum","syd",van"] XsButton{ id: onDiskDisplay - property bool onDisk: { + property int onDisk: { if(index==0) onSiteChn else if(index==1) onSiteLon else if(index==2) onSiteMtl @@ -329,7 +330,7 @@ DelegateChoice { focus: false enabled: false borderWidth: 0 - bgColorNormal: onDisk ? siteColour : palette.base + bgColorNormal: onDisk ? Qt.darker(siteColour, onDisk == 1 ? 1.5:1.0) : palette.base textDiv.topPadding: 2 } } @@ -350,7 +351,7 @@ DelegateChoice { Text{ id: dateDisplay Layout.alignment: Qt.AlignLeft - property var dateFormatted: createdDateRole.toLocaleString().split(" ") + 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 @@ -371,7 +372,7 @@ DelegateChoice { hoverEnabled: true propagateComposedEvents: true } - + } // Component.onCompleted: { // console.log("############# locale", createdDateRole.toLocaleString(Qt.locale(),{dateSty;e:"medium"})) 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 index 16e354799..2f510c922 100644 --- 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 @@ -32,7 +32,11 @@ Rectangle{ id: section } function selectItem(index) { - itemExpandedModel.select(index.parent, ItemSelectionModel.Select) + 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); } 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 index 7cc18c3f2..c155d46ac 100644 --- 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 @@ -309,10 +309,8 @@ ListView{ bgColorEditable: isEnabled && liveLink.isActive? Qt.darker(palette.highlight, 2) : "light grey" onArgroleChanged: { - // console.log("onArgroleChanged") - //special handling.. - if((model == dummyModel || model == sourceModel) && argrole != ""){ + if((model == dummyModel || model == sourceModel|| termRole == "Reference Tags") && argrole != ""){ if(find(argrole) === -1) { let tmp = argrole model.append({nameRole: tmp}) @@ -366,6 +364,8 @@ ListView{ 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" } @@ -446,6 +446,8 @@ ListView{ 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 @@ -473,7 +475,7 @@ ListView{ argRole = data_source.getShotgunUserName() } - if(valueBox.currentIndex == -1 && !(valueBox.model == dummyModel || valueBox.model == sourceModel) && !liveLink.isActive && queryList.expanded) { + if(valueBox.currentIndex == -1 && !(valueBox.model == dummyModel || valueBox.model == sourceModel|| termRole == "Reference Tags") && !liveLink.isActive && queryList.expanded) { // trigger selection.. valueBox.popupOptions.open() } @@ -492,7 +494,7 @@ ListView{ onAccepted: { // special handling for Name text - if((model == dummyModel || model == sourceModel) && find(editText) === -1) { + if((model == dummyModel || model == sourceModel || termRole == "Reference Tags") && find(editText) === -1) { if(editText != "") { model.append({nameRole: editText}) } @@ -502,7 +504,7 @@ ListView{ } Component.onCompleted: { - if(!(model == dummyModel || model == sourceModel)) + if(!(model == dummyModel || model == sourceModel || termRole == "Reference Tags")) updateIndex.start() else { model.append({nameRole: argRole}) @@ -513,16 +515,33 @@ ListView{ onFocusChanged: if(!focus) accepted() function doUpdate() { - // console.log("doUpdate") - if(currentText !== "" && argRole != currentText) - argRole = currentText + // 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() + onActivated: { + doUpdate() + } } XsButton{id: deleteButton @@ -605,14 +624,14 @@ ListView{ 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)) { + 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)", "Twig Name", "Completion Location", "On Disk"].includes(selectField.currentText)) { + "Tag (Version)", "Reference Tag", "Reference Tags", "Twig Name", "Completion Location", "On Disk"].includes(selectField.currentText)) { term["negated"] = false } 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 index 63e66e082..842d86ffc 100644 --- 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 @@ -36,6 +36,7 @@ Rectangle{ id: leftDiv 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 @@ -126,6 +127,8 @@ Rectangle{ id: leftDiv "Preferred Audio", "Preferred Visual", "Production Status", +"Reference Tag", +"Reference Tags", "Result Limit", "Sent To Client", "Sent To Dailies", @@ -157,6 +160,8 @@ Rectangle{ id: leftDiv "Preferred Audio", "Preferred Visual", "Production Status", +"Reference Tag", +"Reference Tags", "Result Limit", "Sent To Client", "Sent To Dailies", @@ -206,6 +211,8 @@ Rectangle{ id: leftDiv "Preferred Audio", "Preferred Visual", "Production Status", +"Reference Tag", +"Reference Tags", "Result Limit", "Sent To Client", "Sent To Dailies", @@ -225,7 +232,9 @@ Rectangle{ id: leftDiv "Filter", "Flag Media", "Lookback", +"Newer Version", "Note Type", +"Older Version", "Order By", "Pipeline Step", "Playlist", @@ -603,7 +612,7 @@ Rectangle{ id: leftDiv if(live == undefined) live = false - if(term != term_type && !live) { + if(term != term_type ) { //&& !live searchTreePresetsViewModel.set(0, term_type, "termRole", index); searchTreePresetsViewModel.set(0, term_value, "argRole", index); if(i == row) { @@ -1191,12 +1200,19 @@ Rectangle{ id: leftDiv Connections { target: searchTreePresetsViewModel - function onActiveSeqShotChanged() { + function onActiveShotChanged() { if(treeMode) { - let index = sequenceTreeModel.search_recursive(searchTreePresetsViewModel.activeSeqShot, "nameRole") - treeTab.selectItem(index) + 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 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 index 11fce2f20..28aec25d2 100644 --- 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 @@ -19,6 +19,9 @@ import xStudio 1.1 Rectangle{ id: rightDiv property var searchResultsViewModel + onSearchResultsViewModelChanged:{ + searchResultsViewModel.setFilterWildcard(filterTextField.text) + } property var currentPresetIndex: -1 @@ -108,7 +111,7 @@ Rectangle{ id: rightDiv searchResultsViewModel.sortRoleName = "shotRole" } else if(actionText == "Creation Date") { searchResultsViewModel.sortRoleName = "createdDateRole" - } else if(actionText == "Reveal In Shotgun") { + } else if(actionText == "Reveal In ShotGrid") { // get selection.. let i = selectionModel.selectedIndexes[0] helpers.openURL(searchResultsViewModel.get(i.row,"URLRole")) @@ -764,7 +767,7 @@ Rectangle{ id: rightDiv XsMenuSeparator {} XsMenuItem { - mytext: "Reveal In Shotgun"; onTriggered: popupMenuAction(text) + mytext: "Reveal In ShotGrid"; onTriggered: popupMenuAction(text) enabled: selectionModel.selectedIndexes.length } XsMenuItem { 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 index 06658e435..2f01d2568 100644 --- 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 @@ -10,7 +10,7 @@ import xstudio.qml.module 1.0 XsDialogModal { id: dlg property string message: "" - title: "Shotgun Authentication" + (message ? " - "+message:"") + title: "ShotGrid Authentication" + (message ? " - "+message:"") width: 300 height: 200 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 index 3e3638624..5fb516969 100644 --- 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 @@ -31,6 +31,7 @@ XsWindow { id: shotgunBrowser 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 @@ -657,6 +658,7 @@ XsWindow { id: shotgunBrowser primaryLocationModel: shotgunBrowser.primaryLocationModel orderByModel: shotgunBrowser.orderByModel resultLimitModel: shotgunBrowser.resultLimitModel + referenceTagModel: shotgunBrowser.referenceTagModel reviewLocationModel: shotgunBrowser.reviewLocationModel boolModel: shotgunBrowser.boolModel lookbackModel: shotgunBrowser.lookbackModel 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 index e3a0dfc03..602b586ed 100644 --- 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 @@ -9,7 +9,7 @@ import xstudio.qml.module 1.0 XsDialogModal { id: dialog - title: "Create Shotgun Playlist" + title: "Create ShotGrid Playlist" property var playlist_uuid: null property int validMediaCount: 0 @@ -144,7 +144,7 @@ XsDialogModal { } } XsLabel { - text: "Valid Shotgun Media : " + text: "Valid ShotGrid Media : " Layout.alignment: Qt.AlignVCenter|Qt.AlignRight } XsLabel { 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 index d0ad9737b..fa0c0e3fb 100644 --- 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 @@ -19,12 +19,12 @@ function handle_response(result_string, title, only_on_error=true, body = "", di } if("error" in data) { - throw "Shotgun error." + throw "ShotGrid error." } if("errors" in data) { if(data["errors"][0]["status"] !== null) - throw "Shotgun error." + throw "ShotGrid error." } // if("status" in data["data"] && data["data"]["status"] !== null && data["data"]["status"] !== "success" ) { 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 index 483c8ced8..aaf06576a 100644 --- 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 @@ -2,20 +2,20 @@ import xStudio 1.0 XsMenu { - title: qsTr("Shotgun Playlists") + title: qsTr("ShotGrid Playlists") XsMenuItem { - mytext: qsTr("Create Selected Shotgun Playlists...") + mytext: qsTr("Create Selected ShotGrid Playlists...") onTriggered: sessionFunction.object_map["ShotgunRoot"].create_playlist() } XsMenuItem { - mytext: qsTr("Update Selected Shotgun Playlists") + mytext: qsTr("Update Selected ShotGrid Playlists") onTriggered: sessionFunction.object_map["ShotgunRoot"].update_playlist() } XsMenuItem { - mytext: qsTr("Refresh Selected Shotgun Playlists") + mytext: qsTr("Refresh Selected ShotGrid Playlists") onTriggered: sessionFunction.object_map["ShotgunRoot"].refresh_playlist() } 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 index 6630d9c44..bc469abb4 100644 --- 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 @@ -9,6 +9,6 @@ import xstudio.qml.module 1.0 XsDialogModal { id: dlg - title: "Shotgun Preferences" + 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 index b7b5c3ce7..c4d548109 100644 --- 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 @@ -118,11 +118,15 @@ XsWindow { function getNotifyGroups() { let result = [] + let email_group_names = [] if(notify_group_cb.checked) { for(let i =0;i +// #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 new file mode 100644 index 000000000..4f283dc28 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_requests_ui.cpp @@ -0,0 +1,1012 @@ +// 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 index 49fcef922..02a0a5808 100644 --- 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 @@ -1,7 +1,10 @@ // 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" @@ -20,8 +23,6 @@ using namespace xstudio::ui::qml; using namespace std::chrono_literals; using namespace xstudio::global_store; -auto SHOTGUN_TIMEOUT = 120s; - const auto PresetModelLookup = std::map( {{"edit", "editPresetsModel"}, {"edit_filter", "editFilterModel"}, @@ -55,196 +56,6 @@ const auto PresetPreferenceLookup = std::map( {"shot_tree", "presets/shot_tree"}}); -const auto TwigTypeCodes = 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); - ShotgunDataSourceUI::ShotgunDataSourceUI(QObject *parent) : QMLActor(parent) { term_models_ = new QQmlPropertyMap(this); @@ -254,15 +65,7 @@ ShotgunDataSourceUI::ShotgunDataSourceUI(QObject *parent) : QMLActor(parent) { term_models_->insert( "primaryLocationModel", QVariant::fromValue(new ShotgunListModel(this))); qvariant_cast(term_models_->value("primaryLocationModel")) - ->populate(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); - + ->populate(locationsJSON); term_models_->insert("stepModel", QVariant::fromValue(new ShotgunListModel(this))); qvariant_cast(term_models_->value("stepModel"))->populate(R"([ @@ -343,6 +146,7 @@ ShotgunDataSourceUI::ShotgunDataSourceUI(QObject *parent) : QMLActor(parent) { 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( @@ -607,140 +411,6 @@ void ShotgunDataSourceUI::syncModelChanges(const QString &preset) { } } -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()); - } - - // we need to preprocess for Disable Global flags.. - auto disable_globals = std::set(); - for (const auto &i : dst_qry["queries"]) { - 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 : dst_qry["queries"]) - if (i.at("enabled").get()) - dup.insert(i.at("term").get()); - - for (const auto &i : src_qry.at("queries")) { - auto term = i.at("term").get(); - if (not dup.count(term) and not disable_globals.count(term)) - dst_qry["queries"].push_back(i); - } - } else { - for (const auto &i : src_qry.at("queries")) { - auto term = i.at("term").get(); - if (not disable_globals.count(term)) - dst_qry["queries"].push_back(i); - } - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return QVariantMapFromJson(dst_qry); -} void ShotgunDataSourceUI::setLiveLinkMetadata(QString &data) { if (data == "null") @@ -802,787 +472,6 @@ void ShotgunDataSourceUI::setLiveLinkMetadata(QString &data) { } } -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. - 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()) { - JsonStore data; - auto model_update = JsonStore( - R"({ - "type": null, - "epoc": null, - "audio_source": "", - "visual_source": "", - "flag_text": "", - "flag_colour": "" - })"_json); - - model_update["epoc"] = utility::to_epoc_milliseconds(utility::clock::now()); - - if (context == "Playlists") - model_update["type"] = "playlist_result"; - else if (context == "Versions") - model_update["type"] = "shot_result"; - else if (context == "Reference") - model_update["type"] = "reference_result"; - else if (context == "Versions Tree") - model_update["type"] = "shot_tree_result"; - else if (context == "Menu Setup") - model_update["type"] = "media_action_result"; - else if (context == "Notes") - model_update["type"] = "note_result"; - else if (context == "Notes Tree") - model_update["type"] = "note_tree_result"; - - try { - scoped_actor sys{system()}; - - const auto &[filter, orderby, max_count, source_selection, flag_selection] = - buildQuery(cxt, project_id, qry); - - model_update["visual_source"] = source_selection.first; - model_update["audio_source"] = source_selection.second; - model_update["flag_text"] = flag_selection.first; - model_update["flag_colour"] = flag_selection.second; - model_update["truncated"] = false; - - if (context == "Playlists") { - data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Playlists", - filter, - std::vector( - {"code", - "versions", - "sg_location", - "updated_at", - "created_at", - "sg_date_and_time", - "sg_type", - "created_by", - "sg_department_unit", - "notes"}), - orderby, - 1, - max_count); - } else if ( - context == "Versions" or context == "Versions Tree" or - context == "Reference" or context == "Menu Setup") { - data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Versions", - filter, - VersionFields, - orderby, - 1, - max_count); - - } else if (context == "Notes" or context == "Notes Tree") { - data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Notes", - filter, - 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"}), - orderby, - 1, - max_count); - } - - if (static_cast(data.at("data").size()) == max_count) - model_update["truncated"] = true; - data["preferred_visual_source"] = source_selection.first; - data["preferred_audio_source"] = source_selection.second; - data["flag_text"] = flag_selection.first; - data["flag_colour"] = flag_selection.second; - - if (update_result_model) - anon_send(as_actor(), shotgun_info_atom_v, model_update, 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, - model_update, - JsonStore(R"({"data":[]})"_json)); - - if (starts_with(std::string(err.what()), "LiveLink ")) { - return QStringFromStd(R"({"data":[]})"); - } - - return QStringFromStd(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 ( - 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 == "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 &) { - // 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); -} - QString ShotgunDataSourceUI::getShotgunUserName() { QString result; // = QString(get_user_name()); @@ -1796,25 +685,23 @@ void ShotgunDataSourceUI::loadPresets(const bool purge_old) { } void ShotgunDataSourceUI::handleResult( - const JsonStore &request, - const JsonStore &data, - const std::string &model, - const std::string &name) { - if (not epoc_map_.count(request.at("type")) or - epoc_map_.at(request.at("type")) < request.at("epoc")) { + 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(data.at("data")); - slm->setTruncated(request.at("truncated").get()); + slm->populate(request.at("result").at("data")); + slm->setTruncated(request.at("context").at("truncated").get()); source_selection_[name] = std::make_pair( - request.at("visual_source").get(), - request.at("audio_source").get()); + request.at("context").at("visual_source").get(), + request.at("context").at("audio_source").get()); flag_selection_[name] = std::make_pair( - request.at("flag_text").get(), - request.at("flag_colour").get()); - epoc_map_[request.at("type")] = request.at("epoc"); + 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"); } } @@ -1856,6 +743,10 @@ void ShotgunDataSourceUI::init(caf::actor_system &system) { 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")); @@ -1897,30 +788,40 @@ void ShotgunDataSourceUI::init(caf::actor_system &system) { playlists_map_[request.at("id")]->populate(data.at("data")); updateQueryValueCache( "Playlist", data.at("data"), request.at("id").get()); - } else if (request.at("type") == "playlist_result") { - handleResult(request, data, "playlistResultsBaseModel", "Playlists"); - } else if (request.at("type") == "shot_result") { - handleResult(request, data, "shotResultsBaseModel", "Versions"); - } else if (request.at("type") == "shot_tree_result") { - handleResult( - request, data, "shotTreeResultsBaseModel", "Versions Tree"); - } else if (request.at("type") == "media_action_result") { - handleResult( - request, data, "mediaActionResultsBaseModel", "Menu Setup"); - } else if (request.at("type") == "reference_result") { - handleResult(request, data, "referenceResultsBaseModel", "Reference"); - } else if (request.at("type") == "note_result") { - handleResult(request, data, "noteResultsBaseModel", "Notes"); - } else if (request.at("type") == "note_tree_result") { - handleResult(request, data, "noteTreeResultsBaseModel", "Notes Tree"); - } else if (request.at("type") == "edit_result") { - handleResult(request, data, "editResultsBaseModel", "Edits"); } } 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) @@ -2046,6 +947,7 @@ void ShotgunDataSourceUI::populateCaches() { getProjectsFuture(); getUsersFuture(); getDepartmentsFuture(); + getReferenceTagsFuture(); getSchemaFieldsFuture("playlist", "sg_location", "location"); getSchemaFieldsFuture("playlist", "sg_review_location_1", "review_location"); @@ -2058,150 +960,6 @@ void ShotgunDataSourceUI::populateCaches() { getSchemaFieldsFuture("version", "sg_status_list", "pipeline_status"); } -QFuture ShotgunDataSourceUI::getProjectsFuture() { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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::getSchemaFieldsFuture( - const QString &entity, const QString &field, const QString &update_name) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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(); - }); -} - -// 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]() { - if (backend_) { - try { - 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()); - - } 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(); - }); -} - - JsonStore ShotgunDataSourceUI::buildDataFromField(const JsonStore &data) { auto result = R"({"data": []})"_json; @@ -2229,881 +987,31 @@ JsonStore ShotgunDataSourceUI::buildDataFromField(const JsonStore &data) { 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::getPlaylistLinkMediaFuture(const QUuid &playlist) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(GetPlaylistLinkMediaJSON); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .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::getPlaylistValidMediaCountFuture(const QUuid &playlist) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(GetPlaylistValidMediaJSON); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .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::getGroupsFuture(const int project_id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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::getSequencesFuture(const int project_id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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, - "Sequences", - JsonStore(filter), - std::vector( - {"id", "code", "shots", "type", "sg_parent", "sg_sequence_type"}), - 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__); - - 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()); - } 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::getPlaylistsFuture(const int project_id) { - - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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::getShotsFuture(const int project_id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - - // removed to include all shots. - // ["sg_status_list", "not_in", ["na","del"]] - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}] - ] - })"_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)); - - 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()); - } 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::getUsersFuture() { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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)); - - if (data["data"].size() == 4999) { - 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"}), - 2, - 4999); - - if (data2["data"].size() == 4999) - spdlog::warn("Exceeding user limit.."); - - data["data"].insert( - data["data"].end(), data2["data"].begin(), data2["data"].end()); - } - - anon_send( - as_actor(), - shotgun_info_atom_v, - JsonStore(R"({"type": "user"})"_json), - data); - - return QStringFromStd(data.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::getDepartmentsFuture() { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ] - })"_json; - // ["sg_status_list", "is", "act"] - - // we've got more that 5000 employees.... - 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()); - } 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::getCustomEntity24Future(const int project_id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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(); - }); -} - -QString ShotgunDataSourceUI::addVersionToPlaylist( - const QString &version, const QUuid &playlist, const QUuid &before) { - return addVersionToPlaylistFuture(version, playlist, before).result(); -} - -QFuture ShotgunDataSourceUI::addVersionToPlaylistFuture( - const QString &version, const QUuid &playlist, const QUuid &before) { - - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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()); - } catch (...) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, "Unknown exception."); - return QString(); - } - } - return QString(); - }); -} - -QString ShotgunDataSourceUI::updateEntity( - const QString &entity, const int record_id, const QString &update_json) { - return updateEntityFuture(entity, record_id, update_json).result(); -} - -QFuture ShotgunDataSourceUI::updateEntityFuture( - const QString &entity, const int record_id, const QString &update_json) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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::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(PreparePlaylistNotesJSON); - - 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) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(CreatePlaylistNotesJSON); - 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()); - } 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::createPlaylistFuture( - const QUuid &playlist, - const int project_id, - const QString &name, - const QString &location, - const QString &playlist_type) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(CreatePlaylistJSON); - 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()); - } 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::updatePlaylistVersionsFuture(const QUuid &playlist) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(UpdatePlaylistJSON); - 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()); - } 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(); - }); -} - -// find playlist id from playlist -// request versions from shotgun -// add to playlist. -QFuture ShotgunDataSourceUI::refreshPlaylistVersionsFuture(const QUuid &playlist) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(RefreshPlaylistJSON); - 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::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(); - }); -} - -QString ShotgunDataSourceUI::getPlaylistNotes(const int id) { - return getPlaylistNotesFuture(id).result(); -} - -QFuture ShotgunDataSourceUI::getPlaylistNotesFuture(const int id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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(); - }); -} - -QString ShotgunDataSourceUI::getPlaylistVersions(const int id) { - return getPlaylistVersionsFuture(id).result(); -} - -QFuture ShotgunDataSourceUI::getPlaylistVersionsFuture(const int id) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - 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()); - } 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, @@ -3148,33 +1056,15 @@ QFuture ShotgunDataSourceUI::requestFileTransferFuture( }); } -QFuture ShotgunDataSourceUI::addDownloadToMediaFuture(const QUuid &media) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(DownloadMediaJSON); - req["media_uuid"] = to_string(UuidFromQUuid(media)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .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(); - }); + +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 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 index e1a7bedd1..ea9ffef59 100644 --- 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 @@ -24,6 +24,8 @@ namespace xstudio { using namespace shotgun_client; namespace ui { namespace qml { + using namespace std::chrono_literals; + const auto SHOTGUN_TIMEOUT = 120s; class ShotModel; class ShotgunListModel; @@ -123,19 +125,27 @@ namespace ui { } } + 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); + QString getPlaylistVersions(const int id) { + return getPlaylistVersionsFuture(id).result(); + } QFuture getPlaylistVersionsFuture(const int id); - QString getPlaylistNotes(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()); + 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()); @@ -168,6 +178,37 @@ namespace ui { 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(); @@ -177,7 +218,9 @@ namespace ui { QFuture getCustomEntity24Future(const int project_id); QString updateEntity( - const QString &entity, const int record_id, const QString &update_json); + 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); @@ -197,13 +240,13 @@ namespace ui { } 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 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, @@ -316,6 +359,12 @@ namespace ui { 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, @@ -420,7 +469,6 @@ namespace ui { void handleResult( const utility::JsonStore &request, - const utility::JsonStore &data, const std::string &model, const std::string &name); 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 index 5440cb58d..7f98689ac 100644 --- 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 @@ -17,114 +17,182 @@ 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; - auto changed = false; + auto done = false; while (not done) { - changed = false; - done = true; + done = true; for (auto seq : src) { - auto id = seq.at("id").get(); - // already logged ? - if (not seqs.count(id)) { - auto parent_id = - seq["relationships"]["sg_parent"]["data"]["id"].get(); - // no parent - if (parent_id == id) { - auto &shots = seq["relationships"]["shots"]["data"]; - if (shots.is_array()) - seq["children"] = seq["relationships"]["shots"]["data"]; - else - seq["children"] = R"([])"_json; - - seq["parent_id"] = seq["relationships"]["sg_parent"]["data"]["id"]; - seq["relationships"].erase("shots"); - seq["relationships"].erase("sg_parent"); - result.emplace_back(seq); - seqs.emplace(std::make_pair( - id, - nlohmann::json::json_pointer( - std::string("/") + std::to_string(result.size() - 1)))); - changed = true; - } else if (seqs.count(parent_id)) { - // parent exists - auto parent_pointer = seqs[parent_id]; - - auto &shots = seq["relationships"]["shots"]["data"]; - if (shots.is_array()) - seq["children"] = seq["relationships"]["shots"]["data"]; - else - seq["children"] = R"([])"_json; - - seq["parent_id"] = seq["relationships"]["sg_parent"]["data"]["id"]; - seq["relationships"].erase("shots"); - seq["relationships"].erase("sg_parent"); - - result[parent_pointer]["children"].emplace_back(seq); - // spdlog::warn("{}", result[parent_pointer].dump(2)); - - seqs.emplace(std::make_pair( - id, - parent_pointer / + 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[parent_pointer]["children"].size() - 1)))); - changed = true; - } else { - done = false; + 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()); } } + } - if (not changed) - done = true; + // 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"); - if (done) { - auto count = 0; - // unresolved.. - for (auto unseq : src) { - auto id = unseq.at("id").get(); - // already logged ? - if (not seqs.count(id)) { - 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"]; - else - unseq["children"] = R"([])"_json; - - unseq["parent_id"] = - unseq["relationships"]["sg_parent"]["data"]["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++; - } + 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++; } - if (count) - spdlog::warn("{} unresolved sequences.", 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 (...) { + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + // sort results.. + // spdlog::warn("{}", result.dump(2)); return result; @@ -187,6 +255,9 @@ QVariant ShotgunSequenceModel::data(const QModelIndex &index, int role) const { 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); @@ -202,7 +273,14 @@ bool ShotgunFilterModel::filterAcceptsRow( continue; try { auto qv = sourceModel()->data(index, k).toString(); - if (v != qv) + + 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 (...) { } @@ -306,6 +384,21 @@ utility::JsonStore ShotgunListModel::getQueryValue( 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; @@ -343,7 +436,9 @@ QVariant ShotgunListModel::data(const QModelIndex &index, int role) const { switch (role) { case Roles::nameRole: case Qt::DisplayRole: - if (data.count("name")) + 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( @@ -605,63 +700,57 @@ QVariant ShotModel::data(const QModelIndex &index, int role) const { case Roles::onSiteMum: try { - result = data.at("attributes").at("sg_on_disk_mum") == "Full" or - data.at("attributes").at("sg_on_disk_mum") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_mum") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_mum") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; case Roles::onSiteMtl: try { - result = data.at("attributes").at("sg_on_disk_mtl") == "Full" or - data.at("attributes").at("sg_on_disk_mtl") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_mtl") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_mtl") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; case Roles::onSiteVan: try { - result = data.at("attributes").at("sg_on_disk_van") == "Full" or - data.at("attributes").at("sg_on_disk_van") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_van") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_van") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; case Roles::onSiteChn: try { - result = data.at("attributes").at("sg_on_disk_chn") == "Full" or - data.at("attributes").at("sg_on_disk_chn") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_chn") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_chn") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; case Roles::onSiteLon: try { - result = data.at("attributes").at("sg_on_disk_lon") == "Full" or - data.at("attributes").at("sg_on_disk_lon") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_lon") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_lon") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; case Roles::onSiteSyd: try { - result = data.at("attributes").at("sg_on_disk_syd") == "Full" or - data.at("attributes").at("sg_on_disk_syd") == "Partial" - ? true - : false; + result = data.at("attributes").at("sg_on_disk_syd") == "Full" ? 2 + : data.at("attributes").at("sg_on_disk_syd") == "Partial" ? 1 + : 0; } catch (...) { - result = false; + result = 0; } break; @@ -671,6 +760,16 @@ QVariant ShotModel::data(const QModelIndex &index, int role) const { 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", "")); @@ -703,7 +802,7 @@ QVariant ShotModel::data(const QModelIndex &index, int role) const { break; } } - } catch (const std::exception & /*err*/) { + } catch (const std::exception &err) { // spdlog::warn("{}", err.what()); } @@ -1376,10 +1475,27 @@ bool ShotgunTreeModel::setData(const QModelIndex &index, const QVariant &value, 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_) { @@ -1445,6 +1561,21 @@ int ShotgunTreeModel::getProjectId(const QVariant &livelink) const { 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++) { @@ -1463,95 +1594,71 @@ JsonStore ShotgunTreeModel::applyLiveLink(const JsonStore &preset, const JsonSto JsonStore ShotgunTreeModel::applyLiveLinkValue(const JsonStore &query, const JsonStore &livelink) { - auto term = query["term"]; - auto value = query["value"]; JsonStore result(""); try { - if (livelink.count("metadata") and livelink.at("metadata").count("shotgun") and - livelink.at("metadata").at("shotgun").count("version")) { - if (term == "Version Name") { - result = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("code"); - } else if (term == "Older Version") { - result = nlohmann::json(std::to_string(livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("sg_dneg_version") - .get())); - } else if (term == "Newer Version") { - result = nlohmann::json(std::to_string(livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("sg_dneg_version") - .get())); - } else if (term == "Author" || term == "Recipient") { - result = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("user") - .at("data") - .at("name"); - } else if (term == "Shot") { - result = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("entity") - .at("data") - .at("name"); - } else if (term == "Twig Name") { - result = nlohmann::json( - std::string("^") + - livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("sg_twig_name") - .get() + - std::string("$")); - } else if (term == "Pipeline Step") { - result = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("sg_pipeline_step"); - } else if (term == "Twig Type") { - result = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("attributes") - .at("sg_twig_type"); - } else if (term == "Sequence") { - auto project_id = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - auto shot_id = livelink.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("entity") - .at("data") - .at("id") - .get(); - auto seq_data = getSequence(project_id, shot_id); - result = seq_data.at("attributes").at("code"); + 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()); + spdlog::warn( + "{} {} {} {}", __PRETTY_FUNCTION__, err.what(), query.dump(2), livelink.dump(2)); } + return result; } @@ -1570,9 +1677,14 @@ void ShotgunTreeModel::setActivePreset(const int row) { if (not row->empty()) { auto jsn = row->front().data(); if (jsn.at("term") == "Shot" and jsn.value("livelink", false) and - active_seq_shot_ != QStringFromStd(jsn.at("value"))) { - active_seq_shot_ = QStringFromStd(jsn.at("value")); - emit activeSeqShotChanged(); + 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(); } } } @@ -1585,9 +1697,8 @@ void ShotgunTreeModel::setActivePreset(const int row) { 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 jsn = indexToData(index); + auto value = jsn.at("value"); auto result = applyLiveLinkValue(jsn, live_link_data_); if (result != value) { @@ -1595,16 +1706,9 @@ void ShotgunTreeModel::updateLiveLink(const QModelIndex &index) { QVariant::fromValue(QStringFromStd(result)), QStringFromStd("argRole"), index.parent()); - - if (index.parent().row() == active_preset_ && jsn.at("term") == "Shot") { - - if (active_seq_shot_ != QStringFromStd(result)) { - active_seq_shot_ = QStringFromStd(result); - emit activeSeqShotChanged(); - } - } } - } catch (...) { + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); set(index.row(), QVariant::fromValue(QString("")), QStringFromStd("argRole"), 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 index d61fdc4d5..bde8ff90c 100644 --- 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 @@ -39,6 +39,9 @@ namespace ui { 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 { @@ -51,7 +54,8 @@ namespace ui { Q_PROPERTY(int activePreset READ activePreset WRITE setActivePreset NOTIFY activePresetChanged) - Q_PROPERTY(QString activeSeqShot READ activeSeqShot NOTIFY activeSeqShotChanged) + Q_PROPERTY(QString activeShot READ activeShot NOTIFY activeShotChanged) + Q_PROPERTY(QString activeSeq READ activeSeq NOTIFY activeSeqChanged) public: enum Roles { @@ -106,7 +110,8 @@ namespace ui { [[nodiscard]] int length() const { return rowCount(); } [[nodiscard]] int activePreset() const { return active_preset_; } - [[nodiscard]] QString activeSeqShot() const { return active_seq_shot_; } + [[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; @@ -185,7 +190,8 @@ namespace ui { void hasActiveFilterChanged(); void hasActiveLiveLinkChanged(); void activePresetChanged(); - void activeSeqShotChanged(); + void activeShotChanged(); + void activeSeqChanged(); private: void checkForActiveFilter(); @@ -197,7 +203,8 @@ namespace ui { bool has_active_live_link_{false}; const QMap *sequence_map_{nullptr}; int active_preset_{-1}; - QString active_seq_shot_{}; + QString active_seq_{}; + QString active_shot_{}; }; @@ -254,6 +261,7 @@ namespace ui { stalkUuidRole, subjectRole, submittedToDailiesRole, + tagRole, thumbRole, twigNameRole, twigTypeRole, @@ -309,6 +317,7 @@ namespace ui { {stalkUuidRole, "stalkUuidRole"}, {subjectRole, "subjectRole"}, {submittedToDailiesRole, "submittedToDailiesRole"}, + {tagRole, "tagRole"}, {thumbRole, "thumbRole"}, {twigNameRole, "twigNameRole"}, {twigTypeRole, "twigTypeRole"}, @@ -352,7 +361,8 @@ namespace ui { [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_INVOKABLE void clear() { populate(utility::JsonStore(R"([])"_json)); } - // protected: + + Q_INVOKABLE void append(const QVariant &data); [[nodiscard]] QHash roleNames() const override { QHash roles; diff --git a/src/plugin/hud/exr_data_window/src/CMakeLists.txt b/src/plugin/hud/exr_data_window/src/CMakeLists.txt index 4b7b8021d..8eb7e4950 100644 --- a/src/plugin/hud/exr_data_window/src/CMakeLists.txt +++ b/src/plugin/hud/exr_data_window/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET(LINK_DEPS xstudio::module xstudio::plugin_manager xstudio::ui::opengl::viewport + xstudio::ui::viewport Imath::Imath ) 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 9f4f346f5..b94235271 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 @@ -116,7 +116,7 @@ plugin::ViewportOverlayRendererPtr EXRDataWindowHUD::make_overlay_renderer(const EXRDataWindowHUD::~EXRDataWindowHUD() = default; -utility::BlindDataObjectPtr EXRDataWindowHUD::prepare_render_data( +utility::BlindDataObjectPtr EXRDataWindowHUD::prepare_overlay_data( const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { auto r = utility::BlindDataObjectPtr(); @@ -150,7 +150,7 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("f8a09960-606d-11ed-9b6a-0242ac120002"), "EXRDataWindowHUD", - plugin_manager::PluginType::PT_HEAD_UP_DISPLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY, 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 8f57b8fe9..dc716bd2d 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 @@ -20,7 +20,7 @@ namespace ui { ) override; protected: - utility::BlindDataObjectPtr prepare_render_data( + utility::BlindDataObjectPtr prepare_overlay_data( const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override; diff --git a/src/plugin/hud/image_boundary/src/CMakeLists.txt b/src/plugin/hud/image_boundary/src/CMakeLists.txt index d2ff10454..6bbf57672 100644 --- a/src/plugin/hud/image_boundary/src/CMakeLists.txt +++ b/src/plugin/hud/image_boundary/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET(LINK_DEPS xstudio::module xstudio::plugin_manager xstudio::ui::opengl::viewport + xstudio::ui::viewport Imath::Imath ) 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 148f07e1a..8bcfd0006 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp @@ -112,7 +112,7 @@ plugin::ViewportOverlayRendererPtr ImageBoundaryHUD::make_overlay_renderer(const ImageBoundaryHUD::~ImageBoundaryHUD() = default; -utility::BlindDataObjectPtr ImageBoundaryHUD::prepare_render_data( +utility::BlindDataObjectPtr ImageBoundaryHUD::prepare_overlay_data( const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { auto r = utility::BlindDataObjectPtr(); @@ -146,7 +146,7 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("95268f7c-88d1-48da-8543-c5275ef5b2c5"), "ImageBoundaryHUD", - plugin_manager::PluginType::PT_HEAD_UP_DISPLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY, 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 c2c55ed56..1e1277bf6 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp @@ -20,7 +20,7 @@ namespace ui { ) override; protected: - utility::BlindDataObjectPtr prepare_render_data( + utility::BlindDataObjectPtr prepare_overlay_data( const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override; diff --git a/src/plugin/hud/pixel_probe/src/CMakeLists.txt b/src/plugin/hud/pixel_probe/src/CMakeLists.txt index 6530b57f9..20f3cff4f 100644 --- a/src/plugin/hud/pixel_probe/src/CMakeLists.txt +++ b/src/plugin/hud/pixel_probe/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET(LINK_DEPS xstudio::module xstudio::plugin_manager xstudio::ui::opengl::viewport + xstudio::ui::viewport Imath::Imath ) diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp index 0c85305a8..0bca83786 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp @@ -85,6 +85,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) { + pixel_info_text_ = add_string_attribute("Pixel Info", "Pixel Info", ""); pixel_info_text_->expose_in_ui_attrs_group("pixel_info_attributes"); @@ -167,30 +168,39 @@ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &i value_precision_->set_preference_path("/plugin/pixel_probe/decimals"); } -PixelProbeHUD::~PixelProbeHUD() { colour_pipeline_ = caf::actor(); } +PixelProbeHUD::~PixelProbeHUD() { + colour_pipelines_.clear(); + colour_pipeline_ = caf::actor(); +} bool PixelProbeHUD::pointer_event(const ui::PointerEvent &e) { last_pointer_pos_ = e.position_in_viewport_coord_sys(); - update_onscreen_info(); + update_onscreen_info(e.context()); return true; } -void PixelProbeHUD::update_onscreen_info() { +void PixelProbeHUD::update_onscreen_info(const std::string &viewport_name) { if (is_enabled_ != enabled_->value()) { is_enabled_ = enabled_->value(); if (is_enabled_) { - listen_to_playhead_events(); connect_to_ui(); } else { - listen_to_playhead_events(false); current_onscreen_image_ = media_reader::ImageBufPtr(); - // disconnect_from_ui(); } } + if (is_enabled_ && 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(); + } + colour_pipeline_ = get_colour_pipeline_actor(viewport_name); + } + static Imath::V2i image_dims(1920, 1080); if (current_onscreen_image_ && enabled_->value()) { @@ -207,9 +217,6 @@ void PixelProbeHUD::update_onscreen_info() { const auto pixel_info = current_onscreen_image_->pixel_info(image_coord); - if (!colour_pipeline_) - get_colour_pipeline_actor(); - if (colour_pipeline_) { // we send the pixel info to the colour pipeline to add it's own colourspace @@ -332,16 +339,26 @@ void PixelProbeHUD::make_pixel_info_onscreen_text(const media_reader::PixelInfo } -void PixelProbeHUD::get_colour_pipeline_actor() { +caf::actor PixelProbeHUD::get_colour_pipeline_actor(const std::string &viewport_name) { + if (colour_pipelines_.find(viewport_name) != colour_pipelines_.end()) { + return colour_pipelines_[viewport_name]; + } auto colour_pipe_manager = system().registry().get(colour_pipeline_registry); caf::scoped_actor sys(system()); - colour_pipeline_ = utility::request_receive( - *sys, - colour_pipe_manager, - xstudio::colour_pipeline::colour_pipeline_atom_v, - "viewport0"); - link_to(colour_pipeline_); + caf::actor r; + try { + r = utility::request_receive( + *sys, + colour_pipe_manager, + xstudio::colour_pipeline::colour_pipeline_atom_v, + viewport_name); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + colour_pipelines_[viewport_name] = r; + return r; } void PixelProbeHUD::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { @@ -349,8 +366,15 @@ void PixelProbeHUD::attribute_changed(const utility::Uuid &attribute_uuid, const HUDPluginBase::attribute_changed(attribute_uuid, role); } -void PixelProbeHUD::on_screen_image(const media_reader::ImageBufPtr &buf) { - current_onscreen_image_ = buf; +void PixelProbeHUD::images_going_on_screen( + const std::vector &images, + const std::string viewport_name, + const bool playhead_playing) { + + if (images.size()) + current_onscreen_images_[viewport_name] = images.front(); + else + current_onscreen_images_[viewport_name].reset(); update_onscreen_info(); } @@ -361,7 +385,7 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("9437e200-80da-4725-97d7-02d5a11b3af1"), "PixelProbeHUD", - plugin_manager::PluginType::PT_HEAD_UP_DISPLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY, true, "Ted Waine", "Viewport HUD Plugin")})); diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp index ff02d5f5a..52b211bc3 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp @@ -40,18 +40,22 @@ namespace ui { const utility::Uuid &attribute_uuid, const int /*role*/ ) override; - // Overriding this allows us to keep updated as to the current on-screen image - void on_screen_image(const media_reader::ImageBufPtr &) override; + void images_going_on_screen( + const std::vector & /*images*/, + const std::string viewport_name, + const bool playhead_playing) override; protected: bool pointer_event(const ui::PointerEvent &e) override; private: - void update_onscreen_info(); - void get_colour_pipeline_actor(); + void update_onscreen_info(const std::string &viewport_name = std::string()); + 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 colour_pipelines_; module::StringAttribute *pixel_info_text_; module::StringAttribute *pixel_info_title_; module::BooleanAttribute *show_code_values_; diff --git a/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt b/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt index 0fc85affe..8b11a20c0 100644 --- a/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt +++ b/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt @@ -1,6 +1,10 @@ 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) diff --git a/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt b/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt index 9e85cf7ba..aeba1263d 100644 --- a/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt +++ b/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt @@ -3,6 +3,7 @@ find_package(OpenColorIO CONFIG) SET(LINK_DEPS xstudio::media_hook xstudio::utility + OpenColorIO::OpenColorIO ) create_plugin_with_alias(media_hook_dneg xstudio::media_hook::dnhook 0.1.0 "${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 750eea326..167ce6d33 100644 --- a/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp +++ b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp @@ -232,8 +232,9 @@ class DNegMediaHook : public MediaHook { result["colour_pipeline"] = colour_p; result["colour_pipeline"]["path"] = path; + static const std::regex show_shot_regex( - R"([\/]+(hosts\/*fs*\/user_data[1-9]{0,1}|jobs)\/([^\/]+)\/([^\/]+))"); + R"([\/]+(hosts\/\w+fs\w+\/user_data[1-9]{0,1}|jobs)\/([^\/]+)\/([^\/]+))"); static const std::regex show_shot_alternative_regex(R"(.+-([^-]+)-([^-]+).dneg.webm$)"); std::smatch match; @@ -258,7 +259,22 @@ class DNegMediaHook : public MediaHook { } } + if (metadata.contains(nlohmann::json::json_pointer("/metadata/timeline/dneg"))) { + // "dnuuid": "b32f9c30-9c18-4e93-ac11-98ae1c685273", + // "job": "NECRUS", + // "shot": "00TS_0020" + const auto &dneg = + metadata.at(nlohmann::json::json_pointer("/metadata/timeline/dneg")); + if (dneg.count("job")) + result["metadata"]["external"]["DNeg"]["show"] = dneg.at("job"); + if (dneg.count("shot")) + result["metadata"]["external"]["DNeg"]["shot"] = dneg.at("shot"); + if (dneg.count("dnuuid")) + result["metadata"]["external"]["DNeg"]["Ivy"]["dnuuid"] = dneg.at("dnuuid"); + } + // spdlog::warn("MediaHook Metadata Result {}", result["colour_pipeline"].dump(2)); + // spdlog::warn("MediaHook Metadata Result {}", result.dump(2)); return result; } @@ -274,7 +290,7 @@ class DNegMediaHook : public MediaHook { std::smatch match; static const std::regex show_shot_regex( - R"(\/(\/hosts\/*fs*\/user_data|jobs)\/([^\/]+)\/([^\/]+))"); + R"([\/]+(hosts\/\w+fs\w+\/user_data[1-9]{0,1}|jobs)\/([^\/]+)\/([^\/]+))"); static const std::regex show_shot_alternative_regex( R"(.+-([^-]+)-([^-]+)\.dneg\.webm$)"); @@ -304,7 +320,24 @@ class DNegMediaHook : public MediaHook { bool is_cms1_config = pipeline_version == "2"; - // Input colour space detection + // Detect override to active displays and views + const std::string active_displays = + get_showvar_or(context["SHOW"], "DN_REVIEW_XSTUDIO_OCIO_ACTIVE_DISPLAYS", ""); + if (!active_displays.empty()) { + r["active_displays"] = active_displays; + } + + std::string active_views = + get_showvar_or(context["SHOW"], "DN_REVIEW_XSTUDIO_OCIO_ACTIVE_VIEWS", ""); + if (!active_views.empty()) { + r["active_views"] = active_views; + } + const auto views = utility::split(active_views, ':'); + const bool has_untonemapped_view = + std::find(views.begin(), views.end(), "Un-tone-mapped") != views.end(); + + + // Input media category detection static const std::regex review_regex(".+\\.review[0-9]\\.mov$"); static const std::regex internal_regex(".+\\.dneg.mov$"); @@ -314,23 +347,33 @@ class DNegMediaHook : public MediaHook { static const std::set stills_ext{ ".png", ".tiff", ".tif", ".jpeg", ".jpg", ".gif"}; - // Newer configs use a colour space instead of inverting the view for - // better integration in the UI source colorspace menu. - auto fill_baked_space = - [is_cms1_config](utility::JsonStore &r, const std::string &display) { - if (is_cms1_config) { - r["input_colorspace"] = std::string("DNEG_") + display; - } else { - r["input_display"] = display; - r["input_view"] = "Film"; - } - }; + std::string input_category = "unknown"; + + if (std::regex_match(path, review_regex)) { + input_category = "review_proxy"; + } else if (std::regex_match(path, internal_regex)) { + input_category = "internal_movie"; + } else if (path.find("/edit_ref/") != std::string::npos) { + input_category = "edit_ref"; + } else if (linear_ext.find(ext) != linear_ext.end()) { + input_category = "linear_media"; + } else if (log_ext.find(ext) != log_ext.end()) { + input_category = "log_media"; + } else if (stills_ext.find(ext) != stills_ext.end()) { + input_category = "still_media"; + } else { + input_category = "movie_media"; + } - std::string media_colorspace = ""; - std::string media_display = ""; - std::string media_view = ""; + r["input_category"] = input_category; + + // Input colour space detection // Extract OCIO metadata from internal and review proxy 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)) { try { @@ -344,71 +387,81 @@ class DNegMediaHook : public MediaHook { } } + // Note that we prefer using input_colorspace when possible, + // this maps better to the UI source colour space menu. + + // Except for specific cases, we convert the source to scene_linear + r["working_space"] = "scene_linear"; + + // If we have OCIO metadata, use it to derive the input space if (!media_colorspace.empty()) { r["input_colorspace"] = media_colorspace; } else if (!media_display.empty() && !media_view.empty()) { r["input_colorspace"] = media_view + "_" + media_display; - } else if (std::regex_match(path, review_regex)) { + } else if (input_category == "review_proxy") { r["input_colorspace"] = "dneg_proxy_log:log"; - /* - http://jira/browse/CLR-2006 - This is a fix for LBP where all - review proxy movies are baked with - log_ARRIWideGamut_ARRILogC3 colorspace. Once the show is - switched to CMS1 config the input colorspace will be wrong - and to avoid proxy re processing, we added this check to - change the input cs to log_ARRIWideGamut_ARRILogC3 which is - the old log space that was used on LBP to make review proxy. - */ - if (context["SHOW"] == "LBP" && is_cms1_config) { + // LBP review proxy before CMS1 migration (no metadata) + // http://jira/browse/CLR-2006 + if (context["SHOW"] == "LBP") { r["input_colorspace"] = "log_ARRIWideGamut_ARRILogC3"; } - } else if (std::regex_match(path, internal_regex)) { - /* - http://jira/browse/CLR-2006 - This is a fix for LBP where all - internal movies are baked with Film_Rec709 colorspace. Once the show is - switched to CMS1 config the input colorspace will be wrong. To avoid - movie re processing, we added this check to change the input cs to - Client_Rec709 which is the old Film_Rec709 cs. - */ - if (context["SHOW"] == "LBP" && is_cms1_config) { + } else if (input_category == "internal_movie") { + // LBP internal movie before CMS1 migration (no metadata) + // http://jira/browse/CLR-2006 + if (context["SHOW"] == "LBP") { r["input_colorspace"] = "Client_Rec709"; + } else if (is_cms1_config) { + r["input_colorspace"] = "DNEG_Rec709"; } else { - fill_baked_space(r, "Rec709"); + 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 { + r["input_display"] = "Rec709"; + r["input_view"] = "Film"; + r["automatic_view"] = "Film"; + } + } else if (input_category == "linear_media") { + r["input_colorspace"] = "scene_linear:linear"; + } else if (input_category == "log_media") { + r["input_colorspace"] = "compositing_log:log"; + } 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"; } - } else if (linear_ext.find(ext) != linear_ext.end()) { - r["input_colorspace"] = "linear"; - } else if (log_ext.find(ext) != log_ext.end()) { - r["input_colorspace"] = "log"; - } else if (stills_ext.find(ext) != stills_ext.end()) { - fill_baked_space(r, "sRGB"); - } else { - fill_baked_space(r, "Rec709"); - } - - // Detect automatic view assignment - if (path.find("/edit_ref/") != std::string::npos) { - r["automatic_view"] = is_cms1_config ? "Client" : "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 override to active displays and views - const std::string active_displays = - get_showvar_or(context["SHOW"], "DN_REVIEW_XSTUDIO_OCIO_ACTIVE_DISPLAYS", ""); - if (!active_displays.empty()) { - r["active_displays"] = active_displays; } - const std::string active_views = - get_showvar_or(context["SHOW"], "DN_REVIEW_XSTUDIO_OCIO_ACTIVE_VIEWS", ""); - if (!active_views.empty()) { - r["active_views"] = active_views; + // 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"; + } } // Detect grading CDLs slots to upgrade as GradingPrimary @@ -421,7 +474,8 @@ class DNegMediaHook : public MediaHook { r["viewing_rules"] = true; } else { - r["ocio_config"] = "__raw__"; + r["ocio_config"] = "__raw__"; + r["working_space"] = "raw"; } return r; diff --git a/src/plugin/media_metadata/ffprobe/src/ffprobe.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe.cpp index be616c58b..069317b14 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe.cpp @@ -78,7 +78,8 @@ FFProbeMediaMetadata::fill_standard_fields(const nlohmann::json &metadata) { std::cmatch m; const std::regex bitdepth("(f?)([0-9]+)(le|be)"); - if (std::regex_search(h["pix_fmt"].get().c_str(), m, bitdepth)) { + auto pix_fmt = h["pix_fmt"].get(); + if (std::regex_search(pix_fmt.c_str(), m, bitdepth)) { if (m[1].str() == "f") { fields.bit_depth_ = m[2].str() + " bit float"; } else { @@ -93,8 +94,9 @@ FFProbeMediaMetadata::fill_standard_fields(const nlohmann::json &metadata) { std::cmatch m; const std::regex aspect("([0-9]+)\\:([0-9]+)"); - if (std::regex_search( - h["sample_aspect_ratio"].get().c_str(), m, aspect)) { + + auto aspect_ratio = h["sample_aspect_ratio"].get(); + if (std::regex_search(aspect_ratio.c_str(), m, aspect)) { try { double num = std::stod(m[1].str()); double den = std::stod(m[2].str()); diff --git a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index 47e8d39e4..b420339d9 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -16,14 +16,18 @@ extern "C" { #include } +#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif using namespace xstudio; using namespace xstudio::ffprobe; namespace { -const auto av_time_base_q = AV_TIME_BASE_Q; +const auto av_time_base_q = + av_get_time_base_q(); // READ + // https://libav-devel.libav.narkive.com/ZQCWfTun/patch-0-2-fix-avutil-h-usage-from-c int check_stream_specifier(AVFormatContext *avfs, AVStream *avs, const char *spec) { auto result = avformat_match_stream_specifier(avfs, avs, spec); @@ -102,7 +106,11 @@ AVDictionary **init_find_stream_opts(AVFormatContext *avfc, AVDictionary *codec_ AVDictionary **result = nullptr; if (avfc->nb_streams) { +#ifdef _WIN32 + result = (AVDictionary **)av_calloc(avfc->nb_streams, sizeof(*result)); +#else result = (AVDictionary **)av_malloc_array(avfc->nb_streams, sizeof(*result)); +#endif if (result) { for (unsigned int i = 0; i < avfc->nb_streams; i++) diff --git a/src/plugin/media_metadata/openexr/src/openexr.cpp b/src/plugin/media_metadata/openexr/src/openexr.cpp index eade41003..34dfff216 100644 --- a/src/plugin/media_metadata/openexr/src/openexr.cpp +++ b/src/plugin/media_metadata/openexr/src/openexr.cpp @@ -490,7 +490,7 @@ bool dump_json_headers(const Imf::Header &h, nlohmann::json &root) { root[i.name()]["type"] = i.attribute().typeName(); root[i.name()]["value"] = nullptr; } - } catch (const Iex::TypeExc &e) { + } catch ([[maybe_unused]] const Iex::TypeExc &e) { root[i.name()]["type"] = i.attribute().typeName(); root[i.name()]["value"] = nullptr; } diff --git a/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt b/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt index 8e3880ae2..92e95dbf3 100644 --- a/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt +++ b/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt @@ -1,6 +1,6 @@ project(media_reader_ffmpeg VERSION 0.1.0 LANGUAGES CXX) -find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale avutil) +find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale avutil swresample) find_package(GLEW REQUIRED) set(SOURCES @@ -17,7 +17,9 @@ target_compile_definitions(${PROJECT_NAME} PUBLIC OPTIMISED_BUFFER=1 ) -target_compile_options(${PROJECT_NAME} PRIVATE -Wfatal-errors) +if(UNIX) + target_compile_options(${PROJECT_NAME} PRIVATE -Wfatal-errors) +endif() target_link_libraries(${PROJECT_NAME} PUBLIC @@ -27,6 +29,16 @@ target_link_libraries(${PROJECT_NAME} FFMPEG::avformat FFMPEG::swscale FFMPEG::avutil -) + FFMPEG::swresample + ) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + +if(WIN32) + +# We want the externally defined ffmpeg dlls to be installed into the bin directory. +instalL(DIRECTORY ${FFMPEG_ROOT}/bin DESTINATION ${CMAKE_INSTALL_PREFIX}/ FILES_MATCHING PATTERN "*.dll") + +# We don't want the vcpkg install, or it will install linked dlls. +_install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin) +endif() diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp index a77ceb991..313c75f5e 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp @@ -5,7 +5,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include "xstudio/global_store/global_store.hpp" #include "xstudio/media/media.hpp" @@ -264,8 +266,15 @@ 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"); +#endif + } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } @@ -301,24 +310,37 @@ AudioBufPtr FFMpegMediaReader::audio(const media::AVFrameID &mptr) { try { + // 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_); + // 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_)); } AudioBufPtr rt; + + // Decode the audio frame using the decoder and get the resulting audio buffer. 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. if (!rt) { rt.reset(new AudioBuffer()); } + // Return the obtained/created audio buffer. return rt; - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { + // If an exception is encountered, rethrow it to be handled by the caller. throw; } + + // If everything else fails, return an empty shared pointer. return AudioBufPtr(); } @@ -351,7 +373,10 @@ xstudio::media::MediaDetail FFMpegMediaReader::detail(const caf::uri &uri) const fmt::format("stream {}", p.first), (p.second->codec_type() == AVMEDIA_TYPE_VIDEO ? media::MT_IMAGE : media::MT_AUDIO), - "{0}@{1}/{2}")); + "{0}@{1}/{2}", + p.second->resolution(), + p.second->pixel_aspect(), + p.first)); } } @@ -396,7 +421,7 @@ FFMpegMediaReader::thumbnail(const media::AVFrameID &mptr, const size_t thumb_si thumbnail_decoder.reset(); return rt; - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { throw; } } diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp index 7d4db21e8..61e950162 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp @@ -46,6 +46,7 @@ namespace media_reader { int readers_per_source_; int soundcard_sample_rate_ = {4000}; + int channels_ = 2; 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 affbb98f1..72c1f640e 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp @@ -5,7 +5,9 @@ #include "ffmpeg_decoder.hpp" #include "xstudio/media/media_error.hpp" +#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif #define MIN_SEEK_FORWARD_FRAMES 16 @@ -61,7 +63,7 @@ void FFMpegDecoder::open_handles() { ffmpeg_threads, movie_file_path_); - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } } @@ -417,7 +419,7 @@ void FFMpegDecoder::decode_audio_frame( last_requested_frame_ = -100; } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // some error has occurred ... force a fresh seek on next try last_requested_frame_ = -100; @@ -490,7 +492,7 @@ void FFMpegDecoder::decode_video_frame( last_requested_frame_ = frame_num; - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // some error has occurred ... force a fresh seek on next try last_requested_frame_ = -100; @@ -537,7 +539,7 @@ FFMpegDecoder::decode_thumbnail_frame(const int64_t frame_num, const size_t size rt = decode_stream_->convert_av_frame_to_thumbnail(size_hint); } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // some error has occurred ... force a fresh seek on next try last_requested_frame_ = -100; diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index 9a5ccbb74..f8ee75459 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -3,11 +3,14 @@ #include #include #include +#include #include "ffmpeg_stream.hpp" #include "xstudio/media/media_error.hpp" +#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif using namespace xstudio::media_reader::ffmpeg; using namespace xstudio::media_reader; @@ -115,7 +118,7 @@ void set_shader_pix_format_info( // Bit depth const int bitdepth = pixel_desc->comp[0].depth; - const int max_cv = std::pow(2, bitdepth) - 1; + const int max_cv = std::floor(std::pow(2, bitdepth) - 1); jsn["bits_per_channel"] = bitdepth; jsn["norm_coeff"] = 1.0f / max_cv; @@ -146,13 +149,13 @@ void set_shader_pix_format_info( switch (color_range) { case AVCOL_RANGE_JPEG: { Imath::V3f offset(1, 128, 128); - offset *= std::pow(2, bitdepth - 8); + offset *= std::pow(2.0f, float(bitdepth - 8)); jsn["yuv_offsets"] = {"ivec3", 1, offset[0], offset[1], offset[2]}; } break; case AVCOL_RANGE_MPEG: default: { Imath::V4f range(16, 235, 16, 240); - range *= std::pow(2, bitdepth - 8); + range *= std::pow(2.0f, float(bitdepth - 8)); Imath::M33f scale; scale[0][0] = 1.f * max_cv / (range[1] - range[0]); @@ -161,7 +164,7 @@ void set_shader_pix_format_info( yuv_to_rgb *= scale; Imath::V3f offset(16, 128, 128); - offset *= std::pow(2, bitdepth - 8); + offset *= std::pow(2.0f, float(bitdepth - 8)); jsn["yuv_offsets"] = {"ivec3", 1, offset[0], offset[1], offset[2]}; } } @@ -533,6 +536,7 @@ 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(); @@ -540,6 +544,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; @@ -600,6 +609,13 @@ FFMpegStream::FFMpegStream( frame->height = avc_stream_->codecpar->height; frame->format = codec_context_->pix_fmt; + // store resolution and pixel aspect + resolution_ = Imath::V2f(avc_stream_->codecpar->width, avc_stream_->codecpar->height); + auto sar = av_guess_sample_aspect_ratio(format_context_, avc_stream_, nullptr); + if (sar.num && sar.den) { + pixel_aspect_ = float(sar.num) / float(sar.den); + } + if (codec_->capabilities & AV_CODEC_CAP_DR1) { // See Note 1 below @@ -628,6 +644,11 @@ FFMpegStream::FFMpegStream( 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; @@ -750,6 +771,33 @@ size_t FFMpegStream::resample_audio( if (offset_into_output_buffer == -1) { // automatically extend the buffer the exact required amount // size_t sz = audio_buffer->size(); + // The multiplication by 2 * 2 seems to be an assumption based on specific audio data + // properties. Here's a possible explanation: + // + // 2 Channels: The first 2 likely represents the fact that there are 2 channels. This + // makes sense given that you've defined the target channel layout to be stereo + // (av_get_default_channel_layout(2)). So, for each sample, there's data for both the + // left and right channels. + // + // 2 Bytes per Sample (16-bit audio): The second 2 presumably represents 2 bytes per + // sample, which corresponds to 16-bit audio samples. This is a common format for audio, + // especially in CD-quality audio. + // + // By multiplying the number of samples by 2 * 2, is calculating the + // offset in bytes to where the new data should be written in the buffer. + // + // However, this calculation has a couple of assumptions: + // + // - The audio always has 2 channels. + // - The audio samples are always 16 bits. + // + // If either of these assumptions is violated (for example, if the audio is mono or if + // the bit depth is different), then the calculation would be incorrect. + // + // It would be safer and clearer to derive these values from variables or constants that + // explicitly state their purpose (like NUM_CHANNELS and BYTES_PER_SAMPLE), rather than + // hardcoding them as 2 and 2. Alternatively, adding a comment to explain this + // arithmetic can also help future maintainers understand the intent. audio_buffer->extend_size(target_out_size); out = (uint8_t *)(audio_buffer->buffer() + audio_buffer->num_samples() * 2 * 2); @@ -865,4 +913,4 @@ int FFMpegStream::duration_frames() const { // 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 */ \ No newline at end of file +// be needed for high res playback */ diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp index 0dab35cec..f8a69ef0c 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp @@ -111,6 +111,10 @@ 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; } @@ -148,6 +152,8 @@ namespace media_reader { bool using_own_frame_allocation = {false}; bool nothing_decoded_yet_ = {true}; int current_frame_ = {CURRENT_FRAME_UNKNOWN}; + Imath::V2i resolution_ = {Imath::V2i(0, 0)}; + float pixel_aspect_ = 1.0f; // for video rescaling SwsContext *sws_context_ = {nullptr}; diff --git a/src/plugin/media_reader/openexr/src/openexr.cpp b/src/plugin/media_reader/openexr/src/openexr.cpp index 582f16c15..adb12cb5d 100644 --- a/src/plugin/media_reader/openexr/src/openexr.cpp +++ b/src/plugin/media_reader/openexr/src/openexr.cpp @@ -5,7 +5,9 @@ #include #include +#ifdef __linux__ #include +#endif #include #include #include @@ -92,7 +94,11 @@ static std::string shader{R"( uniform int width; uniform int height; uniform int num_channels; -uniform int pix_type; +uniform int pix_type_r; +uniform int pix_type_g; +uniform int pix_type_b; +uniform int pix_type_a; +uniform int bytes_per_pixel; uniform ivec2 image_bounds_min; uniform ivec2 image_bounds_max; @@ -101,88 +107,67 @@ uniform ivec2 image_bounds_max; vec2 get_image_data_2floats(int byte_address); float get_image_data_float32(int byte_address); -vec4 fetch_pixel_32bitfloat(ivec2 image_coord) +vec4 fetch_rgba_pixel(ivec2 image_coord) { - if (image_coord.x < image_bounds_min.x || image_coord.x >= image_bounds_max.x) return vec4(0.0,0.0,0.0,0.0); + if (image_coord.x < image_bounds_min.x || image_coord.x >= image_bounds_max.x) return vec4(0.0,0.0,0.0,0.0); if (image_coord.y < image_bounds_min.y || image_coord.y >= image_bounds_max.y) return vec4(0.0,0.0,0.0,0.0); - int pixel_address_bytes = ((image_coord.x-image_bounds_min.x) + (image_coord.y-image_bounds_min.y)*(image_bounds_max.x-image_bounds_min.x))*num_channels*4; + int pixel_address_bytes = ((image_coord.x-image_bounds_min.x) + (image_coord.y-image_bounds_min.y)*(image_bounds_max.x-image_bounds_min.x))*bytes_per_pixel; - float R = get_image_data_float32(pixel_address_bytes); + float R = 0.9; + float G = 0.4; + float B = 0.0; + float A = 1.0; - if (num_channels > 2) { + vec2 pixRG = get_image_data_2floats(pixel_address_bytes); - float G = get_image_data_float32(pixel_address_bytes+4); - float B = get_image_data_float32(pixel_address_bytes+8); - if (num_channels == 3) - { - return vec4(R,G,B,1.0); - } - else - { - float A = get_image_data_float32(pixel_address_bytes+12); - return vec4(R, G, B, A); - } + if(pix_type_r == 1) { + R = pixRG.x; + pixel_address_bytes = pixel_address_bytes+2; + } else if(pix_type_r == 2) { + R = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes = pixel_address_bytes+4; } - else if (num_channels == 2) - { - // using Luminance/Alpha layout - float A = get_image_data_float32(pixel_address_bytes+4); - return vec4(R,R,R,A); - } - else if (num_channels == 1) - { - // 1 channels, assume luminance - return vec4(R, R, R, 1.0); - } - - return vec4(0.9,0.4,0.0,1.0); -} - -vec4 fetch_pixel_16bitfloat(ivec2 image_coord) -{ - if (image_coord.x < image_bounds_min.x || image_coord.x >= image_bounds_max.x) return vec4(0.0,0.0,0.0,0.0); - if (image_coord.y < image_bounds_min.y || image_coord.y >= image_bounds_max.y) return vec4(0.0,0.0,0.0,0.0); + if(num_channels == 1) { + // 1 channels, assume luminance + return vec4(R, R, R, 1.0); + } - int pixel_address_bytes = ((image_coord.x-image_bounds_min.x) + (image_coord.y-image_bounds_min.y)*(image_bounds_max.x-image_bounds_min.x))*num_channels*2; + if(pix_type_g == 1) { + G = pixRG.y; + pixel_address_bytes = pixel_address_bytes+2; + } else if(pix_type_g == 2) { + G = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes = pixel_address_bytes+4; + } - vec2 pixRG = get_image_data_2floats(pixel_address_bytes); + if(num_channels == 2) { + // 2 channels, assume luminance/alpha + return vec4(R, R, R, G); + } - if (num_channels > 2) { + vec2 pixBA = get_image_data_2floats(pixel_address_bytes); - vec2 pixBA = get_image_data_2floats(pixel_address_bytes+4); - if (num_channels == 3) - { - return vec4(pixRG.x,pixRG.y,pixBA.x,1.0); - } - else - { - return vec4(pixRG,pixBA); - } + if(pix_type_b == 1) { + B = pixBA.x; + pixel_address_bytes = pixel_address_bytes+2; + } else if(pix_type_b == 2) { + B = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes = pixel_address_bytes+4; } - else if (num_channels == 2) - { - // 2 channels, assume luminance/alpha - return vec4(pixRG.x,pixRG.x,pixRG.x,pixRG.y); - } - else if (num_channels == 1) - { - // 1 channels, assume luminance - return vec4(pixRG.x,pixRG.x,pixRG.x,1.0); - } - return vec4(0.9,0.4,0.0,1.0); - -} + if(num_channels == 3) { + return vec4(R, G, B, 1.0); + } -vec4 fetch_rgba_pixel(ivec2 image_coord) -{ - if (pix_type == 1) { - return fetch_pixel_16bitfloat(image_coord); - } else if (pix_type == 2) { - return fetch_pixel_32bitfloat(image_coord); + if(pix_type_a == 1) { + A = pixBA.y; + } else if(pix_type_a == 2) { + A = get_image_data_float32(pixel_address_bytes); } + + return vec4(R, G, B, A); } )"}; @@ -239,7 +224,7 @@ ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { Imf::MultiPartInputFile input(path.c_str()); int parts = input.parts(); int part_idx = -1; - Imf::PixelType pix_type; + std::array pix_type; std::vector exr_channels_to_load; for (int prt = 0; prt < parts; ++prt) { @@ -294,17 +279,37 @@ ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { // 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 = (pix_type == Imf::PixelType::HALF ? 2 : 4); - const size_t bytes_per_pixel = bytes_per_channel * exr_channels_to_load.size(); - const size_t buf_size = n_pixels * bytes_per_pixel; + 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"] = int(pix_type); + 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)); @@ -348,15 +353,23 @@ ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { 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( - pix_type, (char *)fPtr, bytes_per_pixel, line_stride, 1, 1, 0)); - fPtr += bytes_per_channel; + channel_type, + (char *)fPtr, + bytes_per_pixel, + line_stride, + 1, + 1, + 0)); + fPtr += channel_type == Imf::PixelType::HALF ? 2 : 4; }); in.setFrameBuffer(fb); @@ -382,15 +395,25 @@ ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { 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( - pix_type, (char *)buffer, bytes_per_pixel, line_stride, 1, 1, 0)); - buffer += bytes_per_channel; + channel_type, + (char *)buffer, + bytes_per_pixel, + line_stride, + 1, + 1, + 0)); + buffer += channel_type == Imf::PixelType::HALF ? 2 : 4; }); in.setFrameBuffer(fb); in.readPixels(data_window.min.y, data_window.max.y); @@ -472,7 +495,7 @@ void OpenEXRMediaReader::stream_ids_from_exr_part( } } -Imf::PixelType OpenEXRMediaReader::pick_exr_channels_from_stream_id( +std::array OpenEXRMediaReader::pick_exr_channels_from_stream_id( const Imf::Header &header, const std::string &stream_id, std::vector &exr_channels_to_load) const { @@ -518,9 +541,9 @@ Imf::PixelType OpenEXRMediaReader::pick_exr_channels_from_stream_id( exr_channels_to_load.end(), [&is_xyzuv](std::string a, std::string b) { if (is_xyzuv) { - return a < b; + return to_lower(a) < to_lower(b); } else { - return a > b; + return to_lower(a) > to_lower(b); } }); @@ -530,22 +553,23 @@ Imf::PixelType OpenEXRMediaReader::pick_exr_channels_from_stream_id( const auto &channels = header.channels(); - int p_type_chk = -1; - Imf::PixelType pix_type; + std::array pix_type; - // fetch the channel names for 16bit float channels - for (Imf::ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) { - if (std::find(exr_channels_to_load.begin(), exr_channels_to_load.end(), i.name()) != - exr_channels_to_load.end()) { - if (p_type_chk == -1) { - p_type_chk = (int)i.channel().type; - pix_type = i.channel().type; - } else if (pix_type != i.channel().type) { - throw std::runtime_error( - "EXR part/layer mixes pixel channel types. This is not supported."); + // fetch the channel type for each of the channels we will load + int ii = 0; + for (const auto &chan_name : exr_channels_to_load) { + + pix_type[ii] = Imf::PixelType::UINT; + for (Imf::ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) { + if (i.name() == chan_name) { + pix_type[ii] = i.channel().type; } } + ii++; + if (ii == 4) + break; // shouldn't happen! } + return pix_type; } @@ -570,10 +594,21 @@ xstudio::media::MediaDetail OpenEXRMediaReader::detail(const caf::uri &uri) cons const Imf::Header &h = input.header(0); const auto rate = h.findTypedAttribute("framesPerSecond"); const auto rate_bogus = h.findTypedAttribute("framesPerSecond"); - const auto timecode = h.findTypedAttribute("timeCode"); + const auto rate_nuke = h.findTypedAttribute("nuke/input/frame_rate"); + const auto timecode = h.findTypedAttribute("timeCode"); const auto timecode_rate = h.findTypedAttribute("timecodeRate"); - if (rate) + + // 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); @@ -599,16 +634,28 @@ xstudio::media::MediaDetail OpenEXRMediaReader::detail(const caf::uri &uri) cons frd.set_rate(fr == 0.0 ? utility::FrameRate() : utility::FrameRate(1.0 / fr)); 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); } + int ct = 0; for (const auto &stream_id : 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++]; } } catch (const std::exception &e) { @@ -673,7 +720,11 @@ PixelInfo OpenEXRMediaReader::exr_buffer_pixel_picker( int width = buf.image_size_in_pixels().x; int height = buf.image_size_in_pixels().y; int num_channels = buf.shader_params().value("num_channels", 0); - int pix_type = buf.shader_params().value("pix_type", 0); + int bytes_per_pixel = buf.shader_params().value("bytes_per_pixel", 0); + int pix_type_r = buf.shader_params().value("pix_type_r", 0); + int pix_type_g = buf.shader_params().value("pix_type_g", 0); + int pix_type_b = buf.shader_params().value("pix_type_b", 0); + int pix_type_a = buf.shader_params().value("pix_type_a", 0); const Imath::V2i image_bounds_min = buf.image_pixels_bounding_box().min; const Imath::V2i image_bounds_max = buf.image_pixels_bounding_box().max; @@ -689,6 +740,7 @@ PixelInfo OpenEXRMediaReader::exr_buffer_pixel_picker( if (chan_names.is_array()) { for (const auto &i : chan_names) { std::string chan_name = i.get(); + if (chan_name.find(stream_id) == 0) { chan_name = std::string(chan_name, stream_id.size()); } @@ -709,71 +761,61 @@ PixelInfo OpenEXRMediaReader::exr_buffer_pixel_picker( return Imath::V2f(v[0], v[1]); }; - auto fetch_pixel_32bitfloat = [&](const Imath::V2i image_coord) { - if (image_coord.x < image_bounds_min.x || image_coord.x >= image_bounds_max.x) - return; - if (image_coord.y < image_bounds_min.y || image_coord.y >= image_bounds_max.y) - return; + if (pixel_location.x < image_bounds_min.x || pixel_location.x >= image_bounds_max.x) + return r; + if (pixel_location.y < image_bounds_min.y || pixel_location.y >= image_bounds_max.y) + return r; + + int pixel_address_bytes = + ((pixel_location.x - image_bounds_min.x) + + (pixel_location.y - image_bounds_min.y) * (image_bounds_max.x - image_bounds_min.x)) * + bytes_per_pixel; - int pixel_address_bytes = - ((image_coord.x - image_bounds_min.x) + - (image_coord.y - image_bounds_min.y) * (image_bounds_max.x - image_bounds_min.x)) * - num_channels * 4; + Imath::V2f pixRG = get_image_data_2xhalf_float(pixel_address_bytes); + if (pix_type_r == 1) { + r.add_raw_channel_info(channel_names[0], pixRG.x); + pixel_address_bytes += 2; + } else { float R = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes += 4; r.add_raw_channel_info(channel_names[0], R); + } - if (num_channels > 2) { - - float G = get_image_data_float32(pixel_address_bytes + 4); + if (num_channels >= 2) { + if (pix_type_g == 1) { + r.add_raw_channel_info(channel_names[1], pixRG.y); + pixel_address_bytes += 2; + } else { + float G = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes += 4; r.add_raw_channel_info(channel_names[1], G); - float B = get_image_data_float32(pixel_address_bytes + 8); - r.add_raw_channel_info(channel_names[2], B); - if (num_channels == 4) { - float A = get_image_data_float32(pixel_address_bytes + 12); - r.add_raw_channel_info(channel_names[3], A); - } - } else if (num_channels == 2) { - // using Luminance/Alpha layout - float A = get_image_data_float32(pixel_address_bytes + 4); - r.add_raw_channel_info(channel_names[1], A); } - }; - auto fetch_pixel_16bitfloat = [&](const Imath::V2i image_coord) { - if (image_coord.x < image_bounds_min.x || image_coord.x >= image_bounds_max.x) - return; - if (image_coord.y < image_bounds_min.y || image_coord.y >= image_bounds_max.y) - return; + if (num_channels == 2) + return r; // using Luminance/Alpha layout - int pixel_address_bytes = - ((image_coord.x - image_bounds_min.x) + - (image_coord.y - image_bounds_min.y) * (image_bounds_max.x - image_bounds_min.x)) * - num_channels * 2; + Imath::V2f pixBA = get_image_data_2xhalf_float(pixel_address_bytes); - Imath::V2f pixRG = get_image_data_2xhalf_float(pixel_address_bytes); + if (pix_type_b == 1) { + r.add_raw_channel_info(channel_names[2], pixBA.x); + pixel_address_bytes += 2; + } else { + float B = get_image_data_float32(pixel_address_bytes); + pixel_address_bytes += 4; + r.add_raw_channel_info(channel_names[2], B); + } - if (num_channels > 2) { + if (num_channels == 3) + return r; - Imath::V2f pixBA = get_image_data_2xhalf_float(pixel_address_bytes + 4); - r.add_raw_channel_info(channel_names[0], pixRG.x); - r.add_raw_channel_info(channel_names[1], pixRG.y); - r.add_raw_channel_info(channel_names[2], pixBA.x); - if (num_channels == 4) { - r.add_raw_channel_info(channel_names[3], pixBA.y); - } - } else if (num_channels == 2) { - r.add_raw_channel_info(channel_names[0], pixRG.x); - r.add_raw_channel_info(channel_names[1], pixRG.y); - } else if (num_channels == 1) { - // 1 channels, assume luminance - r.add_raw_channel_info(channel_names[0], pixRG.x); + if (pix_type_a == 1) { + r.add_raw_channel_info(channel_names[3], pixBA.y); + } else { + float A = get_image_data_float32(pixel_address_bytes); + r.add_raw_channel_info(channel_names[3], A); } - }; + } // else 1 channel, assume luminance - if (pix_type == 1) - fetch_pixel_16bitfloat(pixel_location); - else - fetch_pixel_32bitfloat(pixel_location); return r; -} \ No newline at end of file +} diff --git a/src/plugin/media_reader/openexr/src/openexr.hpp b/src/plugin/media_reader/openexr/src/openexr.hpp index 0716e7486..1edd3eb7a 100644 --- a/src/plugin/media_reader/openexr/src/openexr.hpp +++ b/src/plugin/media_reader/openexr/src/openexr.hpp @@ -44,7 +44,7 @@ namespace media_reader { void stream_ids_from_exr_part( const Imf::Header &header, std::vector &stream_ids) const; - Imf::PixelType pick_exr_channels_from_stream_id( + std::array pick_exr_channels_from_stream_id( const Imf::Header &header, const std::string &stream_id, std::vector &exr_channels_to_load) const; diff --git a/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp b/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp index cc46316de..62f511c06 100644 --- a/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp +++ b/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/thumbnail/thumbnail.hpp" +#undef RGB namespace xstudio { namespace media_reader { @@ -12,9 +13,10 @@ namespace media_reader { exr_size = exr_buf->image_size_in_pixels(); exr_data_win = exr_buf->image_pixels_bounding_box(); exr_chans = exr_buf->shader_params()["num_channels"].get(); - pix_type = exr_buf->shader_params()["pix_type"].get(); + // Check only for the bitness of the R channel since RGB will be similar + pix_type = exr_buf->shader_params()["pix_type_r"].get(); + exr_bytes_per_pixel = exr_buf->shader_params()["bytes_per_pixel"].get(); - exr_bytes_per_pixel = exr_chans * (pix_type == Imf::PixelType::HALF ? 2 : 4); exr_bytes_per_line = (exr_data_win.max.x - exr_data_win.min.x) * exr_bytes_per_pixel; @@ -71,7 +73,7 @@ namespace media_reader { inline std::array sample_16bit_exr_float(const int x, const int y) const { if (x < exr_data_win.min.x || x >= exr_data_win.max.x || y < exr_data_win.min.y || y >= exr_data_win.max.y) - return std::array({1.0f, 0.0f, 1.0f}); + return std::array({0.0f, 0.0f, 0.0f}); half * pix = (half *)(exr_buf_->buffer() + (x-exr_data_win.min.x)*exr_bytes_per_pixel + (y-exr_data_win.min.y)*exr_bytes_per_line); if (exr_chans <= 2) diff --git a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp index 8cee9510b..c4012baea 100644 --- a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp +++ b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __linux__ #include +#endif #include #include #include @@ -345,9 +347,13 @@ template class DNRunPluginActor : public caf::event_based_actor { system().registry().template get(plugin_manager_registry); auto session = request_receive(*sys, global, session::session_atom_v); + auto media_rate = + request_receive(*sys, session, session::media_rate_atom_v); for (const auto &i : requests) { + try { + auto jsn = nlohmann::json::parse(i); // should be dict with paths: array if (jsn["args"]["paths"].empty()) @@ -388,6 +394,18 @@ template class DNRunPluginActor : public caf::event_based_actor { 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"; + for (std::string path : jsn.at("args").at("paths")) { // auto path = j.get(); if (starts_with(path, "xstudio://")) { @@ -397,24 +415,23 @@ template class DNRunPluginActor : public caf::event_based_actor { } if (utility::check_plugin_uri_request(path)) { + // send to plugin manager.. auto uri = caf::make_uri(path); - - auto media_rate = request_receive( - *sys, session, session::media_rate_atom_v); - if (uri) - anon_send( - pm, - data_source::use_data_atom_v, + send_uri_request_to_plugin( *uri, + media_rate, session, playlist, - media_rate); + pm, + quickview, + ab_compare); else { spdlog::warn( "{} Invalid URI {}", __PRETTY_FUNCTION__, path); } + } else { try { FrameList fl; @@ -460,6 +477,21 @@ template class DNRunPluginActor : public caf::event_based_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) { @@ -498,10 +530,64 @@ template class DNRunPluginActor : public caf::event_based_actor { caf::behavior make_behavior() override { return behavior_; } private: + void send_uri_request_to_plugin( + const caf::uri &uri, + const FrameRate &rate, + caf::actor session, + caf::actor playlist, + caf::actor plugin_manager, + const bool quickview, + const bool ab_compare); + caf::behavior behavior_; T utility_; }; +template +void DNRunPluginActor::send_uri_request_to_plugin( + const caf::uri &uri, + const FrameRate &rate, + caf::actor session, + caf::actor playlist, + caf::actor plugin_manager, + const bool quickview, + const bool ab_compare) { + + request( + plugin_manager, infinite, data_source::use_data_atom_v, uri, session, playlist, rate) + .then( + [=](UuidActorVector &new_media) { + if (!new_media.size()) + return; + + // check if we're loading media + request(new_media[0].actor(), infinite, type_atom_v) + .then( + [=](const std::string &type) { + if (type == "Media") { + // 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, + new_media, + ab_compare ? "Off" : "A/B", + quickview); + } + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { return new plugin_manager::PluginFactoryCollection( diff --git a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt index e21a7eac1..c12246157 100644 --- a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt +++ b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt @@ -7,8 +7,6 @@ set(SOURCES annotation.cpp annotation_opengl_renderer.cpp annotation_serialiser.cpp - caption.cpp - pen_stroke.cpp serialisers/1.0/serialiser_1_pt_0.cpp ) diff --git a/src/plugin/viewport_overlay/annotations/src/annotation.cpp b/src/plugin/viewport_overlay/annotations/src/annotation.cpp index d4ef671d9..44fc759c3 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation.cpp @@ -1,32 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 -#include "annotation.hpp" - #include -#include "annotation_serialiser.hpp" + +#include "annotation.hpp" #include "annotations_tool.hpp" +#include "annotation_serialiser.hpp" using namespace xstudio::ui::viewport; using namespace xstudio; -Annotation::Annotation( - std::map> &fonts, bool is_laser_annotatio) - : bookmark::AnnotationBase(), fonts_(fonts), is_laser_annotation_(is_laser_annotatio) {} -Annotation::Annotation( - const utility::JsonStore &s, std::map> &fonts) - : bookmark::AnnotationBase(utility::JsonStore(), utility::Uuid()), fonts_(fonts) { - AnnotationSerialiser::deserialise(this, s); - update_render_data(); -} +Annotation::Annotation() : bookmark::AnnotationBase() {} -Annotation::Annotation(const Annotation &o) - : bookmark::AnnotationBase(utility::JsonStore(), o.bookmark_uuid_) { - strokes_ = o.strokes_; - for (const auto &capt : o.captions_) { - captions_.emplace_back(new Caption(*capt)); - } - fonts_ = o.fonts_; - update_render_data(); +Annotation::Annotation(const utility::JsonStore &s) : bookmark::AnnotationBase() { + + AnnotationSerialiser::deserialise(this, s); } utility::JsonStore Annotation::serialise(utility::Uuid &plugin_uuid) const { @@ -34,469 +21,3 @@ utility::JsonStore Annotation::serialise(utility::Uuid &plugin_uuid) const { plugin_uuid = AnnotationsTool::PLUGIN_UUID; return AnnotationSerialiser::serialise((const Annotation *)this); } - -bool Annotation::test_click_in_caption(const Imath::V2f pointer_position) { - - if (no_fonts()) - return false; - finished_current_stroke(); - std::string::const_iterator cursor; - float r = 0.1f; - - for (auto &caption : captions_) { - - if (!caption->bounding_box_.intersects(pointer_position)) - continue; - - const std::string::const_iterator cursor_pos = - font(caption)->viewport_position_to_cursor( - pointer_position, - caption->text_, - caption->position_, - caption->wrap_width_, - caption->font_size_, - caption->justification_, - 1.0f); - - copy_of_edited_caption_.reset(new Caption(*caption.get())); - current_caption_ = caption; - cursor_position_ = cursor_pos; - break; - } - - return bool(current_caption_); -} - -void Annotation::start_new_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) { - finished_current_stroke(); - copy_of_edited_caption_.reset(); - current_caption_.reset(new Caption( - position, wrap_width, font_size, colour, opacity, justification, font_name)); - captions_.push_back(current_caption_); - update_render_data(); -} - -void Annotation::modify_caption_text(const std::string &t) { - if (current_caption_) - current_caption_->modify_text(t, cursor_position_); - update_render_data(); -} - -void Annotation::key_down(const int key) { - if (no_fonts()) - return; - - if (current_caption_) { - - if (key == 16777235) { - // up arrow - cursor_position_ = font(current_caption_) - ->cursor_up_or_down( - cursor_position_, - true, - current_caption_->text_, - current_caption_->wrap_width_, - current_caption_->font_size_, - current_caption_->justification_, - 1.0f); - - } else if (key == 16777237) { - // down arrow - cursor_position_ = font(current_caption_) - ->cursor_up_or_down( - cursor_position_, - false, - current_caption_->text_, - current_caption_->wrap_width_, - current_caption_->font_size_, - current_caption_->justification_, - 1.0f); - - } else if (key == 16777236) { - // right arrow - if (cursor_position_ != current_caption_->text_.cend()) - cursor_position_++; - - } else if (key == 16777234) { - // left arrow - if (cursor_position_ != current_caption_->text_.cbegin()) - cursor_position_--; - - } else if (key == 16777232) { - // home - cursor_position_ = current_caption_->text_.cbegin(); - - } else if (key == 16777233) { - // end - cursor_position_ = current_caption_->text_.cend(); - } - update_render_data(); - } -} - -Imath::Box2f Annotation::mouse_hover_on_captions( - const Imath::V2f cursor_position, const float viewport_pixel_scale) const { - - Imath::Box2f result; - for (const auto &caption : captions_) { - if (caption->bounding_box_.min.x < cursor_position.x && - caption->bounding_box_.min.y < cursor_position.y && - caption->bounding_box_.max.x > cursor_position.x && - caption->bounding_box_.max.y > cursor_position.y) { - - result = caption->bounding_box_; - break; - } - } - return result; -} - -Caption::HoverState Annotation::mouse_hover_on_selected_caption( - const Imath::V2f cursor_position, const float viewport_pixel_scale) const { - - Caption::HoverState result = Caption::NotHovered; - - if (current_caption_) { - // is the mouse hovering over the caption - // move / resize handles? - Imath::V2f cp = current_caption_->bounding_box_.min - cursor_position; - const auto handle_extent = - Imath::Box2f(Imath::V2f(0.0f, 0.0f), captionHandleSize * viewport_pixel_scale); - - if (handle_extent.intersects( - cp)) { //}.x > 0.0f && cp.x < captionHandleSize.x*viewport_pixel_scale && cp.y > - // 0.0f && cp.y < captionHandleSize.y/viewport_pixel_scale) { - result = Caption::HoveredOnMoveHandle; - } else { - cp = cursor_position - current_caption_->bounding_box_.max; - if (handle_extent.intersects(cp)) { - result = Caption::HoveredOnResizeHandle; - } else { - // delete handle is top left of the box - cp = cursor_position - Imath::V2f( - current_caption_->bounding_box_.max.x, - current_caption_->bounding_box_.min.y - - captionHandleSize.y * viewport_pixel_scale); - if (handle_extent.intersects(cp)) { - result = Caption::HoveredOnDeleteHandle; - } - } - } - } - - return result; -} - -void Annotation::start_pen_stroke( - const utility::ColourTriplet &c, const float thickness, const float opacity) { - finished_current_stroke(); - current_stroke_.reset(new PenStroke(c, thickness, opacity)); -} - -void Annotation::start_erase_stroke(const float thickness) { - - finished_current_stroke(); - current_stroke_.reset(new PenStroke(thickness)); -} - -void Annotation::add_point_to_current_stroke(const Imath::V2f pt) { - if (current_stroke_) { - current_stroke_->add_point(pt); - update_render_data(); - } -} - -AnnotationRenderDataPtr Annotation::render_data(const bool is_edited_annotation) const { - return cached_render_data_; -} - -void Annotation::update_render_data() { - - auto t0 = utility::clock::now(); - cached_render_data_.reset(); - - // each stroke is drawn at a slightly increasing depth so that - // strokes layer ontop of each other - render_data_.clear(); - float depth = 0.0f; - for (auto &stroke : strokes_) { - - depth += 0.001; - // i < int(strokes_.size()) ? strokes_[i] : *current_stroke_.get(); - - AnnotationRenderData::StrokeInfo info; - - info.is_erase_stroke_ = stroke.is_erase_stroke_; - - // this call converts the 'path' (mouse scribble path) into - // solid gl elements (triangles and quads) for drawing to screen - info.stroke_point_count_ = stroke.fetch_render_data(render_data_.pen_stroke_vertices_); - - info.brush_colour_ = stroke.colour_; - info.brush_opacity_ = stroke.opacity_; - info.brush_thickness_ = stroke.thickness_; - info.stroke_depth_ = depth; - - render_data_.stroke_info_.push_back(info); - } - - if (!no_fonts()) { - for (const auto &caption : captions_) { - - render_data_.caption_info_.emplace_back(AnnotationRenderData::CaptionInfo()); - - render_data_.caption_info_.back().bounding_box = - font(caption)->precompute_text_rendering_vertex_layout( - render_data_.caption_info_.back().precomputed_vertex_buffer, - caption->text_, - caption->position_, - caption->wrap_width_, - caption->font_size_, - caption->justification_, - 1.0f); - caption->bounding_box_ = render_data_.caption_info_.back().bounding_box; - render_data_.caption_info_.back().colour = caption->colour_; - render_data_.caption_info_.back().opacity = caption->opacity_; - render_data_.caption_info_.back().text_size = caption->font_size_; - render_data_.caption_info_.back().font_name = caption->font_name_; - } - } - - if (current_stroke_) { - - depth += 0.001; - auto &stroke = *current_stroke_.get(); - - AnnotationRenderData::StrokeInfo info; - - info.is_erase_stroke_ = stroke.is_erase_stroke_; - - // this call converts the 'path' (mouse scribble path) into - // solid gl elements (triangles and quads) for drawing to screen - info.stroke_point_count_ = stroke.fetch_render_data(render_data_.pen_stroke_vertices_); - - info.brush_colour_ = stroke.colour_; - info.brush_opacity_ = stroke.opacity_; - info.brush_thickness_ = stroke.thickness_; - info.stroke_depth_ = depth; - - render_data_.stroke_info_.push_back(info); - } - - cached_render_data_.reset(new AnnotationRenderData(render_data_)); -} - -void Annotation::clear() { - undo_stack_.emplace_back(static_cast(new UndoRedoClear(strokes_))); - strokes_.clear(); - captions_.clear(); - update_render_data(); -} - -void Annotation::undo() { - if (undo_stack_.size()) { - undo_stack_.back()->undo(this); - redo_stack_.push_back(undo_stack_.back()); - undo_stack_.pop_back(); - update_render_data(); - } -} - -void Annotation::redo() { - if (redo_stack_.size()) { - redo_stack_.back()->redo(this); - undo_stack_.push_back(redo_stack_.back()); - redo_stack_.pop_back(); - update_render_data(); - } -} - -std::shared_ptr -Annotation::font(const std::shared_ptr &caption) const { - auto p = fonts_.find(caption->font_name_); - if (p != fonts_.end()) - return p->second; - return fonts_.begin()->second; -} - -void Annotation::finished_current_stroke() { - if (current_stroke_) { - undo_stack_.emplace_back( - static_cast(new UndoRedoStroke(*current_stroke_.get()))); - redo_stack_.clear(); - strokes_.emplace_back(*current_stroke_.get()); - current_stroke_.reset(); - update_render_data(); - } - if (current_caption_) { - - if (current_caption_->text_.empty() && !copy_of_edited_caption_) { - auto p = std::find(captions_.begin(), captions_.end(), current_caption_); - if (p != captions_.end()) { - captions_.erase(p); - } - } else { - undo_stack_.emplace_back(static_cast( - new UndoRedoAddCaption(current_caption_, copy_of_edited_caption_))); - redo_stack_.clear(); - } - current_caption_.reset(); - copy_of_edited_caption_.reset(); - update_render_data(); - } -} - -bool Annotation::fade_strokes(const float selected_opacity) { - - auto p = strokes_.begin(); - while (p != strokes_.end()) { - if (p->opacity_ > selected_opacity * 0.95) { - p->opacity_ -= 0.005f * selected_opacity; - p++; - } else if (p->opacity_ > 0.0f) { - p->opacity_ -= 0.05f * selected_opacity; - p++; - } else { - p = strokes_.erase(p); - } - } - update_render_data(); - return !strokes_.empty(); -} - -void Annotation::set_edited_caption_position(const Imath::V2f p) { - if (current_caption_) - current_caption_->position_ = p; - update_render_data(); -} - -void Annotation::set_edited_caption_width(const float w) { - if (current_caption_) - current_caption_->wrap_width_ = std::max(0.01f, w); - update_render_data(); -} - -void Annotation::set_edited_caption_colour(const utility::ColourTriplet &c) { - if (current_caption_) - current_caption_->colour_ = c; - update_render_data(); -} - -void Annotation::set_edited_caption_opacity(const float opac) { - if (current_caption_) - current_caption_->opacity_ = opac; - update_render_data(); -} - -void Annotation::set_edit_caption_font_size(const float sz) { - if (current_caption_) - current_caption_->font_size_ = sz; - update_render_data(); -} - -void Annotation::set_edited_caption_font(const std::string &font) { - if (current_caption_) - current_caption_->font_name_ = font; - update_render_data(); -} - -void Annotation::delete_edited_caption() { - - if (current_caption_) { - if (current_caption_->text_.empty() && !copy_of_edited_caption_) { - // empty caption deletion doesn't need undo/redo - auto p = std::find(captions_.begin(), captions_.end(), current_caption_); - if (p != captions_.end()) { - captions_.erase(p); - } - } else { - copy_of_edited_caption_.reset(); - undo_stack_.emplace_back(static_cast( - new UndoRedoAddCaption(copy_of_edited_caption_, current_caption_))); - redo_stack_.clear(); - auto p = std::find(captions_.begin(), captions_.end(), current_caption_); - if (p != captions_.end()) { - captions_.erase(p); - } - current_caption_.reset(); - } - update_render_data(); - } -} - - -bool Annotation::caption_cursor_position(Imath::V2f &top, Imath::V2f &bottom) const { - - if (current_caption_) { - - Imath::V2f v = font(current_caption_) - ->get_cursor_screen_position( - current_caption_->text_, - current_caption_->position_, - current_caption_->wrap_width_, - current_caption_->font_size_, - current_caption_->justification_, - 1.0f, - cursor_position_); - - top = v; - bottom = v - Imath::V2f(0.0f, current_caption_->font_size_ * 2.0f / 1920.0f * 0.8f); - return true; - } else { - top = Imath::V2f(0.0f, 0.0f); - bottom = Imath::V2f(0.0f, 0.0f); - } - return false; -} - -void UndoRedoStroke::redo(Annotation *anno) { anno->strokes_.push_back(stroke_data_); } - -void UndoRedoStroke::undo(Annotation *anno) { - if (anno->strokes_.size()) { - anno->strokes_.pop_back(); - anno->update_render_data(); - } -} - -void UndoRedoAddCaption::redo(Annotation *anno) { - if (caption_old_state_) { - Caption c = *caption_; - *caption_ = *caption_old_state_; - *caption_old_state_ = c; - } else { - anno->captions_.push_back(caption_); - } -} - -void UndoRedoAddCaption::undo(Annotation *anno) { - if (caption_old_state_ && caption_) { - // undo a change to a caption - Caption c = *caption_; - *caption_ = *caption_old_state_; - *caption_old_state_ = c; - } else if (caption_old_state_) { - // undo a change to a caption deletion - anno->captions_.push_back(caption_old_state_); - } else { - // undo a caption creation - anno->captions_.pop_back(); - } - anno->update_render_data(); -} - -void UndoRedoClear::redo(Annotation *anno) { - anno->strokes_.clear(); - anno->update_render_data(); -} - -void UndoRedoClear::undo(Annotation *anno) { - anno->strokes_ = strokes_data_; - anno->update_render_data(); -} \ 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 d13e17525..f00c25e3a 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation.hpp @@ -3,260 +3,35 @@ #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/ui/font.hpp" -#include "pen_stroke.hpp" -#include "caption.hpp" +#include "xstudio/ui/canvas/canvas.hpp" namespace xstudio { namespace ui { namespace viewport { - class AnnotationRenderData { - public: - AnnotationRenderData() = default; - AnnotationRenderData(const AnnotationRenderData &o) = default; - - utility::Uuid uuid_; - std::vector pen_stroke_vertices_; - - struct StrokeInfo { - int stroke_point_count_; - utility::ColourTriplet brush_colour_; - float brush_opacity_; - float brush_thickness_; - float stroke_depth_; - bool is_erase_stroke_; - }; - - struct CaptionInfo { - std::vector precomputed_vertex_buffer; - utility::ColourTriplet colour; - float opacity; - float text_size; - Imath::Box2f bounding_box; - float width; - std::string font_name; - }; - - std::vector stroke_info_; - - std::vector caption_info_; - - Imath::V3f last; - - void clear() { - pen_stroke_vertices_.clear(); - stroke_info_.clear(); - caption_info_.clear(); - } - }; - - typedef std::shared_ptr AnnotationRenderDataPtr; - - class AnnotationRenderDataSet : public utility::BlindDataObject { - public: - AnnotationRenderDataSet() = default; - AnnotationRenderDataSet(const AnnotationRenderDataSet &o) = default; - - void add_annotation_render_data(AnnotationRenderDataPtr data) { - annotations_render_data_.push_back(data); - } - - std::vector::const_iterator begin() const { - return annotations_render_data_.cbegin(); - } - - std::vector::const_iterator end() const { - return annotations_render_data_.cend(); - } - - AnnotationRenderDataPtr edited_annotation_render_data_; - - bool show_caption_handles_ = false; - - private: - std::vector annotations_render_data_; - }; - - class Annotation; - - class UndoRedo { - - public: - virtual void redo(Annotation *) = 0; - virtual void undo(Annotation *) = 0; - }; - - typedef std::shared_ptr UndoRedoPtr; - class Annotation : public bookmark::AnnotationBase { public: - Annotation( - std::map> &fonts, - bool is_laser_annotation = false); - Annotation( - const utility::JsonStore &s, - std::map> &fonts); - Annotation(const Annotation &o); - - bool operator==(const Annotation &o) const { return strokes_ == o.strokes_; } - - bool empty() const { return strokes_.empty() && captions_.empty(); } + explicit Annotation(); + explicit Annotation(const utility::JsonStore &s); - bool test_click_in_caption(const Imath::V2f pointer_position); - - void start_new_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); - - void modify_caption_text(const std::string &t); - - void key_down(const int key); - - Imath::Box2f mouse_hover_on_captions( - const Imath::V2f cursor_position, const float viewport_pixel_scale) const; - - Caption::HoverState mouse_hover_on_selected_caption( - const Imath::V2f cursor_position, const float viewport_pixel_scale) const; - - void start_pen_stroke( - const utility::ColourTriplet &c, const float thickness, const float opacity); - - void start_erase_stroke(const float thickness); - - void add_point_to_current_stroke(const Imath::V2f pt); - - void finished_current_stroke(); + bool operator==(const Annotation &o) const { + return canvas_ == o.canvas_ && is_laser_annotation_ == o.is_laser_annotation_; + } [[nodiscard]] utility::JsonStore serialise(utility::Uuid &plugin_uuid) const override; - [[nodiscard]] AnnotationRenderDataPtr - render_data(const bool is_edited_annotation = false) const; - - [[nodiscard]] bool is_laser_annotation() const { return is_laser_annotation_; } - - void clear(); - - void undo(); - - void redo(); - - void update_render_data(); - - bool fade_strokes(const float selected_opacity); - - std::shared_ptr current_stroke_; - std::shared_ptr current_caption_; - std::shared_ptr copy_of_edited_caption_; - std::vector strokes_; - std::vector> captions_; - - inline static const Imath::V2f captionHandleSize = {Imath::V2f(50.0f, 50.0f)}; - - bool have_edited_caption() const { return bool(current_caption_); } - Imath::V2f edited_caption_position() const { - return current_caption_ ? current_caption_->position_ : Imath::V2f(0.0f, 0.0f); - } - Imath::Box2f edited_caption_bounding_box() const { - return current_caption_ ? current_caption_->bounding_box_ : Imath::Box2f(); - } - float edited_caption_width() const { - return current_caption_ ? current_caption_->wrap_width_ : 0.0f; - } - utility::ColourTriplet edited_caption_colour() const { - return current_caption_ ? current_caption_->colour_ : utility::ColourTriplet(); - } - float edited_caption_opacity() const { - return current_caption_ ? current_caption_->opacity_ : 1.0f; - } - std::string edited_caption_font_name() const { - return current_caption_ ? current_caption_->font_name_ : ""; - } - - float edited_caption_font_size() const { - return current_caption_ ? current_caption_->font_size_ : 50.0f; - } - - void set_edited_caption_position(const Imath::V2f p); - void set_edited_caption_width(const float w); - void set_edited_caption_colour(const utility::ColourTriplet &c); - void set_edited_caption_opacity(const float opac); - void set_edit_caption_font_size(const float sz); - void set_edited_caption_font(const std::string &font); - void delete_edited_caption(); - - bool caption_cursor_position(Imath::V2f &top, Imath::V2f &bottom) const; + xstudio::ui::canvas::Canvas &canvas() { return canvas_; } + const xstudio::ui::canvas::Canvas &canvas() const { return canvas_; } private: - bool no_fonts() const { return fonts_.empty(); } - std::shared_ptr font(const std::shared_ptr &caption) const; - - friend class UndoRedoStroke; - friend class UndoRedoClear; - - AnnotationRenderDataPtr cached_render_data_; - - std::map> fonts_; - - friend class AnnotationsRenderer; - friend class AnnotationSerialiser; - - bool stroking_ = {false}; - bool is_laser_annotation_ = {false}; - - AnnotationRenderData render_data_; - - std::vector undo_stack_; - std::vector redo_stack_; - - std::string::const_iterator cursor_position_; + bool is_laser_annotation_{false}; + xstudio::ui::canvas::Canvas canvas_; }; typedef std::shared_ptr AnnotationPtr; - class UndoRedoStroke : public UndoRedo { - - public: - UndoRedoStroke(const PenStroke &stroke) : stroke_data_(stroke) {} - - void redo(Annotation *) override; - void undo(Annotation *) override; - - PenStroke stroke_data_; - }; - - class UndoRedoAddCaption : public UndoRedo { - - public: - UndoRedoAddCaption( - std::shared_ptr &caption, std::shared_ptr &old_state) - : caption_(caption), caption_old_state_(old_state) {} - - void redo(Annotation *) override; - void undo(Annotation *) override; - - std::shared_ptr caption_; - std::shared_ptr caption_old_state_; - }; - - class UndoRedoClear : public UndoRedo { - - public: - UndoRedoClear(const std::vector &strokes) : strokes_data_(strokes) {} - - void redo(Annotation *) override; - void undo(Annotation *) override; - - std::vector strokes_data_; - }; - } // end namespace viewport } // end namespace ui } // end namespace xstudio \ No newline at end of file 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 66419672c..ea6720ba3 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp @@ -1,232 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 -#include "annotation_opengl_renderer.hpp" #include + +#include "annotation_opengl_renderer.hpp" +#include "annotation_render_data.hpp" +#include "annotations_tool.hpp" #include "xstudio/media_reader/image_buffer.hpp" #include "xstudio/utility/helpers.hpp" -using namespace xstudio::ui::viewport; using namespace xstudio; +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::viewport; -namespace { -const char *thick_line_vertex_shader = R"( - #version 430 core - #extension GL_ARB_shader_storage_buffer_object : require - out vec2 viewportCoordinate; - uniform float z_adjust; - uniform float thickness; - uniform float soft_dim; - uniform mat4 to_coord_system; - uniform mat4 to_canvas; - 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; - 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; - } - } - - soft_edge = (do_soft_edge ? soft_dim : 0.00001f); - gl_Position = vec4(vtx,0.0,1.0)*to_coord_system*to_canvas; - gl_Position.z = (zz)*gl_Position.w; - viewportCoordinate = vtx; - frag_pos = vtx; - } - )"; - -const char *thick_line_frag_shader = R"( - #version 330 core - flat in vec2 line_start; - flat in vec2 line_end; - in vec2 frag_pos; - out vec4 FragColor; - uniform vec3 brush_colour; - uniform float brush_opacity; - in float soft_edge; - uniform float thickness; - uniform bool do_soft_edge; - - float distToLine(vec2 pt) - { - - float l2 = (line_end.x - line_start.x)*(line_end.x - line_start.x) + - (line_end.y - line_start.y)*(line_end.y - line_start.y); - - if (l2 == 0.0) return length(pt-line_start); - - vec2 a = pt-line_start; - vec2 L = line_end-line_start; - - float dot = (a.x*L.x + a.y*L.y); - - float t = max(0.0, min(1.0, dot / l2)); - vec2 p = line_start + t*L; - return length(pt-p); - - } - - void main(void) - { - float r = distToLine(frag_pos); - - if (do_soft_edge) { - r = smoothstep( - thickness + soft_edge, - thickness, - r); - } else { - r = r < thickness ? 1.0f: 0.0f; - } - - if (r == 0.0f) discard; - if (do_soft_edge && r == 1.0f) { - discard; - } - float a = brush_opacity*r; - FragColor = vec4( - brush_colour*a, - a - ); - - } - - )"; - -const char *text_handles_vertex_shader = R"( - #version 430 core - uniform mat4 to_coord_system; - uniform mat4 to_canvas; - uniform vec2 box_position; - uniform vec2 box_size; - uniform vec2 aa_nudge; - uniform float du_dx; - layout (location = 0) in vec2 aPos; - //layout (location = 1) in vec2 bPos; - out vec2 screen_pixel; - - void main() - { - - // 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 - vec2 vertex_pos = aPos.xy; - vertex_pos.x = vertex_pos.x*box_size.x; - vertex_pos.y = vertex_pos.y*box_size.y; - vertex_pos += box_position + aa_nudge*du_dx; - screen_pixel = vertex_pos/du_dx; - gl_Position = vec4(vertex_pos,0.0,1.0)*to_coord_system*to_canvas; - } - )"; - -const char *text_handles_frag_shader = R"( - #version 330 core - out vec4 FragColor; - uniform bool shadow; - uniform int box_type; - uniform float opacity; - in vec2 screen_pixel; - void main(void) - { - ivec2 offset_screen_pixel = ivec2(screen_pixel) + ivec2(5000,5000); // move away from origin - if (box_type==1) { - // draws a dotted line - if (((offset_screen_pixel.x/20) & 1) == ((offset_screen_pixel.y/20) & 1)) { - FragColor = vec4(0.0f, 0.0f, 0.0f, opacity); - } else { - FragColor = vec4(1.0f, 1.0f, 1.0f, opacity); - } - } else if (box_type==2) { - FragColor = vec4(0.0f, 0.0f, 0.0f, opacity); - } else if (box_type==3) { - FragColor = vec4(0.7f, 0.7f, 0.7f, opacity); - } else { - FragColor = vec4(1.0f, 1.0f, 1.0f, opacity); - } - - } - - )"; - -static struct AAJitterTable { - - struct { - Imath::V2f operator()(int N, int i, int j) { - auto x = -0.5f + (i + 0.5f) / N; - auto y = -0.5f + (j + 0.5f) / N; - return {x, y}; - } - } gridLookup; - - AAJitterTable() { - aa_nudge.resize(16); - int lookup[16] = {11, 6, 10, 8, 9, 12, 7, 1, 3, 13, 5, 4, 2, 15, 0, 14}; - int ct = 0; - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - aa_nudge[lookup[ct]]["aa_nudge"] = gridLookup(4, i, j); - ct++; - } - } - } - - std::vector aa_nudge; -} aa_jitter_table; +AnnotationsRenderer::AnnotationsRenderer() { -} // namespace + canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); +} void AnnotationsRenderer::render_opengl( const Imath::M44f &transform_window_to_viewport_space, @@ -235,442 +25,65 @@ void AnnotationsRenderer::render_opengl( const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) { - if (!shader_) - init_overlay_opengl(); - - std::lock_guard lock(immediate_data_gate_); utility::BlindDataObjectPtr render_data = - frame.plugin_blind_data(utility::Uuid("46f386a0-cb9a-4820-8e99-fb53f6c019eb")); + frame.plugin_blind_data2(AnnotationsTool::PLUGIN_UUID); const auto *data = dynamic_cast(render_data.get()); - if (data) { - for (const auto &p : *data) { - render_annotation_to_screen( - p, - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel, - !have_alpha_buffer); - } - render_text_handles_to_screen( - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel); - return; - } - if (current_edited_annotation_render_data_) { - render_annotation_to_screen( - current_edited_annotation_render_data_, - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel, - !have_alpha_buffer); - render_text_handles_to_screen( - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel); - } -} - -void AnnotationsRenderer::render_annotation_to_screen( - const AnnotationRenderDataPtr render_data, - const Imath::M44f &transform_window_to_viewport_space, - const Imath::M44f &transform_viewport_to_image_space, - const float viewport_du_dpixel, - const bool do_erase_strokes_first) { - if (!render_data) + if (!data) { + // annotation tool hasn't attached any render data to this image. + // This means annotations aren't visible. return; - - // strokes are made up of partially overlapping triangles - as we - // draw with opacity we use depth test to stop overlapping triangles - - // in the same stroke accumulating in the alpha blend - glEnable(GL_DEPTH_TEST); - glClearDepth(0.0); - glClear(GL_DEPTH_BUFFER_BIT); - - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - glBlendEquation(GL_FUNC_ADD); - - utility::JsonStore shader_params; - shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); - shader_params["to_canvas"] = transform_window_to_viewport_space; - shader_params["soft_dim"] = viewport_du_dpixel * 4.0f; - - shader_->use(); - shader_->set_shader_parameters(shader_params); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_id_); - - const auto sz = static_cast(round(std::pow( - 2.0f, - std::ceil( - std::log(render_data->pen_stroke_vertices_.size() * sizeof(Imath::V2f)) / - std::log(2.0f))))); - - if (sz > ssbo_size_) { - - ssbo_size_ = sz; - glNamedBufferData(ssbo_id_, ssbo_size_, nullptr, GL_DYNAMIC_DRAW); - } - - if (last_data_ != render_data->pen_stroke_vertices_.data()) { - last_data_ = render_data->pen_stroke_vertices_.data(); - auto *buffer_io_ptr = (uint8_t *)glMapNamedBuffer(ssbo_id_, GL_WRITE_ONLY); - memcpy( - buffer_io_ptr, - render_data->pen_stroke_vertices_.data(), - render_data->pen_stroke_vertices_.size() * sizeof(Imath::V2f)); - glUnmapNamedBuffer(ssbo_id_); } - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ssbo_id_); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - utility::JsonStore shader_params2; - utility::JsonStore shader_params3; - shader_params3["do_soft_edge"] = true; - - GLint offset = 0; + // 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 (do_erase_strokes_first) { - glDepthFunc(GL_GREATER); - for (const auto &stroke_info : render_data->stroke_info_) { - if (!stroke_info.is_erase_stroke_) { - offset += (stroke_info.stroke_point_count_); - continue; - } - shader_params2["z_adjust"] = stroke_info.stroke_depth_; - shader_params2["brush_colour"] = stroke_info.brush_colour_; - shader_params2["brush_opacity"] = 0.0f; - shader_params2["thickness"] = stroke_info.brush_thickness_; - shader_params2["do_soft_edge"] = false; - shader_params2["point_count"] = stroke_info.stroke_point_count_; - shader_params2["offset_into_points"] = offset; - shader_->set_shader_parameters(shader_params2); - glDrawArrays(GL_QUADS, 0, (stroke_info.stroke_point_count_ - 1) * 4); - offset += (stroke_info.stroke_point_count_); - } - } - offset = 0; - for (const auto &stroke_info : render_data->stroke_info_) { + // 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.... + for (const auto &anno : frame.bookmarks()) { - if (do_erase_strokes_first && stroke_info.is_erase_stroke_) { - offset += (stroke_info.stroke_point_count_); + // .. we don't draw the annotation attached to the frame if its bookmark + // uuid is the same as the uuid of the annotation that is currently + // 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_) { + draw_interaction_canvas = true; continue; } - /* ---- First pass, draw solid stroke ---- */ - - // strokes are self-overlapping - we can't accumulate colour on the same pixel from - // different segments of the same stroke, because if opacity is not 1.0 - // the strokes don't draw correctly so we must use depth-test to prevent - // this. - // Anti-aliasing the boundary is tricky as we don't want to put down - // anti-alised edge pixels where there will be solid pixels due to some - // other segment of the same stroke, or the depth test means we punch - // little holes in the solid bit with anti-aliased edges where there - // is self-overlapping - // Thus we draw solid filled stroke (not anti-aliased) and then we - // draw a slightly thicker stroke underneath (using depth test) and this - // thick stroke has a slightly soft (fuzzy) edge that achieves anti- - // aliasing. - - // It is not perfect because of the use of glBlendEquation(GL_MAX); - // lower down when plotting the soft edge - this is because even the - // soft edge plotting overlaps in an awkward way and you get bad artifacts - // if you try other strategies .... - // Drawing different, bright colours over each other where opacity is - // not 1.0 shows up a subtle but noticeable flourescent glow effect. - // Solutions on a postcard please! - - // so this prevents overlapping quads from same stroke accumulating together - glDepthFunc(GL_GREATER); - - if (stroke_info.is_erase_stroke_) { - glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); - } else { - glBlendEquation(GL_FUNC_ADD); - } - - // set up the shader uniforms - strok thickness, colour etc - shader_params2["z_adjust"] = stroke_info.stroke_depth_; - shader_params2["brush_colour"] = stroke_info.brush_colour_; - shader_params2["brush_opacity"] = stroke_info.brush_opacity_; - shader_params2["thickness"] = stroke_info.brush_thickness_; - shader_params2["do_soft_edge"] = false; - shader_params2["point_count"] = stroke_info.stroke_point_count_; - shader_params2["offset_into_points"] = offset; - shader_->set_shader_parameters(shader_params2); - - // For each adjacent PAIR of points in a stroke, we draw a quad of - // the required thickness (rectangle) that connects them. We then draw a quad centered - // over every point in the stroke of width & height matching the line - // thickness to plot a circle that fills in the gaps left between the - // rectangles we have already joined, giving rounded start and end caps - // 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_info.stroke_point_count_ - 1) * 4); - - /* ---- Scond pass, draw soft edged stroke underneath ---- */ - - // Edge fragments have transparency and we want the most opaque fragment - // to be plotted, we achieve this by letting them all plot - glDepthFunc(GL_GEQUAL); - - if (stroke_info.is_erase_stroke_) { - // glBlendEquation(GL_MAX); - } else { - glBlendEquation(GL_MAX); - } - - shader_params3["do_soft_edge"] = true; - shader_->set_shader_parameters(shader_params3); - glDrawArrays(GL_QUADS, 0, (stroke_info.stroke_point_count_ - 1) * 4); - - offset += (stroke_info.stroke_point_count_); - } - - glBlendEquation(GL_FUNC_ADD); - glBindVertexArray(0); - - shader_->stop_using(); - - /* draw captions to screen */ - if (text_renderers_.size()) { - for (const auto &caption_info : render_data->caption_info_) { - - auto p = text_renderers_.find(caption_info.font_name); - auto text_renderer = - (p == text_renderers_.end()) ? text_renderers_.begin()->second : p->second; - - text_renderer->render_text( - caption_info.precomputed_vertex_buffer, + const Annotation *my_annotation = + dynamic_cast(anno->annotation_.get()); + if (my_annotation) { + canvas_renderer_->render_canvas( + my_annotation->canvas(), + data->handle_, transform_window_to_viewport_space, transform_viewport_to_image_space, - caption_info.colour, viewport_du_dpixel, - caption_info.text_size, - caption_info.opacity); + have_alpha_buffer); } } -} - -void AnnotationsRenderer::render_text_handles_to_screen( - const Imath::M44f &transform_window_to_viewport_space, - const Imath::M44f &transform_viewport_to_image_space, - const float viewport_du_dpixel) { - - utility::JsonStore shader_params; - - shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); - shader_params["to_canvas"] = transform_window_to_viewport_space; - shader_params["du_dx"] = viewport_du_dpixel; - shader_params["box_type"] = 0; - - glDisable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glBlendEquation(GL_FUNC_ADD); - - text_handles_shader_->use(); - text_handles_shader_->set_shader_parameters(shader_params); - - utility::JsonStore shader_params2; - - if (!current_caption_bdb_.isEmpty()) { - - // draw the box around the current edited caption - shader_params2["box_position"] = current_caption_bdb_.min; - shader_params2["box_size"] = current_caption_bdb_.size(); - shader_params2["opacity"] = 0.6; - shader_params2["box_type"] = 1; - shader_params2["aa_nudge"] = Imath::V2f(0.0f, 0.0f); - - - text_handles_shader_->set_shader_parameters(shader_params2); - glBindVertexArray(handles_vertex_array_); - glLineWidth(2.0f); - glDrawArrays(GL_LINE_LOOP, 0, 4); - - const auto handle_size = Annotation::captionHandleSize * viewport_du_dpixel; - - // Draw the three - static const auto hndls = std::vector( - {Caption::HoveredOnMoveHandle, - Caption::HoveredOnResizeHandle, - Caption::HoveredOnDeleteHandle}); - - static const auto vtx_offsets = std::vector({4, 14, 24}); - static const auto vtx_counts = std::vector({20, 10, 4}); - - const auto positions = std::vector( - {current_caption_bdb_.min - handle_size, - current_caption_bdb_.max, - {current_caption_bdb_.max.x, current_caption_bdb_.min.y - handle_size.y}}); - - shader_params2["box_size"] = handle_size; - - glBindVertexArray(handles_vertex_array_); - - // draw a grey box for each handle - shader_params2["opacity"] = 0.6f; - for (size_t i = 0; i < hndls.size(); ++i) { - shader_params2["box_position"] = positions[i]; - shader_params2["box_type"] = 2; - text_handles_shader_->set_shader_parameters(shader_params2); - glDrawArrays(GL_QUADS, 0, 4); - } - - static const auto aa_jitter = std::vector( - {{-0.33f, -0.33f}, - {-0.0f, -0.33f}, - {0.33f, -0.33f}, - {-0.33f, 0.0f}, - {0.0f, 0.0f}, - {0.33f, 0.0f}, - {-0.33f, 0.33f}, - {0.0f, 0.33f}, - {0.33f, 0.33f}}); - - shader_params2["box_size"] = handle_size * 0.8f; - // draw the lines for each handle - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - shader_params2["opacity"] = 1.0f / 16.0f; - for (size_t i = 0; i < hndls.size(); ++i) { - shader_params2["box_position"] = positions[i] + 0.1f * handle_size; - shader_params2["box_type"] = caption_hover_state_ == hndls[i] ? 4 : 3; - text_handles_shader_->set_shader_parameters(shader_params2); - // plot it 9 times with anti-aliasing jitter to get a better looking - // result - utility::JsonStore param; - for (const auto &aa_nudge : aa_jitter_table.aa_nudge) { - text_handles_shader_->set_shader_parameters(aa_nudge); - glDrawArrays(GL_LINES, vtx_offsets[i], vtx_counts[i]); - } - } - } - - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - if (!under_mouse_caption_bdb_.isEmpty()) { - shader_params2["box_position"] = under_mouse_caption_bdb_.min; - shader_params2["box_size"] = under_mouse_caption_bdb_.size(); - shader_params2["opacity"] = 0.3; - shader_params2["box_type"] = 1; - - text_handles_shader_->set_shader_parameters(shader_params2); - - glBindVertexArray(handles_vertex_array_); - - glLineWidth(2.0f); - glDrawArrays(GL_LINE_LOOP, 0, 4); - } - - if (cursor_position_[0] != Imath::V2f(0.0f, 0.0f)) { - shader_params2["opacity"] = 0.6f; - shader_params2["box_position"] = cursor_position_[0]; - shader_params2["box_size"] = cursor_position_[1] - cursor_position_[0]; - shader_params2["box_type"] = text_cursor_blink_state_ ? 2 : 0; - text_handles_shader_->set_shader_parameters(shader_params2); - glBindVertexArray(handles_vertex_array_); - glLineWidth(3.0f); - glDrawArrays(GL_LINE_LOOP, 0, 4); - } - - glBindVertexArray(0); -} - -void AnnotationsRenderer::init_overlay_opengl() { - - glGenBuffers(1, &ssbo_id_); - - shader_ = std::make_unique( - thick_line_vertex_shader, thick_line_frag_shader); - - text_handles_shader_ = std::make_unique( - text_handles_vertex_shader, text_handles_frag_shader); - - auto font_files = Fonts::available_fonts(); - for (const auto &f : font_files) { - try { - auto font = new ui::opengl::OpenGLTextRendererSDF(f.second, 96); - text_renderers_[f.first].reset(font); - } catch (std::exception &e) { - spdlog::warn("Failed to load font: {}.", e.what()); - } + // 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_)) { + canvas_renderer_->render_canvas( + data->interaction_canvas_, + data->handle_, + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dpixel, + have_alpha_buffer); } - - init_caption_handles_graphics(); -} - -void AnnotationsRenderer::init_caption_handles_graphics() { - - glGenBuffers(1, &handles_vertex_buffer_obj_); - glGenVertexArrays(1, &handles_vertex_array_); - - static std::array handles_vertices = { - - // unit box for drawing boxes! - Imath::V2f(0.0f, 0.0f), - Imath::V2f(1.0f, 0.0f), - Imath::V2f(1.0f, 1.0f), - Imath::V2f(0.0f, 1.0f), - - // double headed arrow, vertical - Imath::V2f(0.5f, 0.0f), - Imath::V2f(0.5f, 1.0f), - - Imath::V2f(0.5f, 0.0f), - Imath::V2f(0.5f - 0.2f, 0.2f), - - Imath::V2f(0.5f, 0.0f), - Imath::V2f(0.5f + 0.2f, 0.2f), - - Imath::V2f(0.5f, 1.0f), - Imath::V2f(0.5f - 0.2f, 1.0f - 0.2f), - - Imath::V2f(0.5f, 1.0f), - Imath::V2f(0.5f + 0.2f, 1.0f - 0.2f), - - // double headed arrow, horizontal - Imath::V2f(0.0f, 0.5f), - Imath::V2f(1.0f, 0.5f), - - Imath::V2f(0.0f, 0.5f), - Imath::V2f(0.2f, 0.5f - 0.2f), - - Imath::V2f(0.0f, 0.5f), - Imath::V2f(0.2f, 0.5f + 0.2f), - - Imath::V2f(1.0f, 0.5f), - Imath::V2f(1.0f - 0.2f, 0.5f - 0.2f), - - Imath::V2f(1.0f, 0.5f), - Imath::V2f(1.0f - 0.2f, 0.5f + 0.2f), - - // crossed lines - Imath::V2f(0.2f, 0.2f), - Imath::V2f(0.8f, 0.8f), - Imath::V2f(0.8f, 0.2f), - Imath::V2f(0.2f, 0.8f), - - }; - - glBindVertexArray(handles_vertex_array_); - // 2. copy our vertices array in a buffer for OpenGL to use - glBindBuffer(GL_ARRAY_BUFFER, handles_vertex_buffer_obj_); - glBufferData( - GL_ARRAY_BUFFER, sizeof(handles_vertices), handles_vertices.data(), GL_STATIC_DRAW); - // 3. then set our vertex module pointers - glEnableVertexAttribArray(0); - // glEnableVertexAttribArray(1); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Imath::V2f), nullptr); - // glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 2*sizeof(Imath::V2f), (void - // *)sizeof(Imath::V2f)); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); -} +} \ No newline at end of file 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 b53fe7ea5..309667a9a 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp @@ -2,10 +2,10 @@ #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/ui/opengl/opengl_canvas_renderer.hpp" #include "annotation.hpp" + namespace xstudio { namespace ui { namespace viewport { @@ -13,6 +13,8 @@ namespace ui { class AnnotationsRenderer : public plugin::ViewportOverlayRenderer { public: + AnnotationsRenderer(); + void render_opengl( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, @@ -20,77 +22,10 @@ namespace ui { const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) override; - RenderPass preferred_render_pass() const { return BeforeImage; } - - void set_edited_annotation_render_data( - AnnotationRenderDataPtr data, - const bool show_text_handles = false, - const Imath::V2f &pointer_pos = Imath::V2f(0.0f, 0.0f)) { - current_edited_annotation_render_data_ = data; - show_text_handles_ = show_text_handles; - } - - void set_caption_hover_state(const Caption::HoverState state) { - caption_hover_state_ = state; - } - - void set_under_mouse_caption_bdb(const Imath::Box2f &bdb) { - under_mouse_caption_bdb_ = bdb; - } - - void set_current_edited_caption_bdb(const Imath::Box2f &bdb) { - current_caption_bdb_ = bdb; - } - - void set_cursor_position(const Imath::V2f top, const Imath::V2f bottom) { - cursor_position_[0] = top; - cursor_position_[1] = bottom; - } - - void blink_text_cursor(const bool show_cursor) { - text_cursor_blink_state_ = show_cursor; - } - - void lock() { immediate_data_gate_.lock(); } - void unlock() { immediate_data_gate_.unlock(); } + RenderPass preferred_render_pass() const override { return BeforeImage; } private: - void render_annotation_to_screen( - const AnnotationRenderDataPtr render_data, - const Imath::M44f &transform_window_to_viewport_space, - const Imath::M44f &transform_viewport_to_image_space, - const float viewport_du_dpixel, - const bool do_erase_strokes_first); - - void render_text_handles_to_screen( - const Imath::M44f &transform_window_to_viewport_space, - const Imath::M44f &transform_viewport_to_image_space, - const float viewport_du_dpixel); - - void init_overlay_opengl(); - void init_caption_handles_graphics(); - - std::unique_ptr shader_, shader2_; - std::unique_ptr text_handles_shader_; - - typedef std::shared_ptr FontRenderer; - std::map text_renderers_; - - GLuint ssbo_id_; - GLuint ssbo_size_ = {0}; - - std::mutex immediate_data_gate_; - utility::BlindDataObjectPtr immediate_data_; - AnnotationRenderDataPtr current_edited_annotation_render_data_; - const void *last_data_ = {nullptr}; - Imath::Box2f under_mouse_caption_bdb_, current_caption_bdb_; - Imath::V2f cursor_position_[2]; - Caption::HoverState caption_hover_state_ = {Caption::NotHovered}; - bool show_text_handles_ = {false}; - GLuint handles_vertex_buffer_obj_; - GLuint handles_vertex_array_; - - bool text_cursor_blink_state_ = {false}; + std::unique_ptr canvas_renderer_; }; } // 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 new file mode 100644 index 000000000..ef2c8448a --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/utility/blind_data.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + class AnnotationRenderDataSet : public utility::BlindDataObject { + public: + AnnotationRenderDataSet( + const xstudio::ui::canvas::Canvas &interaction_canvas, + const utility::Uuid ¤t_edited_bookmark_uuid, + const xstudio::ui::canvas::HandleState handle, + const std::string &interaction_frame_key) + : interaction_canvas_(interaction_canvas), + 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_; + + const utility::Uuid current_edited_bookmark_uuid_; + const xstudio::ui::canvas::HandleState handle_; + const std::string interaction_frame_key_; + }; + + } // namespace viewport +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp index 08b718222..95634b2d1 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp @@ -1,23 +1,51 @@ // SPDX-License-Identifier: Apache-2.0 -#include "annotations_tool.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/utility/helpers.hpp" #include "xstudio/utility/media_reference.hpp" +#include "xstudio/ui/font.hpp" #include "xstudio/ui/viewport/viewport_helpers.hpp" -#include -#include +#include "annotations_tool.hpp" +#include "annotation_render_data.hpp" using namespace xstudio; +using namespace xstudio::bookmark; +using namespace xstudio::ui::canvas; using namespace xstudio::ui::viewport; +namespace { + + +bool find_annotation_by_uuid( + const std::vector &annotations, const utility::Uuid &uuid) { + + const auto it = + std::find_if(annotations.begin(), annotations.end(), [=](const auto &annotation) { + return annotation->bookmark_uuid_ == uuid; + }); + + return it != annotations.end(); +} + +std::vector annotations_uuids(const std::vector &annotations) { + std::vector res; + for (const auto &anno : annotations) { + res.push_back(anno->bookmark_uuid_); + } + return res; +} + +} // anonymous namespace + +static int __a_idx = 0; AnnotationsTool::AnnotationsTool( caf::actor_config &cfg, const utility::JsonStore &init_settings) - : plugin::StandardPlugin(cfg, "AnnotationsTool", init_settings) { + : plugin::StandardPlugin(cfg, fmt::format("AnnotationsTool{}", __a_idx++), init_settings) { module::QmlCodeAttribute *button = add_qml_code_attribute( "MyCode", @@ -28,17 +56,32 @@ AnnotationsButton { } )"); - button->expose_in_ui_attrs_group("media_tools_buttons"); + // 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); - load_fonts(); - - font_choice_ = add_string_choice_attribute( + 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("annotations_tool_fonts"); + font_choice_->expose_in_ui_attrs_group(fonts_group); draw_pen_size_ = add_integer_attribute("Draw Pen Size", "Draw Pen Size", 10, 1, 300); @@ -53,12 +96,22 @@ AnnotationsButton { pen_colour_ = add_colour_attribute( "Pen Colour", "Pen Colour", utility::ColourTriplet(0.5f, 0.4f, 1.0f)); - 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_ = add_colour_attribute( + "Text Background Colour", + "Text Background Colour", + utility::ColourTriplet(0.0f, 0.0f, 0.0f)); + + 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_->set_preference_path("/plugin/annotations/draw_pen_size"); shapes_pen_size_->set_preference_path("/plugin/annotations/shapes_pen_size"); @@ -66,6 +119,8 @@ AnnotationsButton { text_size_->set_preference_path("/plugin/annotations/text_size"); pen_opacity_->set_preference_path("/plugin/annotations/pen_opacity"); pen_colour_->set_preference_path("/plugin/annotations/pen_colour"); + text_bgr_colour_->set_preference_path("/plugin/annotations/text_bgr_colour"); + text_bgr_opacity_->set_preference_path("/plugin/annotations/text_bgr_opacity"); // we can register a preference path with each of these attributes. xStudio // will then automatically intialised the attribute values from preference @@ -79,11 +134,11 @@ AnnotationsButton { utility::map_value_to_vec(tool_names_).front(), 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"); + 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("annotations_tool_settings"); + shape_tool_->expose_in_ui_attrs_group(tools_group); shape_tool_->set_preference_path("/plugin/annotations/shape_tool"); draw_mode_ = add_string_choice_attribute( @@ -91,44 +146,28 @@ AnnotationsButton { "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("anno_scribble_mode_backend"); + 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})); - - // Here we declare QML code to instantiate the actual item that draws - // the overlay on the viewport. Any attribute that has qml_code role data - // and that is exposed in the "viewport_overlay_plugins" attributes group will be - // instantiated as a child of the xStudio 'Viewport' QML Item, and will - // therefore be stacked on top of the viewport and has visibility on any - // properties of the main Viewport class. - auto viewport_code = add_qml_code_attribute( - "MyCode", - R"( - import AnnotationsTool 1.0 - AnnotationsTextItems { - } - )"); - viewport_code->expose_in_ui_attrs_group("viewport_overlay_plugins"); - tool_is_active_ = add_boolean_attribute("annotations_tool_active", "annotations_tool_active", false); - tool_is_active_->expose_in_ui_attrs_group("annotations_tool_settings"); + 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"})); action_attribute_ = add_string_attribute("action_attribute", "action_attribute", ""); - action_attribute_->expose_in_ui_attrs_group("annotations_tool_settings"); + action_attribute_->expose_in_ui_attrs_group(tools_group); 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("annotations_tool_draw_mode"); + display_mode_attribute_->expose_in_ui_attrs_group(draw_mode_group); display_mode_attribute_->set_preference_path("/plugin/annotations/display_mode"); // this attr is used to implement the blinking cursor for caption edit mode @@ -137,21 +176,22 @@ AnnotationsButton { moving_scaling_text_attr_ = add_integer_attribute("moving_scaling_text", "moving_scaling_text", 0); - moving_scaling_text_attr_->expose_in_ui_attrs_group("annotations_tool_settings"); + moving_scaling_text_attr_->expose_in_ui_attrs_group(tools_group); // setting the active tool to -1 disables drawing via 'attribute_changed' attribute_changed(active_tool_->uuid(), module::Attribute::Value); + make_behavior(); + listen_to_playhead_events(true); +} + +AnnotationsTool::~AnnotationsTool() {} + +caf::message_handler AnnotationsTool::message_handler_extensions() { + // provide an extension to the base class message handler to handle timed // callbacks to fade the laser pen strokes - message_handler_ = { - [=](utility::event_atom, bool) { - // 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); - } - }, + return caf::message_handler( [=](utility::event_atom, bool) { // this message is sent when the user finishes a laser bruish stroke if (!fade_looping_) { @@ -160,33 +200,155 @@ AnnotationsButton { } }, [=](utility::event_atom) { - // note Annotation::fade_strokes() returns false when all strokes have vanished - if (is_laser_mode() && current_edited_annotation_ && - current_edited_annotation_->fade_strokes(pen_opacity_->value() / 100.f)) { + // 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); } else { fade_looping_ = false; } redraw_viewport(); - }}; - - make_behavior(); - listen_to_playhead_events(true); + }); } -AnnotationsTool::~AnnotationsTool() = default; +void AnnotationsTool::attribute_changed( + const utility::Uuid &attribute_uuid, const int /*role*/) { -void AnnotationsTool::load_fonts() { + const std::string active_tool = active_tool_->value(); - auto font_files = Fonts::available_fonts(); - for (const auto &f : font_files) { - try { - auto font = new SDFBitmapFont(f.second, 96); - fonts_[f.first].reset(font); - } catch (std::exception &e) { - spdlog::warn("Failed to load font: {}.", e.what()); + if (attribute_uuid == tool_is_active_->uuid()) { + + 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(); + } + + } else if (attribute_uuid == active_tool_->uuid()) { + + if (tool_is_active_->value()) { + + if (active_tool == "None") { + release_mouse_focus(); + } else { + grab_mouse_focus(); + } + + if (active_tool == "Text") { + } else { + end_drawing(); + release_keyboard_focus(); + clear_caption_handle(); + } + } + + } else if ( + attribute_uuid == action_attribute_->uuid() && action_attribute_->value() != "") { + + if (action_attribute_->value() == "Clear") { + clear_onscreen_annotations(); + } else if (action_attribute_->value() == "Undo") { + undo(); + } else if (action_attribute_->value() == "Redo") { + redo(); + } + action_attribute_->set_value(""); + + } else if (attribute_uuid == display_mode_attribute_->uuid()) { + + if (display_mode_attribute_->value() == "Only When Paused") { + 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()) { + + handle_state_.cursor_blink_state = text_cursor_blink_attr_->value(); + + 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), + module::change_attribute_value_atom_v, + attribute_uuid, + utility::JsonStore(!text_cursor_blink_attr_->value()), + true); + } + + } else if (attribute_uuid == pen_colour_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + + interaction_canvas_.update_caption_colour(pen_colour_->value()); + } + + } else if (attribute_uuid == text_size_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + interaction_canvas_.update_caption_font_size(text_size_->value()); + update_caption_handle(); + } + } else if (attribute_uuid == pen_opacity_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + + interaction_canvas_.update_caption_opacity(pen_opacity_->value() / 100.0f); + } + } else if (attribute_uuid == font_choice_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + + interaction_canvas_.update_caption_font_name(font_choice_->value()); + update_caption_handle(); + } + } else if (attribute_uuid == text_bgr_colour_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + + interaction_canvas_.update_caption_background_colour(text_bgr_colour_->value()); + } + } else if (attribute_uuid == text_bgr_opacity_->uuid()) { + + if (interaction_canvas_.has_selected_caption()) { + + interaction_canvas_.update_caption_background_opacity( + text_bgr_opacity_->value() / 100.0f); + } + } + + 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(); +} + +void AnnotationsTool::update_attrs_from_preferences(const utility::JsonStore &j) { + + Module::update_attrs_from_preferences(j); + + // this ensures that 'display_mode_' member data is up to date after being + // updated from prefs + attribute_changed(display_mode_attribute_->uuid(), module::Attribute::Value); } void AnnotationsTool::register_hotkeys() { @@ -211,56 +373,6 @@ void AnnotationsTool::register_hotkeys() { "Redoes your last undone edit on an annotation"); } -void AnnotationsTool::on_playhead_playing_changed(const bool is_playing) { - playhead_is_playing_ = is_playing; -} - -utility::BlindDataObjectPtr AnnotationsTool::prepare_render_data( - const media_reader::ImageBufPtr &image, const bool offscreen) const { - - const bool annoations_visible = - offscreen || ((display_mode_ == Always) || - (display_mode_ == WithDrawTool && tool_is_active_->value()) || - (display_mode_ == OnlyWhenPaused && !playhead_is_playing_)) && - (current_edited_annotation_ || current_viewed_annotations_.size()); - - utility::BlindDataObjectPtr r; - if (annoations_visible) { - - auto *render_data_set = new AnnotationRenderDataSet(); - for (auto &anno_uuid : current_viewed_annotations_) { - - if (current_edited_annotation_ && - current_edited_annotation_->bookmark_uuid_ == anno_uuid) - continue; - - auto p = annotations_render_data_.find(anno_uuid); - if (p != annotations_render_data_.end()) { - render_data_set->add_annotation_render_data(p->second); - } - } - - if (current_edited_annotation_) { - render_data_set->add_annotation_render_data( - current_edited_annotation_->render_data()); - } - r.reset(static_cast(render_data_set)); - } else { - for (auto &r : renderers_) { - r->set_edited_annotation_render_data(AnnotationRenderDataPtr()); - } - } - - - return r; -} - -plugin::ViewportOverlayRendererPtr -AnnotationsTool::make_overlay_renderer(const int /*viewer_index*/) { - renderers_.push_back(new AnnotationsRenderer()); - return plugin::ViewportOverlayRendererPtr(renderers_.back()); -} - void AnnotationsTool::hotkey_pressed( const utility::Uuid &hotkey_uuid, const std::string & /*context*/) { if (hotkey_uuid == toggle_active_hotkey_) { @@ -287,789 +399,530 @@ bool AnnotationsTool::pointer_event(const ui::PointerEvent &e) { if (!tool_is_active_->value()) return false; - // we don't a render to screen to start until we've processed this pointer - // event, the reason is that we might get this pointer event at about the - // same time as a redraw is happening - for low latency drawing we want to - // make sure the annotation renderer get's the updated graphics for the next - // refresh. That means whatever happens in this function *must* be quick as - // we could be holding up the whole UI! - interact_start(); - - bool redraw = false; + bool redraw = true; const Imath::V2f pointer_pos = e.position_in_viewport_coord_sys(); - if (e.type() == ui::Signature::EventType::ButtonRelease && current_edited_annotation_ && - active_tool_->value() != "Text") { - - end_stroke(pointer_pos); - redraw = true; - - } else if ( - e.buttons() == ui::Signature::Button::Left && - (active_tool_->value() == "Draw" || active_tool_->value() == "Erase")) { - - if (e.type() == ui::Signature::EventType::ButtonDown) { - start_freehand_pen_stroke(pointer_pos); - } else if (e.type() == ui::Signature::EventType::Drag) { - freehand_pen_stroke_point(pointer_pos); - } - redraw = true; - - } else if ( - e.buttons() == ui::Signature::Button::Left && active_tool_->value() == "Shapes") { - - if (e.type() == ui::Signature::EventType::ButtonDown) { - start_shape_placement(pointer_pos); - } else if (e.type() == ui::Signature::EventType::Drag) { - update_shape_placement(pointer_pos); - } - redraw = true; - - } else if (e.buttons() == ui::Signature::Button::Left && active_tool_->value() == "Text") { - - if (e.type() == ui::Signature::EventType::ButtonDown) { + if (active_tool_->value() == "Draw" || active_tool_->value() == "Erase") { + if (e.type() == ui::Signature::EventType::ButtonDown && + e.buttons() == ui::Signature::Button::Left) { + start_editing(e.context()); + start_stroke(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) { + end_drawing(); + } + } else if (active_tool_->value() == "Shapes") { + if (e.type() == ui::Signature::EventType::ButtonDown && + e.buttons() == ui::Signature::Button::Left) { + start_editing(e.context()); + start_shape(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) { + end_drawing(); + } + } else if (active_tool_->value() == "Text") { + if (e.type() == ui::Signature::EventType::ButtonDown && + e.buttons() == ui::Signature::Button::Left) { + start_editing(e.context()); start_or_edit_caption(pointer_pos, e.viewport_pixel_scale()); grab_mouse_focus(); - } else if (e.type() == ui::Signature::EventType::Drag) { - caption_drag(pointer_pos); + } else if ( + e.type() == ui::Signature::EventType::Drag && + e.buttons() == ui::Signature::Button::Left) { + start_editing(e.context()); + update_caption_action(pointer_pos); + update_caption_handle(); + } else if (e.buttons() == ui::Signature::Button::None) { + redraw = update_caption_hovered(pointer_pos, e.viewport_pixel_scale()); } - redraw = true; - - } else if (e.buttons() == ui::Signature::Button::None && active_tool_->value() == "Text") { - - redraw = check_pointer_hover_on_text(pointer_pos, e.viewport_pixel_scale()); + } else { + redraw = false; } - interact_end(); if (redraw) redraw_viewport(); + return false; } -void AnnotationsTool::interact_start() { +void AnnotationsTool::text_entered(const std::string &text, const std::string &context) { - if (interacting_with_renderers_) - return; - for (auto &r : renderers_) { - r->lock(); + if (active_tool_->value() == "Text") { + interaction_canvas_.update_caption_text(text); + update_caption_handle(); + redraw_viewport(); } - interacting_with_renderers_ = true; } -void AnnotationsTool::interact_end() { +void AnnotationsTool::key_pressed( + const int key, const std::string &context, const bool auto_repeat) { - if (!interacting_with_renderers_) - return; - if (current_edited_annotation_) { - for (auto &r : renderers_) { - r->set_edited_annotation_render_data( - current_edited_annotation_->render_data(), active_tool_->value() == "Text"); - } - } else { - for (auto &r : renderers_) { - r->set_edited_annotation_render_data(AnnotationRenderDataPtr()); + if (active_tool_->value() == "Text") { + if (key == 16777216) { // escape key + end_drawing(); + release_keyboard_focus(); } + interaction_canvas_.move_caption_cursor(key); + update_caption_handle(); + redraw_viewport(); } - for (auto &r : renderers_) { - r->unlock(); - } - interacting_with_renderers_ = false; } -void AnnotationsTool::start_or_edit_caption( - const Imath::V2f &p, const float viewport_pixel_scale) { - - if (current_edited_annotation_ && hover_state_ != Caption::NotHovered) { - - if (hover_state_ == Caption::HoveredOnMoveHandle) { - caption_drag_pointer_start_pos_ = p; - caption_drag_caption_start_pos_ = - current_edited_annotation_->edited_caption_position(); - } else if (hover_state_ == Caption::HoveredOnResizeHandle) { - caption_drag_pointer_start_pos_ = p; - caption_drag_width_height_ = Imath::V2f( - current_edited_annotation_->edited_caption_width(), - current_edited_annotation_->edited_caption_bounding_box().max.y); - } else if (hover_state_ == Caption::HoveredOnDeleteHandle) { - current_edited_annotation_->delete_edited_caption(); - clear_caption_overlays(); - release_keyboard_focus(); - return; - } else if ( - hover_state_ == Caption::HoveredInCaptionArea && - current_edited_annotation_->test_click_in_caption(p)) { - pen_colour_->set_value(current_edited_annotation_->edited_caption_colour()); - text_size_->set_value(current_edited_annotation_->edited_caption_font_size()); - font_choice_->set_value(current_edited_annotation_->edited_caption_font_name()); - } - - for (auto &r : renderers_) { - r->set_under_mouse_caption_bdb(Imath::Box2f()); +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(); + } + + 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_); + } + + // 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_, + handle_state_, + onscreen_interaction_frame); + + return utility::BlindDataObjectPtr( + static_cast(immediate_render_data)); +} + +void AnnotationsTool::images_going_on_screen( + const std::vector &images, + const std::string viewport_name, + const bool playhead_playing) { + + // each viewport will call this function shortly before it refreshes to + // draw the image data of 'images'. + // Because bookmark data is attached to the images, we can work out + // if the bookmark that we might be in the process of adding annotations + // to is visible on screen for this viewport. If not, it could be that + // the user has scrubbed the timeline since our last edit. + + 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(); + + 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; + } + } } + 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(); + interaction_canvas_.clear(true); + clear_caption_handle(); + current_bookmark_uuid_ = utility::Uuid(); - } else { - - if (!current_edited_annotation_) - create_new_annotation(); - - // if there was an 'on screen annotation' this has now - // been made into the current_edited_annotation_ so we - // should check if the user was clicking on an existing - // caption ... - check_pointer_hover_on_text(p, viewport_pixel_scale); - - // ... ok user was clicking on an existing caption, re-enter this - // function to run the bit in the other half of this if() block - if (current_edited_annotation_ && hover_state_ != Caption::NotHovered) { - start_or_edit_caption(p, viewport_pixel_scale); - return; + // calling these updates the renderes with the now cleared + // interaction canvas data } - - current_edited_annotation_->start_new_caption( - p, - text_size_->value() * 0.01f, - text_size_->value(), - pen_colour_->value(), - pen_opacity_->value() / 100.0f, - JustifyLeft, - font_choice_->value()); } - - grab_keyboard_focus(); - update_caption_overlay(); - text_cursor_blink_attr_->set_value(!text_cursor_blink_attr_->value()); - - current_edited_annotation_->update_render_data(); } -void AnnotationsTool::caption_drag(const Imath::V2f &p) { - - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { - - const auto delta = p - caption_drag_pointer_start_pos_; - if (hover_state_ == Caption::HoveredOnMoveHandle) { - current_edited_annotation_->set_edited_caption_position( - caption_drag_caption_start_pos_ + delta); - } else if (hover_state_ == Caption::HoveredOnResizeHandle) { - current_edited_annotation_->set_edited_caption_width( - caption_drag_width_height_.x + delta.x); - } - - update_caption_overlay(); - } +plugin::ViewportOverlayRendererPtr +AnnotationsTool::make_overlay_renderer(const int /*viewer_index*/) { + return plugin::ViewportOverlayRendererPtr(new AnnotationsRenderer()); } -void AnnotationsTool::update_caption_overlay() { - - if (current_edited_annotation_) { - - auto edited_capt_bdb = current_edited_annotation_->edited_caption_bounding_box(); - Imath::V2f t, b; - current_edited_annotation_->caption_cursor_position(t, b); - interact_start(); - for (auto &r : renderers_) { - r->set_current_edited_caption_bdb(edited_capt_bdb); - r->set_cursor_position(t, b); - } - interact_end(); - } else { - interact_start(); - for (auto &r : renderers_) { - r->set_current_edited_caption_bdb(Imath::Box2f()); - r->set_cursor_position(Imath::V2f(0.0f, 0.0f), Imath::V2f(0.0f, 0.0f)); - } - interact_end(); - } +AnnotationBasePtr AnnotationsTool::build_annotation(const utility::JsonStore &anno_data) { + return AnnotationBasePtr( + static_cast(new Annotation(anno_data))); } -void AnnotationsTool::start_shape_placement(const Imath::V2f &p) { - - if (!current_edited_annotation_) - create_new_annotation(); - - shape_anchor_ = p; - shape_stroke_.reset(new PenStroke( - pen_colour_->value(), - float(shapes_pen_size_->value()) / PEN_STROKE_THICKNESS_SCALE, - float(pen_opacity_->value()) / 100.0f)); - current_edited_annotation_->current_stroke_ = shape_stroke_; - update_shape_placement(p); +bool AnnotationsTool::is_laser_mode() const { + return active_tool_->value() == "Draw" && draw_mode_->value() == "Laser"; } -void AnnotationsTool::update_shape_placement(const Imath::V2f &pointer_pos) { - - if (shape_tool_->value() == Square) { - - shape_stroke_->make_square(shape_anchor_, pointer_pos); - - } else if (shape_tool_->value() == Circle) { - - shape_stroke_->make_circle(shape_anchor_, (shape_anchor_ - pointer_pos).length()); - - } else if (shape_tool_->value() == Arrow) { +void AnnotationsTool::start_editing(const std::string &viewport_name) { - shape_stroke_->make_arrow(shape_anchor_, pointer_pos); - - } else if (shape_tool_->value() == Line) { + if (is_laser_mode()) + return; - shape_stroke_->make_line(shape_anchor_, pointer_pos); + if (!current_bookmark_uuid_.is_null() && + current_interaction_viewport_name_ == viewport_name) { + return; } - current_edited_annotation_->update_render_data(); -} - -bool AnnotationsTool::check_pointer_hover_on_text( - const Imath::V2f &pointer_pos, const float viewport_pixel_scale) { - - auto old = hover_state_; - auto old_bdb = under_mouse_caption_bdb_; - if (current_edited_annotation_) { - hover_state_ = current_edited_annotation_->mouse_hover_on_selected_caption( - pointer_pos, viewport_pixel_scale); - if (hover_state_ == Caption::NotHovered) { - under_mouse_caption_bdb_ = current_edited_annotation_->mouse_hover_on_captions( - pointer_pos, viewport_pixel_scale); - for (auto &r : renderers_) { - r->set_under_mouse_caption_bdb(under_mouse_caption_bdb_); - } - if (!under_mouse_caption_bdb_.isEmpty()) { - hover_state_ = Caption::HoveredInCaptionArea; - } - } - if (hover_state_ != old) { - moving_scaling_text_attr_->set_value(int(hover_state_)); - } - - - } else { - // hover over non edited captions? - hover_state_ = Caption::NotHovered; - under_mouse_caption_bdb_ = Imath::Box2f(); - for (auto &anno_uuid : current_viewed_annotations_) { - - auto p = annotations_render_data_.find(anno_uuid); - if (p != annotations_render_data_.end()) { - for (const auto &cap_info : p->second->caption_info_) { - if (cap_info.bounding_box.intersects(pointer_pos)) { - under_mouse_caption_bdb_ = cap_info.bounding_box; - break; - } + 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()) { + + auto anno = dynamic_cast(bookmark->annotation_.get()); + if (anno) { + to_edit = anno; + current_bookmark_uuid_ = bookmark->detail_.uuid_; + break; + } else if (first_bookmark_uuid.is_null() && !bookmark->annotation_) { + // note if bookmark->annotation_ is set then its annotation data + // from some other plugin (like grading tool) so we only use + // existing empty bookmark if there's not annotation data on it + first_bookmark_uuid = bookmark->detail_.uuid_; } } + if (to_edit) + break; } } - if (hover_state_ != old || under_mouse_caption_bdb_ != old_bdb) { - for (auto &r : renderers_) { - r->set_under_mouse_caption_bdb(under_mouse_caption_bdb_); - r->set_caption_hover_state(hover_state_); - } - return true; - } - return false; -} - -void AnnotationsTool::end_stroke(const Imath::V2f &) { + clear_caption_handle(); - if (current_edited_annotation_) { - - current_edited_annotation_->finished_current_stroke(); - - // update annotation data attached to bookmark - if (!is_laser_mode()) { - push_annotation_to_bookmark(std::shared_ptr( - static_cast( - new Annotation(*current_edited_annotation_)))); - } else { - // 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); - } + // 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_freehand_pen_stroke(const Imath::V2f &point) { - if (!current_edited_annotation_) - create_new_annotation(); +void AnnotationsTool::start_stroke(const Imath::V2f &point) { if (active_tool_->value() == "Draw") { - current_edited_annotation_->start_pen_stroke( + interaction_canvas_.start_stroke( pen_colour_->value(), - float(draw_pen_size_->value()) / PEN_STROKE_THICKNESS_SCALE, - float(pen_opacity_->value()) / 100.0); + draw_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, + 0.0f, + pen_opacity_->value() / 100.0); } else if (active_tool_->value() == "Erase") { - current_edited_annotation_->start_erase_stroke( + interaction_canvas_.start_erase_stroke( erase_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE); } - freehand_pen_stroke_point(point); + update_stroke(point); } -void AnnotationsTool::freehand_pen_stroke_point(const Imath::V2f &point) { +void AnnotationsTool::update_stroke(const Imath::V2f &point) { - if (current_edited_annotation_) { - current_edited_annotation_->add_point_to_current_stroke(point); - } + interaction_canvas_.update_stroke(point); } -void AnnotationsTool::text_entered(const std::string &text, const std::string &context) { +void AnnotationsTool::start_shape(const Imath::V2f &p) { - if (active_tool_->value() == "Text" && current_edited_annotation_) { - current_edited_annotation_->modify_caption_text(text); - update_caption_overlay(); - } - redraw_viewport(); -} + shape_anchor_ = p; -void AnnotationsTool::key_pressed( - const int key, const std::string &context, const bool auto_repeat) { - if (active_tool_->value() == "Text" && current_edited_annotation_) { - if (key == 16777216) { - // escape key - end_stroke(); - release_keyboard_focus(); - } - current_edited_annotation_->key_down(key); - update_caption_overlay(); - } -} + if (shape_tool_->value() == Square) { -void AnnotationsTool::update_attrs_from_preferences(const utility::JsonStore &j) { + interaction_canvas_.start_square( + pen_colour_->value(), + shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, + pen_opacity_->value() / 100.0f); - Module::update_attrs_from_preferences(j); + } else if (shape_tool_->value() == Circle) { - // this ensures that 'display_mode_' member data is up to date after being - // updated from prefs - attribute_changed(display_mode_attribute_->uuid(), module::Attribute::Value); -} + interaction_canvas_.start_circle( + pen_colour_->value(), + shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, + pen_opacity_->value() / 100.0f); -void AnnotationsTool::attribute_changed( - const utility::Uuid &attribute_uuid, const int /*role*/ -) { + } else if (shape_tool_->value() == Arrow) { - const std::string active_tool = active_tool_->value(); + interaction_canvas_.start_arrow( + pen_colour_->value(), + shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, + pen_opacity_->value() / 100.0f); - if (attribute_uuid == tool_is_active_->uuid()) { + } else if (shape_tool_->value() == Line) { - 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_stroke(); - moving_scaling_text_attr_->set_value(0); - clear_caption_overlays(); - } + interaction_canvas_.start_line( + pen_colour_->value(), + shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, + pen_opacity_->value() / 100.0f); + } - } else if (attribute_uuid == active_tool_->uuid()) { + update_shape(p); +} - if (tool_is_active_->value()) { +void AnnotationsTool::update_shape(const Imath::V2f &pointer_pos) { - if (active_tool == "None") { - release_mouse_focus(); - } else { - grab_mouse_focus(); - } + if (shape_tool_->value() == Square) { - if (active_tool == "Text") { - } else { - end_stroke(); - release_keyboard_focus(); - moving_scaling_text_attr_->set_value(0); - clear_caption_overlays(); - } - } + interaction_canvas_.update_square(shape_anchor_, pointer_pos); - } else if ( - attribute_uuid == action_attribute_->uuid() && action_attribute_->value() != "") { + } else if (shape_tool_->value() == Circle) { - // renderer_->lock(); + interaction_canvas_.update_circle( + shape_anchor_, (shape_anchor_ - pointer_pos).length()); - if (action_attribute_->value() == "Clear") { - clear_onscreen_annotations(); - } else if (action_attribute_->value() == "Undo") { - undo(); - } else if (action_attribute_->value() == "Redo") { - redo(); - } - action_attribute_->set_value(""); + } else if (shape_tool_->value() == Arrow) { - /* if (current_edited_annotation_) - renderer_->set_immediate_render_data(current_edited_annotation_->render_data()); + interaction_canvas_.update_arrow(shape_anchor_, pointer_pos); - renderer_->unlock(); */ + } else if (shape_tool_->value() == Line) { - } else if (attribute_uuid == display_mode_attribute_->uuid()) { + interaction_canvas_.update_line(shape_anchor_, pointer_pos); + } +} - if (display_mode_attribute_->value() == "Only When Paused") { - 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; - } +void AnnotationsTool::start_or_edit_caption(const Imath::V2f &pos, float viewport_pixel_scale) { - } else if (attribute_uuid == draw_mode_->uuid()) { + auto &canvas = interaction_canvas_; + bool selected_new_caption = + canvas.select_caption(pos, handle_state_.handle_size, viewport_pixel_scale); - if (current_edited_annotation_) { - if (is_laser_mode()) { - end_stroke(); // this ensure current edited caption is - // finished - release_keyboard_focus(); + if (!canvas.empty()) { + update_bookmark_annotation_data(); + } - // This 'saves' the current edited annotation by pushing to the bookmark - push_annotation_to_bookmark(std::shared_ptr( - static_cast( - new Annotation(*current_edited_annotation_)))); - - edited_annotation_cache_[current_edited_annotation_->bookmark_uuid_] = - current_edited_annotation_; - last_edited_annotation_uuid_ = current_edited_annotation_->bookmark_uuid_; - - // Now we store the annotation's render data for immediate display - // since we are about to 'clear' it from the edited annotation - annotations_render_data_[current_edited_annotation_->bookmark_uuid_] = - current_edited_annotation_->render_data(); - current_viewed_annotations_.push_back( - current_edited_annotation_->bookmark_uuid_); - } - clear_edited_annotation(); - } + // Selecting a new (existing) caption + if (selected_new_caption) { - } else if (attribute_uuid == text_cursor_blink_attr_->uuid()) { + handle_state_.hover_state = HandleHoverState::HoveredInCaptionArea; + pen_colour_->set_value(canvas.caption_colour()); + text_size_->set_value(canvas.caption_font_size()); + font_choice_->set_value(canvas.caption_font_name()); + text_bgr_colour_->set_value(canvas.caption_background_colour()); + text_bgr_opacity_->set_value(canvas.caption_background_opacity() * 100.0); + } + // Interacting with the current caption + else if (canvas.has_selected_caption()) { - for (auto &r : renderers_) { - if (!interacting_with_renderers_) - r->lock(); - r->blink_text_cursor(text_cursor_blink_attr_->value()); - if (!interacting_with_renderers_) - r->unlock(); - } - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { + handle_state_.hover_state = canvas.hover_selected_caption_handle( + pos, handle_state_.handle_size, viewport_pixel_scale); - // send a delayed message to ourselves to continue the blinking - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(250), - module::change_attribute_value_atom_v, - attribute_uuid, - utility::JsonStore(!text_cursor_blink_attr_->value()), - true); + if (handle_state_.hover_state == HandleHoverState::HoveredOnMoveHandle) { + caption_drag_pointer_start_pos_ = pos; + caption_drag_caption_start_pos_ = canvas.caption_position(); + } else if (handle_state_.hover_state == HandleHoverState::HoveredOnResizeHandle) { + caption_drag_pointer_start_pos_ = pos; + caption_drag_width_height_ = + Imath::V2f(canvas.caption_width(), canvas.caption_bounding_box().max.y); + } else if (handle_state_.hover_state == HandleHoverState::HoveredOnDeleteHandle) { + canvas.delete_caption(); + clear_caption_handle(); + release_keyboard_focus(); + return; } + } + // Creating a new caption + else { - } else if (attribute_uuid == pen_colour_->uuid()) { + // if there was already a current caption being edited we need + // to bake that + canvas.end_draw(); - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { + canvas.start_caption( + pos, + font_choice_->value(), + text_size_->value(), + pen_colour_->value(), + pen_opacity_->value() / 100.0f, + text_size_->value() * 0.01f, + JustifyLeft, + text_bgr_colour_->value(), + text_bgr_opacity_->value() / 100.0f); - current_edited_annotation_->set_edited_caption_colour(pen_colour_->value()); - } + update_bookmark_annotation_data(); + } - } else if (attribute_uuid == text_size_->uuid()) { + grab_keyboard_focus(); - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { - current_edited_annotation_->set_edit_caption_font_size(text_size_->value()); - update_caption_overlay(); - } - } else if (attribute_uuid == pen_opacity_->uuid()) { + update_caption_hovered(pos, viewport_pixel_scale); - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { + update_caption_handle(); - current_edited_annotation_->set_edited_caption_opacity( - pen_opacity_->value() / 100.0f); - } - } else if (attribute_uuid == font_choice_->uuid()) { + text_cursor_blink_attr_->set_value(!text_cursor_blink_attr_->value()); +} + +void AnnotationsTool::update_caption_action(const Imath::V2f &p) { - if (current_edited_annotation_ && current_edited_annotation_->have_edited_caption()) { + if (interaction_canvas_.has_selected_caption()) { - current_edited_annotation_->set_edited_caption_font(font_choice_->value()); - update_caption_overlay(); + const auto delta = p - caption_drag_pointer_start_pos_; + if (handle_state_.hover_state == HandleHoverState::HoveredOnMoveHandle) { + interaction_canvas_.update_caption_position( + caption_drag_caption_start_pos_ + delta); + } else if (handle_state_.hover_state == HandleHoverState::HoveredOnResizeHandle) { + interaction_canvas_.update_caption_width(caption_drag_width_height_.x + delta.x); } } +} - if ((attribute_uuid == active_tool_->uuid() || attribute_uuid == draw_mode_->uuid()) && - current_edited_annotation_) { +bool AnnotationsTool::update_caption_hovered( + const Imath::V2f &pointer_pos, float viewport_pixel_scale) { - if (active_tool_->value() == "Draw" && draw_mode_->value() == "Laser") { + const HandleState old_state = handle_state_; - // user might have switch from shapes mode to draw (laser mode) -- need to save - // whatever was in the current annotations before laser drwaing - push_annotation_to_bookmark(std::shared_ptr( - static_cast( - new Annotation(*current_edited_annotation_)))); - edited_annotation_cache_[current_edited_annotation_->bookmark_uuid_] = - current_edited_annotation_; - last_edited_annotation_uuid_ = current_edited_annotation_->bookmark_uuid_; - clear_edited_annotation(); - - } else if (current_edited_annotation_->is_laser_annotation()) { - clear_edited_annotation(); - } + 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( + 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); + if (handle_state_ != old_state) { + moving_scaling_text_attr_->set_value(int(handle_state_.hover_state)); } - redraw_viewport(); + return (handle_state_ != old_state); } -void AnnotationsTool::undo() { - if (current_edited_annotation_) { - current_edited_annotation_->undo(); - // update annotation data attached to bookmark - - push_annotation_to_bookmark( - std::shared_ptr(static_cast( - new Annotation(*current_edited_annotation_)))); - if (current_edited_annotation_->empty()) { - clear_onscreen_annotations(); - } +void AnnotationsTool::update_caption_handle() { + + const HandleState old_state = handle_state_; + + if (interaction_canvas_.has_selected_caption()) { + handle_state_.current_caption_bdb = interaction_canvas_.caption_bounding_box(); + handle_state_.cursor_position = interaction_canvas_.caption_cursor_position(); } else { - if (current_bookmark_uuid_ && edited_annotation_cache_.find(current_bookmark_uuid_) != - edited_annotation_cache_.end()) { - current_edited_annotation_ = edited_annotation_cache_[current_bookmark_uuid_]; - undo(); - } else { - restore_onscreen_annotations(); - } + handle_state_.current_caption_bdb = Imath::Box2f(); + handle_state_.cursor_position = {Imath::V2f{0.0f, 0.0f}, Imath::V2f{0.0f, 0.0f}}; } -} -void AnnotationsTool::redo() { - if (current_edited_annotation_) { - current_edited_annotation_->redo(); - // update annotation data attached to bookmark - push_annotation_to_bookmark( - std::shared_ptr(static_cast( - new Annotation(*current_edited_annotation_)))); - } else { - if (edited_annotation_cache_.find(current_bookmark_uuid_) != - edited_annotation_cache_.end()) { - current_edited_annotation_ = edited_annotation_cache_[current_bookmark_uuid_]; - redo(); - } else { - restore_onscreen_annotations(); - } + if (handle_state_ != old_state) { + redraw_viewport(); } } -void AnnotationsTool::clear_caption_overlays() { +void AnnotationsTool::clear_caption_handle() { - interact_start(); - for (auto &r : renderers_) { - r->set_current_edited_caption_bdb(Imath::Box2f()); - r->set_under_mouse_caption_bdb(Imath::Box2f()); - r->set_cursor_position(Imath::V2f(0.0f, 0.0f), Imath::V2f(0.0f, 0.0f)); - } - interact_end(); + moving_scaling_text_attr_->set_value(0); + handle_state_ = HandleState(); } -void AnnotationsTool::on_screen_media_changed( - caf::actor media, - const utility::MediaReference &media_reference, - const std::string media_name) { - on_screen_media_ref_ = media_reference; - on_screen_media_name_ = media_name; -} +void AnnotationsTool::end_drawing() { -void AnnotationsTool::create_new_annotation() { + interaction_canvas_.end_draw(); if (is_laser_mode()) { - current_edited_annotation_.reset(new Annotation(fonts_, true)); - current_edited_annotation_->bookmark_uuid_ = utility::Uuid(); - return; - } - - if (current_bookmark_uuid_.is_null()) { - if (std::find( - current_viewed_annotations_.begin(), - current_viewed_annotations_.end(), - last_edited_annotation_uuid_) != current_viewed_annotations_.end()) { - current_bookmark_uuid_ = last_edited_annotation_uuid_; - } - } - - cleared_annotations_serialised_data_.clear(); - if (current_bookmark_uuid_.is_null()) { - - bookmark::BookmarkDetail bmd; - // this will make a bookmark of single frame duration on the current frame - bmd.start_ = media_frame_ * on_screen_media_ref_.rate().to_flicks(); - bmd.duration_ = timebase::flicks(0); - std::stringstream subject; - std::string name = on_screen_media_name_; - if (name.rfind("/") != std::string::npos) { - name = std::string(name, name.rfind("/") + 1); - } - subject << name << " annotation @ " << media_logical_frame_; - bmd.subject_ = subject.str(); - // this will result on - current_bookmark_uuid_ = StandardPlugin::create_bookmark_on_current_frame(bmd); - } - - AnnotationPtr annotation_from_cache; - auto p = edited_annotation_cache_.find(current_bookmark_uuid_); - if (p != edited_annotation_cache_.end()) { - annotation_from_cache = p->second; - } - - // fetch the annotation from the bookmarks - std::shared_ptr curr_anno = - fetch_annotation(current_bookmark_uuid_); - Annotation *cast_anno = curr_anno ? dynamic_cast(curr_anno.get()) : nullptr; - if (cast_anno) { - - // does the annotation from the bookmarks match the one from our cache? - // If so, use the one from our cache that still has undo/redo edit - // history. Otherwise assume that the one from bookmarks is the 'true' - // annotation. - if (annotation_from_cache && *cast_anno == *annotation_from_cache) { - current_edited_annotation_ = annotation_from_cache; - } else { - // n.b. bookmarks manager owns 'curr_anno' so we make our own - // editable copy here - current_edited_annotation_.reset(new Annotation(*cast_anno)); - } + // 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); } else { - // make a new blank annotation - current_edited_annotation_.reset(new Annotation(fonts_)); - current_edited_annotation_->bookmark_uuid_ = current_bookmark_uuid_; - } - if (std::find( - current_viewed_annotations_.begin(), - current_viewed_annotations_.end(), - current_bookmark_uuid_) == current_viewed_annotations_.end()) { - current_viewed_annotations_.push_back(current_bookmark_uuid_); + update_bookmark_annotation_data(); } } -std::shared_ptr -AnnotationsTool::build_annotation(const utility::JsonStore &anno_data) { - return std::shared_ptr( - static_cast(new Annotation(anno_data, fonts_))); -} +void AnnotationsTool::update_bookmark_annotation_data() { -void AnnotationsTool::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 &timecode) { - playhead_logical_frame_ = playhead_logical_frame; - media_frame_ = media_frame; - media_logical_frame_ = media_logical_frame; - playhead_position_ = playhead_position; -} + if (!current_bookmark_uuid_.is_null()) { -void AnnotationsTool::on_screen_annotation_changed( - std::vector> annotations) { + // here we clone 'interaction_canvas_' and pass to the bookmark as + // an AnnotationBase shared ptr - this gets attached to the bookmark + // for us by the base class + auto anno_clone = new Annotation(); + anno_clone->canvas() = interaction_canvas_; + auto annotation_pointer = std::shared_ptr( + static_cast(anno_clone)); - if (current_edited_annotation_ && !current_edited_annotation_->is_laser_annotation()) { + StandardPlugin::update_bookmark_annotation( + current_bookmark_uuid_, annotation_pointer, interaction_canvas_.empty()); - // we need to check if the timeline has scrubbed off the in/out range - // of our currently edited annotation, if so we need to store our edited - // annotation on the bookmark and erase it here - bool edited_annotation_still_on_screen = false; - for (auto &a : annotations) { - if (a->bookmark_uuid_ == current_edited_annotation_->bookmark_uuid_) { - edited_annotation_still_on_screen = true; - break; - } + if (interaction_canvas_.empty()) { + // annotation has been wiped either through 'clear' operation or + // by undoing until there are no strokes. + // If the bookmark has no notes, then StandardPlugin will also + // erase the empty bookmark. + // Thus we clear the current_bookmark_uuid_ so we know there is + // possibly no bookmark to attach any new annotations to. + current_bookmark_uuid_ = utility::Uuid(); } - if (!edited_annotation_still_on_screen) { - - // make sure current edited caption is completed - end_stroke(); - release_keyboard_focus(); + } else if (!is_laser_mode() && !interaction_canvas_.empty()) { - // we have moved off the frame range of the current edited annotation - // so we make a full copy and push to the bookmark for storage - push_annotation_to_bookmark(std::shared_ptr( - static_cast( - new Annotation(*current_edited_annotation_)))); - clear_edited_annotation(); - } + // 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); + if (!current_bookmark_uuid_.is_null()) + update_bookmark_annotation_data(); } +} - if (annotations.size()) { - - current_viewed_annotations_.clear(); - current_bookmark_uuid_ = utility::Uuid(); - for (auto &a : annotations) { - auto *cast_anno = dynamic_cast(a.get()); - if (cast_anno) { - if (current_bookmark_uuid_.is_null()) - current_bookmark_uuid_ = cast_anno->bookmark_uuid_; - // note we make our own copy of the annotation since what's being - // passed in here is a shared pty owned by the bookmark and could - // be changed elsewhere - annotations_render_data_[cast_anno->bookmark_uuid_] = cast_anno->render_data(); - current_viewed_annotations_.push_back(cast_anno->bookmark_uuid_); - } - } - if (current_bookmark_uuid_.is_null()) { - current_bookmark_uuid_ = annotations[0]->bookmark_uuid_; - } - } else { - current_viewed_annotations_.clear(); - current_bookmark_uuid_ = utility::Uuid(); - } +void AnnotationsTool::undo() { - update_caption_overlay(); + start_editing(current_interaction_viewport_name_); + interaction_canvas_.undo(); + update_bookmark_annotation_data(); + redraw_viewport(); } -void AnnotationsTool::clear_onscreen_annotations() { +void AnnotationsTool::redo() { - if (!(is_laser_mode() && current_edited_annotation_)) { - cleared_annotations_serialised_data_.push_back( - clear_annotations_and_bookmarks(current_viewed_annotations_)); + start_editing(current_interaction_viewport_name_); + interaction_canvas_.redo(); + update_bookmark_annotation_data(); + redraw_viewport(); +} - for (const auto &uuid : current_viewed_annotations_) { - auto p = annotations_render_data_.find(uuid); - if (p != annotations_render_data_.end()) - annotations_render_data_.erase(p); - } - } - clear_edited_annotation(); -} +void AnnotationsTool::clear_onscreen_annotations() { clear_edited_annotation(); } void AnnotationsTool::restore_onscreen_annotations() { - if (cleared_annotations_serialised_data_.size()) { - auto last_cleared = cleared_annotations_serialised_data_.back(); - cleared_annotations_serialised_data_.pop_back(); - restore_annotations_and_bookmarks(last_cleared); - } + // TODO: reinstate this behaviour redraw_viewport(); } void AnnotationsTool::clear_edited_annotation() { release_keyboard_focus(); - - if (current_edited_annotation_ && !current_edited_annotation_->is_laser_annotation()) { - edited_annotation_cache_[current_edited_annotation_->bookmark_uuid_] = - current_edited_annotation_; - last_edited_annotation_uuid_ = current_edited_annotation_->bookmark_uuid_; - annotations_render_data_[current_edited_annotation_->bookmark_uuid_] = - current_edited_annotation_->render_data(); - } - - current_edited_annotation_.reset(); - current_bookmark_uuid_ = utility::Uuid(); - - for (auto &r : renderers_) { - if (!interacting_with_renderers_) - r->lock(); - r->set_edited_annotation_render_data(AnnotationRenderDataPtr()); - if (!interacting_with_renderers_) - r->unlock(); - } - + start_editing(current_interaction_viewport_name_); + interaction_canvas_.clear(); + update_bookmark_annotation_data(); redraw_viewport(); } @@ -1080,8 +933,9 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( AnnotationsTool::PLUGIN_UUID, "AnnotationsTool", - plugin_manager::PluginType::PT_VIEWPORT_OVERLAY, - true, + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + true, // this is the 'resident' flag, meaning one instance of the plugin is + // created at startup time "Ted Waine", "On Screen Annotations Plugin")})); } diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp index c4686d883..6d34cf1a9 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp @@ -2,8 +2,6 @@ #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 "annotation.hpp" #include "annotation_opengl_renderer.hpp" @@ -17,89 +15,71 @@ namespace ui { utility::Uuid("46f386a0-cb9a-4820-8e99-fb53f6c019eb"); AnnotationsTool(caf::actor_config &cfg, const utility::JsonStore &init_settings); + virtual ~AnnotationsTool(); - ~AnnotationsTool(); + protected: + caf::message_handler message_handler_extensions() override; void attribute_changed( const utility::Uuid &attribute_uuid, const int /*role*/ ) override; - protected: - void register_hotkeys() override; - void update_attrs_from_preferences(const utility::JsonStore &) override; - caf::message_handler message_handler_extensions() override { - return message_handler_; - } + 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; + bool pointer_event(const ui::PointerEvent &e) override; + void text_entered(const std::string &text, const std::string &context) override; + void key_pressed( + const int key, const std::string &context, const bool auto_repeat) override; - utility::BlindDataObjectPtr prepare_render_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 override; plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int viewer_index) override; - std::shared_ptr + bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &anno_data) 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; - bool pointer_event(const ui::PointerEvent &e) override; - void text_entered(const std::string &text, const std::string &context) override; - void key_pressed( - const int key, const std::string &context, const bool auto_repeat) override; + void images_going_on_screen( + const std::vector &images, + const std::string viewport_name, + const bool playhead_playing) override; private: - void load_fonts(); - bool check_pointer_hover_on_text( - const Imath::V2f &pointer_pos, const float viewport_pixel_scale); - void caption_drag(const Imath::V2f &p); - void end_stroke(const Imath::V2f & = Imath::V2f()); - void interact_start(); - void interact_end(); - void start_or_edit_caption(const Imath::V2f &p, const float viewport_pixel_scale); - void start_shape_placement(const Imath::V2f &p); - void update_shape_placement(const Imath::V2f &pointer_pos); - void start_freehand_pen_stroke(const Imath::V2f &point); - void freehand_pen_stroke_point(const Imath::V2f &point); - void undo(); - void redo(); - void clear_caption_overlays(); - void update_caption_overlay(); - 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 - ) override; + bool is_laser_mode() const; - void on_screen_annotation_changed( - std::vector> // ptrs to annotation - // data - ) override; + void start_editing(const std::string &viewport_name); + + void start_stroke(const Imath::V2f &point); + void update_stroke(const Imath::V2f &point); - void on_screen_media_changed( - caf::actor, // media item actor - const utility::MediaReference &, // media reference - const std::string) override; + void start_shape(const Imath::V2f &p); + void update_shape(const Imath::V2f &pointer_pos); - void on_playhead_playing_changed(const bool // is playing - ) override; + void start_or_edit_caption(const Imath::V2f &p, float viewport_pixel_scale); + void update_caption_action(const Imath::V2f &p); + bool + update_caption_hovered(const Imath::V2f &pointer_pos, float viewport_pixel_scale); + void update_caption_handle(); + void clear_caption_handle(); + + void end_drawing(); + + void undo(); + void redo(); void create_new_annotation(); - void change_current_bookmark(const utility::Uuid &onscreen_bookmark); - void push_edited_annotation_back_to_bookmark(); void clear_onscreen_annotations(); void restore_onscreen_annotations(); void clear_edited_annotation(); - bool is_laser_mode() const { - return active_tool_->value() == "Draw" && draw_mode_->value() == "Laser"; - } - - caf::message_handler message_handler_; + void update_bookmark_annotation_data(); + private: enum Tool { Draw, Shapes, Text, Erase, None }; enum ShapeTool { Square, Circle, Arrow, Line }; enum DisplayMode { OnlyWhenPaused, Always, WithDrawTool }; @@ -111,66 +91,59 @@ namespace ui { const std::map draw_mode_names_ = { {Sketch, "Sketch"}, {Laser, "Laser"}, {Onion, "Onion"}}; - module::StringChoiceAttribute *active_tool_; + module::StringChoiceAttribute *active_tool_{nullptr}; - module::IntegerAttribute *draw_pen_size_ = {nullptr}; - module::IntegerAttribute *shapes_pen_size_ = {nullptr}; - module::IntegerAttribute *erase_pen_size_ = {nullptr}; - module::IntegerAttribute *text_size_ = {nullptr}; - module::IntegerAttribute *pen_opacity_ = {nullptr}; - module::ColourAttribute *pen_colour_ = {nullptr}; + module::IntegerAttribute *draw_pen_size_{nullptr}; + module::IntegerAttribute *shapes_pen_size_{nullptr}; + module::IntegerAttribute *erase_pen_size_{nullptr}; + module::IntegerAttribute *text_size_{nullptr}; + module::IntegerAttribute *pen_opacity_{nullptr}; + module::ColourAttribute *pen_colour_{nullptr}; + module::ColourAttribute *text_bgr_colour_{nullptr}; + module::IntegerAttribute *text_bgr_opacity_{nullptr}; - module::BooleanAttribute *text_cursor_blink_attr_ = {nullptr}; - module::BooleanAttribute *tool_is_active_ = {nullptr}; - module::StringAttribute *test_text_ = {nullptr}; - module::StringAttribute *action_attribute_ = {nullptr}; - module::IntegerAttribute *shape_tool_; - module::StringChoiceAttribute *draw_mode_; - module::IntegerAttribute *moving_scaling_text_attr_; - module::StringChoiceAttribute *font_choice_; + 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}; - DisplayMode display_mode_ = {OnlyWhenPaused}; + module::StringChoiceAttribute *display_mode_attribute_{nullptr}; + + DisplayMode display_mode_{OnlyWhenPaused}; + bool playhead_is_playing_{false}; utility::Uuid toggle_active_hotkey_; utility::Uuid undo_hotkey_; utility::Uuid redo_hotkey_; - AnnotationPtr current_edited_annotation_; - std::map edited_annotation_cache_; - - std::map annotations_render_data_; - std::vector current_viewed_annotations_; - std::vector> - cleared_annotations_serialised_data_; - + // Annotations utility::Uuid current_bookmark_uuid_; - utility::Uuid last_edited_annotation_uuid_; - int playhead_logical_frame_ = {-1}; - int media_frame_ = {-1}; - int media_logical_frame_ = {-1}; - timebase::flicks playhead_position_; - utility::MediaReference on_screen_media_ref_; - std::string on_screen_media_name_; + // N.B. this badboy is thread-safe. This means we can happily modify + // and access its data both in our class methods and also in the + // AnnotationsRenderer which has a direct access to it for on-screen + // rendering of brush-strokes during user interaction. + xstudio::ui::canvas::Canvas interaction_canvas_; - bool playhead_is_playing_ = {false}; + std::string current_interaction_viewport_name_; - std::vector renderers_; + utility::BlindDataObjectPtr immediate_render_data_; - std::map> fonts_; + // Current media info (for Bookmark creation) - std::shared_ptr shape_stroke_; - Imath::V2f shape_anchor_; + xstudio::ui::canvas::HandleState handle_state_; - Caption::HoverState hover_state_; Imath::V2f caption_drag_pointer_start_pos_; Imath::V2f caption_drag_caption_start_pos_; Imath::V2f caption_drag_width_height_; - Imath::Box2f under_mouse_caption_bdb_; + Imath::V2f shape_anchor_; - bool fade_looping_ = {false}; - bool interacting_with_renderers_ = {false}; + bool fade_looping_{false}; + std::map> + viewport_current_images_; }; } // namespace viewport 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 index 22ed9fe62..8a7ff6787 100644 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsButton.qml +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsButton.qml @@ -37,7 +37,7 @@ XsTrayButton { // connect to the backend module to give access to attributes XsModuleAttributes { id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings" + attributesGroupNames: "annotations_tool_settings_0" } // make a read only binding to the "annotations_tool_active" backend attribute 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 index 788109be8..3bd7fd052 100644 --- 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 @@ -68,7 +68,7 @@ XsWindow { XsModuleAttributes { id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings" + attributesGroupNames: "annotations_tool_settings_0" } @@ -125,7 +125,7 @@ XsWindow { } - // make a read only binding to the "annotations_tool_active" backend attribute + // 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? @@ -424,7 +424,7 @@ XsWindow { } } maximumLength: 3 - inputMask: "900" + // inputMask: "900" inputMethodHints: Qt.ImhDigitsOnly // validator: IntValidator {bottom: 0; top: 100;} selectByMouse: false @@ -441,6 +441,7 @@ XsWindow { } onAccepted:{ if(currentTool != "Erase"){ + if(parseInt(text) >= 100) { anno_tool_backend_settings.pen_opacity = 100 } @@ -458,6 +459,7 @@ XsWindow { } MouseArea{ id: opacityMArea + anchors.fill: parent cursorShape: Qt.SizeHorCursor hoverEnabled: true @@ -623,7 +625,7 @@ XsWindow { Rectangle { id: toolPreview width: parent.width/2 - spacing - height: parent.height - spacing + height: currentTool === "Text"? (parent.height/3-spacing) : (parent.height-spacing) color: "#595959" //"transparent" border.color: frameColor border.width: frameWidth @@ -693,16 +695,26 @@ XsWindow { opacity: 1 } - Text{ id: textPreview - text: "Example" - visible: currentTool === "Text" - property real sizeScaleFactor: 80/100 - font.pixelSize: currentToolSize *sizeScaleFactor - //font.family: textCategories.currentValue - color: currentToolColour - opacity: currentToolOpacity - horizontalAlignment: Text.AlignHCenter + 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 @@ -1092,14 +1104,14 @@ XsWindow { 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" + 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" + attributesGroupNames: "annotations_tool_draw_mode_0" } XsComboBox { 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 index ffd2b1769..4229cce5b 100644 --- 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 @@ -26,7 +26,7 @@ Item{ // in the backend. XsModuleAttributesModel { id: anno_draw_mode_backend - attributesGroupNames: "anno_scribble_mode_backend" + attributesGroupNames: "anno_scribble_mode_backend_0" } // we have to use a repeater to hook the model into the ListView 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 index 1885ed205..46d319c75 100644 --- 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 @@ -30,7 +30,7 @@ Item{ XsModuleAttributes { id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings" + attributesGroupNames: "annotations_tool_settings_0" } // make a local binding to the backend attribute 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 index 21f14525d..c619cf1ed 100644 --- 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 @@ -13,13 +13,37 @@ import QtGraphicalEffects 1.15 //for RadialGradient import xStudio 1.1 import xstudio.qml.module 1.0 -Item{ +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" + 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 @@ -31,20 +55,240 @@ Item{ id: dropdownFonts - width: parent.width-framePadding_x2 -itemSpacing*2 + width: parent.width -framePadding_x2 -itemSpacing/2 height: buttonHeight model: combo_box_options - anchors.centerIn: parent + 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 index 33385e640..27ee86de2 100644 --- 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 @@ -21,7 +21,7 @@ Item{ XsModuleAttributesModel { id: annotations_tool_types - attributesGroupNames: "annotations_tool_types" + attributesGroupNames: "annotations_tool_types_0" } property var toolImages: [ 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 index df442129f..972a5ca8e 100644 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsTextItems.qml +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsTextItems.qml @@ -19,7 +19,7 @@ Rectangle { XsModuleAttributesModel { id: text_times - attributesGroupNames: "annotations_text_items" + attributesGroupNames: "annotations_text_items_0" } diff --git a/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt index bf3953454..43e48f394 100644 --- a/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt +++ b/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt @@ -1,6 +1,10 @@ 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) diff --git a/src/plugin/viewport_overlay/annotations/src/serialisers/1.0/serialiser_1_pt_0.cpp b/src/plugin/viewport_overlay/annotations/src/serialisers/1.0/serialiser_1_pt_0.cpp index 3b2755e91..57f9490bf 100644 --- a/src/plugin/viewport_overlay/annotations/src/serialisers/1.0/serialiser_1_pt_0.cpp +++ b/src/plugin/viewport_overlay/annotations/src/serialisers/1.0/serialiser_1_pt_0.cpp @@ -1,8 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 + #include "annotation_serialiser.hpp" +#include "xstudio/ui/canvas/canvas.hpp" -using namespace xstudio::ui::viewport; using namespace xstudio; +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::viewport; + class AnnotationSerialiser_1_pt_0 : public AnnotationSerialiser { @@ -11,163 +15,17 @@ class AnnotationSerialiser_1_pt_0 : public AnnotationSerialiser { void _serialise(const Annotation *, nlohmann::json &) const override; void _deserialise(Annotation *anno, const nlohmann::json &data) override; - - void serialise_pen_stroke(const PenStroke &s, nlohmann::json &o) const; - void serialise_caption(const std::shared_ptr &capt, nlohmann::json &o) const; }; RegisterAnnotationSerialiser(AnnotationSerialiser_1_pt_0, 1, 0) void AnnotationSerialiser_1_pt_0::_serialise( const Annotation *anno, nlohmann::json &d) const { - d["pen_strokes"] = nlohmann::json::array(); - for (const auto &stroke : anno->strokes_) { - d["pen_strokes"].emplace_back(nlohmann::json()); - nlohmann::json &s = d["pen_strokes"].back(); - serialise_pen_stroke(stroke, s); - } - d["captions"] = nlohmann::json::array(); - for (const auto &caption : anno->captions_) { - d["captions"].emplace_back(nlohmann::json()); - nlohmann::json &s = d["captions"].back(); - serialise_caption(caption, s); - } -} - -void AnnotationSerialiser_1_pt_0::serialise_pen_stroke( - const PenStroke &s, nlohmann::json &o) const { - // TODO: pack data into bytes and convert to ASCII legal characters. Will be *much* more - // compact than json text encoding - /*std::vector stroke_data(); - size_t sz = 2 // num of points as a short - + s.points_.size() * sizeof(half) * 2 // points data as half float - + sizeof(float) // opacity - + sizeof(float) // thickness - + sizeof(bool) // is_erase_stroke - + 3*sizeof(float); // RGB values; - - stroke_data.resize(sz); - - uint8_t *d = stroke_data.data(); - - short n = s.points_.size(); - memcpy(d, &n, 2); - d += 2; - - for (auto & pt: s.points_) { - half h = pt.x; - memcpy(d, &h, sizeof(half)); - d += sizeof(half); - h = pt.y; - memcpy(d, &h, sizeof(half)); - d += sizeof(half); - } - - memcpy(d, &(s.opacity_), sizeof(float)); - d += sizeof(float); - memcpy(d, &(s.thickness_), sizeof(float)); - d += sizeof(float); - - memcpy(d, &(s.is_erase_stroke_), sizeof(bool)); - d += sizeof(bool); - - memcpy(d, &(s.colour_.r), sizeof(float)); - d += sizeof(float); - memcpy(d, &(s.colour_.g), sizeof(float)); - d += sizeof(float); - memcpy(d, &(s.colour_.b), sizeof(float)); - d += sizeof(float);*/ - - std::vector pts; - pts.reserve(s.points_.size()); - for (auto &pt : s.points_) { - pts.push_back(pt.x); - pts.push_back(pt.y); - } - o["points"] = pts; - o["opacity"] = s.opacity_; - o["thickness"] = s.thickness_; - o["is_erase_stroke"] = s.is_erase_stroke_; - if (!s.is_erase_stroke_) { - o["r"] = s.colour_.r; - o["g"] = s.colour_.g; - o["b"] = s.colour_.b; - } -} - -void AnnotationSerialiser_1_pt_0::serialise_caption( - const std::shared_ptr &capt, nlohmann::json &o) const { - - o["text"] = capt->text_; - o["position"] = capt->position_; - o["wrap_width"] = capt->wrap_width_; - o["font_size"] = capt->font_size_; - o["font_name"] = capt->font_name_; - o["colour"] = capt->colour_; - o["opacity"] = capt->opacity_; - o["justification"] = static_cast(capt->justification_); - o["outline"] = false; + d = anno->canvas(); } - void AnnotationSerialiser_1_pt_0::_deserialise(Annotation *anno, const nlohmann::json &data) { - if (data.contains("pen_strokes") && data["pen_strokes"].is_array()) { - - const auto &d = data["pen_strokes"]; - for (const auto &o : d) - if (o["is_erase_stroke"].get()) { - PenStroke stroke(o["thickness"].get()); - if (o.contains("points") && o["points"].is_array()) { - auto p = o["points"].begin(); - while (p != o["points"].end()) { - auto x = p.value().get(); - p++; - auto y = p.value().get(); - p++; - stroke.add_point(Imath::V2f(x, y)); - } - } - anno->strokes_.push_back(std::move(stroke)); - } else { - PenStroke stroke( - utility::ColourTriplet( - o["r"].get(), o["g"].get(), o["b"].get()), - o["thickness"].get(), - o["opacity"].get()); - if (o.contains("points") && o["points"].is_array()) { - auto p = o["points"].begin(); - while (p != o["points"].end()) { - auto x = p.value().get(); - p++; - auto y = p.value().get(); - p++; - stroke.add_point(Imath::V2f(x, y)); - } - } - anno->strokes_.push_back(std::move(stroke)); - } - } - - if (data.contains("captions") && data["captions"].is_array()) { - - const auto &d = data["captions"]; - for (const auto &o : d) { - try { - auto capt = std::make_shared( - o["position"].get(), - o["wrap_width"].get(), - o["font_size"].get(), - o["colour"].get(), - o["opacity"].get(), - static_cast(o["justification"].get()), - o["font_name"].get()); - - capt->text_ = o["text"].get(); - anno->captions_.push_back(std::move(capt)); - } catch (std::exception &e) { - } - } - } + anno->canvas() = data.template get(); } 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 f9c6b95e9..02f7b4e1d 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,8 @@ #include "xstudio/ui/viewport/viewport_helpers.hpp" #include "xstudio/utility/helpers.hpp" -#include #include +#include using namespace xstudio; using namespace xstudio::ui::viewport; @@ -271,9 +271,10 @@ BasicViewportMasking::BasicViewportMasking( 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("any_toolbar"); 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 @@ -327,7 +328,7 @@ void BasicViewportMasking::register_hotkeys() { "Toggles viewport masking. Find mask settings in the toolbar under the 'Mask' button"); } -utility::BlindDataObjectPtr BasicViewportMasking::prepare_render_data( +utility::BlindDataObjectPtr BasicViewportMasking::prepare_overlay_data( const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { auto r = utility::BlindDataObjectPtr(); @@ -397,7 +398,7 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("4006826a-6ff2-41ec-8ef2-d7a40bfd65e4"), "BasicViewportMasking", - plugin_manager::PluginType::PT_VIEWPORT_OVERLAY, + 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 88209ac15..8a4a1792e 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 @@ -60,7 +60,7 @@ namespace ui { protected: void register_hotkeys() override; - utility::BlindDataObjectPtr prepare_render_data( + utility::BlindDataObjectPtr prepare_overlay_data( const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override { 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 index 5fe3fb015..69faad8ce 100644 --- 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 @@ -178,19 +178,11 @@ XsToolbarItem { hoverEnabled: true anchors.fill: parent onClicked: { - settings_dialog.raise() - settings_dialog.show() - settings_dialog.raise() + launchSettingsDlg() popup.visible = false } } } - - XsModuleAttributesDialog { - id: settings_dialog - title: "Viewport Mask Settings" - attributesGroupNames: "viewport_mask_settings" - } } @@ -212,4 +204,11 @@ XsToolbarItem { 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 1b4dfc7ea..8777b6f7f 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 @@ -76,17 +76,18 @@ Rectangle { } Rectangle { - id: bottom_masking_rect + id: top_masking_rect opacity: mask_opacity color: "black" x: 0 y: 0 + z: -1 width: control.width height: b } Rectangle { - id: top_masking_rect + id: bottom_masking_rect opacity: mask_opacity color: "black" x: 0 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 index 3b0c2ae34..2dd4119c1 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/CMakeLists.txt +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/CMakeLists.txt @@ -1,6 +1,10 @@ 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) diff --git a/src/plugin_manager/src/CMakeLists.txt b/src/plugin_manager/src/CMakeLists.txt index af41c8c5d..0c53ecac2 100644 --- a/src/plugin_manager/src/CMakeLists.txt +++ b/src/plugin_manager/src/CMakeLists.txt @@ -1,11 +1,13 @@ SET(LINK_DEPS caf::core - dl - stdc++fs xstudio::broadcast xstudio::module xstudio::utility ) +if(UNIX AND NOT APPLE) + list(APPEND LINK_DEPS stdc++fs dl) +endif() + create_component(plugin_manager 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin_manager/src/plugin_base.cpp b/src/plugin_manager/src/plugin_base.cpp index 6428c195a..e0d470a9b 100644 --- a/src/plugin_manager/src/plugin_base.cpp +++ b/src/plugin_manager/src/plugin_base.cpp @@ -4,49 +4,16 @@ #include "xstudio/media_reader/image_buffer.hpp" using namespace xstudio; +using namespace xstudio::bookmark; using namespace xstudio::plugin; StandardPlugin::StandardPlugin( caf::actor_config &cfg, std::string name, const utility::JsonStore &init_settings) : caf::event_based_actor(cfg), module::Module(name) { - scoped_actor sys{system()}; - try { - - // 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, caf::actor_cast(this)); - - session_changed(utility::request_receive( - *sys, - 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)); - }); - } - + utility::print_on_exit(this, name); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } + join_studio_events(); message_handler_ = { @@ -67,12 +34,28 @@ StandardPlugin::StandardPlugin( [=](ui::viewport::prepare_overlay_render_data_atom, const media_reader::ImageBufPtr &image, const bool offscreen) -> utility::BlindDataObjectPtr { - return prepare_render_data(image, offscreen); + return prepare_overlay_data(image, offscreen); }, + + [=](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::show_atom, + const std::vector &images, + const std::string &viewport_name, + const bool playing) { images_going_on_screen(images, viewport_name, playing); }, + [=](ui::viewport::overlay_render_function_atom, const int viewer_index) -> ViewportOverlayRendererPtr { return make_overlay_renderer(viewer_index); }, - [=](bookmark::build_annotation_atom, const utility::JsonStore &data) - -> result> { + + [=](ui::viewport::pre_render_gpu_hook_atom, const int viewer_index) + -> GPUPreDrawHookPtr { return make_pre_draw_gpu_hook(viewer_index); }, + + [=](bookmark::build_annotation_atom, + const utility::JsonStore &data) -> result { try { return build_annotation(data); ; @@ -108,21 +91,17 @@ StandardPlugin::StandardPlugin( media_logical_frame, timecode); - check_if_onscreen_bookmarks_have_changed(playhead_logical_frame); - playhead_logical_frame_ = playhead_logical_frame; }, [=](utility::event_atom, bookmark::get_bookmarks_atom, - const std::vector> - &bookmark_frames_ranges) { - bookmark_frame_ranges_ = bookmark_frames_ranges; - check_if_onscreen_bookmarks_have_changed(playhead_logical_frame_, true); - }}; + const std::vector> &) {}, + [=](utility::event_atom, utility::event_atom) { join_studio_events(); }}; } void StandardPlugin::on_screen_media_changed(caf::actor media) { + if (media) { request(media, infinite, utility::name_atom_v) .then( @@ -162,238 +141,57 @@ void StandardPlugin::session_changed(caf::actor session) { }); } +void StandardPlugin::join_studio_events() { -void StandardPlugin::check_if_onscreen_bookmarks_have_changed( - const int media_frame, const bool force_update) { - - auto t0 = utility::clock::now(); - - utility::UuidList onscreen_bookmarks; - for (const auto &a : bookmark_frame_ranges_) { - const int in = std::get<2>(a); - const int out = std::get<3>(a); - - if (in <= media_frame && out >= media_frame) { - onscreen_bookmarks.push_back(std::get<0>(a)); - } - } - if (onscreen_bookmarks.empty()) { - onscreen_bookmarks.push_back(utility::Uuid()); - } - - if (onscreen_bookmarks != onscreen_bookmarks_) { - onscreen_bookmarks_ = onscreen_bookmarks; - } else if (!force_update) { - return; - } - - auto annotations = - std::make_shared>>(); - - if (onscreen_bookmarks_.size() == 1 && (*onscreen_bookmarks_.begin()).is_null()) { - - on_screen_annotation_changed(*(annotations.get())); - return; - } - - if (!bookmark_manager_) - return; - request(bookmark_manager_, infinite, bookmark::get_bookmark_atom_v, onscreen_bookmarks_) - .then( - [=](std::vector curr_bookmarks) mutable { - for (auto &ua : curr_bookmarks) { - request(ua.actor(), infinite, bookmark::get_annotation_atom_v) - .then( - [=](std::shared_ptr annotation) mutable { - annotations->push_back(annotation); - if (annotations->size() == curr_bookmarks.size()) { - on_screen_annotation_changed(*(annotations.get())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); -} - -void StandardPlugin::push_annotation_to_bookmark( - std::shared_ptr annotation) { - if (!bookmark_manager_) - return; scoped_actor sys{system()}; - // loop over bookmarks to clear try { - auto curr_bookmark = utility::request_receive( - *sys, bookmark_manager_, bookmark::get_bookmark_atom_v, annotation->bookmark_uuid_); - - utility::request_receive( - *sys, curr_bookmark.actor(), bookmark::add_annotation_atom_v, annotation); - - // kick the playhead to rebroadcast the bookmarks for the current frame - auto playhead = caf::actor_cast(active_viewport_playhead_); - if (playhead) { - anon_send(playhead, bookmark::get_bookmarks_atom_v); + 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); + return; } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void StandardPlugin::restore_annotations_and_bookmarks( - const std::map &bookmarks_data) { - if (!bookmark_manager_) - return; - - scoped_actor sys{system()}; - for (const auto &p : bookmarks_data) { - - const utility::Uuid bookmark_uuid = p.first; - const utility::JsonStore bookmark_serialisation_data = p.second; - - try { - // this call will create a new bookmark using the serialised data, - // unless the bookmark already exists - if it does exist it updates - // the bookmark's annotation data with the serialisation data - utility::request_receive( - *sys, - bookmark_manager_, - bookmark::add_bookmark_atom_v, - bookmark_uuid, - bookmark_serialisation_data); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } -} - -std::map -StandardPlugin::clear_annotations_and_bookmarks(std::vector bookmark_ids) { - std::map result; - if (!bookmark_manager_) - return result; - scoped_actor sys{system()}; - // loop over bookmarks to clear - for (const auto &bm_uuid : bookmark_ids) { - - try { - // serialise so we can undo .. - auto bookmark_serialise_data = utility::request_receive( - *sys, bookmark_manager_, utility::serialise_atom_v, bm_uuid); - - // get bookmark detail - auto bm_detail = utility::request_receive( - *sys, bookmark_manager_, bookmark::bookmark_detail_atom_v, bm_uuid); - - result[bm_uuid] = bookmark_serialise_data; + // 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); - if (!bm_detail.note_ || *(bm_detail.note_) == "") { - // note is empty, so we want to delete the note entirely - anon_send(bookmark_manager_, bookmark::remove_bookmark_atom_v, bm_uuid); - } else { + utility::request_receive( + *sys, grp, broadcast::join_broadcast_atom_v, caf::actor_cast(this)); - request(bookmark_manager_, infinite, bookmark::get_bookmark_atom_v, bm_uuid) - .then( - [=](utility::UuidActor curr_bookmark) { - // push an empty annotation - anon_send( - curr_bookmark.actor(), - bookmark::add_annotation_atom_v, - std::shared_ptr()); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } + session_changed(utility::request_receive( + *sys, + system().registry().template get(studio_registry), + session::session_atom_v)); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + // 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)); + }); } - } - return result; -} - -utility::Uuid StandardPlugin::create_bookmark_on_current_frame(bookmark::BookmarkDetail bmd) { - - utility::Uuid result; - try { - scoped_actor sys{system()}; - auto playhead = caf::actor_cast(active_viewport_playhead_); - if (playhead) { - auto media = - utility::request_receive(*sys, playhead, playhead::media_atom_v); - if (media) { - auto media_uuid = - utility::request_receive(*sys, media, utility::uuid_atom_v); - auto new_bookmark = utility::request_receive( - *sys, - bookmark_manager_, - bookmark::add_bookmark_atom_v, - utility::UuidActor(media_uuid, media)); - - 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(); - onscreen_bookmarks_.clear(); - onscreen_bookmarks_.push_back(new_bookmark.uuid()); - // sync our list of bookmarks so that it includes the new one - // that we have just created - auto playhead = caf::actor_cast(active_viewport_playhead_); - if (playhead) { - try { - - scoped_actor sys{system()}; - bookmark_frame_ranges_ = utility::request_receive< - std::vector>>( - *sys, playhead, bookmark::get_bookmark_atom_v); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - } - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - - return result; } -std::shared_ptr -StandardPlugin::fetch_annotation(const utility::Uuid &bookmark_uuid) { - - std::shared_ptr r; - if (bookmark_manager_) { - - scoped_actor sys{system()}; - try { - auto bm = utility::request_receive( - *sys, bookmark_manager_, bookmark::get_bookmark_atom_v, bookmark_uuid); - - r = utility::request_receive>( - *sys, bm.actor(), bookmark::get_annotation_atom_v); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - return r; -} void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_playhead_addr) { @@ -411,7 +209,6 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play if (viewed_playhead) { - request(viewed_playhead, infinite, playhead::media_events_group_atom_v) .then( [=](caf::actor playhead_media_events_broadcast_group) { @@ -437,7 +234,7 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play on_screen_media_changed(current_media_actor); }, [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); // make sure we have synced the bookmarks info from the playhead @@ -445,12 +242,8 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play scoped_actor sys{system()}; playhead_logical_frame_ = utility::request_receive( *sys, viewed_playhead, playhead::logical_frame_atom_v); - bookmark_frame_ranges_ = utility::request_receive< - std::vector>>( - *sys, viewed_playhead, bookmark::get_bookmark_atom_v); - check_if_onscreen_bookmarks_have_changed(playhead_logical_frame_); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } catch ([[maybe_unused]] std::exception &e) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } } @@ -462,4 +255,130 @@ void StandardPlugin::qml_viewport_overlay_code(const std::string &code) { } else { viewport_overlay_qml_code_->set_value(code); } -} \ No newline at end of file +} + +utility::Uuid StandardPlugin::create_bookmark_on_current_media( + const std::string &viewport_name, + const std::string &bookmark_subject, + const bookmark::BookmarkDetail &detail, + const bool bookmark_entire_duration) { + + 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); + + auto media = + utility::request_receive(*sys, playhead, playhead::media_atom_v); + if (media) { + + auto media_uuid = + utility::request_receive(*sys, media, utility::uuid_atom_v); + + 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 { + auto media_frame = + utility::request_receive(*sys, playhead, playhead::media_frame_atom_v); + + // this will make a bookmark of single frame duration on the current frame + bmd.start_ = (media_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; +} + +void StandardPlugin::update_bookmark_annotation( + const utility::Uuid bookmark_id, + std::shared_ptr annotation_data, + const bool annotation_is_empty) { + request(bookmark_manager_, infinite, bookmark::get_bookmark_atom_v, bookmark_id) + .then( + [=](utility::UuidActor &bm) { + if (!annotation_is_empty) { + + anon_send(bm.actor(), bookmark::add_annotation_atom_v, annotation_data); + + } else { + + request(bm.actor(), infinite, bookmark::bookmark_detail_atom_v) + .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); + + } else { + anon_send( + bm.actor(), + bookmark::add_annotation_atom_v, + annotation_data); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + +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)); + }); +} diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index e487d48ca..c2d5c28e3 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -1,5 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __linux__ #include +#endif + #include #include @@ -13,21 +16,49 @@ using namespace xstudio::utility; namespace fs = std::filesystem; +#ifdef _WIN32 +std::string GetLastErrorAsString() { + DWORD errorMessageID = GetLastError(); + if (errorMessageID == 0) + return std::string(); // No error message has been recorded + + LPSTR messageBuffer = nullptr; + size_t size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + errorMessageID, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&messageBuffer), + 0, + nullptr); + + std::string message(messageBuffer, size); + + LocalFree(messageBuffer); + + return message; +} +#endif PluginManager::PluginManager(std::list plugin_paths) : plugin_paths_(std::move(plugin_paths)) {} size_t PluginManager::load_plugins() { - // scan for .so for each path. + // 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")) + not(entry.path().extension() == ".so" || + entry.path().extension() == ".dll")) continue; +#ifdef __linux__ // only want .so // clear any errors.. dlerror(); @@ -47,6 +78,38 @@ size_t PluginManager::load_plugins() { dlclose(hndl); continue; } +#elif defined(_WIN32) + // open .dll + std::string dllPath = entry.path().string(); + HMODULE hndl = LoadLibraryA(dllPath.c_str()); + if (hndl == nullptr) { + DWORD errorCode = GetLastError(); + LPSTR buffer = nullptr; + DWORD size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + errorCode, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), + 0, + nullptr); + std::string errorMsg(buffer, size); + LocalFree(buffer); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, errorMsg); + continue; + } + + plugin_factory_collection_ptr pfcp; + pfcp = reinterpret_cast( + GetProcAddress(hndl, "plugin_factory_collection_ptr")); + + if (pfcp == nullptr) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, GetLastErrorAsString()); + FreeLibrary(hndl); + continue; + } +#endif PluginFactoryCollection *pfc = nullptr; try { @@ -55,7 +118,12 @@ size_t PluginManager::load_plugins() { if (not factories_.count(i->uuid())) { // new plugin.. loaded++; +#ifdef _WIN32 + factories_.emplace( + i->uuid(), PluginEntry(i, entry.path().string())); +#else factories_.emplace(i->uuid(), PluginEntry(i, entry.path())); +#endif spdlog::debug( "Add plugin {} {} {}", to_string(i->uuid()), @@ -72,7 +140,11 @@ size_t PluginManager::load_plugins() { spdlog::warn( "{} Failed to init plugin {} {}", __PRETTY_FUNCTION__, +#ifdef _WIN32 + entry.path().string(), +#else entry.path().c_str(), +#endif err.what()); } if (pfc) @@ -86,16 +158,7 @@ size_t PluginManager::load_plugins() { } caf::actor PluginManager::spawn( - caf::blocking_actor &sys, - const utility::Uuid &uuid, - const utility::JsonStore &json, - const bool singleton) { - - if (singleton && singletons_.find(uuid) != singletons_.end() && - caf::actor_cast(singletons_[uuid])) { - return caf::actor_cast(singletons_[uuid]); - } - + caf::blocking_actor &sys, const utility::Uuid &uuid, const utility::JsonStore &json) { auto spawned = caf::actor(); if (factories_.count(uuid)) @@ -103,8 +166,6 @@ caf::actor PluginManager::spawn( else throw std::runtime_error("Invalid plugin uuid"); - if (spawned && singleton) - singletons_[uuid] = caf::actor_cast(spawned); return spawned; } diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index 9ca48dc1e..863112288 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -24,6 +24,16 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base manager_.emplace_front_path(xstudio_root("/plugin")); + + // use env var 'XSTUDIO_PLUGIN_PATH' to extend the folders searched for + // xstudio plugins + char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); + if (plugin_path) { + for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + manager_.emplace_front_path(p); + } + } + manager_.load_plugins(); try { @@ -54,7 +64,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base auto actors = std::vector(); for (const auto &i : manager_.factories()) { - if (i.second.factory()->type() == PluginType::PT_DATA_SOURCE and + if (i.second.factory()->type() & PluginFlags::PF_DATA_SOURCE and resident_.count(i.first)) actors.push_back(resident_[i.first]); } @@ -87,7 +97,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base auto actors = std::vector(); for (const auto &i : manager_.factories()) { - if (i.second.factory()->type() == PluginType::PT_DATA_SOURCE and + if (i.second.factory()->type() & PluginFlags::PF_DATA_SOURCE and resident_.count(i.first)) actors.push_back(resident_[i.first]); } @@ -121,7 +131,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base auto actors = std::vector(); for (const auto &i : manager_.factories()) { - if (i.second.factory()->type() == PluginType::PT_DATA_SOURCE and + if (i.second.factory()->type() & PluginFlags::PF_DATA_SOURCE and resident_.count(i.first)) actors.push_back(resident_[i.first]); } @@ -212,7 +222,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base [=](utility::detail_atom, const PluginType type) -> std::vector { std::vector details; for (const auto &i : manager_.factories()) { - if (i.second.factory()->type() == type) + if (i.second.factory()->type() & type) details.emplace_back(PluginDetail(i.second)); } @@ -258,14 +268,23 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base [=](spawn_plugin_atom, const utility::Uuid &uuid, - const utility::JsonStore &json) -> result { + const utility::JsonStore &json, + bool make_resident) -> result { + if (resident_.count(uuid)) { + return resident_[uuid]; + } + if (not manager_.factories().count(uuid)) return make_error(xstudio_error::error, "Invalid uuid"); + if (make_resident) { + enable_resident(uuid, true, json); + return resident_[uuid]; + } + auto spawned = caf::actor(); try { spawned = manager_.spawn(*scoped_actor(system()), uuid, json); - } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } @@ -274,14 +293,17 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base [=](spawn_plugin_atom, const utility::Uuid &uuid, - const utility::JsonStore &json, - const bool singleton) -> result { + const utility::JsonStore &json) -> result { + if (resident_.count(uuid)) { + return resident_[uuid]; + } + if (not manager_.factories().count(uuid)) return make_error(xstudio_error::error, "Invalid uuid"); auto spawned = caf::actor(); try { - spawned = manager_.spawn(*scoped_actor(system()), uuid, json, singleton); + spawned = manager_.spawn(*scoped_actor(system()), uuid, json); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } diff --git a/src/plugin_manager/test/plugin_test.cpp b/src/plugin_manager/test/plugin_test.cpp index 58363168a..a4f4f5baf 100644 --- a/src/plugin_manager/test/plugin_test.cpp +++ b/src/plugin_manager/test/plugin_test.cpp @@ -38,7 +38,7 @@ class TestPlugin : public PluginFactory { [[nodiscard]] utility::Uuid uuid() const override { return Uuid("17e4323c-8ee7-4d9c-b74a-57ba805c10e8"); } - [[nodiscard]] PluginType type() const override { return PluginType::PT_CUSTOM; } + [[nodiscard]] PluginType type() const override { return PluginFlags::PF_CUSTOM; } [[nodiscard]] bool resident() const override { return false; } [[nodiscard]] std::string author() const override { return "author"; } [[nodiscard]] std::string description() const override { return "description"; } diff --git a/src/pyside2_module/src/CMakeLists.txt b/src/pyside2_module/src/CMakeLists.txt index 9b76e5dd6..afc36ce9c 100644 --- a/src/pyside2_module/src/CMakeLists.txt +++ b/src/pyside2_module/src/CMakeLists.txt @@ -94,10 +94,16 @@ set(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}.bin" + OUTPUT_NAME "${PROJECT_NAME}${EXE_EXTENSION}" LINK_DEPENDS_NO_SHARED true ) diff --git a/src/python_module/src/CMakeLists.txt b/src/python_module/src/CMakeLists.txt index fa8567eef..c47f3b85b 100644 --- a/src/python_module/src/CMakeLists.txt +++ b/src/python_module/src/CMakeLists.txt @@ -1,10 +1,16 @@ project(__pybind_xstudio VERSION 0.1.0 LANGUAGES CXX) -find_package(pybind11 REQUIRED) +find_package(pybind11 CONFIG REQUIRED) +#find_package(caf CONFIG REQUIRED) find_package(Python COMPONENTS Interpreter) set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") +if(WIN32) + add_compile_options("/bigobj") + set(PYTHON_MODULE_EXTENSION .pyd) +endif() + add_library( ${PROJECT_NAME} MODULE @@ -25,6 +31,8 @@ add_library(xstudio::python_module ALIAS ${PROJECT_NAME}) default_options_local(${PROJECT_NAME}) +set_python_to_proper_build_type() + target_link_libraries( ${PROJECT_NAME} PUBLIC @@ -58,4 +66,7 @@ else() endif(INSTALL_XSTUDIO) +if(WIN32) +install(TARGETS ${PROJECT_NAME} DESTINATION "${CMAKE_INSTALL_PREFIX}/python/xstudio/core") +endif() diff --git a/src/python_module/src/py_atoms.cpp b/src/python_module/src/py_atoms.cpp index fdc49703f..0d556e7c9 100644 --- a/src/python_module/src/py_atoms.cpp +++ b/src/python_module/src/py_atoms.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wattributes" +#endif // #include // #include @@ -35,6 +37,7 @@ using namespace xstudio::sync; using namespace xstudio::tag; using namespace xstudio::thumbnail; using namespace xstudio::timeline; +using namespace xstudio::ui; using namespace xstudio::ui::keypress_monitor; using namespace xstudio::ui::qml; using namespace xstudio::ui::viewport; @@ -65,12 +68,21 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::timeline, active_range_atom); ADD_ATOM(xstudio::timeline, available_range_atom); ADD_ATOM(xstudio::timeline, duration_atom); - ADD_ATOM(xstudio::timeline, item_name_atom); - ADD_ATOM(xstudio::timeline, item_atom); + ADD_ATOM(xstudio::timeline, erase_item_at_frame_atom); + ADD_ATOM(xstudio::timeline, erase_item_atom); + ADD_ATOM(xstudio::timeline, insert_item_at_frame_atom); ADD_ATOM(xstudio::timeline, insert_item_atom); - ADD_ATOM(xstudio::timeline, remove_item_atom); + ADD_ATOM(xstudio::timeline, item_atom); + ADD_ATOM(xstudio::timeline, item_name_atom); + ADD_ATOM(xstudio::timeline, item_flag_atom); + ADD_ATOM(xstudio::timeline, focus_atom); ADD_ATOM(xstudio::timeline, move_item_atom); - ADD_ATOM(xstudio::timeline, erase_item_atom); + ADD_ATOM(xstudio::timeline, move_item_at_frame_atom); + ADD_ATOM(xstudio::timeline, remove_item_at_frame_atom); + ADD_ATOM(xstudio::timeline, remove_item_atom); + 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::thumbnail, cache_path_atom); ADD_ATOM(xstudio::thumbnail, cache_stats_atom); @@ -180,6 +192,7 @@ void py_config::add_atoms() { 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, remove_attribute_atom); ADD_ATOM(xstudio::global, exit_atom); ADD_ATOM(xstudio::global, api_exit_atom); @@ -357,6 +370,9 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::history, history_atom); ADD_ATOM(xstudio::ui::viewport, viewport_playhead_atom); + ADD_ATOM(xstudio::ui::viewport, quickview_media_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); } diff --git a/src/python_module/src/py_context.cpp b/src/python_module/src/py_context.cpp index d9e131cd2..0bc22cdfb 100644 --- a/src/python_module/src/py_context.cpp +++ b/src/python_module/src/py_context.cpp @@ -1,5 +1,7 @@ // 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" @@ -297,4 +299,4 @@ bool py_context::connect_local(caf::actor actor) { } -} // namespace caf::python \ No newline at end of file +} // namespace caf::python diff --git a/src/python_module/src/py_link.cpp b/src/python_module/src/py_link.cpp index 72e71b851..f72068277 100644 --- a/src/python_module/src/py_link.cpp +++ b/src/python_module/src/py_link.cpp @@ -1,5 +1,7 @@ // 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" diff --git a/src/python_module/src/py_messages.cpp b/src/python_module/src/py_messages.cpp index d536228ee..0bce6319f 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.cpp @@ -1,9 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wattributes" +#endif // #include // #include // #include +#include "xstudio/utility/helpers.hpp" #include "py_opaque.hpp" @@ -13,7 +16,6 @@ #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" @@ -99,6 +101,19 @@ void py_config::add_messages() { "std::pair>", nullptr); + add_message_type>>( + "std::vector>", "std::vector>", nullptr); + + add_message_type("timebase::flicks", "timebase::flicks", nullptr); + + add_message_type>>( + "std::vector>", + "std::vector>", + nullptr); + + add_message_type( + "xstudio::utility::time_point", "xstudio::utility::time_point", nullptr); + add_message_type>( "UuidVec", "std::vector", ®ister_uuidvec_class); add_message_type( @@ -155,4 +170,4 @@ void py_config::add_messages() { "std::pair", nullptr); } -} // namespace caf::python \ No newline at end of file +} // namespace caf::python diff --git a/src/python_module/src/py_playhead.cpp b/src/python_module/src/py_playhead.cpp index 9ab62c511..2023cdcc2 100644 --- a/src/python_module/src/py_playhead.cpp +++ b/src/python_module/src/py_playhead.cpp @@ -1,5 +1,7 @@ // 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" diff --git a/src/python_module/src/py_plugin.cpp b/src/python_module/src/py_plugin.cpp index f63273a76..6d02551f5 100644 --- a/src/python_module/src/py_plugin.cpp +++ b/src/python_module/src/py_plugin.cpp @@ -1,5 +1,7 @@ // 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" @@ -20,13 +22,17 @@ using namespace xstudio; namespace py = pybind11; void py_plugin(py::module_ &m) { - py::enum_(m, "PluginType") - .value("PT_CUSTOM", plugin_manager::PluginType::PT_CUSTOM) - .value("PT_MEDIA_READER", plugin_manager::PluginType::PT_MEDIA_READER) - .value("PT_MEDIA_HOOK", plugin_manager::PluginType::PT_MEDIA_HOOK) - .value("PT_MEDIA_METADATA", plugin_manager::PluginType::PT_MEDIA_METADATA) - .value("PT_COLOUR_MANAGEMENT", plugin_manager::PluginType::PT_COLOUR_MANAGEMENT) - .value("PT_DATA_SOURCE", plugin_manager::PluginType::PT_DATA_SOURCE) - .value("PT_UTILITY", plugin_manager::PluginType::PT_UTILITY) + py::enum_(m, "PluginFlags") + .value("PF_CUSTOM", plugin_manager::PluginFlags::PF_CUSTOM) + .value("PF_MEDIA_READER", plugin_manager::PluginFlags::PF_MEDIA_READER) + .value("PF_MEDIA_HOOK", plugin_manager::PluginFlags::PF_MEDIA_HOOK) + .value("PF_MEDIA_METADATA", plugin_manager::PluginFlags::PF_MEDIA_METADATA) + .value("PF_COLOUR_MANAGEMENT", plugin_manager::PluginFlags::PF_COLOUR_MANAGEMENT) + .value("PF_COLOUR_OPERATION", plugin_manager::PluginFlags::PF_COLOUR_OPERATION) + .value("PF_DATA_SOURCE", plugin_manager::PluginFlags::PF_DATA_SOURCE) + .value("PF_VIEWPORT_OVERLAY", plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY) + .value("PF_HEAD_UP_DISPLAY", plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY) + .value("PF_UTILITY", plugin_manager::PluginFlags::PF_UTILITY) + .value("PF_CONFORM", plugin_manager::PluginFlags::PF_CONFORM) .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 30def9f59..69d279313 100644 --- a/src/python_module/src/py_register.cpp +++ b/src/python_module/src/py_register.cpp @@ -153,6 +153,12 @@ void register_streamdetail_class(py::module &m, const std::string &name) { .def("name", [](const media::StreamDetail &x) { return x.name_; }) .def("key_format", [](const media::StreamDetail &x) { return x.key_format_; }) .def("duration", [](const media::StreamDetail &x) { return x.duration_; }) + .def( + "resolution", + [](const media::StreamDetail &x) { + return std::vector({x.resolution_.x, x.resolution_.y}); + }) + .def("pixel_aspect", [](const media::StreamDetail &x) { return x.pixel_aspect_; }) .def("media_type", [](const media::StreamDetail &x) { return x.media_type_; }); } @@ -211,6 +217,7 @@ void register_bookmark_detail_class(py::module &m, const std::string &name) { .def_readonly("uuid", &bookmark::BookmarkDetail::uuid_) .def_readwrite("enabled", &bookmark::BookmarkDetail::enabled_) .def_readwrite("has_focus", &bookmark::BookmarkDetail::has_focus_) + .def_readwrite("visible", &bookmark::BookmarkDetail::visible_) .def_readwrite("start", &bookmark::BookmarkDetail::start_) .def_readwrite("duration", &bookmark::BookmarkDetail::duration_) .def_readwrite("author", &bookmark::BookmarkDetail::author_) @@ -404,6 +411,8 @@ void register_item_class(py::module &m, const std::string &name) { .def("rate", &timeline::Item::rate) .def("name", &timeline::Item::name) + .def("flag", &timeline::Item::flag) + .def("prop", &timeline::Item::prop) .def("active_range", &timeline::Item::active_range) .def("active_duration", &timeline::Item::active_duration) @@ -421,7 +430,8 @@ void register_item_class(py::module &m, const std::string &name) { "resolve_time", &timeline::Item::resolve_time, py::arg("time") = utility::FrameRate(), - py::arg("media_type") = media::MediaType::MT_IMAGE) + py::arg("media_type") = media::MediaType::MT_IMAGE, + py::arg("focus") = utility::UuidSet()) .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 090cd9b30..3b8c05b22 100644 --- a/src/python_module/src/py_remote_session_file.cpp +++ b/src/python_module/src/py_remote_session_file.cpp @@ -1,5 +1,7 @@ // 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" diff --git a/src/python_module/src/py_types.cpp b/src/python_module/src/py_types.cpp index 72c22c4b0..d44ee8dec 100644 --- a/src/python_module/src/py_types.cpp +++ b/src/python_module/src/py_types.cpp @@ -1,5 +1,7 @@ // 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" #include "py_config.hpp" diff --git a/src/python_module/src/py_ui.cpp b/src/python_module/src/py_ui.cpp index aa68afc00..ff4dbeb9b 100644 --- a/src/python_module/src/py_ui.cpp +++ b/src/python_module/src/py_ui.cpp @@ -1,5 +1,7 @@ // 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" // CAF_PUSH_WARNINGS diff --git a/src/python_module/src/py_utility.cpp b/src/python_module/src/py_utility.cpp index a0703352b..d5f80870a 100644 --- a/src/python_module/src/py_utility.cpp +++ b/src/python_module/src/py_utility.cpp @@ -1,5 +1,7 @@ // 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" diff --git a/src/python_module/src/py_xstudio.cpp b/src/python_module/src/py_xstudio.cpp index 8a0c1bb48..21abba7dd 100644 --- a/src/python_module/src/py_xstudio.cpp +++ b/src/python_module/src/py_xstudio.cpp @@ -1,5 +1,7 @@ // 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" diff --git a/src/scanner/src/scanner_actor.cpp b/src/scanner/src/scanner_actor.cpp index 8c1e1d5c5..e27b864f4 100644 --- a/src/scanner/src/scanner_actor.cpp +++ b/src/scanner/src/scanner_actor.cpp @@ -53,7 +53,7 @@ media::MediaStatus check_media_status(const MediaReference &mr) { } } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { ms = media::MediaStatus::MS_UNREADABLE; } @@ -175,18 +175,30 @@ ScanHelperActor::ScanHelperActor(caf::actor_config &cfg) : caf::event_based_acto for (const auto &entry : fs::recursive_directory_iterator(path)) { try { if (fs::is_regular_file(entry.status())) { - // check we've not alredy got it in cache.. + // check we've not alredy got it in cache.. +#ifdef _WIN32 + const auto puri = posix_path_to_uri(entry.path().string()); +#else const auto puri = posix_path_to_uri(entry.path()); +#endif if (cache_.count(puri)) { const auto &c = cache_.at(puri); if (c == pin) return puri; } else { +#ifdef _WIN32 + auto size = get_file_size(entry.path().string()); +#else auto size = get_file_size(entry.path()); +#endif if (size == pin.second) { +#ifdef _WIN32 + auto checksum = get_checksum(entry.path().string()); +#else auto checksum = get_checksum(entry.path()); - cache_[puri] = std::make_pair(checksum, size); +#endif + cache_[puri] = std::make_pair(checksum, size); if (checksum == pin.first) return puri; } diff --git a/src/session/src/session_actor.cpp b/src/session/src/session_actor.cpp index 114763f51..fc27d7101 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include "xstudio/atoms.hpp" #include "xstudio/bookmark/bookmarks_actor.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -314,7 +316,7 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { [=](UuidUuidActor playlist) { for (const auto &i : uris_) { fs::path p(uri_to_posix_path(i)); - if (p.extension() != ".xst") + if (not is_session(p.string())) anon_send( playlist.second.actor(), playlist::add_media_atom_v, @@ -332,7 +334,13 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { for (const auto &i : uris_) { 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())) +#else request(session_, infinite, add_playlist_atom_v, std::string(p.filename())) +#endif + .then( [=](UuidUuidActor playlist) { anon_send( @@ -347,7 +355,7 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { }); } else { - if (p.extension() == ".xst") + if (is_session(p.string())) anon_send(session_, merge_session_atom_v, i); else has_files = true; @@ -360,7 +368,7 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { [=](UuidUuidActor playlist) { for (const auto &i : uris_) { fs::path p(uri_to_posix_path(i)); - if (!fs::is_directory(p) and p.extension() != ".xst") + if (!fs::is_directory(p) and not is_session(p.string())) anon_send( playlist.second.actor(), playlist::add_media_atom_v, @@ -875,10 +883,7 @@ caf::message_handler SessionActor::message_handler() { auto rp = make_response_promise(); try { - JsonStore js; - std::ifstream i(uri_to_posix_path(path)); - i >> js; - auto session = spawn(js, path); + auto session = spawn(utility::open_session(path), path); rp.delegate(actor_cast(this), merge_session_atom_v, session); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -1487,6 +1492,18 @@ caf::message_handler SessionActor::message_handler() { } 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, + bool force) { + // forward to the studio actor + anon_send( + home_system().registry().get(studio_registry), + ui::open_quickview_window_atom_v, + media_items, + compare_mode, + force); }}; } @@ -1876,6 +1893,7 @@ void SessionActor::save_json_to( return rp.deliver(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 @@ -1884,27 +1902,51 @@ void SessionActor::save_json_to( 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 - // 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()) { + + // 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(); - fs::remove(save_path + ".tmp"); + } 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"); } - throw std::runtime_error("Failed to open file"); } + // rename tmp to final name fs::rename(save_path + ".tmp", save_path); diff --git a/src/shotgun_client/src/shotgun_client_actor.cpp b/src/shotgun_client/src/shotgun_client_actor.cpp index 47b545e5b..aaaa65eac 100644 --- a/src/shotgun_client/src/shotgun_client_actor.cpp +++ b/src/shotgun_client/src/shotgun_client_actor.cpp @@ -181,12 +181,26 @@ void ShotgunClientActor::init() { return rp; }, + [=](shotgun_update_entity_atom atom, + const std::string &entity, + const int record_id, + const JsonStore &body) { + delegate( + actor_cast(this), + atom, + entity, + record_id, + body, + std::vector()); + }, + [=](shotgun_update_entity_atom, const std::string &entity, const int record_id, - const JsonStore &body) -> result { + const JsonStore &body, + const std::vector &fields) -> result { auto rp = make_response_promise(); - // spdlog::warn("shotgun_update_entity_atom"); + request( http_, infinite, @@ -195,6 +209,8 @@ void ShotgunClientActor::init() { 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 { @@ -217,7 +233,8 @@ void ShotgunClientActor::init() { shotgun_update_entity_atom_v, entity, record_id, - body); + body, + fields); }, [=](error &err) mutable { spdlog::warn( @@ -301,6 +318,69 @@ void ShotgunClientActor::init() { return rp; }, + [=](shotgun_delete_entity_atom, + 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)); }); + + return rp; + }, + [=](shotgun_entity_atom atom, const std::string &entity, const int record_id) { delegate( actor_cast(this), @@ -607,8 +687,6 @@ void ShotgunClientActor::init() { jsn["sort"] = sort; jsn["filters"] = conditions; - // spdlog::warn("{}", jsn.dump(2)); - // requires authentication.. request( http_, @@ -1499,40 +1577,44 @@ void ShotgunClientActor::init() { }); return rp; - }, - - [=](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; - }); + } + + //, + // 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( diff --git a/src/studio/src/studio_actor.cpp b/src/studio/src/studio_actor.cpp index 765a862a4..6a9487871 100644 --- a/src/studio/src/studio_actor.cpp +++ b/src/studio/src/studio_actor.cpp @@ -10,6 +10,7 @@ #include "xstudio/utility/frame_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" +#include "xstudio/global_store/global_store.hpp" using namespace caf; using namespace xstudio::studio; @@ -56,6 +57,8 @@ void StudioActor::init() { [=](session::session_atom) -> caf::actor { return session_; }, + [=](bookmark::get_bookmark_atom atom) { delegate(session_, atom); }, + [=](session::session_atom, caf::actor session) -> bool { unlink_from(session_); send_exit(session_, caf::exit_reason::user_shutdown); @@ -74,6 +77,28 @@ void StudioActor::init() { return true; }, + [=](ui::show_message_box_atom, + const std::string &message_title, + const std::string &message_body, + const bool close_button, + const int timeout_seconds) { + caf::actor studio_ui_actor = + system().registry().template get(studio_ui_registry); + + // 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, + utility::event_atom_v, + ui::show_message_box_atom_v, + message_title, + message_body, + close_button, + timeout_seconds); + } + }, + // [&](session::create_player_atom atom, const std::string &name) {// delegate(session_, // atom, name);// }, [=](utility::serialise_atom) -> result { @@ -96,6 +121,61 @@ void StudioActor::init() { jsn["base"] = base_.serialise(); jsn["session"] = nullptr; return result(jsn); + }, + [=](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); + }, + [=](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 (...) { + } + } + + 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); + } + } + }, + [=](ui::open_quickview_window_atom, caf::actor studio_ui_actor) { + // the StudioUI instance has started up and pinged us with itself + // so we can send it any pending requests for quickviewers + + for (const auto &r : quickview_requests_) { + anon_send( + studio_ui_actor, + utility::event_atom_v, + ui::open_quickview_window_atom_v, + r.media_actors, + r.compare_mode); + } + quickview_requests_.clear(); }); } diff --git a/src/thumbnail/src/thumbnail.cpp b/src/thumbnail/src/thumbnail.cpp index 37cc795f9..e51829376 100644 --- a/src/thumbnail/src/thumbnail.cpp +++ b/src/thumbnail/src/thumbnail.cpp @@ -18,7 +18,7 @@ void DiskCacheStat::populate(const std::string &path) { if (fs::is_regular_file(entry.status())) { auto mtime = fs::last_write_time(entry.path()); add_thumbnail( - std::stoul(entry.path().stem().string(), nullptr, 16), + std::stoull(entry.path().stem().string(), nullptr, 16), fs::file_size(entry.path()), mtime); } diff --git a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp index 739b3ecda..81daaf6d2 100644 --- a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp +++ b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp @@ -259,19 +259,43 @@ ThumbnailBufferPtr TDCHelperActor::decode_thumb(const std::vector &bu static_cast(decompressInfo->output_height)); size_t pixel_size = decompressInfo->output_components; - if (pixel_size != 3) { - throw std::runtime_error("Invalid pixel size"); - } - // int colourspace = decompressInfo->out_color_space; - size_t row_stride = result->width() * pixel_size; - - // should match .. - std::byte *p = &(result->data()[0]); - while (decompressInfo->output_scanline < result->height()) { - ::jpeg_read_scanlines(decompressInfo.get(), reinterpret_cast(&p), 1); - p += row_stride; + + if (pixel_size == 3) { + size_t row_stride = result->width() * 3; + + // should match .. + std::byte *p = &(result->data()[0]); + while (decompressInfo->output_scanline < result->height()) { + ::jpeg_read_scanlines(decompressInfo.get(), reinterpret_cast(&p), 1); + p += row_stride; + } + ::jpeg_finish_decompress(decompressInfo.get()); + } else if (pixel_size == 1) { + size_t row_stride = result->width() * 3; + + char *mono = new char[result->width()]; + + // should match .. + std::byte *p = &(result->data()[0]); + while (decompressInfo->output_scanline < result->height()) { + ::jpeg_read_scanlines(decompressInfo.get(), reinterpret_cast(&mono), 1); + + for (auto i = 0; i < result->width(); i++) { + p[i * 3] = p[(i * 3) + 1] = p[(i * 3) + 2] = + static_cast(*(mono + i)); + } + + p += row_stride; + } + ::jpeg_finish_decompress(decompressInfo.get()); + + delete[] mono; + + } else { + // ::jpeg_finish_decompress(decompressInfo.get()); + throw std::runtime_error( + "Invalid pixel size " + std::to_string(pixel_size) + " != 3 or 1"); } - ::jpeg_finish_decompress(decompressInfo.get()); return result; } @@ -289,7 +313,11 @@ 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()); } @@ -330,8 +358,11 @@ 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()); } diff --git a/src/thumbnail/src/thumbnail_manager_actor.cpp b/src/thumbnail/src/thumbnail_manager_actor.cpp index 38d5c76ed..c849af536 100644 --- a/src/thumbnail/src/thumbnail_manager_actor.cpp +++ b/src/thumbnail/src/thumbnail_manager_actor.cpp @@ -253,6 +253,8 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) auto new_string = preference_value(js, "/core/thumbnail/disk_cache/path"); + new_string = expand_envvars(new_string); + if (cache_path != new_string) { cache_path = new_string; anon_send( diff --git a/src/timeline/src/CMakeLists.txt b/src/timeline/src/CMakeLists.txt index 63b8c58fc..a2f595024 100644 --- a/src/timeline/src/CMakeLists.txt +++ b/src/timeline/src/CMakeLists.txt @@ -1,7 +1,7 @@ -find_package(OpenTime REQUIRED) -find_package(OpenTimelineIO REQUIRED) -find_package(Imath) +#find_package(OpenTime REQUIRED) +#find_package(OpenTimelineIO REQUIRED) +#find_package(Imath) @@ -9,17 +9,17 @@ SET(LINK_DEPS xstudio::playhead xstudio::utility caf::core - OTIO::opentime - OTIO::opentimelineio - Imath::Imath + #OTIO::opentime + #OTIO::opentimelineio + #Imath::Imath ) SET(STATIC_LINK_DEPS xstudio::utility_static caf::core - Imath::Imath - OTIO::opentime - OTIO::opentimelineio + #Imath::Imath + #OTIO::opentime + #OTIO::opentimelineio ) create_component_static(timeline 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") diff --git a/src/timeline/src/clip.cpp b/src/timeline/src/clip.cpp index 24b90dc59..2f07eb906 100644 --- a/src/timeline/src/clip.cpp +++ b/src/timeline/src/clip.cpp @@ -3,6 +3,7 @@ #include "xstudio/timeline/clip.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/json_store.hpp" using namespace xstudio::timeline; using namespace xstudio; @@ -16,20 +17,46 @@ Clip::Clip( item_( ItemType::IT_CLIP, utility::UuidActorAddr(uuid(), caf::actor_cast(actor))), - media_uuid_(std::move(media_uuid)) {} + media_uuid_(std::move(media_uuid)) { + item_.set_name(name); + + auto jsn = R"({"media_uuid": null})"_json; + jsn["media_uuid"] = media_uuid_; + item_.set_prop(utility::JsonStore(jsn)); +} Clip::Clip(const utility::JsonStore &jsn) : Container(static_cast(jsn.at("container"))), item_(static_cast(jsn.at("item"))) { - media_uuid_ = jsn.at("media_uuid"); + + if (jsn.count("media_uuid")) { + media_uuid_ = jsn.at("media_uuid"); + auto jsn = R"({"media_uuid": null})"_json; + jsn["media_uuid"] = media_uuid_; + item_.set_prop(utility::JsonStore(jsn)); + } else { + media_uuid_ = item_.prop().at("media_uuid"); + } +} + +Clip Clip::duplicate() const { + utility::JsonStore jsn; + + auto dup_container = Container::duplicate(); + auto dup_item = item_; + dup_item.set_uuid(dup_container.uuid()); + + jsn["container"] = dup_container.serialise(); + jsn["item"] = dup_item.serialise(); + + return Clip(jsn); } utility::JsonStore Clip::serialise() const { utility::JsonStore jsn; - jsn["container"] = Container::serialise(); - jsn["item"] = item_.serialise(); - jsn["media_uuid"] = media_uuid_; + jsn["container"] = Container::serialise(); + jsn["item"] = item_.serialise(); return jsn; } diff --git a/src/timeline/src/clip_actor.cpp b/src/timeline/src/clip_actor.cpp index 52690e507..3303c4ec0 100644 --- a/src/timeline/src/clip_actor.cpp +++ b/src/timeline/src/clip_actor.cpp @@ -17,6 +17,14 @@ using namespace xstudio::utility; using namespace xstudio::timeline; using namespace caf; +ClipActor::ClipActor(caf::actor_config &cfg, const JsonStore &jsn) + : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { + base_.item().set_actor_addr(this); + base_.item().set_system(&system()); + + init(); +} + ClipActor::ClipActor(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); @@ -38,7 +46,9 @@ ClipActor::ClipActor( base_.item().set_name(name); if (media.actor()) { + media_ = caf::actor_cast(media.actor()); + monitor(media.actor()); join_event_group(this, media.actor()); @@ -47,7 +57,18 @@ ClipActor::ClipActor( auto ref = request_receive>( *sys, media.actor(), media::media_reference_atom_v)[0]; - base_.item().set_available_range(utility::FrameRange(ref.duration())); + if (name.empty()) { + 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); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -88,9 +109,15 @@ void ClipActor::init() { [=](link_media_atom, const UuidActorMap &media) -> bool { if (media.count(base_.media_uuid())) { auto media_actor = media.at(base_.media_uuid()); - monitor(media_actor); - join_event_group(this, media_actor); - media_ = caf::actor_cast(media_actor); + auto addr = caf::actor_cast(media_actor); + + if (media_ != addr) { + monitor(media_actor); + join_event_group(this, media_actor); + media_ = addr; + } + } else { + media_ = caf::actor_addr(); } return true; }, @@ -112,6 +139,13 @@ void ClipActor::init() { 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); + return jsn; + }, + [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) @@ -126,6 +160,16 @@ void ClipActor::init() { return jsn; }, + [=](active_range_atom) -> std::optional { + return base_.item().active_range(); + }, + + [=](available_range_atom) -> std::optional { + return base_.item().available_range(); + }, + + [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](history::undo_atom, const JsonStore &hist) -> result { base_.item().undo(hist); return true; @@ -206,19 +250,51 @@ void ClipActor::init() { // [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, // events from media actor + + // re-evaluate media reference.., needed for lazy loading + [=](media::acquire_media_detail_atom) { + auto actor = caf::actor_cast(media_); + if (actor) { + request(actor, infinite, media::media_reference_atom_v) + .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())); + + if (not jsn.is_null()) + send(event_group_, event_atom_v, item_atom_v, jsn, false); + } else { + // retry ? + delayed_send( + caf::actor_cast(this), + std::chrono::seconds(1), + media::acquire_media_detail_atom_v); + } + }, + [=](const error &err) {}); + } + }, + [=](utility::event_atom, playlist::reflag_container_atom, const 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); + // not sure why we want this.. + // send( + // event_group_, + // utility::event_atom_v, + // bookmark::bookmark_change_atom_v, + // bookmark_uuid); }, + + [=](utility::event_atom, bookmark::remove_bookmark_atom, const utility::Uuid &) {}, + [=](utility::event_atom, bookmark::add_bookmark_atom, const utility::UuidActor &) {}, + [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, [=](utility::event_atom, media::current_media_source_atom, @@ -464,6 +540,28 @@ void ClipActor::init() { delegate(caf::actor_cast(media_), atom); }, + [=](utility::duplicate_atom) -> result { + JsonStore jsn; + auto dup = base_.duplicate(); + jsn["base"] = dup.serialise(); + + auto actor = spawn(jsn); + UuidActorMap media_map; + media_map[base_.media_uuid()] = caf::actor_cast(media_); + + auto rp = make_response_promise(); + + request(actor, infinite, link_media_atom_v, media_map) + .then( + [=](const bool) mutable { rp.deliver(UuidActor(dup.uuid(), actor)); }, + [=](const caf::error &err) mutable { + send_exit(actor, caf::exit_reason::user_shutdown); + rp.deliver(err); + }); + + return rp; + }, + [=](utility::serialise_atom) -> JsonStore { JsonStore jsn; jsn["base"] = base_.serialise(); diff --git a/src/timeline/src/gap.cpp b/src/timeline/src/gap.cpp index e3ee9d177..24e218e0e 100644 --- a/src/timeline/src/gap.cpp +++ b/src/timeline/src/gap.cpp @@ -18,7 +18,9 @@ Gap::Gap( ItemType::IT_GAP, utility::UuidActorAddr(uuid(), caf::actor_cast(actor)), utility::FrameRange(FrameRateDuration(0, duration.rate()), duration), - utility::FrameRange(FrameRateDuration(0, duration.rate()), duration)) {} + utility::FrameRange(FrameRateDuration(0, duration.rate()), duration)) { + item_.set_name(name); +} Gap::Gap(const utility::JsonStore &jsn) : Container(static_cast(jsn.at("container"))), @@ -32,3 +34,17 @@ utility::JsonStore Gap::serialise() const { return jsn; } + + +Gap Gap::duplicate() const { + utility::JsonStore jsn; + + auto dup_container = Container::duplicate(); + auto dup_item = item_; + dup_item.set_uuid(dup_container.uuid()); + + jsn["container"] = dup_container.serialise(); + jsn["item"] = dup_item.serialise(); + + return Gap(jsn); +} diff --git a/src/timeline/src/gap_actor.cpp b/src/timeline/src/gap_actor.cpp index 60e804d3e..0e10f5ca6 100644 --- a/src/timeline/src/gap_actor.cpp +++ b/src/timeline/src/gap_actor.cpp @@ -67,6 +67,12 @@ void GapActor::init() { 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); + return jsn; + }, [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); @@ -89,6 +95,16 @@ void GapActor::init() { return jsn; }, + [=](active_range_atom) -> std::optional { + return base_.item().active_range(); + }, + + [=](available_range_atom) -> std::optional { + return base_.item().available_range(); + }, + + [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](link_media_atom, const UuidActorMap &) -> bool { return true; }, [=](item_atom) -> Item { return base_.item(); }, @@ -117,6 +133,15 @@ void GapActor::init() { [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](const group_down_msg & /*msg*/) {}, + [=](utility::duplicate_atom) -> UuidActor { + JsonStore jsn; + auto dup = base_.duplicate(); + jsn["base"] = dup.serialise(); + + auto actor = spawn(jsn); + return UuidActor(dup.uuid(), actor); + }, + [=](utility::serialise_atom) -> JsonStore { JsonStore jsn; jsn["base"] = base_.serialise(); diff --git a/src/timeline/src/item.cpp b/src/timeline/src/item.cpp index 626989f1e..1f9888971 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include -#include "xstudio/timeline/item.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/timeline/item.hpp" using namespace xstudio; using namespace xstudio::timeline; @@ -14,6 +15,8 @@ Item::Item(const utility::JsonStore &jsn, caf::actor_system *system) item_type_ = jsn.at("type"); enabled_ = jsn.at("enabled"); name_ = jsn.value("name", ""); + flag_ = jsn.value("flag", ""); + prop_ = jsn.value("prop", JsonStore()); if (jsn.count("actor_addr")) uuid_addr_.second = string_to_actor_addr(jsn.at("actor_addr")); @@ -67,6 +70,8 @@ utility::JsonStore Item::serialise(const int depth) const { jsn["type"] = item_type_; jsn["enabled"] = enabled_; jsn["name"] = name_; + jsn["flag"] = flag_; + jsn["prop"] = prop_; if (has_available_range_) jsn["available_range"] = available_range_; @@ -338,8 +343,10 @@ utility::UuidActorVector Item::find_all_uuid_actors(const ItemType item_type) co return items; } -std::optional> -Item::resolve_time(const utility::FrameRate &time, const media::MediaType mt) const { +std::optional Item::resolve_time( + const utility::FrameRate &time, + const media::MediaType mt, + const utility::UuidSet &focus) const { if (transparent()) return {}; @@ -350,7 +357,7 @@ Item::resolve_time(const utility::FrameRate &time, const media::MediaType mt) co case IT_TIMELINE: // pass to stack if (not empty()) { - auto t = front().resolve_time(time + trimmed_start(), mt); + auto t = front().resolve_time(time + trimmed_start(), mt, focus); if (t) return *t; } @@ -362,23 +369,55 @@ Item::resolve_time(const utility::FrameRate &time, const media::MediaType mt) co // needs depth first search ? // most of the logic lives here.. if (mt == media::MediaType::MT_IMAGE) { + std::optional found_item = {}; + for (const auto &it : *this) { // we skip audio track.. if (it.transparent() or it.item_type() == IT_AUDIO_TRACK) continue; - auto t = it.resolve_time(time + trimmed_start(), mt); - if (t) - return *t; + + auto t = it.resolve_time(time + trimmed_start(), mt, focus); + + if (t) { + if (focus.empty()) + return *t; + + if (focus.count(it.uuid()) and std::get<0>(*t).item_type() == IT_CLIP) + return *t; + + if (focus.count(std::get<0>(*t).uuid())) + return *t; + + if (not found_item and std::get<0>(*t).item_type() == IT_CLIP) + found_item = *t; + } } + if (found_item) + return *found_item; + } else { + std::optional found_item = {}; for (const auto &it : *this) { // we skip video track if (it.transparent() or it.item_type() == IT_VIDEO_TRACK) continue; - auto t = it.resolve_time(time + trimmed_start(), mt); - if (t) - return *t; + auto t = it.resolve_time(time + trimmed_start(), mt, focus); + if (t) { + if (focus.empty()) + return *t; + + if (focus.count(it.uuid()) and std::get<0>(*t).item_type() == IT_CLIP) + return *t; + + if (focus.count(std::get<0>(*t).uuid())) + return *t; + + if (not found_item and std::get<0>(*t).item_type() == IT_CLIP) + found_item = *t; + } } + if (found_item) + return *found_item; } // we shouldn't return the container.. break; @@ -397,7 +436,7 @@ Item::resolve_time(const utility::FrameRate &time, const media::MediaType mt) co if (ttp + ts >= td) { ttp -= td; } else { - auto t = it.resolve_time(ttp + ts, mt); + auto t = it.resolve_time(ttp + ts, mt, focus); if (t) return *t; break; @@ -422,6 +461,10 @@ void Item::set_enabled_direct(const bool &value) { enabled_ = 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; } + utility::JsonStore Item::set_enabled(const bool &value) { if (enabled_ != value) { utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); @@ -450,6 +493,34 @@ utility::JsonStore Item::set_name(const std::string &value) { return utility::JsonStore(); } +utility::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; + jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; + jsn[0]["undo"]["value"] = flag_; + jsn[0]["redo"]["value"] = value; + set_flag_direct(value); + return jsn; + } + + return utility::JsonStore(); +} + +utility::JsonStore Item::set_prop(const utility::JsonStore &value) { + if (prop_ != value) { + utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_PROP; + jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; + jsn[0]["undo"]["value"] = prop_; + jsn[0]["redo"]["value"] = value; + set_prop_direct(value); + return jsn; + } + + return utility::JsonStore(); +} + void Item::set_actor_addr_direct(const caf::actor_addr &value) { uuid_addr_.second = value; } utility::JsonStore Item::make_actor_addr_update() const { @@ -563,7 +634,7 @@ 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) { +utility::JsonStore Item::erase(Items::iterator position, const utility::JsonStore &blind) { utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); auto index = std::distance(begin(), position); @@ -572,7 +643,7 @@ utility::JsonStore Item::erase(Items::iterator position) { jsn[0]["undo"]["action"] = ItemAction::IT_INSERT; jsn[0]["undo"]["index"] = index; jsn[0]["undo"]["item"] = position->serialise(); - jsn[0]["undo"]["blind"] = nullptr; + jsn[0]["undo"]["blind"] = blind; jsn[0]["redo"]["action"] = ItemAction::IT_REMOVE; jsn[0]["redo"]["index"] = index; @@ -599,34 +670,35 @@ utility::JsonStore Item::splice( utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - auto pos_index = std::distance(cbegin(), pos); - auto first_index = std::distance(cbegin(), first); - auto last_index = std::distance(cbegin(), last); - - // splice can't insert into range.. - // move position to end of range. - if (pos_index >= first_index and pos_index <= last_index) { - pos_index = last_index + 1; - pos = std::next(last, 1); - } + auto dst_index = std::distance(cbegin(), pos); + auto start_index = std::distance(cbegin(), first); + auto count = std::distance(first, last); jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["redo"]["action"] = ItemAction::IT_SPLICE; - jsn[0]["redo"]["dst"] = pos_index; // dst - jsn[0]["redo"]["first"] = first_index; // frst - jsn[0]["redo"]["last"] = last_index; // lst + jsn[0]["redo"]["dst"] = dst_index; // dst + jsn[0]["redo"]["first"] = start_index; // frst + jsn[0]["redo"]["count"] = count; + + int undo_first; + auto undo_dst = start_index; - if (pos_index > last_index) { - pos_index -= (last_index - first_index); + if (dst_index > start_index) { + undo_first = dst_index - count; + } else { + undo_first = dst_index; + undo_dst += count; } jsn[0]["undo"]["action"] = ItemAction::IT_SPLICE; - jsn[0]["undo"]["dst"] = first_index; // dst - jsn[0]["undo"]["first"] = pos_index; - jsn[0]["undo"]["last"] = pos_index + (last_index - first_index); + jsn[0]["undo"]["dst"] = undo_dst; + jsn[0]["undo"]["first"] = undo_first; + jsn[0]["undo"]["count"] = count; - // spdlog::warn("{}", jsn.dump(2)); + // std::cerr << "first " << undo_first << std::endl; + // std::cerr << " dst " << undo_dst << std::endl; + // std::cerr << std::endl << std::flush; splice_direct(pos, other, first, last); @@ -652,9 +724,9 @@ void Item::redo(const utility::JsonStore &event) { } bool Item::process_event(const utility::JsonStore &event) { - // spdlog::warn("{} {} {}", event["uuid"], to_string(uuid_addr_.first), event["uuid"] == - // uuid_addr_.first); - if (event.at("uuid") == uuid_addr_.first) { + // spdlog::warn("{}", event.dump(2)); + + if (Uuid(event.at("uuid")) == uuid_addr_.first) { switch (static_cast(event.at("action"))) { case IT_ENABLE: set_enabled_direct(event.at("value")); @@ -662,6 +734,12 @@ bool Item::process_event(const utility::JsonStore &event) { case IT_NAME: set_name_direct(event.at("value")); break; + case IT_FLAG: + set_flag_direct(event.at("value")); + break; + case IT_PROP: + set_prop_direct(event.at("value")); + break; case IT_ACTIVE: set_active_range_direct(event.at("value")); has_active_range_ = event.at("value2"); @@ -671,23 +749,46 @@ bool Item::process_event(const utility::JsonStore &event) { has_available_range_ = event.at("value2"); break; case IT_INSERT: { - auto it = begin(); - std::advance(it, event.at("index")); - insert_direct(it, Item(JsonStore(event.at("item")), the_system_)); + // spdlog::warn("IT_INSERT {}", event.dump(2)); + auto index = event.at("index").get(); + if (index == 0 or 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)); + } } break; case IT_REMOVE: { - auto it = begin(); - std::advance(it, event.at("index")); - erase_direct(it); + // 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 {} {} {} {} {} {}", + to_string(uuid()), + name(), + to_string(item_type()), + size(), + index, + event.dump(2)); + } } break; case IT_SPLICE: { - auto it1 = begin(); - std::advance(it1, event.at("dst")); - auto it2 = begin(); - std::advance(it2, event.at("first")); - auto it3 = begin(); - std::advance(it3, event.at("last")); - splice_direct(it1, *this, it2, it3); + // spdlog::warn("IT_SPLICE {}", event.dump(2)); + auto dst = event.at("dst").get(); + auto first = event.at("first").get(); + if ((dst == 0 or dst <= size()) and (first == 0 or first < size())) { + auto it1 = std::next(begin(), dst); + auto it2 = std::next(begin(), first); + auto it3 = std::next(it2, event.at("count").get()); + + splice_direct(it1, *this, it2, it3); + } else { + spdlog::error( + "IT_SPLICE - INVALID INDEX {} {} {} {}", size(), first, dst, event.dump(2)); + } } break; case IT_ADDR: @@ -725,3 +826,74 @@ void Item::bind_item_event_func(ItemEventFunc fn, const bool recursive) { i.bind_item_event_func(fn, recursive_bind_); } } + +std::optional> +Item::item_at_frame(const int track_frame) const { + auto start = trimmed_frame_start().frames(); + auto duration = trimmed_frame_duration().frames(); + + if (track_frame >= start or track_frame <= start + duration - 1) { + // should be valid.. + // increment start til we find item. + + for (auto it = cbegin(); it != cend(); it++) { + if (start + it->trimmed_frame_duration().frames() > track_frame) { + return std::make_pair( + it, (track_frame - start) + it->trimmed_frame_start().frames()); + } else { + start += it->trimmed_frame_duration().frames(); + } + } + } + + return {}; +} + +utility::FrameRange Item::range_at_index(const int item_index) const { + auto result = utility::FrameRange(); + result.set_rate(trimmed_range().rate()); + + auto start = trimmed_range().start(); + auto end = + item_index >= static_cast(size()) ? cend() : std::next(cbegin(), item_index); + auto it = cbegin(); + + for (; it != end; ++it) + start += it->trimmed_range().duration(); + + if (it != cend()) + result.set_duration(it->trimmed_range().duration()); + else + result.set_duration(FrameRate()); + + result.set_start(start); + + return result; +} + + +int Item::frame_at_index(const int item_index) const { + int result = trimmed_frame_start().frames(); + auto end = + item_index >= static_cast(size()) ? cend() : std::next(cbegin(), item_index); + + for (auto it = cbegin(); it != end; ++it) + result += it->trimmed_frame_duration().frames(); + + return result; +} + +int Item::frame_at_index(const int item_index, const int item_frame) const { + auto dur = item_frame; + + if (item_index < static_cast(size()) and item_index >= 0) + dur -= std::next(cbegin(), item_index)->trimmed_frame_start().frames(); + + return frame_at_index(item_index) + dur; +} + +std::optional Item::item_at_index(const int index) const { + if (index < 0 or index >= static_cast(size())) + return {}; + return std::next(begin(), index); +} diff --git a/src/timeline/src/stack.cpp b/src/timeline/src/stack.cpp index 381d0cb9c..590930175 100644 --- a/src/timeline/src/stack.cpp +++ b/src/timeline/src/stack.cpp @@ -12,7 +12,9 @@ Stack::Stack(const std::string &name, const utility::Uuid &uuid_, const caf::act : Container(name, "Stack", uuid_), item_( ItemType::IT_STACK, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) {} + utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + item_.set_name(name); +} Stack::Stack(const JsonStore &jsn) : Container(static_cast(jsn.at("container"))), @@ -26,3 +28,16 @@ JsonStore Stack::serialise() const { return jsn; } + +Stack Stack::duplicate() const { + utility::JsonStore jsn; + + auto dup_container = Container::duplicate(); + auto dup_item = item_; + dup_item.set_uuid(dup_container.uuid()); + + jsn["container"] = dup_container.serialise(); + jsn["item"] = dup_item.serialise(1); + + return Stack(jsn); +} \ No newline at end of file diff --git a/src/timeline/src/stack_actor.cpp b/src/timeline/src/stack_actor.cpp index d4e453968..29d4c7abb 100644 --- a/src/timeline/src/stack_actor.cpp +++ b/src/timeline/src/stack_actor.cpp @@ -58,6 +58,25 @@ caf::actor StackActor::deserialise(const utility::JsonStore &value, const bool r return actor; } +StackActor::StackActor(caf::actor_config &cfg, const utility::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()) { + try { + deserialise(value, true); + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } + base_.item().set_system(&system()); + base_.item().bind_item_event_func([this](const utility::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"))) { @@ -226,6 +245,13 @@ void StackActor::init() { return rp; }, + [=](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); + return jsn; + }, + [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) @@ -263,6 +289,17 @@ void StackActor::init() { return jsn; }, + [=](active_range_atom) -> std::optional { + return base_.item().active_range(); + }, + + [=](available_range_atom) -> std::optional { + return base_.item().available_range(); + }, + + [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + + // should these be reflected upward ? [=](history::undo_atom, const JsonStore &hist) -> result { base_.item().undo(hist); if (actors_.empty()) @@ -295,15 +332,6 @@ void StackActor::init() { return rp; }, - // 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(event_group_, event_atom_v, item_atom_v, base_.item()); - // } - // }, - // handle child change events. [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { if (base_.item().update(update)) { @@ -318,154 +346,39 @@ void StackActor::init() { send(event_group_, event_atom_v, item_atom_v, update, hidden); }, - - [=](insert_item_atom, const int index, const UuidActor &ua) -> result { - auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - index, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); - - return rp; - }, - - // we only allow access to direct children.. ? - [=](insert_item_atom, const int index, const UuidActor &ua, const Item &item) - -> result { - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); - } - - // take ownership - add_item(ua); - - auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - // insert on index.. - // cheat.. - auto it = base_.item().begin(); - auto ind = 0; - for (int i = 0; it != base_.item().end(); i++, it++) { - if (i == index) - break; - } - - auto changes = base_.item().insert(it, item); - auto 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); - rp.deliver(changes); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); - - return rp; - }, - - [=](insert_item_atom, - const utility::Uuid &before_uuid, - const UuidActor &ua) -> result { + const int index, + const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - before_uuid, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); - + insert_items(index, uav, rp); return rp; }, [=](insert_item_atom, const utility::Uuid &before_uuid, - const UuidActor &ua, - const Item &item) -> result { - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); - } - // take ownership - add_item(ua); - + const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - auto changes = utility::JsonStore(); - - if (before_uuid.is_null()) { - changes = base_.item().insert(base_.item().end(), item); - } else { - auto it = find_uuid(base_.item().children(), before_uuid); - if (it == base_.item().end()) { - return rp.deliver( - make_error(xstudio_error::error, "Invalid uuid")); - } - changes = base_.item().insert(it, item); - } - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + auto index = base_.item().size(); + // find index. for uuid + if (not before_uuid.is_null()) { + auto it = find_uuid(base_.item().children(), before_uuid); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + else + index = std::distance(base_.item().begin(), it); + } - send(event_group_, event_atom_v, item_atom_v, changes, false); - rp.deliver(changes); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + if (rp.pending()) + insert_items(index, uav, rp); return rp; }, [=](move_item_atom, const int src_index, const int count, const int dst_index) -> result { - auto sit = base_.item().children().begin(); - std::advance(sit, src_index); - - if (sit == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid src index"); - - auto src_uuid = sit->uuid(); - // dst index is the index it should be after the move. - // we need to account for the items we're moving.. - auto dit = base_.item().children().begin(); - - if (dst_index == src_index) - return make_error(xstudio_error::error, "Invalid Move"); - - auto adj_dst = dst_index; - - if (dst_index > src_index) - adj_dst += count; - - // spdlog::warn("{} {} {} -> {}", src_index, count, dst_index, adj_dst); - - std::advance(dit, adj_dst); - auto dst_uuid = utility::Uuid(); - if (dit != base_.item().children().end()) - dst_uuid = dit->uuid(); - auto rp = make_response_promise(); - rp.delegate( - caf::actor_cast(this), move_item_atom_v, src_uuid, count, dst_uuid); + move_items(src_index, count, dst_index, rp); return rp; }, @@ -474,83 +387,111 @@ void StackActor::init() { const int count, const utility::Uuid &before_uuid) -> result { // check src is valid. + auto rp = make_response_promise(); auto sitb = find_uuid(base_.item().children(), src_uuid); if (sitb == base_.item().end()) - return make_error(xstudio_error::error, "Invalid src uuid"); + rp.deliver(make_error(xstudio_error::error, "Invalid src uuid")); - auto dit = base_.item().children().end(); - if (not before_uuid.is_null()) { - dit = find_uuid(base_.item().children(), before_uuid); - if (dit == base_.item().end()) - return make_error(xstudio_error::error, "Invalid dst uuid"); - } - if (count) { - auto site = sitb; - std::advance(site, count); - auto changes = base_.item().splice(dit, base_.item().children(), sitb, site); - auto 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); - return changes; + if (rp.pending()) { + auto dit = base_.item().children().end(); + if (not before_uuid.is_null()) { + dit = find_uuid(base_.item().children(), before_uuid); + if (dit == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid dst uuid")); + } + if (rp.pending()) + move_items( + std::distance(base_.item().begin(), sitb), + count, + std::distance(base_.item().begin(), dit), + rp); } - return JsonStore(); + return rp; }, - [=](remove_item_atom, const int index) -> result> { - auto it = base_.item().children().begin(); - std::advance(it, index); - if (it == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid index"); + [=](remove_item_atom, + const int index) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, 1, rp); + return rp; + }, - auto rp = make_response_promise>(); - rp.delegate(caf::actor_cast(this), remove_item_atom_v, it->uuid()); + [=](remove_item_atom, + const int index, + const int count) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, count, rp); return rp; }, - [=](remove_item_atom, const utility::Uuid &uuid) -> result> { - auto it = find_uuid(base_.item().children(), uuid); - if (it == base_.item().end()) - return make_error(xstudio_error::error, "Invalid uuid"); + [=](remove_item_atom, + const utility::Uuid &uuid) -> result>> { + auto rp = make_response_promise>>(); - auto item = *it; - demonitor(item.actor()); - actors_.erase(item.uuid()); + auto it = find_uuid(base_.item().children(), uuid); - auto changes = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); - send(event_group_, event_atom_v, item_atom_v, changes, false); + if (rp.pending()) + remove_items(std::distance(base_.item().begin(), it), 1, rp); - // as the item/actor still exists.. ? - // ACK!!!! What do we do !!! - return std::make_pair(changes, item); + return rp; }, [=](erase_item_atom, const int index) -> result { - auto it = base_.item().children().begin(); - std::advance(it, index); - if (it == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid index"); auto rp = make_response_promise(); - rp.delegate(caf::actor_cast(this), erase_item_atom_v, it->uuid()); + erase_items(index, 1, rp); + return rp; + }, + + [=](erase_item_atom, const int index, const int count) -> result { + auto rp = make_response_promise(); + erase_items(index, count, rp); return rp; }, [=](erase_item_atom, const utility::Uuid &uuid) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, remove_item_atom_v, uuid) - .then( - [=](const std::pair &hist_item) mutable { - send_exit(hist_item.second.actor(), caf::exit_reason::user_shutdown); - rp.deliver(hist_item.first); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + + auto it = find_uuid(base_.item().children(), uuid); + + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + + if (rp.pending()) + erase_items(std::distance(base_.item().begin(), it), 1, rp); + + return rp; + }, + + [=](utility::duplicate_atom) -> result { + auto rp = make_response_promise(); + JsonStore jsn; + auto dup = base_.duplicate(); + dup.item().clear(); + + jsn["base"] = dup.serialise(); + jsn["actors"] = {}; + auto actor = spawn(jsn); + + if (actors_.empty()) { + rp.deliver(UuidActor(dup.uuid(), actor)); + } else { + // duplicate all children and relink against items. + scoped_actor sys{system()}; + + for (const auto &i : base_.children()) { + auto ua = request_receive( + *sys, actors_[i.uuid()], utility::duplicate_atom_v); + request_receive( + *sys, actor, insert_item_atom_v, -1, UuidActorVector({ua})); + } + rp.deliver(UuidActor(dup.uuid(), actor)); + } + return rp; }, @@ -599,3 +540,133 @@ void StackActor::add_item(const utility::UuidActor &ua) { monitor(ua.actor()); actors_[ua.uuid()] = ua.actor(); } + +void StackActor::insert_items( + const int index, + const UuidActorVector &uav, + caf::typed_response_promise rp) { + // validate items can be inserted. + fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) + .then( + [=](std::vector items) mutable { + // items are valid for insertion ? + for (const auto &i : items) { + if (not base_.item().valid_child(i)) + return rp.deliver( + make_error(xstudio_error::error, "Invalid child type")); + } + + // take ownership + for (const auto &ua : uav) + add_item(ua); + + // find insertion point.. + auto it = std::next(base_.item().begin(), index); + + // insert items.. + // our list will be out of order.. + auto changes = JsonStore(R"([])"_json); + for (const auto &ua : uav) { + // find item.. + auto found = false; + for (const auto &i : items) { + if (ua.uuid() == i.uuid()) { + auto tmp = base_.item().insert(it, i); + changes.insert(changes.begin(), tmp.begin(), tmp.end()); + found = true; + break; + } + } + + if (not found) { + spdlog::error("item not found for insertion"); + } + } + + // add changes to stack + auto 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); + rp.deliver(changes); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); +} + +void StackActor::remove_items( + const int index, + const int count, + caf::typed_response_promise>> + rp) { + + 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")); + else { + scoped_actor sys{system()}; + + 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()); + 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); + } + } + + auto 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); + + rp.deliver(std::make_pair(changes, items)); + } +} + +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)); }); +} + +void StackActor::move_items( + const int src_index, + const int count, + const int dst_index, + caf::typed_response_promise rp) { + + // don't allow mixing audio / video tracks ? + + if (dst_index == src_index or not count) + rp.deliver(make_error(xstudio_error::error, "Invalid Move")); + else { + auto sit = std::next(base_.item().begin(), src_index); + auto eit = std::next(sit, count); + auto dit = std::next(base_.item().begin(), dst_index); + + auto changes = base_.item().splice(dit, base_.item().children(), sit, eit); + auto 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); + rp.deliver(changes); + } +} diff --git a/src/timeline/src/timeline.cpp b/src/timeline/src/timeline.cpp index b217efa86..c82bd2c75 100644 --- a/src/timeline/src/timeline.cpp +++ b/src/timeline/src/timeline.cpp @@ -12,7 +12,9 @@ Timeline::Timeline(const std::string &name, const utility::Uuid &_uuid, const ca : Container(name, "Timeline", _uuid), item_( ItemType::IT_TIMELINE, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) {} + utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + item_.set_name(name); +} Timeline::Timeline(const JsonStore &jsn) : Container(static_cast(jsn.at("container"))), @@ -28,3 +30,17 @@ JsonStore Timeline::serialise() const { return jsn; } + +Timeline Timeline::duplicate() const { + utility::JsonStore jsn; + + auto dup_container = Container::duplicate(); + auto dup_item = item_; + dup_item.set_uuid(dup_container.uuid()); + + jsn["container"] = dup_container.serialise(); + jsn["item"] = dup_item.serialise(1); + jsn["media"] = media_list_.serialise(); + + return Timeline(jsn); +} \ No newline at end of file diff --git a/src/timeline/src/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index cb638785a..84f9a6b4f 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -1,12 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #include +#ifdef BUILD_OTIO #include #include #include #include #include #include +#endif #include "xstudio/atoms.hpp" #include "xstudio/bookmark/bookmark_actor.hpp" @@ -80,6 +82,8 @@ void TimelineActor::item_event_callback(const utility::JsonStore &event, Item &i child_item_it->make_actor_addr_update(), true); } + spdlog::warn("TimelineActor IT_INSERT"); + // rebuilt child.. trigger relink } break; case IT_REMOVE: { @@ -105,6 +109,7 @@ void TimelineActor::item_event_callback(const utility::JsonStore &event, Item &i } } +#ifdef BUILD_OTIO namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; @@ -140,7 +145,12 @@ void process_item( source_range->duration().rate()))) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request(parent, infinite, insert_item_atom_v, -1, UuidActor(uuid, actor)) + self->request( + parent, + infinite, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); process_item(ii->children(), self, actor, media_lookup); @@ -161,7 +171,12 @@ void process_item( source_range->duration().rate()))) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request(parent, infinite, insert_item_atom_v, -1, UuidActor(uuid, actor)) + self->request( + parent, + infinite, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); process_item(ii->children(), self, actor, media_lookup); @@ -182,7 +197,12 @@ void process_item( source_range->duration().rate()))) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request(parent, infinite, insert_item_atom_v, -1, UuidActor(uuid, actor)) + self->request( + parent, + infinite, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); } else if (auto ii = dynamic_cast(&(*i))) { @@ -236,7 +256,12 @@ void process_item( .receive([=](const JsonStore &) {}, [=](const error &err) {}); } - self->request(parent, infinite, insert_item_atom_v, -1, UuidActor(uuid, actor)) + self->request( + parent, + infinite, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); } else if (auto ii = dynamic_cast(&(*i))) { @@ -256,7 +281,12 @@ void process_item( source_range->duration().rate()))) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request(parent, infinite, insert_item_atom_v, -1, UuidActor(uuid, actor)) + self->request( + parent, + infinite, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); process_item(ii->children(), self, parent, media_lookup); @@ -311,6 +341,18 @@ void timeline_importer( 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")) { + clip_metadata = JsonStore(clip_meta.at("metadata")); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + // check we're not adding the same media twice. UuidActorVector sources; @@ -332,11 +374,30 @@ void timeline_importer( rate = FrameRate(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("ExternalReference") : extname, *uri, rate, source_uuid); + + if (not source_metadata.is_null()) + anon_send( + source, + json_store::set_json_atom_v, + source_metadata, + "/metadata/timeline"); + sources.emplace_back(UuidActor(source_uuid, source)); } } @@ -349,6 +410,14 @@ void timeline_importer( auto uuid = Uuid::generate(); target_url_map[active_path] = 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, @@ -425,6 +494,8 @@ void timeline_importer( rp.deliver(true); } +#endif BUILD_OTIO + TimelineActor::TimelineActor( caf::actor_config &cfg, const utility::JsonStore &jsn, const caf::actor &playlist) @@ -434,7 +505,8 @@ TimelineActor::TimelineActor( base_.item().set_actor_addr(this); // parse and generate tracks/stacks. - anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); + if (playlist) + anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); for (const auto &[key, value] : jsn["actors"].items()) { try { @@ -464,7 +536,8 @@ TimelineActor::TimelineActor( // create default stack auto suuid = Uuid::generate(); auto stack = spawn("Stack", suuid); - anon_send(this, insert_item_atom_v, 0, UuidActor(suuid, stack)); + 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) { @@ -573,11 +646,28 @@ void TimelineActor::init() { 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 } 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); + return jsn; + }, + [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) @@ -589,18 +679,47 @@ void TimelineActor::init() { 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 } return jsn; }, + [=](active_range_atom) -> std::optional { + return base_.item().active_range(); + }, + + [=](available_range_atom) -> std::optional { + return base_.item().available_range(); + }, + + [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](item_atom) -> Item { return base_.item(); }, [=](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 } return jsn; }, @@ -649,17 +768,34 @@ void TimelineActor::init() { 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) + 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); return; } } send(event_group_, event_atom_v, item_atom_v, update, hidden); - if (not 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); }, @@ -691,6 +827,64 @@ void TimelineActor::init() { return rp; }, + [=](history::undo_atom, const utility::sys_time_duration &duration) -> result { + auto rp = make_response_promise(); + request(history_, infinite, history::undo_atom_v, duration) + .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) + .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(); + request(history_, infinite, history::redo_atom_v, duration) + .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) + .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(); request(history_, infinite, history::undo_atom_v) @@ -716,222 +910,190 @@ void TimelineActor::init() { }, [=](history::undo_atom, const JsonStore &hist) -> result { - base_.item().undo(hist); - if (actors_.empty()) - return true; - // push to children.. auto rp = make_response_promise(); - fan_out_request( - map_value_to_vec(actors_), infinite, history::undo_atom_v, hist) - .then( - [=](std::vector updated) mutable { rp.deliver(true); }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + 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); + } + + // send(event_group_, event_atom_v, item_atom_v, JsonStore(inverted), true); + 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_send(this, link_media_atom_v, media_actors_); + send( + event_group_, + event_atom_v, + item_atom_v, + JsonStore(inverted), + true); + rp.deliver(true); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + send(event_group_, event_atom_v, item_atom_v, JsonStore(inverted), true); + rp.deliver(true); + } return rp; }, [=](history::redo_atom, const JsonStore &hist) -> result { - base_.item().redo(hist); - if (actors_.empty()) - return true; - // push to children.. auto rp = make_response_promise(); + base_.item().redo(hist); - fan_out_request( - map_value_to_vec(actors_), infinite, history::redo_atom_v, hist) - .then( - [=](std::vector updated) mutable { rp.deliver(true); }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - }, + // send(event_group_, event_atom_v, item_atom_v, hist, true); - [=](insert_item_atom, const int index, const UuidActor &ua) -> result { - if (not base_.item().empty()) - return make_error(xstudio_error::error, "Only one child allowed"); - auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - index, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); - - return rp; - }, + 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_send(this, link_media_atom_v, media_actors_); - // we only allow access to direct children.. ? - [=](insert_item_atom, const int index, const UuidActor &ua, const Item &item) - -> result { - if (not base_.item().empty()) - return make_error(xstudio_error::error, "Only one child allowed"); - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); + send(event_group_, event_atom_v, item_atom_v, hist, true); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + send(event_group_, event_atom_v, item_atom_v, hist, true); + rp.deliver(true); } - // take ownership - add_item(ua); - - auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - // insert on index.. - // cheat.. - auto it = base_.item().begin(); - auto ind = 0; - for (int i = 0; it != base_.item().end(); i++, it++) { - if (i == index) - break; - } - - auto changes = base_.item().insert(it, item); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); - - // broadcast change. (may need to be finer grained) - 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); - - rp.deliver(true); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, [=](insert_item_atom, - const utility::Uuid &before_uuid, - const UuidActor &ua) -> result { - if (not base_.item().empty()) - return make_error(xstudio_error::error, "Only one child allowed"); + const int index, + const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - before_uuid, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + 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); return rp; }, [=](insert_item_atom, const utility::Uuid &before_uuid, - const UuidActor &ua, - const Item &item) -> result { - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); - } - - if (not base_.item().empty()) - return make_error(xstudio_error::error, "Only one child allowed"); + const UuidActorVector &uav) -> result { + auto rp = make_response_promise(); - // take ownership - add_item(ua); + if (not base_.item().empty() or uav.size() > 1) + rp.deliver(make_error(xstudio_error::error, "Only one child allowed")); + else { + + auto index = base_.item().size(); + // find index. for uuid + if (not before_uuid.is_null()) { + auto it = find_uuid(base_.item().children(), before_uuid); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + else + index = std::distance(base_.item().begin(), it); + } - auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - auto changes = utility::JsonStore(); - - if (before_uuid.is_null()) { - changes = base_.item().insert(base_.item().end(), item); - } else { - auto it = find_uuid(base_.item().children(), before_uuid); - if (it == base_.item().end()) { - return rp.deliver( - make_error(xstudio_error::error, "Invalid uuid")); - } - changes = base_.item().insert(it, item); - } + if (rp.pending()) + insert_items(index, uav, rp); + } - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + return rp; + }, - 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); - rp.deliver(true); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + [=](remove_item_atom, + const int index) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, 1, rp); return rp; }, - [=](remove_item_atom, const int index) -> result> { - auto it = base_.item().children().begin(); - std::advance(it, index); - if (it == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid index"); - auto rp = make_response_promise>(); - rp.delegate(caf::actor_cast(this), remove_item_atom_v, it->uuid()); + [=](remove_item_atom, + const int index, + const int count) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, count, rp); return rp; }, - [=](remove_item_atom, const utility::Uuid &uuid) -> result> { + [=](remove_item_atom, + const utility::Uuid &uuid) -> result>> { + auto rp = make_response_promise>>(); + auto it = find_uuid(base_.item().children(), uuid); - if (it == base_.item().end()) - return make_error(xstudio_error::error, "Invalid uuid"); - auto item = *it; - demonitor(item.actor()); - actors_.erase(item.uuid()); - auto changes = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); - // send(event_group_, event_atom_v, item_atom_v, changes, false); - anon_send(history_, history::log_atom_v, sysclock::now(), changes); + if (rp.pending()) + remove_items(std::distance(base_.item().begin(), it), 1, rp); - send(this, utility::event_atom_v, change_atom_v); - return std::make_pair(changes, item); + return rp; }, [=](erase_item_atom, const int index) -> result { - auto it = base_.item().children().begin(); - std::advance(it, index); - if (it == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid index"); auto rp = make_response_promise(); - rp.delegate(caf::actor_cast(this), erase_item_atom_v, it->uuid()); + erase_items(index, 1, rp); return rp; }, - [=](erase_item_atom, const utility::Uuid &uuid) -> result { + [=](erase_item_atom, const int index, const int count) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, remove_item_atom_v, uuid) - .then( - [=](const Item &item) mutable { - send_exit(item.actor(), caf::exit_reason::user_shutdown); - rp.deliver(true); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + erase_items(index, count, rp); return rp; }, + [=](erase_item_atom, const utility::Uuid &uuid) -> result { + auto rp = make_response_promise(); + + auto it = find_uuid(base_.item().children(), uuid); + + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + + if (rp.pending()) + erase_items(std::distance(base_.item().begin(), it), 1, rp); + + return rp; + }, // emulate subset [=](playlist::sort_alphabetically_atom) { sort_alphabetically(); }, + [=](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 { + // 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()) @@ -1046,6 +1208,41 @@ void TimelineActor::init() { return rp; }, + [=](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) + .then( + [=](caf::actor actor) mutable { + rp.delegate( + caf::actor_cast(this), + playlist::add_media_atom_v, + 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)); + rp.deliver(false); + }); + + return rp; + }, + [=](playlist::add_media_atom, const Uuid &uuid, const caf::actor &actor, @@ -1100,14 +1297,17 @@ void TimelineActor::init() { 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"); + fmt::format( + "playlist::get_next_media_atom called with uuid that is not in " + "timeline {}", + to_string(after_this_uuid))); } while (skip_by--) { i++; @@ -1116,8 +1316,8 @@ void TimelineActor::init() { break; } } - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); + if (media_actors_.count(*i)) + return UuidActor(*i, media_actors_[*i]); } else { auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); @@ -1125,8 +1325,10 @@ void TimelineActor::init() { // not found! return make_error( xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "playlist"); + fmt::format( + "playlist::get_next_media_atom called with uuid that is not in " + "playlist", + to_string(after_this_uuid))); } while (skip_by++) { i++; @@ -1136,27 +1338,46 @@ void TimelineActor::init() { } } - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); + if (media_actors_.count(*i)) + return UuidActor(*i, media_actors_[*i]); } return make_error( xstudio_error::error, - "playlist::get_next_media_atom called with uuid for which no media actor " - "exists"); + fmt::format( + "playlist::get_next_media_atom called with uuid for which no media actor " + "exists {}", + to_string(after_this_uuid))); }, [=](playlist::create_playhead_atom) -> UuidActor { if (playhead_) return playhead_; - auto uuid = utility::Uuid::generate(); - auto actor = spawn( - std::string("Timeline Playhead"), selection_actor_, uuid); - link_to(actor); - anon_send(actor, playhead::playhead_rate_atom_v, base_.rate()); + auto uuid = utility::Uuid::generate(); + + /*auto actor = spawn( + std::string("Timeline Playhead"), selection_actor_, uuid);*/ + + // N.B. for now we're not using the 'selection_actor_' as this + // feeds 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); - playhead_ = UuidActor(uuid, actor); + link_to(playhead_actor); + + anon_send(playhead_actor, playhead::playhead_rate_atom_v, base_.rate()); + + // now make this timeline the (only) source for the playhead + anon_send( + playhead_actor, + playhead::source_atom_v, + std::vector({caf::actor_cast(this)})); + + playhead_ = UuidActor(uuid, playhead_actor); return playhead_; }, @@ -1393,6 +1614,18 @@ void TimelineActor::init() { // 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 @@ -1413,94 +1646,171 @@ void TimelineActor::init() { // }, [=](duplicate_atom) -> result { - // clone ourself.. - auto uuid = Uuid::generate(); - auto actor = spawn( - base_.name(), uuid, caf::actor_cast(playlist_)); + auto rp = make_response_promise(); - // anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); - // get uuid from actor.. try { + // clone ourself.. caf::scoped_actor sys(system()); - // 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, media_actors_[i])); + JsonStore jsn; + auto dup = base_.duplicate(); + dup.item().clear(); - request_receive( - *sys, actor, playlist::add_media_atom_v, media_actors, Uuid(), true); + jsn["base"] = dup.serialise(); + jsn["actors"] = {}; + auto actor = spawn(jsn, caf::actor()); - return UuidActor(uuid, actor); + auto hactor = request_receive(*sys, actor, history::history_atom_v); + anon_send(hactor.actor(), plugin_manager::enable_atom_v, false); + + 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})); + } + + // enable history + anon_send(hactor.actor(), plugin_manager::enable_atom_v, true); + + rp.deliver(UuidActor(dup.uuid(), actor)); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); } - return make_error(xstudio_error::error, "Invalid uuid"); + return rp; }, + [=](timeline::focus_atom) -> UuidVector { + auto tmp = base_.focus_list(); + return UuidVector(tmp.begin(), tmp.end()); + }, + + [=](timeline::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); + }, [=](playhead::source_atom) -> caf::actor { return caf::actor_cast(playlist_); }, // set source (playlist), triggers relinking - [=](playhead::source_atom, caf::actor playlist, const UuidUuidMap &swap) -> bool { - for (const auto &i : base_.media()) { - if (media_actors_.count(i)) - demonitor(media_actors_[i]); - media_actors_.erase(i); - } + [=](playhead::source_atom, + caf::actor playlist, + const UuidUuidMap &swap) -> result { + auto rp = make_response_promise(); - // for (const auto &i : actors_) - // demonitor(i.second); - // actors_.clear(); + for (const auto &i : media_actors_) + demonitor(i.second); + media_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; + request(playlist, infinite, playlist::get_media_atom_v) + .then( + [=](const std::vector &media) mutable { + // 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); - } - media_actors_[ii] = amap[ii]; - monitor(amap[ii]); - } - } catch (const std::exception &e) { - spdlog::error("Failed to init Subset {}", e.what()); - base_.clear(); - } - base_.send_changed(event_group_, this); - return true; + // link + for (const auto &i : base_.media()) { + auto ii = (swap.count(i) ? swap.at(i) : i); + if (ii != i) { + base_.swap_media(i, ii); + } + media_actors_[ii] = amap[ii]; + monitor(amap[ii]); + } + base_.send_changed(event_group_, this); + rp.deliver(true); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + + return rp; }, [=](duration_atom, const int) {}, + [=](media::get_media_pointers_atom atom, + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate) -> caf::result { + // This is required by SubPlayhead actor to make the timeline + // playable. + + auto rp = make_response_promise(); + + if (!base_.item().available_range()) { + rp.deliver(media::FrameTimeMap()); + return rp; + } + + // 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) + .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); + }, + [=](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, @@ -1525,7 +1835,9 @@ void TimelineActor::init() { 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); + FrameRate(i * override_rate.to_flicks()), + media_type, + base_.focus_list()); if (ii) { item_tp.emplace_back(*ii); (*count)++; @@ -1708,11 +2020,20 @@ 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 }, [=](playhead::select_next_media_atom, const int skip_by) {}, @@ -1725,9 +2046,8 @@ void TimelineActor::init() { [=](playlist::select_media_atom, utility::Uuid media_uuid) {}, - [=](playhead::get_selected_sources_atom) -> std::vector { - std::vector result; - return result; + [=](playhead::get_selected_sources_atom) -> utility::UuidActorVector { + return utility::UuidActorVector(); }, [=](session::get_playlist_atom) -> caf::actor { @@ -1736,15 +2056,16 @@ void TimelineActor::init() { [=](session::import_atom, const std::string &data) -> result { auto rp = make_response_promise(); - // purge timeline.. ? + // purge timeline.. ? +#ifdef BUILD_OTIO spawn( timeline_importer, rp, caf::actor_cast(playlist_), UuidActor(base_.uuid(), actor_cast(this)), data); - +#endif return rp; }); } @@ -1945,3 +2266,116 @@ void TimelineActor::sort_alphabetically() { }); } } + +void TimelineActor::insert_items( + const int index, + const UuidActorVector &uav, + caf::typed_response_promise rp) { + // validate items can be inserted. + fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) + .then( + [=](std::vector items) mutable { + // items are valid for insertion ? + for (const auto &i : items) { + if (not base_.item().valid_child(i)) + return rp.deliver( + make_error(xstudio_error::error, "Invalid child type")); + } + + // take ownership + for (const auto &ua : uav) + add_item(ua); + + // find insertion point.. + auto it = std::next(base_.item().begin(), index); + + // insert items.. + // our list will be out of order.. + auto changes = JsonStore(R"([])"_json); + for (const auto &ua : uav) { + // find item.. + auto found = false; + for (const auto &i : items) { + if (ua.uuid() == i.uuid()) { + auto tmp = base_.item().insert(it, i); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + found = true; + break; + } + } + + if (not found) { + spdlog::error("item not found for insertion"); + } + } + + // add changes to stack + auto 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); + anon_send(history_, history::log_atom_v, sysclock::now(), changes); + send(this, utility::event_atom_v, change_atom_v); + + 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::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")); + else { + scoped_actor sys{system()}; + + 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()); + 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); + } + } + + auto more = base_.item().refresh(); + if (not more.is_null()) + changes.insert(changes.begin(), more.begin(), more.end()); + + // why was this commented out ? + // 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); + + rp.deliver(std::make_pair(changes, items)); + } +} + +void TimelineActor::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)); }); +} diff --git a/src/timeline/src/track.cpp b/src/timeline/src/track.cpp index a9260638c..9cd5f332a 100644 --- a/src/timeline/src/track.cpp +++ b/src/timeline/src/track.cpp @@ -21,7 +21,9 @@ Track::Track( item_( media_type == MediaType::MT_IMAGE ? ItemType::IT_VIDEO_TRACK : ItemType::IT_AUDIO_TRACK, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) {} + utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + item_.set_name(name); +} Track::Track(const JsonStore &jsn) : Container(static_cast(jsn.at("container"))), @@ -29,6 +31,20 @@ Track::Track(const JsonStore &jsn) media_type_ = jsn.at("media_type"); } +Track Track::duplicate() const { + utility::JsonStore jsn; + + auto dup_container = Container::duplicate(); + auto dup_item = item_; + dup_item.set_uuid(dup_container.uuid()); + + jsn["container"] = dup_container.serialise(); + jsn["media_type"] = media_type_; + jsn["item"] = dup_item.serialise(1); + + return Track(jsn); +} + JsonStore Track::serialise() const { JsonStore jsn; diff --git a/src/timeline/src/track_actor.cpp b/src/timeline/src/track_actor.cpp index 5dc9d3e7f..c4e8968f1 100644 --- a/src/timeline/src/track_actor.cpp +++ b/src/timeline/src/track_actor.cpp @@ -61,11 +61,17 @@ void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item switch (static_cast(event.at("action"))) { case IT_INSERT: { + // spdlog::warn("TrackActor IT_INSERT {}", event.dump(2)); auto cuuid = utility::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.. + + // is it a direct child.. auto child_item_it = find_uuid(base_.item().children(), cuuid); + // 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 not event.at("blind").is_null()) { // our child @@ -77,7 +83,8 @@ void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item // spdlog::warn("{}",to_string(caf::actor_cast(child_item_it->actor()))); child_item_it->set_actor_addr(actor); // change item actor addr - // spdlog::warn("{}",to_string(caf::actor_cast(child_item_it->actor()))); + // spdlog::warn("TrackActor create + // {}",to_string(caf::actor_cast(child_item_it->actor()))); // item actor_addr will be wrong.. in ancestors // send special update.. @@ -91,6 +98,8 @@ void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item } break; case IT_REMOVE: { + // spdlog::warn("TrackActor IT_REMOVE {} {}", base_.item().name(), event.dump(2)); + auto cuuid = utility::Uuid(event.at("item_uuid")); // child destroyed if (actors_.count(cuuid)) { @@ -113,6 +122,27 @@ void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item } } +TrackActor::TrackActor(caf::actor_config &cfg, const utility::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()) { + try { + deserialise(value, true); + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } + + base_.item().set_system(&system()); + base_.item().bind_item_event_func([this](const utility::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"))) { base_.item().set_actor_addr(this); @@ -167,6 +197,8 @@ void TrackActor::init() { // 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); @@ -223,6 +255,13 @@ void TrackActor::init() { [=](item_atom) -> Item { return base_.item(); }, + [=](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); + return jsn; + }, + [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) @@ -263,6 +302,17 @@ void TrackActor::init() { send(event_group_, event_atom_v, item_atom_v, jsn, false); return jsn; }, + + [=](active_range_atom) -> std::optional { + return base_.item().active_range(); + }, + + [=](available_range_atom) -> std::optional { + return base_.item().available_range(); + }, + + [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + // handle child change events. [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { if (base_.item().update(update)) { @@ -309,211 +359,168 @@ void TrackActor::init() { return rp; }, - // // 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(event_group_, event_atom_v, item_atom_v, base_.item()); - // } - // }, - - - [=](insert_item_atom, const int index, const UuidActor &ua) -> result { + [=](insert_item_at_frame_atom, + const int frame, + const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .then( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - index, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + insert_items_at_frame(frame, uav, rp); + return rp; + }, + [=](insert_item_atom, + const int index, + const UuidActorVector &uav) -> result { + auto rp = make_response_promise(); + insert_items(index, uav, rp); return rp; }, - // we only allow access to direct children.. ? - [=](insert_item_atom, const int index, const UuidActor &ua, const Item &item) + [=](insert_item_atom, const int index, const int frame, const UuidActorVector &uav) -> result { - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); - } - // take ownership and join events - add_item(ua); + auto rp = make_response_promise(); + auto tframe = base_.item().frame_at_index(index, frame); + insert_items_at_frame(tframe, uav, rp); + return rp; + }, + [=](insert_item_atom, + const utility::Uuid &before_uuid, + const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v, true) - .await( - [=](const std::pair &jitem) mutable { - // insert on index.. - // cheat.. - auto it = base_.item().begin(); - auto ind = 0; - for (int i = 0; it != base_.item().end(); i++, it++) { - if (i == index) - break; - } - auto changes = base_.item().insert(it, jitem.second, jitem.first); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + auto index = base_.item().size(); + // find index. for uuid + if (not before_uuid.is_null()) { + auto it = find_uuid(base_.item().children(), before_uuid); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + else + index = std::distance(base_.item().begin(), it); + } + + if (rp.pending()) + insert_items(index, uav, rp); - send(event_group_, event_atom_v, item_atom_v, changes, false); - rp.deliver(changes); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, - [=](insert_item_atom, - const utility::Uuid &before_uuid, - const UuidActor &ua) -> result { - auto rp = make_response_promise(); - // get item.. - request(ua.actor(), infinite, item_atom_v) - .await( - [=](const Item &item) mutable { - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, - before_uuid, - ua, - item); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + [=](remove_item_at_frame_atom, + const int frame, + const int duration) -> result>> { + auto rp = make_response_promise>>(); + remove_items_at_frame(frame, duration, rp); + return rp; + }, + [=](remove_item_atom, + const int index) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, 1, rp); return rp; }, - [=](insert_item_atom, - const utility::Uuid &before_uuid, - const UuidActor &ua, - const Item &item) -> result { - if (not base_.item().valid_child(item)) { - return make_error(xstudio_error::error, "Invalid child type"); - } - // take ownership and join events - add_item(ua); + [=](remove_item_atom, + const int index, + const int count) -> result>> { + auto rp = make_response_promise>>(); + remove_items(index, count, rp); + return rp; + }, - auto rp = make_response_promise(); - // re-aquire item. as we may have gone out of sync.. - request(ua.actor(), infinite, item_atom_v, true) - .await( - [=](const std::pair &jitem) mutable { - auto changes = utility::JsonStore(); - if (before_uuid.is_null()) { - changes = base_.item().insert( - base_.item().end(), jitem.second, jitem.first); - } else { - auto it = find_uuid(base_.item().children(), before_uuid); - if (it == base_.item().end()) { - return rp.deliver( - make_error(xstudio_error::error, "Invalid uuid")); - } - changes = base_.item().insert(it, jitem.second, jitem.first); - } + [=](remove_item_atom, + const utility::Uuid &uuid) -> result>> { + auto rp = make_response_promise>>(); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + auto it = find_uuid(base_.item().children(), uuid); - send(event_group_, event_atom_v, item_atom_v, changes, false); + if (it == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + + if (rp.pending()) + remove_items(std::distance(base_.item().begin(), it), 1, rp); - rp.deliver(changes); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, - [=](remove_item_atom, const int index) -> result> { - auto it = base_.item().children().begin(); - std::advance(it, index); - if (it == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid index"); - auto rp = make_response_promise>(); - rp.delegate(caf::actor_cast(this), remove_item_atom_v, it->uuid()); + [=](erase_item_at_frame_atom, + const int frame, + const int duration) -> result { + auto rp = make_response_promise(); + erase_items_at_frame(frame, duration, rp); + return rp; + }, + + [=](erase_item_atom, const int index) -> result { + auto rp = make_response_promise(); + erase_items(index, 1, rp); + return rp; + }, + + [=](erase_item_atom, const int index, const int count) -> result { + auto rp = make_response_promise(); + erase_items(index, count, rp); return rp; }, - [=](remove_item_atom, const utility::Uuid &uuid) -> result> { + [=](erase_item_atom, const utility::Uuid &uuid) -> result { + auto rp = make_response_promise(); + auto it = find_uuid(base_.item().children(), uuid); + if (it == base_.item().end()) - return make_error(xstudio_error::error, "Invalid uuid"); - auto item = *it; - demonitor(item.actor()); - actors_.erase(item.uuid()); + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); - auto changes = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + if (rp.pending()) + erase_items(std::distance(base_.item().begin(), it), 1, rp); - send(event_group_, event_atom_v, item_atom_v, changes, false); + return rp; + }, - return std::make_pair(changes, item); + [=](split_item_at_frame_atom, const int frame) -> result { + auto rp = make_response_promise(); + auto split_point = base_.item().item_at_frame(frame); + if (not split_point) + rp.deliver(make_error(xstudio_error::error, "Invalid split frame")); + else + split_item(split_point->first, split_point->second, rp); + + return rp; }, - [=](erase_item_atom, const int index) -> result { + [=](split_item_atom, const int index, const int frame) -> result { auto it = base_.item().children().begin(); std::advance(it, index); if (it == base_.item().children().end()) return make_error(xstudio_error::error, "Invalid index"); auto rp = make_response_promise(); - rp.delegate(caf::actor_cast(this), erase_item_atom_v, it->uuid()); + split_item(it, frame, rp); return rp; }, - [=](erase_item_atom, const utility::Uuid &uuid) -> result { + [=](split_item_atom, const utility::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(); - request(caf::actor_cast(this), infinite, remove_item_atom_v, uuid) - .then( - [=](std::pair &hist_item) mutable { - send_exit(hist_item.second.actor(), caf::exit_reason::user_shutdown); - rp.deliver(hist_item.first); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + split_item(it, frame, rp); + return rp; + }, + + [=](move_item_at_frame_atom, + const int frame, + const int duration, + const int dest_frame, + const bool insert) -> result { + auto rp = make_response_promise(); + move_items_at_frame(frame, duration, dest_frame, insert, rp); return rp; }, [=](move_item_atom, const int src_index, const int count, const int dst_index) -> result { - auto sit = base_.item().children().begin(); - std::advance(sit, src_index); - - if (sit == base_.item().children().end()) - return make_error(xstudio_error::error, "Invalid src index"); - - auto src_uuid = sit->uuid(); - // dst index is the index it should be after the move. - // we need to account for the items we're moving.. - auto dit = base_.item().children().begin(); - - if (dst_index == src_index) - return make_error(xstudio_error::error, "Invalid Move"); - - auto adj_dst = dst_index; - - if (dst_index > src_index) - adj_dst += count; - - // spdlog::warn("{} {} {} -> {}", src_index, count, dst_index, adj_dst); - - std::advance(dit, adj_dst); - auto dst_uuid = utility::Uuid(); - if (dit != base_.item().children().end()) - dst_uuid = dit->uuid(); - auto rp = make_response_promise(); - rp.delegate( - caf::actor_cast(this), move_item_atom_v, src_uuid, count, dst_uuid); + move_items(src_index, count, dst_index, rp); return rp; }, @@ -522,30 +529,29 @@ void TrackActor::init() { const int count, const utility::Uuid &before_uuid) -> result { // check src is valid. + auto rp = make_response_promise(); + auto sitb = find_uuid(base_.item().children(), src_uuid); if (sitb == base_.item().end()) - return make_error(xstudio_error::error, "Invalid src uuid"); - - auto dit = base_.item().children().end(); - if (not before_uuid.is_null()) { - dit = find_uuid(base_.item().children(), before_uuid); - if (dit == base_.item().end()) - return make_error(xstudio_error::error, "Invalid dst uuid"); - } + rp.deliver(make_error(xstudio_error::error, "Invalid src uuid")); - if (count) { - auto site = sitb; - std::advance(site, count); - auto changes = base_.item().splice(dit, base_.item().children(), sitb, site); - auto 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); - return changes; + if (rp.pending()) { + auto dit = base_.item().children().end(); + if (not before_uuid.is_null()) { + dit = find_uuid(base_.item().children(), before_uuid); + if (dit == base_.item().end()) + rp.deliver(make_error(xstudio_error::error, "Invalid dst uuid")); + } + if (rp.pending()) + move_items( + std::distance(base_.item().begin(), sitb), + count, + std::distance(base_.item().begin(), dit), + rp); } - return JsonStore(); + return rp; }, [=](utility::event_atom, utility::change_atom) { @@ -555,6 +561,34 @@ void TrackActor::init() { [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, + [=](utility::duplicate_atom) -> result { + auto rp = make_response_promise(); + JsonStore jsn; + auto dup = base_.duplicate(); + dup.item().clear(); + + jsn["base"] = dup.serialise(); + jsn["actors"] = {}; + auto actor = spawn(jsn); + + if (actors_.empty()) { + rp.deliver(UuidActor(dup.uuid(), actor)); + } else { + // duplicate all children and relink against items. + scoped_actor sys{system()}; + + for (const auto &i : base_.children()) { + auto ua = request_receive( + *sys, actors_[i.uuid()], utility::duplicate_atom_v); + request_receive( + *sys, actor, insert_item_atom_v, -1, UuidActorVector({ua})); + } + rp.deliver(UuidActor(dup.uuid(), actor)); + } + + return rp; + }, + [=](utility::serialise_atom) -> result { if (not actors_.empty()) { auto rp = make_response_promise(); @@ -683,6 +717,500 @@ void TrackActor::add_item(const utility::UuidActor &ua) { actors_[ua.uuid()] = ua.actor(); } + +void TrackActor::split_item( + const Items::const_iterator &itemit, + const int frame, + caf::typed_response_promise rp) { + // validate frame is inside item.. + // validate item type.. + auto item = *itemit; + if (item.item_type() == IT_GAP or item.item_type() == IT_CLIP) { + auto trimmed_range = item.trimmed_range(); + auto orig_start = trimmed_range.frame_start().frames(); + auto orig_duration = trimmed_range.frame_duration().frames(); + auto orig_end = orig_start + orig_duration - 1; + + if (frame > orig_start and frame < orig_end) { + // duplicate item to split. + request(item.actor(), infinite, utility::duplicate_atom_v) + .await( + [=](const UuidActor &ua) mutable { + // adjust start frames if clip. + auto item1_range = trimmed_range; + auto item2_range = trimmed_range; + + item1_range.set_duration( + (frame - item1_range.frame_start().frames()) * item1_range.rate()); + + if (item.item_type() != IT_GAP) + item2_range.set_start(FrameRate(item2_range.rate() * frame)); + item2_range.set_duration(FrameRate( + item2_range.rate() * + (orig_duration - item1_range.frame_duration().frames()))); + + // set range on new item + request( + ua.actor(), infinite, timeline::active_range_atom_v, item2_range) + .await( + [=](const JsonStore &) mutable { + // set range on old item + request( + item.actor(), + infinite, + timeline::active_range_atom_v, + item1_range) + .await( + [=](const JsonStore &) mutable { + // insert next to original. + rp.delegate( + caf::actor_cast(this), + insert_item_atom_v, + static_cast( + std::distance( + base_.item().cbegin(), itemit) + + 1), + UuidActorVector({ua})); + }, + [=](error &err) mutable { + rp.deliver(std::move(err)); + }); + // insert next to original. + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid frame to split on")); + } + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid type to split")); + } +} + +void TrackActor::insert_items( + const int index, + const UuidActorVector &uav, + caf::typed_response_promise rp) { + // validate items can be inserted. + fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) + .then( + [=](std::vector items) mutable { + // items are valid for insertion ? + for (const auto &i : items) { + if (not base_.item().valid_child(i)) + return rp.deliver( + make_error(xstudio_error::error, "Invalid child type")); + } + + scoped_actor sys{system()}; + + // take ownership + for (const auto &ua : uav) + add_item(ua); + + // find insertion point.. + auto it = std::next(base_.item().begin(), index); + + // insert items.. + // our list will be out of order.. + auto changes = JsonStore(R"([])"_json); + for (const auto &ua : uav) { + // find item.. + auto found = false; + for (const auto &i : items) { + if (ua.uuid() == i.uuid()) { + // we need to serialise item so undo redo can remove recreate it. + auto blind = + request_receive(*sys, ua.actor(), serialise_atom_v); + + auto tmp = base_.item().insert(it, i, blind); + changes.insert(changes.begin(), tmp.begin(), tmp.end()); + found = true; + break; + } + } + + if (not found) { + spdlog::error("item not found for insertion"); + } + } + + // add changes to stack + auto 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); + + rp.deliver(changes); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); +} + +void TrackActor::insert_items_at_frame( + const int frame, + const utility::UuidActorVector &uav, + caf::typed_response_promise rp) { + auto item_frame = base_.item().item_at_frame(frame); + + if (not item_frame) { + // off the end of the track.. + // insert gap to fill space.. + UuidActorVector uav_plus_gap; + 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_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); + } else { + auto cit = item_frame->first; + auto cframe = item_frame->second; + auto index = std::distance(base_.item().cbegin(), cit); + + if (cframe == cit->trimmed_frame_start().frames()) { + // simple insertion.. + insert_items(index, uav, rp); + } else { + // complex.. we need to split item + request( + caf::actor_cast(this), + infinite, + split_item_atom_v, + static_cast(index), + static_cast(cframe)) + .then( + [=](const JsonStore &) { insert_items_at_frame(frame, uav, rp); }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + } + } +} + +// find in / out points and split if inside clips +// 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( + const int frame, + const int duration, + caf::typed_response_promise>> + rp) { + + auto in_point = base_.item().item_at_frame(frame); + + 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, + split_item_atom_v, + static_cast(std::distance(base_.item().cbegin(), in_point->first)), + static_cast(in_point->second)) + .then( + [=](const JsonStore &) { remove_items_at_frame(frame, duration, rp); }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + + } else { + // split end point... + auto out_point = base_.item().item_at_frame(frame + duration); + if (out_point->second != out_point->first->trimmed_frame_start().frames()) { + // split at in point, and recall function. + request( + caf::actor_cast(this), + infinite, + split_item_atom_v, + static_cast(std::distance(base_.item().cbegin(), out_point->first)), + static_cast(out_point->second)) + .then( + [=](const JsonStore &) { remove_items_at_frame(frame, duration, rp); }, + [=](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); + } + } + } +} + +void TrackActor::remove_items( + const int index, + const int count, + caf::typed_response_promise>> + rp) { + + 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")); + else { + scoped_actor sys{system()}; + 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()); + 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.begin(), tmp.begin(), tmp.end()); + items.push_back(item); + } + } + + // reverse order as we deleted back to front. + std::reverse(items.begin(), items.end()); + + auto 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); + + rp.deliver(std::make_pair(changes, items)); + } +} + +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) + .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)); }); +} + +void TrackActor::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)); }); +} + + +void TrackActor::move_items( + const int src_index, + const int count, + const int dst_index, + caf::typed_response_promise rp) { + + + if (dst_index == src_index or not count) + rp.deliver(make_error(xstudio_error::error, "Invalid Move")); + else { + auto sit = std::next(base_.item().begin(), src_index); + auto eit = std::next(sit, count); + auto dit = std::next(base_.item().begin(), dst_index); + + auto changes = base_.item().splice(dit, base_.item().children(), sit, eit); + auto 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); + 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) { + + // 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")); + 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, + split_item_atom_v, + static_cast(std::distance(base_.item().cbegin(), start->first)), + start->second) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame(frame, duration, dest_frame, insert, rp); + }, + [=](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, + split_item_atom_v, + static_cast(std::distance(base_.item().cbegin(), end->first)), + 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 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)); }); + } 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); + } 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)); + }); + } + } + } + } + } + } + } +} + + // void TrackActor::deliver_media_pointer( // const int logical_frame, caf::typed_response_promise rp) { // // should be able to use edit_list ? diff --git a/src/timeline/test/stack_actor_test.cpp b/src/timeline/test/stack_actor_test.cpp index 91e674995..595c82377 100644 --- a/src/timeline/test/stack_actor_test.cpp +++ b/src/timeline/test/stack_actor_test.cpp @@ -34,23 +34,23 @@ TEST(StackActorMoveTest, Test) { auto uuid1 = utility::Uuid::generate(); valid = f.self->spawn("Gap1", utility::FrameRateDuration(), uuid1); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid1, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid1, valid)}))); auto uuid2 = utility::Uuid::generate(); valid = f.self->spawn("Gap2", utility::FrameRateDuration(), uuid2); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid2, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid2, valid)}))); auto uuid3 = utility::Uuid::generate(); valid = f.self->spawn("Gap3", utility::FrameRateDuration(), uuid3); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid3, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid3, valid)}))); auto uuid4 = utility::Uuid::generate(); valid = f.self->spawn("Gap4", utility::FrameRateDuration(), uuid4); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid4, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid4, valid)}))); auto uuid5 = utility::Uuid::generate(); valid = f.self->spawn("Gap5", utility::FrameRateDuration(), uuid5); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid5, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid5, valid)}))); auto item = request_receive(*(f.self), t, item_atom_v); @@ -92,8 +92,8 @@ TEST(StackActorTest, Test) { auto guuid = Uuid::generate(); auto g = f.self->spawn("Gap1", FrameRateDuration(10, timebase::k_flicks_24fps), guuid); - auto result = - request_receive(*(f.self), s, insert_item_atom_v, 0, UuidActor(guuid, g)); + auto result = request_receive( + *(f.self), s, insert_item_atom_v, 0, UuidActorVector({UuidActor(guuid, g)})); // stack duration should have changed. item = request_receive(*(f.self), s, item_atom_v); @@ -133,7 +133,7 @@ TEST(NestedStackActorTest, Test) { auto c = f.self->spawn("ChildStack", cuuid); { auto result = request_receive( - *(f.self), s, insert_item_atom_v, -1, UuidActor(cuuid, c)); + *(f.self), s, insert_item_atom_v, -1, UuidActorVector({UuidActor(cuuid, c)})); } std::this_thread::sleep_for(500ms); @@ -143,21 +143,21 @@ TEST(NestedStackActorTest, Test) { "Gap1", FrameRateDuration(10, timebase::k_flicks_24fps), guuid1); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid1, g1)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid1, g1)})); } auto guuid2 = Uuid::generate(); auto g2 = f.self->spawn( "Gap2", FrameRateDuration(20, timebase::k_flicks_24fps), guuid2); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid2, g2)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid2, g2)})); } auto guuid3 = Uuid::generate(); auto g3 = f.self->spawn( "Gap3", FrameRateDuration(30, timebase::k_flicks_24fps), guuid3); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid3, g3)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid3, g3)})); } // Stack[ChildStack[Gap1,Gap2,Gap3]] @@ -198,7 +198,7 @@ TEST(NestedTrackStackActorTest, Test) { auto c = f.self->spawn("ChildTrack", media::MediaType::MT_IMAGE, cuuid); { auto result = request_receive( - *(f.self), s, insert_item_atom_v, -1, UuidActor(cuuid, c)); + *(f.self), s, insert_item_atom_v, -1, UuidActorVector({UuidActor(cuuid, c)})); } auto guuid1 = Uuid::generate(); @@ -206,21 +206,21 @@ TEST(NestedTrackStackActorTest, Test) { "Gap1", FrameRateDuration(10, timebase::k_flicks_24fps), guuid1); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid1, g1)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid1, g1)})); } auto guuid2 = Uuid::generate(); auto g2 = f.self->spawn( "Gap2", FrameRateDuration(20, timebase::k_flicks_24fps), guuid2); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid2, g2)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid2, g2)})); } auto guuid3 = Uuid::generate(); auto g3 = f.self->spawn( "Gap3", FrameRateDuration(30, timebase::k_flicks_24fps), guuid3); { auto result = request_receive( - *(f.self), c, insert_item_atom_v, -1, UuidActor(guuid3, g3)); + *(f.self), c, insert_item_atom_v, -1, UuidActorVector({UuidActor(guuid3, g3)})); } // Stack[ChildStack[Gap1,Gap2,Gap3]] @@ -261,7 +261,11 @@ TEST(StackActorAddTest, Test) { auto invalid = f.self->spawn("Invalid Timeline", uuid); EXPECT_THROW( request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, invalid)), + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(uuid, invalid)})), std::runtime_error); f.self->send_exit(invalid, caf::exit_reason::user_shutdown); } @@ -269,36 +273,105 @@ TEST(StackActorAddTest, Test) { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn("Valid Track", media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(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); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn(UuidActor(), "Valid Clip", uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn(UuidActor(), "Valid Clip", uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, -1, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn("Valid Gap", utility::FrameRateDuration(), uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } f.self->send_exit(t, caf::exit_reason::user_shutdown); } + +// test +TEST(StackActorMoveTest2, Test) { + fixture f; + // start_logger(spdlog::level::debug); + auto t = f.self->spawn("Test Stack"); + + { + auto uuid = utility::Uuid::generate(); + auto valid = f.self->spawn("Track 4", 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); + 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); + 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); + EXPECT_NO_THROW(request_receive( + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); + } + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 1"); + request_receive(*(f.self), t, move_item_atom_v, 0, 1, 1); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 2"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 1"); + + request_receive(*(f.self), t, move_item_atom_v, 1, 1, 0); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 1"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 2"); + + request_receive(*(f.self), t, move_item_atom_v, 0, 2, 1); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 3"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 1"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 2).name(), "Track 2"); + + request_receive(*(f.self), t, move_item_atom_v, 1, 2, 0); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 1"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 2"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 2).name(), "Track 3"); + + auto hist = request_receive(*(f.self), t, move_item_atom_v, 0, 2, 1); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 3"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 1"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 2).name(), "Track 2"); + + request_receive(*(f.self), t, history::undo_atom_v, hist); + + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 0).name(), "Track 1"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 1).name(), "Track 2"); + EXPECT_EQ(request_receive(*(f.self), t, item_atom_v, 2).name(), "Track 3"); + + f.self->send_exit(t, caf::exit_reason::user_shutdown); +} diff --git a/src/timeline/test/stack_test.cpp b/src/timeline/test/stack_test.cpp index 639d1d3aa..3459dbdb9 100644 --- a/src/timeline/test/stack_test.cpp +++ b/src/timeline/test/stack_test.cpp @@ -62,19 +62,63 @@ TEST(StackTest, Test) { it++; EXPECT_EQ(it->uuid(), g2.item().uuid()); - // move first entry to end - auto it2 = s.item().cbegin(); - it2++; - auto ru5 = s.item().splice(s.item().cend(), s.item().children(), s.item().cbegin(), it2); + auto start = std::next(s.item().begin(), 0); + auto end = std::next(s.item().begin(), 1); + auto pos = std::next(s.item().begin(), 3); + + auto ru5 = s.item().splice(pos, s.item().children(), start, end); + + it = s.item().begin(); + EXPECT_EQ(it->uuid(), g2.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g3.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g1.item().uuid()); + + s.item().undo(ru5); it = s.item().begin(); + EXPECT_EQ(it->uuid(), g1.item().uuid()); + it++; EXPECT_EQ(it->uuid(), g2.item().uuid()); it++; EXPECT_EQ(it->uuid(), g3.item().uuid()); + + // move / undo + start = std::next(s.item().begin(), 0); + end = std::next(s.item().begin(), 2); + pos = std::next(s.item().begin(), 3); + + ru5 = s.item().splice(pos, s.item().children(), start, end); + + it = s.item().begin(); + EXPECT_EQ(it->uuid(), g3.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g1.item().uuid()); it++; + EXPECT_EQ(it->uuid(), g2.item().uuid()); + + s.item().undo(ru5); + it = s.item().begin(); EXPECT_EQ(it->uuid(), g1.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g2.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g3.item().uuid()); - // spdlog::warn("{}", ru5.dump(2)); + // move / undo + start = std::next(s.item().begin(), 0); + end = std::next(s.item().begin(), 2); + pos = std::next(s.item().begin(), 3); + + ru5 = s.item().splice(pos, s.item().children(), start, end); + + it = s.item().begin(); + EXPECT_EQ(it->uuid(), g3.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g1.item().uuid()); + it++; + EXPECT_EQ(it->uuid(), g2.item().uuid()); s.item().undo(ru5); it = s.item().begin(); @@ -84,6 +128,7 @@ TEST(StackTest, Test) { it++; EXPECT_EQ(it->uuid(), g3.item().uuid()); + // do nested changes work ? auto ru6 = s.item().children().front().set_enabled(false); EXPECT_FALSE(s.item().children().front().enabled()); diff --git a/src/timeline/test/timeline_actor_test.cpp b/src/timeline/test/timeline_actor_test.cpp index cd2065ad1..7c763d7c5 100644 --- a/src/timeline/test/timeline_actor_test.cpp +++ b/src/timeline/test/timeline_actor_test.cpp @@ -47,34 +47,27 @@ TEST(TimelineActorSerialiseTest, Test) { f.self->send_exit(t2, caf::exit_reason::user_shutdown); } -// Mirror layout of timeline test.. -TEST(TimelineActorChildTest, Test) { + +TEST(TimelineActorHistoryTest, Test) { fixture f; - // start_logger(); - - auto uuid = utility::Uuid(); - auto t = f.self->spawn(); + start_logger(spdlog::level::debug); + auto uuid = utility::Uuid(); + auto t = f.self->spawn(); auto stack_item = request_receive(*(f.self), t, item_atom_v, 0); auto stack1 = stack_item.actor(); - // // add stack.. - // uuid.generate_in_place(); - // auto stack1 = f.self->spawn("Stack-001", uuid); - // auto result = request_receive(*(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, - // stack1)); - // add track to stack uuid.generate_in_place(); auto track1 = f.self->spawn("Track-001", media::MediaType::MT_IMAGE, uuid); - auto result = request_receive( - *(f.self), stack1, insert_item_atom_v, 0, UuidActor(uuid, track1)); + auto hist1 = request_receive( + *(f.self), stack1, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, track1)})); // add clip to track.. uuid.generate_in_place(); - auto clip1 = f.self->spawn(UuidActor(), "Clip-001", uuid); - result = request_receive( + auto clip1 = f.self->spawn(UuidActor(), "Clip-001", uuid); + auto result = request_receive( *(f.self), clip1, active_range_atom_v, @@ -82,13 +75,13 @@ TEST(TimelineActorChildTest, Test) { FrameRateDuration(3, timebase::k_flicks_24fps), FrameRateDuration(3, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, clip1)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip1)})); // stack uuid.generate_in_place(); auto stack2 = f.self->spawn("Nested Stack-002", uuid); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, stack2)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, stack2)})); result = request_receive( *(f.self), stack2, @@ -102,7 +95,7 @@ TEST(TimelineActorChildTest, Test) { auto gap1 = f.self->spawn( "Gap-001", FrameRateDuration(4, timebase::k_flicks_24fps), uuid); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, gap1)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, gap1)})); // clip 4 uuid.generate_in_place(); @@ -115,20 +108,20 @@ TEST(TimelineActorChildTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(6, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, clip4)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip4)})); // now populate nested stack uuid.generate_in_place(); auto track2 = f.self->spawn("Nested Track-002", media::MediaType::MT_IMAGE, uuid); result = request_receive( - *(f.self), stack2, insert_item_atom_v, -1, UuidActor(uuid, track2)); + *(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); result = request_receive( - *(f.self), stack2, insert_item_atom_v, -1, UuidActor(uuid, track3)); + *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track3)})); result = request_receive( *(f.self), track3, @@ -142,7 +135,7 @@ TEST(TimelineActorChildTest, Test) { auto gap2 = f.self->spawn( "Gap-002", FrameRateDuration(7, timebase::k_flicks_24fps), uuid); result = request_receive( - *(f.self), track2, insert_item_atom_v, -1, UuidActor(uuid, gap2)); + *(f.self), track2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, gap2)})); uuid.generate_in_place(); auto clip3 = f.self->spawn(UuidActor(), "Clip-003", uuid); @@ -154,7 +147,7 @@ TEST(TimelineActorChildTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(9, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track2, insert_item_atom_v, -1, UuidActor(uuid, clip3)); + *(f.self), track2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip3)})); // populate nested track 3 @@ -168,153 +161,165 @@ TEST(TimelineActorChildTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(9, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track3, insert_item_atom_v, -1, UuidActor(uuid, clip5)); + *(f.self), track3, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip5)})); - uuid.generate_in_place(); - auto clip6 = f.self->spawn(UuidActor(), "Clip-006", uuid); - result = request_receive( + auto clip6_uuid = Uuid::generate(); + auto clip6 = f.self->spawn(UuidActor(), "Clip-006", clip6_uuid); + result = request_receive( + *(f.self), + track3, + insert_item_atom_v, + -1, + UuidActorVector({UuidActor(clip6_uuid, clip6)})); + result = request_receive( *(f.self), clip6, active_range_atom_v, FrameRange( FrameRateDuration(3, timebase::k_flicks_24fps), FrameRateDuration(3, timebase::k_flicks_24fps))); - result = request_receive( - *(f.self), track3, insert_item_atom_v, -1, UuidActor(uuid, clip6)); + std::this_thread::sleep_for(500ms); + + { + // validate clip 6 directly + auto item = request_receive(*(f.self), clip6, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); + + auto titem = request_receive(*(f.self), t, item_atom_v); + auto sitem = find_item(titem.children(), clip6_uuid); + // validate cache in timeline + EXPECT_EQ( + (*sitem)->trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); + } // timeline should be valid.. // but might need to wait for the updates to bubble up // something not working... ordering of events ? - std::this_thread::sleep_for(500ms); - // validate trimmed ranges - auto item = request_receive(*(f.self), clip6, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - item = request_receive(*(f.self), clip5, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(100, timebase::k_flicks_24fps), - FrameRateDuration(9, timebase::k_flicks_24fps))); - - item = request_receive(*(f.self), track3, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(1, timebase::k_flicks_24fps), - FrameRateDuration(10, timebase::k_flicks_24fps))); + auto history = request_receive(*(f.self), t, history::history_atom_v); + EXPECT_EQ(request_receive(*(f.self), history.actor(), media_cache::count_atom_v), 15); - item = request_receive(*(f.self), clip3, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(100, timebase::k_flicks_24fps), - FrameRateDuration(9, timebase::k_flicks_24fps))); - item = request_receive(*(f.self), gap2, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(7, timebase::k_flicks_24fps))); + // THE BIG ONE.. + // TEST SIMPLE CHANGE UNDO/REDO + request_receive(*(f.self), t, history::undo_atom_v); + // clip duration should now be reset.. - item = request_receive(*(f.self), track2, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(16, timebase::k_flicks_24fps))); + { + // validate clip 6 directly + auto item = request_receive(*(f.self), clip6, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(0, timebase::k_flicks_24fps))); - item = request_receive(*(f.self), stack2, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(2, timebase::k_flicks_24fps), - FrameRateDuration(6, timebase::k_flicks_24fps))); + auto titem = request_receive(*(f.self), t, item_atom_v); + auto sitem = find_item(titem.children(), clip6_uuid); + // validate cache in timeline + EXPECT_EQ( + (*sitem)->trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(0, timebase::k_flicks_24fps))); + } - item = request_receive(*(f.self), clip1, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); + request_receive(*(f.self), t, history::redo_atom_v); - item = request_receive(*(f.self), gap1, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(4, timebase::k_flicks_24fps))); + { + // validate clip 6 directly + auto item = request_receive(*(f.self), clip6, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); - item = request_receive(*(f.self), clip4, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(100, timebase::k_flicks_24fps), - FrameRateDuration(6, timebase::k_flicks_24fps))); + auto titem = request_receive(*(f.self), t, item_atom_v); + auto sitem = find_item(titem.children(), clip6_uuid); + // validate cache in timeline + EXPECT_EQ( + (*sitem)->trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); + } - item = request_receive(*(f.self), track1, item_atom_v); - // spdlog::warn("{}", item.serialise().dump(2)); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(19, timebase::k_flicks_24fps))); + // TEST MORE COMPLEX CHANGE. + request_receive(*(f.self), t, history::undo_atom_v); + request_receive(*(f.self), t, history::undo_atom_v); + // undos insertion ? + // clip6 is now invalid + { + auto titem = request_receive(*(f.self), t, item_atom_v); + auto sitem = find_item(titem.children(), clip6_uuid); + EXPECT_EQ((*sitem), titem.cend()); + } - item = request_receive(*(f.self), stack1, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(19, timebase::k_flicks_24fps))); + request_receive(*(f.self), t, history::redo_atom_v); + request_receive(*(f.self), t, history::redo_atom_v); - item = request_receive(*(f.self), t, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(19, timebase::k_flicks_24fps))); + { + auto titem = request_receive(*(f.self), t, item_atom_v); + auto sitem = find_item(titem.children(), clip6_uuid); + // validate cache in timeline + EXPECT_EQ( + (*sitem)->trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); - // serialise test + // validate clip 6 directly + auto item = request_receive(*(f.self), (*sitem)->actor(), item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); + } + // FTW it actually worked.. - auto serialise = request_receive(*(f.self), t, serialise_atom_v); - auto t2 = f.self->spawn(serialise); - item = request_receive(*(f.self), t2, item_atom_v); - EXPECT_EQ( - item.trimmed_frame_duration().frames(), - FrameRateDuration(19, timebase::k_flicks_24fps).frames()); f.self->send_exit(t, caf::exit_reason::user_shutdown); - f.self->send_exit(t2, caf::exit_reason::user_shutdown); } - -TEST(TimelineActorHistoryTest, Test) { +// Mirror layout of timeline test.. +TEST(TimelineActorChildTest, Test) { fixture f; // start_logger(); - auto uuid = utility::Uuid(); - auto t = f.self->spawn(); + auto uuid = utility::Uuid(); + auto t = f.self->spawn(); + auto stack_item = request_receive(*(f.self), t, item_atom_v, 0); auto stack1 = stack_item.actor(); + // // add stack.. + // uuid.generate_in_place(); + // auto stack1 = f.self->spawn("Stack-001", uuid); + // auto result = request_receive(*(f.self), t, insert_item_atom_v, 0, + // UuidActor(uuid, stack1)); + // add track to stack uuid.generate_in_place(); auto track1 = f.self->spawn("Track-001", media::MediaType::MT_IMAGE, uuid); - auto hist1 = request_receive( - *(f.self), stack1, insert_item_atom_v, 0, UuidActor(uuid, track1)); + auto result = request_receive( + *(f.self), stack1, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, track1)})); // add clip to track.. uuid.generate_in_place(); - auto clip1 = f.self->spawn(UuidActor(), "Clip-001", uuid); - auto result = request_receive( + auto clip1 = f.self->spawn(UuidActor(), "Clip-001", uuid); + result = request_receive( *(f.self), clip1, active_range_atom_v, @@ -322,13 +327,13 @@ TEST(TimelineActorHistoryTest, Test) { FrameRateDuration(3, timebase::k_flicks_24fps), FrameRateDuration(3, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, clip1)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip1)})); // stack uuid.generate_in_place(); auto stack2 = f.self->spawn("Nested Stack-002", uuid); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, stack2)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, stack2)})); result = request_receive( *(f.self), stack2, @@ -342,7 +347,7 @@ TEST(TimelineActorHistoryTest, Test) { auto gap1 = f.self->spawn( "Gap-001", FrameRateDuration(4, timebase::k_flicks_24fps), uuid); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, gap1)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, gap1)})); // clip 4 uuid.generate_in_place(); @@ -355,20 +360,20 @@ TEST(TimelineActorHistoryTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(6, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track1, insert_item_atom_v, -1, UuidActor(uuid, clip4)); + *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip4)})); // now populate nested stack uuid.generate_in_place(); auto track2 = f.self->spawn("Nested Track-002", media::MediaType::MT_IMAGE, uuid); result = request_receive( - *(f.self), stack2, insert_item_atom_v, -1, UuidActor(uuid, track2)); + *(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); result = request_receive( - *(f.self), stack2, insert_item_atom_v, -1, UuidActor(uuid, track3)); + *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track3)})); result = request_receive( *(f.self), track3, @@ -382,7 +387,7 @@ TEST(TimelineActorHistoryTest, Test) { auto gap2 = f.self->spawn( "Gap-002", FrameRateDuration(7, timebase::k_flicks_24fps), uuid); result = request_receive( - *(f.self), track2, insert_item_atom_v, -1, UuidActor(uuid, gap2)); + *(f.self), track2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, gap2)})); uuid.generate_in_place(); auto clip3 = f.self->spawn(UuidActor(), "Clip-003", uuid); @@ -394,7 +399,7 @@ TEST(TimelineActorHistoryTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(9, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track2, insert_item_atom_v, -1, UuidActor(uuid, clip3)); + *(f.self), track2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip3)})); // populate nested track 3 @@ -408,132 +413,133 @@ TEST(TimelineActorHistoryTest, Test) { FrameRateDuration(100, timebase::k_flicks_24fps), FrameRateDuration(9, timebase::k_flicks_24fps))); result = request_receive( - *(f.self), track3, insert_item_atom_v, -1, UuidActor(uuid, clip5)); + *(f.self), track3, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip5)})); - auto clip6_uuid = Uuid::generate(); - auto clip6 = f.self->spawn(UuidActor(), "Clip-006", clip6_uuid); - result = request_receive( - *(f.self), track3, insert_item_atom_v, -1, UuidActor(clip6_uuid, clip6)); - result = request_receive( + uuid.generate_in_place(); + auto clip6 = f.self->spawn(UuidActor(), "Clip-006", uuid); + result = request_receive( *(f.self), clip6, active_range_atom_v, FrameRange( FrameRateDuration(3, timebase::k_flicks_24fps), FrameRateDuration(3, timebase::k_flicks_24fps))); + result = request_receive( + *(f.self), track3, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, clip6)})); - std::this_thread::sleep_for(500ms); - - { - // validate clip 6 directly - auto item = request_receive(*(f.self), clip6, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - - auto titem = request_receive(*(f.self), t, item_atom_v); - auto sitem = find_item(titem.children(), clip6_uuid); - // validate cache in timeline - EXPECT_EQ( - sitem->trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - } // timeline should be valid.. // but might need to wait for the updates to bubble up // something not working... ordering of events ? + std::this_thread::sleep_for(500ms); - auto history = request_receive(*(f.self), t, history::history_atom_v); - EXPECT_EQ(request_receive(*(f.self), history.actor(), media_cache::count_atom_v), 15); + // validate trimmed ranges + auto item = request_receive(*(f.self), clip6, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); + item = request_receive(*(f.self), clip5, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(100, timebase::k_flicks_24fps), + FrameRateDuration(9, timebase::k_flicks_24fps))); + item = request_receive(*(f.self), track3, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(1, timebase::k_flicks_24fps), + FrameRateDuration(10, timebase::k_flicks_24fps))); - // THE BIG ONE.. - // TEST SIMPLE CHANGE UNDO/REDO - request_receive(*(f.self), t, history::undo_atom_v); - // clip duration should now be reset.. + item = request_receive(*(f.self), clip3, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(100, timebase::k_flicks_24fps), + FrameRateDuration(9, timebase::k_flicks_24fps))); - { - // validate clip 6 directly - auto item = request_receive(*(f.self), clip6, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(0, timebase::k_flicks_24fps))); + item = request_receive(*(f.self), gap2, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(7, timebase::k_flicks_24fps))); - auto titem = request_receive(*(f.self), t, item_atom_v); - auto sitem = find_item(titem.children(), clip6_uuid); - // validate cache in timeline - EXPECT_EQ( - sitem->trimmed_range(), - FrameRange( - FrameRateDuration(0, timebase::k_flicks_24fps), - FrameRateDuration(0, timebase::k_flicks_24fps))); - } + item = request_receive(*(f.self), track2, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(16, timebase::k_flicks_24fps))); - request_receive(*(f.self), t, history::redo_atom_v); + item = request_receive(*(f.self), stack2, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(2, timebase::k_flicks_24fps), + FrameRateDuration(6, timebase::k_flicks_24fps))); - { - // validate clip 6 directly - auto item = request_receive(*(f.self), clip6, item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); + item = request_receive(*(f.self), clip1, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(3, timebase::k_flicks_24fps), + FrameRateDuration(3, timebase::k_flicks_24fps))); - auto titem = request_receive(*(f.self), t, item_atom_v); - auto sitem = find_item(titem.children(), clip6_uuid); - // validate cache in timeline - EXPECT_EQ( - sitem->trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - } + item = request_receive(*(f.self), gap1, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(4, timebase::k_flicks_24fps))); - // TEST MORE COMPLEX CHANGE. - request_receive(*(f.self), t, history::undo_atom_v); - request_receive(*(f.self), t, history::undo_atom_v); - // undos insertion ? - // clip6 is now invalid - { - auto titem = request_receive(*(f.self), t, item_atom_v); - auto sitem = find_item(titem.children(), clip6_uuid); - EXPECT_EQ(sitem, titem.cend()); - } + item = request_receive(*(f.self), clip4, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(100, timebase::k_flicks_24fps), + FrameRateDuration(6, timebase::k_flicks_24fps))); + item = request_receive(*(f.self), track1, item_atom_v); + // spdlog::warn("{}", item.serialise().dump(2)); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(19, timebase::k_flicks_24fps))); - request_receive(*(f.self), t, history::redo_atom_v); - request_receive(*(f.self), t, history::redo_atom_v); + item = request_receive(*(f.self), stack1, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(19, timebase::k_flicks_24fps))); - { - auto titem = request_receive(*(f.self), t, item_atom_v); - auto sitem = find_item(titem.children(), clip6_uuid); - // validate cache in timeline - EXPECT_EQ( - sitem->trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - // validate clip 6 directly - auto item = request_receive(*(f.self), sitem->actor(), item_atom_v); - EXPECT_EQ( - item.trimmed_range(), - FrameRange( - FrameRateDuration(3, timebase::k_flicks_24fps), - FrameRateDuration(3, timebase::k_flicks_24fps))); - } - // FTW it actually worked.. + item = request_receive(*(f.self), t, item_atom_v); + EXPECT_EQ( + item.trimmed_range(), + FrameRange( + FrameRateDuration(0, timebase::k_flicks_24fps), + FrameRateDuration(19, timebase::k_flicks_24fps))); + + // serialise test + auto serialise = request_receive(*(f.self), t, serialise_atom_v); + auto t2 = f.self->spawn(serialise); + item = request_receive(*(f.self), t2, item_atom_v); + EXPECT_EQ( + item.trimmed_frame_duration().frames(), + FrameRateDuration(19, timebase::k_flicks_24fps).frames()); f.self->send_exit(t, caf::exit_reason::user_shutdown); + f.self->send_exit(t2, caf::exit_reason::user_shutdown); } + + // fixture f; // auto gsa = f.self->spawn(); // auto pl = f.self->spawn("Test"); diff --git a/src/timeline/test/track_actor_test.cpp b/src/timeline/test/track_actor_test.cpp index 130d08306..5861deb7d 100644 --- a/src/timeline/test/track_actor_test.cpp +++ b/src/timeline/test/track_actor_test.cpp @@ -35,7 +35,11 @@ TEST(TrackActorAddTest, Test) { f.self->spawn("Invalid Track", media::MediaType::MT_IMAGE, uuid); EXPECT_THROW( request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, invalid)), + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(uuid, invalid)})), std::runtime_error); f.self->send_exit(invalid, caf::exit_reason::user_shutdown); } @@ -45,7 +49,11 @@ TEST(TrackActorAddTest, Test) { auto invalid = f.self->spawn("Invalid Timeline", uuid); EXPECT_THROW( request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, invalid)), + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(uuid, invalid)})), std::runtime_error); f.self->send_exit(invalid, caf::exit_reason::user_shutdown); } @@ -54,21 +62,21 @@ TEST(TrackActorAddTest, Test) { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn("Valid Stack", uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn("Valid GAP", utility::FrameRateDuration(), uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn(UuidActor(), "Valid Clip", uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } auto item = request_receive(*(f.self), t, item_atom_v); @@ -78,12 +86,12 @@ TEST(TrackActorAddTest, Test) { item = request_receive(*(f.self), t, item_atom_v); EXPECT_EQ(item.size(), 2); - auto jitem = std::pair(); + auto jitem = std::pair>(); EXPECT_NO_THROW( - (jitem = - request_receive>(*(f.self), t, remove_item_atom_v, 0))); - EXPECT_EQ(jitem.second.item_type(), ItemType::IT_GAP); - f.self->send_exit(jitem.second.actor(), caf::exit_reason::user_shutdown); + (jitem = request_receive>>( + *(f.self), t, remove_item_atom_v, 0))); + EXPECT_EQ(jitem.second[0].item_type(), ItemType::IT_GAP); + f.self->send_exit(jitem.second[0].actor(), caf::exit_reason::user_shutdown); item = request_receive(*(f.self), t, item_atom_v); EXPECT_EQ(item.size(), 1); @@ -107,7 +115,7 @@ TEST(TrackActorTest, Test) { auto uuid = utility::Uuid::generate(); auto valid = f.self->spawn("Valid GAP", utility::FrameRateDuration(), uuid); EXPECT_NO_THROW(request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(uuid, valid))); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } @@ -124,27 +132,82 @@ TEST(TrackActorTest, Test) { f.self->send_exit(t2, caf::exit_reason::user_shutdown); } +// [=](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); +// return rp; +// }, + + +TEST(TrackMoveActorTest, Test) { + fixture f; + // start_logger(spdlog::level::debug); + auto t = f.self->spawn(); + auto duration = FrameRateDuration(10, timebase::k_flicks_24fps); + + auto gap_uuid1 = utility::Uuid::generate(); + auto gap_actor1 = f.self->spawn("Gap1", duration, gap_uuid1); + auto gap_uuid2 = utility::Uuid::generate(); + auto gap_actor2 = f.self->spawn("Gap2", duration, gap_uuid2); + auto gap_uuid3 = utility::Uuid::generate(); + auto gap_actor3 = f.self->spawn("Gap3", duration, gap_uuid3); + + request_receive( + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(gap_uuid3, gap_actor3)})); + request_receive( + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(gap_uuid2, gap_actor2)})); + request_receive( + *(f.self), + t, + insert_item_atom_v, + 0, + UuidActorVector({UuidActor(gap_uuid1, gap_actor1)})); + + auto item = request_receive(*(f.self), t, item_atom_v); + EXPECT_EQ((*(item.item_at_index(0)))->uuid(), gap_uuid1); + EXPECT_EQ((*(item.item_at_index(1)))->uuid(), gap_uuid2); + EXPECT_EQ((*(item.item_at_index(2)))->uuid(), gap_uuid3); + + request_receive(*(f.self), t, move_item_atom_v, 0, 1, 2); + + item = request_receive(*(f.self), t, item_atom_v); + EXPECT_EQ((*(item.item_at_index(0)))->uuid(), gap_uuid2); + EXPECT_EQ((*(item.item_at_index(1)))->uuid(), gap_uuid1); + EXPECT_EQ((*(item.item_at_index(2)))->uuid(), gap_uuid3); + + + f.self->send_exit(t, caf::exit_reason::user_shutdown); +} + TEST(TrackUndoActorTest, Test) { fixture f; // start_logger(spdlog::level::debug); auto t = f.self->spawn(); auto item = request_receive(*(f.self), t, item_atom_v); + // create gap, check duration. auto guuid1 = utility::Uuid::generate(); auto duration1 = FrameRateDuration(10, timebase::k_flicks_24fps); auto range1 = FrameRange(duration1); - - // create gap, check duration. - auto gap1 = f.self->spawn("Gap1", duration1, guuid1); - auto gitem = request_receive(*(f.self), gap1, item_atom_v); + auto gap1 = f.self->spawn("Gap1", duration1, guuid1); + auto gitem = request_receive(*(f.self), gap1, item_atom_v); EXPECT_EQ(gitem.trimmed_range(), range1); // insert gap. auto hist1 = request_receive( - *(f.self), t, insert_item_atom_v, 0, UuidActor(guuid1, gap1)); + *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(guuid1, gap1)})); - auto item2 = request_receive(*(f.self), t, item_atom_v); // check gap in track + auto item2 = request_receive(*(f.self), t, item_atom_v); EXPECT_EQ(item2.children().front().trimmed_range(), range1); // change gap .. diff --git a/src/timeline/test/track_test.cpp b/src/timeline/test/track_test.cpp index 3776dda68..3d2e429e1 100644 --- a/src/timeline/test/track_test.cpp +++ b/src/timeline/test/track_test.cpp @@ -30,4 +30,113 @@ TEST(TrackTest, Test) { EXPECT_EQ(sum_trimmed_duration(t.children()), timebase::k_flicks_24fps * 20); t.refresh_item(); EXPECT_EQ(t.item().trimmed_range().duration(), timebase::k_flicks_24fps * 20); + + auto iaf = t.item().item_at_frame(0); + EXPECT_TRUE(iaf); + EXPECT_EQ(iaf->first, t.item().cbegin()); + + iaf = t.item().item_at_frame(20); + EXPECT_FALSE(iaf); + + iaf = t.item().item_at_frame(9); + EXPECT_TRUE(iaf); + EXPECT_EQ(iaf->first, t.item().cbegin()); + EXPECT_EQ(iaf->second, 9); + + iaf = t.item().item_at_frame(10); + EXPECT_TRUE(iaf); + EXPECT_EQ(iaf->first, std::next(t.item().cbegin(), 1)); + EXPECT_EQ(iaf->second, 0); + + EXPECT_EQ(t.item().frame_at_index(0), t.item().trimmed_frame_start().frames()); + EXPECT_EQ(t.item().frame_at_index(1), t.item().trimmed_frame_start().frames() + 10); + EXPECT_EQ(t.item().frame_at_index(2), t.item().trimmed_frame_start().frames() + 20); + EXPECT_EQ(t.item().frame_at_index(3), t.item().trimmed_frame_start().frames() + 20); + + EXPECT_EQ(t.item().frame_at_index(0, 1), t.item().trimmed_frame_start().frames() + 1); +} + +std::pair doMoveTest(Track &t, int start, int count, int before) { + auto sit = std::next(t.item().begin(), start); + auto eit = std::next(sit, count); + auto dit = std::next(t.item().begin(), before); + + auto changes = t.item().splice(dit, t.item().children(), sit, eit); + auto more = t.item().refresh(); + + return std::make_pair(more, changes); +} + +#define TESTMOVE(...) \ + t.item().undo(more); \ + t.item().undo(changes); \ + \ + EXPECT_EQ((*(t.item().item_at_index(0)))->name(), "Gap 1"); \ + EXPECT_EQ((*(t.item().item_at_index(1)))->name(), "Gap 2"); \ + EXPECT_EQ((*(t.item().item_at_index(2)))->name(), "Gap 3"); \ + EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 4"); + + +TEST(TrackMoveTest, Test) { + Track t("Track", MediaType::MT_IMAGE); + + t.children().emplace_back( + Gap("Gap 1", utility::FrameRateDuration(10, timebase::k_flicks_24fps)).item()); + t.children().emplace_back( + Gap("Gap 2", utility::FrameRateDuration(10, timebase::k_flicks_24fps)).item()); + t.children().emplace_back( + Gap("Gap 3", utility::FrameRateDuration(10, timebase::k_flicks_24fps)).item()); + t.children().emplace_back( + Gap("Gap 4", utility::FrameRateDuration(10, timebase::k_flicks_24fps)).item()); + + EXPECT_EQ(t.item().item_at_frame(0)->first->name(), "Gap 1"); + EXPECT_EQ(t.item().item_at_frame(10)->first->name(), "Gap 2"); + EXPECT_EQ(t.item().item_at_frame(20)->first->name(), "Gap 3"); + EXPECT_EQ(t.item().item_at_frame(30)->first->name(), "Gap 4"); + + { + auto [more, changes] = doMoveTest(t, 0, 1, 3); + + EXPECT_EQ((*(t.item().item_at_index(0)))->name(), "Gap 2"); + EXPECT_EQ((*(t.item().item_at_index(1)))->name(), "Gap 3"); + EXPECT_EQ((*(t.item().item_at_index(2)))->name(), "Gap 1"); + EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 4"); + + TESTMOVE() + } + + { + auto [more, changes] = doMoveTest(t, 0, 2, 3); + + EXPECT_EQ((*(t.item().item_at_index(0)))->name(), "Gap 3"); + EXPECT_EQ((*(t.item().item_at_index(1)))->name(), "Gap 1"); + EXPECT_EQ((*(t.item().item_at_index(2)))->name(), "Gap 2"); + EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 4"); + + TESTMOVE() + } + + { + auto [more, changes] = doMoveTest(t, 1, 1, 0); + + EXPECT_EQ((*(t.item().item_at_index(0)))->name(), "Gap 2"); + EXPECT_EQ((*(t.item().item_at_index(1)))->name(), "Gap 1"); + EXPECT_EQ((*(t.item().item_at_index(2)))->name(), "Gap 3"); + EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 4"); + + TESTMOVE() + } + + { + auto [more, changes] = doMoveTest(t, 1, 3, 0); + + // should be 0,3,4 + EXPECT_EQ((*(t.item().item_at_index(0)))->name(), "Gap 2"); + EXPECT_EQ((*(t.item().item_at_index(1)))->name(), "Gap 3"); + EXPECT_EQ((*(t.item().item_at_index(2)))->name(), "Gap 4"); + EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 1"); + + + TESTMOVE() + } } diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 673135c80..59d23a909 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -1,4 +1,5 @@ add_src_and_test(base) +add_src_and_test(canvas) add_src_and_test(model_data) add_src_and_test(viewport) add_src_and_test(qt) diff --git a/src/ui/base/src/CMakeLists.txt b/src/ui/base/src/CMakeLists.txt index f3b876259..b2e3ef692 100644 --- a/src/ui/base/src/CMakeLists.txt +++ b/src/ui/base/src/CMakeLists.txt @@ -1,19 +1,25 @@ SET(LINK_DEPS - pthread xstudio::utility Imath::Imath OpenEXR::OpenEXR ) +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}") -include_directories("${FREETYPE_INCLUDE_DIRS}") - target_link_libraries(${PROJECT_NAME} PRIVATE freetype -) \ No newline at end of file +) diff --git a/src/ui/base/src/font.cpp b/src/ui/base/src/font.cpp index 22c8698a2..11fc56e7d 100644 --- a/src/ui/base/src/font.cpp +++ b/src/ui/base/src/font.cpp @@ -711,6 +711,39 @@ void VectorFont::glyph_shape_decomposition_complete() { } } +std::map> SDFBitmapFont::available_fonts() { + + static std::map> res; + + if (res.empty()) { + for (const auto &f : Fonts::available_fonts()) { + try { + res[f.first] = std::make_shared(f.second, 96); + } catch (std::exception &e) { + spdlog::warn("Failed to load font: {}.", e.what()); + } + } + } + + return res; +} + +std::shared_ptr SDFBitmapFont::font_by_name(const std::string &name) { + + for (auto &[fontName, fontPtr] : available_fonts()) { + if (name == fontName) { + return fontPtr; + } + } + + const auto &fonts = SDFBitmapFont::available_fonts(); + if (!fonts.empty()) { + return fonts.begin()->second; + } + + return std::shared_ptr(); +} + void SDFBitmapFont::generate_atlas(const std::string &font_path, const int glyph_pixel_size) { auto t0 = xstudio::utility::clock::now(); diff --git a/src/ui/base/src/keyboard.cpp b/src/ui/base/src/keyboard.cpp index b344d1353..545adf670 100644 --- a/src/ui/base/src/keyboard.cpp +++ b/src/ui/base/src/keyboard.cpp @@ -5,138 +5,6 @@ /* forward declaration of this function from tessellation_helpers.cpp */ using namespace xstudio::ui; -// 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. -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"}}; - Hotkey::Hotkey( const int k, const int mod, diff --git a/src/ui/canvas/src/CMakeLists.txt b/src/ui/canvas/src/CMakeLists.txt new file mode 100644 index 000000000..240b4d906 --- /dev/null +++ b/src/ui/canvas/src/CMakeLists.txt @@ -0,0 +1,27 @@ +SET(LINK_DEPS + xstudio::utility + Imath::Imath + OpenEXR::OpenEXR + ui_base +) + +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 diff --git a/src/ui/canvas/src/canvas.cpp b/src/ui/canvas/src/canvas.cpp new file mode 100644 index 000000000..391e37ea5 --- /dev/null +++ b/src/ui/canvas/src/canvas.cpp @@ -0,0 +1,663 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "xstudio/ui/canvas/canvas.hpp" +#include "xstudio/ui/canvas/canvas_undo_redo.hpp" + +using namespace xstudio; +using namespace xstudio::ui; +using namespace xstudio::ui::canvas; + + +namespace { + +template inline constexpr bool always_false_v = false; + +} // anonymous namespace + + +void Canvas::clear(const bool clear_history) { + + std::unique_lock l(mutex_); + if (clear_history) { + undo_stack_.clear(); + redo_stack_.clear(); + } else { + undo_stack_.emplace_back(new UndoRedoClear(items_)); + } + items_.clear(); + current_item_.reset(); + changed(); +} + +void Canvas::undo() { + + std::unique_lock l(mutex_); + if (undo_stack_.size()) { + undo_stack_.back()->undo(this); + redo_stack_.push_back(undo_stack_.back()); + undo_stack_.pop_back(); + } + changed(); +} + +void Canvas::redo() { + + std::unique_lock l(mutex_); + if (redo_stack_.size()) { + redo_stack_.back()->redo(this); + undo_stack_.push_back(redo_stack_.back()); + redo_stack_.pop_back(); + } + changed(); +} + +void Canvas::start_stroke( + const utility::ColourTriplet &colour, float thickness, float softness, float opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Pen(colour, thickness, softness, opacity); + changed(); +} + +void Canvas::start_erase_stroke(float thickness) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Erase(thickness); + changed(); +} + +void Canvas::update_stroke(const Imath::V2f &pt) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().add_point(pt); + } + changed(); +} + +bool Canvas::fade_all_strokes(float opacity) { + + std::unique_lock l(mutex_); + for (auto &item : items_) { + if (std::holds_alternative(item)) { + auto &stroke = std::get(item); + + if (stroke.opacity > opacity * 0.95) { + stroke.opacity -= 0.005f * opacity; + } else if (stroke.opacity > 0.0f) { + stroke.opacity -= 0.05f * opacity; + } + } + } + + // Number of stroke still visible (opacity greater than 0) + size_t remaining_strokes = 0; + items_.erase( + std::remove_if( + items_.begin(), + items_.end(), + [&remaining_strokes](auto &item) { + if (std::holds_alternative(item)) { + const auto &stroke = std::get(item); + if (stroke.opacity <= 0.0f) { + return true; + } else { + remaining_strokes++; + } + } + return false; + }), + items_.end()); + changed(); + return remaining_strokes > 0; +} + +void Canvas::start_square( + const utility::ColourTriplet &colour, float thickness, float opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Pen(colour, thickness, 0.0f, opacity); + changed(); +} + +void Canvas::update_square(const Imath::V2f &corner1, const Imath::V2f &corner2) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().make_square(corner1, corner2); + } + changed(); +} + +void Canvas::start_circle( + const utility::ColourTriplet &colour, float thickness, float opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Pen(colour, thickness, 0.0f, opacity); + changed(); +} + +void Canvas::update_circle(const Imath::V2f ¢er, float radius) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().make_circle(center, radius); + } + changed(); +} + +void Canvas::start_arrow(const utility::ColourTriplet &colour, float thickness, float opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Pen(colour, thickness, 0.0f, opacity); + changed(); +} + +void Canvas::update_arrow(const Imath::V2f &start, const Imath::V2f &end) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().make_arrow(start, end); + } + changed(); +} + +void Canvas::start_line(const utility::ColourTriplet &colour, float thickness, float opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Stroke::Pen(colour, thickness, 0.0f, opacity); +} + +void Canvas::update_line(const Imath::V2f &start, const Imath::V2f &end) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().make_line(start, end); + } + changed(); +} + +void Canvas::start_caption( + const Imath::V2f &position, + const std::string &font_name, + float font_size, + const utility::ColourTriplet &colour, + float opacity, + float wrap_width, + Justification justification, + const utility::ColourTriplet &background_colour, + float background_opacity) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + current_item_ = Caption( + position, + wrap_width, + font_size, + colour, + opacity, + justification, + font_name, + background_colour, + background_opacity); + changed(); +} + +std::string Canvas::caption_text() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().text; + } + + return ""; +} + +Imath::V2f Canvas::caption_position() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().position; + } + + return Imath::V2f(0.0f, 0.0f); +} + +float Canvas::caption_width() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().wrap_width; + } + + return 0.0f; +} + +float Canvas::caption_font_size() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().font_size; + } + + return 0.0f; +} + +utility::ColourTriplet Canvas::caption_colour() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().colour; + } + + return utility::ColourTriplet(); +} + +float Canvas::caption_opacity() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().opacity; + } + + return 0.0f; +} + +std::string Canvas::caption_font_name() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().font_name; + } + + return ""; +} + +utility::ColourTriplet Canvas::caption_background_colour() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().background_colour; + } + + return utility::ColourTriplet(); +} + +float Canvas::caption_background_opacity() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().background_opacity; + } + + return 0.0f; +} + +Imath::Box2f Canvas::caption_bounding_box() const { + + std::shared_lock l(mutex_); + if (has_current_item_nolock()) { + return current_item().bounding_box(); + } + + return Imath::Box2f(); +} + +std::array Canvas::caption_cursor_position() const { + + std::shared_lock l(mutex_); + std::array position = {Imath::V2f(0.0f, 0.0f), Imath::V2f(0.0f, 0.0f)}; + + if (has_current_item_nolock()) { + const Caption &caption = current_item(); + + Imath::V2f v = SDFBitmapFont::font_by_name(caption.font_name) + ->get_cursor_screen_position( + caption.text, + caption.position, + caption.wrap_width, + caption.font_size, + caption.justification, + 1.0f, + cursor_position_); + + position[0] = v; + position[1] = v - Imath::V2f(0.0f, caption.font_size * 2.0f / 1920.0f * 0.8f); + } + + return position; +} + +void Canvas::update_caption_text(const std::string &text) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().modify_text(text, cursor_position_); + } + changed(); +} + +void Canvas::update_caption_position(const Imath::V2f &position) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().position = position; + } + changed(); +} + +void Canvas::update_caption_width(float wrap_width) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().wrap_width = wrap_width; + } + changed(); +} + +void Canvas::update_caption_font_size(float font_size) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().font_size = font_size; + } + changed(); +} + +void Canvas::update_caption_colour(const utility::ColourTriplet &colour) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().colour = colour; + } + changed(); +} + +void Canvas::update_caption_opacity(float opacity) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().opacity = opacity; + } + changed(); +} + +void Canvas::update_caption_font_name(const std::string &font_name) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().font_name = font_name; + } + changed(); +} + +void Canvas::update_caption_background_colour(const utility::ColourTriplet &colour) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().background_colour = colour; + } + changed(); +} + +void Canvas::update_caption_background_opacity(float opacity) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + current_item().background_opacity = opacity; + } + changed(); +} + +bool Canvas::has_selected_caption() const { + std::shared_lock l(mutex_); + return has_current_item_nolock(); +} + +bool Canvas::select_caption( + const Imath::V2f &pos, const Imath::V2f &handle_size, float viewport_pixel_scale) { + + std::unique_lock l(mutex_); + auto update_cursor_position = [&]() { + const Caption &c = current_item(); + cursor_position_ = + SDFBitmapFont::font_by_name(c.font_name) + ->viewport_position_to_cursor( + pos, c.text, c.position, c.wrap_width, c.font_size, c.justification, 1.0f); + }; + + auto find_interesecting_caption = [&]() { + return std::find_if(items_.begin(), items_.end(), [pos](auto &item) { + if (std::holds_alternative(item)) { + auto &caption = std::get(item); + return caption.bounding_box().intersects(pos); + } + return false; + }); + }; + + // Early exit if we already have selected this caption. + // (But update the cursor position beforehand) + if (has_current_item_nolock()) { + HandleHoverState state = + hover_selected_caption_handle_nolock(pos, handle_size, viewport_pixel_scale); + + if (state == HandleHoverState::HoveredInCaptionArea) { + update_cursor_position(); + } + if (state != HandleHoverState::NotHovered) { + return false; + } + } + + // Not selecting the current caption so it will be unselected. + end_draw_no_lock(); + changed(); + + // We selected an existing caption. + if (auto it = find_interesecting_caption(); it != items_.end()) { + current_item_ = *it; + items_.erase(it); + + update_cursor_position(); + return true; + } + + // Reaching this point means no existing caption was under the cursor. + + return false; +} + +HandleHoverState Canvas::hover_selected_caption_handle( + const Imath::V2f &pos, const Imath::V2f &handle_size, float viewport_pixel_scale) const { + std::shared_lock l(mutex_); + return hover_selected_caption_handle_nolock(pos, handle_size, viewport_pixel_scale); +} + +HandleHoverState Canvas::hover_selected_caption_handle_nolock( + const Imath::V2f &pos, const Imath::V2f &handle_size, float viewport_pixel_scale) const { + + if (has_current_item_nolock()) { + + const auto &caption = current_item(); + + const Imath::V2f cp_move = caption.bounding_box().min - pos; + const Imath::V2f cp_resize = pos - caption.bounding_box().max; + const Imath::V2f cp_delete = + pos - Imath::V2f( + caption.bounding_box().max.x, + caption.bounding_box().min.y - handle_size.y * viewport_pixel_scale); + const Imath::Box2f handle_extent = + Imath::Box2f(Imath::V2f(0.0f, 0.0f), handle_size * viewport_pixel_scale); + + if (handle_extent.intersects(cp_move)) { + return HandleHoverState::HoveredOnMoveHandle; + } else if (handle_extent.intersects(cp_resize)) { + return HandleHoverState::HoveredOnResizeHandle; + } else if (handle_extent.intersects(cp_delete)) { + return HandleHoverState::HoveredOnDeleteHandle; + } else if (caption.bounding_box().intersects(pos)) { + return HandleHoverState::HoveredInCaptionArea; + } + } + + return HandleHoverState::NotHovered; +} + +Imath::Box2f +Canvas::hover_caption_bounding_box(const Imath::V2f &pos, float viewport_pixel_scale) const { + + std::shared_lock l(mutex_); + for (auto &item : items_) { + if (std::holds_alternative(item)) { + auto &caption = std::get(item); + + if (caption.bounding_box().intersects(pos)) { + return caption.bounding_box(); + } + } + } + + return Imath::Box2f(); +} + +void Canvas::move_caption_cursor(int key) { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + auto &caption = current_item(); + + if (key == 16777235) { + // up arrow + cursor_position_ = SDFBitmapFont::font_by_name(caption.font_name) + ->cursor_up_or_down( + cursor_position_, + true, + caption.text, + caption.wrap_width, + caption.font_size, + caption.justification, + 1.0f); + + } else if (key == 16777237) { + // down arrow + cursor_position_ = SDFBitmapFont::font_by_name(caption.font_name) + ->cursor_up_or_down( + cursor_position_, + false, + caption.text, + caption.wrap_width, + caption.font_size, + caption.justification, + 1.0f); + + } else if (key == 16777236) { + // right arrow + if (cursor_position_ != caption.text.cend()) + cursor_position_++; + + } else if (key == 16777234) { + // left arrow + if (cursor_position_ != caption.text.cbegin()) + cursor_position_--; + + } else if (key == 16777232) { + // home + cursor_position_ = caption.text.cbegin(); + + } else if (key == 16777233) { + // end + cursor_position_ = caption.text.cend(); + } + } + changed(); +} + +void Canvas::delete_caption() { + + std::unique_lock l(mutex_); + if (has_current_item_nolock()) { + auto &caption = current_item(); + + // Empty caption deletion doesn't need undo/redo + if (caption.text.empty()) { + current_item_.reset(); + } else { + undo_stack_.emplace_back(new UndoRedoDel(current_item_.value())); + redo_stack_.clear(); + current_item_.reset(); + } + } + changed(); +} + +void Canvas::end_draw() { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + changed(); +} + +void Canvas::end_draw_no_lock() { + + // Empty caption deletion doesn't need undo/redo + if (has_current_item_nolock()) { + if (current_item().text.empty()) { + current_item_.reset(); + } + } + + if (current_item_) { + undo_stack_.emplace_back(new UndoRedoAdd(current_item_.value())); + redo_stack_.clear(); + items_.push_back(current_item_.value()); + current_item_.reset(); + } +} + +void Canvas::changed() { last_change_time_ = utility::clock::now(); } + + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Canvas &c) { + + if (j.contains("pen_strokes") && j["pen_strokes"].is_array()) { + for (const auto &item : j["pen_strokes"]) { + c.items_.push_back(item.template get()); + } + } + + if (j.contains("captions") && j["captions"].is_array()) { + for (const auto &item : j["captions"]) { + c.items_.push_back(item.template get()); + } + } + c.changed(); +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Canvas &c) { + + j["pen_strokes"] = nlohmann::json::array(); + j["captions"] = nlohmann::json::array(); + + for (const auto &item : c) { + std::visit( + [&j](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + j["pen_strokes"].push_back(nlohmann::json(arg)); + else if constexpr (std::is_same_v) + j["captions"].push_back(nlohmann::json(arg)); + else + static_assert(always_false_v, "Missing serialiser for canvas item!"); + }, + item); + } +} diff --git a/src/ui/canvas/src/canvas_undo_redo.cpp b/src/ui/canvas/src/canvas_undo_redo.cpp new file mode 100644 index 000000000..a10f4659e --- /dev/null +++ b/src/ui/canvas/src/canvas_undo_redo.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/canvas/canvas_undo_redo.hpp" +#include "xstudio/ui/canvas/canvas.hpp" + +using namespace xstudio::ui::canvas; +using namespace xstudio; + + +void UndoRedoAdd::redo(Canvas *canvas) { canvas->items_.push_back(item_); } + +void UndoRedoAdd::undo(Canvas *canvas) { + + if (canvas->items_.size()) { + canvas->items_.pop_back(); + } +} + +void UndoRedoDel::redo(Canvas *canvas) { + + if (canvas->items_.size()) { + canvas->items_.pop_back(); + } +} + +void UndoRedoDel::undo(Canvas *canvas) { canvas->items_.push_back(item_); } + +void UndoRedoClear::redo(Canvas *canvas) { canvas->items_.clear(); } + +void UndoRedoClear::undo(Canvas *canvas) { canvas->items_ = items_; } \ No newline at end of file diff --git a/src/ui/canvas/src/caption.cpp b/src/ui/canvas/src/caption.cpp new file mode 100644 index 000000000..697723601 --- /dev/null +++ b/src/ui/canvas/src/caption.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/canvas/caption.hpp" + +using namespace xstudio::ui::canvas; +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, + const utility::ColourTriplet background_colour, + const float background_opacity) + : position(position), + wrap_width(wrap_width), + font_size(font_size), + colour(colour), + opacity(opacity), + justification(justification), + font_name(std::move(font_name)), + background_colour(background_colour), + background_opacity(background_opacity) {} + +bool Caption::operator==(const Caption &o) const { + + return ( + text == o.text && position == o.position && wrap_width == o.wrap_width && + font_size == o.font_size && font_name == o.font_name && colour == o.colour && + opacity == o.opacity && justification == o.justification && + background_colour == o.background_colour && background_opacity == o.background_opacity); +} + +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; + + update_vertices(); +} + +Imath::Box2f Caption::bounding_box() const { + + update_vertices(); + return bounding_box_; +} + +std::vector Caption::vertices() const { + + update_vertices(); + return vertices_; +} + +std::string Caption::hash() const { + + std::string hash; + hash += text; + hash += std::to_string(position.x); + hash += std::to_string(position.y); + hash += std::to_string(wrap_width); + hash += std::to_string(font_size); + hash += std::to_string((int)justification); + + return hash; +} + +void Caption::update_vertices() const { + const std::string curr_hash = hash(); + + if (curr_hash != hash_) { + bounding_box_ = + SDFBitmapFont::font_by_name(font_name)->precompute_text_rendering_vertex_layout( + vertices_, text, position, wrap_width, font_size, justification, 1.0f); + hash_ = curr_hash; + } +} + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Caption &c) { + + j.at("text").get_to(c.text); + j.at("position").get_to(c.position); + j.at("wrap_width").get_to(c.wrap_width); + j.at("font_size").get_to(c.font_size); + j.at("font_name").get_to(c.font_name); + j.at("colour").get_to(c.colour); + j.at("opacity").get_to(c.opacity); + j.at("justification").get_to(c.justification); + + if (j.contains("background_colour") && j.contains("background_opacity")) { + j.at("background_colour").get_to(c.background_colour); + j.at("background_opacity").get_to(c.background_opacity); + } +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Caption &c) { + + j = nlohmann::json{ + {"text", c.text}, + {"position", c.position}, + {"wrap_width", c.wrap_width}, + {"font_size", c.font_size}, + {"font_name", c.font_name}, + {"colour", c.colour}, + {"opacity", c.opacity}, + {"justification", c.justification}, + {"background_colour", c.background_colour}, + {"background_opacity", c.background_opacity}}; +} \ No newline at end of file diff --git a/src/ui/canvas/src/stroke.cpp b/src/ui/canvas/src/stroke.cpp new file mode 100644 index 000000000..f8b37b202 --- /dev/null +++ b/src/ui/canvas/src/stroke.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/canvas/stroke.hpp" + +using namespace xstudio::ui::canvas; +using namespace xstudio; + +namespace { + +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); + +} // anonymous namespace + + +Stroke Stroke::Pen( + const utility::ColourTriplet &colour, + const float thickness, + const float softness, + const float opacity) { + + Stroke s; + s.thickness = thickness; + s.softness = softness; + s.colour = colour; + s.opacity = opacity; + s.type = StrokeType_Pen; + return s; +} + +Stroke Stroke::Erase(const float thickness) { + + Stroke s; + s.thickness = thickness; + s.softness = 0.0f; + s.colour = {1.0f, 1.0f, 1.0f}; + s.opacity = 1.0f; + s.type = StrokeType_Erase; + return s; +} + +bool Stroke::operator==(const Stroke &o) const { + return ( + opacity == o.opacity && thickness == o.thickness && softness == o.softness && + colour == o.colour && type == o.type && points == o.points); +} + +void Stroke::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 Stroke::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 Stroke::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 Stroke::make_line(const Imath::V2f &start, const Imath::V2f &end) { + + points.clear(); + points.push_back(start); + points.push_back(end); +} + +void Stroke::add_point(const Imath::V2f &pt) { + + if (!(!points.empty() && points.back() == pt)) { + points.emplace_back(pt); + } +} + +std::vector Stroke::vertices() const { + + std::vector result; + + if (!points.empty()) { + result = points; + result.push_back(points.back()); // repeat last point to make end 'cap' + } + + return result; +} + +// Note the below is slightly more complex than it could because +// we try to maintain bakward compatibility with previous format. + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Stroke &s) { + + j.at("opacity").get_to(s.opacity); + j.at("thickness").get_to(s.thickness); + + if (j.contains("softness")) { + j.at("softness").get_to(s.softness); + } + + s.type = j["is_erase_stroke"].get() ? StrokeType_Erase : StrokeType_Pen; + s.colour = + utility::ColourTriplet{j.value("r", 1.0f), j.value("g", 1.0f), j.value("b", 1.0f)}; + + if (j.contains("points") && j["points"].is_array()) { + auto it = j["points"].begin(); + while (it != j["points"].end()) { + auto x = it++.value().get(); + auto y = it++.value().get(); + s.add_point(Imath::V2f(x, y)); + } + } +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Stroke &s) { + + j = nlohmann::json{ + {"opacity", s.opacity}, + {"thickness", s.thickness}, + {"softness", s.softness}, + {"is_erase_stroke", s.type == StrokeType_Erase}}; + + std::vector pts; + pts.reserve(s.points.size() * 2); + for (auto &pt : s.points) { + pts.push_back(pt.x); + pts.push_back(pt.y); + } + j["points"] = pts; + + if (s.type != StrokeType_Erase) { + j["r"] = s.colour.r; + j["g"] = s.colour.g; + j["b"] = s.colour.b; + } +} \ No newline at end of file diff --git a/src/ui/canvas/test/CMakeLists.txt b/src/ui/canvas/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/ui/model_data/src/CMakeLists.txt b/src/ui/model_data/src/CMakeLists.txt index db2b837cd..0a05284cf 100644 --- a/src/ui/model_data/src/CMakeLists.txt +++ b/src/ui/model_data/src/CMakeLists.txt @@ -1,5 +1,7 @@ SET(LINK_DEPS ${CAF_LIBRARY_core} + xstudio::json_store + xstudio::global_store xstudio::utility ) diff --git a/src/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index 58248aed7..f92f20960 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -1,10 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 +#include #include #include "xstudio/atoms.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/module/attribute.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/ui/model_data/model_data_actor.hpp" @@ -14,6 +16,66 @@ using namespace xstudio; using namespace xstudio::ui::model_data; using namespace xstudio::utility; +namespace { + +utility::JsonTree *add_node(utility::JsonTree *node, const nlohmann::json &new_entry_data) { + + auto p = node->insert(std::next(node->begin(), node->size()), new_entry_data); + return &(*p); +} + +std::string path_from_node(utility::JsonTree *node) { + if (!node->parent()) + return std::string(); + return path_from_node(node->parent()) + + std::string(fmt::format("/children/{}", node->index())); +} + +inline void dump_tree2(const JsonTree &node, const int depth = 0) { + spdlog::warn("{:>{}} {}", " ", depth * 4, node.data().dump(2)); + 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) { + 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 (r) + return r; + } catch (...) { + } + } + return nullptr; +} + +bool find_and_delete( + utility::JsonTree *data, const std::string &field, const std::string &field_value) { + if (data->data().contains(field) && data->data()[field].get() == field_value) { + + data->parent()->erase(std::next(data->parent()->begin(), data->index())); + return true; + + } else { + for (auto p = data->begin(); p != data->end(); ++p) { + if (find_and_delete(&(*p), field, field_value)) { + return true; + } + } + } + return false; +} + +static const std::string attr_uuid_role_name( + module::Attribute::role_names.find(module::Attribute::UuidRole)->second); + +} // namespace + GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_actor(cfg) { print_on_create(this, "GlobalUIModelData"); @@ -59,6 +121,31 @@ 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 &attribute_data, + const utility::Uuid &attribute_uuid, + const std::string &sort_role, + caf::actor client) -> result { + try { + insert_attribute_data_into_model( + model_name, attribute_uuid, attribute_data, sort_role, 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::Uuid &attribute_uuid, + caf::actor client) { + try { + remove_attribute_data_from_model(model_name, attribute_uuid, client); + } catch (std::exception &e) { + std::cerr << "E " << e.what() << "\n"; + // return caf::make_error(xstudio_error::error, e.what()); + } + }, [=](register_model_data_atom, const std::string &model_name, const std::string &preferences_path, @@ -86,13 +173,24 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ const std::string path, const utility::JsonStore &data, const std::string &role) { set_data(model_name, path, data, role); }, + [=](set_node_data_atom, + const std::string &model_name, + const utility::Uuid &attribute_uuid, + const std::string &role, + const utility::JsonStore &data, + caf::actor setter) { set_data(model_name, attribute_uuid, role, data, setter); }, [=](insert_rows_atom, const std::string &model_name, const std::string &path, const int row, const int count, - const utility::JsonStore &data) { + const utility::JsonStore &data) -> result { insert_rows(model_name, path, row, count, data); + try { + return model_data_as_json(model_name); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } }, [=](insert_rows_atom, const std::string &model_name, @@ -113,6 +211,27 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ const std::string &path, const int row, const int count) { remove_rows(model_name, path, row, count); }, + + [=](remove_rows_atom, + const std::string &model_name, + const utility::Uuid &attribute_uuid) { + remove_attribute_from_model(model_name, attribute_uuid); + }, + + [=](remove_rows_atom, + const std::string &model_name, + const std::string &path, + const int row, + const int count, + const bool broadcast_back_to_sender) { + if (broadcast_back_to_sender) { + remove_rows(model_name, path, row, count); + } else { + auto sender = actor_cast(current_sender()); + remove_rows(model_name, path, row, count, sender); + } + }, + [=](menu_node_activated_atom, const std::string &model_name, const std::string &path) { node_activated(model_name, path); }, @@ -127,8 +246,28 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ const std::string model_name, const utility::Uuid &model_item_id) { remove_node(model_name, model_item_id); }, [=](json_store::update_atom, const std::string model_name) { - push_to_prefs(model_name); - }); + push_to_prefs(model_name, true); + }, + [=](model_data_atom) { + 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); + } + } + models_to_be_fully_broadcasted_.clear(); + }, + [=](const caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }, + [=](caf::message &msg) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(msg)); }); } void GlobalUIModelData::set_data( @@ -155,18 +294,34 @@ void GlobalUIModelData::set_data( 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; + if (j.contains(attr_uuid_role_name)) { + uuid_role_data = utility::Uuid(j[attr_uuid_role_name].get()); + } } } else if (role.empty()) { - j = data; + + JsonTree new_node = json_to_tree(data, "children"); + *node = new_node; + changed = true; } - if (changed) { + if (changed && !role.empty()) { for (auto &client : models_[model_name]->clients_) - send(client, utility::event_atom_v, set_node_data_atom_v, path, data, role); + send( + client, + utility::event_atom_v, + set_node_data_atom_v, + model_name, + path, + data, + role, + uuid_role_data); + push_to_prefs(model_name); if (j.contains("uuid")) { @@ -179,16 +334,210 @@ void GlobalUIModelData::set_data( watcher, utility::event_atom_v, set_node_data_atom_v, + model_name, path, role, - data); + data, + uuid_role_data); } } } + } else if (changed) { + + // this was a bigger JSon push which could go down any number + // of levels in the tree, so do a full brute force update + push_to_prefs(model_name); + broadcast_whole_model_data(model_name); } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } catch ([[maybe_unused]] std::exception &e) { + // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void GlobalUIModelData::set_data( + const std::string &model_name, + const utility::Uuid &attribute_uuid, + const std::string &role, + const utility::JsonStore &data, + caf::actor setter) { + + try { + + check_model_is_registered(model_name); + + utility::JsonTree *model_data = &(models_[model_name]->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"); + } + 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; + if (j.contains(attr_uuid_role_name)) { + uuid_role_data = utility::Uuid(j[attr_uuid_role_name].get()); + } + } + } else if (role.empty()) { + j = data; + } + + if (changed) { + + std::string path = path_from_node(node); + + for (auto &client : models_[model_name]->clients_) { + if (client != setter) + send( + client, + utility::event_atom_v, + set_node_data_atom_v, + model_name, + path, + data, + role, + uuid_role_data); + } + + push_to_prefs(model_name); + + 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) { + // we don't notify the thing that is setting this data + // as it will update it's local data + if (watcher != setter) + send( + watcher, + utility::event_atom_v, + set_node_data_atom_v, + model_name, + path, + role, + data, + uuid_role_data); + } + } + } + } + + } catch ([[maybe_unused]] std::exception &e) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void GlobalUIModelData::insert_attribute_data_into_model( + const std::string &model_name, + const utility::Uuid &attribute_uuid, + const utility::JsonStore &attr_data, + const std::string &sort_role, + caf::actor client) { + + const utility::JsonStore attribute_data = attr_data; + 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 + bool already_a_client = false; + for (auto &c : p->second->clients_) { + if (c == client) { + already_a_client = true; + } + } + if (!already_a_client) { + p->second->clients_.push_back(client); + monitor(client); + } + + } else { + utility::JsonStore blank_model(nlohmann::json::parse(R"({ "children": [] })")); + 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( + 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"); + } + + const auto &d = parent_node->data(); + + bool full_push = false; + std::vector changed; + for (auto it = attribute_data.begin(); it != attribute_data.end(); it++) { + if (d.contains(it.key()) && d[it.key()] != it.value()) { + changed.push_back(it.key()); + } else if (not d.contains(it.key())) { + full_push = true; + } + } + + if (full_push) { + broadcast_whole_model_data(model_name); + } else { + for (const auto &c : changed) { + set_data(model_name, attribute_uuid, c, attribute_data[c], client); + } + } + + } catch ([[maybe_unused]] std::exception &e) { + // exception is thrown if we fail to find a match + 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); + } +} + +void GlobalUIModelData::remove_attribute_data_from_model( + const std::string &model_name, const utility::Uuid &attribute_uuid, caf::actor client) { + + try { + + check_model_is_registered(model_name); + + utility::JsonTree *model_data = &(models_[model_name]->data_); + auto &clients = models_[model_name]->clients_; + for (auto c = clients.begin(); c != clients.end(); ++c) { + if (*c == client) { + clients.erase(c); + break; + } + } + if (find_and_delete(model_data, attr_uuid_role_name, to_string(attribute_uuid))) { + broadcast_whole_model_data(model_name); + } + + } catch (...) { + throw; } } @@ -217,6 +566,7 @@ void GlobalUIModelData::register_model( client, utility::event_atom_v, model_data_atom_v, + model_name, model_data_as_json(model_name)); } @@ -229,7 +579,7 @@ void GlobalUIModelData::register_model( 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, data_from_prefs); + send(client, utility::event_atom_v, model_data_atom_v, model_name, data_from_prefs); monitor(client); } else { @@ -290,6 +640,8 @@ void GlobalUIModelData::insert_rows( throw std::runtime_error(ss.str().c_str()); } + const auto model_data_json = model_data_as_json(model_name); + 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 @@ -298,7 +650,8 @@ void GlobalUIModelData::insert_rows( client, utility::event_atom_v, model_data_atom_v, - model_data_as_json(model_name)); + model_name, + model_data_json); } } @@ -308,7 +661,11 @@ void GlobalUIModelData::insert_rows( } void GlobalUIModelData::remove_rows( - const std::string &model_name, const std::string &path, const int row, int count) { + const std::string &model_name, + const std::string &path, + const int row, + int count, + caf::actor requester) { try { @@ -322,12 +679,12 @@ void GlobalUIModelData::remove_rows( j->erase(std::next(j->begin(), row)); } - for (auto &client : models_[model_name]->clients_) - send( - client, - utility::event_atom_v, - model_data_atom_v, - model_data_as_json(model_name)); + const auto model_data_json = model_data_as_json(model_name); + 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); + } push_to_prefs(model_name); } catch (std::exception &e) { @@ -335,7 +692,23 @@ void GlobalUIModelData::remove_rows( } } -void GlobalUIModelData::push_to_prefs(const std::string &model_name) { +void GlobalUIModelData::remove_attribute_from_model( + const std::string &model_name, const utility::Uuid &attr_uuid) { + try { + + check_model_is_registered(model_name); + utility::JsonTree *model_root = &(models_[model_name]->data_); + + if (find_and_delete(model_root, attr_uuid_role_name, to_string(attr_uuid))) { + broadcast_whole_model_data(model_name); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void GlobalUIModelData::push_to_prefs(const std::string &model_name, const bool actually_push) { try { @@ -343,14 +716,21 @@ void GlobalUIModelData::push_to_prefs(const std::string &model_name) { if (models_[model_name]->preference_path_.empty()) return; + // if we haven't sheduled and update, mark as pending and send ourselves + // a delayed message to actually do the update. + // + // The reason is that we don't want to update the prefs store with + // every single change as if the user is dragging something in the UI + // that is stored in the prefs (like window/panel sizing) the prefs + // system will get overloaded with update messages if (!models_[model_name]->pending_prefs_update_) { models_[model_name]->pending_prefs_update_ = true; delayed_anon_send( caf::actor_cast(this), - std::chrono::seconds(10), + std::chrono::seconds(20), json_store::update_atom_v, model_name); - } else { + } else if (actually_push) { models_[model_name]->pending_prefs_update_ = false; auto prefs = global_store::GlobalStoreHelper(home_system()); @@ -391,32 +771,6 @@ void GlobalUIModelData::node_activated(const std::string &model_name, const std: } } -utility::JsonTree *add_node(utility::JsonTree *node, const nlohmann::json &new_entry_data) { - - auto p = node->insert(std::next(node->begin(), node->size()), new_entry_data); - return &(*p); -} - -utility::JsonTree *find_node_matching_string_field( - utility::JsonTree *data, const std::string &field_name, const std::string &field_value) { - 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 (r) - return r; - } catch (...) { - } - } - std::stringstream ss; - ss << "Failed to find field \"" << field_name << "\" with value matching \"" << field_value - << "\""; - throw std::runtime_error(ss.str().c_str()); - return nullptr; -} void GlobalUIModelData::insert_into_menu_model( const std::string &model_name, @@ -441,8 +795,11 @@ void GlobalUIModelData::insert_into_menu_model( try { 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"); + } parent_menus.erase(parent_menus.begin()); - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // exception is thrown if we fail to find a match break; } @@ -481,36 +838,13 @@ void GlobalUIModelData::insert_into_menu_model( watchers.push_back(watcher); } monitor(watcher); - - for (auto &client : models_[model_name]->clients_) { - send( - client, - utility::event_atom_v, - model_data_atom_v, - model_data_as_json(model_name)); - } + broadcast_whole_model_data(model_name); } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } -bool find_and_delete( - utility::JsonTree *data, const std::string &field, const std::string &field_value) { - if (data->data().contains(field) && data->data()[field].get() == field_value) { - - data->parent()->erase(std::next(data->parent()->begin(), data->index())); - return true; - - } else { - for (auto p = data->begin(); p != data->end(); ++p) { - if (find_and_delete(&(*p), field, field_value)) { - return true; - } - } - } - return false; -} void GlobalUIModelData::remove_node( const std::string &model_name, const utility::Uuid &model_item_id) { @@ -523,13 +857,7 @@ void GlobalUIModelData::remove_node( std::string uuid_string = to_string(model_item_id); std::string field("uuid"); if (find_and_delete(menu_model_data, field, uuid_string)) { - for (auto &client : models_[model_name]->clients_) { - send( - client, - utility::event_atom_v, - model_data_atom_v, - model_data_as_json(model_name)); - } + broadcast_whole_model_data(model_name); } } catch (std::exception &e) { @@ -538,3 +866,18 @@ void GlobalUIModelData::remove_node( } void GlobalUIModelData::on_exit() { system().registry().erase(global_ui_model_data_registry); } + +void GlobalUIModelData::broadcast_whole_model_data(const std::string &model_name) { + // sometimes when a model is being built by backend components like a Module + // that is setting up attribute data to be exposed in a model we could get + // many full broadcasts of the entire data in short succession (thanks to + // GlobalUIModelData::insert_attribute_data_into_model). Instead we put + // the model in a list waiting to be fully broadcasted and then do it once + // in 50ms. + if (models_to_be_fully_broadcasted_.find(model_name) == + models_to_be_fully_broadcasted_.end()) { + if (models_to_be_fully_broadcasted_.empty()) + delayed_anon_send(this, std::chrono::milliseconds(50), model_data_atom_v); + models_to_be_fully_broadcasted_.insert(model_name); + } +} diff --git a/src/ui/opengl/src/CMakeLists.txt b/src/ui/opengl/src/CMakeLists.txt index 096a98af1..3d3eaa1cb 100644 --- a/src/ui/opengl/src/CMakeLists.txt +++ b/src/ui/opengl/src/CMakeLists.txt @@ -3,23 +3,32 @@ SET(LINK_DEPS OpenGL::GL OpenGL::GLU GLEW::GLEW - pthread xstudio::ui::base + xstudio::ui::canvas xstudio::utility xstudio::media_reader OpenEXR::OpenEXR Imath::Imath ) +if(UNIX) + list(APPEND LINK_DEPS pthread) +endif() + 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}") -include_directories("${FREETYPE_INCLUDE_DIRS}") + 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 fad2c7d91..8435e456c 100644 --- a/src/ui/opengl/src/gl_debug_utils.cpp +++ b/src/ui/opengl/src/gl_debug_utils.cpp @@ -1,5 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __linux__ #include +#else +#include +#endif #include #include #include diff --git a/src/ui/opengl/src/opengl_canvas_renderer.cpp b/src/ui/opengl/src/opengl_canvas_renderer.cpp new file mode 100644 index 000000000..1a528c8c1 --- /dev/null +++ b/src/ui/opengl/src/opengl_canvas_renderer.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_canvas_renderer.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio; +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::opengl; + + +OpenGLCanvasRenderer::OpenGLCanvasRenderer() { + + stroke_renderer_.reset(new OpenGLStrokeRenderer()); + caption_renderer_.reset(new OpenGLCaptionRenderer()); +} + +void OpenGLCanvasRenderer::render_canvas( + const Canvas &canvas, + const HandleState &handle_state, + 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) { + + 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); +} diff --git a/src/ui/opengl/src/opengl_caption_renderer.cpp b/src/ui/opengl/src/opengl_caption_renderer.cpp new file mode 100644 index 000000000..3e3cf5c72 --- /dev/null +++ b/src/ui/opengl/src/opengl_caption_renderer.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_caption_renderer.hpp" + +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::opengl; + +namespace { + +const char *flat_color_vertex_shader = R"( + #version 430 core + uniform mat4 to_coord_system; + uniform mat4 to_canvas; + layout (location = 0) in vec2 aPos; + + void main() + { + // as simple as it gets. Do I actually need to do this to draw a + // filled triangle?? OpenGL 3.3+ + vec2 vertex_pos = aPos.xy; + gl_Position = vec4(vertex_pos,0.0,1.0)*to_coord_system*to_canvas; + } +)"; + +const char *flat_color_frag_shader = R"( + #version 330 core + out vec4 FragColor; + uniform vec3 brush_colour; + uniform float opacity; + void main(void) + { + FragColor = vec4( + brush_colour*opacity, + opacity + ); + } +)"; + +} // anonymous namespace + + +OpenGLCaptionRenderer::~OpenGLCaptionRenderer() { cleanup_gl(); } + +void OpenGLCaptionRenderer::init_gl() { + + auto font_files = Fonts::available_fonts(); + for (const auto &f : font_files) { + try { + auto font = new ui::opengl::OpenGLTextRendererSDF(f.second, 96); + text_renderers_[f.first].reset(font); + } catch (std::exception &e) { + spdlog::warn("Failed to load font: {}.", e.what()); + } + } + + texthandle_renderer_.reset(new ui::opengl::OpenGLTextHandleRenderer()); + + bg_shader_ = std::make_unique( + flat_color_vertex_shader, flat_color_frag_shader); + + if (!bg_vertex_buffer_) { + glGenBuffers(1, &bg_vertex_buffer_); + glGenVertexArrays(1, &bg_vertex_array_); + } +} + +void OpenGLCaptionRenderer::cleanup_gl() { + + if (bg_vertex_array_) { + glDeleteVertexArrays(1, &bg_vertex_array_); + bg_vertex_array_ = 0; + } + + if (bg_vertex_buffer_) { + glDeleteBuffers(1, &bg_vertex_buffer_); + bg_vertex_buffer_ = 0; + } +} + +void OpenGLCaptionRenderer::render_captions( + const std::vector &captions, + const HandleState &handle_state, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx) { + + if (!texthandle_renderer_) { + init_gl(); + } + + if (text_renderers_.empty()) { + return; + } + + for (const auto &caption : captions) { + + auto it = text_renderers_.find(caption.font_name); + auto text_renderer = + it == text_renderers_.end() ? text_renderers_.begin()->second : it->second; + + render_background( + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dx, + caption.background_colour, + caption.background_opacity, + caption.bounding_box()); + + text_renderer->render_text( + caption.vertices(), + transform_window_to_viewport_space, + transform_viewport_to_image_space, + caption.colour, + viewport_du_dx, + caption.font_size, + caption.opacity); + } + + texthandle_renderer_->render_handles( + handle_state, + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dx); +} + +void OpenGLCaptionRenderer::render_background( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const utility::ColourTriplet &background_colour, + const float background_opacity, + const Imath::Box2f &bounding_box) { + + if (!bounding_box.isEmpty() && background_opacity > 0.f) { + + // TBH I preferred glBegin(GL_TRIANGLES) !! + + std::array bg_verts = { + + Imath::V2f(bounding_box.min.x, bounding_box.min.y), + Imath::V2f(bounding_box.max.x, bounding_box.min.y), + Imath::V2f(bounding_box.max.x, bounding_box.max.y), + + Imath::V2f(bounding_box.max.x, bounding_box.max.y), + Imath::V2f(bounding_box.min.x, bounding_box.max.y), + Imath::V2f(bounding_box.min.x, bounding_box.min.y)}; + + glBindVertexArray(bg_vertex_array_); + glBindBuffer(GL_ARRAY_BUFFER, bg_vertex_buffer_); + glBufferData(GL_ARRAY_BUFFER, sizeof(bg_verts), bg_verts.data(), GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Imath::V2f), nullptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + utility::JsonStore shader_params; + + shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["brush_colour"] = background_colour; + shader_params["opacity"] = background_opacity; + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + + bg_shader_->use(); + bg_shader_->set_shader_parameters(shader_params); + + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + } +} \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_offscreen_renderer.cpp b/src/ui/opengl/src/opengl_offscreen_renderer.cpp new file mode 100644 index 000000000..96a6d9afb --- /dev/null +++ b/src/ui/opengl/src/opengl_offscreen_renderer.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_offscreen_renderer.hpp" + +using namespace xstudio; +using namespace xstudio::ui::opengl; + + +OpenGLOffscreenRenderer::OpenGLOffscreenRenderer(GLint color_format) + : color_format_(color_format) {} + +OpenGLOffscreenRenderer::~OpenGLOffscreenRenderer() { cleanup(); } + +void OpenGLOffscreenRenderer::resize(const Imath::V2f &dims) { + if (dims == fbo_dims_) { + return; + } + + cleanup(); + + fbo_dims_ = dims; + unsigned int w = dims.x; + unsigned int h = dims.y; + + glGenTextures(1, &tex_id_); + + glBindTexture(tex_target_, tex_id_); + glTexImage2D(tex_target_, 0, color_format_, w, h, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(tex_target_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(tex_target_, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glBindTexture(tex_target_, 0); + + glGenRenderbuffers(1, &rbo_id_); + glBindRenderbuffer(GL_RENDERBUFFER, rbo_id_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h); + + glGenFramebuffers(1, &fbo_id_); + glBindFramebuffer(GL_FRAMEBUFFER, fbo_id_); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex_target_, tex_id_, 0); + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo_id_); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void OpenGLOffscreenRenderer::begin() { + // Save viewport state + unsigned int w = fbo_dims_.x; + unsigned int h = fbo_dims_.y; + + glGetIntegerv(GL_VIEWPORT, vp_state_.data()); + glViewport(0, 0, w, h); + + glBindFramebuffer(GL_FRAMEBUFFER, fbo_id_); +} + +void OpenGLOffscreenRenderer::end() { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // Restore viewport state + glViewport(vp_state_[0], vp_state_[1], vp_state_[2], vp_state_[3]); +} + +void OpenGLOffscreenRenderer::cleanup() { + if (fbo_id_) { + glDeleteFramebuffers(1, &fbo_id_); + fbo_id_ = 0; + } + if (rbo_id_) { + glDeleteRenderbuffers(1, &rbo_id_); + rbo_id_ = 0; + } + if (tex_id_) { + glDeleteTextures(1, &tex_id_); + tex_id_ = 0; + } + + fbo_dims_ = Imath::V2f(0.0f, 0.0f); + vp_state_ = std::array{0, 0, 0, 0}; +} diff --git a/src/ui/opengl/src/opengl_stroke_renderer.cpp b/src/ui/opengl/src/opengl_stroke_renderer.cpp new file mode 100644 index 000000000..42c2cbcb9 --- /dev/null +++ b/src/ui/opengl/src/opengl_stroke_renderer.cpp @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_stroke_renderer.hpp" + +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 + uniform float z_adjust; + uniform float thickness; + uniform float soft_dim; + uniform mat4 to_coord_system; + uniform mat4 to_canvas; + 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; + 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; + } + } + + soft_edge = (do_soft_edge ? soft_dim : 0.00001f); + gl_Position = vec4(vtx,0.0,1.0)*to_coord_system*to_canvas; + gl_Position.z = (zz)*gl_Position.w; + frag_pos = vtx; + } +)"; + +const char *frag_shader = R"( + #version 330 core + flat in vec2 line_start; + flat in vec2 line_end; + in vec2 frag_pos; + out vec4 FragColor; + uniform vec3 brush_colour; + uniform float brush_opacity; + in float soft_edge; + uniform float thickness; + uniform bool do_soft_edge; + + float distToLine(vec2 pt) + { + + float l2 = (line_end.x - line_start.x)*(line_end.x - line_start.x) + + (line_end.y - line_start.y)*(line_end.y - line_start.y); + + if (l2 == 0.0) return length(pt-line_start); + + vec2 a = pt-line_start; + vec2 L = line_end-line_start; + + float dot = (a.x*L.x + a.y*L.y); + + float t = max(0.0, min(1.0, dot / l2)); + vec2 p = line_start + t*L; + return length(pt-p); + + } + + void main(void) + { + float r = distToLine(frag_pos); + + if (do_soft_edge) { + r = smoothstep( + thickness + soft_edge, + thickness, + r); + } else { + r = r < thickness ? 1.0f: 0.0f; + } + + if (r == 0.0f) discard; + if (do_soft_edge && r == 1.0f) { + discard; + } + float a = brush_opacity*r; + FragColor = vec4( + brush_colour*a, + a + ); + + } +)"; + +} // anonymous namespace + + +OpenGLStrokeRenderer::~OpenGLStrokeRenderer() { cleanup_gl(); } + +void OpenGLStrokeRenderer::init_gl() { + + if (!shader_) { + shader_ = std::make_unique(vertex_shader, frag_shader); + } + + if (!ssbo_id_) { + glCreateBuffers(1, &ssbo_id_); + } +} + +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_); + } +} + +void OpenGLStrokeRenderer::render_strokes( + const std::vector &strokes, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + 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; + for (const auto &stroke : strokes) { + auto vertices = stroke.vertices(); + strokes_vertices.insert(strokes_vertices.end(), vertices.begin(), vertices.end()); + } + + 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); + + // strokes are made up of partially overlapping triangles - as we + // draw with opacity we use depth test to stop overlapping triangles + // in the same stroke accumulating in the alpha blend + glEnable(GL_DEPTH_TEST); + glClearDepth(0.0); + glClear(GL_DEPTH_BUFFER_BIT); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + + utility::JsonStore shader_params; + shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["soft_dim"] = viewport_du_dx * 4.0f; + + shader_->use(); + shader_->set_shader_parameters(shader_params); + + utility::JsonStore shader_params2; + utility::JsonStore shader_params3; + shader_params3["do_soft_edge"] = true; + + GLint offset = 0; + float depth = 0.0f; + + if (do_erase_strokes_first) { + 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); + } + } + 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); + continue; + } + + /* ---- First pass, draw solid stroke ---- */ + + // strokes are self-overlapping - we can't accumulate colour on the same pixel from + // different segments of the same stroke, because if opacity is not 1.0 + // the strokes don't draw correctly so we must use depth-test to prevent + // this. + // Anti-aliasing the boundary is tricky as we don't want to put down + // anti-alised edge pixels where there will be solid pixels due to some + // other segment of the same stroke, or the depth test means we punch + // little holes in the solid bit with anti-aliased edges where there + // is self-overlapping + // Thus we draw solid filled stroke (not anti-aliased) and then we + // draw a slightly thicker stroke underneath (using depth test) and this + // thick stroke has a slightly soft (fuzzy) edge that achieves anti- + // aliasing. + + // It is not perfect because of the use of glBlendEquation(GL_MAX); + // lower down when plotting the soft edge - this is because even the + // soft edge plotting overlaps in an awkward way and you get bad artifacts + // if you try other strategies .... + // Drawing different, bright colours over each other where opacity is + // not 1.0 shows up a subtle but noticeable flourescent glow effect. + // Solutions on a postcard please! + + // so this prevents overlapping quads from same stroke accumulating together + glDepthFunc(GL_GREATER); + + if (stroke.type == StrokeType_Erase) { + glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); + } else { + glBlendEquation(GL_FUNC_ADD); + } + + // 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_->set_shader_parameters(shader_params2); + + // For each adjacent PAIR of points in a stroke, we draw a quad of + // the required thickness (rectangle) that connects them. We then draw a quad centered + // over every point in the stroke of width & height matching the line + // thickness to plot a circle that fills in the gaps left between the + // rectangles we have already joined, giving rounded start and end caps + // 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); + + /* ---- Second pass, draw soft edged stroke underneath ---- */ + + // Edge fragments have transparency and we want the most opaque fragment + // to be plotted, we achieve this by letting them all plot + glDepthFunc(GL_GEQUAL); + + if (stroke.type == StrokeType_Erase) { + // glBlendEquation(GL_MAX); + } else { + glBlendEquation(GL_MAX); + } + + 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); + + offset += (stroke.points.size() + 1); + } + + glBlendEquation(GL_FUNC_ADD); + glBindVertexArray(0); + + shader_->stop_using(); +} \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_text_rendering.cpp b/src/ui/opengl/src/opengl_text_rendering.cpp index a75d7eda8..9cf1aa08b 100644 --- a/src/ui/opengl/src/opengl_text_rendering.cpp +++ b/src/ui/opengl/src/opengl_text_rendering.cpp @@ -278,6 +278,7 @@ void OpenGLTextRendererSDF::render_text( glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBindTexture(GL_TEXTURE_RECTANGLE, texture_); + // update content of vbo_ memory glBindBuffer(GL_ARRAY_BUFFER, vbo_); @@ -473,8 +474,6 @@ void OpenGLTextRendererVector::render_text( glUniform1f(location2, y); { - - for (const auto &shape_details : character.negative_shapes_) { uint8_t *v = nullptr; @@ -503,9 +502,8 @@ void OpenGLTextRendererVector::render_text( glUniform1f(location, x); glUniform1f(location2, y); - { - + { for (const auto &shape_details : character.positive_shapes_) { uint8_t *v = nullptr; diff --git a/src/ui/opengl/src/opengl_texthandle_renderer.cpp b/src/ui/opengl/src/opengl_texthandle_renderer.cpp new file mode 100644 index 000000000..7a6035f70 --- /dev/null +++ b/src/ui/opengl/src/opengl_texthandle_renderer.cpp @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_texthandle_renderer.hpp" + +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::opengl; + + +namespace { + +const char *vertex_shader = R"( + #version 430 core + uniform mat4 to_coord_system; + uniform mat4 to_canvas; + uniform vec2 box_position; + uniform vec2 box_size; + uniform vec2 aa_nudge; + uniform float du_dx; + layout (location = 0) in vec2 aPos; + //layout (location = 1) in vec2 bPos; + out vec2 screen_pixel; + + void main() + { + + // 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 + vec2 vertex_pos = aPos.xy; + vertex_pos.x = vertex_pos.x*box_size.x; + vertex_pos.y = vertex_pos.y*box_size.y; + vertex_pos += box_position + aa_nudge*du_dx; + screen_pixel = vertex_pos/du_dx; + gl_Position = vec4(vertex_pos,0.0,1.0)*to_coord_system*to_canvas; + } +)"; + +const char *frag_shader = R"( + #version 330 core + out vec4 FragColor; + uniform bool shadow; + uniform int box_type; + uniform float opacity; + in vec2 screen_pixel; + void main(void) + { + ivec2 offset_screen_pixel = ivec2(screen_pixel) + ivec2(5000,5000); // move away from origin + if (box_type==1) { + // draws a dotted line + if (((offset_screen_pixel.x/20) & 1) == ((offset_screen_pixel.y/20) & 1)) { + FragColor = vec4(0.0f, 0.0f, 0.0f, opacity); + } else { + FragColor = vec4(1.0f, 1.0f, 1.0f, opacity); + } + } else if (box_type==2) { + FragColor = vec4(0.0f, 0.0f, 0.0f, opacity); + } else if (box_type==3) { + FragColor = vec4(0.7f, 0.7f, 0.7f, opacity); + } else { + FragColor = vec4(1.0f, 1.0f, 1.0f, opacity); + } + } +)"; + +static struct AAJitterTable { + + struct { + Imath::V2f operator()(int N, int i, int j) { + auto x = -0.5f + (i + 0.5f) / N; + auto y = -0.5f + (j + 0.5f) / N; + return {x, y}; + } + } gridLookup; + + AAJitterTable() { + aa_nudge.resize(16); + int lookup[16] = {11, 6, 10, 8, 9, 12, 7, 1, 3, 13, 5, 4, 2, 15, 0, 14}; + int ct = 0; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + aa_nudge[lookup[ct]]["aa_nudge"] = gridLookup(4, i, j); + ct++; + } + } + } + + std::vector aa_nudge; + +} aa_jitter_table; + +static std::array handles_vertices = { + // unit box for drawing boxes! + Imath::V2f(0.0f, 0.0f), + Imath::V2f(1.0f, 0.0f), + Imath::V2f(1.0f, 1.0f), + Imath::V2f(0.0f, 1.0f), + + // double headed arrow, vertical + Imath::V2f(0.5f, 0.0f), + Imath::V2f(0.5f, 1.0f), + + Imath::V2f(0.5f, 0.0f), + Imath::V2f(0.5f - 0.2f, 0.2f), + + Imath::V2f(0.5f, 0.0f), + Imath::V2f(0.5f + 0.2f, 0.2f), + + Imath::V2f(0.5f, 1.0f), + Imath::V2f(0.5f - 0.2f, 1.0f - 0.2f), + + Imath::V2f(0.5f, 1.0f), + Imath::V2f(0.5f + 0.2f, 1.0f - 0.2f), + + // double headed arrow, horizontal + Imath::V2f(0.0f, 0.5f), + Imath::V2f(1.0f, 0.5f), + + Imath::V2f(0.0f, 0.5f), + Imath::V2f(0.2f, 0.5f - 0.2f), + + Imath::V2f(0.0f, 0.5f), + Imath::V2f(0.2f, 0.5f + 0.2f), + + Imath::V2f(1.0f, 0.5f), + Imath::V2f(1.0f - 0.2f, 0.5f - 0.2f), + + Imath::V2f(1.0f, 0.5f), + Imath::V2f(1.0f - 0.2f, 0.5f + 0.2f), + + // crossed lines + Imath::V2f(0.2f, 0.2f), + Imath::V2f(0.8f, 0.8f), + Imath::V2f(0.8f, 0.2f), + Imath::V2f(0.2f, 0.8f), + +}; + +} // anonymous namespace + + +OpenGLTextHandleRenderer::~OpenGLTextHandleRenderer() { cleanup_gl(); } + +void OpenGLTextHandleRenderer::init_gl() { + + if (!shader_) { + shader_ = std::make_unique(vertex_shader, frag_shader); + } + + if (!handles_vertex_buffer_obj_ && !handles_vertex_array_) { + glGenBuffers(1, &handles_vertex_buffer_obj_); + glGenVertexArrays(1, &handles_vertex_array_); + + glBindVertexArray(handles_vertex_array_); + + glBindBuffer(GL_ARRAY_BUFFER, handles_vertex_buffer_obj_); + glBufferData( + GL_ARRAY_BUFFER, sizeof(handles_vertices), handles_vertices.data(), GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Imath::V2f), nullptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glBindVertexArray(0); + } +} + +void OpenGLTextHandleRenderer::cleanup_gl() { + + if (handles_vertex_array_) { + glDeleteVertexArrays(1, &handles_vertex_array_); + handles_vertex_array_ = 0; + } + + if (handles_vertex_buffer_obj_) { + glDeleteBuffers(1, &handles_vertex_buffer_obj_); + handles_vertex_buffer_obj_ = 0; + } +} + +void OpenGLTextHandleRenderer::render_handles( + const HandleState &handle_state, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx) { + + if (!shader_) + init_gl(); + + utility::JsonStore shader_params; + + shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["du_dx"] = viewport_du_dx; + shader_params["box_type"] = 0; + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + + shader_->use(); + shader_->set_shader_parameters(shader_params); + + utility::JsonStore shader_params2; + + if (!handle_state.current_caption_bdb.isEmpty()) { + + // draw the box around the current edited caption + shader_params2["box_position"] = handle_state.current_caption_bdb.min; + shader_params2["box_size"] = handle_state.current_caption_bdb.size(); + shader_params2["opacity"] = 0.6; + shader_params2["box_type"] = 1; + shader_params2["aa_nudge"] = Imath::V2f(0.0f, 0.0f); + + + shader_->set_shader_parameters(shader_params2); + glBindVertexArray(handles_vertex_array_); + glLineWidth(2.0f); + glDrawArrays(GL_LINE_LOOP, 0, 4); + + const auto handle_size = handle_state.handle_size * viewport_du_dx; + + // Draw the three + static const auto hndls = std::vector( + {HandleHoverState::HoveredOnMoveHandle, + HandleHoverState::HoveredOnResizeHandle, + HandleHoverState::HoveredOnDeleteHandle}); + + static const auto vtx_offsets = std::vector({4, 14, 24}); + static const auto vtx_counts = std::vector({20, 10, 4}); + + const auto positions = std::vector( + {handle_state.current_caption_bdb.min - handle_size, + handle_state.current_caption_bdb.max, + {handle_state.current_caption_bdb.max.x, + handle_state.current_caption_bdb.min.y - handle_size.y}}); + + shader_params2["box_size"] = handle_size; + + glBindVertexArray(handles_vertex_array_); + + // draw a grey box for each handle + shader_params2["opacity"] = 0.6f; + for (size_t i = 0; i < hndls.size(); ++i) { + shader_params2["box_position"] = positions[i]; + shader_params2["box_type"] = 2; + shader_->set_shader_parameters(shader_params2); + glDrawArrays(GL_QUADS, 0, 4); + } + + static const auto aa_jitter = std::vector( + {{-0.33f, -0.33f}, + {-0.0f, -0.33f}, + {0.33f, -0.33f}, + {-0.33f, 0.0f}, + {0.0f, 0.0f}, + {0.33f, 0.0f}, + {-0.33f, 0.33f}, + {0.0f, 0.33f}, + {0.33f, 0.33f}}); + + + shader_params2["box_size"] = handle_size * 0.8f; + // draw the lines for each handle + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + shader_params2["opacity"] = 1.0f / 16.0f; + for (size_t i = 0; i < hndls.size(); ++i) { + + shader_params2["box_position"] = positions[i] + 0.1f * handle_size; + shader_params2["box_type"] = handle_state.hover_state == hndls[i] ? 4 : 3; + + shader_->set_shader_parameters(shader_params2); + // plot 9 times with anti-aliasing jitter to get a better looking result + for (const auto &aa_nudge : aa_jitter_table.aa_nudge) { + shader_->set_shader_parameters(aa_nudge); + glDrawArrays(GL_LINES, vtx_offsets[i], vtx_counts[i]); + } + } + } + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if (!handle_state.under_mouse_caption_bdb.isEmpty()) { + + shader_params2["box_position"] = handle_state.under_mouse_caption_bdb.min; + shader_params2["box_size"] = handle_state.under_mouse_caption_bdb.size(); + shader_params2["opacity"] = 0.3; + shader_params2["box_type"] = 1; + + shader_->set_shader_parameters(shader_params2); + + glBindVertexArray(handles_vertex_array_); + + glLineWidth(2.0f); + glDrawArrays(GL_LINE_LOOP, 0, 4); + } + + if (handle_state.cursor_position[0] != Imath::V2f(0.0f, 0.0f)) { + + shader_params2["opacity"] = 0.6f; + shader_params2["box_position"] = handle_state.cursor_position[0]; + shader_params2["box_size"] = + handle_state.cursor_position[1] - handle_state.cursor_position[0]; + shader_params2["box_type"] = handle_state.cursor_blink_state ? 2 : 0; + shader_->set_shader_parameters(shader_params2); + glBindVertexArray(handles_vertex_array_); + glLineWidth(3.0f); + glDrawArrays(GL_LINE_LOOP, 0, 4); + } + + glBindVertexArray(0); +} diff --git a/src/ui/opengl/src/opengl_viewport_renderer.cpp b/src/ui/opengl/src/opengl_viewport_renderer.cpp index 5c3d85ec8..26d8f072a 100644 --- a/src/ui/opengl/src/opengl_viewport_renderer.cpp +++ b/src/ui/opengl/src/opengl_viewport_renderer.cpp @@ -51,6 +51,14 @@ void ColourPipeLutCollection::upload_luts( } } +void ColourPipeLutCollection::register_texture( + const std::vector &textures) { + + for (const auto &tex : textures) { + active_textures_[tex.name] = tex; + } +} + void ColourPipeLutCollection::bind_luts(GLShaderProgramPtr shader, int &tex_idx) { for (const auto &lut : active_luts_) { utility::JsonStore txshder_param(nlohmann::json{{lut->texture_name(), tex_idx}}); @@ -58,6 +66,19 @@ void ColourPipeLutCollection::bind_luts(GLShaderProgramPtr shader, int &tex_idx) shader->set_shader_parameters(txshder_param); tex_idx++; } + for (const auto &[name, tex] : active_textures_) { + glActiveTexture(GL_TEXTURE0 + tex_idx); + switch (tex.target) { + case colour_pipeline::ColourTextureTarget::TEXTURE_2D: { + glBindTexture(GL_TEXTURE_2D, tex.id); + } + } + + utility::JsonStore txshder_param(nlohmann::json{{tex.name, tex_idx}}); + shader->set_shader_parameters(txshder_param); + + tex_idx++; + } } OpenGLViewportRenderer::OpenGLViewportRenderer( @@ -67,17 +88,9 @@ OpenGLViewportRenderer::OpenGLViewportRenderer( viewport_index_(viewer_index) {} void OpenGLViewportRenderer::upload_image_and_colour_data( - std::vector next_images) { - + std::vector &next_images) { - if (!next_images.size()) { - if (onscreen_frame_) - onscreen_frame_.reset(); - active_shader_program_ = no_image_shader_program_; - return; - } - onscreen_frame_ = next_images.front(); colour_pipeline::ColourPipelineDataPtr colour_pipe_data = onscreen_frame_.colour_pipe_data_; if (!textures_.size()) @@ -101,6 +114,7 @@ void OpenGLViewportRenderer::upload_image_and_colour_data( 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_); } latest_colour_pipe_data_cacheid_ = colour_pipe_data->cache_id_; } @@ -211,11 +225,14 @@ void OpenGLViewportRenderer::set_prefs(const utility::JsonStore &prefs) { } void OpenGLViewportRenderer::render( - const std::vector &next_images, + 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 std::lock_guard mutex_locker(m); init(); @@ -235,7 +252,6 @@ void OpenGLViewportRenderer::render( // (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()); @@ -249,12 +265,33 @@ void OpenGLViewportRenderer::render( /* we do our own clear of the viewport */ clear_viewport_area(to_scene_matrix); - // 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); - 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, + viewport_du_dx, + onscreen_frame_); + } + + // 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. */ @@ -265,7 +302,7 @@ void OpenGLViewportRenderer::render( orf.second->render_opengl( to_scene_matrix, transform_viewport_to_image_space, - viewport_du_dx, + abs(viewport_du_dx), onscreen_frame_, has_alpha_); } @@ -298,7 +335,7 @@ void OpenGLViewportRenderer::render( } // coordinate system set-up - utility::JsonStore shader_params; + 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; @@ -316,20 +353,20 @@ void OpenGLViewportRenderer::render( // 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.0, - 1.0, + -1.0f, + 1.0f, 0.0f, 1.0f, - 1.0, - 1.0, + 1.0f, + 1.0f, 0.0f, 1.0f, - 1.0, - -1.0, + 1.0f, + -1.0f, 0.0f, 1.0f, - -1.0, - -1.0, + -1.0f, + -1.0f, 0.0f, 1.0f}; @@ -371,7 +408,7 @@ void OpenGLViewportRenderer::render( orf.second->render_opengl( to_scene_matrix, transform_viewport_to_image_space, - viewport_du_dx, + abs(viewport_du_dx), onscreen_frame_, has_alpha_); } @@ -420,7 +457,7 @@ bool OpenGLViewportRenderer::activate_shader( try { - std::vector shader_componenets; + std::vector shader_components; for (const auto &colour_op : colour_operations) { // sanity check - this should be impossible, though if (colour_op->shader_->graphics_api() != GraphicsAPI::OpenGL) { @@ -428,13 +465,13 @@ bool OpenGLViewportRenderer::activate_shader( "Non-OpenGL shader data in colour operation chain!"); } auto pr = static_cast(colour_op->shader_.get()); - shader_componenets.push_back(pr->shader_code()); + shader_components.push_back(pr->shader_code()); } programs_[shader_id].reset(new GLShaderProgram( default_vertex_shader, image_buffer_unpack_shader->shader_code(), - shader_componenets, + shader_components, use_ssbo_)); } catch (std::exception &e) { diff --git a/src/ui/opengl/src/shader_program_base.cpp b/src/ui/opengl/src/shader_program_base.cpp index 5f6dfb830..34e10c8ce 100644 --- a/src/ui/opengl/src/shader_program_base.cpp +++ b/src/ui/opengl/src/shader_program_base.cpp @@ -76,7 +76,10 @@ 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; + texPosition = vec2( + (rpos.x + 1.0f) * float(image_dims.x), + (rpos.y * pixel_aspect * float(image_dims.x)) + float(image_dims.y) + ) * 0.5f; } )"; @@ -94,6 +97,7 @@ uniform bool use_bilinear_filtering; uniform usampler2DRect the_tex; uniform ivec2 tex_dims; +uniform bool pack_rgb_10_bit; ivec2 step_sample(ivec2 tex_coord) { @@ -274,6 +278,31 @@ 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); @@ -291,8 +320,14 @@ 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; - FragColor = vec4(rgb_frag_value.rgb, 1.0); } } )"; @@ -306,6 +341,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_bilinear_filtering; @@ -466,6 +502,30 @@ 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); @@ -483,7 +543,14 @@ void main(void) } //INJECT_COLOUR_OPS_CALL - FragColor = vec4(rgb_frag_value.rgb, 1.0); + + 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; } } )"; @@ -611,10 +678,20 @@ GLShaderProgram::GLShaderProgram( } } +GLShaderProgram::~GLShaderProgram() { + // We don't need the program anymore. + glDeleteProgram(program_); + + // Always detach shaders after a successful link. + std::for_each(shaders_.begin(), shaders_.end(), [](GLuint shdr) { glDeleteShader(shdr); }); +} + + bool GLShaderProgram::is_colour_op_shader_source(const std::string &shader_code) const { // colour op shaders implement a specific signature function that we can look for. - static const std::regex op_func_match(R"(vec4\s*colour_transform_op\s*\(\s*vec4[^\)]+\))"); + static const std::regex op_func_match( + R"(vec4\s*colour_transform_op\s*\(\s*vec4[^\)]+\s*\,\s*vec2[^\)]+\s*\))"); std::smatch m; return std::regex_search(shader_code, m, op_func_match); } @@ -636,7 +713,7 @@ void GLShaderProgram::inject_colour_op_shader(const std::string &colour_op_shade std::stringstream transform_op_fwd_declaration; transform_op_fwd_declaration << "vec4 colour_transform_op" << colour_operation_index_ - << "(vec4 rgba);\n"; + << "(vec4 rgba, vec2 image_pos);\n"; // search the frag shaders for the injection points for colour op shaders for (auto &frag_shader : fragment_shaders_) { @@ -648,7 +725,7 @@ void GLShaderProgram::inject_colour_op_shader(const std::string &colour_op_shade if (j != std::string::npos) { std::stringstream transform_op_call; transform_op_call << "\t\trgb_frag_value = " << renamed_transform_op.str() - << "(rgb_frag_value);\n"; + << "(rgb_frag_value, texPosition/image_dims);\n"; frag_shader.insert(j, transform_op_call.str()); } } @@ -668,36 +745,34 @@ void GLShaderProgram::compile() { // Get a program object. program_ = glCreateProgram(); - std::vector shaders; - try { // compile the vertex shader objects std::for_each( vertex_shaders_.begin(), vertex_shaders_.end(), - [&shaders](const std::string &shader_code) { - shaders.push_back(compile_vertex_shader(shader_code)); + [=](const std::string &shader_code) { + shaders_.push_back(compile_vertex_shader(shader_code)); }); // compile the fragment shader objects std::for_each( fragment_shaders_.begin(), fragment_shaders_.end(), - [&shaders](const std::string &shader_code) { - shaders.push_back(compile_frag_shader(shader_code)); + [=](const std::string &shader_code) { + shaders_.push_back(compile_frag_shader(shader_code)); }); } catch (...) { // a shader hasn't compiled ... delete anthing that did compile std::for_each( - shaders.begin(), shaders.end(), [](GLuint shdr) { glDeleteShader(shdr); }); + shaders_.begin(), shaders_.end(), [](GLuint shdr) { glDeleteShader(shdr); }); throw; } // attach the shaders to the program - std::for_each(shaders.begin(), shaders.end(), [&](GLuint shader_id) { + std::for_each(shaders_.begin(), shaders_.end(), [&](GLuint shader_id) { glAttachShader(program_, shader_id); }); @@ -720,7 +795,9 @@ void GLShaderProgram::compile() { // Always detach shaders after a successful link. std::for_each( - shaders.begin(), shaders.end(), [](GLuint shdr) { glDeleteShader(shdr); }); + shaders_.begin(), shaders_.end(), [](GLuint shdr) { glDeleteShader(shdr); }); + + shaders_.clear(); // Use the infoLog as you see fit. std::stringstream e; @@ -730,7 +807,7 @@ void GLShaderProgram::compile() { // Always detach shaders after a successful link. std::for_each( - shaders.begin(), shaders.end(), [&](GLuint shdr) { glDetachShader(program_, shdr); }); + shaders_.begin(), shaders_.end(), [&](GLuint shdr) { glDetachShader(program_, shdr); }); } void GLShaderProgram::use() const { glUseProgram(program_); } diff --git a/src/ui/opengl/src/texture.cpp b/src/ui/opengl/src/texture.cpp index 3a52798b7..1ffb77ffd 100644 --- a/src/ui/opengl/src/texture.cpp +++ b/src/ui/opengl/src/texture.cpp @@ -31,10 +31,14 @@ class DebugTimer { } // namespace void GLBlindTex::release() { - mutex_.unlock(); + // if linux + // mutex_.unlock(); + // endif when_last_used_ = utility::clock::now(); } +GLBlindTex::~GLBlindTex() {} + GLDoubleBufferedTexture::GLDoubleBufferedTexture() { if (using_ssbo_) { @@ -134,9 +138,13 @@ void GLDoubleBufferedTexture::upload_next( void GLDoubleBufferedTexture::release() { current_->release(); } GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { - // ensure no copying is in flight + // 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) { @@ -215,19 +223,23 @@ void GLBlindRGBA8bitTex::resize(const size_t required_size_bytes) { void GLBlindRGBA8bitTex::start_pixel_upload() { - if (new_source_frame_) { - if (upload_thread_.joinable()) - upload_thread_.join(); - mutex_.lock(); - upload_thread_ = std::thread(&GLBlindRGBA8bitTex::pixel_upload, this); - } + // 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()) { - mutex_.unlock(); + // if linux + // mutex_.unlock(); + // endif return; } @@ -251,15 +263,18 @@ void GLBlindRGBA8bitTex::pixel_upload() { if (t.joinable()) t.join(); } - mutex_.unlock(); + + // 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(); + // mutex_.lock(); + // std::lock_guard lock(mutex_); new_source_frame_ = frame; media_key_ = frame->media_key(); @@ -277,15 +292,18 @@ void GLBlindRGBA8bitTex::map_buffer_for_upload(media_reader::ImageBufPtr &frame) buffer_io_ptr_ = (uint8_t *)glMapNamedBuffer(pixel_buf_object_id_, GL_WRITE_ONLY); } - - mutex_.unlock(); + // 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(); + // mutex_.lock(); + // std::unique_lock lck(mutex_); dims.x = tex_width_; dims.y = tex_height_; @@ -293,9 +311,9 @@ void GLBlindRGBA8bitTex::bind(int tex_index, Imath::V2i &dims) { if (new_source_frame_) { if (new_source_frame_->size()) { - if (upload_thread_.joinable()) { - upload_thread_.join(); - } + // if (upload_thread_.joinable()) { + // upload_thread_.join(); + // } // now the texture data is transferred (on the GPU). // Assumption is that this is fast. @@ -480,6 +498,11 @@ GLColourLutTexture::GLColourLutTexture( 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; @@ -567,6 +590,7 @@ GLSsboTex::GLSsboTex() { glGenBuffers(1, &ssbo_id_); } GLSsboTex::~GLSsboTex() { if (upload_thread_.joinable()) upload_thread_.join(); + glDeleteBuffers(1, &ssbo_id_); } @@ -672,4 +696,4 @@ void GLSsboTex::pixel_upload() { } mutex_.unlock(); -} \ No newline at end of file +} diff --git a/src/ui/qml/CMakeLists.txt b/src/ui/qml/CMakeLists.txt index 6bd7782dd..0554a4823 100644 --- a/src/ui/qml/CMakeLists.txt +++ b/src/ui/qml/CMakeLists.txt @@ -8,19 +8,15 @@ set(CMAKE_AUTORCC ON) find_package(Qt5 COMPONENTS Core Quick Gui Widgets OpenGL Test Concurrent REQUIRED) configure_file(.clang-tidy .clang-tidy) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic") -set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") - +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() # QT5_ADD_RESOURCES(PROTOTYPE_RCS) # if (Qt5_POSITION_INDEPENDENT_CODE) # SET(CMAKE_POSITION_INDEPENDENT_CODE ON) # endif() -# add_src_and_test(contact_sheet) -# add_src_and_test(playlist) -# add_src_and_test(subset) -# add_src_and_test(timeline) - add_src_and_test(bookmark) add_src_and_test(embedded_python) add_src_and_test(event) @@ -31,7 +27,6 @@ add_src_and_test(log) # add_src_and_test(media) add_src_and_test(module) add_src_and_test(playhead) -add_src_and_test(quickfuture/src) add_src_and_test(session) add_src_and_test(studio) add_src_and_test(tag) diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index 25379ec13..ff3d3367b 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -81,13 +81,20 @@ bool BookmarkFilterModel::filterAcceptsRow( bool result = true; QModelIndex index = sourceModel()->index(source_row, 0, source_parent); - auto owner = sourceModel()->data(index, BookmarkModel::Roles::ownerRole).toString(); + auto visible = sourceModel()->data(index, BookmarkModel::Roles::visibleRole).toBool(); + + if (not visible) + return false; + + auto owner = sourceModel()->data(index, BookmarkModel::Roles::ownerRole).toString(); if (StdFromQString(index.data(BookmarkModel::Roles::startTimecodeRole).toString()) == "--:--:--:--") return false; switch (depth_) { + case 3: + break; case 2: case 1: result = media_order_.contains(owner); @@ -176,7 +183,8 @@ BookmarkModel::BookmarkModel(QObject *parent) : super(parent) { "objectRole", "startRole", "durationRole", - "durationFrameRole"})); + "durationFrameRole", + "visibleRole"})); } // don't optimise yet. @@ -191,19 +199,16 @@ QFuture BookmarkModel::getJSONFuture(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, - StdFromQString(path)); + *sys, bookmark_actor_, json_store::get_json_atom_v, addr, path_string); return QStringFromStd(result.dump()); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return QString(); // QStringFromStd(err.what()); } @@ -230,9 +235,7 @@ void BookmarkModel::init(caf::actor_system &_system) { // spdlog::warn("bookmark::bookmark_change_atom {}", to_string(ua.uuid()) ); auto ind = search_recursive(QUuidFromUuid(ua.uuid()), "uuidRole"); - if (not ind.isValid()) { - spdlog::warn("new bookmark ??"); - } else { + if (ind.isValid()) { try { auto detail = getDetail(ua.actor()); @@ -323,7 +326,7 @@ void BookmarkModel::setBookmarkActorAddr(const QString &addr) { try { request_receive( *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { } backend_events_ = caf::actor(); } @@ -451,6 +454,11 @@ QVariant BookmarkModel::data(const QModelIndex &index, int role) const { result = QVariant::fromValue(QUuidFromUuid(detail.uuid_)); break; + case visibleRole: + result = QVariant::fromValue(*(detail.visible_)); + break; + + case enabledRole: result = QVariant::fromValue(*(detail.enabled_)); break; diff --git a/src/ui/qml/bookmark/src/include/bookmark_qml_export.h b/src/ui/qml/bookmark/src/include/bookmark_qml_export.h new file mode 100644 index 000000000..cf2dd4ade --- /dev/null +++ b/src/ui/qml/bookmark/src/include/bookmark_qml_export.h @@ -0,0 +1,42 @@ + +#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/embedded_python/src/CMakeLists.txt b/src/ui/qml/embedded_python/src/CMakeLists.txt index 50eeed001..3fd46c74c 100644 --- a/src/ui/qml/embedded_python/src/CMakeLists.txt +++ b/src/ui/qml/embedded_python/src/CMakeLists.txt @@ -3,6 +3,7 @@ SET(LINK_DEPS Qt5::Core xstudio::ui::qml::helper xstudio::utility + xstudio::global_store ) SET(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 a23a0766f..54a8a54e2 100644 --- a/src/ui/qml/embedded_python/src/embedded_python_ui.cpp +++ b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp @@ -37,7 +37,7 @@ void EmbeddedPythonUI::set_backend(caf::actor backend) { try { request_receive( *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } backend_events_ = caf::actor(); @@ -120,11 +120,12 @@ bool EmbeddedPythonUI::sendInput(const QString &str) { try { waiting_ = true; emit waitingChanged(); + std::string input_string = StdFromQString(str); sys->anon_send( backend_, embedded_python::python_session_input_atom_v, event_uuid_, - StdFromQString(str)); + input_string); return true; } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -223,6 +224,7 @@ 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)); } diff --git a/src/ui/qml/embedded_python/src/export.h b/src/ui/qml/embedded_python/src/export.h new file mode 100644 index 000000000..900fd2778 --- /dev/null +++ b/src/ui/qml/embedded_python/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..900fd2778 --- /dev/null +++ b/src/ui/qml/embedded_python/src/include/embedded_python_qml_export.h @@ -0,0 +1,42 @@ + +#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/export.h b/src/ui/qml/event/src/export.h new file mode 100644 index 000000000..3432c93fc --- /dev/null +++ b/src/ui/qml/event/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..3432c93fc --- /dev/null +++ b/src/ui/qml/event/src/include/event_qml_export.h @@ -0,0 +1,42 @@ + +#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/export.h b/src/ui/qml/global_store/src/export.h new file mode 100644 index 000000000..a13120dfd --- /dev/null +++ b/src/ui/qml/global_store/src/export.h @@ -0,0 +1,42 @@ + +#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 4aff43c1d..a92d186eb 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 @@ -201,13 +201,13 @@ bool GlobalStoreModel::updateProperty( // convert to internal representation. nlohmann::json GlobalStoreModel::storeToTree(const nlohmann::json &src) { - auto result = R"([])"_json; + auto result = R"([])"_json; for (const auto &[k, v] : src.items()) { if (v.count("datatype")) { // spdlog::warn("{}", v.dump(2)); result.push_back(v); - } else { + } else if (v.is_object()) { auto item = R"({})"_json; item["path"] = k; item["children"] = storeToTree(v); @@ -218,6 +218,7 @@ nlohmann::json GlobalStoreModel::storeToTree(const nlohmann::json &src) { return result; } + QVariant GlobalStoreModel::data(const QModelIndex &index, int role) const { auto result = QVariant(); 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 new file mode 100644 index 000000000..a13120dfd --- /dev/null +++ b/src/ui/qml/global_store/src/include/global_store_qml_export.h @@ -0,0 +1,42 @@ + +#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 c56378357..d0a4e6e07 100644 --- a/src/ui/qml/helper/src/CMakeLists.txt +++ b/src/ui/qml/helper/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET(LINK_DEPS ${CAF_LIBRARY_core} Qt5::Core Qt5::Qml + Qt5::Gui xstudio::utility ) @@ -10,6 +11,8 @@ SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/shotgun_provider_ui.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" ) create_qml_component(helper 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/helper/src/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index 0d68b3c07..d5c1ac272 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -15,6 +15,10 @@ using namespace xstudio::ui::qml; #include #include +QMLActor::QMLActor(QObject *parent) : super(parent) {} + +QMLActor::~QMLActor() {} + CafSystemObject::CafSystemObject(QObject *parent, caf::actor_system &sys) : QObject(parent), system_ref_(sys) { setObjectName("CafSystemObject"); @@ -48,7 +52,8 @@ QString xstudio::ui::qml::actorToQString(actor_system &sys, const caf::actor &ac } caf::actor xstudio::ui::qml::actorFromQString(actor_system &sys, const QString &addr_str) { - return actorFromString(sys, StdFromQString(addr_str)); + std::string addr = StdFromQString(addr_str); + return actorFromString(sys, addr); } @@ -82,11 +87,14 @@ QString xstudio::ui::qml::getThumbnailURL( auto mp = utility::request_receive( *sys, actor, media::get_media_pointer_atom_v, media::MT_IMAGE, frame); + auto mhash = utility::request_receive>( + *sys, actor, media::checksum_atom_v); + 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)); - } catch (const std::exception &err) { + hash = std::hash{}(static_cast( + display_transform_hash + mhash.first + std::to_string(mhash.second))); + } catch ([[maybe_unused]] const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -99,7 +107,7 @@ QString xstudio::ui::qml::getThumbnailURL( (cache_to_disk ? "1" : "0"), hash)); thumburl = QStringFromStd(thumbstr); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } diff --git a/src/ui/qml/helper/src/include/helper_qml_export.h b/src/ui/qml/helper/src/include/helper_qml_export.h new file mode 100644 index 000000000..8f856285f --- /dev/null +++ b/src/ui/qml/helper/src/include/helper_qml_export.h @@ -0,0 +1,42 @@ + +#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 1c203db85..3197701a0 100644 --- a/src/ui/qml/helper/src/json_tree_model_ui.cpp +++ b/src/ui/qml/helper/src/json_tree_model_ui.cpp @@ -47,8 +47,8 @@ nlohmann::json JSONTreeModel::modelData() const { return tree_to_json(data_, children_); } -nlohmann::json JSONTreeModel::indexToFullData(const QModelIndex &index) const { - return tree_to_json(*indexToTree(index), children_); +nlohmann::json JSONTreeModel::indexToFullData(const QModelIndex &index, const int depth) const { + return tree_to_json(*indexToTree(index), children_, depth); } nlohmann::json &JSONTreeModel::indexToData(const QModelIndex &index) { @@ -267,6 +267,22 @@ int JSONTreeModel::countExpandedChildren( return count; } +bool JSONTreeModel::canFetchMore(const QModelIndex &parent) const { + auto result = false; + + try { + if (parent.isValid()) { + const auto &jsn = indexToData(parent); + if (jsn.count(children_) and jsn.at(children_).is_null()) + result = true; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + bool JSONTreeModel::hasChildren(const QModelIndex &parent) const { auto result = false; @@ -443,11 +459,26 @@ bool JSONTreeModel::setData(const QModelIndex &index, const QVariant &value, int auto new_node = json_to_tree(jval, children_); auto old_node = indexToTree(index); // remove old children - old_node->clear(); + + if (old_node->size()) { + emit beginRemoveRows(index, 0, old_node->size() - 1); + old_node->clear(); + emit endRemoveRows(); + } + // replace data.. old_node->data() = new_node.data(); // copy children - old_node->splice(old_node->end(), new_node.base()); + // this doesn't work.. + // need to invalidate/add surplus rows. + if (new_node.size()) { + emit beginInsertRows(index, 0, new_node.size() - 1); + + old_node->splice(old_node->end(), new_node.base()); + + emit endInsertRows(); + } + result = true; roles.clear(); @@ -596,7 +627,12 @@ bool JSONTreeModel::moveRows( // dest // ); } else { - spdlog::warn("{} invalid move", __PRETTY_FUNCTION__); + spdlog::warn( + "{} invalid move: f {} l {} d {}", + __PRETTY_FUNCTION__, + moveFirst, + moveLast, + dest); } @@ -752,8 +788,11 @@ bool JSONTreeFilterModel::filterAcceptsRow( if (not v.isNull()) { try { auto qv = sourceModel()->data(index, k); - if (v != qv) - return false; + if (v.userType() == QMetaType::Bool) + if (v.toBool() != qv.toBool()) + return false; + else if (v != qv) + return false; } catch (...) { } } diff --git a/src/ui/qml/helper/src/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 30bb1be25..0f024fe36 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -53,14 +53,12 @@ UIModelData::UIModelData(QObject *parent) : super(parent) { void UIModelData::setModelDataName(QString name) { - if (model_name_ != StdFromQString(name)) { + std::string m_name = StdFromQString(name); + if (model_name_ != m_name) { - model_name_ = StdFromQString(name); + model_name_ = m_name; - caf::scoped_actor sys{self()->home_system()}; - - auto data = request_receive( - *sys, + anon_send( central_models_data_actor_, ui::model_data::register_model_data_atom_v, model_name_, @@ -68,7 +66,6 @@ void UIModelData::setModelDataName(QString name) { as_actor()); // process app/user.. - setModelData(data); emit modelDataNameChanged(); } } @@ -79,7 +76,7 @@ void UIModelData::init(caf::actor_system &system) { self()->set_default_handler(caf::drop); try { - utility::print_on_create(as_actor(), "SessionModel"); + utility::print_on_create(as_actor(), "UIModelData"); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -89,6 +86,7 @@ void UIModelData::init(caf::actor_system &system) { [=](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 { @@ -99,14 +97,21 @@ void UIModelData::init(caf::actor_system &system) { emit dataChanged(idx, idx, QVector()); } catch (std::exception &e) { spdlog::warn( - "{} {} : {} {}", __PRETTY_FUNCTION__, e.what(), path, data.dump()); + "{} {} : {} {} {}", + __PRETTY_FUNCTION__, + e.what(), + path, + data.dump(), + path); } }, [=](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 std::string role, + const utility::Uuid &uuid) { try { QModelIndex idx = getPathIndex(nlohmann::json::json_pointer(path)); @@ -115,26 +120,58 @@ void UIModelData::init(caf::actor_system &system) { j[role] = data; for (size_t i = 0; i < role_names_.size(); ++i) { if (role_names_[i] == role) { - emit dataChanged(idx, idx, QVector({Roles::LASTROLE + i})); + + emit dataChanged( + idx, + idx, + QVector({static_cast( + Roles::LASTROLE + static_cast(i))})); + break; } } } } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + if (!length()) { + // we have no data - Let's say we are exposing the model + // called 'foo'. If the backend object that wants to + // expose itself in 'foo' hasn't got around to pushing + // its data to central_models_data_actor_ against the + // 'foo' model ID, but we have created the UI model data + // access thing and said we want the 'foo' model data + // then we can be in this situation. Do a force fetch + // to ensure we are updated now. + caf::scoped_actor sys{self()->home_system()}; + 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()); + + // 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); + } } }, [=](utility::event_atom, xstudio::ui::model_data::model_data_atom, + const std::string model_name, const utility::JsonStore &data) { setModelData(data); }}; }); } bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int role) { - bool result = JSONTreeModel::setData(index, value, role); - + bool result = false; try { auto path = getIndexPath(index).to_string(); @@ -147,6 +184,9 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r QJsonDocument::fromVariant(value.value().toVariant()) .toJson(QJsonDocument::Compact) .constData()); + } else if (std::string(value.typeName()) == "QString") { + std::string value_string = StdFromQString(value.toString()); + j = nlohmann::json::parse(value_string); } else { j = nlohmann::json::parse(QJsonDocument::fromVariant(value) .toJson(QJsonDocument::Compact) @@ -163,6 +203,8 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r } else { + result = JSONTreeModel::setData(index, value, role); + auto id = role - Roles::LASTROLE; if (id >= 0 and id < static_cast(role_names_.size())) { auto field = role_names_.at(id); @@ -208,6 +250,33 @@ bool UIModelData::removeRows(int row, int count, const QModelIndex &parent) { return result; } +bool UIModelData::removeRowsSync(int row, int count, const QModelIndex &parent) { + + auto result = false; + + 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); + + result = JSONTreeModel::removeRows(row, count, parent); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + result = false; + } + return result; +} + + bool UIModelData::moveRows( const QModelIndex &sourceParent, int sourceRow, @@ -353,7 +422,7 @@ MenusModelData::MenusModelData(QObject *parent) : UIModelData(parent) { ViewsModelData::ViewsModelData(QObject *parent) : UIModelData(parent) { - setRoleNames(std::vector{"view_name", "view_qml_path"}); + setRoleNames(std::vector{"view_name", "view_qml_source"}); setModelDataName("view widgets"); } @@ -362,10 +431,18 @@ void ViewsModelData::register_view(QString qml_path, QString view_name) { auto rc = rowCount(index(-1, -1)); // QModelIndex()); insertRowsSync(rc, 1, index(-1, -1)); QModelIndex view_reg_index = index(rc, 0, index(-1, -1)); - set(view_reg_index, QVariant(view_name), QString("view_name")); - set(view_reg_index, QVariant(qml_path), QString("view_qml_path")); + 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")); } +QVariant ViewsModelData::view_qml_source(QString view_name) { + + QModelIndex idx = search(QVariant(view_name), "view_name"); + if (idx.isValid()) { + return get(idx, "view_qml_source"); + } + return QVariant(); +} ReskinPanelsModel::ReskinPanelsModel(QObject *parent) : UIModelData( @@ -373,6 +450,125 @@ ReskinPanelsModel::ReskinPanelsModel(QObject *parent) std::string("reskin panels model"), std::string("/ui/qml/reskin_windows_and_panels_model")) {} +void ReskinPanelsModel::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 + // Splitters can have splitters as children, meaning you can subdivide + // the xSTUDIO interface many times with really flexible panel arrangements. + // When you want to close a panel, the json tree data that backs the + // arrangement needs to be reconfigured carefully to get the expected + // behaviour .... + + // how many siblings including the panel we are about to delete? + const int siblings = rowCount(panel_index.parent()); + + 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) { + QList divs = dividers.toList(); + divs.removeAt(panel_index.row() ? panel_index.row() - 1 : 0); + std::ignore = set(panel_index.parent(), divs, "child_dividers"); + } + + } else if (siblings == 2) { + + QModelIndex parentNode = panel_index.parent(); + + // 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)); + + // now we wipe out the parent splitter with the 'other' panel + setData(parentNode, QVariantMapFromJson(other_panel_data), Roles::JSONRole); + + } else { + + // do nothing if there is only one panel at this index - we can't + // collapse a panel that isn't part of a split panel + } +} + +void ReskinPanelsModel::split_panel(QModelIndex panel_index, bool horizontal_split) { + + QModelIndex parentNode = panel_index.parent(); + const int insertion_row = panel_index.row(); + + QVariant h = get(parentNode, "split_horizontal"); + if (!h.isNull() && h.canConvert(QMetaType::Bool) && h.toBool() == horizontal_split) { + + // parent splitter is of type matching the type of split we want + // so we need to insert a new panel. + + // we need to reset the divider positions for the parent splitter + // now it has more children + int num_dividers = rowCount(parentNode); + QList divider_positions; + for (int i = 0; i < num_dividers; ++i) { + divider_positions.push_back(float(i + 1) / float(num_dividers + 1)); + } + 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"}])"); + + insertRowsSync(insertion_row, 1, parentNode); + setData(index(insertion_row, 0, parentNode), QVariantMapFromJson(j), Roles::JSONRole); + + + } else { + + nlohmann::json current_panel_data = indexToFullData(panel_index); + + // 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 + + // 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; + + // this is the new panel + nlohmann::json new_child; + new_child["current_tab"] = 0; + new_child["children"] = nlohmann::json::parse(R"([{"tab_view" : "Playlists"}])"); + + // 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); + + // wipe out the existing panel with the new splitter and its children + setData(panel_index, QVariantMapFromJson(j), Roles::JSONRole); + } +} + +void ReskinPanelsModel::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); +} + + +MediaListColumnsModel::MediaListColumnsModel(QObject *parent) + : UIModelData( + parent, + std::string("media list columns model"), + std::string("/ui/qml/media_list_columns_config")) {} MenuModelItem::MenuModelItem(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); @@ -384,10 +580,11 @@ MenuModelItem::~MenuModelItem() { 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, - StdFromQString(menu_name_), + menu_name_string, model_entry_id_); } } @@ -410,9 +607,11 @@ void MenuModelItem::init(caf::actor_system &system) { const std::string path) { emit activated(); }, [=](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::JsonStore &data, + const utility::Uuid &menu_item_uuid) { dont_update_model_ = true; if (role == "is_checked" && data.is_boolean()) { setIsChecked(data.get()); @@ -458,11 +657,17 @@ void MenuModelItem::insertIntoMenuModel() { global_ui_model_data_registry); utility::JsonStore menu_item_data; - menu_item_data["name"] = StdFromQString(text_); - if (!hotkey_.isEmpty()) - menu_item_data["hotkey"] = StdFromQString(hotkey_); - if (!current_choice_.isEmpty()) - menu_item_data["current_choice"] = StdFromQString(current_choice_); + 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; + } + if (!choices_.empty()) { auto choices = nlohmann::json::parse("[]"); for (const auto &c : choices_) { @@ -474,9 +679,11 @@ void MenuModelItem::insertIntoMenuModel() { menu_item_data["is_checked"] = is_checked_; } menu_item_data["menu_item_position"] = menu_item_position_; - menu_item_data["menu_item_type"] = StdFromQString(menu_item_type_); + std::string menu_item_type_string = StdFromQString(menu_item_type_); + menu_item_data["menu_item_type"] = menu_item_type_string; menu_item_data["uuid"] = model_entry_id_; + anon_send( central_models_data_actor, ui::model_data::insert_or_update_menu_node_atom_v, diff --git a/src/ui/qml/helper/src/model_helper_ui.cpp b/src/ui/qml/helper/src/model_helper_ui.cpp index 1538f6a9e..4c1736cae 100644 --- a/src/ui/qml/helper/src/model_helper_ui.cpp +++ b/src/ui/qml/helper/src/model_helper_ui.cpp @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/utility/helpers.hpp" @@ -6,6 +7,76 @@ using namespace xstudio::ui::qml; #include #include +void ModelRowCount::setCount(const int count) { + if (count != count_) { + count_ = count; + emit countChanged(); + } +} + +void ModelRowCount::inserted(const QModelIndex &parent, int first, int last) { + if (index_.isValid() and parent == index_) { + setCount(count_ + ((last - first) + 1)); + } +} + +void ModelRowCount::moved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow) { + if (index_.isValid()) { + if (sourceParent == destinationParent) { + } else if (sourceParent == index_) { + setCount(count_ - ((sourceEnd - sourceStart) + 1)); + } else if (destinationParent == index_) { + setCount(count_ + ((sourceEnd - sourceStart) + 1)); + } + } +} + +void ModelRowCount::removed(const QModelIndex &parent, int first, int last) { + if (index_.isValid() and parent == index_) { + setCount(count_ - ((last - first) + 1)); + } +} + + +void ModelRowCount::setIndex(const QModelIndex &index) { + if (index.isValid()) { + if (index_.isValid()) { + disconnect( + index_.model(), + &QAbstractItemModel::rowsRemoved, + this, + &ModelRowCount::removed); + disconnect( + index_.model(), + &QAbstractItemModel::rowsInserted, + this, + &ModelRowCount::inserted); + disconnect( + index_.model(), &QAbstractItemModel::rowsMoved, this, &ModelRowCount::moved); + } + + connect(index.model(), &QAbstractItemModel::rowsRemoved, this, &ModelRowCount::removed); + connect( + index.model(), &QAbstractItemModel::rowsInserted, this, &ModelRowCount::inserted); + connect(index.model(), &QAbstractItemModel::rowsMoved, this, &ModelRowCount::moved); + + index_ = QPersistentModelIndex(index); + emit indexChanged(); + + setCount(index_.model()->rowCount(index_)); + } else { + index_ = QPersistentModelIndex(index); + emit indexChanged(); + setCount(0); + } +} + + void ModelProperty::setIndex(const QModelIndex &index) { if (index.isValid()) { if (index_.isValid()) @@ -148,6 +219,22 @@ void ModelPropertyMap::setIndex(const QModelIndex &index) { } } +void ModelPropertyMap::dump() { + + if (index_.isValid()) { + auto hash = index_.model()->roleNames(); + + QHash::const_iterator i = hash.constBegin(); + while (i != hash.constEnd()) { + const auto role = i.key(); + auto model_value = index_.data(role); + auto propery_name = QString(i.value()); + qDebug() << propery_name << " " << model_value << "\n"; + ++i; + } + } +} + void ModelPropertyMap::updateValues(const QVector &roles) { if (index_.isValid()) { auto hash = index_.model()->roleNames(); @@ -159,8 +246,9 @@ void ModelPropertyMap::updateValues(const QVector &roles) { auto model_value = index_.data(role); auto propery_name = QString(i.value()); - if (model_value != (*values_)[propery_name]) + if (model_value != (*values_)[propery_name]) { values_->setProperty(StdFromQString(propery_name).c_str(), model_value); + } } ++i; } diff --git a/src/ui/qml/helper/src/module_data_ui.cpp b/src/ui/qml/helper/src/module_data_ui.cpp new file mode 100644 index 000000000..e5d9de144 --- /dev/null +++ b/src/ui/qml/helper/src/module_data_ui.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include "xstudio/ui/qml/module_data_ui.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/module/attribute.hpp" + +using namespace xstudio; +using namespace xstudio::ui::qml; +using namespace std::chrono_literals; + +ModulesModelData::ModulesModelData(QObject *parent) : UIModelData(parent) { + + setRoleNames(utility::map_value_to_vec(module::Attribute::role_names)); +} diff --git a/src/ui/qml/helper/src/snapshot_model_ui.cpp b/src/ui/qml/helper/src/snapshot_model_ui.cpp new file mode 100644 index 000000000..99e29d752 --- /dev/null +++ b/src/ui/qml/helper/src/snapshot_model_ui.cpp @@ -0,0 +1,213 @@ +// 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/export.h b/src/ui/qml/json_store/src/export.h new file mode 100644 index 000000000..1f5b8916a --- /dev/null +++ b/src/ui/qml/json_store/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..1f5b8916a --- /dev/null +++ b/src/ui/qml/json_store/src/include/json_store_qml_export.h @@ -0,0 +1,42 @@ + +#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/log/src/export.h b/src/ui/qml/log/src/export.h new file mode 100644 index 000000000..750db9793 --- /dev/null +++ b/src/ui/qml/log/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..750db9793 --- /dev/null +++ b/src/ui/qml/log/src/include/log_qml_export.h @@ -0,0 +1,42 @@ + +#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/module/src/export.h b/src/ui/qml/module/src/export.h new file mode 100644 index 000000000..25f4f6c2c --- /dev/null +++ b/src/ui/qml/module/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..25f4f6c2c --- /dev/null +++ b/src/ui/qml/module/src/include/module_qml_export.h @@ -0,0 +1,42 @@ + +#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 index 36d5c57c8..ea885eebc 100644 --- a/src/ui/qml/module/src/module_menu_ui.cpp +++ b/src/ui/qml/module/src/module_menu_ui.cpp @@ -61,7 +61,7 @@ QVariant ModuleMenusModel::data(const QModelIndex &index, int role) const { rt = attributes_data_[index.row()][role]; } else { } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } return rt; } @@ -94,6 +94,9 @@ QString pathAtDepth(const QString &path, const int depth) { bool ModuleMenusModel::is_attr_in_this_menu(const ConstAttributePtr &attr) { + if (menu_path_.isEmpty()) + return false; + auto bf = submenu_names_; try { bool changed = false; @@ -140,6 +143,8 @@ bool ModuleMenusModel::is_attr_in_this_menu(const ConstAttributePtr &attr) { } void ModuleMenusModel::add_attributes_from_backend(const module::AttributeSet &attrs) { + + const bool e = empty(); try { if (not attrs.empty()) { @@ -174,10 +179,14 @@ void ModuleMenusModel::add_attributes_from_backend(const module::AttributeSet &a } 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 = @@ -234,12 +243,16 @@ void ModuleMenusModel::add_multi_choice_menu_item(const ConstAttributePtr &attr) } 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; @@ -290,10 +303,14 @@ void ModuleMenusModel::update_multi_choice_menu_item( 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)); @@ -315,10 +332,13 @@ void ModuleMenusModel::add_checkable_menu_item(const ConstAttributePtr &attr) { } 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)); @@ -343,10 +363,14 @@ void ModuleMenusModel::add_menu_action_item(const ConstAttributePtr &attr) { } 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(); @@ -386,6 +410,8 @@ void ModuleMenusModel::remove_attribute(const utility::Uuid &attr_uuid) { if (changed) { emit submenu_namesChanged(); } + if (empty() != e) + emit emptyChanged(); } bool ModuleMenusModel::setData(const QModelIndex &index, const QVariant &value, int role) { @@ -417,6 +443,8 @@ bool ModuleMenusModel::setData(const QModelIndex &index, const QVariant &value, 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; @@ -442,6 +470,8 @@ void ModuleMenusModel::update_full_attribute_from_backend( } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } + if (empty() != e) + emit emptyChanged(); } void ModuleMenusModel::update_attribute_from_backend( @@ -510,7 +540,7 @@ void ModuleMenusModel::update_attribute_from_backend( } } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } } diff --git a/src/ui/qml/module/src/module_ui.cpp b/src/ui/qml/module/src/module_ui.cpp index 990ed0dcd..16590b638 100644 --- a/src/ui/qml/module/src/module_ui.cpp +++ b/src/ui/qml/module/src/module_ui.cpp @@ -32,7 +32,7 @@ bool attr_is_in_group( break; } } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } return match; @@ -55,6 +55,8 @@ ModuleAttrsDirect::ModuleAttrsDirect(QObject *parent) emit roleNameChanged(); } +ModuleAttrsDirect::~ModuleAttrsDirect() {} + void ModuleAttrsDirect::add_attributes_from_backend( const module::AttributeSet &attrs, bool check_group) { try { @@ -203,6 +205,8 @@ ModuleAttrsModel::ModuleAttrsModel(QObject *parent) : QAbstractListModel(parent) new ModuleAttrsToQMLShim(this); } +ModuleAttrsModel::~ModuleAttrsModel() {} + QHash ModuleAttrsModel::roleNames() const { QHash roles; for (const auto &p : Attribute::role_names) { @@ -261,29 +265,31 @@ void ModuleAttrsModel::add_attributes_from_backend( } } - beginInsertRows( - QModelIndex(), - attributes_data_.size(), - static_cast(attributes_data_.size() + new_attrs.size()) - 1); + if (!new_attrs.empty()) { + beginInsertRows( + QModelIndex(), + attributes_data_.size(), + static_cast(attributes_data_.size() + new_attrs.size()) - 1); - for (const auto &attr : new_attrs) { + for (const auto &attr : new_attrs) { - QMap attr_qt_data; - const nlohmann::json json = attr->as_json(); + 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()); + 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); } - attributes_data_.push_back(attr_qt_data); - } - endInsertRows(); - emit rowCountChanged(); + endInsertRows(); + emit rowCountChanged(); + } } } @@ -395,7 +401,7 @@ void ModuleAttrsModel::update_attribute_from_backend( } row++; } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } } @@ -408,12 +414,22 @@ void ModuleAttrsModel::setattributesGroupNames(QStringList 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 (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -511,13 +527,12 @@ void ModuleAttrsToQMLShim::init(caf::actor_system &system) { 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 &) { - - }, - [=](const group_down_msg &g) { - // caf::aout(self()) << "ModuleAttrsToQMLShim down: " << to_string(g.source) << - // std::endl; + [=](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) { diff --git a/src/ui/qml/playhead/src/export.h b/src/ui/qml/playhead/src/export.h new file mode 100644 index 000000000..ed4cfde8c --- /dev/null +++ b/src/ui/qml/playhead/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..ed4cfde8c --- /dev/null +++ b/src/ui/qml/playhead/src/include/playhead_qml_export.h @@ -0,0 +1,42 @@ + +#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 index e5953703d..6b3233f15 100644 --- a/src/ui/qml/playhead/src/playhead_ui.cpp +++ b/src/ui/qml/playhead/src/playhead_ui.cpp @@ -32,6 +32,10 @@ PlayheadUI::PlayheadUI(QObject *parent) // helper ? void PlayheadUI::set_backend(caf::actor backend) { + + if (backend_ == backend) + return; + scoped_actor sys{system()}; bool had_backend = bool(backend_); @@ -55,6 +59,7 @@ void PlayheadUI::set_backend(caf::actor backend) { backend_events_ = caf::actor(); } + if (!backend_) { looping_ = playhead::LoopMode::LM_LOOP; play_rate_mode_ = TimeSourceMode::FIXED; @@ -92,7 +97,7 @@ void PlayheadUI::set_backend(caf::actor backend) { 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); + 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_ = @@ -176,7 +181,6 @@ void PlayheadUI::init(actor_system &system_) { // if(msg.source == store) // unsubscribe(); // }); - scoped_actor sys{system()}; // media_uuid_ = QUuid(); // emit mediaUuidChanged(media_uuid_); @@ -228,7 +232,7 @@ void PlayheadUI::init(actor_system &system_) { emit bookmarkedFramesChanged(); }, - [=](utility::event_atom, playhead::duration_frames_atom, const int frames) { + [=](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) { @@ -437,7 +441,7 @@ void PlayheadUI::media_changed() { media_uuid_ = tmp; emit mediaUuidChanged(media_uuid_); } - } catch (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { if (media_uuid_ != QUuid()) { media_uuid_ = QUuid(); emit mediaUuidChanged(media_uuid_); diff --git a/src/ui/qml/quickfuture/src/CMakeLists.txt b/src/ui/qml/quickfuture/src/CMakeLists.txt deleted file mode 100644 index f98e15582..000000000 --- a/src/ui/qml/quickfuture/src/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -SET(LINK_DEPS - Qt5::Core -) - -SET(EXTRAMOC - "${ROOT_DIR}/src/ui/qml/quickfuture/src/quickfuture.h" -) - -create_qml_component(quickfuture 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/quickfuture/src/QuickFuture b/src/ui/qml/quickfuture/src/QuickFuture deleted file mode 120000 index 3ae515624..000000000 --- a/src/ui/qml/quickfuture/src/QuickFuture +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/QuickFuture \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/qffuture.cpp b/src/ui/qml/quickfuture/src/qffuture.cpp deleted file mode 100644 index 1a709f2b3..000000000 --- a/src/ui/qml/quickfuture/src/qffuture.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include -#include -#include - -#include "qffuture.h" -#include "quickfuture.h" - -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture) - -namespace QuickFuture { - -static QMap m_wrappers; - -static int typeId(const QVariant &v) { return v.userType(); } - -Future::Future(QObject *parent) : QObject(parent) {} - -void Future::registerType(int typeId, VariantWrapperBase *wrapper) { - if (m_wrappers.contains(typeId)) { - qWarning() << QString("QuickFuture::registerType:It is already registered:%1") - .arg(QMetaType::typeName(typeId)); - return; - } - - m_wrappers[typeId] = wrapper; -} - -QJSEngine *Future::engine() const { return m_engine; } - -void Future::setEngine(QQmlEngine *engine) { - m_engine = engine; - if (m_engine.isNull()) { - return; - } - - QString qml = "import QtQuick 2.0\n" - "import QuickPromise 1.0\n" - "import QuickFuture 1.0\n" - "QtObject { \n" - "function create(future) {\n" - " var promise = Q.promise();\n" - " Future.onFinished(future, function(value) {\n" - " if (Future.isCanceled(future)) {\n" - " promise.reject();\n" - " } else {\n" - " promise.resolve(value);\n" - " }\n" - " });\n" - " return promise;\n" - "}\n" - "}\n"; - - QQmlComponent comp(engine); - comp.setData(qml.toUtf8(), QUrl()); - QObject *holder = comp.create(); - if (holder == nullptr) { - return; - } - - promiseCreator = engine->newQObject(holder); -} - -bool Future::isFinished(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->isFinished(future); -} - -bool Future::isRunning(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->isRunning(future); -} - -bool Future::isCanceled(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future.isCanceled: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->isCanceled(future); -} - -int Future::progressValue(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future.progressValue: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->progressValue(future); -} - -int Future::progressMinimum(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future.progressMinimum: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->progressMinimum(future); -} - -int Future::progressMaximum(const QVariant &future) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future.progressMaximum: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return false; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->progressMaximum(future); -} - -void Future::onFinished(const QVariant &future, QJSValue func, QJSValue owner) { - Q_UNUSED(owner); - - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return; - } - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - wrapper->onFinished(m_engine, future, func, owner.toQObject()); -} - -void Future::onCanceled(const QVariant &future, QJSValue func, QJSValue owner) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(static_cast(future.type()))); - return; - } - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - wrapper->onCanceled(m_engine, future, func, owner.toQObject()); -} - -void Future::onProgressValueChanged(const QVariant &future, QJSValue func) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString( - "Future.onProgressValueChanged: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return; - } - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - wrapper->onProgressValueChanged(m_engine, future, func); -} - -QVariant Future::result(const QVariant &future) { - QVariant res; - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return res; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->result(future); -} - -QVariant Future::results(const QVariant &future) { - QVariant res; - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return res; - } - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - return wrapper->results(future); -} - -QJSValue Future::promise(QJSValue future) { - QJSValue create = promiseCreator.property("create"); - QJSValueList args; - args << future; - - QJSValue result = create.call(args); - if (result.isError() || result.isUndefined()) { - qWarning() << "Future.promise: QuickPromise is not installed or setup properly"; - result = QJSValue(); - } - - return result; -} - -void Future::sync( - const QVariant &future, - const QString &propertyInFuture, - QObject *target, - const QString &propertyInTarget) { - if (!m_wrappers.contains(typeId(future))) { - qWarning() << QString("Future: Can not handle input data type: %1") - .arg(QMetaType::typeName(future.type())); - return; - } - - - VariantWrapperBase *wrapper = m_wrappers[typeId(future)]; - wrapper->sync(future, propertyInFuture, target, propertyInTarget); -} - -static QObject *provider(QQmlEngine *engine, QJSEngine *scriptEngine) { - Q_UNUSED(scriptEngine); - - auto object = new Future(); - object->setEngine(engine); - - return object; -} - -static void init() { - bool called = false; - if (called) { - return; - } - called = true; - - QCoreApplication *app = QCoreApplication::instance(); - auto tmp = new QObject(app); - - QObject::connect(tmp, &QObject::destroyed, [=]() { - auto iter = m_wrappers.begin(); - while (iter != m_wrappers.end()) { - delete iter.value(); - iter++; - } - }); - - qmlRegisterSingletonType("QuickFuture", 1, 0, "Future", provider); - - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); - Future::registerType(); -} - -#ifndef QUICK_FUTURE_BUILD_PLUGIN -Q_COREAPP_STARTUP_FUNCTION(init) -#endif -} // namespace QuickFuture - -#ifdef QUICK_FUTURE_BUILD_PLUGIN -void QuickFutureQmlPlugin::registerTypes(const char *uri) { - Q_ASSERT(QString("QuickFuture") == uri); - QuickFuture::init(); -} -#endif diff --git a/src/ui/qml/quickfuture/src/qffuture.h b/src/ui/qml/quickfuture/src/qffuture.h deleted file mode 120000 index b9fe99793..000000000 --- a/src/ui/qml/quickfuture/src/qffuture.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/qffuture.h \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/qfvariantwrapper.h b/src/ui/qml/quickfuture/src/qfvariantwrapper.h deleted file mode 120000 index 950128958..000000000 --- a/src/ui/qml/quickfuture/src/qfvariantwrapper.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/qfvariantwrapper.h \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/qmldir b/src/ui/qml/quickfuture/src/qmldir deleted file mode 120000 index 56238483d..000000000 --- a/src/ui/qml/quickfuture/src/qmldir +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/qmldir \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/quickfuture.h b/src/ui/qml/quickfuture/src/quickfuture.h deleted file mode 120000 index 1f07cca8f..000000000 --- a/src/ui/qml/quickfuture/src/quickfuture.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/quickfuture.h \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/quickfuture.qmltypes b/src/ui/qml/quickfuture/src/quickfuture.qmltypes deleted file mode 120000 index e8b96a211..000000000 --- a/src/ui/qml/quickfuture/src/quickfuture.qmltypes +++ /dev/null @@ -1 +0,0 @@ -../../../../../extern/quickfuture/src/quickfuture.qmltypes \ No newline at end of file diff --git a/src/ui/qml/session/src/CMakeLists.txt b/src/ui/qml/session/src/CMakeLists.txt index 0d7a31a40..e525aa794 100644 --- a/src/ui/qml/session/src/CMakeLists.txt +++ b/src/ui/qml/session/src/CMakeLists.txt @@ -3,12 +3,16 @@ SET(LINK_DEPS Qt5::Core Qt5::Test xstudio::ui::qml::helper + xstudio::ui::qml::tag + xstudio::timeline xstudio::utility + xstudio::session ) 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}") diff --git a/src/ui/qml/session/src/caf_response_ui.cpp b/src/ui/qml/session/src/caf_response_ui.cpp index 0a26531b7..521acb7ec 100644 --- a/src/ui/qml/session/src/caf_response_ui.cpp +++ b/src/ui/qml/session/src/caf_response_ui.cpp @@ -1,11 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 -#include "xstudio/session/session_actor.hpp" #include "xstudio/media/media.hpp" +#include "xstudio/session/session_actor.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/json_tree_model_ui.hpp" -#include "xstudio/ui/qml/caf_response_ui.hpp" +#include "xstudio/ui/qml/session_model_ui.hpp" + +#include CAF_PUSH_WARNINGS #include @@ -27,6 +30,17 @@ 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; @@ -100,6 +114,7 @@ class CafRequest : public ControllableJob> { case SessionModel::Roles::formatRole: case SessionModel::Roles::pixelAspectRole: if (type == "MediaSource") { + auto data = request_receive( *sys, actorFromString(system_, json_.at("actor")), @@ -163,11 +178,12 @@ class CafRequest : public ControllableJob> { 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") { + type == "Timeline" or type == "Media" or type == "PlayheadSelection" or + type == "Playhead") { auto actor = caf::actor(); - if (not json_.at("actor").is_null()) { + 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") { @@ -179,6 +195,19 @@ class CafRequest : public ControllableJob> { 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) { @@ -203,11 +232,15 @@ class CafRequest : public ControllableJob> { auto target = actorFromString(system_, json_.at("actor")); if (target) { - auto answer = request_receive( - *sys, target, media::media_status_atom_v); - - result[SessionModel::Roles::mediaStatusRole] = - QStringFromStd(json(answer).dump()); + 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; @@ -234,7 +267,8 @@ class CafRequest : public ControllableJob> { } break; - case SessionModel::Roles::flagRole: + case SessionModel::Roles::flagColourRole: + case SessionModel::Roles::flagTextRole: if (type == "Media") { auto target = actorFromString(system_, json_.at("actor")); if (target) { @@ -242,12 +276,30 @@ class CafRequest : public ControllableJob> { request_receive>( *sys, target, playlist::reflag_container_atom_v); - result[SessionModel::Roles::flagRole] = + result[SessionModel::Roles::flagColourRole] = QStringFromStd(json(flag).dump()); + result[SessionModel::Roles::flagTextRole] = + QStringFromStd(json(text).dump()); } } 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); + } + + result[SessionModel::Roles::selectionRole] = QStringFromStd(j.dump()); + } + } + } break; case SessionModel::Roles::audioActorUuidRole: if (type == "Media") { try { @@ -283,6 +335,7 @@ class CafRequest : public ControllableJob> { 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( @@ -315,6 +368,18 @@ class CafRequest : public ControllableJob> { for (const auto &i : detail) jsn.emplace_back(SessionModel::containerDetailToJson(i, system_)); + result[SessionModel::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[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); } else if (type == "Container List") { // // only happens from playlist. @@ -345,6 +410,40 @@ class CafRequest : public ControllableJob> { 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, @@ -424,15 +523,73 @@ class CafRequest : public ControllableJob> { } + } else if (type == "Playhead") { + // Playhead has no children } else { spdlog::warn("CafRequest unhandled ChildrenRole type {}", type); } break; + default: + + 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()); + } + break; } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - spdlog::warn("{} {} {}", role_name_, role_, json_.dump(2)); } return result; @@ -442,6 +599,7 @@ class CafRequest : public ControllableJob> { const nlohmann::json json_; const int role_; const std::string role_name_; + const std::map metadata_paths_; }; CafResponse::CafResponse( @@ -451,12 +609,45 @@ 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_, + &QFutureWatcher>::finished, + 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_, @@ -475,6 +666,8 @@ CafResponse::CafResponse( } void CafResponse::handleFinished() { + emit finished(search_value_, search_role_, role_); + if (watcher_.future().resultCount()) { auto result = watcher_.result(); diff --git a/src/ui/qml/session/src/export.h b/src/ui/qml/session/src/export.h new file mode 100644 index 000000000..cc4807985 --- /dev/null +++ b/src/ui/qml/session/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..cc4807985 --- /dev/null +++ b/src/ui/qml/session/src/include/session_qml_export.h @@ -0,0 +1,42 @@ + +#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 657b418db..224f8ca67 100644 --- a/src/ui/qml/session/src/session_model_core_ui.cpp +++ b/src/ui/qml/session/src/session_model_core_ui.cpp @@ -24,20 +24,78 @@ SessionModel::SessionModel(QObject *parent) : super(parent) { tag_manager_ = new TagManagerUI(this); init(CafSystemObject::get_actor_system()); - setRoleNames(std::vector({ - {"actorRole"}, {"actorUuidRole"}, {"audioActorUuidRole"}, - {"bitDepthRole"}, {"busyRole"}, {"childrenRole"}, - {"containerUuidRole"}, {"flagRole"}, {"formatRole"}, - {"groupActorRole"}, {"idRole"}, {"imageActorUuidRole"}, - {"mediaCountRole"}, {"mediaStatusRole"}, {"mtimeRole"}, - {"nameRole"}, {"pathRole"}, {"pixelAspectRole"}, - {"placeHolderRole"}, {"rateFPSRole"}, {"resolutionRole"}, - {"thumbnailURLRole"}, {"typeRole"}, {"uuidRole"}, - })); - + 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"}, + }); + + setRoleNames(role_names); request_handler_ = new QThreadPool(this); } +void SessionModel::fetchMore(const QModelIndex &parent) { + try { + if (parent.isValid() and canFetchMore(parent)) { + const auto &j = indexToData(parent); + + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + parent, + parent, + Roles::childrenRole); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + QModelIndexList SessionModel::search_recursive_list_base( const QVariant &value, @@ -48,32 +106,65 @@ QModelIndexList SessionModel::search_recursive_list_base( const int depth) { QModelIndexList results; + auto cached_result = true; - if (role == idRole or role == actorUuidRole or role == containerUuidRole) { + if (role == idRole) { auto uuid = UuidFromQUuid(value.toUuid()); if (uuid.is_null()) { return QModelIndexList(); } - if (role == idRole or role == actorUuidRole) { - auto it = uuid_lookup_.find(uuid); - if (it != std::end(uuid_lookup_)) { - for (auto iit = std::begin(it->second); iit != std::end(it->second);) { - if (iit->isValid()) { - results.push_back(*iit); - iit++; - } else { - iit = it->second.erase(iit); - } + auto it = id_uuid_lookup_.find(uuid); + if (it != std::end(id_uuid_lookup_)) { + // spdlog::info("found {}", to_string(uuid)); + for (auto iit = std::begin(it->second); iit != std::end(it->second);) { + if (iit->isValid()) { + results.push_back(*iit); + iit++; + // spdlog::info("is valid"); + } else { + // spdlog::info("isn't valid"); + iit = it->second.erase(iit); } - } else { - results = JSONTreeModel::search_recursive_list_base( - value, role, parent, start, hits, depth); - for (const auto &i : results) { - add_uuid_lookup(uuid, i); + } + } else { + cached_result = false; + results = JSONTreeModel::search_recursive_list_base( + value, role, parent, start, hits, depth); + for (const auto &i : results) { + add_id_uuid_lookup(uuid, i); + } + } + } + if (role == actorUuidRole or role == containerUuidRole) { + auto uuid = UuidFromQUuid(value.toUuid()); + if (uuid.is_null()) { + return QModelIndexList(); + } + + // if (role == actorUuidRole) { + auto it = uuid_lookup_.find(uuid); + if (it != std::end(uuid_lookup_)) { + // spdlog::info("found {}", to_string(uuid)); + for (auto iit = std::begin(it->second); iit != std::end(it->second);) { + if (iit->isValid()) { + results.push_back(*iit); + iit++; + // spdlog::info("is valid"); + } else { + // spdlog::info("isn't valid"); + iit = it->second.erase(iit); } } + } else { + cached_result = false; + results = JSONTreeModel::search_recursive_list_base( + value, role, parent, start, hits, depth); + for (const auto &i : results) { + add_uuid_lookup(uuid, i); + } } + // } } else if (role == actorRole) { auto str = StdFromQString(value.toString()); @@ -92,7 +183,8 @@ QModelIndexList SessionModel::search_recursive_list_base( } } else { // back populate.. - results = JSONTreeModel::search_recursive_list_base( + cached_result = false; + results = JSONTreeModel::search_recursive_list_base( value, role, parent, start, hits, depth); for (const auto &i : results) { add_string_lookup(str, i); @@ -102,10 +194,14 @@ QModelIndexList SessionModel::search_recursive_list_base( // spdlog::error("No lookup {}", StdFromQString(roleName(role))); } - if (results.empty()) + + if (results.empty()) { + cached_result = false; results = JSONTreeModel::search_recursive_list_base(value, role, parent, start, hits, depth); - else { + } else if (cached_result) { + // spdlog::info("have cached result {} {}", parent.isValid(), depth); + // make sure results exist under parent.. if (parent.isValid()) { for (auto it = results.begin(); it != results.end();) { @@ -187,6 +283,11 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { result = QVariant::fromValue(j.at("media_count").get()); break; + case Roles::errorRole: + if (j.count("error_count")) + result = QVariant::fromValue(j.at("error_count").get()); + break; + case Roles::idRole: if (j.count("id")) result = QVariant::fromValue(QUuidFromUuid(j.at("id"))); @@ -221,6 +322,42 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; + case Roles::trackIndexRole: { + auto type = j.value("type", ""); + if (type == "Audio Track" or type == "Video Track") { + // get parent. + const auto pj = indexToFullData(index.parent(), 1); + const auto id = j.value("id", ""); + + auto vcount = 0; + auto acount = 0; + auto tindex = 0; + auto found = false; + + for (const auto &i : pj.at("children")) { + auto ttype = i.value("type", ""); + auto tid = i.value("id", ""); + if (ttype == "Video Track") { + if (tid == id) + found = true; + if (not found and type == ttype) + tindex++; + vcount++; + } else { + if (tid == id) + found = true; + if (not found and type == ttype) + tindex++; + acount++; + } + } + if (type == "Audio Track") + result = tindex + 1; + else + result = vcount - tindex; + } + } break; + case Roles::rateFPSRole: if (j.count("rate")) { if (j.at("rate").is_null()) { @@ -233,6 +370,18 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } else { result = QVariant::fromValue(j.at("rate").get().to_fps()); } + } else if (j.count("active_range") or j.count("available_range")) { + // timeline.. + if (j.count("active_range") and j.at("active_range").is_object()) { + auto fr = j.value("active_range", FrameRange()); + result = QVariant::fromValue(fr.rate().to_fps()); + } else if ( + j.count("available_range") and j.at("available_range").is_object()) { + auto fr = j.value("available_range", FrameRange()); + result = QVariant::fromValue(fr.rate().to_fps()); + } else if (j.count("available_range") or j.count("active_range")) { + result = QVariant::fromValue(0.0); + } } break; @@ -251,6 +400,12 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; + case clipMediaUuidRole: + if (j.count("prop") and j.at("prop").count("media_uuid")) { + result = QVariant::fromValue(QUuidFromUuid(j.at("prop").at("media_uuid"))); + } + break; + case Roles::resolutionRole: if (j.count("resolution")) { if (j.at("resolution").is_null()) { @@ -418,7 +573,21 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { result = QVariant::fromValue(QUuidFromUuid(j.at("container_uuid"))); break; - case Roles::flagRole: + case Roles::selectionRole: + if (j.count("playhead_selection")) { + if (j.at("playhead_selection").is_null()) { + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + } else + result = json_to_qvariant(j.at("playhead_selection")); + } + break; + + case Roles::flagColourRole: if (j.count("flag")) { if (j.at("flag").is_null()) { requestData( @@ -430,7 +599,118 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } else result = QString::fromStdString(j.at("flag")); } + break; + + case Roles::flagTextRole: + if (j.count("flag_text")) { + if (j.at("flag_text").is_null()) { + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + } else + result = QString::fromStdString(j.at("flag_text")); + } + break; + + + case Roles::enabledRole: + if (j.count("enabled")) { + result = QVariant::fromValue(j.value("enabled", true)); + } + break; + + case Roles::trimmedStartRole: + 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()) { + auto fr = j.value("available_range", FrameRange()); + result = QVariant::fromValue(fr.frame_start().frames()); + } else if (j.count("available_range") or j.count("active_range")) { + result = QVariant::fromValue(0); + } + 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::trimmedDurationRole: + 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()) { + auto fr = j.value("available_range", FrameRange()); + result = QVariant::fromValue(fr.frame_duration().frames()); + } else if (j.count("available_range") or j.count("active_range")) { + result = QVariant::fromValue(0); + } + break; + + case Roles::activeStartRole: + if (j.count("active_range")) { + if (j.at("active_range").is_object()) { + auto fr = j.value("active_range", FrameRange()); + result = QVariant::fromValue(fr.frame_start().frames()); + } else { + result = QVariant::fromValue(0); + } + } + break; + + + case Roles::activeDurationRole: + if (j.count("active_range")) { + if (j.at("active_range").is_object()) { + auto fr = j.value("active_range", FrameRange()); + result = QVariant::fromValue(fr.frame_duration().frames()); + } else { + result = QVariant::fromValue(0); + } + } + break; + + case Roles::availableDurationRole: + 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()); + } else { + result = QVariant::fromValue(0); + } + } + break; + case Roles::availableStartRole: + 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()); + } else { + result = QVariant::fromValue(0); + } + } break; case Qt::DisplayRole: @@ -462,9 +742,31 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { result = QVariantMapFromJson(j.at("children")); } break; - default: - result = JSONTreeModel::data(index, role); - 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); + } + } break; } } } catch (const std::exception &err) { @@ -506,6 +808,123 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int } break; + case errorRole: + if (j.count("error_count") and j["error_count"] != value) { + j["error_count"] = value; + result = true; + } + break; + + case idRole: + if (j.count("id") and j["id"] != value) { + j["id"] = value; + result = true; + } + break; + + case activeStartRole: + if (j.count("active_range")) { + auto fr = FrameRange(); + // has range adjust duration.. + if (j.at("active_range").is_object()) { + fr = j.value("active_range", FrameRange()); + if (fr.frame_start().frames() != value) { + fr.set_start(FrameRate(fr.rate() * value.get())); + result = true; + } + } else { + fr = j.value("available_range", FrameRange()); + if (fr.frame_start().frames() != value) { + fr.set_start(FrameRate(fr.rate() * value.get())); + result = true; + } + } + + if (result) { + j["active_range"] = fr; + // probably pointless, as this will trigger from the backend update + roles.push_back(trimmedStartRole); + if (actor) + anon_send(actor, timeline::active_range_atom_v, fr); + } + } + break; + + case activeDurationRole: + if (j.count("active_range")) { + auto fr = FrameRange(); + // has range adjust duration.. + if (j.at("active_range").is_object()) { + fr = j.value("active_range", FrameRange()); + if (fr.frame_duration().frames() != value) { + fr.set_duration(FrameRate(fr.rate() * value.get())); + result = true; + } + } else { + fr = j.value("available_range", FrameRange()); + if (fr.frame_duration().frames() != value) { + fr.set_duration(FrameRate(fr.rate() * value.get())); + result = true; + } + } + + if (result) { + j["active_range"] = fr; + // probably pointless, as this will trigger from the backend update + roles.push_back(trimmedDurationRole); + if (actor) + anon_send(actor, timeline::active_range_atom_v, fr); + } + } + break; + + case availableStartRole: + if (j.count("available_range")) { + auto fr = j.value("available_range", FrameRange()); + if (fr.frame_start().frames() != value) { + fr.set_start(FrameRate(fr.rate() * value.get())); + result = true; + } + + if (result) { + j["available_range"] = fr; + // probably pointless, as this will trigger from the backend update + roles.push_back(trimmedStartRole); + if (actor) + anon_send(actor, timeline::available_range_atom_v, fr); + } + } + break; + + case availableDurationRole: + if (j.count("available_range")) { + auto fr = j.value("available_range", FrameRange()); + if (fr.frame_duration().frames() != value) { + fr.set_duration(FrameRate(fr.rate() * value.get())); + result = true; + } + + if (result) { + j["available_range"] = fr; + // probably pointless, as this will trigger from the backend update + roles.push_back(trimmedDurationRole); + if (actor) + anon_send(actor, timeline::available_range_atom_v, fr); + } + } + break; + + case enabledRole: + if (j.count("enabled") and j["enabled"] != value) { + j["enabled"] = value; + 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()); + } + } + break; + case mediaCountRole: if (j.count("media_count") and j.at("media_count") != value) { j["media_count"] = value; @@ -638,11 +1057,17 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int result = true; } } + } 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()); + j["name"] = value; + result = true; } } break; - case flagRole: - if (j.count("flag") and j["flag"] != value) { + case flagTextRole: + if (j.count("flag_text") and j["flag_text"] != value) { if (type == "Media") { if (index.isValid()) { nlohmann::json &j = indexToData(index); @@ -655,12 +1080,34 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int actor, playlist::reflag_container_atom_v, std::make_tuple( - std::optional(value.get()), - std::optional())); - j["flag"] = value; - result = true; + std::optional(), + std::optional(value.get()))); + j["flag_text"] = value; + result = true; } } + } + } + break; + + case flagColourRole: + if (j.count("flag") and j["flag"] != value) { + if (type == "Media") { + 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_send( + actor, + playlist::reflag_container_atom_v, + std::make_tuple( + std::optional(value.get()), + std::optional())); + j["flag"] = value; + result = true; + } } else if ( type == "ContainerDivider" or type == "Subset" or type == "Timeline" or type == "Playlist") { @@ -688,6 +1135,19 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int result = true; } } + } else if ( + type == "Clip" or type == "Gap" or type == "Audio Track" or + type == "Video Track" or type == "Stack") { + 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) { + anon_send( + actor, timeline::item_flag_atom_v, value.get()); + j["flag"] = value; + result = true; + } } } break; @@ -717,3 +1177,20 @@ 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); + } +} 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 52cf27944..f2b6532e5 100644 --- a/src/ui/qml/session/src/session_model_handler_ui.cpp +++ b/src/ui/qml/session/src/session_model_handler_ui.cpp @@ -22,6 +22,24 @@ using namespace xstudio::utility; using namespace xstudio::ui::qml; +void SessionModel::updateMedia() { + mediaStatusChangePending_ = false; + emit mediaStatusChanged(mediaStatusIndex_); + mediaStatusIndex_ = QModelIndex(); +} + +void SessionModel::triggerMediaStatusChange(const QModelIndex &index) { + if (mediaStatusChangePending_ and mediaStatusIndex_ == index) { + // no op + } else if (mediaStatusChangePending_) { + emit mediaStatusChanged(index); + } else { + mediaStatusChangePending_ = true; + mediaStatusIndex_ = index; + QTimer::singleShot(100, this, SLOT(updateMedia())); + } +} + void SessionModel::init(caf::actor_system &_system) { super::init(_system); @@ -34,11 +52,58 @@ 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, timeline::item_atom, const timeline::Item &) {}, - [=](utility::event_atom, timeline::item_atom, const JsonStore &, const bool) {}, + [=](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 + // &"); + }, + [=](utility::event_atom, + timeline::item_atom, + const JsonStore &event, + const bool silent) { + try { + // spdlog::info("utility::event_atom, timeline::item_atom, {}, {}", + // event.dump(2), silent); + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + if (timeline_lookup_.count(src)) { + // spdlog::warn("update timeline"); + if (timeline_lookup_[src].update(event)) { + // refresh ? + // timeline_lookup_[src].refresh(-1); + // spdlog::warn("state changed"); + // spdlog::warn("{}", timeline_lookup_[src].serialise(-1).dump(2)); + } + } else { + // spdlog::warn("failed update timeline"); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, [=](json_store::update_atom, const utility::JsonStore & /*change*/, @@ -47,7 +112,7 @@ void SessionModel::init(caf::actor_system &_system) { [=](utility::event_atom, media_metadata::get_metadata_atom, - const utility::JsonStore &) {}, + const utility::JsonStore &jsn) {}, [=](utility::event_atom, media::media_status_atom, @@ -55,7 +120,6 @@ void SessionModel::init(caf::actor_system &_system) { try { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - // spdlog::info("event_atom name_atom {} {} {}", name, to_string(src), // src_str); search from index.. receivedData( json(src_str), actorRole, QModelIndex(), mediaStatusRole, json(status)); @@ -366,7 +430,7 @@ void SessionModel::init(caf::actor_system &_system) { const std::string &value) { // spdlog::info("reflag_container_atom {} {}", to_string(uuid), value); receivedData( - json(uuid), containerUuidRole, QModelIndex(), flagRole, json(value)); + json(uuid), containerUuidRole, QModelIndex(), flagColourRole, json(value)); }, [=](utility::event_atom, @@ -376,7 +440,10 @@ void SessionModel::init(caf::actor_system &_system) { // spdlog::info("reflag_container_atom {}", to_string(uuid)); const auto [flag, text] = value; - receivedData(json(uuid), actorUuidRole, QModelIndex(), flagRole, json(flag)); + receivedData( + json(uuid), actorUuidRole, QModelIndex(), flagColourRole, json(flag)); + receivedData( + json(uuid), actorUuidRole, QModelIndex(), flagTextRole, json(text)); }, [=](utility::event_atom, @@ -391,6 +458,8 @@ void SessionModel::init(caf::actor_system &_system) { media::current_media_source_atom, const utility::UuidActor &ua, const media::MediaType mt) { + START_SLOW_WATCHER() + // comes from media actor.. auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); @@ -401,7 +470,6 @@ void SessionModel::init(caf::actor_system &_system) { // mt, // src_str); - // find first instance of media auto media_source_variant = QVariant::fromValue(QStringFromStd(actorToString(system(), ua.actor()))); @@ -420,6 +488,7 @@ void SessionModel::init(caf::actor_system &_system) { if (media_index.isValid()) { auto plindex = getPlaylistIndex(media_index); + // trigger model update. if (mt == media::MediaType::MT_IMAGE) { receivedData( @@ -448,14 +517,14 @@ void SessionModel::init(caf::actor_system &_system) { // get playlist.. if (plindex.isValid()) { - // notify playlist that it's media might have changed. - emit mediaStatusChanged(plindex); + triggerMediaStatusChange(plindex); // for each instance of this media emit a source change event. for (const auto &i : media_indexes) { if (i.isValid()) { auto media_source_index = search(media_source_variant, actorRole, i); + if (media_source_index.isValid()) { emit mediaSourceChanged( i, media_source_index, static_cast(mt)); @@ -464,6 +533,7 @@ void SessionModel::init(caf::actor_system &_system) { } } } + CHECK_SLOW_WATCHER() }, [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &) {}, @@ -617,7 +687,6 @@ 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); 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 05990e794..8791eb4cc 100644 --- a/src/ui/qml/session/src/session_model_manip_ui.cpp +++ b/src/ui/qml/session/src/session_model_manip_ui.cpp @@ -2,6 +2,10 @@ #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" +#include "xstudio/timeline/clip_actor.hpp" #include "xstudio/media/media.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" @@ -103,59 +107,109 @@ bool SessionModel::duplicateRows(int row, int count, const QModelIndex &parent) auto result = false; // spdlog::warn("duplicateRows {} {}", row, count); + std::set timeline_types( + {"Gap", "Clip", "Stack", "Video Track", "Audio Track"}); + + if (not parent.isValid()) + return false; + try { - auto before = Uuid(); - try { - auto before_ind = SessionModel::index(row + 1, 0, parent); - if (before_ind.isValid()) { - nlohmann::json &j = indexToData(before_ind); - if (j.count("container_uuid")) - before = j.at("container_uuid").get(); - else if (j.count("actor_uuid")) - before = j.at("actor_uuid").get(); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } + auto before = Uuid(); + auto first_index = SessionModel::index(row, 0, parent); + auto first_type = std::string(); - for (auto i = row; i < row + count; i++) { - auto index = SessionModel::index(i, 0, parent); - if (index.isValid()) { - nlohmann::json &j = indexToData(index); + if (first_index.isValid()) { + nlohmann::json &j = indexToData(first_index); + first_type = j.at("type"); + } - if (j.at("type") == "ContainerDivider" or j.at("type") == "Subset" or - j.at("type") == "Timeline" or j.at("type") == "Playlist") { - auto pactor = actorFromIndex(index.parent(), true); + if (timeline_types.count(first_type)) { + scoped_actor sys{system()}; - if (pactor) { - // spdlog::warn("Send Duplicate {}", j["container_uuid"]); - anon_send( - pactor, - playlist::duplicate_container_atom_v, - j.at("container_uuid").get(), - before, - false); - can_duplicate = true; - emit playlistsChanged(); + 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. + for (auto i = row; i < row + count; i++) { + auto index = SessionModel::index(i, 0, parent); + if (index.isValid()) { + 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) { + auto actor_uuid = request_receive( + *sys, actor, utility::duplicate_atom_v); + // add to parent, next to original.. + // mess up next ? + request_receive( + *sys, + pactor, + timeline::insert_item_atom_v, + i, + UuidActorVector({actor_uuid})); + } } - } else if (j.at("type") == "Media") { - // find parent - auto uuid = actorUuidFromIndex(parent, true); + } + } + } else { - if (not uuid.is_null()) { - anon_send( - session_actor_, - playlist::copy_media_atom_v, - uuid, - UuidVector({j.at("actor_uuid").get()}), - true, - before, - false); - } + try { + auto before_ind = SessionModel::index(row + 1, 0, parent); + if (before_ind.isValid()) { + nlohmann::json &j = indexToData(before_ind); + if (j.count("container_uuid")) + before = j.at("container_uuid").get(); + else if (j.count("actor_uuid")) + before = j.at("actor_uuid").get(); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } - } else { - spdlog::warn( - "duplicateRows unhandled type {}", j.at("type").get()); + for (auto i = row; i < row + count; i++) { + auto index = SessionModel::index(i, 0, parent); + if (index.isValid()) { + 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") { + auto pactor = actorFromIndex(index.parent(), true); + + if (pactor) { + // spdlog::warn("Send Duplicate {}", j["container_uuid"]); + anon_send( + pactor, + playlist::duplicate_container_atom_v, + j.at("container_uuid").get(), + before, + false); + can_duplicate = true; + emit playlistsChanged(); + } + } else if (j.at("type") == "Media") { + // find parent + auto uuid = actorUuidFromIndex(parent, true); + + if (not uuid.is_null()) { + anon_send( + session_actor_, + playlist::copy_media_atom_v, + uuid, + UuidVector({j.at("actor_uuid").get()}), + true, + before, + false); + } + + } else { + spdlog::warn( + "duplicateRows unhandled type {}", j.at("type").get()); + } } } } @@ -326,207 +380,285 @@ QModelIndexList SessionModel::insertRows( const auto type = StdFromQString(qtype); const auto name = StdFromQString(qname); scoped_actor sys{system()}; - // spdlog::warn("SessionModel::insertRows {} {} {} {}", row, count, type, name); - nlohmann::json &j = indexToData(parent); + + // spdlog::warn("SessionModel::insertRows {} {} {} {}", row, count, type, name); // spdlog::warn("{}", j.dump(2)); - auto before = Uuid(); - auto insertrow = index(row, 0, parent); - auto actor = caf::actor(); + if (type == "ContainerDivider" or type == "Subset" or type == "Timeline" or + type == "Playlist") { + auto before = Uuid(); + auto insertrow = index(row, 0, parent); + auto actor = caf::actor(); - if (insertrow.isValid()) { - nlohmann::json &irj = indexToData(insertrow); - before = irj.at("container_uuid"); - } + if (insertrow.isValid()) { + nlohmann::json &irj = indexToData(insertrow); + before = irj.at("container_uuid"); + } - if (type == "ContainerDivider") { - if (j.at("type") == "Session") { - actor = j.count("actor") and not j.at("actor").is_null() - ? actorFromString(system(), j.at("actor")) - : caf::actor(); - } else { + if (type == "ContainerDivider") { + if (j.at("type") == "Session") { + actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + } else { + actor = j.count("actor_owner") and not j.at("actor_owner").is_null() + ? actorFromString(system(), j.at("actor_owner")) + : caf::actor(); + // parent is nested.. + } + + if (actor) { + nlohmann::json &pj = indexToData(parent); + // spdlog::warn("divider parent {}", pj.dump(2)); + + if (before.is_null()) + row = rowCount(parent); + + JSONTreeModel::insertRows( + row, + count, + parent, + 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 + } + result.push_back(index(row + i, 0, parent)); + } + } + } else if (type == "Subset") { actor = j.count("actor_owner") and not j.at("actor_owner").is_null() ? actorFromString(system(), j.at("actor_owner")) : caf::actor(); - // parent is nested.. - } - - if (actor) { - nlohmann::json &pj = indexToData(parent); - // spdlog::warn("divider parent {}", pj.dump(2)); - - if (before.is_null()) - row = rowCount(parent); - - JSONTreeModel::insertRows( - row, - count, - parent, - 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 (actor) { + if (before.is_null()) + row = rowCount(parent); + + JSONTreeModel::insertRows( + row, + count, + parent, + R"({ + "type": "Subset", "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++) { + 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); + } + result.push_back(index(row + i, 0, parent)); } - result.push_back(index(row + i, 0, parent)); } - } - } else if (type == "Subset") { - 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": "Subset", "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++) { - 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); + } else if (type == "Timeline") { + actor = j.count("actor_owner") and not j.at("actor_owner").is_null() + ? actorFromString(system(), j.at("actor_owner")) + : caf::actor(); - 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); + if (actor) { + if (before.is_null()) + row = rowCount(parent); + + JSONTreeModel::insertRows( + row, + count, + parent, + R"({ + "type": "Timeline", "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++) { + 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); + } + result.push_back(index(row + i, 0, parent)); } - result.push_back(index(row + i, 0, parent)); } - } - } else if (type == "Timeline") { - 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": "Timeline", "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++) { - 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); + } else if (type == "Playlist") { + if (j.at("type") == "Session") { + actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + + if (before.is_null()) + row = rowCount(parent); + + JSONTreeModel::insertRows( + row, + count, + parent, + R"({"type": "Playlist", "placeholder": true, "container_uuid": null, "actor_uuid": null, "actor": null})"_json); + + // spdlog::warn( + // "JSONTreeModel::insertRows Playlist ({}, {}, parent);", row, + // 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)); + result.push_back(index(row + i, 0, parent)); } - result.push_back(index(row + i, 0, parent)); + emit playlistsChanged(); } } - } else if (type == "Playlist") { - if (j.at("type") == "Session") { - actor = j.count("actor") and not j.at("actor").is_null() - ? actorFromString(system(), j.at("actor")) - : caf::actor(); - - if (before.is_null()) - row = rowCount(parent); - - JSONTreeModel::insertRows( - row, - count, - parent, - R"({"type": "Playlist", "placeholder": true, "container_uuid": null, "actor_uuid": null, "actor": null})"_json); - - // spdlog::warn( - // "JSONTreeModel::insertRows Playlist ({}, {}, parent);", row, count); + } else if ( + type == "Gap" or type == "Clip" or type == "Stack" or type == "Audio Track" or + type == "Video Track") { + auto parent_actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + if (parent_actor) { + auto insertion_json = + R"({"type": null, "id": null, "placeholder": true, "actor": null})"_json; + insertion_json["type"] = type; + + JSONTreeModel::insertRows(row, count, parent, insertion_json); 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); + auto new_uuid = utility::Uuid::generate(); + auto new_item = caf::actor(); + + if (type == "Video Track") { + new_item = self()->spawn( + "New Video Track", media::MediaType::MT_IMAGE, new_uuid); + } else if (type == "Audio Track") { + new_item = self()->spawn( + "New Audio Track", media::MediaType::MT_AUDIO, new_uuid); + } else if (type == "Stack") { + new_item = + self()->spawn("New Stack", 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); + } else if (type == "Clip") { + new_item = self()->spawn( + UuidActor(), "New Clip", new_uuid); + } + + // hopefully add to parent.. + try { + request_receive( + *sys, + parent_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + setData(index(row + i, 0, parent), QUuidFromUuid(new_uuid), idRole); 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()), + actorToQString(system(), new_item), actorRole); + + result.push_back(index(row + i, 0, parent)); + } catch (...) { + // failed to insert, kill it.. + self()->send_exit(new_item, caf::exit_reason::user_shutdown); } - // spdlog::warn("ROW {}, {}", row + i, data_.dump(2)); - result.push_back(index(row + i, 0, parent)); } - emit playlistsChanged(); } } else { spdlog::warn("insertRows: unsupported type {}", type); @@ -559,3 +691,30 @@ void SessionModel::mergeRows(const QModelIndexList &indexes, const QString &name spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } + +void SessionModel::updateErroredCount(const QModelIndex &media_index) { + try { + if (media_index.isValid()) { + auto media_list_index = media_index.parent(); + if (media_list_index.isValid() and + media_list_index.data(typeRole).toString() == QString("Media List")) { + auto parent = media_list_index.parent(); + // either subgroup or playlist + if (parent.isValid() and not parent.data(errorRole).isNull()) { + // count statuses of media.. + auto errors = 0; + for (auto i = 0; i < rowCount(media_list_index); i++) { + auto mind = index(i, 0, media_list_index); + if (mind.isValid() and + mind.data(mediaStatusRole).toString() != QString("Online")) + errors++; + } + setData(parent, QVariant::fromValue(errors), errorRole); + } + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} \ No newline at end of file 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 f65f864b5..aeba9ff2e 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -1,11 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 +#include "xstudio/conform/conformer.hpp" +#include "xstudio/media/media.hpp" #include "xstudio/session/session_actor.hpp" #include "xstudio/tag/tag.hpp" -#include "xstudio/media/media.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/ui/qml/caf_response_ui.hpp" CAF_PUSH_WARNINGS #include @@ -35,6 +38,21 @@ QVariant SessionModel::playlists() const { return mapFromValue(data); } +QStringList SessionModel::conformTasks() const { return conform_tasks_; } + +void SessionModel::updateConformTasks(const std::vector &tasks) { + QStringList result; + + for (const auto &i : tasks) + result.push_back(QStringFromStd(i)); + + if (result != conform_tasks_) { + conform_tasks_ = result; + emit conformTasksChanged(); + } +} + + QModelIndex SessionModel::getPlaylistIndex(const QModelIndex &index) const { QModelIndex result = index; auto matched = QVariant::fromValue(QString("Playlist")); @@ -152,6 +170,7 @@ void SessionModel::setSessionActorAddr(const QString &addr) { } // clear lookup.. + id_uuid_lookup_.clear(); uuid_lookup_.clear(); string_lookup_.clear(); @@ -291,6 +310,8 @@ QFuture> SessionModel::handleDropFuture( auto jdrop = dropToJsonStore(drop); if (jdrop.count("xstudio/media-ids")) return handleMediaIdDropFuture(proposedAction_, jdrop, index); + else if (jdrop.count("xstudio/timeline-ids")) + return handleTimelineIdDropFuture(proposedAction_, jdrop, index); else if (jdrop.count("xstudio/container-ids")) return handleContainerIdDropFuture(proposedAction_, jdrop, index); else if (jdrop.count("text/uri-list")) @@ -316,6 +337,7 @@ QFuture> SessionModel::handleMediaIdDropFuture( QList results; UuidActorVector new_media; auto proposedAction = proposedAction_; + auto dropIndex = index; try { // spdlog::warn( @@ -323,7 +345,7 @@ QFuture> SessionModel::handleMediaIdDropFuture( auto valid_index = index.isValid(); Uuid before; // build list of media actor uuids - UuidVector media_uuids; + UuidActorVector media; Uuid media_owner_uuid; std::string media_owner_name; @@ -338,70 +360,119 @@ QFuture> SessionModel::handleMediaIdDropFuture( media_owner_uuid = UuidFromQUuid(p.data(actorUuidRole).toUuid()); } } - media_uuids.emplace_back(UuidFromQUuid(mind.data(actorUuidRole).toUuid())); + media.emplace_back(UuidActor( + UuidFromQUuid(mind.data(actorUuidRole).toUuid()), + actorFromQString(system(), mind.data(actorRole).toString()))); } } - if (not media_uuids.empty()) { + + if (not media.empty()) { if (valid_index) { // Moving or copying Media to existing playlist, possibly itself. const auto &ij = indexToData(index); // spdlog::warn("{}", ij.at("type").get()); + auto type = ij.at("type").get(); auto target = caf::actor(); - auto target_uuid = ij.at("actor_uuid").get(); - - if (ij.at("type") == "Playlist") { - target = actorFromString(system(), ij.at("actor")); - } else if (ij.at("type") == "Subset") { - target = actorFromIndex(index.parent(), true); - } else if (ij.at("type") == "Timeline") { - target = actorFromIndex(index.parent(), true); - } else if (ij.at("type") == "Media") { - before = ij.at("actor_uuid"); - target = actorFromIndex(index.parent().parent(), true); + auto target_uuid = Uuid(); + + if (type == "Playlist") { + target = actorFromString(system(), ij.at("actor")); + target_uuid = ij.at("actor_uuid").get(); + } else if (type == "Subset") { + target = actorFromIndex(index.parent(), true); + target_uuid = ij.at("actor_uuid").get(); + } else if (type == "Timeline") { + target = actorFromIndex(index.parent(), true); + target_uuid = ij.at("actor_uuid").get(); + } else if (type == "Media") { + before = ij.at("actor_uuid"); + target = actorFromIndex(index.parent().parent(), true); + target_uuid = ij.at("actor_uuid").get(); + } else if ( + type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto tindex = getTimelineIndex(index); + dropIndex = tindex.parent(); + target = actorFromIndex(tindex.parent(), true); + target_uuid = UuidFromQUuid(tindex.data(actorUuidRole).toUuid()); } else { - spdlog::warn("UNHANDLED {}", ij.at("type").get()); + spdlog::warn("UNHANDLED {}", type); } - if (target and not media_uuids.empty()) { + if (target and not media.empty()) { bool local_mode = false; // might be adding to end of playlist, which different // check these uuids aren't already in playlist.. - if (ij.at("type") == "Media") + if (type == "Media") local_mode = true; else { // just check first. auto dup = search( - QVariant::fromValue(QUuidFromUuid(media_uuids[0])), + QVariant::fromValue(QUuidFromUuid(media[0].uuid())), actorUuidRole, - SessionModel::index(0, 0, index)); + SessionModel::index(0, 0, dropIndex)); + if (dup.isValid()) local_mode = true; } if (local_mode) { - anon_send(target, playlist::move_media_atom_v, media_uuids, before); + anon_send( + target, + playlist::move_media_atom_v, + vector_to_uuid_vector(media), + before); } else { if (proposedAction == Qt::MoveAction) { + // spdlog::warn("proposedAction == Qt::MoveAction"); // move media to new playlist anon_send( session_actor_, playlist::move_media_atom_v, target_uuid, media_owner_uuid, - media_uuids, + vector_to_uuid_vector(media), before, false); } else { + // spdlog::warn("proposedAction == Qt::CopyAction"); anon_send( session_actor_, playlist::copy_media_atom_v, target_uuid, - media_uuids, + vector_to_uuid_vector(media), false, before, false); } } + + // post process timeline drops.. + if (type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto track_actor = caf::actor(); + auto row = -1; + + if (type == "Video Track" or type == "Audio Track") + track_actor = actorFromIndex(index, false); + else { + track_actor = actorFromIndex(index.parent(), false); + row = index.row(); + } + + // append to track as clip. + // assuming media_id exists in timeline already. + for (const auto &i : media) { + auto new_uuid = utility::Uuid::generate(); + auto new_item = + self()->spawn(i, "", new_uuid); + anon_send( + track_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + } + } } } else { // Moving or copying Media to new playlist @@ -426,7 +497,7 @@ QFuture> SessionModel::handleMediaIdDropFuture( playlist::move_media_atom_v, uua.second.uuid(), media_owner_uuid, - media_uuids, + vector_to_uuid_vector(media), Uuid(), false); } else { @@ -435,7 +506,7 @@ QFuture> SessionModel::handleMediaIdDropFuture( session_actor_, playlist::copy_media_atom_v, uua.second.uuid(), - media_uuids, + vector_to_uuid_vector(media), false, Uuid(), false); @@ -585,7 +656,9 @@ QFuture> SessionModel::handleContainerIdDropFuture( QFuture> SessionModel::handleUriListDropFuture( - const int proposedAction_, const utility::JsonStore &jdrop, const QModelIndex &index) { + const int proposedAction_, const utility::JsonStore &drop, const QModelIndex &idx) { + const utility::JsonStore jdrop = drop; + const QModelIndex index = idx; return QtConcurrent::run([=]() { scoped_actor sys{system()}; @@ -602,15 +675,34 @@ QFuture> SessionModel::handleUriListDropFuture( // Moving or copying Media to existing playlist, possibly itself. const auto &ij = indexToData(index); auto target = caf::actor(); - auto target_uuid = ij.at("actor_uuid").get(); + auto target_uuid = ij.value("actor_uuid", Uuid()); const auto &type = ij.at("type").get(); + auto sub_target = caf::actor(); + + spdlog::warn("{}", type); + std::string actor; if (type == "Playlist") { - target = actorFromString(system(), ij.at("actor")); + actor = ij.at("actor"); + target = actorFromString(system(), actor); } else if (type == "Subset") { - target = actorFromIndex(index.parent(), true); + target = actorFromIndex(index.parent(), true); + actor = ij.at("actor"); + sub_target = actorFromString(system(), actor); } else if (type == "Timeline") { - target = actorFromIndex(index.parent(), true); + target = actorFromIndex(index.parent(), true); + actor = ij.at("actor"); + sub_target = actorFromString(system(), actor); + } else if ( + type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto tindex = getTimelineIndex(index); + auto pindex = tindex.parent(); + + sub_target = actorFromIndex(tindex, true); + + target = actorFromIndex(pindex, true); + target_uuid = UuidFromQUuid(pindex.data(actorUuidRole).toUuid()); } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); @@ -620,7 +712,8 @@ QFuture> SessionModel::handleUriListDropFuture( if (target) { for (const auto &path : jdrop.at("text/uri-list")) { - auto uri = caf::make_uri(url_clean(path.get())); + auto path_string = path.get(); + auto uri = caf::make_uri(url_clean(path_string)); if (uri) { // uri maybe timeline... // hacky... @@ -650,19 +743,35 @@ QFuture> SessionModel::handleUriListDropFuture( } } - if (type == "Subset") { - auto sub_actor = actorFromString(system(), ij.at("actor")); - if (sub_actor) { - for (const auto &i : new_media) - anon_send( - sub_actor, playlist::add_media_atom_v, i.uuid(), Uuid()); - } - } else if (type == "Timeline") { - auto sub_actor = actorFromString(system(), ij.at("actor")); - if (sub_actor) { - for (const auto &i : new_media) + if (sub_target) { + for (const auto &i : new_media) + anon_send(sub_target, playlist::add_media_atom_v, i.uuid(), Uuid()); + + // post process timeline drops.. + if (type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto track_actor = caf::actor(); + auto row = -1; + + if (type == "Video Track" or type == "Audio Track") + track_actor = actorFromIndex(index, false); + else { + track_actor = actorFromIndex(index.parent(), false); + row = index.row(); + } + + // append to track as clip. + // assuming media_id exists in timeline already. + for (const auto &i : new_media) { + auto new_uuid = utility::Uuid::generate(); + auto new_item = + self()->spawn(i, "", new_uuid); anon_send( - sub_actor, playlist::add_media_atom_v, i.uuid(), Uuid()); + track_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + } } } } @@ -708,20 +817,34 @@ QFuture> SessionModel::handleOtherDropFuture( auto pm = system().registry().template get(plugin_manager_registry); auto target = caf::actor(); auto sub_target = caf::actor(); + auto type = std::string(); if (valid_index) { const auto &ij = indexToData(index); + type = ij.at("type").get(); + + // spdlog::warn("{}", ij.at("type").get()); - if (ij.at("type") == "Playlist") { + if (type == "Playlist") { target = actorFromString(system(), ij.at("actor")); - } else if (ij.at("type") == "Subset") { + } else if (type == "Subset") { target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), ij.at("actor")); - } else if (ij.at("type") == "Timeline") { + } else if (type == "Timeline") { target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), ij.at("actor")); - } else if (ij.at("type") == "Media") { + } else if ( + type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto tindex = getTimelineIndex(index); + auto pindex = tindex.parent(); + + sub_target = actorFromIndex(tindex, true); + + target = actorFromIndex(pindex, true); + // target_uuid = UuidFromQUuid(pindex.data(actorUuidRole).toUuid()); + } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); } else { @@ -770,9 +893,36 @@ QFuture> SessionModel::handleOtherDropFuture( } } + if (sub_target) { for (const auto &i : new_media) anon_send(sub_target, playlist::add_media_atom_v, i.uuid(), Uuid()); + + // post process timeline drops.. + if (type == "Video Track" or type == "Audio Track" or type == "Gap" or + type == "Clip") { + auto track_actor = caf::actor(); + auto row = -1; + + if (type == "Video Track" or type == "Audio Track") + track_actor = actorFromIndex(index, false); + else { + track_actor = actorFromIndex(index.parent(), false); + row = index.row(); + } + + // append to track as clip. + // assuming media_id exists in timeline already. + for (const auto &i : new_media) { + auto new_uuid = utility::Uuid::generate(); + auto new_item = self()->spawn(i, "", new_uuid); + anon_send( + track_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + } + } } } catch (const std::exception &err) { @@ -827,8 +977,7 @@ QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) if (json.isNull()) { try { - std::ifstream i(StdFromQString(path.path())); - i >> js; + js = utility::open_session(StdFromQString(path.path())); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return false; @@ -980,18 +1129,15 @@ QFuture SessionModel::getJSONFuture(const QModelIndex &index, const QSt 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(), - StdFromQString(path)); + *sys, actor, json_store::get_json_atom_v, Uuid(), path_string); result = QStringFromStd(jsn.dump()); } else { auto jsn = request_receive( - *sys, actor, json_store::get_json_atom_v, StdFromQString(path)); + *sys, actor, json_store::get_json_atom_v, path_string); result = QStringFromStd(jsn.dump()); } @@ -1086,6 +1232,7 @@ void SessionModel::setPlayheadTo(const QModelIndex &index) { 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")) { auto ph_events = system().registry().template get(global_playhead_events_actor); @@ -1104,3 +1251,60 @@ void SessionModel::setPlayheadTo(const QModelIndex &index) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } + +QFuture +SessionModel::conformInsertFuture(const QString &task, const QModelIndexList &indexes) { + auto playlist_ua = UuidActor(); + auto media_uas = UuidActorVector(); + + 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()); + } + + return QtConcurrent::run([=]() { + QModelIndexList result; + try { + + scoped_actor sys{system()}; + + 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()); + } + + + return result; + }); +} \ No newline at end of file diff --git a/src/ui/qml/session/src/session_model_timeline_ui.cpp b/src/ui/qml/session/src/session_model_timeline_ui.cpp new file mode 100644 index 000000000..1a8448dba --- /dev/null +++ b/src/ui/qml/session/src/session_model_timeline_ui.cpp @@ -0,0 +1,887 @@ +// SPDX-License-Identifier: Apache-2.0 + +#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" +#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; +using namespace xstudio::ui::qml; + +void SessionModel::setTimelineFocus( + const QModelIndex &timeline, const QModelIndexList &indexes) const { + try { + UuidVector uuids; + + if (timeline.isValid()) { + auto actor = actorFromQString(system(), timeline.data(actorRole).toString()); + + for (auto &i : indexes) { + uuids.emplace_back(UuidFromQUuid(i.data(idRole).toUuid())); + } + + anon_send(actor, timeline::focus_atom_v, uuids); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + + +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(); +} + +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); + result = true; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + + +bool SessionModel::removeTimelineItems(const QModelIndexList &indexes) { + auto result = false; + try { + + // ignore indexes that are not timeline items.. + // be careful of invalidation, deletion order matters ? + + // 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()) { + + 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); + } + } + } + } + } + } 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; + }); +} + +// trigger actor creation +void SessionModel::item_event_callback(const utility::JsonStore &event, timeline::Item &item) { + try { + auto index = search_recursive( + QVariant::fromValue(QUuidFromUuid(event.at("uuid").get())), + idRole, + QModelIndex(), + 0, + -1); + + switch (static_cast(event.at("action"))) { + case timeline::IT_INSERT: + + // 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.. + for (auto &i : *tree) { + auto place_row = 0; + auto data = i.data(); + if (data.count("placeholder") and data.at("id") == new_node.at("id")) { + i.data() = new_node; + replaced = true; + emit dataChanged( + SessionModel::index(place_row, 0, index), + SessionModel::index(place_row, 0, index), + QVector({})); + break; + } + place_row++; + } + + if (not replaced) { + auto new_tree = json_to_tree(new_node, children_); + auto row = event.at("index").get(); + beginInsertRows(index, row, row); + tree->insert(tree->child(row), new_tree); + endInsertRows(); + } + } + + 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_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})); + } + } + 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})); + } + } + 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})); + } + } + 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})); + } + } + 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})); + } + } + 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})); + } + } + } + break; + + case timeline::IT_AVAIL: + if (index.isValid()) { + // spdlog::warn("timeline::IT_AVAIL {}", event.dump(2)); + + 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})); + } + } 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})); + } + } + } + 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(); + + // 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::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})); + } + } + break; + + case timeline::IA_NONE: + default: + break; + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +QModelIndex SessionModel::insertTimelineGap( + const int row, + const QModelIndex &parent, + const int frames, + const double rate, + const QString &qname) { + auto result = QModelIndex(); + + try { + if (parent.isValid()) { + const auto name = StdFromQString(qname); + scoped_actor sys{system()}; + nlohmann::json &j = indexToData(parent); + + auto parent_actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + if (parent_actor) { + auto insertion_json = R"({ + "actor": null, + "enabled": true, + "id": null, + "name": null, + "placeholder": true, + "active_range": null, + "available_range": null, + "type": "Gap" + })"_json; + + + auto new_uuid = utility::Uuid::generate(); + auto duration = utility::FrameRateDuration(frames, FrameRate(1.0 / rate)); + auto new_item = self()->spawn(name, duration, new_uuid); + + insertion_json["actor"] = actorToString(system(), new_item); + insertion_json["id"] = new_uuid; + insertion_json["name"] = name; + insertion_json["available_range"] = utility::FrameRange(duration); + + JSONTreeModel::insertRows(row, 1, parent, insertion_json); + + // { + // "active_range": { + // "duration": 8085000000, + // "rate": 29400000, + // "start": 0 + // }, + // "actor": + // "00000000000001F9010000256B6541E50248C6C675AF42C5E8F50EA28AC388D2D0", + // "available_range": { + // "duration": 0, + // "rate": 0, + // "start": 0 + // }, + // "children": [], + // "enabled": true, + // "flag": "", + // "id": "b5b2bc54-d8e3-49c1-b1ef-94f2b3dae89f", + // "name": "HELLO GAP", + // "prop": null, + // "transparent": true, + // "type": "Gap" + // }, + + + // hopefully add to parent.. + try { + request_receive( + *sys, + parent_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + + result = index(row, 0, parent); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + // failed to insert, kill it.. + self()->send_exit(new_item, caf::exit_reason::user_shutdown); + } + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; +} + +QModelIndex SessionModel::insertTimelineClip( + const int row, + const QModelIndex &parent, + const QModelIndex &mediaIndex, + const QString &qname) { + auto result = QModelIndex(); + + try { + if (parent.isValid()) { + const auto name = StdFromQString(qname); + scoped_actor sys{system()}; + nlohmann::json &j = indexToData(parent); + + auto parent_actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + if (parent_actor) { + auto insertion_json = + R"({"type": "Clip", "id": null, "placeholder": true, "actor": null})"_json; + + JSONTreeModel::insertRows(row, 1, parent, insertion_json); + + auto new_uuid = utility::Uuid::generate(); + // get media .. + auto media_uuid = UuidFromQUuid(mediaIndex.data(actorUuidRole).toUuid()); + auto media_actor = + actorFromQString(system(), mediaIndex.data(actorRole).toString()); + + auto new_item = self()->spawn( + UuidActor(media_uuid, media_actor), name, new_uuid); + + // hopefully add to parent.. + try { + request_receive( + *sys, + parent_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({UuidActor(new_uuid, new_item)})); + setData(index(row, 0, parent), QUuidFromUuid(new_uuid), idRole); + + setData( + index(row, 0, parent), actorToQString(system(), new_item), actorRole); + + result = index(row, 0, parent); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + // failed to insert, kill it.. + self()->send_exit(new_item, caf::exit_reason::user_shutdown); + } + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; +} + +QModelIndex SessionModel::splitTimelineClip(const int frame, const QModelIndex &index) { + auto result = QModelIndex(); + + // only makes sense in Clip, Gap / Track ? + + try { + auto parent_index = index.parent(); + + if (index.isValid() and parent_index.isValid()) { + nlohmann::json &pj = indexToData(parent_index); + + auto parent_actor = pj.count("actor") and not pj.at("actor").is_null() + ? actorFromString(system(), pj.at("actor")) + : caf::actor(); + + scoped_actor sys{system()}; + request_receive( + *sys, parent_actor, timeline::split_item_atom_v, index.row(), frame); + result = SessionModel::index(index.row() + 1, 0, parent_index); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool SessionModel::moveTimelineItem(const QModelIndex &index, const int distance) { + auto result = false; + auto real_distance = (distance == 1 ? 2 : distance); + + // stop mixing of audio and video tracks. + // as this upsets the DelegateModel + + try { + auto parent_index = index.parent(); + if (index.isValid() and parent_index.isValid() and (index.row() + distance >= 0) and + (index.row() + distance < rowCount(parent_index))) { + auto block_move = false; + auto type = StdFromQString(index.data(typeRole).toString()); + if (distance > 0 and type == "Video Track") { + // check next entry.. + auto ntype = + StdFromQString(SessionModel::index(index.row() + 1, 0, index.parent()) + .data(typeRole) + .toString()); + if (ntype != "Video Track") + block_move = true; + } else if (distance < 0 and type == "Audio Track") { + auto ntype = + StdFromQString(SessionModel::index(index.row() - 1, 0, index.parent()) + .data(typeRole) + .toString()); + if (ntype != "Audio Track") + block_move = true; + } + + if (not block_move) { + nlohmann::json &pj = indexToData(parent_index); + + auto parent_actor = pj.count("actor") and not pj.at("actor").is_null() + ? actorFromString(system(), pj.at("actor")) + : caf::actor(); + + scoped_actor sys{system()}; + request_receive( + *sys, + parent_actor, + timeline::move_item_atom_v, + index.row(), + 1, + index.row() + real_distance); + + result = true; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool SessionModel::moveRangeTimelineItems( + const QModelIndex &track_index, + const int frame, + const int duration, + const int dest, + const bool insert) { + auto result = false; + + try { + if (track_index.isValid()) { + nlohmann::json &pj = indexToData(track_index); + + auto track_actor = pj.count("actor") and not pj.at("actor").is_null() + ? actorFromString(system(), pj.at("actor")) + : caf::actor(); + + scoped_actor sys{system()}; + request_receive( + *sys, + track_actor, + timeline::move_item_at_frame_atom_v, + frame, + duration, + dest, + insert); + + result = true; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool SessionModel::alignTimelineItems(const QModelIndexList &indexes, const bool align_right) { + auto result = false; + + if (indexes.size() > 1) { + // index 0 is item to align to with respect to the track. + int align_to = indexes[0].data(parentStartRole).toInt(); + 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(); + if (align_right) + frame += indexes[i].data(trimmedDurationRole).toInt(); + + if (align_to != frame) { + auto duration = indexes[i].data(trimmedDurationRole).toInt(); + + if (align_right) { + setData(indexes[i], duration + (align_to - frame), activeDurationRole); + } else { + // can't align to start + if (indexes[i].row()) { + auto start = indexes[i].data(trimmedStartRole).toInt(); + auto prev_index = index(indexes[i].row() - 1, 0, indexes[i].parent()); + auto prev_duration = prev_index.data(trimmedDurationRole).toInt(); + + setData( + prev_index, prev_duration + (align_to - frame), activeDurationRole); + setData(indexes[i], start + (align_to - frame), activeStartRole); + setData(indexes[i], duration - (align_to - frame), activeDurationRole); + } + } + } + } + } + + return result; +} + +QFuture> SessionModel::handleTimelineIdDropFuture( + const int proposedAction_, const utility::JsonStore &jdrop, const QModelIndex &index) { + + return QtConcurrent::run([=]() { + scoped_actor sys{system()}; + QList results; + auto proposedAction = proposedAction_; + + auto dropIndex = index; + + // UuidActorVector new_media; + + try { + // spdlog::warn( + // "handleTimelineIdDropFuture {} {} {}", + // proposedAction, + // jdrop.dump(2), + // index.isValid()); + auto valid_index = index.isValid(); + + // build list of media actor uuids + + using item_tuple = + std::tuple; + + std::vector items; + + for (const auto &i : jdrop.at("xstudio/timeline-ids")) { + // find media index + auto mind = search_recursive(QUuid::fromString(QStringFromStd(i)), idRole); + + if (mind.isValid()) { + auto item_uuid = UuidFromQUuid(mind.data(idRole).toUuid()); + auto item_actor = actorFromIndex(mind); + auto item_parent_actor = actorFromIndex(mind.parent()); + + auto item_type = StdFromQString(mind.data(typeRole).toString()); + + items.push_back(std::make_tuple( + mind, item_uuid, item_actor, item_parent_actor, item_type)); + } + } + + std::sort(items.begin(), items.end(), [&](item_tuple a, item_tuple b) { + return std::get<0>(a).row() < std::get<0>(b).row(); + }); + + // valid desination ? + if (valid_index) { + auto before_type = StdFromQString(index.data(typeRole).toString()); + auto before_uuid = UuidFromQUuid(index.data(idRole).toUuid()); + auto before_parent = index.parent(); + auto before_parent_actor = actorFromIndex(index.parent()); + auto before_actor = actorFromIndex(index); + + // spdlog::warn( + // "BEFORE {} {} {} {}", + // before_type, + // to_string(before_uuid), + // to_string(before_actor), + // to_string(before_parent_actor)); + + // this can get tricky... + // as index rows will change underneath us.. + + // check before type is timeline.. + if (timeline::TIMELINE_TYPES.count(before_type)) { + auto timeline_index = getTimelineIndex(index); + + // spdlog::warn("{}",before_type); + + for (const auto &i : items) { + const auto item_index = std::get<0>(i); + const auto item_type = std::get<4>(i); + + // spdlog::warn("->{}",item_type); + + + auto item_timeline_index = getTimelineIndex(item_index); + + if (timeline_index == item_timeline_index) { + auto item_uuid = std::get<1>(i); + + // move inside container + if (before_parent == item_index.parent()) { + // get actor.. + request_receive( + *sys, + actorFromIndex(item_index.parent()), + timeline::move_item_atom_v, + item_uuid, + 1, + before_uuid); + } else if (index == item_index.parent()) { + // get actor.. + request_receive( + *sys, + actorFromIndex(index), + timeline::move_item_atom_v, + item_uuid, + 1, + Uuid()); + } else { + // this needs to be an atomic operation, or we end up with two + // items with the same id. this happens when the operation is + // reversed by redo. + + auto new_item = request_receive( + *sys, std::get<2>(i), duplicate_atom_v); + + request_receive( + *sys, + std::get<3>(i), + timeline::erase_item_atom_v, + std::get<1>(i)); + + // we should be able to insert this.. + // make sure before is a container... + + if (before_type == "Clip" or before_type == "Gap") { + request_receive( + *sys, + before_parent_actor, + timeline::insert_item_atom_v, + before_uuid, + UuidActorVector({new_item})); + } else { + request_receive( + *sys, + before_actor, + timeline::insert_item_atom_v, + Uuid(), + UuidActorVector({new_item})); + } + } + } else { + spdlog::warn("timelines don't match"); + } + } + } else { + // target isn't a timeline + } + } else { + // target is undefined + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return results; + }); +} diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index f5ac923eb..8246975af 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -1,11 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 +#include "xstudio/media/media.hpp" #include "xstudio/session/session_actor.hpp" #include "xstudio/tag/tag.hpp" -#include "xstudio/media/media.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/caf_response_ui.hpp" CAF_PUSH_WARNINGS #include @@ -19,6 +20,14 @@ using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::ui::qml; +void SessionModel::add_id_uuid_lookup(const utility::Uuid &uuid, const QModelIndex &index) { + if (not uuid.is_null()) { + if (not id_uuid_lookup_.count(uuid)) + id_uuid_lookup_[uuid] = std::set(); + id_uuid_lookup_[uuid].insert(index); + } +} + void SessionModel::add_uuid_lookup(const utility::Uuid &uuid, const QModelIndex &index) { if (not uuid.is_null()) { if (not uuid_lookup_.count(uuid)) @@ -39,7 +48,7 @@ void SessionModel::add_lookup(const utility::JsonTree &tree, const QModelIndex & // add actorUuidLookup if (tree.data().count("id") and not tree.data().at("id").is_null()) - add_uuid_lookup(tree.data().at("id").get(), index); + add_id_uuid_lookup(tree.data().at("id").get(), index); if (tree.data().count("actor_uuid") and not tree.data().at("actor_uuid").is_null()) add_uuid_lookup(tree.data().at("actor_uuid").get(), index); if (tree.data().count("container_uuid") and not tree.data().at("container_uuid").is_null()) @@ -55,23 +64,23 @@ void SessionModel::add_lookup(const utility::JsonTree &tree, const QModelIndex & } -caf::actor SessionModel::actorFromIndex(const QModelIndex &index, const bool try_parent) { +caf::actor SessionModel::actorFromIndex(const QModelIndex &index, const bool try_parent) const { auto result = caf::actor(); try { if (index.isValid()) { - nlohmann::json &j = indexToData(index); - result = j.count("actor") and not j.at("actor").is_null() - ? actorFromString(system(), j.at("actor")) - : caf::actor(); + const nlohmann::json &j = indexToData(index); + result = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); if (not result and try_parent) { QModelIndex pindex = index.parent(); if (pindex.isValid()) { - nlohmann::json &pj = indexToData(pindex); - result = pj.count("actor") and not pj.at("actor").is_null() - ? actorFromString(system(), pj.at("actor")) - : caf::actor(); + const nlohmann::json &pj = indexToData(pindex); + result = pj.count("actor") and not pj.at("actor").is_null() + ? actorFromString(system(), pj.at("actor")) + : caf::actor(); } } } @@ -116,15 +125,8 @@ 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) { + if (grp) anon_send(grp, broadcast::join_broadcast_atom_v, as_actor()); - // spdlog::error("join grp {} {} {}", - // ijc.back().at("type").is_string() ? - // ijc.back().at("type").get() : "", - // ijc.back().at("name").is_string() ? - // ijc.back().at("name").get() : "", - // to_string(grp)); - } } else if ( tjson.count("actor") and not tjson.at("actor").is_null() and @@ -205,7 +207,8 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & try { if (type == "Session" or type == "Container List" or type == "Media" or - type == "Media List" or type == "PlayheadSelection" or type == "MediaSource") { + type == "Clip" or type == "Media List" or type == "PlayheadSelection" or + type == "MediaSource") { // point to result children.. auto rjc = nlohmann::json(); @@ -227,7 +230,7 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & if (type == "Session" or type == "Container List") { emit playlistsChanged(); compare_key = "container_uuid"; - } else if (type == "Media List" or type == "Media") + } else if (type == "Media List" or type == "Media" or type == "Clip") compare_key = "actor_uuid"; else if (type == "PlayheadSelection") compare_key = "uuid"; @@ -452,17 +455,38 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & if (changed) { // update totals. - 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); + 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); + } } emit dataChanged(parent_index, parent_index, roles); } + emit jsonChanged(); + CHECK_SLOW_WATCHER_FAST() } +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::receivedDataSlot( const QVariant &search_value, const int search_role, @@ -479,11 +503,6 @@ void SessionModel::receivedDataSlot( json::parse(StdFromQString(result))); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - // spdlog::warn("{}", data_.dump(2)); - } - auto inflight = std::make_tuple(search_value, search_role, role); - if (in_flight_requests_.count(inflight)) { - in_flight_requests_.erase(inflight); } } @@ -531,8 +550,21 @@ void SessionModel::receivedData( {Roles::audioActorUuidRole, "audio_actor_uuid"}, {Roles::imageActorUuidRole, "image_actor_uuid"}, {Roles::mediaStatusRole, "media_status"}, - {Roles::flagRole, "flag"}, + {Roles::flagColourRole, "flag"}, + {Roles::flagTextRole, "flag_text"}, + {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"}, }); for (auto &index : indexes) { @@ -547,9 +579,39 @@ void SessionModel::receivedData( 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); + } + } + break; + + // this might be inefficient, we need a new event for media changing underneath + // us + case Roles::mediaStatusRole: + if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { + j[role_to_key[role]] = result; + + if (j.count(role_to_key[Roles::thumbnailURLRole])) { + roles.push_back(Roles::thumbnailURLRole); + j[role_to_key[Roles::thumbnailURLRole]] = nullptr; + } + emit dataChanged(index, index, roles); + + // update error counts in parents. + updateErroredCount(index); + + } else { + // force update of thumbnail.. + if (j.count(role_to_key[Roles::thumbnailURLRole])) { + roles.clear(); + roles.push_back(Roles::thumbnailURLRole); + j[role_to_key[Roles::thumbnailURLRole]] = nullptr; + emit dataChanged(index, 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) { @@ -566,6 +628,25 @@ void SessionModel::receivedData( auto grp = actorFromString(system(), result.get()); if (grp) { anon_send(grp, broadcast::join_broadcast_atom_v, as_actor()); + // 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); + if (media_list_index.isValid()) { + const nlohmann::json &jj = indexToData(media_list_index); + // spdlog::warn("{} {}", + // StdFromQString(media_list_index.data(typeRole).toString()), + // jj.at("id").dump() + // ); + requestData( + QVariant::fromValue(QUuidFromUuid(jj.at("id"))), + idRole, + index, + media_list_index, + childrenRole); + } + } + + // spdlog::error( // "join grp {} {} {}", // j.at("type").is_string() ? j.at("type").get() : @@ -578,6 +659,45 @@ void SessionModel::receivedData( } break; + case JSONTreeModel::Roles::JSONTextRole: + if (j.at("type") == "TimelineItem") { + // this is an init setup.. + 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()); + } + break; + case Roles::childrenRole: processChildren(result, index); break; @@ -591,20 +711,19 @@ void SessionModel::receivedData( CHECK_SLOW_WATCHER() } -// if(can_duplicate) -// result = JSONTreeModel::insertRows(row, count, parent); - void SessionModel::requestData( const QVariant &search_value, const int search_role, const QPersistentModelIndex &search_hint, const QModelIndex &index, - const int role) const { + const int role, + const std::map &metadata_paths) 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); + requestData( + search_value, search_role, search_hint, indexToData(index), role, metadata_paths); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -615,20 +734,14 @@ void SessionModel::requestData( const int search_role, const QPersistentModelIndex &search_hint, const nlohmann::json &data, - const int role) const { + const int role, + const std::map &metadata_paths) const { // dispatch call to backend to retrieve missing data. - auto inflight = std::make_tuple(search_value, search_role, role); + auto inflight = mapFromValue(search_value).dump() + std::to_string(search_role) + "-" + + std::to_string(role); if (not in_flight_requests_.count(inflight)) { in_flight_requests_.emplace(inflight); - - // spdlog::warn("request {} {} {} {}", - // mapFromValue(search_value).dump(2), - // StdFromQString(roleName(search_role)), - // data.dump(2), - // StdFromQString(roleName(role)) - // ); - auto tmp = new CafResponse( search_value, search_role, @@ -636,13 +749,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); } } - nlohmann::json SessionModel::playlistTreeToJson( const utility::PlaylistTree &tree, actor_system &sys, @@ -653,6 +767,7 @@ nlohmann::json SessionModel::playlistTreeToJson( "group_actor": null, "actor_uuid": null, "flag": null, + "flag_text": null, "name": null })"_json); @@ -663,7 +778,8 @@ nlohmann::json SessionModel::playlistTreeToJson( // playlists and dividers.. for (const auto &i : tree.children_ref()) { - if (i.value().type() == "ContainerDivider") { + const auto type = i.value().type(); + if (type == "ContainerDivider") { auto n = createEntry(R"({ "name": null, "actor_uuid": null, @@ -672,26 +788,28 @@ nlohmann::json SessionModel::playlistTreeToJson( "type": null })"_json); n.erase("children"); - n["type"] = i.value().type(); + n["type"] = type; n["name"] = i.value().name(); n["flag"] = i.value().flag(); n["actor_uuid"] = i.value().uuid(); n["container_uuid"] = i.uuid(); result["children"].emplace_back(n); - } else if (i.value().type() == "Subset" or i.value().type() == "Timeline") { + } else if (type == "Subset" or type == "Timeline") { auto n = createEntry(R"({ "name": null, "actor_uuid": null, "container_uuid": null, "group_actor": null, "flag": null, + "flag_text": null, "type": null, "actor": null, "busy": false, - "media_count": 0 + "media_count": 0, + "error_count": 0 })"_json); - n["type"] = i.value().type(); + n["type"] = type; n["name"] = i.value().name(); n["flag"] = i.value().flag(); n["actor_uuid"] = i.value().uuid(); @@ -706,12 +824,22 @@ nlohmann::json SessionModel::playlistTreeToJson( n["children"][0]["actor_owner"] = n["actor"]; n["children"].push_back(createEntry( - R"({"type": "PlayheadSelection", "name": null, "actor_owner": null, "actor_uuid": null, "actor": null, "group_actor": null})"_json)); + R"({"type": "PlayheadSelection", "playhead_selection": null, "name": null, "actor_owner": null, "actor_uuid": null, "actor": null, "group_actor": null})"_json)); n["children"][1]["actor_owner"] = n["actor"]; + if (type == "Timeline") { + n["children"].push_back(createEntry( + R"({"type": "TimelineItem", "name": null, "actor_owner": null})"_json)); + n["children"][2]["actor_owner"] = n["actor"]; + } + + n["children"].push_back(createEntry( + R"({"type": "Playhead", "actor": null, "actor_owner": null, "actor_uuid": null})"_json)); + n["children"].back()["actor_owner"] = n["actor"]; + result["children"].emplace_back(n); } else { - spdlog::warn("{} invalid type {}", __PRETTY_FUNCTION__, i.value().type()); + spdlog::warn("{} invalid type {}", __PRETTY_FUNCTION__, type); } } @@ -764,7 +892,8 @@ nlohmann::json SessionModel::sessionTreeToJson( "type": null, "actor": null, "busy": false, - "media_count": 0 + "media_count": 0, + "error_count": 0 })"_json); n["type"] = i.value().type(); @@ -782,13 +911,17 @@ nlohmann::json SessionModel::sessionTreeToJson( n["children"][0]["actor_owner"] = n["actor"]; n["children"].push_back(createEntry( - R"({"type": "PlayheadSelection", "name": null, "actor_owner": null, "actor_uuid": null, "actor": null, "group_actor": null})"_json)); + R"({"type": "PlayheadSelection", "playhead_selection": null, "name": null, "actor_owner": null, "actor_uuid": null, "actor": null, "group_actor": null})"_json)); n["children"][1]["actor_owner"] = n["actor"]; n["children"].push_back( createEntry(R"({"type": "Container List", "actor_owner": null})"_json)); n["children"][2]["actor_owner"] = n["actor"]; + n["children"].push_back(createEntry( + R"({"type": "Playhead", "actor_owner": null, "actor_uuid": null})"_json)); + n["children"].back()["actor_owner"] = n["actor"]; + result["children"].emplace_back(n); } else { spdlog::warn("{} invalid type {}", __PRETTY_FUNCTION__, i.value().type()); @@ -829,11 +962,23 @@ nlohmann::json SessionModel::containerDetailToJson( } 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; result["audio_actor_uuid"] = nullptr; result["image_actor_uuid"] = nullptr; result["media_status"] = nullptr; if (detail.type_ == "Media") { - result["flag"] = nullptr; + result["flag"] = nullptr; + result["flag_text"] = nullptr; } else if (detail.type_ == "MediaSource") { result["thumbnail_url"] = nullptr; result["rate"] = nullptr; @@ -857,7 +1002,6 @@ nlohmann::json SessionModel::createEntry(const nlohmann::json &update) { return result; } - void SessionModel::moveSelectionByIndex(const QModelIndex &index, const int offset) { try { if (index.isValid()) { @@ -881,7 +1025,7 @@ void SessionModel::updateSelection(const QModelIndex &index, const QModelIndexLi nlohmann::json &j = indexToData(index); // spdlog::warn("{}", j.dump(2)); - if (j.at("type") == "PlayheadSelection") { + if (j.at("type") == "PlayheadSelection" && j.at("actor").is_string()) { auto actor = actorFromString(system(), j.at("actor")); if (actor) { UuidList uv; @@ -900,3 +1044,69 @@ void SessionModel::updateSelection(const QModelIndex &index, const QModelIndexLi } } } + + +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["active_range"] = nullptr; + result["available_range"] = nullptr; + + auto active_range = item.active_range(); + auto available_range = item.available_range(); + + switch (item.item_type()) { + case timeline::IT_NONE: + break; + + case timeline::IT_GAP: + case timeline::IT_AUDIO_TRACK: + case timeline::IT_VIDEO_TRACK: + case timeline::IT_STACK: + case timeline::IT_TIMELINE: + 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; + } + + if (recurse) + switch (item.item_type()) { + case timeline::IT_NONE: + case timeline::IT_GAP: + result["children"] = nlohmann::json::array(); + break; + + case timeline::IT_CLIP: + result["children"] = nlohmann::json::array(); + break; + + case timeline::IT_AUDIO_TRACK: + case timeline::IT_VIDEO_TRACK: + case timeline::IT_STACK: + case timeline::IT_TIMELINE: + result["children"] = nlohmann::json::array(); + if (recurse) { + int index = 0; + for (const auto &i : item.children()) { + result["children"].push_back(timelineItemToJson(i, sys, recurse)); + index++; + } + } + break; + } + + return result; +} diff --git a/src/ui/qml/studio/src/CMakeLists.txt b/src/ui/qml/studio/src/CMakeLists.txt index 39ae256f4..58853a88e 100644 --- a/src/ui/qml/studio/src/CMakeLists.txt +++ b/src/ui/qml/studio/src/CMakeLists.txt @@ -3,6 +3,8 @@ SET(LINK_DEPS Qt5::Core xstudio::ui::qml::helper xstudio::utility + xstudio::session + xstudio::ui::qt::viewport_widget ) SET(EXTRAMOC diff --git a/src/ui/qml/studio/src/export.h b/src/ui/qml/studio/src/export.h new file mode 100644 index 000000000..767079013 --- /dev/null +++ b/src/ui/qml/studio/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..767079013 --- /dev/null +++ b/src/ui/qml/studio/src/include/studio_qml_export.h @@ -0,0 +1,42 @@ + +#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/studio_ui.cpp b/src/ui/qml/studio/src/studio_ui.cpp index d11d1b5cb..dded1291f 100644 --- a/src/ui/qml/studio/src/studio_ui.cpp +++ b/src/ui/qml/studio/src/studio_ui.cpp @@ -19,6 +19,21 @@ StudioUI::StudioUI(caf::actor_system &system, QObject *parent) : QMLActor(parent init(system); } +StudioUI::~StudioUI() { + caf::scoped_actor sys(system()); + for (auto output_plugin : video_output_plugins_) { + sys->send_exit(output_plugin, caf::exit_reason::user_shutdown); + } + video_output_plugins_.clear(); + // Ofscreen viewports are unparented as they are running + // in their own threads. Schedule deletion here. + for (auto vp : offscreen_viewports_) { + vp->stop(); + } + system().registry().erase(studio_ui_registry); + snapshot_offscreen_viewport_->stop(); +} + void StudioUI::init(actor_system &system_) { QMLActor::init(system_); @@ -57,6 +72,9 @@ void StudioUI::init(actor_system &system_) { updateDataSources(); + // put ourselves in the registry + system().registry().template put(studio_ui_registry, as_actor()); + set_message_handler([=](actor_companion * /*self_*/) -> message_handler { return { [=](utility::event_atom, utility::change_atom) {}, @@ -72,8 +90,74 @@ void StudioUI::init(actor_system &system_) { setSessionActorAddr(actorToQString(system(), session)); }, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}}; + [=](utility::event_atom, + ui::open_quickview_window_atom, + const utility::UuidActorVector &media_items, + const std::string &compare_mode) { + 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)); + }, + + [=](utility::event_atom, + ui::show_message_box_atom, + const std::string &message_title, + const std::string &message_body, + const bool close_button, + const int timeout_seconds) { + emit showMessageBox( + QStringFromStd(message_title), + QStringFromStd(message_body), + close_button, + timeout_seconds); + }, + + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](ui::offscreen_viewport_atom, const std::string name) -> caf::actor { + // create a new offscreen viewport and return the actor handle + offscreen_viewports_.push_back(new xstudio::ui::qt::OffscreenViewport(name)); + return offscreen_viewports_.back()->as_actor(); + }, + + [=](ui::offscreen_viewport_atom, const std::string name, caf::actor requester) { + // create a new offscreen viewport and send it back to the 'requester' actor. + // The reason we do it this way is because the requester might be a mixin + // 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(); + } + + }; }); + + // 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) { @@ -139,8 +223,7 @@ QFuture StudioUI::loadSessionFuture(const QUrl &path, const QVariant &json JsonStore js; if (json.isNull()) { - std::ifstream i(StdFromQString(path.path())); - i >> js; + js = utility::open_session(StdFromQString(path.path())); } else { js = qvariant_to_json(json); } @@ -165,10 +248,7 @@ QFuture StudioUI::loadSessionRequestFuture(const QUrl &path) { auto result = false; try { scoped_actor sys{system()}; - JsonStore js; - - std::ifstream i(StdFromQString(path.path())); - i >> js; + JsonStore js = utility::open_session(StdFromQString(path.path())); // if current session is empty load. // else notify UI @@ -215,7 +295,10 @@ void StudioUI::updateDataSources() { // watch for changes.. auto pm = system().registry().template get(plugin_manager_registry); auto details = request_receive>( - *sys, pm, utility::detail_atom_v, plugin_manager::PluginType::PT_DATA_SOURCE); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_DATA_SOURCE)); for (const auto &i : details) { try { @@ -254,3 +337,35 @@ void StudioUI::updateDataSources() { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } + +void StudioUI::loadVideoOutputPlugins() { + + try { + scoped_actor sys{system()}; + bool changed = false; + + // connect to plugin manager, acquire enabled datasource plugins + // watch for changes.. + auto pm = system().registry().template get(plugin_manager_registry); + auto details = request_receive>( + *sys, + pm, + utility::detail_atom_v, + 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); + + } catch (const std::exception &err) { + spdlog::info("{}", err.what()); + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} diff --git a/src/ui/qml/tag/src/CMakeLists.txt b/src/ui/qml/tag/src/CMakeLists.txt index d132328c2..4e8093aa6 100644 --- a/src/ui/qml/tag/src/CMakeLists.txt +++ b/src/ui/qml/tag/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET(LINK_DEPS ${CAF_LIBRARY_core} Qt5::Core xstudio::ui::qml::helper + xstudio::global_store ) SET(EXTRAMOC diff --git a/src/ui/qml/tag/src/export.h b/src/ui/qml/tag/src/export.h new file mode 100644 index 000000000..b662f8cb2 --- /dev/null +++ b/src/ui/qml/tag/src/export.h @@ -0,0 +1,42 @@ + +#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 new file mode 100644 index 000000000..b662f8cb2 --- /dev/null +++ b/src/ui/qml/tag/src/include/tag_qml_export.h @@ -0,0 +1,42 @@ + +#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 index 6686c0198..24aebf4a2 100644 --- a/src/ui/qml/tag/src/tag_ui.cpp +++ b/src/ui/qml/tag/src/tag_ui.cpp @@ -60,7 +60,7 @@ void TagManagerUI::set_backend(caf::actor backend) { try { request_receive( *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { + } catch ([[maybe_unused]] const std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } backend_events_ = caf::actor(); diff --git a/src/ui/qml/viewport/src/CMakeLists.txt b/src/ui/qml/viewport/src/CMakeLists.txt index b08e4ef31..51400dff8 100644 --- a/src/ui/qml/viewport/src/CMakeLists.txt +++ b/src/ui/qml/viewport/src/CMakeLists.txt @@ -6,9 +6,10 @@ SET(LINK_DEPS Qt5::Quick Qt5::Widgets xstudio::module + xstudio::playhead xstudio::ui::opengl::viewport - xstudio::ui::qt::viewport_widget xstudio::ui::viewport + xstudio::ui::qml::helper xstudio::ui::qml::playhead xstudio::utility ) diff --git a/src/ui/qml/viewport/src/export.h b/src/ui/qml/viewport/src/export.h new file mode 100644 index 000000000..6c16367e5 --- /dev/null +++ b/src/ui/qml/viewport/src/export.h @@ -0,0 +1,42 @@ + +#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/include/viewport_qml_export.h b/src/ui/qml/viewport/src/include/viewport_qml_export.h new file mode 100644 index 000000000..6c16367e5 --- /dev/null +++ b/src/ui/qml/viewport/src/include/viewport_qml_export.h @@ -0,0 +1,42 @@ + +#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 93a7e5538..beba767b2 100644 --- a/src/ui/qml/viewport/src/qml_viewport.cpp +++ b/src/ui/qml/viewport/src/qml_viewport.cpp @@ -7,7 +7,6 @@ #include "xstudio/ui/qt/viewport_widget.hpp" #include "xstudio/ui/viewport/viewport.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/ui/qt/offscreen_viewport.hpp" CAF_PUSH_WARNINGS #include @@ -61,9 +60,6 @@ int qtModifierToOurs(const Qt::KeyboardModifiers qt_modifiers) { } // namespace -qt::OffscreenViewport *QMLViewport::offscreen_viewport_ = nullptr; - - QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::ArrowCursor) { playhead_ = new PlayheadUI(this); @@ -72,7 +68,7 @@ QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::A connect(this, &QQuickItem::windowChanged, this, &QMLViewport::handleWindowChanged); static int index = 0; viewport_index_ = index++; - renderer_actor = new QMLViewportRenderer(static_cast(this), viewport_index_); + renderer_actor = new QMLViewportRenderer(this, viewport_index_); connect(renderer_actor, SIGNAL(zoomChanged(float)), this, SIGNAL(zoomChanged(float))); connect( renderer_actor, @@ -114,19 +110,36 @@ QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::A this, SLOT(setNoAlphaChannel(bool))); + connect( + this, + SIGNAL(quickViewSource(QStringList, QString)), + renderer_actor, + SLOT(quickViewSource(QStringList, QString))); + + connect( + renderer_actor, + SIGNAL(quickViewBackendRequest(QStringList, QString)), + this, + SIGNAL(quickViewBackendRequest(QStringList, QString))); + + connect( + renderer_actor, + SIGNAL(quickViewBackendRequestWithSize(QStringList, QString, QPoint, QSize)), + this, + SIGNAL(quickViewBackendRequestWithSize(QStringList, QString, QPoint, QSize))); + + connect( + renderer_actor, + SIGNAL(snapshotRequestResult(QString)), + this, + SIGNAL(snapshotRequestResult(QString))); + setAcceptedMouseButtons(Qt::AllButtons); setAcceptHoverEvents(true); - - if (!offscreen_viewport_) { - try { - offscreen_viewport_ = - new xstudio::ui::qt::OffscreenViewport(static_cast(this)); - } catch (std::exception &e) { - spdlog::debug("Unable to create offscreen viewport renderer: {}", e.what()); - } - } } +QMLViewport::~QMLViewport() { delete renderer_actor; } + void QMLViewport::handleWindowChanged(QQuickWindow *win) { spdlog::debug("QMLViewport::handleWindowChanged"); if (win) { @@ -140,12 +153,14 @@ void QMLViewport::handleWindowChanged(QQuickWindow *win) { this, &QMLViewport::sync, Qt::DirectConnection); + connect( win, &QQuickWindow::sceneGraphInvalidated, this, &QMLViewport::cleanup, Qt::DirectConnection); + connect( win, &QQuickWindow::frameSwapped, @@ -171,6 +186,18 @@ void QMLViewport::handleWindowChanged(QQuickWindow *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"); @@ -182,6 +209,7 @@ void QMLViewport::handleScreenChanged(QScreen *screen) { screen->refreshRate()); } + PointerEvent QMLViewport::makePointerEvent(Signature::EventType t, QMouseEvent *event, int force_modifiers) { @@ -238,7 +266,8 @@ void QMLViewport::sync() { mapToScene(boundingRect().topRight()), mapToScene(boundingRect().bottomRight()), mapToScene(boundingRect().bottomLeft()), - window()->size()); + window()->size(), + window()->devicePixelRatio()); /*static bool share = false; if (window() && !share) { @@ -258,17 +287,34 @@ void QMLViewport::sync() { } void QMLViewport::cleanup() { + + spdlog::debug("QMLViewport::cleanup"); if (renderer_actor) { // delete renderer_actor; - renderer_actor->deleteLater(); - renderer_actor = nullptr; - } - if (offscreen_viewport_) { - offscreen_viewport_->deleteLater(); + delete renderer_actor; renderer_actor = nullptr; } } +void QMLViewport::deleteRendererActor() { + + delete renderer_actor; + renderer_actor = nullptr; +} + +void QMLViewport::hoverEnterEvent(QHoverEvent *event) { + + emit pointerEntered(); + QQuickItem::hoverEnterEvent(event); +} + +void QMLViewport::hoverLeaveEvent(QHoverEvent *event) { + + emit pointerExited(); + QQuickItem::hoverLeaveEvent(event); +} + + void QMLViewport::mousePressEvent(QMouseEvent *event) { mouse_position = event->pos(); @@ -431,15 +477,6 @@ void QMLViewport::setScale(const float s) { renderer_actor->setScale(s); } void QMLViewport::setTranslate(const QVector2D &t) { renderer_actor->setTranslate(t); } -void QMLViewport::setColourUnderCursor(const QVector3D &c) { - - colour_under_cursor = QStringList( - {QString("%1").arg(c.x(), 3, 'f', 3, '0'), - QString("%1").arg(c.y(), 3, 'f', 3, '0'), - QString("%1").arg(c.z(), 3, 'f', 3, '0')}); - emit(colourUnderCursorChanged()); -} - void QMLViewport::wheelEvent(QWheelEvent *event) { // make a mouse wheel event and pass to viewport to process @@ -516,21 +553,28 @@ void QMLViewport::setNoAlphaChannel(bool no_alpha_channel) { class CleanupJob : public QRunnable { public: - CleanupJob(QMLViewportRenderer *renderer) : m_renderer(renderer) {} - void run() override { delete m_renderer; } + /* N.B. - we use a shared_ptr to manage the deletion of the viewport. The + reason is that sometimes (on xstudio shotdown) the CleanupJob instance + is created but run does NOT get executed. */ + CleanupJob(QMLViewportRenderer *vp) : renderer(vp) {} + void run() override { renderer.reset(); } private: - QMLViewportRenderer *m_renderer; + std::shared_ptr renderer; }; void QMLViewport::releaseResources() { - spdlog::debug("QMLViewport::releaseResources"); + + /* 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 + we can do the appropriare release of OpenGL resources*/ window()->scheduleRenderJob( new CleanupJob(renderer_actor), QQuickWindow::BeforeSynchronizingStage); renderer_actor = nullptr; } -QString QMLViewport::renderImageToFile( +void QMLViewport::renderImageToFile( const QUrl filePath, const int format, const int compression, @@ -538,25 +582,8 @@ QString QMLViewport::renderImageToFile( const int height, const bool bakeColor) { - if (!offscreen_viewport_) { - return QString("Offscreen viewport renderer was not found."); - } - - QString error_message; - try { - - offscreen_viewport_->renderSnapshot( - playhead_->backend(), width, height, compression, bakeColor, UriFromQUrl(filePath)); - - spdlog::info( - "Snapshot successfully generated: {}", - xstudio::utility::uri_to_posix_path(UriFromQUrl(filePath))); - - } catch (std::exception &e) { - - error_message = QStringFromStd(e.what()); - } - return error_message; + renderer_actor->renderImageToFile( + filePath, playhead_->backend(), format, compression, width, height, bakeColor); } @@ -602,4 +629,12 @@ void QMLViewport::setRegularCursor(const Qt::CursorShape cname) { this->setCursor(cursor_); } -QString QMLViewport::name() const { return renderer_actor->name(); } \ No newline at end of file +QString QMLViewport::name() const { return renderer_actor->name(); } + +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 diff --git a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp index 5444240eb..745697707 100644 --- a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp +++ b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp @@ -13,11 +13,19 @@ using namespace xstudio; namespace {} // 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(parent), m_window(nullptr), viewport_index_(viewport_index) { + : QMLActor(nullptr), m_window(nullptr), viewport_index_(viewport_index) { + + viewport_qml_item_ = dynamic_cast(parent); + init_system(); } +QMLViewportRenderer::~QMLViewportRenderer() { delete viewport_renderer_; } + static QQuickWindow *win = nullptr; void QMLViewportRenderer::init_renderer() { @@ -66,7 +74,9 @@ void QMLViewportRenderer::paint() { } } -void QMLViewportRenderer::frameSwapped() { viewport_renderer_->framebuffer_swapped(); } +void QMLViewportRenderer::frameSwapped() { + viewport_renderer_->framebuffer_swapped(utility::clock::now()); +} void QMLViewportRenderer::setWindow(QQuickWindow *window) { m_window = window; } @@ -75,7 +85,8 @@ void QMLViewportRenderer::setSceneCoordinates( const QPointF topright, const QPointF bottomright, const QPointF bottomleft, - const QSize sceneSize) { + const QSize sceneSize, + const float devicePixelRatio) { // this is called on every draw, as Qt does not provide a suitable // signal to detect when the viewport coordinates in the top level @@ -91,7 +102,8 @@ void QMLViewportRenderer::setSceneCoordinates( Imath::V2f(topright.x(), topright.y()), Imath::V2f(bottomright.x(), bottomright.y()), Imath::V2f(bottomleft.x(), bottomleft.y()), - Imath::V2i(sceneSize.width(), sceneSize.height())); + Imath::V2i(sceneSize.width(), sceneSize.height()), + devicePixelRatio); } } @@ -107,12 +119,12 @@ void QMLViewportRenderer::init_system() { /* Here we create the all important Viewport class that actually draws images to the screen */ - viewport_renderer_.reset(new ui::viewport::Viewport( + viewport_renderer_ = new ui::viewport::Viewport( jsn, as_actor(), viewport_index_, ui::viewport::ViewportRendererPtr( - new opengl::OpenGLViewportRenderer(viewport_index_, false)))); + new opengl::OpenGLViewportRenderer(viewport_index_, false))); /* 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 */ @@ -135,7 +147,51 @@ void QMLViewportRenderer::init_system() { thread because this class is a caf::mixing/QObject combo that ensures messages are received through QTs event loop thread rather than a regular caf thread.*/ set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { - return caf::message_handler(/*messagehandlers here*/) + return caf::message_handler( + [=](quickview_media_atom, + const std::vector &media_items, + const std::string &compare_mode) { + // the viewport has been sent a quickview request message - we + // use qsignal to pass this up to the QMLViewport object as + // a repeated signal that can be used in the QML engine to + // execute the necessary QML code to launch a new light viewer + // to show the media + QStringList media; + for (auto &media_item : media_items) { + media.append( + QStringFromStd(actorToString(system(), media_item.actor()))); + } + emit quickViewBackendRequest(media, QStringFromStd(compare_mode)); + }, + [=](quickview_media_atom, + const std::vector &media_items, + const std::string &compare_mode, + const int x_position, + const int y_position, + const int x_size, + const int y_size) { + // the viewport has been sent a quickview request message - we + // use qsignal to pass this up to the QMLViewport object as + // a repeated signal that can be used in the QML engine to + // execute the necessary QML code to launch a new light viewer + // to show the media + QStringList media; + for (auto &media_item : media_items) { + media.append( + QStringFromStd(actorToString(system(), media_item.actor()))); + } + emit quickViewBackendRequestWithSize( + media, + QStringFromStd(compare_mode), + QPoint(x_position, y_position), + QSize(x_size, y_size)); + }, + [=](viewport::render_viewport_to_image_atom, + std::string snapshotRenderResult) { + emit snapshotRequestResult(QStringFromStd(snapshotRenderResult)); + } + + ) .or_else(viewport_renderer_->message_handler()); }); @@ -261,6 +317,21 @@ QVector2D QMLViewportRenderer::translate() { return QVector2D(viewport_renderer_->pan().x, viewport_renderer_->pan().y); } +void QMLViewportRenderer::quickViewSource(QStringList mediaActors, QString compareMode) { + + std::vector media; + for (const auto &media_actor_as_string : mediaActors) { + caf::actor media_actor = + actorFromString(system(), StdFromQString(media_actor_as_string)); + if (media_actor) { + media.push_back(media_actor); + } + } + if (!media.empty()) { + anon_send(self(), quickview_media_atom_v, media, StdFromQString(compareMode)); + } +} + void QMLViewportRenderer::receive_change_notification(Viewport::ChangeCallbackId id) { if (id == Viewport::ChangeCallbackId::Redraw) { @@ -280,9 +351,8 @@ void QMLViewportRenderer::receive_change_notification(Viewport::ChangeCallbackId emit translateChanged( QVector2D(viewport_renderer_->pan().x, viewport_renderer_->pan().y)); } else if (id == Viewport::ChangeCallbackId::PlayheadChanged) { - auto *vp = dynamic_cast(parent()); - if (vp) { - vp->setPlayhead(viewport_renderer_->playhead()); + if (viewport_qml_item_) { + viewport_qml_item_->setPlayhead(viewport_renderer_->playhead()); } } else if (id == Viewport::ChangeCallbackId::NoAlphaChannelChanged) { emit noAlphaChannelChanged(viewport_renderer_->no_alpha_channel()); @@ -301,4 +371,49 @@ void QMLViewportRenderer::setScreenInfos( manufacturer.toStdString(), serialNumber.toStdString(), refresh_rate); -} \ No newline at end of file +} + +void QMLViewportRenderer::linkToViewport(QMLViewportRenderer *other_viewport) { + viewport_renderer_->link_to_viewport(other_viewport->as_actor()); +} + +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::setIsQuickViewer(const bool is_quick_viewer) {} diff --git a/src/ui/qt/CMakeLists.txt b/src/ui/qt/CMakeLists.txt index 16f7303a5..04aad249b 100644 --- a/src/ui/qt/CMakeLists.txt +++ b/src/ui/qt/CMakeLists.txt @@ -6,8 +6,10 @@ set(CMAKE_AUTOUIC ON) find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL Qml Quick QUIET) # QT5_ADD_RESOURCES(PROTOTYPE_RCS) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic") -set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") +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() # if (Qt5_POSITION_INDEPENDENT_CODE) # SET(CMAKE_POSITION_INDEPENDENT_CODE ON) diff --git a/src/ui/qt/viewport_widget/src/CMakeLists.txt b/src/ui/qt/viewport_widget/src/CMakeLists.txt index 654967159..216f69612 100644 --- a/src/ui/qt/viewport_widget/src/CMakeLists.txt +++ b/src/ui/qt/viewport_widget/src/CMakeLists.txt @@ -35,4 +35,7 @@ target_link_libraries(${PROJECT_NAME} Qt5::Qml Qt5::Quick xstudio::ui::opengl::viewport -) \ No newline at end of file + xstudio::ui::viewport + xstudio::thumbnail + xstudio::ui::qml::helper +) diff --git a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp index f61a3a937..db93fdb21 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -25,110 +25,64 @@ using namespace caf; using namespace xstudio; using namespace xstudio::ui; using namespace xstudio::ui::qt; +using namespace xstudio::ui::viewport; namespace fs = std::filesystem; +namespace { +static void threaded_memcpy(void *_dst, void *_src, size_t n, int n_threads) { -/* This actor allows other actors to interact with the OffscreenViewport in -the regular request().then() pattern, which is otherwise not possible with -QObject/caf::actor mixin class which normally can't have message handlers that -have a return value */ -class OffscreenViewportMiddlemanActor : public caf::event_based_actor { - public: - OffscreenViewportMiddlemanActor(caf::actor_config &cfg, caf::actor offscreen_viewport) - : caf::event_based_actor(cfg), offscreen_viewport_(std::move(offscreen_viewport)) { + std::vector memcpy_threads; + size_t step = ((n / n_threads) / 4096) * 4096; - system().registry().put(offscreen_viewport_registry, this); + uint8_t *dst = (uint8_t *)_dst; + uint8_t *src = (uint8_t *)_src; - behavior_.assign( - // incoming request from somewhere in xstudio for a screen render - [=](viewport::render_viewport_to_image_atom, - caf::actor playhead, - const int width, - const int height, - const int compression, - const bool bakeColor, - const caf::uri path) -> result { - auto rp = make_response_promise(); - request( - offscreen_viewport_, - infinite, - viewport::render_viewport_to_image_atom_v, - playhead, - width, - height, - compression, - bakeColor, - path) - .then( - [=](bool r) mutable { rp.deliver(r); }, - [=](caf::error &err) mutable { rp.deliver(err); }); - return rp; - }, - [=](viewport::render_viewport_to_image_atom, - caf::actor media_actor, - const int media_frame, - const int width, - const int height, - const caf::uri path) -> result { - auto rp = make_response_promise(); - render_to_file(media_actor, media_frame, width, height, path, rp); - return rp; - }, - [=](viewport::render_viewport_to_image_atom, - caf::actor media_actor, - const int media_frame, - const thumbnail::THUMBNAIL_FORMAT format, - const int width, - const bool auto_scale, - const bool show_annotations) -> result { - auto rp = make_response_promise(); - render_to_thumbail( - rp, media_actor, media_frame, format, width, auto_scale, show_annotations); - return rp; - }); + 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; } - ~OffscreenViewportMiddlemanActor() override { offscreen_viewport_ = caf::actor(); } - - void render_to_thumbail( - caf::typed_response_promise rp, - caf::actor media_actor, - const int media_frame, - const thumbnail::THUMBNAIL_FORMAT format, - const int width, - const bool auto_scale, - const bool show_annotations); - - void render_to_file( - caf::actor media_actor, - const int media_frame, - const int width, - const int height, - const caf::uri path, - caf::typed_response_promise rp); + // ensure any threads still running to copy data to this texture are done + for (auto &t : memcpy_threads) { + if (t.joinable()) + t.join(); + } +} - caf::behavior behavior_; - caf::actor offscreen_viewport_; +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}}; - caf::behavior make_behavior() override { return behavior_; } -}; +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}}; -OffscreenViewport::OffscreenViewport(QObject *parent) : super(parent) { +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}}; - /*thread_ = new QThread(this); - moveToThread(thread_);*/ +} // namespace - /*thread_ = new QThread(this); - moveToThread(thread_);*/ +OffscreenViewport::OffscreenViewport(const std::string name) : super() { // 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(xstudio::ui::qml::CafSystemObject::get_actor_system()); + super::init(qml::CafSystemObject::get_actor_system()); - scoped_actor sys{xstudio::ui::qml::CafSystemObject::get_actor_system()}; - middleman_ = sys->spawn(as_actor()); + scoped_actor sys{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 @@ -137,28 +91,51 @@ OffscreenViewport::OffscreenViewport(QObject *parent) : super(parent) { // to render the viewport into our GLContext static int offscreen_idx = -1; utility::JsonStore jsn; - jsn["base"] = utility::JsonStore(); - viewport_renderer_.reset(new ui::viewport::Viewport( + jsn["base"] = utility::JsonStore(); + viewport_renderer_ = new Viewport( jsn, as_actor(), offscreen_idx--, - ui::viewport::ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false)))); + ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false)), + 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); + + self()->set_down_handler([=](down_msg &msg) { + if (msg.source == video_output_actor_) { + video_output_actor_ = caf::actor(); + } + }); // 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 // rendering set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { 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) + -> result { + try { + // copies a QImage to the Clipboard + renderSnapshot(width, height); + return true; + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](viewport::render_viewport_to_image_atom, - caf::actor playhead, + const caf::uri path, const int width, - const int height, - const int compression, - const bool bakeColor, - const caf::uri path) -> result { + const int height) -> result { try { - renderSnapshot(playhead, width, height, compression, bakeColor, path); + renderSnapshot(width, height, path); return true; } catch (std::exception &e) { return caf::make_error(xstudio_error::error, e.what()); @@ -166,33 +143,77 @@ OffscreenViewport::OffscreenViewport(QObject *parent) : super(parent) { }, [=](viewport::render_viewport_to_image_atom, - caf::actor playhead, const thumbnail::THUMBNAIL_FORMAT format, const int width, - const bool render_annotations, - const bool fit_to_annotations_outside_image) - -> result { + const int height) -> result { + try { + return renderToThumbnail(format, width, height); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + + [=](viewport::render_viewport_to_image_atom, + caf::actor media_actor, + const int media_frame, + const thumbnail::THUMBNAIL_FORMAT format, + const int width, + const bool auto_scale, + const bool show_annotations) -> result { + thumbnail::ThumbnailBufferPtr r; try { - return renderToThumbnail( - playhead, - format, - width, - render_annotations, - fit_to_annotations_outside_image); + r = renderMediaFrameToThumbnail( + media_actor, media_frame, 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) { + video_output_actor_ = video_output_actor; + vid_out_width_ = outputWidth; + vid_out_height_ = outputHeight; + vid_out_format_ = format; + }, + + [=](video_output_actor_atom, caf::actor video_output_actor) { + video_output_actor_ = video_output_actor; + }, + + [=](render_viewport_to_image_atom) { + // force a redraw + receive_change_notification(Viewport::ChangeCallbackId::Redraw); + } + + }); }); + + initGL(); } OffscreenViewport::~OffscreenViewport() { - caf::scoped_actor sys(self()->home_system()); - sys->send_exit(middleman_, caf::exit_reason::user_shutdown); - middleman_ = caf::actor(); + gl_context_->makeCurrent(surface_); + delete viewport_renderer_; + glDeleteTextures(1, &texId_); + glDeleteFramebuffers(1, &fboId_); + glDeleteTextures(1, &depth_texId_); + + delete gl_context_; + delete surface_; + + video_output_actor_ = caf::actor(); } + +void OffscreenViewport::autoDelete() { delete this; } + + void OffscreenViewport::initGL() { if (!gl_context_) { @@ -205,8 +226,7 @@ void OffscreenViewport::initGL() { format.setAlphaBufferSize(8); format.setRenderableType(QSurfaceFormat::OpenGL); - gl_context_ = - new QOpenGLContext(static_cast(this)); // m_window->openglContext(); + gl_context_ = new QOpenGLContext(nullptr); // m_window->openglContext(); gl_context_->setFormat(format); if (!gl_context_) throw std::runtime_error( @@ -217,56 +237,44 @@ void OffscreenViewport::initGL() { } // we also require a QSurface to use the GL context - surface_ = new QOffscreenSurface(nullptr, static_cast(this)); + surface_ = new QOffscreenSurface(nullptr, nullptr); surface_->setFormat(format); surface_->create(); - gl_context_->makeCurrent(surface_); - } -} + // gl_context_->makeCurrent(surface_); -thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( - caf::actor playhead, - const thumbnail::THUMBNAIL_FORMAT format, - const int width, - const bool render_annotations, - const bool fit_to_annotations_outside_image) { - - initGL(); - media_reader::ImageBufPtr image = viewport_renderer_->get_image_from_playhead(playhead); - - const Imath::V2i image_dims = image->image_size_in_pixels(); - if (image_dims.x <= 0 || image_dims.y <= 0) { - throw std::runtime_error("On screen image is null."); - } - viewport_renderer_->update_fit_mode_matrix( - image_dims.x, image_dims.y, image->pixel_aspect()); - viewport_renderer_->set_fit_mode(viewport::FitMode::One2One); + // we also require a QSurface to use the GL context + surface_ = new QOffscreenSurface(nullptr, nullptr); + surface_->setFormat(format); + surface_->create(); - const int x_size = image_dims.x; - const int y_size = (int)round(float(image_dims.y) * image->pixel_aspect()); + thread_ = new QThread(); + gl_context_->moveToThread(thread_); + moveToThread(thread_); + thread_->start(); - thumbnail::ThumbnailBufferPtr r = renderOffscreen(x_size, y_size, image); + // Note - the only way I seem to be able to 'cleanly' exit is + // delete ourselves when the thread quits. Not 100% sure if this + // is correct approach. I'm still cratching my head as to how + // to destroy thread_ ... calling deleteLater() directly or + // using finished signal has no effect. - if (width > 0) - r->bilin_resize(width, (y_size * width) / x_size); + connect(thread_, SIGNAL(finished()), this, SLOT(autoDelete())); - r->convert_to(format); + // this has no effect! + // connect(thread_, SIGNAL(finished()), this, SLOT(deleteLater())); + } +} - return r; +void OffscreenViewport::stop() { + thread_->quit(); + thread_->wait(); } -void OffscreenViewport::renderSnapshot( - caf::actor playhead, - const int width, - const int height, - const int compression, - const bool bakeColor, - const caf::uri path) { +void OffscreenViewport::renderSnapshot(const int width, const int height, const caf::uri path) { - initGL(); + // initGL(); - media_reader::ImageBufPtr image = viewport_renderer_->get_image_from_playhead(playhead); // 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)); @@ -279,51 +287,76 @@ void OffscreenViewport::renderSnapshot( throw std::runtime_error("Invalid image dimensions."); } - thumbnail::ThumbnailBufferPtr r = renderOffscreen(width, height, image); + 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)); 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(r, path); + this->exportToEXR(image, path); } else { - this->exportToCompressedFormat(r, path, compression, ext); + this->exportToCompressedFormat(image, path, ext); } } -void OffscreenViewport::exportToEXR(thumbnail::ThumbnailBufferPtr r, const caf::uri path) { - std::unique_ptr buf(new Imf::Rgba[r->height() * r->width()]); - Imf::Rgba *tbuf = buf.get(); - - // m_image.convertTo(QImage::Format_RGBA64); - auto *ff = (float *)r->data().data(); - int px = r->height() * r->width(); - while (px--) { - tbuf->r = *(ff++); - tbuf->g = *(ff++); - tbuf->b = *(ff++); - tbuf->a = 1.0f; - tbuf++; +void OffscreenViewport::setPlayhead(const QString &playheadAddress) { + + try { + + scoped_actor sys{as_actor()->home_system()}; + auto playhead_actor = qml::actorFromQString(as_actor()->home_system(), playheadAddress); + + if (playhead_actor) { + viewport_renderer_->set_playhead(playhead_actor); + + if (viewport_renderer_->colour_pipeline()) { + // get the current on screen media source + auto media_source = utility::request_receive( + *sys, playhead_actor, playhead::media_source_atom_v, true); + + // update the colour pipeline with the media source so it can + // run its logic to update the view/display attributes etc. + utility::request_receive( + *sys, + viewport_renderer_->colour_pipeline(), + playhead::media_source_atom_v, + media_source); + } + } + + + } catch (std::exception &e) { + spdlog::warn("{} {} ", __PRETTY_FUNCTION__, e.what()); } +} +void OffscreenViewport::exportToEXR(const media_reader::ImageBufPtr &buf, const caf::uri path) { Imf::Header header; - header.dataWindow() = header.displayWindow() = - Imath::Box2i(Imath::V2i(0, 0), Imath::V2i(r->width() - 1, r->height() - 1)); - header.compression() = Imf::PIZ_COMPRESSION; + const Imath::V2i dim = buf->image_size_in_pixels(); + Imath::Box2i box; + box.min.x = 0; + box.min.y = 0; + box.max.x = dim.x - 1; + box.max.y = dim.y - 1; + header.dataWindow() = header.displayWindow() = box; + header.compression() = Imf::PIZ_COMPRESSION; Imf::RgbaOutputFile outFile(utility::uri_to_posix_path(path).c_str(), header); - outFile.setFrameBuffer(buf.get(), 1, r->width()); - outFile.writePixels(r->height()); + outFile.setFrameBuffer((Imf::Rgba *)buf->buffer(), 1, dim.x); + outFile.writePixels(dim.y); } void OffscreenViewport::exportToCompressedFormat( - thumbnail::ThumbnailBufferPtr r, - const caf::uri path, - int compression, - const std::string &ext) { + const media_reader::ImageBufPtr &buf, const caf::uri path, const std::string &ext) { + thumbnail::ThumbnailBufferPtr r = rgb96thumbFromHalfFloatImage(buf); r->convert_to(thumbnail::TF_RGB24); // N.B. We can't pass our thumnail buffer directly to QImage constructor as @@ -347,37 +380,71 @@ void OffscreenViewport::exportToCompressedFormat( QApplication::clipboard()->setImage(im, QClipboard::Clipboard); - int compLevel = - ext == "TIF" || ext == "TIFF" ? std::max(compression, 1) : (10 - compression) * 10; + /*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()) + return; + QImageWriter writer(xstudio::utility::uri_to_posix_path(path).c_str()); - writer.setCompression(compLevel); + // writer.setCompression(compLevel); if (!writer.write(im)) { throw std::runtime_error(writer.errorString().toStdString().c_str()); } } -thumbnail::ThumbnailBufferPtr OffscreenViewport::renderOffscreen( - const int w, const int h, const media_reader::ImageBufPtr &image) { - // ensure our GLContext is current - gl_context_->makeCurrent(surface_); - if (!gl_context_->isValid()) { - throw std::runtime_error( - "OffscreenViewport::renderOffscreen - GL Context is not valid."); +void OffscreenViewport::setupTextureAndFrameBuffer( + const int width, const int height, const viewport::ImageFormat format) { + + if (tex_width_ == width && tex_height_ == height && format == vid_out_format_) { + // bind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, fboId_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId_, 0); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texId_, 0); + return; } - // intialises shaders and textures where necessary - viewport_renderer_->init(); + if (texId_) { + glDeleteTextures(1, &texId_); + glDeleteFramebuffers(1, &fboId_); + glDeleteTextures(1, &depth_texId_); + } - unsigned int texId, depth_texId; - unsigned int fboId; + tex_width_ = width; + 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); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_FLOAT, nullptr); + glGenTextures(1, &texId_); + glBindTexture(GL_TEXTURE_2D, texId_); + glTexImage2D( + GL_TEXTURE_2D, + 0, + format_to_gl_tex_format[vid_out_format_], + tex_width_, + tex_height_, + 0, + GL_RGBA, + GL_UNSIGNED_SHORT, + nullptr); + + GLint iTexFormat; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &iTexFormat); + if (iTexFormat != format_to_gl_tex_format[vid_out_format_]) { + spdlog::warn( + "{} offscreen viewport texture internal format is {:#x}, which does not match " + "desired format {:#x}", + __PRETTY_FUNCTION__, + iTexFormat, + format_to_gl_tex_format[vid_out_format_]); + } glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); @@ -386,8 +453,8 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderOffscreen( { - glGenTextures(1, &depth_texId); - glBindTexture(GL_TEXTURE_2D, depth_texId); + glGenTextures(1, &depth_texId_); + glBindTexture(GL_TEXTURE_2D, depth_texId_); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); @@ -400,8 +467,8 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderOffscreen( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, - w, - h, + tex_width_, + tex_height_, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, @@ -414,16 +481,38 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderOffscreen( } // init framebuffer - glGenFramebuffers(1, &fboId); + glGenFramebuffers(1, &fboId_); // bind framebuffer - glBindFramebuffer(GL_FRAMEBUFFER, fboId); + glBindFramebuffer(GL_FRAMEBUFFER, fboId_); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId_, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texId_, 0); +} + +void OffscreenViewport::renderToImageBuffer( + const int w, + const int h, + media_reader::ImageBufPtr &image, + const viewport::ImageFormat format) { + auto t0 = utility::clock::now(); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texId, 0); + // ensure our GLContext is current + gl_context_->makeCurrent(surface_); + if (!gl_context_->isValid()) { + throw std::runtime_error( + "OffscreenViewport::renderToImageBuffer - GL Context is not valid."); + } + + setupTextureAndFrameBuffer(w, h, format); + + // intialises shaders and textures where necessary + viewport_renderer_->init(); + + auto t1 = utility::clock::now(); // Clearup before render, probably useless for a new buffer - glClearColor(0.0, 0.0, 0.0, 0.0); - glClear(GL_COLOR_BUFFER_BIT); + // glClearColor(0.0, 1.0, 0.0, 0.0); + // glClear(GL_COLOR_BUFFER_BIT); glViewport(0, 0, w, h); @@ -434,155 +523,227 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderOffscreen( Imath::V2f(w, 0.0), Imath::V2f(w, h), Imath::V2f(0.0f, h), - Imath::V2i(w, h)); + Imath::V2i(w, h), + 1.0f); - viewport_renderer_->render(image); + viewport_renderer_->render(); // Not sure if this is necessary - glFinish(); + // 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 - thumbnail::ThumbnailBufferPtr r(new thumbnail::ThumbnailBuffer(w, h, thumbnail::TF_RGBF96)); + 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; + + if (!pixel_buffer_object_) { + glGenBuffers(1, &pixel_buffer_object_); + } + + if (pix_buf_size != pix_buf_size_) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, pixel_buffer_object_); + glBufferData(GL_PIXEL_PACK_BUFFER, pix_buf_size, NULL, GL_STREAM_COPY); + pix_buf_size_ = pix_buf_size; + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texId_); glPixelStorei(GL_PACK_SKIP_ROWS, 0); glPixelStorei(GL_PACK_SKIP_PIXELS, 0); glPixelStorei(GL_PACK_ROW_LENGTH, w); glPixelStorei(GL_PACK_ALIGNMENT, 1); - // read GL pixels to array - glReadPixels(0, 0, w, h, GL_RGB, GL_FLOAT, r->data().data()); - glFinish(); - // unbind and delete + auto t3 = utility::clock::now(); + glBindFramebuffer(GL_FRAMEBUFFER, 0); - glDeleteTextures(1, &texId); - glDeleteFramebuffers(1, &fboId); - glDeleteTextures(1, &depth_texId); + 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); + + + // now mapped buffer contains the pixel data + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + 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";*/ +} + + +void OffscreenViewport::receive_change_notification(Viewport::ChangeCallbackId id) { + + if (id == Viewport::ChangeCallbackId::Redraw) { + + if (video_output_actor_) { + + 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); + } + + renderToImageBuffer(vid_out_width_, vid_out_height_, ready_buf, vid_out_format_); + anon_send(video_output_actor_, ready_buf); + } + } +} + +void OffscreenViewport::make_conversion_lut() { + + if (half_to_int_32_lut_.empty()) { + const double int_max = double(std::numeric_limits::max()); + half_to_int_32_lut_.resize(1 << 16); + for (size_t i = 0; i < (1 << 16); ++i) { + half h; + h.setBits(i); + half_to_int_32_lut_[i] = + uint32_t(round(std::max(0.0, std::min(1.0, double(h))) * int_max)); + } + } +} + +thumbnail::ThumbnailBufferPtr +OffscreenViewport::rgb96thumbFromHalfFloatImage(const media_reader::ImageBufPtr &image) { + + const Imath::V2i image_size = image->image_size_in_pixels(); + + // since we only run this routine ourselves and set-up the image properly + // this mismatch can't happen but check anyway just in case. Due to padding + // image buffers are usually a bit larger than the tight pixel size. + size_t expected_size = image_size.x * image_size.y * sizeof(half) * 4; + if (expected_size > image->size()) { + + std::string err(fmt::format( + "{} Image buffer size of {} does not agree with image pixels size of {} ({}x{}).", + __PRETTY_FUNCTION__, + image->size(), + expected_size, + image_size.x, + image_size.y)); + throw std::runtime_error(err.c_str()); + } + + + // init RGBA float array + thumbnail::ThumbnailBufferPtr r( + new thumbnail::ThumbnailBuffer(image_size.x, image_size.y, thumbnail::TF_RGBF96)); + + // note 'image' is (probably) already in a display space. The offscreen + // viewport has its own instance of ColourPipeline plugin doing the colour + // management. So our colours are normalised to 0-1 range. + + make_conversion_lut(); + + const half *in = (half *)image->buffer(); + float *out = (float *)r->data().data(); + size_t sz = image_size.x * image_size.y; + while (sz--) { + *(out++) = *(in++); + *(out++) = *(in++); + *(out++) = *(in++); + in++; // skip alpha + } - // Thumbanil coord system has y=0 at top of image, whereas GL viewport is - // y=0 at bottom. r->flip(); return r; } -void OffscreenViewportMiddlemanActor::render_to_thumbail( - caf::typed_response_promise rp, - caf::actor media_actor, - const int media_frame, +thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( const thumbnail::THUMBNAIL_FORMAT format, const int width, const bool auto_scale, const bool show_annotations) { - caf::actor playhead_actor; - try { - scoped_actor sys{system()}; - - // make a temporary playhead - playhead_actor = sys->spawn("Offscreen Viewport Playhead"); - - // set the incoming media actor as the source for the playhead - utility::request_receive( - *sys, - playhead_actor, - playhead::source_atom_v, - std::vector({media_actor})); - - // set the playhead frame - utility::request_receive( - *sys, playhead_actor, playhead::jump_atom_v, media_frame); - - // TODO: remove this and find - // std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - - // send a request to the offscreen viewport to - // render an image - request( - offscreen_viewport_, - infinite, - viewport::render_viewport_to_image_atom_v, - playhead_actor, - format, - width, - auto_scale, - show_annotations) - .then( - [=](thumbnail::ThumbnailBufferPtr buf) mutable { - rp.deliver(buf); - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - }, - [=](caf::error &err) mutable { - rp.deliver(err); - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - }); - - // add + media_reader::ImageBufPtr image = viewport_renderer_->get_onscreen_image(); - } catch (std::exception &e) { - if (playhead_actor) - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - rp.deliver(caf::make_error(xstudio_error::error, e.what())); + if (!image) { + std::string err(fmt::format( + "{} Failed to pull images to offscreen renderer.", __PRETTY_FUNCTION__)); + throw std::runtime_error(err.c_str()); } + + const Imath::V2i image_dims = image->image_size_in_pixels(); + if (image_dims.x <= 0 || image_dims.y <= 0) { + std::string err(fmt::format("{} Null image in viewport.", __PRETTY_FUNCTION__)); + throw std::runtime_error(err.c_str()); + } + + float effective_image_height = float(image_dims.y) / image->pixel_aspect(); + + if (width <= 0 || auto_scale) { + viewport_renderer_->set_fit_mode(viewport::FitMode::One2One); + return renderToThumbnail(format, image_dims.x, int(round(effective_image_height))); + } else { + viewport_renderer_->set_fit_mode(viewport::FitMode::Best); + return renderToThumbnail( + format, width, int(round(width * effective_image_height / image_dims.x))); + } +} + +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); + thumbnail::ThumbnailBufferPtr r = rgb96thumbFromHalfFloatImage(image); + r->convert_to(format); + return r; } -void OffscreenViewportMiddlemanActor::render_to_file( + +thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( caf::actor media_actor, const int media_frame, + const thumbnail::THUMBNAIL_FORMAT format, const int width, - const int height, - const caf::uri path, - caf::typed_response_promise rp) { + 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_); - caf::actor playhead_actor; - try { + scoped_actor sys{as_actor()->home_system()}; - scoped_actor sys{system()}; - - // make a temporary playhead - playhead_actor = sys->spawn("Offscreen Viewport Playhead"); - - // set the incoming media actor as the source for the playhead - utility::request_receive( - *sys, - playhead_actor, - playhead::source_atom_v, - std::vector({media_actor})); - - // set the playhead frame - utility::request_receive( - *sys, playhead_actor, playhead::jump_atom_v, media_frame); - - // TODO: remove this and find - // std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - - // send a request to the offscreen viewport to - // render an image - request( - offscreen_viewport_, - infinite, - viewport::render_viewport_to_image_atom_v, - playhead_actor, - width, - height, - 10, - true, - path) - .then( - [=](bool r) mutable { - rp.deliver(r); - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - }, - [=](caf::error &err) mutable { - rp.deliver(err); - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - }); - - // add + // now set the media source on the local playhead + utility::request_receive( + *sys, local_playhead_, playhead::source_atom_v, std::vector({media_actor})); - } catch (std::exception &e) { - if (playhead_actor) - send_exit(playhead_actor, caf::exit_reason::user_shutdown); - rp.deliver(caf::make_error(xstudio_error::error, e.what())); - } -} \ No newline at end of file + // 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); +} diff --git a/src/ui/qt/viewport_widget/src/viewport_widget.cpp b/src/ui/qt/viewport_widget/src/viewport_widget.cpp index 76a42d99a..6d0cb12b4 100644 --- a/src/ui/qt/viewport_widget/src/viewport_widget.cpp +++ b/src/ui/qt/viewport_widget/src/viewport_widget.cpp @@ -21,7 +21,8 @@ void ViewportGLWidget::resizeGL(int w, int h) { Imath::V2f(w, 0), Imath::V2f(w, h), Imath::V2f(0, h), - Imath::V2i(w, h)); + Imath::V2i(w, h), + 1.0f); } void ViewportGLWidget::paintGL() { the_viewport_->render(); } diff --git a/src/ui/viewport/src/CMakeLists.txt b/src/ui/viewport/src/CMakeLists.txt index 33f44bee3..f641f4fb4 100644 --- a/src/ui/viewport/src/CMakeLists.txt +++ b/src/ui/viewport/src/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(${PROJECT_NAME} xstudio::module xstudio::plugin_manager xstudio::utility + xstudio::playhead OpenEXR::OpenEXR Imath::Imath caf::core diff --git a/src/ui/viewport/src/fps_monitor.cpp b/src/ui/viewport/src/fps_monitor.cpp index 07524b3a4..646a4f5d2 100644 --- a/src/ui/viewport/src/fps_monitor.cpp +++ b/src/ui/viewport/src/fps_monitor.cpp @@ -215,7 +215,7 @@ void FpsMonitor::connect_to_playhead(caf::actor &playhead) { } catch (...) { } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } anon_send(this, update_actual_fps_atom_v); } \ 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 e4d0a08a5..265905722 100644 --- a/src/ui/viewport/src/keypress_monitor.cpp +++ b/src/ui/viewport/src/keypress_monitor.cpp @@ -21,8 +21,10 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto link_to(hotkey_config_events_group_); set_down_handler([=](down_msg &msg) { - if (msg.source == actor_grabbing_all_mouse_input_) { - actor_grabbing_all_mouse_input_ = caf::actor(); + 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(); @@ -65,8 +67,10 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto } }, [=](mouse_event_atom, const PointerEvent &e) { - if (actor_grabbing_all_mouse_input_) { - anon_send(actor_grabbing_all_mouse_input_, mouse_event_atom_v, e); + if (actor_grabbing_all_mouse_input_.size()) { + for (auto &a : actor_grabbing_all_mouse_input_) { + anon_send(a, mouse_event_atom_v, e); + } } else { send(keyboard_events_group_, mouse_event_atom_v, e); } @@ -80,9 +84,12 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto }, [=](module::grab_all_mouse_input_atom, caf::actor actor, const bool grab) { if (grab) { - actor_grabbing_all_mouse_input_ = actor; - } else if (actor_grabbing_all_mouse_input_ == actor) { - actor_grabbing_all_mouse_input_ = caf::actor(); + 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)); } }, @@ -125,7 +132,7 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto void KeypressMonitor::on_exit() { system().registry().erase(keyboard_events); actor_grabbing_all_keyboard_input_ = caf::actor(); - actor_grabbing_all_mouse_input_ = caf::actor(); + actor_grabbing_all_mouse_input_.clear(); } void KeypressMonitor::held_keys_changed(const std::string &context, const bool auto_repeat) { diff --git a/src/ui/viewport/src/viewport.cpp b/src/ui/viewport/src/viewport.cpp index 44f655d30..6e9cf8a18 100644 --- a/src/ui/viewport/src/viewport.cpp +++ b/src/ui/viewport/src/viewport.cpp @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #include #include @@ -8,6 +9,7 @@ #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/plugin_manager/plugin_manager.hpp" +#include "xstudio/playhead/playhead_actor.hpp" #include "fps_monitor.hpp" @@ -104,10 +106,13 @@ Viewport::Viewport( const utility::JsonStore &state_data, caf::actor parent_actor, const int viewport_index, - ViewportRendererPtr the_renderer) + ViewportRendererPtr the_renderer, + const std::string &_name) : Module( - viewport_index >= 0 ? fmt::format("viewport{0}", viewport_index) - : fmt::format("offscreen_viewport{0}", abs(viewport_index))), + _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)) { @@ -165,8 +170,12 @@ Viewport::Viewport( const Imath::V4f delta_trans = interact_start_state_.pointer_position_ - normalised_pointer_position() * interact_start_projection_matrix_; - state_.translate_.x = delta_trans.x + interact_start_state_.translate_.x; - state_.translate_.y = delta_trans.y + interact_start_state_.translate_.y; + state_.translate_.x = + (state_.mirror_mode_ & MirrorMode::Flip ? -delta_trans.x : delta_trans.x) + + interact_start_state_.translate_.x; + state_.translate_.y = + (state_.mirror_mode_ & MirrorMode::Flop ? -delta_trans.y : delta_trans.y) + + interact_start_state_.translate_.y; update_matrix(); return true; }; @@ -178,8 +187,8 @@ Viewport::Viewport( normalised_pointer_position() * interact_start_projection_matrix_; const float scale_factor = powf( 2.0, - -delta_trans.x * state_.size_.x * - settings_["pointer_zoom_senistivity"].get() * + (state_.mirror_mode_ & MirrorMode::Flip ? delta_trans.x : -delta_trans.x) * + state_.size_.x * settings_["pointer_zoom_senistivity"].get() * interact_start_state_.scale_ / 1000.0f); state_.scale_ = interact_start_state_.scale_ * scale_factor; const Imath::V4f anchor_before = @@ -287,13 +296,11 @@ Viewport::Viewport( std::string toolbar_name = name() + "_toolbar"; zoom_mode_toggle_->set_role_data( - module::Attribute::Groups, nlohmann::json{toolbar_name, "viewport_zoom_and_pan_modes"}); + module::Attribute::Groups, nlohmann::json{"viewport_zoom_and_pan_modes"}); pan_mode_toggle_->set_role_data( - module::Attribute::Groups, nlohmann::json{toolbar_name, "viewport_zoom_and_pan_modes"}); - fit_mode_->set_role_data(module::Attribute::Groups, nlohmann::json{toolbar_name}); + module::Attribute::Groups, nlohmann::json{"viewport_zoom_and_pan_modes"}); - mirror_mode_->set_role_data(module::Attribute::Groups, nlohmann::json{toolbar_name}); mirror_mode_->set_role_data( module::Attribute::ToolTip, "Set how image is mirrored on screen : flip(on X axis), flop(on Y axis), both, off. " @@ -320,10 +327,23 @@ Viewport::Viewport( 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( @@ -331,7 +351,6 @@ Viewport::Viewport( hud_toggle_ = add_boolean_attribute("Hud", "Hud", true); hud_toggle_->set_tool_tip("Access HUD controls"); - hud_toggle_->expose_in_ui_attrs_group(name() + "_toolbar"); 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 @@ -347,51 +366,31 @@ Viewport::Viewport( )"); hud_toggle_->set_role_data(module::Attribute::ToolbarPosition, 0.0f); - // give the attributes static uuids so that - - bool is_offscreen = false; - 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(); - is_offscreen = viewport_index_ == -1; + fps_monitor_ = sys->spawn(); + bool is_offscreen = viewport_index_ < 0; if (!is_offscreen) connect_to_ui(); - instance_overlay_plugins(!is_offscreen); + instance_overlay_plugins(); get_colour_pipeline(); - if (viewport_index_ == 0) { - if (!is_offscreen) { - a->system().registry().put(main_viewport_registry, a); - } - } else if (viewport_index_ == 1) { - // Popout viewer - other_viewport_ = - a->system().registry().template get(main_viewport_registry); - anon_send(other_viewport_, other_viewport_atom_v, parent_actor_, colour_pipeline_); - } - // join the FPS monitor event group auto group = request_receive(*sys, fps_monitor_, utility::get_event_group_atom_v); utility::request_receive( *sys, group, broadcast::join_broadcast_atom_v, parent_actor_); - // join the global playhead events group - this tells us when the playhead that should - // be on screen changes, among other things - listen_to_playhead_events(); + // 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); - // get current playhead, if there is one: - auto playhead = - request_receive(*sys, ph_events, viewport::viewport_playhead_atom_v); - if (playhead) - set_playhead(playhead); + anon_send(ph_events, viewport_atom_v, name(), parent_actor_); } set_fit_mode(FitMode::Best); @@ -399,12 +398,40 @@ Viewport::Viewport( 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); + + 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); + + // 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); + + auto_connect_to_playhead(true); } 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); + } +} + +void Viewport::link_to_viewport(caf::actor other_viewport) { + + other_viewports_.push_back(other_viewport); + anon_send(other_viewport, other_viewport_atom_v, parent_actor_, colour_pipeline_); } void Viewport::register_hotkeys() { @@ -537,13 +564,9 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { if (pointer_event_handlers_[pointer_event.signature()](pointer_event)) { // Send message to other_viewport_ and pass zoom/pan - if (other_viewport_) { - anon_send( - other_viewport_, - viewport_pan_atom_v, - state_.translate_.x, - state_.translate_.y); - anon_send(other_viewport_, viewport_scale_atom_v, state_.scale_); + 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 || @@ -554,8 +577,8 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { previous_fit_zoom_state_.scale_ = old_scale; state_.fit_mode_ = Free; fit_mode_->set_value("Off"); - if (other_viewport_) { - anon_send(other_viewport_, fit_mode_atom_v, Free); + for (auto &o : other_viewports_) { + anon_send(o, fit_mode_atom_v, Free); } } } @@ -572,7 +595,8 @@ bool Viewport::set_scene_coordinates( const Imath::V2f topright, const Imath::V2f bottomright, const Imath::V2f bottomleft, - const Imath::V2i scene_size) { + const Imath::V2i scene_size, + const float devicePixelRatio) { // These coordinates describe the quad into which the viewport // will be rendered in the coordinate system of the parent 'canvas'. @@ -598,7 +622,10 @@ bool Viewport::set_scene_coordinates( if (vp2c != viewport_to_canvas_ || (bottomright - bottomleft).length() != state_.size_.x || (bottomleft - topleft).length() != state_.size_.y) { viewport_to_canvas_ = vp2c; - set_size((bottomright - bottomleft).length(), (bottomleft - topleft).length()); + set_size( + (bottomright - bottomleft).length(), + (bottomleft - topleft).length(), + devicePixelRatio); return true; } return false; @@ -648,7 +675,12 @@ void Viewport::update_fit_mode_matrix( } else if (fit_mode() == One2One && state_.image_size_.x) { - state_.fit_mode_zoom_ = float(state_.image_size_.x) / 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_); + + state_.fit_mode_zoom_ = float(state_.image_size_.x) / screen_pix_size_x; // 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: @@ -656,11 +688,11 @@ void Viewport::update_fit_mode_matrix( // screen pixels. Floating point errors result in samples jumping to // 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(state_.size_.x)) & 1)) { - tx = 0.5f / state_.size_.x; + if ((state_.image_size_.x & 1) != (int(round(screen_pix_size_x)) & 1)) { + tx = 0.5f / screen_pix_size_x; } - if ((state_.image_size_.y & 1) != (int(round(state_.size_.y)) & 1)) { - ty = 0.5f / state_.size_.y; + if ((state_.image_size_.y & 1) != (int(round(screen_pix_size_y)) & 1)) { + ty = 0.5f / screen_pix_size_y; } } @@ -678,8 +710,9 @@ void Viewport::set_scale(const float scale) { update_matrix(); } -void Viewport::set_size(const float w, const float h) { - state_.size_ = Imath::V2f(w, h); +void Viewport::set_size(const float w, const float h, const float devicePixelRatio) { + state_.size_ = Imath::V2f(w, h); + devicePixelRatio_ = devicePixelRatio; update_matrix(); } @@ -761,7 +794,7 @@ void Viewport::set_pixel_zoom(const float zoom) { } } -void Viewport::revert_fit_zoom_to_previous() { +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_); @@ -779,12 +812,18 @@ void Viewport::revert_fit_zoom_to_previous() { else if (state_.fit_mode_ == FitMode::Fill) fit_mode_->set_value("Fill"); else if (state_.fit_mode_ == FitMode::Free) - fit_mode_->set_value("Off"); + 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"); + } + } } void Viewport::switch_mirror_mode() { @@ -825,27 +864,23 @@ Imath::V2i Viewport::raw_pointer_position() const { return state_.raw_pointer_po void Viewport::update_matrix() { + const float flipFactor = (state_.mirror_mode_ & MirrorMode::Flip) ? -1.0f : 1.0f; + const float flopFactor = (state_.mirror_mode_ & MirrorMode::Flop) ? -1.0f : 1.0f; + inv_projection_matrix_.makeIdentity(); inv_projection_matrix_.scale(Imath::V3f(1.0f, -1.0f, 1.0f)); - inv_projection_matrix_.scale(Imath::V3f(1.0f, state_.size_.x / state_.size_.y, 1.0f)); + inv_projection_matrix_.scale(Imath::V3f(1.0, state_.size_.x / state_.size_.y, 1.0f)); inv_projection_matrix_.scale(Imath::V3f(state_.scale_, state_.scale_, state_.scale_)); inv_projection_matrix_.translate( Imath::V3f(-state_.translate_.x, -state_.translate_.y, 0.0f)); + inv_projection_matrix_.scale(Imath::V3f(flipFactor, flopFactor, 1.0)); projection_matrix_.makeIdentity(); + projection_matrix_.scale(Imath::V3f(flipFactor, flopFactor, 1.0)); projection_matrix_.translate(Imath::V3f(state_.translate_.x, state_.translate_.y, 0.0f)); projection_matrix_.scale( Imath::V3f(1.0f / state_.scale_, 1.0f / state_.scale_, 1.0f / state_.scale_)); - - float scale_factor_x = 1.0f; - if (state_.mirror_mode_ == MirrorMode::Flop || state_.mirror_mode_ == MirrorMode::Both) - scale_factor_x = -scale_factor_x; - - float scale_factor_y = state_.size_.y / state_.size_.x; - if (state_.mirror_mode_ == MirrorMode::Flip || state_.mirror_mode_ == MirrorMode::Both) - scale_factor_y = -scale_factor_y; - - projection_matrix_.scale(Imath::V3f(scale_factor_x, scale_factor_y, 1.0f)); + 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(); @@ -862,8 +897,16 @@ Imath::Box2f Viewport::image_bounds_in_viewport_pixels() const { Imath::Vec4 b(1.0f, aspect, 0.0f, 1.0f); b *= m; - Imath::V2f topLeft((a.x / a.w + 1.0f) / 2.0f, (-a.y / a.w + 1.0f) / 2.0f); - Imath::V2f bottomRight((b.x / b.w + 1.0f) / 2.0f, (-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. + + 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; + + Imath::V2f topLeft(std::min(x0, x1), std::max(y0, y1)); + Imath::V2f bottomRight(std::max(x0, x1), std::min(y0, y1)); return Imath::Box2f(topLeft, bottomRight); } @@ -897,10 +940,16 @@ caf::message_handler Viewport::message_handler() { const Imath::V2f &topright, const Imath::V2f &bottomright, const Imath::V2f &bottomleft, - const Imath::V2i &scene_size) { + const Imath::V2i &scene_size, + const float devicePixelRatio) { float zoom = pixel_zoom(); if (set_scene_coordinates( - topleft, topright, bottomright, bottomleft, scene_size)) { + topleft, + topright, + bottomright, + bottomleft, + scene_size, + devicePixelRatio)) { if (zoom != pixel_zoom()) { event_callback_(ZoomChanged); } @@ -913,21 +962,58 @@ caf::message_handler Viewport::message_handler() { [=](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++; + } + + 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++; + } + } + unlink_module(other_view); + } + }, + [=](other_viewport_atom, caf::actor other_view, caf::actor other_colour_pipeline) { - other_viewport_ = other_view; - - // 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) - ); + 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) + ); + } }, [=](colour_pipeline::colour_pipeline_atom) -> caf::actor { @@ -963,13 +1049,21 @@ caf::message_handler Viewport::message_handler() { event_callback_(Redraw); }, - [=](viewport_playhead_atom, caf::actor playhead) -> bool { + [=](viewport_playhead_atom, caf::actor playhead, bool pin) -> bool { + playhead_pinned_ = pin; set_playhead(playhead); return true; }, + [=](viewport_playhead_atom, caf::actor playhead) -> bool { + if (!playhead_pinned_) + set_playhead(playhead); + return true; + }, + [=](utility::event_atom, viewport_playhead_atom, caf::actor playhead) { - set_playhead(playhead); + if (!playhead_pinned_) + set_playhead(playhead); }, [=](viewport_playhead_atom) -> caf::actor_addr { return playhead_addr_; }, @@ -993,7 +1087,20 @@ caf::message_handler Viewport::message_handler() { event_callback_(Redraw); }, - [=](const error &err) mutable {} + [=](quickview_media_atom, + std::vector &media_items, + std::string compare_mode) { quickview_media(media_items, compare_mode); }, + + [=](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); + }, + + [=](const error &err) mutable { std::cerr << "ERR " << to_string(err) << "\n"; } }) .or_else(module::Module::message_handler()); @@ -1001,19 +1108,30 @@ caf::message_handler Viewport::message_handler() { void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { - spdlog::debug("Viewport::set_playhead {0}", to_string(playhead)); - // if null playhead stop here. if (!parent_actor_) { - // set_new_playhead(utility::Uuid()); return; } + caf::actor old_playhead(caf::actor_cast(playhead_addr_)); + + 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); + } + auto a = caf::actor_cast(parent_actor_); caf::scoped_actor sys(a->system()); try { + // leave previous playhead's broacast events group if (playhead_viewport_events_group_) { try { @@ -1082,12 +1200,18 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { } } - 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) - anon_send(ph_events, viewport::viewport_playhead_atom_v, playhead); + 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); + } + + anon_send( + playhead, connect_to_viewport_toolbar_atom_v, name(), name() + "_toolbar", true); } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -1106,7 +1230,10 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { } void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) { + + if (attr_uuid == fit_mode_->uuid()) { + const std::string mode = fit_mode_->value(); if (mode == "1:1") set_fit_mode(FitMode::One2One); @@ -1121,61 +1248,19 @@ void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) else set_fit_mode(FitMode::Free); - // bind fit mode between the two viewports (main and popout) - // don't do that! - /*if(other_viewport_){ - anon_send(other_viewport_, - xstudio::module::change_attribute_value_atom_v, - fit_mode_->get_role_data(module::Attribute::Title), - utility::JsonStore(fit_mode_->value()), - true); - }*/ } else if (attr_uuid == zoom_mode_toggle_->uuid() && role == module::Attribute::Value) { if (zoom_mode_toggle_->value()) { pan_mode_toggle_->set_value(false); - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - pan_mode_toggle_->get_role_data(module::Attribute::Title), - utility::JsonStore(false), - false); - } - } - - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - zoom_mode_toggle_->get_role_data(module::Attribute::Title), - utility::JsonStore(zoom_mode_toggle_->value()), - false); } } else if (attr_uuid == pan_mode_toggle_->uuid() && role == module::Attribute::Value) { if (pan_mode_toggle_->value()) { zoom_mode_toggle_->set_value(false); - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - zoom_mode_toggle_->get_role_data(module::Attribute::Title), - utility::JsonStore(false), - false); - } } - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - pan_mode_toggle_->get_role_data(module::Attribute::Title), - utility::JsonStore(pan_mode_toggle_->value()), - false); - } } else if (attr_uuid == filter_mode_preference_->uuid()) { const std::string filter_mode_pref = filter_mode_preference_->value(); @@ -1186,43 +1271,7 @@ void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) } event_callback_(Redraw); - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - filter_mode_preference_->get_role_data(module::Attribute::Title), - utility::JsonStore(filter_mode_preference_->value()), - true); - } - - - } else if (attr_uuid == texture_mode_preference_->uuid()) { - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - texture_mode_preference_->get_role_data(module::Attribute::Title), - utility::JsonStore(texture_mode_preference_->value()), - true); - } - } else if (attr_uuid == mouse_wheel_behaviour_->uuid()) { - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - mouse_wheel_behaviour_->get_role_data(module::Attribute::Title), - utility::JsonStore(mouse_wheel_behaviour_->value()), - true); - } } else if (attr_uuid == hud_toggle_->uuid()) { - if (other_viewport_) { - anon_send( - other_viewport_, - xstudio::module::change_attribute_value_atom_v, - hud_toggle_->get_role_data(module::Attribute::Title), - utility::JsonStore(hud_toggle_->value()), - true); - } for (auto &p : hud_plugin_instances_) { anon_send(p.second, enable_hud_atom_v, hud_toggle_->value()); } @@ -1248,8 +1297,11 @@ void Viewport::update_attrs_from_preferences(const utility::JsonStore &j) { 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) { + + if (!context.empty() && context != name()) + return; + if (hotkey_uuid == zoom_hotkey_) { zoom_mode_toggle_->set_role_data(module::Attribute::Activated, true); zoom_mode_toggle_->set_value(true); @@ -1294,9 +1346,7 @@ void Viewport::update_onscreen_frame_info(const media_reader::ImageBufPtr &frame return; } - if (about_to_go_on_screen_frame_buffer_ != frame) { - about_to_go_on_screen_frame_buffer_ = frame; - } + about_to_go_on_screen_frame_buffer_ = frame; // check if the frame buffer has some error message attached if (frame->error_state()) { @@ -1330,30 +1380,61 @@ void Viewport::update_onscreen_frame_info(const media_reader::ImageBufPtr &frame } } -void Viewport::framebuffer_swapped() { +void Viewport::framebuffer_swapped(const utility::time_point swap_time) { anon_send( display_frames_queue_actor_, ui::fps_monitor::framebuffer_swapped_atom_v, - utility::clock::now(), + swap_time, screen_refresh_period_, viewport_index_); + static auto tp = utility::clock::now(); + auto t0 = utility::clock::now(); + if (about_to_go_on_screen_frame_buffer_ != on_screen_frame_buffer_) { on_screen_frame_buffer_ = about_to_go_on_screen_frame_buffer_; - int f = 0; + + 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(); } + + /*static std::map ff; + if ((f-ff[viewport_index_]) != 1) { + + std::cerr << name() << " frame missed " << f << " " << ff[viewport_index_] << " " << + std::chrono::duration_cast(t0-tp).count() << "\n"; + + } + + ff[viewport_index_] = f;*/ + anon_send( fps_monitor(), 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";*/ } + + 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]; } void Viewport::get_frames_for_display(std::vector &next_images) { @@ -1388,8 +1469,7 @@ void Viewport::get_frames_for_display(std::vector &ne colour_pipeline_, std::chrono::milliseconds(1000), colour_pipeline::colour_operation_uniforms_atom_v, - image.frame_id(), - image.colour_pipe_data_); + image); } if (next_images.size()) { @@ -1401,6 +1481,31 @@ void Viewport::get_frames_for_display(std::vector &ne update_fit_mode_matrix(image_dims.x, image_dims.y, image->pixel_aspect()); } + std::vector going_on_screen; + if (next_images.size()) { + + 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, + next_images.front(), + name()); + + next_images.front().add_plugin_blind_data2(overlay_actor_uuid, bdata); + } + + going_on_screen.push_back(next_images.front()); + } + + // 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 (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -1408,7 +1513,7 @@ void Viewport::get_frames_for_display(std::vector &ne t1_ = utility::clock::now(); } -void Viewport::instance_overlay_plugins(const bool share_plugin_instances) { +void Viewport::instance_overlay_plugins() { if (!parent_actor_) return; @@ -1417,6 +1522,12 @@ void Viewport::instance_overlay_plugins(const bool share_plugin_instances) { try { + // Each viewport instance has its own instance of the 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 overlay_plugin_details = @@ -1424,41 +1535,43 @@ void Viewport::instance_overlay_plugins(const bool share_plugin_instances) { *sys, pm, utility::detail_atom_v, - plugin_manager::PluginType::PT_VIEWPORT_OVERLAY); + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY)); for (const auto &pd : overlay_plugin_details) { - if (true) { // pd.enabled_) { + + if (pd.enabled_) { // Note the use of the singleton flag on spawning - if this // plugin has already been spawned we want to use the existing // instance - hence the pop-out viewport will share the plugin // with the main viewport - overlay_actor_ = request_receive( - *sys, - pm, - plugin_manager::spawn_plugin_atom_v, - pd.uuid_, - utility::JsonStore(), - share_plugin_instances // this is the 'singleton' flag - ); - - // Note on that 'singleton' flag. We want the pop-out viewer and - // main viewport to share viewport plugin instances because they - // should both show the same overlay graphics. Other viewport - // instances (e.g. offscreen viewport for rendering images) needs - // it's own overlay plugins because it won't necessarily be - // rendering the same content as what's on screen in the GUI. - - if (share_plugin_instances) - anon_send(overlay_actor_, module::connect_to_ui_atom_v); - - auto funkydunc = request_receive( - *sys, overlay_actor_, overlay_render_function_atom_v, viewport_index_); - - if (funkydunc) { - the_renderer_->add_overlay_renderer(pd.uuid_, funkydunc); + 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); + } + + auto overlay_renderer = request_receive( + *sys, overlay_actor, overlay_render_function_atom_v, viewport_index_); + + if (overlay_renderer) { + the_renderer_->add_overlay_renderer(pd.uuid_, overlay_renderer); } - overlay_plugin_instances_[pd.uuid_] = overlay_actor_; + auto pre_render_hook = request_receive( + *sys, overlay_actor, pre_render_gpu_hook_atom_v, viewport_index_); + + if (pre_render_hook) { + the_renderer_->add_pre_renderer_hook(pd.uuid_, pre_render_hook); + } + + overlay_plugin_instances_[pd.uuid_] = overlay_actor; } } @@ -1466,32 +1579,43 @@ void Viewport::instance_overlay_plugins(const bool share_plugin_instances) { that they are activated through a single HUD pop-up in the toolbar and the are 'aware' of the screenspace that other HUDs have already occupied */ auto hud_plugin_details = request_receive>( - *sys, pm, utility::detail_atom_v, plugin_manager::PluginType::PT_HEAD_UP_DISPLAY); + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY)); for (const auto &pd : hud_plugin_details) { - if (true) { // pd.enabled_) { - overlay_actor_ = request_receive( - *sys, - pm, - plugin_manager::spawn_plugin_atom_v, - pd.uuid_, - utility::JsonStore(), - share_plugin_instances // this is the 'singleton' flag - ); + if (pd.enabled_) { + auto overlay_actor = request_receive( + *sys, pm, plugin_manager::spawn_plugin_atom_v, pd.uuid_, plugin_init_data); - if (share_plugin_instances) - anon_send(overlay_actor_, module::connect_to_ui_atom_v); + 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); + } - auto funkydunc = request_receive( - *sys, overlay_actor_, overlay_render_function_atom_v, viewport_index_); + auto overlay_renderer = request_receive( + *sys, overlay_actor, overlay_render_function_atom_v, viewport_index_); - if (funkydunc) { - the_renderer_->add_overlay_renderer(pd.uuid_, funkydunc); + if (overlay_renderer) { + the_renderer_->add_overlay_renderer(pd.uuid_, overlay_renderer); } - 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()); + auto pre_render_hook = request_receive( + *sys, overlay_actor, pre_render_gpu_hook_atom_v, viewport_index_); + + if (pre_render_hook) { + the_renderer_->add_pre_renderer_hook(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()); } } @@ -1540,8 +1664,7 @@ media_reader::ImageBufPtr Viewport::get_image_from_playhead(caf::actor playhead) colour_pipeline_, std::chrono::milliseconds(1000), colour_pipeline::colour_operation_uniforms_atom_v, - image.frame_id(), - image.colour_pipe_data_); + image); // get the overlay plugins to generate their data for onscreen rendering // (e.g. annotations strokes) and add to the image @@ -1554,6 +1677,11 @@ media_reader::ImageBufPtr Viewport::get_image_from_playhead(caf::actor playhead) *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()); + + image.add_plugin_blind_data2(overlay_actor_uuid, bdata2); } return image; @@ -1577,19 +1705,26 @@ void Viewport::get_colour_pipeline() { if (colour_pipeline_ != colour_pipe) { colour_pipeline_ = colour_pipe; - } - if (viewport_index_ >= 0) { - // negative index is offscreen - anon_send(colour_pipeline_, module::connect_to_ui_atom_v); - anon_send( - colour_pipeline_, - colour_pipeline::connect_to_viewport_atom_v, - self(), - name(), - viewport_index_); + auto colour_pipe_gpu_hook = request_receive( + *sys, colour_pipeline_, pre_render_gpu_hook_atom_v, viewport_index_); + if (colour_pipe_gpu_hook) { + the_renderer_->add_pre_renderer_hook( + 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_, + colour_pipeline::connect_to_viewport_atom_v, + self(), + name(), + name() + "_toolbar", + true); + anon_send( display_frames_queue_actor_, colour_pipeline::colour_pipeline_atom_v, @@ -1615,4 +1750,90 @@ void Viewport::set_screen_infos( serialNumber); if (refresh_rate) screen_refresh_period_ = timebase::to_flicks(1.0 / refresh_rate); -} \ No newline at end of file +} + +void Viewport::quickview_media(std::vector &media_items, std::string compare_mode) { + + // 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()); + + if (!quickview_playhead_) { + // create a new quickview playhead, or use existing one. + quickview_playhead_ = sys->spawn("QuickviewPlayhead"); + } + // set the compare mode + anon_send( + quickview_playhead_, + module::change_attribute_request_atom_v, + std::string("Compare"), + (int)module::Attribute::Value, + utility::JsonStore(compare_mode)); + + // make the playhead view the media + anon_send(quickview_playhead_, playhead::source_atom_v, media_items); + + // view the playhead + set_playhead(quickview_playhead_, true); + + playhead_pinned_ = true; +} + +void Viewport::auto_connect_to_playhead(bool auto_connect) { + + listen_to_playhead_events(auto_connect); + + if (auto_connect) { + + // 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()); + + auto ph_events = + a->system().registry().template get(global_playhead_events_actor); + + auto playhead = + request_receive(*sys, ph_events, viewport::viewport_playhead_atom_v); + + if (playhead) + set_playhead(playhead); + } +} + +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(); + } + } 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 c6e9de0ab..17dbe83dd 100644 --- a/src/ui/viewport/src/viewport_frame_queue_actor.cpp +++ b/src/ui/viewport/src/viewport_frame_queue_actor.cpp @@ -18,6 +18,8 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( overlay_actors_(std::move(overlay_actors)), viewport_index_(viewport_index) { + print_on_exit(this, "ViewportFrameQueueActor"); + set_default_handler(caf::drop); set_down_handler([=](down_msg &msg) { @@ -164,7 +166,7 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( std::vector future_bufs) { // now insert the new future frames ready for drawing for (auto &buf : future_bufs) { - if (buf && buf.colour_pipe_data_) { + if (buf) { queue_image_buffer_for_drawing(buf, playhead_uuid); } } @@ -456,9 +458,11 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi if (!playhead_) return timebase::flicks(0); - const timebase::flicks video_refresh_period = compute_video_refresh(); + 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 { @@ -498,6 +502,7 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi 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() + @@ -509,6 +514,17 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi 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); + } } return rounded_phase_adjusted_tp; @@ -527,10 +543,68 @@ xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( // and then make up an appropriate refresh time if we need to. utility::time_point last_vid_refresh; if (video_refresh_data_.refresh_history_.empty()) { + last_vid_refresh = utility::clock::now() - std::chrono::duration_cast(video_refresh_period); + + } else if (video_refresh_data_.refresh_history_.size() > 64) { + + // refresh_history_ is a list of recent timepoints (system steady clock) when we were + // told (utlimately by Qt or graphics driver) that the video frame buffer was swapped. + // We're using this data to predict when the video buffer will be swapped to the + // screen NEXT time and therefore pick the correct frame to go up on the screen. + // + // We might know the video refresh exactly, or we might have been lied to, but either + // way we need to know the phase of the refresh beat to predict when the next refresh + // is due. So we need to fit a line to the video refresh events (as measured by the + // system clock) and filter out events that are innaccurate and also take account + // of moments when a video refresh was missed completely. + + + // average cadence of video refresh... + const double expected_video_refresh_period = average_video_refresh_period(); + + // Here we are essentially fitting a straight line to the video refresh event + // timepoints - we use the line to predict when the next video refresh is + // going to happen. + auto now = utility::clock::now(); + double next_refresh = 0.0; + double refresh_event_index = 1.0; + double estimate_count = 0.0; + auto p = video_refresh_data_.refresh_history_.rbegin(); + auto p2 = p; + p2++; + while (p2 != video_refresh_data_.refresh_history_.rend()) { + + // period between subsequent video refreshes + auto delta = std::chrono::duration_cast(*p - *p2); + + // how many whole video refresh beats is this? It's possible that sometimes + // a redraw doesn't happen within the video refresh period. We need to take + // account of that when using the timepoints of video refreshes to predict + // the next refresh + double n_refreshes_between_events = + round(timebase::to_seconds(delta) / expected_video_refresh_period); + + auto estimate_refresh = + timebase::to_seconds(std::chrono::duration_cast(*p - now)) + + refresh_event_index * expected_video_refresh_period; + next_refresh += estimate_refresh; + estimate_count++; + p++; + p2++; + refresh_event_index += n_refreshes_between_events; + } + + next_refresh *= 1.0 / estimate_count; + auto offset = std::chrono::duration_cast( + timebase::to_flicks(next_refresh)); + auto result = now + offset; + return result; + } else { + if (std::chrono::duration_cast( utility::clock::now() - video_refresh_data_.last_video_refresh_) < timebase::k_flicks_one_fifteenth_second) { @@ -554,27 +628,6 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { } else if (video_refresh_data_.refresh_history_.size() > 64) { - // 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; - deltas.reserve(video_refresh_data_.refresh_history_.size()); - auto p = video_refresh_data_.refresh_history_.begin(); - auto pp = p; - pp++; - while (pp != video_refresh_data_.refresh_history_.end()) { - deltas.push_back(std::chrono::duration_cast(*pp - *p)); - pp++; - p++; - } - std::sort(deltas.begin(), deltas.end()); - - auto r = deltas.begin() + 8; - int ct = deltas.size() - 16; - timebase::flicks t(0); - while (ct--) { - t += *(r++); - } - // This measurement of the refresh rate is only accurate if the UI layer // (probably Qt) is giving us time-accurate signals when the GLXSwapBuffers // call completes. Also the assumption is that the UI redraw is limited to @@ -584,7 +637,8 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { // Assume 24fps is the minimum refresh we'll ever encounter const int hertz_refresh = - std::max(24, int(round(float(deltas.size() - 16)) / timebase::to_seconds(t))); + std::max(24, int(round(1.0 / average_video_refresh_period()))); + static const std::vector common_refresh_rates( {24, 25, 30, 48, 60, 75, 90, 120, 144, 240, 360}); auto match = std::lower_bound( @@ -596,4 +650,30 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { // default fallback to 60Hz return timebase::k_flicks_one_sixtieth_second; -} \ No newline at end of file +} + +double ViewportFrameQueueActor::average_video_refresh_period() const { + + // 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; + deltas.reserve(video_refresh_data_.refresh_history_.size()); + auto p = video_refresh_data_.refresh_history_.begin(); + auto pp = p; + pp++; + while (pp != video_refresh_data_.refresh_history_.end()) { + deltas.push_back(std::chrono::duration_cast(*pp - *p)); + pp++; + p++; + } + std::sort(deltas.begin(), deltas.end()); + + auto r = deltas.begin() + 8; + int ct = deltas.size() - 16; + timebase::flicks t(0); + while (ct--) { + t += *(r++); + } + + return timebase::to_seconds(t) / (double(deltas.size() - 16)); +} diff --git a/src/utility/src/CMakeLists.txt b/src/utility/src/CMakeLists.txt index 0a21b08a3..38b4e8821 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -2,18 +2,24 @@ find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(Imath REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(PkgConfig REQUIRED) -pkg_search_module(UUID REQUIRED uuid) +find_package(ZLIB REQUIRED) +if(WIN32) + # Not Required +elseif(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_search_module(UUID REQUIRED uuid) +endif() + SET(LINK_DEPS + PUBLIC caf::core fmt::fmt Imath::Imath nlohmann_json::nlohmann_json - reproc++ spdlog::spdlog - stdc++fs uuid + ZLIB::ZLIB ) SET(STATIC_LINK_DEPS @@ -21,11 +27,14 @@ SET(STATIC_LINK_DEPS fmt::fmt Imath::Imath nlohmann_json::nlohmann_json - reproc++ spdlog::spdlog - stdc++fs uuid + ZLIB::ZLIB ) +if(UNIX AND NOT APPLE) + 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}") +create_component_static(utility 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file diff --git a/src/utility/src/chrono.cpp b/src/utility/src/chrono.cpp index 2fda69403..2250180c9 100644 --- a/src/utility/src/chrono.cpp +++ b/src/utility/src/chrono.cpp @@ -5,6 +5,10 @@ xstudio::utility::sys_time_point xstudio::utility::to_sys_time_point(const std::string &datetime) { std::istringstream in{datetime}; sys_time_point tp; +#ifdef _WIN32 +// TODO: Ahead to fix +#else in >> date::parse("%Y-%m-%dT%TZ", tp); +#endif return tp; } diff --git a/src/utility/src/container.cpp b/src/utility/src/container.cpp index 4fcf75bb5..59cd1894c 100644 --- a/src/utility/src/container.cpp +++ b/src/utility/src/container.cpp @@ -42,6 +42,14 @@ void Container::deserialise(const utility::JsonStore &jsn) { last_changed_ = utility::clock::now(); } +Container Container::duplicate() const { + Container result = *this; + + result.set_uuid(Uuid::generate()); + + return result; +} + JsonStore Container::serialise() const { utility::JsonStore jsn; diff --git a/src/utility/src/edit_list.cpp b/src/utility/src/edit_list.cpp index a446112a1..df717851b 100644 --- a/src/utility/src/edit_list.cpp +++ b/src/utility/src/edit_list.cpp @@ -1,5 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef _WIN32 +#include +#else #include +#endif #include #include diff --git a/src/utility/src/file_system_item.cpp b/src/utility/src/file_system_item.cpp new file mode 100644 index 000000000..1b15f2c1b --- /dev/null +++ b/src/utility/src/file_system_item.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/utility/file_system_item.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio::utility; + +FileSystemItem::FileSystemItem(const fs::directory_entry &entry) : FileSystemItems() { + if (fs::is_regular_file(entry.status())) + type_ = FSIT_FILE; + else + type_ = FSIT_DIRECTORY; + + path_ = posix_path_to_uri(path_to_string(entry.path())); + // last_write_ = fs::last_write_time(entry.path()); + name_ = path_to_string(entry.path().filename()); +} + +bool FileSystemItem::scan(const int depth, const bool ignore_last_write) { + auto changed = false; + try { + switch (type_) { + case FSIT_NONE: + case FSIT_FILE: + break; + + case FSIT_ROOT: + if (depth) + for (auto &i : *this) + changed |= i.scan(depth - 1, ignore_last_write); + break; + + case FSIT_DIRECTORY: { + // check path exists. + auto path = uri_to_posix_path(path_); + auto update = false; + std::map child_paths; + + for (auto it = begin(); it != end(); ++it) + child_paths.insert( + std::pair(it->path(), it)); + + if (not fs::exists(path)) { + // we only deal with our own children.. + // erase children + FileSystemItems::clear(); + set_last_write(); + changed = true; + } else { + auto scan = ignore_last_write; + + if (not ignore_last_write) { + auto last_write = fs::last_write_time(path); + if (last_write != last_write_) { + scan = true; + update = true; + last_write_ = last_write; + } + } + + if (scan) { + // remove children when we see themm. + for (const auto &entry : fs::directory_iterator(path)) { + try { + if (ignore_entry_callback_ == nullptr or + not ignore_entry_callback_(entry)) { + auto cpath = posix_path_to_uri(path_to_string(entry.path())); + + // is new entry ? + FileSystemItems::iterator it = end(); + + if (child_paths.count(cpath)) { + it = child_paths.at(cpath); + child_paths.erase(cpath); + } else { + it = insert(end(), FileSystemItem(entry)); + changed = true; + } + + if (it->type() == FSIT_DIRECTORY) { + if (depth) { + changed |= it->scan(depth - 1, ignore_last_write); + } + } else { + it->set_last_write(fs::last_write_time(path)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + for (auto &i : child_paths) { + erase(i.second); + changed = true; + } + + } else { + if (depth) { + for (auto it = begin(); it != end(); ++it) { + if (it->type() == FSIT_DIRECTORY) { + changed |= it->scan(depth - 1, ignore_last_write); + } + } + } + } + } + } break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return changed; +} + +JsonStore FileSystemItem::dump() const { + auto result = JsonStore( + R"({"type_name": null, "type": null, "last_write": null, "name": null, "path": null})"_json); + + result["type_name"] = to_string(type_); + result["type"] = type_; + result["last_write"] = last_write_; + result["name"] = name_; + result["path"] = to_string(path_); + + if (type_ == FSIT_ROOT or type_ == FSIT_DIRECTORY) { + auto children = nlohmann::json::array(); + + for (const auto &i : *this) { + children.emplace_back(i.dump()); + } + + result["children"] = children; + } + + return result; +} + +// void FileSystemItem::bind_event_func(FileSystemItemEventFunc fn) { +// event_callback_ = [fn](auto &&PH1, auto &&PH2) { +// return fn(std::forward(PH1), std::forward(PH2)); +// }; + +// for (auto &i : *this) +// i.bind_event_func(fn); +// } + +void FileSystemItem::bind_ignore_entry_func(FileSystemItemIgnoreFunc fn) { + ignore_entry_callback_ = [fn](auto &&PH1) { return fn(std::forward(PH1)); }; + + for (auto &i : *this) + i.bind_ignore_entry_func(fn); +} + +FileSystemItems::iterator +FileSystemItem::insert(FileSystemItems::iterator position, const FileSystemItem &val) { + auto it = FileSystemItems::insert(position, val); + it->bind_ignore_entry_func(ignore_entry_callback_); + return it; +} + +FileSystemItems::iterator FileSystemItem::erase(FileSystemItems::iterator position) { + return FileSystemItems::erase(position); +} + + +bool xstudio::utility::ignore_not_session(const fs::directory_entry &entry) { + auto result = false; + + if (fs::is_regular_file(entry.status())) { + auto ext = to_lower(path_to_string(entry.path().extension())); + if (ext != ".xst" and ext != ".xsz") + result = true; + } + + return result; +} + + +FileSystemItem *FileSystemItem::find_by_path(const caf::uri &path) { + FileSystemItem *result = nullptr; + + if (path == path_) + result = this; + else { + for (auto &i : *this) { + result = i.find_by_path(path); + if (result) + break; + } + } + + return result; +} diff --git a/src/utility/src/frame_list.cpp b/src/utility/src/frame_list.cpp index 9159d1ddc..c9dcee03c 100644 --- a/src/utility/src/frame_list.cpp +++ b/src/utility/src/frame_list.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include "xstudio/utility/helpers.hpp" + #include #include @@ -7,7 +9,6 @@ #include #include "xstudio/utility/frame_list.hpp" -#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/string_helpers.hpp" @@ -51,7 +52,7 @@ int FrameGroup::frame(const size_t index, const bool implied, const bool valid) if (implied) { if (valid) { // find previous valid frame. - _frame = ((index / step_) * step_) + start_; + _frame = (int)((index / step_) * step_) + start_; } else _frame = start_ + index; } else { @@ -261,14 +262,23 @@ xstudio::utility::frame_groups_from_sequence_spec(const caf::uri &from_path) { std::string path = uri_to_posix_path(from_path); const std::regex spec_re("\\{[^}]+\\}"); const std::regex path_re("^" + std::regex_replace(path, spec_re, "([0-9-]+)") + "$"); +#ifdef _WIN32 + std::smatch m; +#else std::cmatch m; +#endif std::set frames; for (const auto &entry : fs::directory_iterator(fs::path(path).parent_path())) { if (not fs::is_regular_file(entry.status())) { continue; } - if (std::regex_match(entry.path().c_str(), m, path_re)) { +#ifdef _WIN32 + auto entryPath = entry.path().string(); // Convert to std::string +#else + auto entryPath = entry.path().c_str(); +#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()) { frames.insert(frame); diff --git a/src/utility/src/frame_rate_and_duration.cpp b/src/utility/src/frame_rate_and_duration.cpp index 4cadccb78..01d781614 100644 --- a/src/utility/src/frame_rate_and_duration.cpp +++ b/src/utility/src/frame_rate_and_duration.cpp @@ -1,5 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef _WIN32 +#include +#else #include +#endif #include #include @@ -25,9 +29,9 @@ double FrameRateDuration::seconds(const FrameRate &override) const { int FrameRateDuration::frames(const FrameRate &override) const { long int frames = 0; if (override.count()) { - frames = std::round(duration_ / override); + frames = (long)std::round(duration_ / override); } else if (rate_.count()) { - frames = std::round(duration_ / rate_); + frames = (long)std::round(duration_ / rate_); } return static_cast(frames); } diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 33283f2ec..471d6e333 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -1,26 +1,32 @@ // SPDX-License-Identifier: Apache-2.0 -#define __USE_POSIX +#include "xstudio/utility/helpers.hpp" +#ifdef __linux__ +#define __USE_POSIX +#include +#include +#include +#include +#endif #include #include #include #include -#include #include -#include -#include -#include #include -#include -#include +// #include +// #include #include "xstudio/utility/frame_list.hpp" -#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/sequence.hpp" #include "xstudio/utility/string_helpers.hpp" +#ifndef MAXHOSTNAMELEN +#define MAXHOSTNAMELEN 256 +#endif + using namespace xstudio::utility; using namespace caf; namespace fs = std::filesystem; @@ -37,6 +43,11 @@ namespace fs = std::filesystem; static std::shared_ptr s_actor_system_singleton; +caf::actor_system &ActorSystemSingleton::actor_system_ref() { + assert(s_actor_system_singleton); + return s_actor_system_singleton->get_system(); +} + caf::actor_system &ActorSystemSingleton::actor_system_ref(caf::actor_system &sys) { // Note that this function is called when instancing the xstudio python module and @@ -73,7 +84,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::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + spdlog::debug("{} {}", __PRETTY_FUNCTION__, err.what()); } return result; @@ -99,6 +110,7 @@ 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) .then( [=](const bool) mutable {}, @@ -167,8 +179,28 @@ void xstudio::utility::leave_event_group(caf::event_based_actor *source, caf::ac } +void xstudio::utility::print_on_exit(const caf::actor &hdl, const Container &cont) { + hdl->attach_functor([=](const caf::error &reason) { + spdlog::debug( + "{} {} {} exited: {}", + cont.type(), + cont.name(), + to_string(cont.uuid()), + to_string(reason)); + }); +} + +void xstudio::utility::print_on_exit( + const caf::actor &hdl, const std::string &name, const Uuid &uuid) { + + hdl->attach_functor([=](const caf::error &reason) { + spdlog::debug( + "{} {} exited: {}", name, uuid.is_null() ? "" : to_string(uuid), to_string(reason)); + }); +} + std::string xstudio::utility::exec(const std::vector &cmd, int &exit_code) { - reproc::process process; + /*reproc::process process; std::error_code ec = process.start(cmd); if (ec == std::errc::no_such_file_or_directory) { @@ -195,16 +227,34 @@ std::string xstudio::utility::exec(const std::vector &cmd, int &exi return ec.message(); } - return output; + return output;*/ + return std::string(); } std::string xstudio::utility::uri_to_posix_path(const caf::uri &uri) { if (uri.path().data()) { // spdlog::warn("{} {}",uri.path().data(), uri_decode(uri.path().data())); std::string path = uri_decode(uri.path().data()); +#ifdef __linux__ if (not path.empty() and path[0] != '/' and not uri.authority().empty()) { path = "/" + path; } +#endif +#ifdef _WIN32 + + std::size_t pos = path.find("/"); + if (pos == 0) { + // 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 ""; @@ -333,7 +383,14 @@ caf::uri xstudio::utility::parse_cli_posix_path( const std::regex xstudio_prefix_shake( R"(^(.+\.)([-0-9x,]+)([#@]+)(\..+)$)", std::regex::optimize); +#ifdef _WIN32 + std::string abspath = path; + if (abspath[0] == '\\') { + abspath.erase(abspath.begin()); + } +#else const std::string abspath = fs::absolute(path); +#endif if (std::regex_match(abspath.c_str(), m, xstudio_prefix_spec)) { uri = posix_path_to_uri(m[1].str() + m[3].str()); @@ -398,9 +455,15 @@ caf::uri xstudio::utility::posix_path_to_uri(const std::string &path, const bool if (abspath) { auto pwd = get_env("PWD"); if (pwd and not pwd->empty()) +#ifdef _WIN32 + p = (fs::path(*pwd) / path).lexically_normal().string(); + else + p = (std::filesystem::current_path() / path).lexically_normal().string(); +#else p = fs::path(fs::path(*pwd) / path).lexically_normal(); else p = fs::path(std::filesystem::current_path() / path).lexically_normal(); +#endif } // spdlog::warn("posix_path_to_uri: {} -> {}", path, p); @@ -429,14 +492,23 @@ 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 (not entry.path().filename().empty() and - std::string(entry.path().filename())[0] == '.') + if (!entry.path().filename().empty() && + entry.path().filename().string()[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 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("[\]"), "/")); +#else files.push_back(entry.path()); +#endif } auto file_items = uri_from_file_list(files); items.insert(items.end(), file_items.begin(), file_items.end()); @@ -483,12 +555,20 @@ std::string xstudio::utility::filemanager_show_uris(const std::vector std::string xstudio::utility::get_host_name() { std::array hostname{0}; - gethostname(hostname.data(), hostname.size()); + gethostname(hostname.data(), (int)hostname.size()); return hostname.data(); } std::string xstudio::utility::get_user_name() { std::string result; + +#ifdef _WIN32 + TCHAR username[MAX_PATH]; + DWORD size = MAX_PATH; + if (GetUserName(username, &size)) { + result = std::string(username); + } +#else long strsize = sysconf(_SC_GETPW_R_SIZE_MAX); std::vector buf; @@ -504,6 +584,7 @@ std::string xstudio::utility::get_user_name() { result = pw->pw_name; } } +#endif return result; } @@ -511,6 +592,17 @@ std::string xstudio::utility::get_user_name() { std::string xstudio::utility::expand_envvars( const std::string &src, const std::map &additional) { + +#ifdef _WIN32 +#else + // some prefs have ${USERPROFILE} which is MS Windows only, on UNIX we want + // ${HOME} + if (src.find("${USERPROFILE}") != std::string::npos) { + std::string unix_home = utility::replace_once(src, "${USERPROFILE}", "${HOME}"); + return expand_envvars(unix_home, additional); + } +#endif + // use regex to capture envs and replace. std::regex words_regex(R"(\$\{[^\}]+\})"); auto env_begin = std::sregex_iterator(src.begin(), src.end(), words_regex); @@ -549,6 +641,14 @@ 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; + if (GetUserName(username, &size)) { + result = std::string(username); + } +#else long strsize = sysconf(_SC_GETPW_R_SIZE_MAX); std::vector buf; @@ -561,6 +661,7 @@ std::string xstudio::utility::get_login_name() { result = pw->pw_name; } } +#endif return result; } diff --git a/src/utility/src/json_store.cpp b/src/utility/src/json_store.cpp index 611f4c308..8ebe62527 100644 --- a/src/utility/src/json_store.cpp +++ b/src/utility/src/json_store.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // #include +#include #include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/helpers.hpp" using namespace nlohmann; using namespace xstudio::utility; @@ -27,6 +29,20 @@ bool JsonStore::remove(const std::string &path) { return true; } +JsonStore xstudio::utility::open_session(const caf::uri &path) { + return open_session(utility::uri_to_posix_path(path)); +} + +JsonStore xstudio::utility::open_session(const std::string &path) { + JsonStore js; + + zstr::ifstream i(path); + i >> js; + + return js; +} + + // void JsonStore::merge(const JsonStore &json, const std::string &path) { // merge(json, path); // } @@ -86,3 +102,22 @@ std::string xstudio::utility::to_string(const xstudio::utility::JsonStore &x) { void xstudio::utility::to_json(nlohmann::json &j, const JsonStore &c) { j = c.get(""); } void xstudio::utility::from_json(const nlohmann::json &j, JsonStore &c) { c = JsonStore(j); } + +nlohmann::json +xstudio::utility::sort_by(const nlohmann::json &jsn, const nlohmann::json::json_pointer &ptr) { + auto result = jsn; + + if (result.is_array()) { + std::sort( + result.begin(), result.end(), [ptr = ptr](const auto &a, const auto &b) -> bool { + try { + return a.at(ptr) < b.at(ptr); + } catch (const std::exception &err) { + spdlog::warn("{}", err.what()); + } + return false; + }); + } + + return result; +} diff --git a/src/utility/src/logging.cpp b/src/utility/src/logging.cpp index 6ddfeca71..4e8772bb5 100644 --- a/src/utility/src/logging.cpp +++ b/src/utility/src/logging.cpp @@ -27,7 +27,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/remote_session_file.cpp b/src/utility/src/remote_session_file.cpp index 9f6454e45..20ab3d800 100644 --- a/src/utility/src/remote_session_file.cpp +++ b/src/utility/src/remote_session_file.cpp @@ -20,7 +20,11 @@ RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { // build entry.. fs::path p(file_path); // parse path.. +#ifdef _WIN32 + path_ = p.parent_path().string(); +#else path_ = p.parent_path(); +#endif auto file_name = p.filename().string(); std::smatch match; @@ -107,7 +111,14 @@ fs::path RemoteSessionFile::filepath() const { return p; } -pid_t RemoteSessionFile::get_pid() const { return getpid(); } +pid_t RemoteSessionFile::get_pid() const { + +#ifdef _WIN32 + return _getpid(); +#else + return getpid(); +#endif +} bool RemoteSessionFile::create_session_file() { @@ -146,7 +157,11 @@ void RemoteSessionManager::scan() { for (const auto &entry : fs::directory_iterator(path_)) { if (fs::is_regular_file(entry.status())) { try { +#ifdef _WIN32 + sessions_.emplace_back(RemoteSessionFile(entry.path().string())); +#else sessions_.emplace_back(RemoteSessionFile(entry.path())); +#endif } catch (const std::exception &err) { spdlog::debug("{} {}", __PRETTY_FUNCTION__, err.what()); } diff --git a/src/utility/src/sequence.cpp b/src/utility/src/sequence.cpp index f7062483b..53a1f1d75 100644 --- a/src/utility/src/sequence.cpp +++ b/src/utility/src/sequence.cpp @@ -1,8 +1,23 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef _WIN32 +// Windows specific implementation +#include +#include + +time_t get_mtim(const struct stat &st) { return st.st_mtime; } + +time_t get_ctim(const struct stat &st) { return st.st_ctime; } + +#else +// Linux specific implementation +#define get_mtim(st) (st).st_mtim.tv_sec +#define get_ctim(st) (st).st_ctim.tv_sec + +#endif + +#include #include #include -// #include - #include #include @@ -59,17 +74,44 @@ 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 + ULARGE_INTEGER blockSize; + DWORD blockSizeLow, blockSizeHigh; + + if (!GetDiskFreeSpaceExA(path.c_str(), nullptr, nullptr, &blockSize)) { + // Error occurred, handle it accordingly + // ... + } + + blockSizeLow = blockSize.LowPart; + blockSizeHigh = blockSize.HighPart; + + return static_cast(blockSizeLow) | (static_cast(blockSizeHigh) << 32); +} +#endif Sequence::Sequence(const Entry &entry) : count_(1), uid_(entry.stat_.st_uid), gid_(entry.stat_.st_gid), +#ifdef _WIN32 + size_(get_block_size_windows(entry) * 512), +#else size_(entry.stat_.st_blocks * 512), +#endif apparent_size_(entry.stat_.st_size), +#ifdef _WIN32 + mtim_(get_mtim(entry.stat_)), + ctim_(get_ctim(entry.stat_)), +#else mtim_(entry.stat_.st_mtim.tv_sec), ctim_(entry.stat_.st_ctim.tv_sec), +#endif name_(entry.name_), - frames_() {} + frames_() { +} Sequence::Sequence(const std::string name) : name_(std::move(name)), frames_() {} std::string make_frame_sequence( @@ -118,6 +160,7 @@ struct DefaultSequenceHelper { seq.apparent_size_ = 0; for (const auto &entry : entries_) { +#ifdef __linux__ if (entry.stat_.st_mtim.tv_sec > seq.mtim_) { seq.mtim_ = entry.stat_.st_mtim.tv_sec; if (seq.mtim_ > max_t) { @@ -134,7 +177,12 @@ struct DefaultSequenceHelper { seq.gid_ = entry.stat_.st_gid; } } +#endif +#ifdef _WIN32 + seq.size_ += get_block_size_windows(entry) * 512; +#else seq.size_ += entry.stat_.st_blocks * 512; +#endif seq.apparent_size_ += entry.stat_.st_size; } if (frames_.empty()) @@ -313,7 +361,11 @@ static const std::set not_sequence_ext_set{ bool default_is_sequence(const Entry &entry) { // things that are never sequences.. +#ifdef _WIN32 + std::string ext = std::filesystem::path(entry.name_).extension().string(); +#else std::string ext = std::filesystem::path(entry.name_).extension(); +#endif // we don't try and handle case, as that get's trick when utf-8 is in use.. // we assume that it'll not be mixed.. if (not_sequence_ext_set.count(to_lower(ext))) @@ -368,4 +420,4 @@ std::vector sequences_from_entries( return sequences; } -} // namespace xstudio::utility \ No newline at end of file +} // namespace xstudio::utility diff --git a/src/utility/test/file_system_item_test.cpp b/src/utility/test/file_system_item_test.cpp new file mode 100644 index 000000000..fd5550e4d --- /dev/null +++ b/src/utility/test/file_system_item_test.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/utility/serialise_headers.hpp" + +#include "xstudio/utility/file_system_item.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/logging.hpp" + +using namespace caf; +using namespace xstudio::utility; + +ACTOR_TEST_SETUP() + + +TEST(FileTest, Test) { + start_logger(); + FileSystemItem root; + root.bind_ignore_entry_func(ignore_not_session); + + EXPECT_EQ(root.dump()["type_name"], "ROOT"); + + // root.insert(root.end(), FileSystemItem("test", posix_path_to_uri("/user_data/XSTUDIO"))); + // root.scan(); + + // EXPECT_EQ(root.dump().dump(2), "ROOT"); +} diff --git a/src/utility/test/managed_dir_test.cpp b/src/utility/test/managed_dir_test.cpp index 8d50c8dc4..28a0dda83 100644 --- a/src/utility/test/managed_dir_test.cpp +++ b/src/utility/test/managed_dir_test.cpp @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #include #include diff --git a/ui/qml/reskin/assets/icons/new/ad_group.svg b/ui/qml/reskin/assets/icons/new/ad_group.svg new file mode 100644 index 000000000..9a9d0e120 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/ad_group.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/reskin/assets/icons/new/arrow_selector_tool.svg new file mode 100644 index 000000000..5342330b3 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/arrow_selector_tool.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/reskin/assets/icons/new/arrows_outward.svg new file mode 100644 index 000000000..a629c06c9 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/arrows_outward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/brush.svg b/ui/qml/reskin/assets/icons/new/brush.svg new file mode 100644 index 000000000..d583fce54 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/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/reskin/assets/icons/new/build_gang.svg new file mode 100644 index 000000000..b21bc6fa3 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/build_gang.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/center_focus_strong.svg b/ui/qml/reskin/assets/icons/new/center_focus_strong.svg new file mode 100644 index 000000000..73b950606 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/center_focus_strong.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/reskin/assets/icons/new/content_cut.svg new file mode 100644 index 000000000..972b60483 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/content_cut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/content_paste.svg b/ui/qml/reskin/assets/icons/new/content_paste.svg new file mode 100644 index 000000000..81ab799f9 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/content_paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/delete.svg b/ui/qml/reskin/assets/icons/new/delete.svg new file mode 100644 index 000000000..560d174b9 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/disabled.svg b/ui/qml/reskin/assets/icons/new/disabled.svg new file mode 100644 index 000000000..0ee17ef17 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/disabled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/expand.svg b/ui/qml/reskin/assets/icons/new/expand.svg new file mode 100644 index 000000000..229cba947 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/expand_all.svg b/ui/qml/reskin/assets/icons/new/expand_all.svg new file mode 100644 index 000000000..06fb21872 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/expand_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/fast_rewind.svg b/ui/qml/reskin/assets/icons/new/fast_rewind.svg new file mode 100644 index 000000000..bf3d13d35 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/fast_rewind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/filter.svg b/ui/qml/reskin/assets/icons/new/filter.svg new file mode 100644 index 000000000..e4af9efd5 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/format_size.svg b/ui/qml/reskin/assets/icons/new/format_size.svg new file mode 100644 index 000000000..484e925f4 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/format_size.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/reskin/assets/icons/new/ink_pen.svg new file mode 100644 index 000000000..7c96b0a03 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/ink_pen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/input.svg b/ui/qml/reskin/assets/icons/new/input.svg new file mode 100644 index 000000000..4f74c9af8 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/laps.svg b/ui/qml/reskin/assets/icons/new/laps.svg new file mode 100644 index 000000000..6f1c6e88d --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/laps.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/reskin/assets/icons/new/library_music.svg new file mode 100644 index 000000000..41e229bb9 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/library_music.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/list.svg b/ui/qml/reskin/assets/icons/new/list.svg new file mode 100644 index 000000000..252f30536 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/list.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/reskin/assets/icons/new/list_default.svg new file mode 100644 index 000000000..6fb17dfe3 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/list_default.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/qml/reskin/assets/icons/new/list_shotgun.svg b/ui/qml/reskin/assets/icons/new/list_shotgun.svg new file mode 100644 index 000000000..ce5fdb1ab --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/list_shotgun.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/qml/reskin/assets/icons/new/list_subset.svg b/ui/qml/reskin/assets/icons/new/list_subset.svg new file mode 100644 index 000000000..db103c7b2 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/list_subset.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/qml/reskin/assets/icons/new/list_view.svg b/ui/qml/reskin/assets/icons/new/list_view.svg new file mode 100644 index 000000000..8bff278f9 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/list_view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/more_vert.svg b/ui/qml/reskin/assets/icons/new/more_vert.svg new file mode 100644 index 000000000..e172f878a --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/more_vert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/movie.svg b/ui/qml/reskin/assets/icons/new/movie.svg new file mode 100644 index 000000000..e98fa4847 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/movie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/open_in.svg b/ui/qml/reskin/assets/icons/new/open_in.svg new file mode 100644 index 000000000..42895ffd1 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/open_in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/open_in_new.svg b/ui/qml/reskin/assets/icons/new/open_in_new.svg new file mode 100644 index 000000000..42895ffd1 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/open_in_new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/open_with.svg b/ui/qml/reskin/assets/icons/new/open_with.svg new file mode 100644 index 000000000..a09337ddf --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/open_with.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/output.svg b/ui/qml/reskin/assets/icons/new/output.svg new file mode 100644 index 000000000..efed99e34 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/output.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/pan.svg b/ui/qml/reskin/assets/icons/new/pan.svg new file mode 100644 index 000000000..3c6e28ab2 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/pan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/pause.svg b/ui/qml/reskin/assets/icons/new/pause.svg new file mode 100644 index 000000000..95bc792fc --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/photo_camera.svg b/ui/qml/reskin/assets/icons/new/photo_camera.svg new file mode 100644 index 000000000..e863b0dfa --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/photo_camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/rectangle.svg b/ui/qml/reskin/assets/icons/new/rectangle.svg new file mode 100644 index 000000000..ada92f2cf --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/rectangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/redo.svg b/ui/qml/reskin/assets/icons/new/redo.svg new file mode 100644 index 000000000..94d65b4c8 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/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/reskin/assets/icons/new/repartition.svg new file mode 100644 index 000000000..f2af4fa9b --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/repartition.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/repeat.svg b/ui/qml/reskin/assets/icons/new/repeat.svg new file mode 100644 index 000000000..c1b09d802 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/repeat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/reset_image.svg b/ui/qml/reskin/assets/icons/new/reset_image.svg new file mode 100644 index 000000000..fd5df031e --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/reset_image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/reset_tv.svg b/ui/qml/reskin/assets/icons/new/reset_tv.svg new file mode 100644 index 000000000..93b62b941 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/reset_tv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/restart.svg b/ui/qml/reskin/assets/icons/new/restart.svg new file mode 100644 index 000000000..6b7b35908 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/search_off.svg b/ui/qml/reskin/assets/icons/new/search_off.svg new file mode 100644 index 000000000..0081c9e5d --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/search_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/settings.svg b/ui/qml/reskin/assets/icons/new/settings.svg new file mode 100644 index 000000000..66aadd02d --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/skip.svg b/ui/qml/reskin/assets/icons/new/skip.svg new file mode 100644 index 000000000..ddc0c0d9b --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/skip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/sort.svg b/ui/qml/reskin/assets/icons/new/sort.svg new file mode 100644 index 000000000..8b81bf90a --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/sort.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..56de447fb --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/splitscreen.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..8396226dc --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/splitscreen2.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/reskin/assets/icons/new/sticky_note.svg new file mode 100644 index 000000000..639ffdb65 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/sticky_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/sync.svg b/ui/qml/reskin/assets/icons/new/sync.svg new file mode 100644 index 000000000..f65d5b39d --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/sync.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/theaters.svg b/ui/qml/reskin/assets/icons/new/theaters.svg new file mode 100644 index 000000000..e0f937cae --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/theaters.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/trending.svg b/ui/qml/reskin/assets/icons/new/trending.svg new file mode 100644 index 000000000..6dbf2c394 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/trending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/triangle.svg b/ui/qml/reskin/assets/icons/new/triangle.svg new file mode 100644 index 000000000..1027687d5 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/tune.svg b/ui/qml/reskin/assets/icons/new/tune.svg new file mode 100644 index 000000000..887f8bd49 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/tune.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/undo.svg b/ui/qml/reskin/assets/icons/new/undo.svg new file mode 100644 index 000000000..c451e1adc --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/upload.svg b/ui/qml/reskin/assets/icons/new/upload.svg new file mode 100644 index 000000000..39ca9642a --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/variables_insert.svg b/ui/qml/reskin/assets/icons/new/variables_insert.svg new file mode 100644 index 000000000..73f985101 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/variables_insert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/view.svg b/ui/qml/reskin/assets/icons/new/view.svg new file mode 100644 index 000000000..e07d78c5f --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/view_grid.svg b/ui/qml/reskin/assets/icons/new/view_grid.svg new file mode 100644 index 000000000..962298238 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/view_grid.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/reskin/assets/icons/new/volume_down.svg new file mode 100644 index 000000000..a3fbc4100 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/volume_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/volume_mute.svg b/ui/qml/reskin/assets/icons/new/volume_mute.svg new file mode 100644 index 000000000..34545be4b --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/volume_mute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/volume_no_sound.svg b/ui/qml/reskin/assets/icons/new/volume_no_sound.svg new file mode 100644 index 000000000..a3ab91a5c --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/volume_no_sound.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/reskin/assets/icons/new/volume_up.svg new file mode 100644 index 000000000..fd9006a6d --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/volume_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/zoom_in.svg b/ui/qml/reskin/assets/icons/new/zoom_in.svg new file mode 100644 index 000000000..a09499ef4 --- /dev/null +++ b/ui/qml/reskin/assets/icons/new/zoom_in.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..5c99b7806 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/brush_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..078b4395c --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/delete_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..a9360d96d --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/fast_forward_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..f4353fe23 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/fast_rewind_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..b5a7763d8 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/open_in_new_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..0b15ddf1b --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/open_with_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..63d4e92a0 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/pan_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..0e8b16d61 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/pause_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..aeadccb5b --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/photo_camera_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..7be81f799 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/play_arrow_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..cb61f567a --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/repeat_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..873173467 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/restart_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..d72b70a07 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/search_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..b832f2fb1 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/skip_next_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..d8091a382 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/skip_previous_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..9ba94f04f --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/sticky_note_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..ebb8d982a --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/sync_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..c39338063 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/trending_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..94a3e5587 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/tune_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..b65f2c0ff --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/view_grid_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..61f465e4d --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/view_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..c81a7f968 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/volume_down_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..537de53f1 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/volume_mute_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..5f3970437 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/volume_no_sound_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..2e66df655 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/volume_up_w500.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 000000000..e5a03f680 --- /dev/null +++ b/ui/qml/reskin/assets/icons/retired/zoom_in_w500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/sort_flipped.png b/ui/qml/reskin/assets/icons/sort_flipped.png new file mode 100644 index 000000000..f9a9b91ff Binary files /dev/null and b/ui/qml/reskin/assets/icons/sort_flipped.png differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Black.ttf b/ui/qml/reskin/fonts/Inter/Inter-Black.ttf new file mode 100644 index 000000000..b27822bae Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Black.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Bold.ttf b/ui/qml/reskin/fonts/Inter/Inter-Bold.ttf new file mode 100644 index 000000000..fe23eeb9c Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Bold.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-ExtraBold.ttf b/ui/qml/reskin/fonts/Inter/Inter-ExtraBold.ttf new file mode 100644 index 000000000..874b1b0dd Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-ExtraBold.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-ExtraLight.ttf b/ui/qml/reskin/fonts/Inter/Inter-ExtraLight.ttf new file mode 100644 index 000000000..c993e8221 Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-ExtraLight.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Light.ttf b/ui/qml/reskin/fonts/Inter/Inter-Light.ttf new file mode 100644 index 000000000..71188f5cb Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Light.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Medium.ttf b/ui/qml/reskin/fonts/Inter/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Medium.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Regular.ttf b/ui/qml/reskin/fonts/Inter/Inter-Regular.ttf new file mode 100644 index 000000000..5e4851f0a Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Regular.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-SemiBold.ttf b/ui/qml/reskin/fonts/Inter/Inter-SemiBold.ttf new file mode 100644 index 000000000..ecc7041e2 Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-SemiBold.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/Inter-Thin.ttf b/ui/qml/reskin/fonts/Inter/Inter-Thin.ttf new file mode 100644 index 000000000..fe77243fc Binary files /dev/null and b/ui/qml/reskin/fonts/Inter/Inter-Thin.ttf differ diff --git a/ui/qml/reskin/fonts/Inter/OFL.txt b/ui/qml/reskin/fonts/Inter/OFL.txt new file mode 100644 index 000000000..ad214842c --- /dev/null +++ b/ui/qml/reskin/fonts/Inter/OFL.txt @@ -0,0 +1,93 @@ +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 new file mode 100644 index 000000000..bb34ed520 --- /dev/null +++ b/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml @@ -0,0 +1,210 @@ +// 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 index d4f662fa4..1a3f76e3f 100644 --- a/ui/qml/reskin/layout_framework/XsPanelDivider.qml +++ b/ui/qml/reskin/layout_framework/XsPanelDivider.qml @@ -1,58 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 -Rectangle { +import xStudioReskin 1.0 +Item { + + id: divider property bool isVertical: true property real thresholdSize: 10 - property real dividerSize: 5 + 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 isDragging: mArea.drag.active + + 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" - color: "white"//mArea.containsMouse || mArea.drag.active? mArea.pressed? "yellow" : "#AA000000": "#AA000000" + Component.onCompleted: { + if(isVertical) anchors.left = parent.left + else anchors.top = parent.top + } + + } - width: isVertical? dividerSize : parent.width - height: isVertical? parent.height : dividerSize + property var fractional_position: child_dividers[index] property var computed_position: (isVertical ? parent.width : parent.height)*fractional_position onComputed_positionChanged: { - if (!isDragging) { + if (!dragging) { if (isVertical) x = computed_position else y = computed_position } } onYChanged: { - if (isDragging && !isVertical) { - fractional_position = y/parent.height - dividerMoved() + if (dragging && !isVertical) { + var v = child_dividers; + v[index] = y/parent.height + child_dividers= v; } } onXChanged: { - if (isDragging && isVertical) { - fractional_position = x/parent.width - dividerMoved() + if (dragging && isVertical) { + var v = child_dividers; + v[index] = x/parent.width + child_dividers= v; } } - signal dividerMoved - MouseArea{ id: mArea - anchors.fill: parent + width: divider.width + height: divider.height + anchors.centerIn: divider + preventStealing: true + hoverEnabled: true cursorShape: isVertical? Qt.SizeHorCursor : Qt.SizeVerCursor - drag.target: parent + drag.target: divider drag.axis: isVertical? Drag.XAxis : Drag.YAxis - drag.smoothed: false - drag.minimumY: drag.minimumX - drag.maximumY: drag.maximumX - drag.minimumX: minLimit + (dividerSize + thresholdSize) - drag.maximumX: maxLimit - (dividerSize + thresholdSize) } diff --git a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml b/ui/qml/reskin/layout_framework/XsPanelSplitter.qml index 34bd5be6b..4366ecd54 100644 --- a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml +++ b/ui/qml/reskin/layout_framework/XsPanelSplitter.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 1.4 import Qt.labs.qmlmodels 1.0 @@ -5,64 +6,40 @@ 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) - onHeightChanged: { - resizeWidgets() - } - - onWidthChanged: { - resizeWidgets() - } - onRowCountChanged: { - resizeWidgets() - } + property var dividers: child_dividers !== undefined ? child_dividers : [] + Repeater { - model: DelegateModel{ - - model: panels_layout_model - rootIndex: panels_layout_model_index - delegate: - - XsDivider { - - visible: index != 0 - isVertical: topItem.isVertical - z:1000 + // '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 - onDividerMoved: { - topItem.resizeWidgets() - } - } - } - } + XsDivider { + + isVertical: topItem.isVertical + z:1000 // make sure the dividers are on top - function resizeWidgets() { - var prev_frac = 0.0 - let rc = panels_layout_model.rowCount(panels_layout_model_index) - if (rc) { - panels_layout_model.set(panels_layout_model.index(0, 0, panels_layout_model_index), 0.0, "fractional_position") - for(let i=1; i= topItem.dividers.length ? 1.0 : topItem.dividers[index] + property var frac_size: e-d - function resizeWidgets() { - if (child) { - child.width = width - child.height = height - } - topItem.resizeWidgets() - } + 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 - function loadWindows() { + property var child + property var child_type - if (split !== undefined && split !== "tbd") { + 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") { - resizeWidgets() + child.buildSubPanels() return } if (child) child.destroy() @@ -136,7 +94,7 @@ Rectangle { { panels_layout_model: topItem.panels_layout_model, panels_layout_model_index: recurse_into_model_idx, - isVertical: split == "widthwise" + isVertical: split_horizontal }) } else { @@ -145,13 +103,15 @@ Rectangle { } 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") { - resizeWidgets() return } if (child) child.destroy() - child_type = "XsPanelContainer" + 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) { @@ -168,19 +128,10 @@ Rectangle { console.log("component", component, component.errorString()) } } - resizeWidgets() - } - - onLlChanged: { - loadWindows() - } - - onLl2Changed: { - loadWindows() } Component.onCompleted: { - loadWindows() + buildSubPanels() } } diff --git a/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml b/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml index f3df9e4cb..6f91b27a7 100644 --- a/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml +++ b/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 1.4 @@ -7,40 +8,40 @@ import QtQml.Models 2.14 import xStudioReskin 1.0 import xstudio.qml.models 1.0 -Rectangle { +XsSecondaryButton { id: hamBtn // background: Rectangle{color:"red"} width: 15*1.7 height: width*1.1 z: 1000 - property string panel_id + imgSrc: "qrc:/icons/menu.svg" + smooth: true + antialiasing: true + + property string panelId onPanel_idChanged: { hamburgerMenu.menu_model_index = panels_menus_model.index(-1, -1) } - MouseArea { - - anchors.fill: parent - onClicked: { - hamburgerMenu.x = x-hamburgerMenu.width - hamburgerMenu.visible = true - } - } - - Image{ - width: parent.width-6 - height: parent.height-6 - anchors.centerIn: parent - source: "qrc:///assets/icons/menu.svg" + onClicked: { + hamburgerMenu.x = -hamburgerMenu.width + hamburgerMenu.visible = true } + // MouseArea { + // anchors.fill: parent + // onClicked: { + // hamburgerMenu.x = x-hamburgerMenu.width + // hamburgerMenu.visible = true + // } + // } // this gives us access to the global tree model that defines menus, // sub-menus and menu items XsMenusModel { id: panels_menus_model - modelDataName: panel_id + modelDataName: panelId onJsonChanged: { hamburgerMenu.menu_model_index = index(-1, -1) diff --git a/ui/qml/reskin/layout_framework/XsViewContainer.qml b/ui/qml/reskin/layout_framework/XsViewContainer.qml index 7c0f19299..47bf7786e 100644 --- a/ui/qml/reskin/layout_framework/XsViewContainer.qml +++ b/ui/qml/reskin/layout_framework/XsViewContainer.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 1.4 import Qt.labs.qmlmodels 1.0 @@ -5,165 +6,332 @@ import QtQml.Models 2.14 import xStudioReskin 1.0 import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 -Rectangle { +// 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 - color: "transparent" + 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 var panel_name: panel_source_qml ? panel_source_qml: undefined - property var previous_panel_name - property var the_panel - - onPanel_nameChanged: { - if (panel_name) loadPanel() - else the_panel = undefined - } - - // we use the id of this item to create a unique string which identifies - // this instance of the XsViewContainer. This is used as the name for the - // model data that constructs the menu for this particular XsViewContainer - property string panel_id: "" + container - - XsPanelsMenuButton { - panel_id: container.panel_id - anchors.right: parent.right - anchors.rightMargin: 1 - anchors.top: parent.top - anchors.topMargin: 1 - z: 1000 - } - - Rectangle { - anchors.fill: parent - color: "black" - visible: the_panel === undefined - Text { - anchors.centerIn: parent - text: "Empty" - color: "white" - } - } - - function loadPanel() { - - if (panel_name && panel_name == previous_panel_name) return; + property int modified_tab_index: -1 - let component = Qt.createComponent(panel_name) - previous_panel_name = panel_name - if (component.status == Component.Ready) { + // update the currentIndex from the model + currentIndex: current_tab === undefined ? 0 : current_tab - if (the_panel != undefined) the_panel.destroy() - the_panel = component.createObject( - container, - { - }) - - } else { - console.log("Error loading panel:", component, component.errorString()) + // when user changes tab, store in the model + onCurrentIndexChanged: { + if (currentIndex != current_tab) { + current_tab = currentIndex } } - XsMenuModelItem { - text: "Split Panel Horizontally" - menuPath: "" - menuModelName: panel_id - onActivated: { - split_panel(true) - } + // 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 : "" + } + } } - XsMenuModelItem { - text: "Split Panel Vertically" - menuPath: "" - menuModelName: panel_id - onActivated: { - split_panel(false) - } + + 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 - XsMenuModelItem { - text: "Undock Panel" - menuPath: "" - menuModelName: panel_id - onActivated: { - undock_panel() - } + /*********************************************************************** + * + * 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 } - XsMenuModelItem { - text: "Close Panel" - menuPath: "" - menuModelName: panel_id - onActivated: { - panels_layout_model.removeRows(panels_layout_model_row, 1, panels_layout_model_index.parent) + // 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) } } - XsViewsModel { - id: views_model + // 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: panel_id + menuPath: "" //"Set Panel To|" + menuModelName: "TabMenu"+panelId + menuItemPosition: index onActivated: { - switch_panel_to(view_qml_path) + 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 + } - function undock_panel() { - + // 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 + } } - function split_panel(horizontal) { + /************************************************************************/ + + 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 + } + + } - var split_direction = horizontal ? "widthwise" : "heightwise" + 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 + } + } - container.panel_name = "XsSplitPanel" - var parent_split_type = panels_layout_model.get(panels_layout_model.index(0, 0, panels_layout_model_index.parent), "split") - if (parent_split_type == "tbd") { - panels_layout_model.set(panels_layout_model.index(0, 0, panels_layout_model_index.parent), split_direction, "split") } - if (parent_split_type == split_direction) { - panels_layout_model.insertRows(panels_layout_model_row, 1, panels_layout_model_index.parent) - var new_row_count = panels_layout_model.rowCount(panels_layout_model_index.parent)+1 - for(let i=0; i + + 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 @@ -15,65 +21,207 @@ 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/XsMenuChoice.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 + - widgets/prototypes/new/XsImage.qml - widgets/prototypes/new/XsText.qml - widgets/prototypes/new/XsToolTip.qml - - assets/icons/menu.svg - assets/icons/play.svg - assets/icons/plus.svg - assets/icons/circle.svg - assets/icons/search.svg - assets/icons/x.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_500.svg - assets/icons/new/error.svg - assets/icons/new/filter_none.svg - assets/icons/new/search_w500.svg - assets/icons/new/delete_w500.svg - assets/icons/new/view_w500.svg - assets/icons/new/view_grid_w500.svg - assets/icons/new/draft.svg - assets/icons/new/more_horiz.svg - assets/icons/new/arrow_drop_down.svg - - assets/images/sample.png + 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 new file mode 100644 index 000000000..b3b9c2d26 --- /dev/null +++ b/ui/qml/reskin/session_data/XsMediaListModelData.qml @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 000000000..ebce36425 --- /dev/null +++ b/ui/qml/reskin/session_data/XsPlaylistsModelData.qml @@ -0,0 +1,10 @@ +// 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 new file mode 100644 index 000000000..ad7252a2a --- /dev/null +++ b/ui/qml/reskin/session_data/XsSessionData.qml @@ -0,0 +1,206 @@ +// 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 index 87b4a24b1..6f26ae477 100644 --- a/ui/qml/reskin/views/media/delegates/XsMediaItemDelegate.qml +++ b/ui/qml/reskin/views/media/delegates/XsMediaItemDelegate.qml @@ -3,103 +3,163 @@ 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 { -Button { id: contentDiv - text: isMissing? "This media no longer exists" : _title width: parent.width; height: parent.height - property color bgColorPressed: "#33FFFFFF" + 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 var itemNumber: 2 - property bool isSelected: false - property bool isMissing: false + + 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 + //font.pixelSize: textSize + //font.family: textFont + //hoverEnabled: true opacity: enabled ? 1.0 : 0.33 - contentItem: - Item{ + property var columns_model + + Item { anchors.fill: parent - RowLayout{ - x: 4 - spacing: 4 - width: parent.width-(x*2) - height: XsStyleSheet.widgetStdHeight - anchors.verticalCenter: parent.verticalCenter + Rectangle{ id: rowDividerLine + width: parent.width; height: headerThumbWidth + color: bgColorPressed + anchors.bottom: parent.bottom + } + + RowLayout{ - Rectangle{ id: flagIndicator - Layout.preferredWidth: 4 - Layout.preferredHeight: 16 - color: index%2!=0?"yellow":"blue" - } - XsText{ id: countDiv - text: itemNumber - color: hintColor - Layout.preferredWidth: countDiv.textWidth<16? 16: countDiv.textWidth - Layout.preferredHeight: 16 - } - Rectangle{ - Layout.preferredWidth: 48+ border.width*2 - Layout.preferredHeight: 16+ border.width*2 - color: "transparent" - border.width: 1 - border.color:isSelected? borderColorHovered : "transparent" - - XsImage{ - width: 48 - height: 16 - anchors.centerIn: parent - fillMode: isMissing ? Image.PreserveAspectFit : Image.Stretch - source: isMissing? "qrc:/assets/icons/new/error.svg" : _thumbnail// "qrc:/assets/icons/new/check_box_unchecked.svg" - imgOverlayColor: isMissing? errorColor : "transparent" - } - - } - Text { - id: textDiv - text: contentDiv.text+"-"+index //#TODO - font: contentDiv.font - color: isMissing? hintColor : textColorNormal - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - topPadding: 2 - bottomPadding: 2 - leftPadding: 8 - - // anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight + 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" - Layout.fillWidth: true - Layout.preferredHeight: 16 + DelegateChoice { + roleValue: "flag" + XsMediaFlagIndicator{ + Layout.preferredWidth: size + Layout.minimumHeight: itemHeight + } + } - XsToolTip{ - text: contentDiv.text - visible: contentDiv.hovered && parent.truncated - width: metricsDiv.width == 0? 0 : contentDiv.width + 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: + //background: Rectangle { id: bgDiv - implicitWidth: 100 - implicitHeight: 40 + anchors.fill: parent border.color: contentDiv.down || contentDiv.hovered ? borderColorHovered: borderColorNormal border.width: borderWidth - color: contentDiv.down || isSelected? bgColorPressed : forcedBgColorNormal + color: contentDiv.down || isSelected ? bgColorPressed : forcedBgColorNormal Rectangle { id: bgFocusDiv @@ -112,14 +172,107 @@ Button { 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: { - focus = true - isSelected = !isSelected + mediaSelectionModel.select(media_item_model_index, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) } - onReleased: focus = false - onPressAndHold: isMissing = !isMissing + 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 new file mode 100644 index 000000000..d6f270eb1 --- /dev/null +++ b/ui/qml/reskin/views/media/delegates/XsMediaSourceSelector.qml @@ -0,0 +1,60 @@ +// 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 new file mode 100644 index 000000000..d547dfade --- /dev/null +++ b/ui/qml/reskin/views/playlists/XsPlaylistItems.qml @@ -0,0 +1,57 @@ +// 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 index 2bcf7e8f9..4cca1ace6 100644 --- a/ui/qml/reskin/views/playlists/XsPlaylists.qml +++ b/ui/qml/reskin/views/playlists/XsPlaylists.qml @@ -14,6 +14,7 @@ Item{ anchors.fill: parent + property color panelColor: XsStyleSheet.panelBgColor property color bgColorPressed: palette.highlight property color bgColorNormal: "transparent" property color forcedBgColorNormal: bgColorNormal @@ -27,145 +28,109 @@ Item{ 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: 28+(8*2) + height: btnHeight+(panelPadding*2) RowLayout{ - x: 8 - spacing: 8 + x: panelPadding + spacing: 1 width: parent.width-(x*2) - height: XsStyleSheet.widgetStdHeight+4 + height: btnHeight anchors.verticalCenter: parent.verticalCenter XsPrimaryButton{ id: addPlaylistBtn - Layout.preferredWidth: 40 + 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 - imgSrc: "qrc:/assets/icons/new/add.svg" + isExpanded: false + hint: "Search playlists..." } - XsSearchBar{ + Item{ Layout.fillWidth: true Layout.preferredHeight: parent.height - placeholderText: activeFocus?"":"Search playlists..." } XsPrimaryButton{ id: morePlaylistBtn - Layout.preferredWidth: 40 + Layout.preferredWidth: btnWidth Layout.preferredHeight: parent.height - imgSrc: "qrc:/assets/icons/new/more_vert_500.svg" + imgSrc: "qrc:/icons/more_vert.svg" } } } Rectangle{ id: playlistDiv - x: 8 - y: actionDiv.height+4 - width: panel.width-(8*2) - height: panel.height-y-(4*2) - color: XsStyleSheet.panelBgColor - + 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 - Text{ - text: "Playlist" + XsText{ + text: "Playlist ("+playlistItems.count+")" anchors.left: parent.left - anchors.leftMargin: 4 + anchors.leftMargin: panelPadding anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignLeft - color: textColorNormal - } - - XsSecondaryButton{ id: infoBtn - width: 16 - height: 16 - imgSrc: "qrc:/assets/icons/new/error.svg" - anchors.right: parent.right - anchors.rightMargin: 4 - anchors.verticalCenter: parent.verticalCenter } XsSecondaryButton{ - width: 16 - height: 16 - imgSrc: "qrc:/assets/icons/new/filter_none.svg" - anchors.right: infoBtn.left - anchors.rightMargin: 4 + 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 } } - - ListModel{ - id: dataModel - ListElement{_type: "divider"; _count:"23"; _title: "Playlist"} - ListElement{_type: "content"; _count:"23"; _title: "Item1Item2Item3Item4Item5Item6Item7"; count:5} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "divider"; _count:"23"; _title: "Playlist"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "divider"; _count:"23"; _title: "Playlist"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - ListElement{_type: "divider"; _count:"23"; _title: "Playlist"} - ListElement{_type: "content"; _count:"23"; _title: "Item"} - } - - ListView { id: playlist - - y: titleBar.height - clip: true - spacing: 0 - width: contentWidth - height: contentHeight textWidth? textWidth : (parent.width-textDiv.titlePadding*2) + height: parent.height - // XsToolTip{ - // text: dividerDiv.text - // visible: dividerDiv.hovered && parent.truncated - // width: metricsDiv.width == 0? 0 : dividerDiv.width - // } - } + // XsToolTip{ + // text: visibleItemDiv.text + // visible: visibleItemDiv.hovered && parent.truncated + // width: metricsDiv.width == 0? 0 : visibleItemDiv.width + // } + } - XsSecondaryButton{ id: moreBtn - visible: dividerDiv.hovered - width: 16 - height: 16 - imgSrc: "qrc:/assets/icons/new/more_horiz.svg" - anchors.right: parent.right - anchors.rightMargin: 4 - anchors.verticalCenter: parent.verticalCenter - imgOverlayColor: hintColor + 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: dividerDiv.down || dividerDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: dividerDiv.down? bgColorPressed : forcedBgColorNormal - + background: Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: dividerDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered + id: bgDiv + implicitWidth: 100 + implicitHeight: 40 + border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal border.width: borderWidth - anchors.centerIn: parent + 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 - onReleased: focus = false + 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 index 224224404..83d2a2c1c 100644 --- a/ui/qml/reskin/views/playlists/delegates/XsPlaylistItemDelegate.qml +++ b/ui/qml/reskin/views/playlists/delegates/XsPlaylistItemDelegate.qml @@ -3,140 +3,259 @@ 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 -Button { +Item { id: contentDiv - text: isMissing? "This playlist no longer exists" : _title - width: parent.width; - height: parent.height + 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: "#33FFFFFF" + 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: 2 //_count + + /* 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 - - font.pixelSize: textSize - font.family: textFont - hoverEnabled: true - opacity: enabled ? 1.0 : 0.33 + property bool isExpanded: false - contentItem: - Item{ - anchors.fill: parent - - RowLayout{ - x: 4 - spacing: 4 - width: parent.width-(x*2) - height: XsStyleSheet.widgetStdHeight - anchors.verticalCenter: parent.verticalCenter - - XsSecondaryButton{ id: subsetBtn - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - imgSrc: "qrc:/assets/icons/new/arrow_drop_down.svg" - } - XsImage{ - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - source: isMissing? "qrc:/assets/icons/new/error.svg" : "qrc:/assets/icons/new/draft.svg" - imgOverlayColor: isMissing? errorColor : hintColor - } - Text { - id: textDiv - text: contentDiv.text+"-"+index //#TODO - font: contentDiv.font - color: isMissing? hintColor : textColorNormal - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - topPadding: 2 - bottomPadding: 2 - leftPadding: 8 - - // anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight - - Layout.fillWidth: true - Layout.preferredHeight: 16 - - XsToolTip{ - text: contentDiv.text - visible: contentDiv.hovered && parent.truncated - width: metricsDiv.width == 0? 0 : contentDiv.width + // 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 + } + + } } - } - Item{ - Layout.preferredWidth: addBtn.visible? 16: countDiv.textWidth - Layout.preferredHeight: 16 + 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 - XsText{ id: countDiv - text: itemCount - anchors.centerIn: parent - visible: !addBtn.visible - color: hintColor + leftPadding: itemPadding + horizontalAlignment: Text.AlignLeft + tooltipText: visibleItemDiv.text + tooltipVisibility: visibleItemDiv.hovered && truncated + toolTipWidth: visibleItemDiv.width+5 } XsSecondaryButton{ id: addBtn - anchors.fill: parent - imgSrc: "qrc:/assets/icons/new/add.svg" - visible: contentDiv.hovered - imgOverlayColor: hintColor - } - } - - Item{ - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - - XsImage{ id: errorIndicator - anchors.fill: parent - source: "qrc:/assets/icons/new/error.svg" - visible: !moreBtn.visible && index%2==0 + imgSrc: "qrc:/icons/add.svg" imgOverlayColor: hintColor + visible: visibleItemDiv.hovered + Layout.preferredWidth: buttonWidth + Layout.preferredHeight: buttonWidth } XsSecondaryButton{ id: moreBtn - anchors.fill: parent - imgSrc: "qrc:/assets/icons/new/more_horiz.svg" - visible: contentDiv.hovered + 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: contentDiv.down || contentDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: contentDiv.down || isSelected? bgColorPressed : forcedBgColorNormal - + background: Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: contentDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered + id: bgDiv + implicitWidth: 100 + implicitHeight: 40 + border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal border.width: borderWidth - anchors.centerIn: parent + 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 + } + } + } - onPressed: { - focus = true - isSelected = !isSelected + // 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 + + } } - onReleased: focus = false - onPressAndHold: isMissing = !isMissing - } \ 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 new file mode 100644 index 000000000..48dd9be41 --- /dev/null +++ b/ui/qml/reskin/views/playlists/delegates/XsSubsetItemDelegate.qml @@ -0,0 +1,185 @@ +// 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 new file mode 100644 index 000000000..756b4857f --- /dev/null +++ b/ui/qml/reskin/views/playlists/delegates/XsTimelineItemDelegate.qml @@ -0,0 +1,189 @@ +// 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 index 848d4820b..c4dda65ef 100644 --- a/ui/qml/reskin/views/timeline/XsTimeline.qml +++ b/ui/qml/reskin/views/timeline/XsTimeline.qml @@ -1,14 +1,278 @@ -import QtQuick 2.15 -import QtQuick.Controls 1.4 +// 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 -Rectangle { +Item{ + + id: panel anchors.fill: parent - color: XsStyleSheet.panelBgColor - Text { - anchors.centerIn: parent - text: "TimeLine View" + 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 new file mode 100644 index 000000000..e73e76dba --- /dev/null +++ b/ui/qml/reskin/views/timeline/data/XsSortFilterModel.qml @@ -0,0 +1,136 @@ +// 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 new file mode 100644 index 000000000..5993475b9 --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsDelegateAudioTrack.qml @@ -0,0 +1,134 @@ +// 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 new file mode 100644 index 000000000..642d43e1e --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsDelegateClip.qml @@ -0,0 +1,217 @@ +// 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 new file mode 100644 index 000000000..d50f867bc --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsDelegateGap.qml @@ -0,0 +1,83 @@ +// 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 new file mode 100644 index 000000000..612263713 --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsDelegateStack.qml @@ -0,0 +1,421 @@ +// 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 new file mode 100644 index 000000000..c9d4d65e9 --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsDelegateVideoTrack.qml @@ -0,0 +1,147 @@ +// 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 new file mode 100644 index 000000000..dcc41c9ef --- /dev/null +++ b/ui/qml/reskin/views/timeline/delegates/XsTimelineEditToolItems.qml @@ -0,0 +1,16 @@ +// 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 new file mode 100644 index 000000000..a6eb55f44 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsClipItem.qml @@ -0,0 +1,181 @@ +// 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 new file mode 100644 index 000000000..9ec2a8452 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml @@ -0,0 +1,38 @@ +// 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 new file mode 100644 index 000000000..996bbb134 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsDragLeft.qml @@ -0,0 +1,37 @@ +// 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 new file mode 100644 index 000000000..6c5e6329c --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsDragRight.qml @@ -0,0 +1,6 @@ +// 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 new file mode 100644 index 000000000..a03542ecb --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsElideLabel.qml @@ -0,0 +1,39 @@ +// 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 new file mode 100644 index 000000000..f6ae4534c --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsGapItem.qml @@ -0,0 +1,90 @@ +// 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 new file mode 100644 index 000000000..9ec2a8452 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsMoveClip.qml @@ -0,0 +1,38 @@ +// 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 new file mode 100644 index 000000000..ecd17e363 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsTickWidget.qml @@ -0,0 +1,80 @@ +// 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 new file mode 100644 index 000000000..1b76b4da4 --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsTimelineCursor.qml @@ -0,0 +1,61 @@ +// 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 new file mode 100644 index 000000000..66400560c --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsTrackHeader.qml @@ -0,0 +1,184 @@ +// 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 index b80c3b3b5..a4a6a55e8 100644 --- a/ui/qml/reskin/views/viewport/XsViewport.qml +++ b/ui/qml/reskin/views/viewport/XsViewport.qml @@ -1,24 +1,151 @@ +// 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 { +//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 - property var mainWindow: appWindow - onMainWindowChanged: { - appWindow.viewport = viewport + + 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 + } - onFocusChanged: { - console.log("focus", focus) + 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() + } - onActiveFocusChanged: { - console.log("focus", activeFocus) } + 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 new file mode 100644 index 000000000..bcd5a473b --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportActionBar.qml @@ -0,0 +1,196 @@ +// 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 new file mode 100644 index 000000000..6a8523b7c --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml @@ -0,0 +1,139 @@ +// 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 new file mode 100644 index 000000000..d58b161ed --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportToolBar.qml @@ -0,0 +1,122 @@ +// 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 new file mode 100644 index 000000000..772694618 --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml @@ -0,0 +1,231 @@ +// 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 new file mode 100644 index 000000000..0e06b0e3d --- /dev/null +++ b/ui/qml/reskin/views/viewport/widgets/XsViewerMenuButton.qml @@ -0,0 +1,206 @@ +// 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 new file mode 100644 index 000000000..d579b190d --- /dev/null +++ b/ui/qml/reskin/views/viewport/widgets/XsViewerSeekEditButton.qml @@ -0,0 +1,259 @@ +// 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 new file mode 100644 index 000000000..621e22112 --- /dev/null +++ b/ui/qml/reskin/views/viewport/widgets/XsViewerTextDisplay.qml @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 000000000..97b12936a --- /dev/null +++ b/ui/qml/reskin/views/viewport/widgets/XsViewerToggleButton.qml @@ -0,0 +1,122 @@ +// 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 new file mode 100644 index 000000000..832885703 --- /dev/null +++ b/ui/qml/reskin/views/viewport/widgets/XsViewerVolumeButton.qml @@ -0,0 +1,88 @@ +// 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/XsSearchBar.qml b/ui/qml/reskin/widgets/bars_and_tabs/XsSearchBar.qml index 757168ae2..92a5a3afc 100644 --- a/ui/qml/reskin/widgets/bars_and_tabs/XsSearchBar.qml +++ b/ui/qml/reskin/widgets/bars_and_tabs/XsSearchBar.qml @@ -35,9 +35,9 @@ TextField { id: widget activeFocusOnTab: true opacity: widget.enabled? 1 : 0.3 horizontalAlignment: TextInput.AlignLeft - leftPadding: searchIcon.sourceSize.width + searchIcon.anchors.leftMargin*2 rightPadding: clearBtn.width + clearBtn.anchors.rightMargin*2 + onEditingFinished: { // focus = false editingCompleted() @@ -72,33 +72,25 @@ TextField { id: widget } } - Image { id: searchIcon - source: "qrc:/assets/icons/new/search.svg" - // width: parent.height-6 - // height: parent.height-6 - sourceSize.width: 16 - sourceSize.height: 16 - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - smooth: true - antialiasing: true - layer { - enabled: true - effect: ColorOverlay { color: iconOverlayColor } - } - } XsSecondaryButton{ id: clearBtn width: 16 height: 16 anchors.right: parent.right anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/assets/icons/new/close.svg" + imgSrc: "qrc:/icons/close.svg" visible: widget.length!=0 - smooth: true - antialiasing: true - onClicked: widget.text="" + onClicked: { + clearSearch() + widget.focus = true + } + } + + + function clearSearch() + { + widget.text="" } + } diff --git a/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml b/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml new file mode 100644 index 000000000..9da51682e --- /dev/null +++ b/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 000000000..f91e667f8 --- /dev/null +++ b/ui/qml/reskin/widgets/bars_and_tabs/XsTabView.qml @@ -0,0 +1,248 @@ +// 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 new file mode 100644 index 000000000..e70fbe7fa --- /dev/null +++ b/ui/qml/reskin/widgets/buttons/XsNavButton.qml @@ -0,0 +1,104 @@ +// 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 index a1b2db97f..c563e5168 100644 --- a/ui/qml/reskin/widgets/buttons/XsPrimaryButton.qml +++ b/ui/qml/reskin/widgets/buttons/XsPrimaryButton.qml @@ -8,9 +8,12 @@ import xStudioReskin 1.0 Button { id: widget - property string imgSrc: "" + 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 @@ -18,35 +21,43 @@ Button { property color borderColorHovered: bgColorPressed property color borderColorNormal: "transparent" property real borderWidth: 1 + focusPolicy: Qt.NoFocus - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - hoverEnabled: true + hoverEnabled: true contentItem: Item{ anchors.fill: parent opacity: enabled ? 1.0 : 0.33 - Image { + + XsImage { id: imageDiv - source: imgSrc - // width: parent.height-4 - // height: parent.height-4 - // topPadding: 2 - // bottomPadding: 2 - // leftPadding: 8 - // rightPadding: 8 - sourceSize.height: 24 - sourceSize.width: 24 - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter + sourceSize.height: 20 //24 + sourceSize.width: 20 //24 anchors.centerIn: parent - smooth: true - antialiasing: true - layer { - enabled: true - effect: ColorOverlay { color: imgOverlayColor } - } + 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 } } @@ -59,8 +70,8 @@ Button { 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 } + GradientStop { position: 0.0; color: widget.down || (isActive && !isActiveViaIndicator)? bgColorPressed: "#33FFFFFF" } + GradientStop { position: 1.0; color: widget.down || (isActive && !isActiveViaIndicator)? bgColorPressed: forcedBgColorNormal } } Rectangle { @@ -74,10 +85,16 @@ Button { 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 + /*onPressed: focus = true + onReleased: focus = false*/ } diff --git a/ui/qml/reskin/widgets/buttons/XsSearchButton.qml b/ui/qml/reskin/widgets/buttons/XsSearchButton.qml new file mode 100644 index 000000000..94de70cca --- /dev/null +++ b/ui/qml/reskin/widgets/buttons/XsSearchButton.qml @@ -0,0 +1,55 @@ +// 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 index 69adbf83b..621af1140 100644 --- a/ui/qml/reskin/widgets/buttons/XsSecondaryButton.qml +++ b/ui/qml/reskin/widgets/buttons/XsSecondaryButton.qml @@ -8,8 +8,9 @@ import xStudioReskin 1.0 Button { id: widget - property string imgSrc: "" + property alias imgSrc: imageDiv.source property bool isActive: false + property bool onlyVisualyEnabled: false property color imgOverlayColor: "#C1C1C1" property color bgColorPressed: palette.highlight @@ -19,34 +20,24 @@ Button { 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 ? 1.0 : 0.33 - Image { + opacity: enabled || onlyVisualyEnabled ? 1.0 : 0.33 + XsImage { id: imageDiv - source: imgSrc - // width: parent.height-4 - // height: parent.height-4 - // topPadding: 2 - // bottomPadding: 2 - // leftPadding: 8 - // rightPadding: 8 sourceSize.height: 16 sourceSize.width: 16 - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter + imgOverlayColor: widget.imgOverlayColor anchors.centerIn: parent - smooth: true - antialiasing: true - layer { - enabled: true - effect: ColorOverlay { color: imgOverlayColor } - } } } @@ -72,6 +63,15 @@ Button { } } + + 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 new file mode 100644 index 000000000..0783a4936 --- /dev/null +++ b/ui/qml/reskin/widgets/controls/XsScrollBar.qml @@ -0,0 +1,27 @@ +// 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/controls/XsSlider.qml b/ui/qml/reskin/widgets/controls/XsSlider.qml new file mode 100644 index 000000000..f8dc53867 --- /dev/null +++ b/ui/qml/reskin/widgets/controls/XsSlider.qml @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.14 +import QtGraphicalEffects 1.15 + +import xStudioReskin 1.0 + +Slider{ id: widget + from: 0 + to: 10 + value: from + + stepSize: 0 + snapMode: Slider.SnapAlways + // orientation: Qt.Vertical + + focusPolicy: Qt.WheelFocus + wheelEnabled: true + topPadding: 0 + + property bool isHorizontal: orientation==Qt.Horizontal + property color fillColor: palette.highlight + property color handleColor: palette.text + + signal released() + onPressedChanged : { + if (pressed === false) { + released() + } + } + + background: Rectangle { + x: isHorizontal? + widget.leftPadding : + widget.leftPadding + widget.availableWidth/2 - width/2 + y: isHorizontal? + widget.topPadding + widget.availableHeight/2 - height/2 : + widget.topPadding + implicitWidth: isHorizontal? 250:6 + implicitHeight: isHorizontal? 6:250 + width: isHorizontal? widget.availableWidth : implicitWidth + height: isHorizontal? implicitHeight : widget.availableHeight + radius: implicitHeight/2 + color: fillColor + // rotation: isHorizontal? -90 : 0 + + Rectangle { + width: isHorizontal? widget.visualPosition*parent.width : parent.width + height: isHorizontal? parent.height : widget.visualPosition*parent.height + color: palette.base + radius: parent.radius + } + } + + handle: Rectangle { + x: isHorizontal? + widget.leftPadding + widget.visualPosition * (widget.availableWidth - width) : + widget.leftPadding + widget.availableWidth / 2 - width / 2 + y: isHorizontal? + widget.topPadding + widget.availableHeight / 2 - height / 2 : + widget.topPadding + widget.visualPosition * (widget.availableHeight - height) + implicitWidth: 16 + implicitHeight: 6 + // radius: implicitHeight/2 + color: widget.pressed ? Qt.darker(handleColor,1.3) : handleColor + border.color: widget.hovered? palette.highlight : "transparent" + } + +} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml b/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml new file mode 100644 index 000000000..ffac45461 --- /dev/null +++ b/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml @@ -0,0 +1,29 @@ +// 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 new file mode 100644 index 000000000..0e4f24b45 --- /dev/null +++ b/ui/qml/reskin/widgets/dialogs/XsPopup.qml @@ -0,0 +1,34 @@ +// 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 new file mode 100644 index 000000000..35e6b353d --- /dev/null +++ b/ui/qml/reskin/widgets/labels/XsText.qml @@ -0,0 +1,54 @@ +// 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 new file mode 100644 index 000000000..ef59a2098 --- /dev/null +++ b/ui/qml/reskin/widgets/labels/XsTextField.qml @@ -0,0 +1,60 @@ +// 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 new file mode 100644 index 000000000..d66981593 --- /dev/null +++ b/ui/qml/reskin/widgets/labels/XsToolTip.qml @@ -0,0 +1,56 @@ +// 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 index a8067e4d9..2528dde14 100644 --- a/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml +++ b/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 1.4 import QtQml.Models 2.14 @@ -8,11 +9,15 @@ import xstudio.qml.models 1.0 Rectangle { - height: XsStyleSheet.menuHeight - color: XsStyleSheet.menuBarColor 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, + // this gives us access to the global tree model that defines menus, // sub-menus and menu items XsMenusModel { id: menus_model @@ -26,97 +31,235 @@ Rectangle { // 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: "Do Something" - menuPath: "Session|Something|Something Else" + text: "New Session" + menuPath: "File" menuItemPosition: 1 menuModelName: "main menu bar" - hotkey: "Ctrl+Z" onActivated: { - console.log("Clicke on File~Load") } } - XsMenuModelItem { - menuItemType: "divider" - menuPath: "Session|Something|Something Else" + 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: "Load" - menuPath: "File" + text: "Flag Media" + menuPath: "Media" menuItemPosition: 1 menuModelName: "main menu bar" onActivated: { - console.log("Clicke on File~Load") } } XsMenuModelItem { - text: "Save" - menuPath: "File" - menuItemPosition: 2 + text: "New Sequence" + menuPath: "Timeline" + menuItemPosition: 1 menuModelName: "main menu bar" - hotkey: "Ctrl+S" onActivated: { - console.log("Well I never!") } } XsMenuModelItem { - menuItemType: "divider" - menuPath: "File" - menuItemPosition: 3 + text: "Play/Pause" + menuPath: "Playback" + menuItemPosition: 1 menuModelName: "main menu bar" + onActivated: { + } } XsMenuModelItem { - text: "Quit" - menuPath: "File" - menuItemPosition: 4 + text: "Hide UI" + menuPath: "Viewer" + menuItemPosition: 1 menuModelName: "main menu bar" onActivated: { - console.log("Well I never!") } } XsMenuModelItem { - text: "New" - menuPath: "Playlists" + text: "Save Layout.." + menuPath: "Layout" menuItemPosition: 1 menuModelName: "main menu bar" onActivated: { - console.log("Well I never!") } } XsMenuModelItem { - text: "Publish All" - menuPath: "Playlists|Publish" + text: "Drawing Tools" + menuPath: "Panels" menuItemPosition: 1 - hotkey: "Ctrl+P" menuModelName: "main menu bar" onActivated: { - console.log("Well I never!") } } XsMenuModelItem { - text: "Publish Selected" + 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: { - console.log("Well I never!") } } + XsMenuModelItem { - text: "Colour Management" + text: "Bypass Colour Management" menuItemType: "toggle" menuPath: "Colour" menuItemPosition: 1 @@ -129,34 +272,96 @@ Rectangle { XsMenuModelItem { text: "Channels" - menuPath: "Colour" + menuPath: "" menuItemType: "multichoice" menuItemPosition: 1 choices: ["RGB", "R", "G", "B", "A", "Luminance"] - currentChoice: "RGB" + currentChoice: "Luminance" menuModelName: "main menu bar" onCurrentChoiceChanged: { console.log("currentChoice", currentChoice) } } - ListView { + // 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 - spacing: 0 //10 - snapMode: ListView.SnapToItem + isScrollbarVisibile: false - model: DelegateModel { + model: DelegateModel { model: menus_model rootIndex: root_index - delegate: XsMenuItemNew { - + delegate: XsMenuItemNew { + menu_model: menus_model - // As we loop over the top level items in the 'main menu bar' + // 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) diff --git a/ui/qml/reskin/widgets/menus/XsMenu.qml b/ui/qml/reskin/widgets/menus/XsMenu.qml index c93f8a252..f847086de 100644 --- a/ui/qml/reskin/widgets/menus/XsMenu.qml +++ b/ui/qml/reskin/widgets/menus/XsMenu.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQml.Models 2.14 @@ -6,38 +7,24 @@ import Qt.labs.qmlmodels 1.0 import xStudioReskin 1.0 import xstudio.qml.models 1.0 -Popup { +XsPopup { id: the_popup + // x: 30 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 bgColorPressed: palette.highlight - 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 } - } - } + property alias menuWidth: view.width ListView { id: view orientation: ListView.Vertical spacing: 0 - width: contentWidth + width: 160 //contentWidth height: contentHeight contentHeight: contentItem.childrenRect.height contentWidth: contentItem.childrenRect.width @@ -55,9 +42,8 @@ Popup { delegate: chooser DelegateChooser { - id: chooser - role: "menu_item_type" + role: "menu_item_type" DelegateChoice { roleValue: "button" @@ -72,7 +58,11 @@ Popup { 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 { @@ -88,49 +78,85 @@ Popup { the_popup.menu_model_index // the parent index into the model ) parent_menu: the_popup + parentWidth: view.width } } DelegateChoice { roleValue: "divider" - XsMenuDivider {} + XsMenuDivider { + parentWidth: view.width + } } DelegateChoice { - roleValue: "choice" + 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: "multichoice" + roleValue: "toggle" - XsMenuItemNew { + 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" + 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) - onChecked:{ + + 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 index a6a6b3e4b..4bdb2a5a7 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuChoice.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuChoice.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQml.Models 2.14 diff --git a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml index 4bc60ef28..901350266 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml @@ -1,11 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xStudioReskin 1.0 Item { - width: parent.width + width: parentWidth height: XsStyleSheet.menuPadding*2 + XsStyleSheet.menuDividerHeight + property real parentWidth: 0 + Rectangle { width: parent.width height: XsStyleSheet.menuDividerHeight diff --git a/ui/qml/reskin/widgets/menus/XsMenuItem.qml b/ui/qml/reskin/widgets/menus/XsMenuItem.qml index e8ede226b..174f07d25 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuItem.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuItem.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQml.Models 2.14 @@ -10,7 +11,9 @@ import xstudio.qml.models 1.0 Item { id: widget - width: ( (menuRealWidth > menuStdWidth) || is_in_bar )? menuRealWidth : menuStdWidth + // 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 @@ -18,16 +21,20 @@ Item { 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 menuStdWidth: XsStyleSheet.menuStdWidth + property real parentWidth: 0 + property real menuWidth: parentWidth>" : "") height: parent.height font.pixelSize: XsStyleSheet.fontSize font.family: XsStyleSheet.fontFamily color: labelColor - anchors.left: parent.left + 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 { id: hotKeyDiv text: hotkey ? " " + hotkey : "" height: parent.height font: labelDiv.font @@ -117,12 +178,14 @@ Item { id: labelMetrics font: labelDiv.font text: labelDiv.text + hotKeyDiv.text + // Component.onCompleted: { + // console.log("matrix:", width, text, menuRealWidth) + // } } - Image { - id: subMenuIndicatorDiv + Image { id: subMenuIndicatorDiv visible: sub_menu? !is_in_bar: false - source: "qrc:/assets/icons/new/chevron_right.svg" + source: "qrc:/icons/chevron_right.svg" sourceSize.height: 16 sourceSize.width: 16 horizontalAlignment: Image.AlignHCenter @@ -138,7 +201,6 @@ Item { } } - Component.onCompleted: { make_submenus() } @@ -166,26 +228,17 @@ Item { widget, { menu_model: widget.menu_model, - menu_model_index: widget.menu_model_index - }) + 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") == "choice") { - let component = Qt.createComponent("./XsMenuChoice.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()) } } + } diff --git a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml b/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml index b0d79dfe3..29e5a897c 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQml.Models 2.14 @@ -13,27 +14,31 @@ Item { // 'hotkey' strings. id: widget - width: menuRealWidth menuStdWidth)? menuWidth : menuStdWidth height: XsStyleSheet.menuHeight - property real menuRealWidth: checkBox.width + labelMetrics.width + (checkBoxPadding*2) + (labelPadding) - + 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 checked() + signal clicked() property bool isHovered: menuMouseArea.containsMouse property bool isActive: menuMouseArea.pressed property bool isFocused: menuMouseArea.activeFocus - property bool isRadioButton: false - property var radioSelectedChoice: "" + 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 0 ? '+' : '') + playhead.sourceOffsetFrames @@ -40,32 +39,6 @@ Rectangle { NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } } - /*Rectangle { - id: filename_display - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: offset_group.left - color: "transparent" - - Label { - - id: label - text: filename - color: XsStyle.controlColor - anchors.fill: parent - anchors.leftMargin: 8 - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - - font { - pixelSize: XsStyle.mediaInfoBarFontSize+2 - family: XsStyle.controlTitleFontFamily - hintingPreference: Font.PreferNoHinting - } - } - }*/ - RowLayout { anchors.fill: parent @@ -123,518 +96,4 @@ Rectangle { } } - - /*Rectangle { - - id: pixel_colour - color: "transparent" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - property int comp_width: 34 - width: comp_width*3 - - Text { - - text: mediaInfoBar.pixel_colour[0] - color: "#f66" - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - x: 0 - - font { - family: XsStyle.pixValuesFontFamily - pixelSize: XsStyle.pixValuesFontSize - } - - } - - Text { - - text: mediaInfoBar.pixel_colour[1] - color: "#6f6" - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - x: pixel_colour.comp_width - - font { - family: XsStyle.pixValuesFontFamily - pixelSize: XsStyle.pixValuesFontSize - } - - } - - Text { - - text: mediaInfoBar.pixel_colour[2] - color: "#88f" - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - x: pixel_colour.comp_width*2 - - font { - family: XsStyle.pixValuesFontFamily - pixelSize: XsStyle.pixValuesFontSize - } - - } - - }*/ - - /* - - ListModel { - - id: basic_data - - ListElement { - labelText: "Format" - tooltip: "The image format or video codec of the current source" - demotext: "OpenEXR" - } - ListElement { - labelText: "Bit Depth" - tooltip: "The image bitdepth of the current source" - demotext: "16 bit float" - } - ListElement { - labelText: "FPS" - tooltip: "The playback rate of the current source" - demotext: "23.976" - } - ListElement { - labelText: "Res" - tooltip: "The image resolution in pixels of the current source" - demotext: "8888 x 8888" - } - } - - - - Rectangle { - id: topLine - anchors.left: parent.left - anchors.right: parent.right - height: 2 - y: 1 - color: XsStyle.mediaInfoBarBorderColour - } - - Rectangle { - id: bottomLine - anchors.left: parent.left - anchors.right: parent.right - height: 2 - y: parent.height-2 - color: XsStyle.mediaInfoBarBorderColour - }*/ -} - -/* ListModel { - - id: firstPartModel - - ListElement { - labelText: "FileName" - keyword: "filename" - tooltip: "The filename of the current source" - demotext: "The filename of the current source" - } - - } - - ListModel { - - id: midPartModel - - ListElement { - labelText: "Compare Layer" - keyword: "compare_layer" - tooltip: "In A/B compare mode, indicates which source you are viewing. Hit numeric keys to switch between sources." - demotext: "A" - } - - } - - ListModel { - - id: secondPartModel - - ListElement { - labelText: "Format" - keyword: "format" - tooltip: "The image format or video codec of the current source" - demotext: "OpenEXR" - } - ListElement { - labelText: "Bit Depth" - keyword: "bitdepth" - tooltip: "The image bitdepth of the current source" - demotext: "16 bit float" - } - ListElement { - labelText: "FPS" - keyword: "fps" - tooltip: "The playback rate of the current source" - demotext: "23.976" - } - ListElement { - labelText: "Res" - keyword: "resolution" - tooltip: "The image resolution in pixels of the current source" - demotext: "8888 x 8888" - } - } - - ListModel { - id: pixColourModel - ListElement { - channel: "R" - keyword: "red_pix_val" - } - ListElement { - channel: "G" - keyword: "green_pix_val" - } - ListElement { - channel: "B" - keyword: "blue_pix_val" - } - } - - Component { - - id: mediaInfoItemDelegate - - Row - { - spacing: 8 - Layout.fillWidth: keyword == 'filename' - enabled: keyword == 'compare_layer' ? mediaInfoBar.offset_enabled : true - - Rectangle { - width: mediaInfoBar.itemSpacing - height: 10 - color: "transparent" - visible: enabled ? (keyword != 'filename') : false - } - - Label { - - id: label - text: labelText - color: XsStyle.controlTitleColor - visible: enabled - Layout.fillWidth: keyword == 'filename' - property bool mouseHovered: mouseArea.containsMouse - horizontalAlignment: Qt.AlignRight - verticalAlignment: Qt.AlignVCenter - - font { - pixelSize: XsStyle.mediaInfoBarFontSize - family: XsStyle.controlTitleFontFamily - hintingPreference: Font.PreferNoHinting - } - - TextMetrics { - id: textMetricsL - font: label.font - text: label.text - } - - width: textMetricsL.width - - MouseArea { - id: mouseArea - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - if(mouse.button & Qt.RightButton) { - contextMenu.popup() - } - } - } - - onMouseHoveredChanged: { - if (mouseHovered) { - status_bar.normalMessage(tooltip, labelText) - } else { - status_bar.clearMessage() - } - } - } - - Label { - - id: value - text: getMediaInfo(keyword) - color: XsStyle.controlColor - visible: enabled - property bool fill_space: {keyword == 'filename'} - width: { - return fill_space ? parent.width - 100 : textMetrics.width - } - Layout.fillWidth: fill_space - elide: fill_space ? Text.ElideLeft : Text.ElideNone - - horizontalAlignment: Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - - font { - pixelSize: XsStyle.mediaInfoBarFontSize - family: XsStyle.controlContentFontFamily - hintingPreference: Font.PreferNoHinting - } - - TextMetrics { - id: textMetrics - font: value.font - text: value.fill_space ? value.text : demotext - } - - } - - } - } - - Component { - - id: pixColourDelegate - Row - { - spacing: 4 - - Label { - - id: label - text: channel - color: XsStyle.controlTitleColor - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 3 - verticalAlignment: Qt.AlignVCenter - - font { - pixelSize: XsStyle.mediaInfoBarFontSize - family: XsStyle.controlTitleFontFamily - hintingPreference: Font.PreferNoHinting - } - - TextMetrics { - id: textMetrics - font: label.font - text: label.text - } - - width: textMetrics.width - } - - Label { - - id: value - text: getMediaInfo(keyword) - anchors.margins: 3 - color: XsStyle.controlColor - // VCenter doesn't quite work with this fixed width font - y: (parent.height-valTextMetrics.height)/2+1 - - font { - pixelSize: XsStyle.pixValuesFontSize - family: XsStyle.pixValuesFontFamily - hintingPreference: Font.PreferNoHinting - } - - TextMetrics { - id: valTextMetrics - font: value.font - text: value.text - } - - width: 32 - - } - - Label { - width: 4 - } - } - } - - RowLayout { - - id: row_layout - anchors.fill: parent - Layout.fillWidth: true - - Rectangle { - width: 5 - } - - Repeater { - model: firstPartModel - delegate: mediaInfoItemDelegate - } - - Repeater { - model: midPartModel - delegate: mediaInfoItemDelegate - visible: mediaInfoBar.offset_enabled - } - - Rectangle { - width: mediaInfoBar.itemSpacing - height: 10 - color: "transparent" - visible: mediaInfoBar.offset_enabled - } - - Rectangle { - - color: label.mouseHovered ? XsStyle.highlightColor : XsStyle.mediaInfoBarBackground - width: { label.width + offsetInputBox.width + 16} - Layout.fillHeight: true - Layout.topMargin: 3 - Layout.bottomMargin: 2 - visible: mediaInfoBar.offset_enabled - - id: offset_group - - Label { - - id: label - text: 'Offset' - color: enabled ? (mouseHovered ? "white" : XsStyle.controlTitleColor) : XsStyle.controlTitleColorDisabled - enabled: mediaInfoBar.offset_enabled - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 3 - verticalAlignment: Qt.AlignVCenter - property bool mouseHovered: mouseArea.containsMouse - - font { - pixelSize: XsStyle.mediaInfoBarFontSize - family: XsStyle.controlTitleFontFamily - hintingPreference: Font.PreferNoHinting - } - - TextMetrics { - id: textMetricsL - font: label.font - text: label.text - } - width: textMetricsL.width - - MouseArea { - - id: mouseArea - property var offsetStart: 0 - property var xdown - property bool dragging: false - anchors.fill: parent - // We make the mouse area massive so the cursor remains - // as Qt.SizeHorCursor during dragging - anchors.margins: dragging ? -2048 : 0 - - acceptedButtons: Qt.LeftButton - hoverEnabled: true - focus: true - cursorShape: containsMouse ? Qt.SizeHorCursor : Qt.ArrowCursor - onPressed: { - dragging = true - offsetStart = playhead.sourceOffsetFrames - xdown = mouseX - focus = true - } - onReleased: { - dragging = false - focus = false - } - onMouseXChanged: { - if (pressed) { - var new_offset = offsetStart + Math.round((mouseX - xdown)/10) - playhead.sourceOffsetFrames = new_offset - } - } - - } - - onMouseHoveredChanged: { - if (mouseHovered) { - status_bar.normalMessage("In a/b mode sets frame offset on this source relative to others. Click and drag this label to adjust with mouse.", "Source compare offset") - } else { - status_bar.clearMessage() - } - } - - } - - Rectangle { - - color: enabled ? XsStyle.mediaInfoBarOffsetBgColour : XsStyle.mediaInfoBarOffsetBgColourDisabled - border.color: enabled ? XsStyle.mediaInfoBarOffsetEdgeColour : XsStyle.mediaInfoBarOffsetEdgeColourDisabled - border.width: 1 - width: offsetInput.font.pixelSize*2 - height: offsetInput.font.pixelSize*1.2 - id: offsetInputBox - enabled: mediaInfoBar.offset_enabled - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: 3 - - TextInput { - - id: offsetInput - text: playhead ? "" + playhead.sourceOffsetFrames : "" - Layout.minimumWidth: font.pixelSize*2 - width: font.pixelSize*2 - color: enabled ? XsStyle.controlColor : XsStyle.controlColorDisabled - selectByMouse: true - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - - font { - pixelSize: XsStyle.mediaInfoBarFontSize - family: XsStyle.controlContentFontFamily - hintingPreference: Font.PreferNoHinting - } - - onEditingFinished: { - focus = false - playhead.sourceOffsetFrames = parseInt(text) - } - } - - - } - } - - Repeater { - model: secondPartModel - delegate: mediaInfoItemDelegate - } - - Rectangle { - width: mediaInfoBar.itemSpacing - height: 10 - color: "transparent" - visible: mediaInfoBar.offset_enabled - } - - Repeater { - model: pixColourModel - delegate: pixColourDelegate - } - - } - -}*/ +} \ No newline at end of file diff --git a/ui/qml/xstudio/bars/XsMenuBar.qml b/ui/qml/xstudio/bars/XsMenuBar.qml index d60c1bbb4..40bee2caa 100644 --- a/ui/qml/xstudio/bars/XsMenuBar.qml +++ b/ui/qml/xstudio/bars/XsMenuBar.qml @@ -48,7 +48,7 @@ MenuBar { onFocusChanged: { // this prevents stealing keypresses that might be needed elsewhere. // (like space for play/pause-- not opening a menu) - focus = false + //focus = false } } @@ -63,9 +63,9 @@ MenuBar { id: playback_menu } XsViewerContextMenu { - is_popout_viewport: viewport.is_popout_viewport } XsLayoutMenu {} + XsSnapshotMenu {} XsPanelMenu { id: panel_menu } diff --git a/ui/qml/xstudio/bars/XsShortcuts.qml b/ui/qml/xstudio/bars/XsShortcuts.qml index 47d90e544..2afb8614f 100644 --- a/ui/qml/xstudio/bars/XsShortcuts.qml +++ b/ui/qml/xstudio/bars/XsShortcuts.qml @@ -202,7 +202,7 @@ Item { // controlled behaviour) XsModuleAttributes { id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings" + attributesGroupNames: "annotations_tool_settings_0" } XsHotkey { @@ -215,7 +215,7 @@ Item { XsHotkey { context: shortcuts.context - sequence: "Shift+p" + sequence: "Ctrl+Shift+p" name: "Create New Playlist" description: "Creates a new playlist." onActivated: sessionFunction.newPlaylist( @@ -225,7 +225,7 @@ Item { XsHotkey { context: shortcuts.context - sequence: "Shift+d" + sequence: "Ctrl+Shift+d" name: "Create Divider" description: "Creates a divider in the session playlist view." onActivated: sessionFunction.newDivider( @@ -244,7 +244,7 @@ Item { } XsHotkey { context: shortcuts.context - sequence: "Shift+s" + sequence: "Ctrl+Shift+s" name: "Create Subset" description: "Creates a playlsit subset under the current playlist." onActivated: { @@ -258,20 +258,20 @@ Item { } XsHotkey { context: shortcuts.context - sequence: "Shift+t" + sequence: "Ctrl+Shift+t" name: "Create Timeline" description: "Creates a timeline under the current playlist." } XsHotkey { context: shortcuts.context - sequence: "Shift+c" + sequence: "Ctrl+Shift+c" name: "Create Contact Sheet" description: "Creates a contact sheet under the current playlist." } XsHotkey { context: shortcuts.context - sequence: "Shift+i" + sequence: "Ctrl+Shift+i" name: "Create Playlist Divider" description: "Creates a divider within the subsets of the current playlist." onActivated: { @@ -327,14 +327,6 @@ Item { onActivated: playerWidget.toggleFullscreen() } - XsHotkey { - context: shortcuts.context - sequence: "Alt+f" - name: "Toggle Full Screen" - description: "Toggles the xStudio UI in/out of full-screen mode" - onActivated: parent_win.fitWindowToImage() - } - XsHotkey { context: shortcuts.context sequence: "1" @@ -409,12 +401,17 @@ Item { 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) + //onActivated: app_window.sessionFunction.flagSelectedMedia(colour, control.name) + onActivated: { + let model = app_window.mediaFlags + app_window.sessionFunction.flagSelectedMedia(colour, myName) + } } } } @@ -454,7 +451,7 @@ Item { } XsHotkey { context: shortcuts.context - sequence: "Ctrl+shift+s" + sequence: "Ctrl+Shift+s" name: "Save Session As" description: "Saves current session under a new file path." onActivated: app_window.sessionFunction.saveSessionAs() diff --git a/ui/qml/xstudio/bars/XsToolBar.qml b/ui/qml/xstudio/bars/XsToolBar.qml index f12803146..186f518b7 100644 --- a/ui/qml/xstudio/bars/XsToolBar.qml +++ b/ui/qml/xstudio/bars/XsToolBar.qml @@ -7,6 +7,7 @@ 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 @@ -22,9 +23,9 @@ Rectangle { return (barHeight + topPadding) * opacity } - XsModuleAttributesModel { - id: attrs - attributesGroupNames: [viewport.name + "_toolbar", "any_toolbar"] + XsModuleData { + id: tester + modelDataName: viewport.name + "_toolbar" } opacity: 1 @@ -73,36 +74,7 @@ Rectangle { id: the_view anchors.fill: parent - model: attrs - property var ordering: [] - - onItemAdded: { - arrange_widgets() - } - - onItemRemoved: { - arrange_widgets() - } - - function arrange_widgets() { - - function compare( a, b ) { - if ( a[1] < b[1] ) { return -1; } - if ( a[1] > b[1] ) { return 1; } - return 0; - } - - var toolbar_items = [] - for (var idx = 0; idx < count; idx++) { - if (itemAt(idx)) toolbar_items.push([itemAt(idx), itemAt(idx).order_value]) - } - toolbar_items.sort( compare ); - - for (var idx = 0; idx < toolbar_items.length; idx++) { - toolbar_items[idx][0].ordered_x_position = idx - } - - } + model: tester delegate: Item { @@ -111,7 +83,7 @@ Rectangle { anchors.top: parent.top width: (myBar.width-separator_width*(the_view.count-1))/the_view.count property var ordered_x_position: 0 - x: ordered_x_position*(width+separator_width) + x: index*(width+separator_width) property var dynamic_widget // 'title', 'type', 'qml_code' attributes may or may not be provided by the Repeater model, diff --git a/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml b/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml index 644053d5e..090df945b 100644 --- a/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml +++ b/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml @@ -15,6 +15,7 @@ Item { 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() diff --git a/ui/qml/xstudio/base/core/XsSortFilterModel.qml b/ui/qml/xstudio/base/core/XsSortFilterModel.qml index ce2981b76..e73e76dba 100644 --- a/ui/qml/xstudio/base/core/XsSortFilterModel.qml +++ b/ui/qml/xstudio/base/core/XsSortFilterModel.qml @@ -13,45 +13,124 @@ DelegateModel { onSrcModelChanged: model = srcModel + signal updated() + function update() { - if (items.count > 0) { - items.setGroups(0, items.count, "items"); - } + hiddenItems.setGroups(0, hiddenItems.count, "unsorted") + items.setGroups(0, items.count, "unsorted") + } - // 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); + 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) - // 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); + if(!filterAcceptsItem(item.model)) { + item.groups = "hidden" + } else { + const index = insertPosition(lessThan, item) + item.groups = "items" + items.move(item.itemsIndex, index) } } } - items.onChanged: update() - onLessThanChanged: update() - onFilterAcceptsItemChanged: update() + items.includeByDefault: false + groups: [ + DelegateModelGroup { + id: unsortedItems + name: "unsorted" - groups: DelegateModelGroup { - id: visibleItems + includeByDefault: true - name: "ivisible" - includeByDefault: false - } + 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 +// filterOnGroup: "ivisible" +// } \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml b/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml index c064c5cd6..17bf8664a 100644 --- a/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml +++ b/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml @@ -29,7 +29,7 @@ XsDialogModal { XsLabel { Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillHeight: visible ? true : false Layout.minimumHeight: 20 Layout.alignment: Qt.AlignVCenter|Qt.AlignHCenter diff --git a/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml b/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml index d72b317c0..0693e0ebf 100644 --- a/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml +++ b/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml @@ -7,6 +7,7 @@ import Qt.labs.qmlmodels 1.0 import xStudio 1.1 import xstudio.qml.module 1.0 +import xstudio.qml.models 1.0 XsWindow { @@ -22,6 +23,11 @@ XsWindow { attributesGroupNames: dialog.attributesGroupNames } + /*XsModuleData { + id: attribute_set + modelDataName: dialog.attributesGroupNames + }*/ + RowLayout { anchors.fill: parent diff --git a/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml b/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml index 683a81c69..473316982 100644 --- a/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml +++ b/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml @@ -80,7 +80,7 @@ XsDialogModal { Layout.topMargin: 10 Layout.minimumHeight: 20 - focus: true + //focus: true Keys.onReturnPressed: okayed() Keys.onEscapePressed: cancelled() diff --git a/ui/qml/xstudio/base/dialogs/XsWindow.qml b/ui/qml/xstudio/base/dialogs/XsWindow.qml index 67dd46624..d517fb83e 100644 --- a/ui/qml/xstudio/base/dialogs/XsWindow.qml +++ b/ui/qml/xstudio/base/dialogs/XsWindow.qml @@ -66,6 +66,6 @@ ApplicationWindow { } } - flags: (asDialog ? Qt.Dialog : asWindow ? Qt.WindowSystemMenuHint : Qt.Tool) |(frameLess ? Qt.FramelessWindowHint : 0) | (onTop ? Qt.WindowStaysOnTopHint : 0) + 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 index 4bec00e0a..4f513c32d 100644 --- a/ui/qml/xstudio/base/widgets/XsBoolAttrCheckBox.qml +++ b/ui/qml/xstudio/base/widgets/XsBoolAttrCheckBox.qml @@ -63,6 +63,7 @@ Rectangle { anchors.fill: parent hoverEnabled: true onClicked: { + console.log("clicked", value) value = !value } } diff --git a/ui/qml/xstudio/base/widgets/XsBorder.qml b/ui/qml/xstudio/base/widgets/XsBorder.qml new file mode 100644 index 000000000..562dda692 --- /dev/null +++ b/ui/qml/xstudio/base/widgets/XsBorder.qml @@ -0,0 +1,62 @@ +// 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/XsCheckBoxWithMultiComboBox.qml b/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml index da520e5f6..0a69eba8a 100644 --- a/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml +++ b/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml @@ -16,10 +16,10 @@ Control { property alias checked: checkBox.checked property alias popup: multiComboBox.popup property alias checkedIndexes: multiComboBox.checkedIndexes - + signal hide() onHide:{ - multiComboBox.close() + multiComboBox.close() } XsCheckbox{ id: checkBox @@ -39,10 +39,10 @@ Control { 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/XsComboBoxMultiSelect.qml b/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml index 980d38949..67919fd2d 100644 --- a/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml +++ b/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml @@ -38,12 +38,14 @@ Item{ id: widget } } 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() @@ -74,13 +76,8 @@ Item{ id: widget model: valuesModel } - - - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: itemHeight - - + // width: parent.width + // height: itemHeight Rectangle{ id: searchField width: parent.width @@ -92,7 +89,7 @@ Item{ id: widget XsTextField { id: searchTextField width: parent.width - height: itemHeight + height: widget.height font.pixelSize: fontSize*1.2 placeholderText: hint forcedHover: arrowButton.hovered @@ -145,7 +142,7 @@ Item{ id: widget text: "" imgSrc: isActive?"qrc:/feather_icons/chevron-up.svg": "qrc:/feather_icons/chevron-down.svg" width: height - height: itemHeight - framePadding + height: widget.height - framePadding anchors.verticalCenter: parent.verticalCenter anchors.right: searchTextField.right anchors.rightMargin: framePadding/2 @@ -159,7 +156,7 @@ Item{ id: widget valuesPopup.visible = false arrowButton.isArrowBtnClicked = false } - else{ + else{ valuesPopup.visible = true arrowButton.isArrowBtnClicked = true } @@ -172,7 +169,7 @@ Item{ id: widget imgSrc: "qrc:/feather_icons/x.svg" visible: searchTextField.length!=0 width: height - height: itemHeight - framePadding + height: widget.height - framePadding anchors.verticalCenter: parent.verticalCenter anchors.right: checkedCount>0 && countDisplay.visible? countDisplay.left: arrowButton.left anchors.rightMargin: framePadding/2 @@ -188,7 +185,7 @@ Item{ id: widget font.pixelSize: text.length==1? 10 : 9 visible: checkedCount>0 width: height - height: itemHeight - framePadding*1.10 + height: widget.height - framePadding*1.10 borderRadius: width/1.2 isActive: isCountBtnClicked //isFiltered textColorNormal: isActive? "light grey": palette.highlight @@ -205,7 +202,7 @@ Item{ id: widget countDisplay.isCountBtnClicked = false } - else{ + else{ isFiltered = true valuesPopup.visible = true @@ -222,7 +219,7 @@ Item{ id: widget } ListView{ id: valuesPopup z: 10 - property real valuesItemHeight: itemHeight/1.3 + property real valuesItemHeight: widget.height/1.3 model: valuesModel Rectangle{ anchors.fill: parent; color: "transparent"; diff --git a/ui/qml/xstudio/base/widgets/XsElideLabel.qml b/ui/qml/xstudio/base/widgets/XsElideLabel.qml new file mode 100644 index 000000000..a03542ecb --- /dev/null +++ b/ui/qml/xstudio/base/widgets/XsElideLabel.qml @@ -0,0 +1,39 @@ +// 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/XsModuleSubMenu.qml b/ui/qml/xstudio/base/widgets/XsModuleSubMenu.qml index 80a77278b..5d6adafd7 100644 --- a/ui/qml/xstudio/base/widgets/XsModuleSubMenu.qml +++ b/ui/qml/xstudio/base/widgets/XsModuleSubMenu.qml @@ -22,6 +22,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim2 root_menu_name: submenu_.root_menu_name ? submenu_.root_menu_name : "" + } Instantiator { @@ -46,6 +47,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim3 root_menu_name: submenu2_.root_menu_name + } Instantiator { @@ -70,6 +72,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim4 root_menu_name: submenu3_.root_menu_name + } Instantiator { @@ -94,6 +97,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim5 root_menu_name: submenu4_.root_menu_name + } Instantiator { @@ -118,6 +122,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim6 root_menu_name: submenu5_.root_menu_name + } Instantiator { @@ -142,6 +147,7 @@ XsMenu { XsModuleMenu { id: module_menu_shim7 root_menu_name: submenu6_.root_menu_name + } } diff --git a/ui/qml/xstudio/base/widgets/XsSplitView.qml b/ui/qml/xstudio/base/widgets/XsSplitView.qml index b0b1f11c7..954d9b626 100644 --- a/ui/qml/xstudio/base/widgets/XsSplitView.qml +++ b/ui/qml/xstudio/base/widgets/XsSplitView.qml @@ -10,6 +10,8 @@ SplitView { property color textColorActive: "white" property color textColorNormal: "light grey" + focus: false + property Component splitHandleHorizontal: Rectangle { implicitWidth: framePadding; implicitHeight: framePadding; color: "transparent" @@ -54,8 +56,4 @@ SplitView { orientation: Qt.Horizontal handle: orientation === Qt.Horizontal? splitHandleHorizontal: splitHandleVertical - - // anchors.fill: parent - - } \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsTickWidget.qml b/ui/qml/xstudio/base/widgets/XsTickWidget.qml new file mode 100644 index 000000000..ecd17e363 --- /dev/null +++ b/ui/qml/xstudio/base/widgets/XsTickWidget.qml @@ -0,0 +1,80 @@ +// 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/xstudio/base/widgets/XsTimelineCursor.qml b/ui/qml/xstudio/base/widgets/XsTimelineCursor.qml new file mode 100644 index 000000000..90f23a8ab --- /dev/null +++ b/ui/qml/xstudio/base/widgets/XsTimelineCursor.qml @@ -0,0 +1,61 @@ +// 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: XsStyle.highlightColor + + 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/xstudio/base/widgets/XsToolbarItem.qml b/ui/qml/xstudio/base/widgets/XsToolbarItem.qml index 9fa9f8be3..e4979c0ba 100644 --- a/ui/qml/xstudio/base/widgets/XsToolbarItem.qml +++ b/ui/qml/xstudio/base/widgets/XsToolbarItem.qml @@ -25,13 +25,14 @@ Rectangle { property bool is_overridden: override_value ? (override_value != "" ? true : false) : false property var value_text: value ? value : "" property var display_value: is_overridden ? override_value : value_text - property var short_display_value: short_value ? short_value : display_value.slice(0,3) + "..." + property var short_display_value: display_value ? display_value.slice(0,3) + "..." : "" property bool fixed_width_font: false property var min_pad: 2 property bool collapse: false property bool inactive : attr_enabled != undefined ? !attr_enabled : false property var custom_message_: custom_message != undefined ? custom_message : undefined property bool hovered: false + property var actual_text: collapse_mode <= 1 ? display_value : short_display_value property var showHighlighted: hovered | (activated != undefined && activated) @@ -105,13 +106,13 @@ Rectangle { TextMetrics { id: full_value_metrics font: value_widget.font - text: control.display_value + text: control.display_value ? control.display_value : "" } TextMetrics { id: short_value_metrics font: value_widget.font - text: control.short_display_value + text: control.short_display_value ? control.short_display_value : "" } Rectangle { @@ -144,7 +145,7 @@ Rectangle { id: value_widget - text: collapse_mode <= 1 ? display_value : short_display_value + text: actual_text ? actual_text : "" opacity: collapse_mode != 3 visible: opacity > 0.2 diff --git a/ui/qml/xstudio/core/XsGlobalPreferences.qml b/ui/qml/xstudio/core/XsGlobalPreferences.qml index 6075bd2d2..99466990b 100644 --- a/ui/qml/xstudio/core/XsGlobalPreferences.qml +++ b/ui/qml/xstudio/core/XsGlobalPreferences.qml @@ -35,6 +35,8 @@ Item property alias display: display property alias python_history: python_history property alias recent_history: recent_history + property alias session_compression: session_compression + property alias quickview_all_incoming_media: quickview_all_incoming_media property alias session_link_prefix: session_link_prefix property alias click_to_toggle_play: click_to_toggle_play // property alias panel_geoms: panel_geoms @@ -48,6 +50,7 @@ Item property alias viewport_scrub_sensitivity: viewport_scrub_sensitivity property alias default_playhead_compare_mode: default_playhead_compare_mode property alias default_media_folder: default_media_folder + property alias snapshot_paths: snapshot_paths property color accent_color: '#bb7700' @@ -57,6 +60,12 @@ Item index: app_window.globalStoreModel.search_recursive("/core/bookmark/note_category", "pathRole") } + XsModelProperty { + id: snapshot_paths + role: "valueRole" + index: app_window.globalStoreModel.search_recursive("/core/snapshot/paths", "pathRole") + } + XsModelProperty { id: note_depth role: "valueRole" @@ -99,6 +108,18 @@ Item index: app_window.globalStoreModel.search_recursive("/core/session/session_link_prefix", "pathRole") } + XsModelProperty { + id: session_compression + role: "valueRole" + index: app_window.globalStoreModel.search_recursive("/core/session/compression", "pathRole") + } + + XsModelProperty { + id: quickview_all_incoming_media + role: "valueRole" + index: app_window.globalStoreModel.search_recursive("/core/session/quickview_all_incoming_media", "pathRole") + } + XsModelProperty { id: xplayer_window role: "valueRole" diff --git a/ui/qml/xstudio/cursors/move-edge-left.svg b/ui/qml/xstudio/cursors/move-edge-left.svg new file mode 100644 index 000000000..e7d3e0512 --- /dev/null +++ b/ui/qml/xstudio/cursors/move-edge-left.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ui/qml/xstudio/cursors/move-edge-right.svg b/ui/qml/xstudio/cursors/move-edge-right.svg new file mode 100644 index 000000000..c9002c903 --- /dev/null +++ b/ui/qml/xstudio/cursors/move-edge-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/cursors/move-join.svg b/ui/qml/xstudio/cursors/move-join.svg new file mode 100644 index 000000000..4d20077ef --- /dev/null +++ b/ui/qml/xstudio/cursors/move-join.svg @@ -0,0 +1,96 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/qml/xstudio/dialogs/XsImportSessionDialog.qml b/ui/qml/xstudio/dialogs/XsImportSessionDialog.qml index 3cdca1219..9a362e3fc 100644 --- a/ui/qml/xstudio/dialogs/XsImportSessionDialog.qml +++ b/ui/qml/xstudio/dialogs/XsImportSessionDialog.qml @@ -11,7 +11,7 @@ FileDialog { folder: app_window.sessionFunction.defaultSessionFolder() || shortcuts.home defaultSuffix: "xst" - nameFilters: ["Xstudio (*.xst)"] + nameFilters: ["xStudio (*.xst *.xsz)"] selectExisting: true selectMultiple: false onAccepted: { diff --git a/ui/qml/xstudio/dialogs/XsMediaMoveCopyDialog.qml b/ui/qml/xstudio/dialogs/XsMediaMoveCopyDialog.qml new file mode 100644 index 000000000..aa0576b4d --- /dev/null +++ b/ui/qml/xstudio/dialogs/XsMediaMoveCopyDialog.qml @@ -0,0 +1,46 @@ +// 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 QtQuick.Dialogs 1.0 + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.1 + + +XsButtonDialog { + text: "Selected Media" + width: 300 + buttonModel: ["Cancel", "Move", "Copy"] + property var data: null + property var index: null + + onSelected: { + if(button_index == 1) { + // is selection still valid ? + let items = XsUtils.cloneArray(app_window.mediaSelectionModel.selectedIndexes).sort((a,b) => b.row - a.row ) + app_window.sessionFunction.setActiveMedia(app_window.sessionFunction.mediaIndexAfterRemoved(items)) + if(index == null) + Future.promise( + app_window.sessionModel.handleDropFuture(Qt.MoveAction, data) + ).then(function(quuids){}) + else + Future.promise( + app_window.sessionModel.handleDropFuture(Qt.MoveAction, data, index) + ).then(function(quuids){}) + + } else if(button_index == 2) { + if(index == null) + Future.promise( + app_window.sessionModel.handleDropFuture(Qt.CopyAction, data) + ).then(function(quuids){}) + else + Future.promise( + app_window.sessionModel.handleDropFuture(Qt.CopyAction, data, index) + ).then(function(quuids){}) + } + } +} diff --git a/ui/qml/xstudio/dialogs/XsNewSnapshotDialog.qml b/ui/qml/xstudio/dialogs/XsNewSnapshotDialog.qml new file mode 100644 index 000000000..edcd0f59e --- /dev/null +++ b/ui/qml/xstudio/dialogs/XsNewSnapshotDialog.qml @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.3 +import QtQuick.Dialogs 1.3 + +import xStudio 1.1 + +XsDialog { + id: control + title: "Select Folder" + minimumHeight: 130 + minimumWidth: 300 + + keepCentered: true + centerOnOpen: true + + property alias okay_text: okay.text + property alias cancel_text: cancel.text + property alias text: text_control.text + property alias path: path_control.text + property alias input: text_control + + signal cancelled() + signal okayed() + + + function okaying() { + okayed() + accept() + } + function cancelling() { + cancelled() + reject() + } + + FileDialog { + id: select_path_dialog + title: "Select Snapshot Path" + folder: path_control.text || app_window.sessionFunction.defaultSessionFolder() || shortcuts.home + + selectFolder: true + selectExisting: true + selectMultiple: false + + onAccepted: { + path_control.text = select_path_dialog.fileUrls[0] + } + } + + Connections { + target: control + function onVisibleChanged() { + if(visible){ + text_control.selectAll() + text_control.forceActiveFocus() + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + TextField { + id: path_control + text: "" + placeholderText: "Select Snapshot Folder..." + + 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 + } + } + XsRoundButton { + id: browse + text: "Browse..." + + Layout.fillHeight: true + Layout.minimumWidth: control.width / 5 + + onClicked: select_path_dialog.open() + } + } + + TextField { + id: text_control + placeholderText: "Set Menu Title" + 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: "Cancel" + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: control.width / 5 + + onClicked: { + cancelling() + } + } + XsHSpacer{} + XsRoundButton { + id: okay + text: "Done" + highlighted: true + + Layout.minimumWidth: control.width / 5 + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: { + forceActiveFocus() + if(text_control.text == "") + text_control.text = path_control.text.split('/').pop() + okaying() + } + } + } + } +} \ No newline at end of file diff --git a/ui/qml/xstudio/dialogs/XsNotesDialog.qml b/ui/qml/xstudio/dialogs/XsNotesDialog.qml index 31fc1b94c..46e83f263 100644 --- a/ui/qml/xstudio/dialogs/XsNotesDialog.qml +++ b/ui/qml/xstudio/dialogs/XsNotesDialog.qml @@ -716,7 +716,7 @@ XsWindow { Layout.topMargin: 4 Layout.bottomMargin: 4 Layout.preferredHeight: note.height - model: ["Media", "Media List", "Playlist"] + model: ["Media", "Media List", "Playlist", "Session"] currentIndex: preferences.note_depth.value onCurrentIndexChanged: preferences.note_depth.value = currentIndex } diff --git a/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml b/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml index 93613d15f..b8c6f3a95 100644 --- a/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml +++ b/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml @@ -11,7 +11,7 @@ FileDialog { folder: app_window.sessionFunction.defaultSessionFolder() || shortcuts.home defaultSuffix: "xst" - nameFilters: ["Xstudio (*.xst)"] + nameFilters: ["xStudio (*.xst *.xsz)"] selectExisting: true selectMultiple: false onAccepted: { @@ -20,7 +20,8 @@ FileDialog { // console.log(result) } ) - app_window.sessionFunction.newRecentPath(fileUrl) + var path = fileUrl + app_window.sessionFunction.newRecentPath(path) app_window.sessionFunction.defaultSessionFolder(path.slice(0, path.lastIndexOf("/") + 1)) } onRejected: { diff --git a/ui/qml/xstudio/dialogs/XsSaveSelectedSessionDialog.qml b/ui/qml/xstudio/dialogs/XsSaveSelectedSessionDialog.qml index be5af3b6f..26b263fe6 100644 --- a/ui/qml/xstudio/dialogs/XsSaveSelectedSessionDialog.qml +++ b/ui/qml/xstudio/dialogs/XsSaveSelectedSessionDialog.qml @@ -8,12 +8,12 @@ import xStudio 1.0 FileDialog { title: "Save selected as session" folder: app_window.sessionFunction.defaultSessionFolder() || shortcuts.home - defaultSuffix: "xst" + defaultSuffix: preferences.session_compression.value ? "xsz" : "xst" signal saved signal cancelled - nameFilters: ["XStudio (*.xst)"] + nameFilters: ["xStudio (*.xst *.xsz)"] selectExisting: false selectMultiple: false @@ -22,7 +22,7 @@ FileDialog { var path = fileUrl.toString() var ext = path.split('.').pop() if(path == ext) { - path = path + ".xst" + path = path + (preferences.session_compression.value ? ".xsz" : ".xst") } app_window.sessionFunction.saveSelectedSession(path).then(function(result){ if (result != "") { diff --git a/ui/qml/xstudio/dialogs/XsSaveSessionDialog.qml b/ui/qml/xstudio/dialogs/XsSaveSessionDialog.qml index bc1a3e6c2..c37317f3b 100644 --- a/ui/qml/xstudio/dialogs/XsSaveSessionDialog.qml +++ b/ui/qml/xstudio/dialogs/XsSaveSessionDialog.qml @@ -8,12 +8,12 @@ import xStudio 1.0 FileDialog { title: "Save session" folder: app_window.sessionFunction.defaultSessionFolder() || shortcuts.home - defaultSuffix: "xst" + defaultSuffix: preferences.session_compression.value ? "xsz" : "xst" signal saved signal cancelled - nameFilters: ["XStudio (*.xst)"] + nameFilters: ["xStudio (*.xst *.xsz)"] selectExisting: false selectMultiple: false @@ -22,7 +22,7 @@ FileDialog { var path = fileUrl.toString() var ext = path.split('.').pop() if(path == ext) { - path = path + ".xst" + path = path + (preferences.session_compression.value ? ".xsz" : ".xst") } app_window.sessionFunction.newRecentPath(path) diff --git a/ui/qml/xstudio/dialogs/XsSettingsDialog.qml b/ui/qml/xstudio/dialogs/XsSettingsDialog.qml index c9a72b9d9..bacab40fc 100644 --- a/ui/qml/xstudio/dialogs/XsSettingsDialog.qml +++ b/ui/qml/xstudio/dialogs/XsSettingsDialog.qml @@ -438,7 +438,6 @@ XsDialogModal { } } - XsLabel { text: "Image Cache (MB)" Layout.alignment: Qt.AlignVCenter | Qt.AlignRight @@ -579,6 +578,18 @@ XsDialogModal { } } } + + XsLabel { + text: "Launch QuickView window for all incoming media" + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + XsCheckboxOld { + checked: preferences.quickview_all_incoming_media.value + onTriggered: { + preferences.quickview_all_incoming_media.value = !preferences.quickview_all_incoming_media.value + } + } + } DialogButtonBox { diff --git a/ui/qml/xstudio/dialogs/XsSnapshotDialog.qml b/ui/qml/xstudio/dialogs/XsSnapshotDialog.qml index 42b34811c..d07fe29cb 100644 --- a/ui/qml/xstudio/dialogs/XsSnapshotDialog.qml +++ b/ui/qml/xstudio/dialogs/XsSnapshotDialog.qml @@ -79,50 +79,29 @@ XsDialog { Layout.fillHeight: true } - XsButtonDialog { - id: resultDialog - // parent: sessionWidget - width: text.width + 20 - title: "Snapshot export fail" - text: { - return "The snapshot could not be exported. Please check the parameters" - } - buttonModel: ["Ok"] - onSelected: { - resultDialog.close() - } - } - - FileDialog { - id: filedialog - title: qsTr("Name / select a file to save") - selectMultiple: false - selectFolder: false - selectExisting: false - nameFilters: [ "JPEG files (*.jpg)", "PNG files (*.png)", "TIF files (*.tif *.tiff)", "EXR files (*.exr)" ] - property var suffixes: ["jpg", "png", "tif", "exr"] - property var formatIdx: formatBox.currentIndex - defaultSuffix: suffixes[formatIdx] - selectedNameFilter: nameFilters[formatIdx] - onAccepted: { - var fixedfileUrl = fileUrl.toString().includes("." + suffixes[formatIdx]) ? fileUrl : (fileUrl + "." + suffixes[formatIdx]) - var ret = playerWidget.viewport.renderImageToFile( - fixedfileUrl, - formatIdx, - slider.value, - widthInput.text, - heightInput.text, - bakeColorBox.checked) - if (ret != "") { - resultDialog.title = "Snapshot export failed" - resultDialog.text = ret - resultDialog.open() - } else { - dlg.close() - } - - } + FileDialog { + id: filedialog + title: qsTr("Name / select a file to save") + selectMultiple: false + selectFolder: false + selectExisting: false + nameFilters: [ "JPEG files (*.jpg)", "PNG files (*.png)", "TIF files (*.tif *.tiff)", "EXR files (*.exr)" ] + property var suffixes: ["jpg", "png", "tif", "exr"] + property var formatIdx: formatBox.currentIndex + defaultSuffix: suffixes[formatIdx] + selectedNameFilter: nameFilters[formatIdx] + onAccepted: { + var fixedfileUrl = fileUrl.toString().includes("." + suffixes[formatIdx]) ? fileUrl : (fileUrl + "." + suffixes[formatIdx]) + playerWidget.viewport.renderImageToFile( + fixedfileUrl, + formatIdx, + slider.value, + widthInput.text, + heightInput.text, + bakeColorBox.checked) + dlg.close() } + } Rectangle { color: "transparent" diff --git a/ui/qml/xstudio/extern/QuickPromise b/ui/qml/xstudio/extern/QuickPromise deleted file mode 120000 index 9352e9364..000000000 --- a/ui/qml/xstudio/extern/QuickPromise +++ /dev/null @@ -1 +0,0 @@ -../../../../extern/quickpromise/qml/QuickPromise \ No newline at end of file diff --git a/ui/qml/xstudio/main.qml b/ui/qml/xstudio/main.qml index b4b4da778..60cca4621 100644 --- a/ui/qml/xstudio/main.qml +++ b/ui/qml/xstudio/main.qml @@ -62,6 +62,9 @@ ApplicationWindow { palette.buttonText: XsStyle.hoverColor palette.windowText: XsStyle.hoverColor + // so visible clients can action requests. + signal flagSelectedItems(string flag) + // palette.alternateBase: "Red" // palette.dark: "Red" // palette.link: "Red" @@ -76,8 +79,6 @@ ApplicationWindow { property var preFullScreenVis: [app_window.x, app_window.y, app_window.width, app_window.height] property var qmlWindowRef: Window // so javascript can reference Window enums. - property var popout_window: undefined - FontLoader {source: "qrc:/fonts/Overpass/Overpass-Regular.ttf"} FontLoader {source: "qrc:/fonts/Overpass/Overpass-Black.ttf"} FontLoader {source: "qrc:/fonts/Overpass/Overpass-BlackItalic.ttf"} @@ -257,22 +258,66 @@ ApplicationWindow { } } - function togglePopoutViewer() { - if (popout_window) { - popout_window.toggle_visible() - return - } + function launchQuickViewerWithSize(sources, compare_mode, __position, __size) { - var component = Qt.createComponent("player/XsPlayerWindow.qml"); + var component = Qt.createComponent("player/XsLightPlayerWindow.qml"); if (component.status == Component.Ready) { - popout_window = component.createObject(app_window, {x: 100, y: 100, mediaImageSource: mediaImageSource}); - popout_window.show() + if (compare_mode == "Off" || compare_mode == "") { + for (var source in sources) { + var light_viewer = component.createObject(app_window, {x: __position.x, y: __position.y, width: __size.width, height: __size.height, sessionModel: sessionModel}); + light_viewer.show() + light_viewer.viewport.quickViewSource([sources[source]], "Off") + light_viewer.raise() + light_viewer.requestActivate() + light_viewer.raise() + } + } else { + var light_viewer = component.createObject(app_window, {x: __position.x, y: __position.y, width: __size.width, height: __size.height, sessionModel: sessionModel}); + light_viewer.show() + light_viewer.viewport.quickViewSource(sources, compare_mode) + light_viewer.raise() + light_viewer.requestActivate() + light_viewer.raise() + } } else { // Error Handling console.log("Error loading component:", component.errorString()); } } + // QuickView window position management + property var quickWinPosition: Qt.point(100, 100) + property var quickWinSize: Qt.size(1280,720) + property bool quickWinPosSet: false + + function closingQuickviewWindow(position, size) { + // when a QuickView window is closed, remember its size and position and + // re-use for next QuickView window + quickWinPosition = position + quickWinSize = size + quickWinPosSet = true + } + + function launchQuickViewer(sources, compare_mode) { + launchQuickViewerWithSize(sources, compare_mode, quickWinPosition, quickWinSize) + if (quickWinPosSet) { + // rest the default position for the next QuickView window + quickWinPosition = Qt.point(100, 100) + quickWinPosSet = false + } else { + // each new window will be positioned 100 pixels to the bottom and + // right of the previous one + quickWinPosition = Qt.point(quickWinPosition.x + 100, quickWinPosition.y + 100) + } + } + + width: 1280 + height: 820 + + function togglePopoutViewer() { + popout_window.toggle_visible() + } + function toggleFullscreen() { if (visibility !== Window.FullScreen) { preFullScreenVis = [x, y, width, height] @@ -292,11 +337,6 @@ ApplicationWindow { } } - function fitWindowToImage() { - // doesn't apply to session window - spawnNewViewer() - } - XsButtonDialog { id: overwriteDialog // parent: sessionWidget @@ -476,6 +516,7 @@ ApplicationWindow { onIndexChanged: { // console.log("*****************************mediaImageSource, onIndexChanged", index) + screenMedia.index = index.parent if(index.valid) { // we need these populated first.. if(index.model.rowCount(index)) { @@ -550,9 +591,16 @@ ApplicationWindow { sessionModel.setPlayheadTo(index) } } - XsTimer { - id: m_timer - } + + property alias screenMedia: screenMedia + XsModelPropertyMap { + id: screenMedia + index: mediaImageSource.index + } + + XsTimer { + id: m_timer + } // manages media selection, for current source. property alias mediaSelectionModel: mediaSelectionModel @@ -579,7 +627,6 @@ ApplicationWindow { m_timer.setTimeout(function(index) { return function() { let model = index.model let mind = model.search_recursive(model.get(index, "imageActorUuidRole"), "actorUuidRole", index) - console.log(mind) if(mind.valid && mediaImageSource.index != mind) { mediaImageSource.index = mind } @@ -676,6 +723,30 @@ ApplicationWindow { property var index: null property string type: "" + XsTimer { + id: timelineReady + } + + function addTracks(timeline_index) { + delayTimer.setTimeout(function() { + let model = timeline_index.model; + + let timelineItemIndex = model.index(2,0,timeline_index) + if(timelineItemIndex.valid) { + let stackIndex = model.index(0,0,timelineItemIndex) + if(stackIndex.valid) { + app_window.sessionModel.insertRowsSync(0, 1, "Audio Track", "Audio Track", stackIndex) + app_window.sessionModel.insertRowsSync(0, 1, "Video Track", "Video Track", stackIndex) + } else { + addTracks(timeline_index) + } + } else { + addTracks(timeline_index) + } + }, 100) + + } + onOkayed: { let new_indexes = index.model.insertRowsSync( index.model.rowCount(index), @@ -683,6 +754,11 @@ ApplicationWindow { type, text, index ) + if(type == "Timeline") { + index.model.index(2, 0, new_indexes[0]) + addTracks(new_indexes[0]) + } + app_window.sessionExpandedModel.select(index.parent, ItemSelectionModel.Select) } } @@ -1032,7 +1108,7 @@ ApplicationWindow { remove_selected.open() } - function newPlaylist(index, text=null) { + function newPlaylist(index, text=null, centeron=null) { if(index != null) { request_new.text = "Untitled Playlist" request_new.okay_text = "Add Playlist" @@ -1128,10 +1204,12 @@ ApplicationWindow { } } - function flagSelected(flag) { + function flagSelected(flag, flag_text="") { let sindexs = sessionSelectionModel.selectedIndexes for(let i = 0; i< sindexs.length; i++) { - sessionModel.set(sindexs[i], flag, "flagRole") + sessionModel.set(sindexs[i], flag, "flagColourRole") + if(flag_text) + sessionModel.set(sindexs[i], flag_text, "flagTextRole") } } @@ -1211,6 +1289,7 @@ ApplicationWindow { function newSession() { studio.newSession("New Session") + studio.clearImageCache() } function mediaIndexAfterRemoved(indexes) { @@ -1372,10 +1451,12 @@ ApplicationWindow { } - function flagSelectedMedia(flag) { + function flagSelectedMedia(flag, flag_text="") { let sindexs = mediaSelectionModel.selectedIndexes for(let i = 0; i< sindexs.length; i++) { - sessionModel.set(sindexs[i], flag, "flagRole") + sessionModel.set(sindexs[i], flag, "flagColourRole") + if(flag_text) + sessionModel.set(sindexs[i], flag_text, "flagTextRole") } } @@ -1441,6 +1522,10 @@ ApplicationWindow { } + function conformInsertSelectedMedia(item) { + sessionModel.conformInsert(item, mediaSelectionModel.selectedIndexes) + } + function duplicateSelectedMedia() { var media = XsUtils.cloneArray(mediaSelectionModel.selectedIndexes) media.forEach( @@ -1530,6 +1615,9 @@ ApplicationWindow { // session.sessionActorAddr = session_addr // } + function onOpenQuickViewers(media_actors, compare_mode) { + launchQuickViewer(media_actors, compare_mode) + } function onSessionRequest(path, jsn) { // console.log("onSessionRequest") @@ -1538,6 +1626,40 @@ ApplicationWindow { dialog.payload = jsn dialog.show() } + + function onShowMessageBox(title, body, closeButton, timeoutSecs) { + messageBox.title = title + messageBox.text = body + messageBox.buttonModel = closeButton ? ["Close"] : [] + messageBox.hideTimer(timeoutSecs) + messageBox.show() + } + } + + XsButtonDialog { + id: messageBox + // parent: sessionWidget + width: 400 + onSelected: { + if(button_index == 0) { + hide() + } + } + + Timer { + id: hide_timer + repeat: false + interval: 500 + onTriggered: messageBox.hide() + } + + function hideTimer(seconds) { + if (seconds != 0) { + hide_timer.interval = seconds*1000 + hide_timer.start() + } + } + } // Session { @@ -1587,5 +1709,15 @@ ApplicationWindow { snapshotDialog.open() } + XsPlayerWindow { + id: popout_window + visible: false + mediaImageSource: app_window.mediaImageSource + Component.onCompleted: { + popout_window.viewport.linkToViewport(app_window.viewport) + } + } + property alias popout_window: popout_window + } diff --git a/ui/qml/xstudio/menus/XsMediaMenu.qml b/ui/qml/xstudio/menus/XsMediaMenu.qml index 7d024a774..6047557f2 100644 --- a/ui/qml/xstudio/menus/XsMediaMenu.qml +++ b/ui/qml/xstudio/menus/XsMediaMenu.qml @@ -17,7 +17,7 @@ XsMenu { XsFlagMenu { title: qsTr("Flag Media") showChecked: false - onFlagSet: app_window.sessionFunction.flagSelectedMedia(hex) + onFlagSet: app_window.sessionFunction.flagSelectedMedia(hex, text) } XsMenu { @@ -53,7 +53,6 @@ XsMenu { XsMenuItem { mytext: qsTr("Select All") shortcut: "Ctrl+A" - onTriggered: app_window.sessionFunction.selectAllMedia() } @@ -107,6 +106,27 @@ XsMenu { } } + // XsMenuSeparator {} + // XsMenu { + + // id: conform_menu + // title: "Conform" + // fakeDisabled: false + // Repeater { + // model: app_window.sessionModel.conformTasks + // onItemAdded: conform_menu.insertItem(index, item) + // onItemRemoved: conform_menu.removeItem(item) + + // XsMenuItem { + // mytext: modelData + // enabled: true + // onTriggered: { + // app_window.sessionFunction.conformInsertSelectedMedia(modelData) + // } + // } + // } + // } + XsMenuSeparator {} XsMenu { @@ -199,7 +219,22 @@ XsMenu { parent_menu: menu root_menu_name: "Plugins" } - XsMenuSeparator {} + + XsMenuSeparator { + id: after_plugins_separator + visible: !extras_menu.empty + height: visible ? implicitHeight : 0 + } + + XsModuleMenuBuilder { + id: extras_menu + parent_menu: menu + root_menu_name: "media_menu_extras" + insert_after: after_plugins_separator + } + + XsMenuSeparator { + } // XsButtonDialog { // id: removeMedia diff --git a/ui/qml/xstudio/menus/XsPanelMenu.qml b/ui/qml/xstudio/menus/XsPanelMenu.qml index be4cc4038..4b100d69a 100644 --- a/ui/qml/xstudio/menus/XsPanelMenu.qml +++ b/ui/qml/xstudio/menus/XsPanelMenu.qml @@ -33,7 +33,7 @@ XsMenu { // connect to the backend module to give access to attributes XsModuleAttributes { id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings" + attributesGroupNames: "annotations_tool_settings_0" } // XsMenuSeparator { } diff --git a/ui/qml/xstudio/menus/XsPlaylistMenu.qml b/ui/qml/xstudio/menus/XsPlaylistMenu.qml index 523ea150f..621261529 100644 --- a/ui/qml/xstudio/menus/XsPlaylistMenu.qml +++ b/ui/qml/xstudio/menus/XsPlaylistMenu.qml @@ -14,14 +14,14 @@ XsMenu { title: "New" XsMenuItem { mytext: qsTr("&Playlist") - shortcut: "Shift+P" + shortcut: "Ctrl+Shift+P" onTriggered: sessionFunction.newPlaylist( app_window.sessionModel.index(0, 0), null ) } XsMenuItem {mytext: qsTr("Session &Divider") - shortcut: "Shift+D" + shortcut: "Ctrl+Shift+D" onTriggered: sessionFunction.newDivider( app_window.sessionModel.index(0, 0), null, playlist_panel ) @@ -35,7 +35,7 @@ XsMenu { } XsMenuItem {mytext: qsTr("&Subset") - shortcut: "Shift+S" + shortcut: "Ctrl+Shift+S" onTriggered: { let ind = app_window.sessionFunction.firstSelected("Playlist") if(ind != null) { @@ -47,17 +47,17 @@ XsMenu { } XsMenuItem {mytext: qsTr("&Timeline") - shortcut: "Shift+T" + shortcut: "Ctrl+Shift+T" enabled: false } XsMenuItem {mytext: qsTr("&Contact Sheet") - shortcut: "Shift+C" + shortcut: "Ctrl+Shift+C" enabled: false } XsMenuItem {mytext: qsTr("D&ivider") - shortcut: "Shift+i" + shortcut: "Ctrl+Shift+I" onTriggered: { let ind = app_window.sessionFunction.firstSelected("Playlist") if(ind != null) { diff --git a/ui/qml/xstudio/menus/XsSnapshotDirectoryMenu.qml b/ui/qml/xstudio/menus/XsSnapshotDirectoryMenu.qml new file mode 100644 index 000000000..0515760ca --- /dev/null +++ b/ui/qml/xstudio/menus/XsSnapshotDirectoryMenu.qml @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQml 2.14 +import QtQml.Models 2.14 +import Qt.labs.qmlmodels 1.0 +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +XsMenu { + id: control + + property var snapshotModel: null + property var rootIndex: null + property string itemType: typeRole + title: nameRole + + function createSelf(parent, myModel, myRootIndex, myType, myTitle) { + let comp = Qt.createComponent("XsSnapshotDirectoryMenu.qml").createObject(parent, { + snapshotModel: myModel, rootIndex: myRootIndex, itemType:myType, + title: myTitle + }) + + if (comp == null) { + console.log("Error creating object"); + } + return comp; + } + + XsStringRequestDialog { + id: add_folder + okay_text: "Add" + title: "Add Folder" + + onOkayed: snapshotModel.createFolder(rootIndex, text) + } + + XsStringRequestDialog { + id: save_snapshot + okay_text: "Save" + title: "Save Snapshot" + + onOkayed: { + let path = snapshotModel.buildSavePath(rootIndex, text) + app_window.sessionFunction.newRecentPath(path) + app_window.sessionFunction.saveSessionPath(path).then(function(result){ + if (result != "") { + var dialog = XsUtils.openDialog("qrc:/dialogs/XsErrorMessage.qml") + dialog.title = "Save session failed" + dialog.text = result + dialog.show() + } else { + app_window.sessionFunction.newRecentPath(path) + app_window.sessionFunction.copySessionLink(false) + snapshotModel.rescan(rootIndex, 0); + } + }) + } + } + + DelegateChooser { + id: chooser + role: "typeRole" + + DelegateChoice { + roleValue: "DIRECTORY" + + Item { + id: menu_holder + property string itemType: typeRole + property var item: null + + Component.onCompleted: { + item = createSelf(menu_holder, control.snapshotModel, control.snapshotModel.index(index, 0, control.rootIndex), typeRole, nameRole) + } + } + } + + DelegateChoice { + roleValue: "FILE" + + XsMenuItem { + property string itemType: typeRole + mytext: nameRole + onTriggered: Future.promise(studio.loadSessionRequestFuture(pathRole)).then(function(result){}) + } + } + } + + DelegateModel { + id: snapshot_items + property var srcModel: control.snapshotModel + model: srcModel + rootIndex: control.rootIndex + delegate: chooser + } + + onAboutToShow: { + control.snapshotModel.rescan(control.rootIndex,1) + if(builder.model != null) + builder.model = snapshot_items + } + + Instantiator { + id: builder + model: [] + onObjectAdded: { + if(object.itemType == "DIRECTORY") + control.insertMenu(index, object.item) + else + control.insertItem(index, object) + } + onObjectRemoved: { + if(object.itemType == "DIRECTORY") + control.removeMenu(object.item) + else + control.removeItem(object) + } + } + + XsMenuSeparator { + visible: true + } + + XsMenuItem { + mytext: "Add Folder..." + onTriggered: add_folder.open() + } + XsMenuItem { + mytext: "Save Snapshot..." + onTriggered: save_snapshot.open() + } + XsMenuItem { + mytext: "Remove "+control.snapshotModel.get(rootIndex, "nameRole")+" Menu" + visible: !rootIndex.parent.valid + onTriggered: { + let v = preferences.snapshot_paths.value + let new_v = [] + let ppath = control.snapshotModel.get(rootIndex, "pathRole") + + for(let i =0; i< v.length;i++) { + if(ppath != v[i].path) + new_v.push(v[i]) + } + + preferences.snapshot_paths.value = new_v + } + } +} + diff --git a/ui/qml/xstudio/menus/XsSnapshotMenu.qml b/ui/qml/xstudio/menus/XsSnapshotMenu.qml new file mode 100644 index 000000000..6cffbc0ea --- /dev/null +++ b/ui/qml/xstudio/menus/XsSnapshotMenu.qml @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQml 2.14 +import QtQml.Models 2.14 +import Qt.labs.qmlmodels 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + + +XsMenu { + id: snapshotMenu + title: "Snapshots" + + XsSnapshotModel { + id: snapshotModel + paths: preferences.snapshot_paths.value + onModelReset: snapshot_items.rootIndex = index(-1,-1) + } + + XsNewSnapshotDialog { + id: snapshot_path_dialog + onOkayed: { + if(text.length && path.length) { + let v = preferences.snapshot_paths.value + v.push({'path': path, "name":text}) + preferences.snapshot_paths.value = v + + text = "" + path = "" + } + } + } + + onAboutToShow: snapshotModel.rescan(snapshot_items.rootIndex, 1) + + DelegateChooser { + id: chooser + role: "typeRole" + + DelegateChoice { + roleValue: "DIRECTORY" + + XsSnapshotDirectoryMenu { + itemType: typeRole + title: nameRole + rootIndex: snapshot_items.srcModel.index(index, 0, snapshot_items.rootIndex) + snapshotModel: snapshot_items.srcModel + } + } + + DelegateChoice { + roleValue: "FILE" + + XsMenuItem { + property string itemType: typeRole + mytext: nameRole + } + } + } + + DelegateModel { + id: snapshot_items + property var srcModel: snapshotModel + model: srcModel + rootIndex: null + delegate: chooser + } + + Instantiator { + model: snapshot_items + onObjectAdded: { + if(object.itemType == "DIRECTORY") + snapshotMenu.insertMenu(index,object) + else + snapshotMenu.insertItem(index,object) + } + onObjectRemoved: { + if(object.itemType == "DIRECTORY") + snapshotMenu.removeMenu(object) + else + snapshotMenu.removeItem(object) + } + } + + + XsMenuSeparator { + visible: true + } + + XsMenuItem { + mytext: "Select Folder..." + onTriggered: snapshot_path_dialog.open() + } +} \ No newline at end of file diff --git a/ui/qml/xstudio/menus/XsTimelineMenu.qml b/ui/qml/xstudio/menus/XsTimelineMenu.qml index 69d835488..484523027 100644 --- a/ui/qml/xstudio/menus/XsTimelineMenu.qml +++ b/ui/qml/xstudio/menus/XsTimelineMenu.qml @@ -23,6 +23,12 @@ XsMenu { mytext: qsTr("Focus Mode") enabled: false } + + XsFlagMenu { + showChecked: false + onFlagSet: app_window.flagSelectedItems(hex) + } + XsMenu { title: "Tracks" fakeDisabled: true diff --git a/ui/qml/xstudio/menus/XsViewerContextMenu.qml b/ui/qml/xstudio/menus/XsViewerContextMenu.qml index 1d45a14dd..89213223c 100644 --- a/ui/qml/xstudio/menus/XsViewerContextMenu.qml +++ b/ui/qml/xstudio/menus/XsViewerContextMenu.qml @@ -9,7 +9,6 @@ XsMenu { title: qsTr("Viewer") id: viewer_context_menu - property bool is_popout_viewport: false XsMenuItem { mytext: qsTr("Presentation Mode") diff --git a/ui/qml/xstudio/panels/media_list/XsMediaPanelListView.qml b/ui/qml/xstudio/panels/media_list/XsMediaPanelListView.qml index 843288518..831d58220 100644 --- a/ui/qml/xstudio/panels/media_list/XsMediaPanelListView.qml +++ b/ui/qml/xstudio/panels/media_list/XsMediaPanelListView.qml @@ -113,7 +113,7 @@ Rectangle { moveTimer.stop() if(drop.hasUrls) { for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { + if(drop.urls[i].toLowerCase().endsWith('.xst') || drop.urls[i].toLowerCase().endsWith('.xsz')) { Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) app_window.sessionFunction.newRecentPath(drop.urls[i]) return; diff --git a/ui/qml/xstudio/panels/media_list/delegates/XsDelegateMedia.qml b/ui/qml/xstudio/panels/media_list/delegates/XsDelegateMedia.qml index 1822388e1..acb0c814f 100644 --- a/ui/qml/xstudio/panels/media_list/delegates/XsDelegateMedia.qml +++ b/ui/qml/xstudio/panels/media_list/delegates/XsDelegateMedia.qml @@ -40,7 +40,7 @@ DelegateChoice { property bool insertionFlag: false - property var model_index: control.DelegateModel.model.srcModel.index(-1,-1) + property var media_item_model_index: control.DelegateModel.model.srcModel.index(-1,-1) property var image_source_model_index: control.DelegateModel.model.srcModel.index(-1,-1) // may not be required.. @@ -54,7 +54,7 @@ DelegateChoice { property bool copying: false Component.onCompleted: { - control.DelegateModel.model.srcModel.get(modelIndex(), "childrenRole") + control.DelegateModel.model.srcModel.fetchMore(modelIndex()) control.updateProperties() control.updateSelected() } @@ -77,7 +77,7 @@ DelegateChoice { } function modelIndex() { - return model_index + return media_item_model_index } function imageSouceIndex() { @@ -131,13 +131,13 @@ DelegateChoice { // console.log(control.DelegateModel.model.rootIndex) if(control.DelegateModel.model.srcModel) { - control.model_index = helpers.makePersistent(control.DelegateModel.model.srcModel.index( + control.media_item_model_index = helpers.makePersistent(control.DelegateModel.model.srcModel.index( index, 0, control.DelegateModel.model.rootIndex )) - if(control.model_index.valid) { - control.image_source_model_index = helpers.makePersistent(control.model_index.model.search_recursive( - imageActorUuidRole, "actorUuidRole", control.model_index + if(control.media_item_model_index.valid) { + control.image_source_model_index = helpers.makePersistent(control.media_item_model_index.model.search_recursive( + imageActorUuidRole, "actorUuidRole", control.media_item_model_index )) if(control.image_source_model_index.valid) { @@ -182,7 +182,10 @@ DelegateChoice { app_window.mediaSelectionModel.select(helpers.createItemSelection(indexs), ItemSelectionModel.Select) } else if(mouse.modifiers & Qt.ControlModifier) { app_window.mediaSelectionModel.select(modelIndex(), ItemSelectionModel.Toggle) - } else if(mouse.modifiers == Qt.NoModifier) { + } else if(mouse.modifiers & Qt.AltModifier) { + // alt + click will launch a 'quick viewer' + app_window.launchQuickViewer([actorRole], "Off") + } else if(mouse.modifiers == Qt.NoModifier) { if(currentSource.index == screenSource.index){ if(selection_index == -1) { app_window.sessionFunction.setActiveMedia(modelIndex(), true) @@ -345,7 +348,7 @@ DelegateChoice { Rectangle { - color: flagRole != undefined ? flagRole : "#00000000" + color: flagColourRole != undefined ? flagColourRole : "#00000000" width:3 anchors.left: parent.left anchors.top: parent.top @@ -536,18 +539,33 @@ DelegateChoice { color: XsStyle.highlightColor anchors.top: thumb.top anchors.right: thumb.right - // anchors.topMargin: 1 - // anchors.bottomMargin: 1 - // anchors.leftMargin: 1 - // anchors.rightMargin: 4 - visible: app_window.bookmarkModel.search(actorUuidRole, "ownerRole").valid + visible: false - Connections { + property var actorUuid: actorUuidRole + onActorUuidChanged: { + rescan_for_bookmarks() + } + + function rescan_for_bookmarks() { + var vis = false + var idx = app_window.bookmarkModel.search(actorUuidRole, "ownerRole") + if (idx.valid) { + var foo = app_window.bookmarkModel.search_list(actorUuidRole, "ownerRole", idx.parent, 0, -1) + for (var i = 0; i < foo.length; ++i) { + if (app_window.bookmarkModel.get(foo[i], "visibleRole")) { + vis = true + break + } + } + } + bookmark_indicator.visible = vis + } + Connections { target: app_window.bookmarkModel function onLengthChanged() { callback_delay_timer.setTimeout( function(){ - bookmark_indicator.visible = app_window.bookmarkModel.search(actorUuidRole, "ownerRole").valid + bookmark_indicator.rescan_for_bookmarks() }, 500 ); diff --git a/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml b/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml index aa0bf2289..579f16603 100644 --- a/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml +++ b/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml @@ -118,7 +118,7 @@ Rectangle { moveTimer.stop() if(drop.hasUrls) { for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { + if(drop.urls[i].toLowerCase().endsWith('.xst') || drop.urls[i].toLowerCase().endsWith('.xsz')) { Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) app_window.sessionFunction.newRecentPath(drop.urls[i]) return; @@ -180,43 +180,10 @@ Rectangle { } } - XsButtonDialog { + XsMediaMoveCopyDialog { id: media_move_copy_dialog - // parent: sessionWidget.media_list - text: "Selected Media" - width: 300 - buttonModel: ["Cancel", "Move", "Copy"] - property var data: null - property var index: null - - onSelected: { - if(button_index == 1) { - // is selection still valid ? - let items = XsUtils.cloneArray(app_window.mediaSelectionModel.selectedIndexes).sort((a,b) => b.row - a.row ) - app_window.sessionFunction.setActiveMedia(app_window.sessionFunction.mediaIndexAfterRemoved(items)) - if(index == null) - Future.promise( - app_window.sessionModel.handleDropFuture(Qt.MoveAction, data) - ).then(function(quuids){}) - else - Future.promise( - app_window.sessionModel.handleDropFuture(Qt.MoveAction, data, index) - ).then(function(quuids){}) - - } else if(button_index == 2) { - if(index == null) - Future.promise( - app_window.sessionModel.handleDropFuture(Qt.CopyAction, data) - ).then(function(quuids){}) - else - Future.promise( - app_window.sessionModel.handleDropFuture(Qt.CopyAction, data, index) - ).then(function(quuids){}) - } - } } - Label { anchors.centerIn: parent verticalAlignment: Qt.AlignVCenter diff --git a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceDivider.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceDivider.qml index 5b1d3fb10..2c89a7c3f 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceDivider.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceDivider.qml @@ -38,7 +38,7 @@ DelegateChoice { anchors.right: parent.right color: highlighted || dropFlag ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - tint: flagRole == undefined ? "" : flagRole + tint: flagColourRole == undefined ? "" : flagColourRole expand_button_holder: true @@ -86,8 +86,8 @@ DelegateChoice { fakeDisabled: true XsFlagMenu { - flag: flagRole == undefined ? "" : flagRole - onFlagHexChanged: flagRole = flagHex + flag: flagColourRole == undefined ? "" : flagColourRole + onFlagHexChanged: flagColourRole = flagHex } XsMenuItem { diff --git a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoicePlaylist.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoicePlaylist.qml index 80e8a68a4..bf5e1799f 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoicePlaylist.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoicePlaylist.qml @@ -99,7 +99,7 @@ DelegateChoice { Component.onCompleted: { let ind = modelIndex() - ind.model.get(ind, "childrenRole") + ind.model.fetchMore(ind) updateCounts() control.highlighted = sessionSelectionModel.isSelected(modelIndex()) } @@ -156,7 +156,7 @@ DelegateChoice { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - tint: flagRole != undefined ? flagRole : "" + tint: flagColourRole != undefined ? flagColourRole : "" busy.running: busyRole != undefined ? busyRole : false @@ -261,9 +261,9 @@ DelegateChoice { target: control.DelegateModel.model.srcModel function onDataChanged(indexa,indexb,role) { if(modelIndex() == indexa && (!role.length || role.includes(sess.model.roleId("childrenRole")))) { - control.DelegateModel.model.srcModel.get(modelIndex(), "childrenRole") - control.DelegateModel.model.srcModel.get(sessionModel.index(0,0,modelIndex()), "childrenRole") - control.DelegateModel.model.srcModel.get(sessionModel.index(2,0,modelIndex()), "childrenRole") + control.DelegateModel.model.srcModel.fetchMore(modelIndex()) + control.DelegateModel.model.srcModel.fetchMore(sessionModel.index(0, 0, modelIndex())) + control.DelegateModel.model.srcModel.fetchMore(sessionModel.index(2, 0, modelIndex())) updateCounts() sess.rootIndex = control.DelegateModel.model.srcModel.index(2,0,control.DelegateModel.model.srcModel.index(index, 0, control.DelegateModel.model.rootIndex)) } diff --git a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml index 338b83101..c0c1aa9ce 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import Qt.labs.qmlmodels 1.0 import QtGraphicalEffects 1.15 //for RadialGradient import QtQml 2.15 @@ -42,7 +43,7 @@ DelegateChoice { Component.onCompleted: { // grab children - control.DelegateModel.model.srcModel.get(modelIndex(), "childrenRole") + control.DelegateModel.model.srcModel.fetchMore(modelIndex()) } function modelIndex() { @@ -87,7 +88,7 @@ DelegateChoice { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - tint: flagRole != undefined ? flagRole : "" + tint: flagColourRole != undefined ? flagColourRole : "" type_icon_source: "qrc:///feather_icons/trello.svg" type_icon_color: XsStyle.highlightColor @@ -149,8 +150,8 @@ DelegateChoice { fakeDisabled: true XsFlagMenu { - flag: flagRole != undefined ? flagRole : "" - onFlagHexChanged: flagRole = flagHex + flag: flagColourRole != undefined ? flagColourRole : "" + onFlagHexChanged: flagColourRole = flagHex } XsMenuSeparator {} diff --git a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml index 3a81dde5e..6849519c4 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import Qt.labs.qmlmodels 1.0 import QtGraphicalEffects 1.15 //for RadialGradient import QtQml 2.15 @@ -41,7 +42,18 @@ DelegateChoice { Component.onCompleted: { // grab children - control.DelegateModel.model.srcModel.get(modelIndex(), "childrenRole") + control.DelegateModel.model.srcModel.fetchMore(modelIndex()) + control.DelegateModel.model.srcModel.fetchMore(control.DelegateModel.model.srcModel.index(2, 0, modelIndex())) + + // just in case it's not ready yet. + delayTimer.setTimeout(function() { + control.DelegateModel.model.srcModel.fetchMore(control.DelegateModel.model.srcModel.index(2, 0, modelIndex())) + }, 200) + + } + + XsTimer { + id: delayTimer } function modelIndex() { @@ -86,7 +98,7 @@ DelegateChoice { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - tint: flagRole != undefined ? flagRole : "" + tint: flagColourRole != undefined ? flagColourRole : "" type_icon_source: "qrc:///feather_icons/align-left.svg" type_icon_color: XsStyle.highlightColor @@ -148,8 +160,8 @@ DelegateChoice { fakeDisabled: true XsFlagMenu { - flag: flagRole != undefined ? flagRole : "" - onFlagHexChanged: flagRole = flagHex + flag: flagColourRole != undefined ? flagColourRole : "" + onFlagHexChanged: flagColourRole = flagHex } XsMenuSeparator {} diff --git a/ui/qml/xstudio/panels/timeline/XsTimelinePanel.qml b/ui/qml/xstudio/panels/timeline/XsTimelinePanel.qml new file mode 100644 index 000000000..40a0d1025 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/XsTimelinePanel.qml @@ -0,0 +1,1395 @@ +// 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 QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.1 +import xstudio.qml.helpers 1.0 + +Rectangle { + id: timeline + color: timelineBackground + + property var hovered: null + property real scaleX: 3.0 + property real scaleY: 1.0 + property real itemHeight: 30.0 + property real trackHeaderWidth: 200.0 + + property color timelineBackground: "#FF333333" + property color timelineText: "#FFAFAFAF" + property color trackBackground: "#FF474747" + property color trackEdge: "#FF5B5B5B" + property color defaultClip: "#FF595959" + + focus: true + property alias timelineSelection: timelineSelection + property alias timelineFocusSelection: timelineFocusSelection + // onActiveFocusChanged: { + // console.log("onActiveFocusChanged", activeFocusItem) + // forceActiveFocus() + // } + + signal jumpToStart() + signal jumpToEnd() + signal jumpToFrame(int frame, bool center) + + DelegateChooser { + id: chooser + role: "typeRole" + + XsDelegateStack {} + } + + DelegateModel { + id: timeline_items + property var srcModel: app_window.sessionModel + model: srcModel + rootIndex: null + delegate: chooser + } + + ItemSelectionModel { + id: timelineSelection + model: timeline_items.srcModel + } + + ItemSelectionModel { + id: timelineFocusSelection + model: timeline_items.srcModel + + onSelectionChanged: focusItems(timelineFocusSelection.selectedIndexes) + } + + Connections { + target: app_window.currentSource + function onIndexChanged() { + if(app_window.currentSource.values.typeRole == "Timeline") { + forceActiveFocus() + timeline_items.rootIndex = app_window.sessionModel.index(2, 0, app_window.currentSource.index) + startFrames.index = app_window.sessionModel.index(2, 0, app_window.currentSource.index) + } + // else + // items.rootIndex = app_window.sessionModel.index(-1,-1) + } + } + + XsModelProperty { + id: startFrames + role: "trimmedStartRole" + index: app_window.sessionModel.index(2, 0, app_window.currentSource.index) + } + + XsStringRequestDialog { + id: set_name_dialog + title: "Change Name" + okay_text: "Set Name" + text: "Tag" + property var index: null + onOkayed: setItemName(index, text) + } + + XsButtonDialog { + id: new_item_dialog + rejectIndex: 0 + acceptIndex: -1 + width: 500 + text: "Choose item to add" + title: "Add Timeline Item" + property var insertion_parent: null + property int insertion_row: 0 + + buttonModel: ["Cancel", "Clip", "Gap", "Audio Track", "Video Track", "Stack"] + onSelected: { + if(button_index != 0) + addItem(buttonModel[button_index], insertion_parent, insertion_row) + } + } + + function setTrackHeaderWidth(val) { + trackHeaderWidth = Math.max(val, 40) + } + + function addItem(type, insertion_parent, insertion_row) { + + // insertion type + let insertion_index_type = app_window.sessionModel.get(insertion_parent, "typeRole") + if(type == "Video Track") { + if(insertion_index_type == "Timeline") { + insertion_parent = app_window.sessionModel.index(2, 0, insertion_parent) // timelineitem + insertion_parent = app_window.sessionModel.index(0, 0, insertion_parent) // stack + insertion_row = 0 + } else if(insertion_index_type != "Stack") { + insertion_parent = null + } + } + else if(type == "Audio Track") { + if(insertion_index_type == "Timeline") { + insertion_parent = app_window.sessionModel.index(2, 0, insertion_parent) // timelineitem + insertion_parent = app_window.sessionModel.index(0, 0, insertion_parent) // stack + insertion_row = app_window.sessionModel.rowCount(insertion_parent) // last track + 1 + } else if(insertion_index_type != "Stack") { + insertion_parent = null + } + } + else if(type == "Gap" || type == "Clip") { + if(insertion_index_type == "Timeline") { + insertion_parent = app_window.sessionModel.index(2, 0, insertion_parent) // timelineitem + insertion_parent = app_window.sessionModel.index(0, 0, insertion_parent) // stack + insertion_parent = app_window.sessionModel.index(0, 0, insertion_parent) // track + insertion_row = app_window.sessionModel.rowCount(insertion_parent) // last clip + } else if (insertion_index_type == "Stack") { + insertion_parent = app_window.sessionModel.index(insertion_row, 0, insertion_parent) + insertion_row = app_window.sessionModel.rowCount(insertion_parent) + } else { + console.log(insertion_parent, insertion_index_type) + } + } + + if(insertion_parent != null) { + app_window.sessionModel.insertRowsSync(insertion_row, 1, type, "New Item", insertion_parent) + } + } + + Connections { + target: app_window + function onFlagSelectedItems(flag) { + if(timeline.visible) { + let indexes = timelineSelection.selectedIndexes + for(let i=0;i= 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(app_window.currentSource.index); + event.accepted = true + } else if(event.key == Qt.Key_Z && event.modifiers == Qt.ControlModifier) { + // REDO + redo(app_window.currentSource.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) { + app_window.sessionModel.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) { + app_window.sessionModel.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) { + app_window.sessionModel.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) { + app_window.sessionModel.removeTimelineItems([resizePreceedingItem.modelIndex()]) + } + + if(delete_anteceeding) { + app_window.sessionModel.removeTimelineItems([resizeAnteceedingItem.modelIndex()]) + } + + if(insert_preceeding) { + app_window.sessionModel.insertTimelineGap(mindex.row, mindex.parent, resizeItem.adjustPreceedingGap, resizeItem.fps, "New Gap") + } + + if(insert_anteceeding) { + app_window.sessionModel.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.popup() + } 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: viewport.playhead.frame + property string itemFlag: "" + + onPlayheadFrameChanged: { + if (itemAtIndex(0)) { + itemAtIndex(0).jumpToFrame(playheadFrame, ListView.Visible) + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/qml/xstudio/panels/timeline/XsTimelinePanelHeader.qml b/ui/qml/xstudio/panels/timeline/XsTimelinePanelHeader.qml new file mode 100644 index 000000000..6a0581013 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/XsTimelinePanelHeader.qml @@ -0,0 +1,54 @@ +// 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/panels/timeline/delegates/XsDelegateAudioTrack.qml b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateAudioTrack.qml new file mode 100644 index 000000000..2fe8197db --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateAudioTrack.qml @@ -0,0 +1,134 @@ +// 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 xStudio 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: app_window.sessionModel + 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/xstudio/panels/timeline/delegates/XsDelegateClip.qml b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateClip.qml new file mode 100644 index 000000000..b5938f10f --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateClip.qml @@ -0,0 +1,221 @@ +// 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 xStudio 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() + } + + XsTimer { + id: delayTimer + } + + 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/xstudio/panels/timeline/delegates/XsDelegateGap.qml b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateGap.qml new file mode 100644 index 000000000..494aab125 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateGap.qml @@ -0,0 +1,83 @@ +// 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 xStudio 1.1 +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/xstudio/panels/timeline/delegates/XsDelegateStack.qml b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateStack.qml new file mode 100644 index 000000000..eaa0a2616 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateStack.qml @@ -0,0 +1,418 @@ +// 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 xStudio 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: app_window.sessionModel + 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: app_window.sessionModel + 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: app_window.sessionModel + + 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: viewport.playhead.frame = frame + onFrameDragging: viewport.playhead.frame = 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 + } + } + } + } + + XsBorder { + id: sizer + Layout.minimumWidth: parent.width + Layout.preferredHeight: handleSize + Layout.minimumHeight: handleSize + Layout.maximumHeight: handleSize + color: trackEdge + leftBorder: false + rightBorder: false + 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/xstudio/panels/timeline/delegates/XsDelegateVideoTrack.qml b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateVideoTrack.qml new file mode 100644 index 000000000..eb82ae9c1 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/delegates/XsDelegateVideoTrack.qml @@ -0,0 +1,147 @@ +// 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 xStudio 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: app_window.sessionModel + 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/xstudio/panels/timeline/widgets/XsClipItem.qml b/ui/qml/xstudio/panels/timeline/widgets/XsClipItem.qml new file mode 100644 index 000000000..3fb3e559d --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsClipItem.qml @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import xStudio 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 ? XsStyle.highlightColor : 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/xstudio/panels/timeline/widgets/XsDragBoth.qml b/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml new file mode 100644 index 000000000..612ac109c --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Shapes 1.12 +import xStudio 1.0 + + +Shape { + id: control + property real thickness: 2 + property color color: XsStyle.highlightColor + + 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/xstudio/panels/timeline/widgets/XsDragLeft.qml b/ui/qml/xstudio/panels/timeline/widgets/XsDragLeft.qml new file mode 100644 index 000000000..badd2ec7f --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsDragLeft.qml @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Shapes 1.12 +import xStudio 1.0 + + +Shape { + id: control + property real thickness: 2 + property color color: XsStyle.highlightColor + + 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/xstudio/panels/timeline/widgets/XsDragRight.qml b/ui/qml/xstudio/panels/timeline/widgets/XsDragRight.qml new file mode 100644 index 000000000..1b0304667 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsDragRight.qml @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +import xStudio 1.0 + +XsDragLeft { + rotation: 180.0 +} \ No newline at end of file diff --git a/ui/qml/xstudio/panels/timeline/widgets/XsGapItem.qml b/ui/qml/xstudio/panels/timeline/widgets/XsGapItem.qml new file mode 100644 index 000000000..7cfe0fc88 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsGapItem.qml @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import xStudio 1.1 + +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/xstudio/panels/timeline/widgets/XsMoveClip.qml b/ui/qml/xstudio/panels/timeline/widgets/XsMoveClip.qml new file mode 100644 index 000000000..612ac109c --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsMoveClip.qml @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Shapes 1.12 +import xStudio 1.0 + + +Shape { + id: control + property real thickness: 2 + property color color: XsStyle.highlightColor + + 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/xstudio/panels/timeline/widgets/XsTrackHeader.qml b/ui/qml/xstudio/panels/timeline/widgets/XsTrackHeader.qml new file mode 100644 index 000000000..b7b80fa6f --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsTrackHeader.qml @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import xStudio 1.1 + +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/xstudio/player/XsLightPlayerWidget.qml b/ui/qml/xstudio/player/XsLightPlayerWidget.qml new file mode 100644 index 000000000..9d9d416e5 --- /dev/null +++ b/ui/qml/xstudio/player/XsLightPlayerWidget.qml @@ -0,0 +1,228 @@ +// 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 QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 +import QtGraphicalEffects 1.12 +import QtQuick.Shapes 1.12 +import Qt.labs.platform 1.1 +import Qt.labs.settings 1.0 +import QtQml.Models 2.14 +import QtQml 2.14 + + +//------------------------------------------------------------------------------ +// BEGIN COMMENT OUT WHEN WORKING INSIDE Qt Creator +//------------------------------------------------------------------------------ +import xstudio.qml.viewport 1.0 +import xstudio.qml.semver 1.0 +import xstudio.qml.cursor_pos_provider 1.0 +import xstudio.qml.uuid 1.0 +import xstudio.qml.module 1.0 +import xstudio.qml.helpers 1.0 + +//------------------------------------------------------------------------------ +// END COMMENT OUT WHEN WORKING INSIDE Qt Creator +//------------------------------------------------------------------------------ + + +import xStudio 1.0 + +Rectangle { + + id: playerWidget + visible: true + color: "#00000000" + + property var global_store + + property var qmlWindowRef: Window // so javascript can reference Window enums. + property bool controlsVisible: true + property bool doTrayAnim: true + property alias player_prefs: player_prefs + property bool is_full_screen: { light_player ? light_player.visibility == Window.FullScreen : false } + property var playlist_panel + property string preferencePath: "" + property bool is_presentation_mode: false + property bool is_main_window: true + // quick fix for v1.0.1 + XsShortcuts { + anchors.fill: parent + id: shortcuts + context: viewport.name + //enabled: viewport.enableShortcuts + } + + property bool media_info_bar_visible: true + property bool tool_bar_visible: true + property bool transport_controls_visible: true + + XsModelNestedPropertyMap { + id: player_prefs + index: app_window.globalStoreModel.search_recursive(playerWidget.preferencePath, "pathRole") + property alias properties: player_prefs.values + } + + function toggleFullscreen() { + light_player.toggleFullscreen() + } + + function normalScreen() { + light_player.normalScreen() + } + + XsStatusBar { + id: status_bar + opacity: 0 + visible: false + } + + property alias viewport: viewport + property var playhead: viewport.playhead + + Keys.forwardTo: viewport + focus: true + + function toggleControlsVisible() { + + if (media_info_bar_visible) { + media_info_bar_visible = false + tool_bar_visible = false + transport_controls_visible = false + } else { + media_info_bar_visible = true + tool_bar_visible = true + transport_controls_visible = true + } + } + + ColumnLayout { + spacing: 0 + anchors.fill: parent + + RowLayout { + + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 0 + + /*Rectangle { + color: XsStyle.mainBackground + width: 16 + Layout.fillHeight: true + }*/ + + ColumnLayout { + + id: playerWidgetItem + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + + // visible: !xstudioWarning.visible + property alias winWidth: playerWidget.width + + XsMediaInfoBar { + id: mediaInfoBar + objectName: "mediaInfoBar" + Layout.fillWidth: true + opacity: media_info_bar_visible + } + + XsViewport { + id: viewport + objectName: "viewport" + Layout.fillWidth: true + Layout.fillHeight: true + isQuickViewer: true + } + + XsToolBar { + id: toolBar + Layout.fillWidth: true + opacity: tool_bar_visible + } + + Rectangle { + color: XsStyle.mainBackground + height: 8*opacity + Layout.fillWidth: true + visible: opacity !== 0 + opacity: transport_controls_visible + Behavior on opacity { + NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } + } + } + + XsMainControls { + id: myMainControls + Layout.fillWidth: true + opacity: transport_controls_visible + } + + Rectangle { + color: XsStyle.mainBackground + height: 8*opacity + Layout.fillWidth: true + visible: opacity !== 0 + opacity: transport_controls_visible + Behavior on opacity { + NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } + } + } + + + } + + /*Rectangle { + color: XsStyle.mainBackground + width: 16 + Layout.fillHeight: true + }*/ + + } + } + + property alias toolBar: toolBar + + // When certain custom pop-up widgets are visible a click outside + // of it should hide it, to do this we need a mouse area + // underneath it that captures all mouse events while it + // is visisble + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + hoverEnabled: false + id: override_mouse_area + enabled: false + z: -10000 + onClicked: { + if (hider) hider() + enabled = false + z = -10000 + } + function activate(hider_func) { + enabled = true + z = 9000 + hider = hider_func + } + function deactivate() { + if (hider) hider() + enabled = false + z = -10000 + } + property var hider + } + + function activateWidgetHider(window_to_hide) { + override_mouse_area.activate(window_to_hide) + } + + function deactivateWidgetHider() { + override_mouse_area.deactivate() + } + + +} diff --git a/ui/qml/xstudio/player/XsLightPlayerWindow.qml b/ui/qml/xstudio/player/XsLightPlayerWindow.qml new file mode 100644 index 000000000..2071e8f14 --- /dev/null +++ b/ui/qml/xstudio/player/XsLightPlayerWindow.qml @@ -0,0 +1,158 @@ +// 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 QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 +import QtGraphicalEffects 1.12 +import QtQuick.Shapes 1.12 +import Qt.labs.platform 1.1 +import Qt.labs.settings 1.0 +import QtQml.Models 2.14 +import QtQml 2.14 + + +//------------------------------------------------------------------------------ +// BEGIN COMMENT OUT WHEN WORKING INSIDE Qt Creator +//------------------------------------------------------------------------------ +//import xstudio.qml.playlist 1.0 +import xstudio.qml.semver 1.0 +import xstudio.qml.cursor_pos_provider 1.0 +import xstudio.qml.global_store_model 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.session 1.0 + +//------------------------------------------------------------------------------ +// END COMMENT OUT WHEN WORKING INSIDE Qt Creator +//------------------------------------------------------------------------------ + +// import "../fonts/Overpass" +// import "../fonts/BitstreamVeraMono" +import xStudio 1.0 + +ApplicationWindow { + + width: 1280 + height: 820 + color: "#00000000" + title: "xSTUDIO QuickView: " + mediaImageSource.fileName + minimumHeight: 320 + minimumWidth: 480 + id: light_player + property var preFullScreenVis: [app_window.x, app_window.y, app_window.width, app_window.height] + property int vis_before_hide: -1 + + palette.base: XsStyle.controlBackground + palette.text: XsStyle.hoverColor + palette.button: XsStyle.controlTitleColor + palette.highlight: highlightColor + palette.light: highlightColor + palette.highlightedText: XsStyle.mainBackground + palette.brightText: highlightColor + palette.buttonText: XsStyle.hoverColor + palette.windowText: XsStyle.hoverColor + + onClosing: { + destroy() + app_window.closingQuickviewWindow(Qt.point(x, y), Qt.size(width, height)) + } + + Component.onCompleted: { + requestActivate() + raise() + } + + property var sessionModel + + // This thing monitors the 'mediaUuid' which is a property of the playhead + // and tells us the uuid of the media that is on-screen. When mediaUuid + // changes, we find the model data for the media in the sessionModel. We + // then use this to get the 'imageActorUuidRole' which gives us the uuid + // of the media source (video). We then search the session again to find + // the media source (video) model data, and update mediaImageSource to + // track it. + + XsTimer { + id: m_timer + } + + XsModelPropertyMap { + id: currentMediaItem + index: sessionModel.index(-1,-1) + property var screenMediaUuid: mediaUuid + property var imageSource: values ? values.imageActorUuidRole ? values.imageActorUuidRole : undefined : undefined + + onScreenMediaUuidChanged: { + index = sessionModel.search_recursive(mediaUuid, "actorUuidRole") + } + + function setMediaImageSource() { + let mind = sessionModel.search_recursive(imageSource, "actorUuidRole") + if(mind.valid && mediaImageSource.index != mind) { + mediaImageSource.index = mind + return true + } + return false + } + + onImageSourceChanged: { + + if(!setMediaImageSource()) { + // This is a bit ugly - the session model is a bit behind us, + // and it hasn't yet been updated with the media item that we're + // interested in (where actorUuidRole == imageSource). + // Therefore we repeat the search 100ms later + m_timer.setTimeout(currentMediaItem.setMediaImageSource, 100) + } + + } + + } + + // current MT_IMAGE media source + property alias mediaImageSource: mediaImageSource + XsModelPropertyMap { + id: mediaImageSource + index: sessionModel.index(-1,-1) + property var fileName: { + let result = "" + if(index.valid && values.pathRole != undefined) { + result = helpers.fileFromURL(values.pathRole) + } + return result + } + + } + + function toggleFullscreen() { + if (visibility !== Window.FullScreen) { + preFullScreenVis = [x, y, width, height] + showFullScreen(); + } else { + visibility = qmlWindowRef.Windowed + x = preFullScreenVis[0] + y = preFullScreenVis[1] + width = preFullScreenVis[2] + height = preFullScreenVis[3] + } + } + + function normalScreen() { + if (visibility == Window.FullScreen) { + toggleFullscreen() + } + } + + XsLightPlayerWidget { + id: sessionWidget + anchors.fill: parent + focus: true + } + + property alias playerWidget: sessionWidget + property var viewport: sessionWidget.viewport + property var mediaUuid: viewport.playhead ? viewport.playhead.mediaUuid : undefined + property alias sessionWidget: sessionWidget + +} diff --git a/ui/qml/xstudio/player/XsPlayerWidget.qml b/ui/qml/xstudio/player/XsPlayerWidget.qml index bd70439cc..31139d829 100644 --- a/ui/qml/xstudio/player/XsPlayerWidget.qml +++ b/ui/qml/xstudio/player/XsPlayerWidget.qml @@ -188,9 +188,8 @@ Rectangle { XsViewport { id: viewport objectName: "viewport" - is_popout_viewport: !playerWidget.is_main_window Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillHeight: true } XsToolBar { diff --git a/ui/qml/xstudio/player/XsPlayerWindow.qml b/ui/qml/xstudio/player/XsPlayerWindow.qml index 2c55cb724..8dd8418f8 100644 --- a/ui/qml/xstudio/player/XsPlayerWindow.qml +++ b/ui/qml/xstudio/player/XsPlayerWindow.qml @@ -102,25 +102,6 @@ ApplicationWindow { } } - function fitWindowToImage() { - - if (visibility === Window.FullScreen) return - - // get the bdb of the image (in viewport pixel coordinates) - // and adjust position and size of window so it hugs the - // image - var img_dbd = viewport.imageCoordsOnScreen() - y = y+img_dbd.y - height = height-(viewport.height-img_dbd.height) - x = x+img_dbd.x - width = width-(viewport.width-img_dbd.width) - if (viewport.fitMode === "Off") { - viewport.scale = 1.0 - viewport.translate = Qt.vector2d(0.0,0.0) - } - - } - XsPopoutViewerWidget { anchors.fill: parent id: sessionWidget @@ -128,7 +109,7 @@ ApplicationWindow { window_name: "second_window" // this is important for picking up window settings for the 2nd window is_main_window: false focus: true - Keys.forwardTo: viewport + // } property alias sessionWidget: sessionWidget property var viewport: sessionWidget.viewport diff --git a/ui/qml/xstudio/player/XsPopoutViewerWidget.qml b/ui/qml/xstudio/player/XsPopoutViewerWidget.qml index 47baf5a57..a3dfb2869 100644 --- a/ui/qml/xstudio/player/XsPopoutViewerWidget.qml +++ b/ui/qml/xstudio/player/XsPopoutViewerWidget.qml @@ -91,6 +91,7 @@ Rectangle { } property var viewport: playerWidget.viewport + Keys.forwardTo: viewport XsStatusBar { id: status_bar diff --git a/ui/qml/xstudio/player/XsSessionWidget.qml b/ui/qml/xstudio/player/XsSessionWidget.qml index 09da324f6..0c8ff3e82 100644 --- a/ui/qml/xstudio/player/XsSessionWidget.qml +++ b/ui/qml/xstudio/player/XsSessionWidget.qml @@ -38,7 +38,6 @@ Rectangle { property real borderWidth: XsStyle.outerBorderWidth property var sessionMenu: menu_row.menuBar - property var mediaMenu1: media_list.mediaMenu property alias playerWidget: playerWidget @@ -72,6 +71,7 @@ Rectangle { id: prefs index: app_window.globalStoreModel.search_recursive("/ui/qml/" + window_name + "_settings", "pathRole") property alias properties: prefs.values + } property string layout_name: prefs.values.layout_name !== undefined ? prefs.values.layout_name : "" @@ -328,7 +328,7 @@ Rectangle { //bottom_divider: vert_divider2 - header_component: "qrc:/bars/XsTimelinePanelHeader.qml" + header_component: "qrc:/panels/timeline/XsTimelinePanelHeader.qml" XsTimelinePanel { id: timeline diff --git a/ui/qml/xstudio/player/XsViewport.qml b/ui/qml/xstudio/player/XsViewport.qml index 5b3f3e54b..245a364e8 100644 --- a/ui/qml/xstudio/player/XsViewport.qml +++ b/ui/qml/xstudio/player/XsViewport.qml @@ -19,9 +19,35 @@ Viewport { id: viewport objectName: "viewport" - property bool is_popout_viewport: false property bool viewing_alpha_channel: false + onPointerEntered: { + focus = true; + forceActiveFocus() + } + + XsButtonDialog { + id: snapshotResultDialog + // parent: sessionWidget + width: text.width + 20 + title: "Snapshot export fail" + text: { + return "The snapshot could not be exported. Please check the parameters" + } + buttonModel: ["Ok"] + onSelected: { + snapshotResultDialog.close() + } + } + + onSnapshotRequestResult: { + if (resultMessage != "") { + snapshotResultDialog.title = "Snapshot export failed" + snapshotResultDialog.text = resultMessage + snapshotResultDialog.open() + } + } + XsOutOfRangeOverlay { visible: viewport.frameOutOfRange anchors.fill: parent @@ -54,7 +80,15 @@ Viewport { id: blank_viewport_card anchors.fill: parent - visible: playhead.mediaUuid == "{00000000-0000-0000-0000-000000000000}" + visible: false//playhead.mediaUuid == "{00000000-0000-0000-0000-000000000000}" + } + + onQuickViewBackendRequest: { + app_window.launchQuickViewer(mediaActors, compareMode) + } + + onQuickViewBackendRequestWithSize: { + app_window.launchQuickViewerWithSize(mediaActors, compareMode, position, size) } DropArea { @@ -68,7 +102,7 @@ Viewport { onDropped: { if(drop.hasUrls) { for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { + if(drop.urls[i].toLowerCase().endsWith('.xst') || drop.urls[i].toLowerCase().endsWith('.xsz')) { Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) app_window.sessionFunction.newRecentPath(drop.urls[i]) return; @@ -101,7 +135,6 @@ Viewport { XsViewerContextMenu { id: viewerContextMenu - is_popout_viewport: viewport.is_popout_viewport } XsModelProperty { @@ -136,6 +169,29 @@ Viewport { } } + Repeater { + + id: viewport_overlay_plugins + anchors.fill: parent + model: viewport_overlays + + delegate: Item { + + id: parent_item + anchors.fill: parent + + property var dynamic_widget + + property var type_: type ? type : null + + onType_Changed: { + if (type == "QmlCode") { + dynamic_widget = Qt.createQmlObject(qml_code, parent_item) + } + } + } + } + Item { id: hud anchors.fill: parent @@ -353,26 +409,4 @@ Viewport { } } - Repeater { - - id: viewport_overlay_plugins - anchors.fill: parent - model: viewport_overlays - - delegate: Item { - - id: parent_item - anchors.fill: parent - - property var dynamic_widget - - property var type_: type ? type : null - - onType_Changed: { - if (type == "QmlCode") { - dynamic_widget = Qt.createQmlObject(qml_code, parent_item) - } - } - } - } } diff --git a/ui/qml/xstudio/qml.qrc b/ui/qml/xstudio/qml.qrc index a442bc310..82442c087 100644 --- a/ui/qml/xstudio/qml.qrc +++ b/ui/qml/xstudio/qml.qrc @@ -294,10 +294,8 @@ bars/XsMediaListPanelHeader.qml bars/XsMenuBar.qml bars/XsSessionControls.qml - panels/playlist/XsSessionPanelHeader.qml bars/XsShortcuts.qml bars/XsStatusBar.qml - bars/XsTimelinePanelHeader.qml bars/XsToolBar.qml bars/XsViewportTitleBar.qml base/core/XsDraggableItem.qml @@ -313,6 +311,7 @@ base/dialogs/XsStringRequestDialog.qml base/dialogs/XsWindow.qml base/widgets/XsBoolAttrCheckBox.qml + base/widgets/XsBorder.qml base/widgets/XsBusyIndicator.qml base/widgets/XsButton.qml base/widgets/XsButtonNew.qml @@ -324,11 +323,12 @@ base/widgets/XsCollapsibleLabel.qml base/widgets/XsColoredImage.qml base/widgets/XsColourChooser.qml - base/widgets/XsComboBoxNew.qml base/widgets/XsComboBoxMultiSelect.qml base/widgets/XsComboBoxNew.qml + base/widgets/XsComboBoxNew.qml base/widgets/XsComboBoxWithText.qml base/widgets/XsDecoratorWidget.qml + base/widgets/XsElideLabel.qml base/widgets/XsExpandButton.qml base/widgets/XsFloatAttrSlider.qml base/widgets/XsFrame.qml @@ -365,6 +365,8 @@ base/widgets/XsTextFieldNew.qml base/widgets/XsTextHoverable.qml base/widgets/XsTextInput.qml + base/widgets/XsTickWidget.qml + base/widgets/XsTimelineCursor.qml base/widgets/XsTimelineSlider.qml base/widgets/XsToolbarComboBox.qml base/widgets/XsToolbarFloatScrubber.qml @@ -379,6 +381,9 @@ base/widgets/XsTreeView.qml core/XsGlobalPreferences.qml cursors/magnifier_cursor.svg + cursors/move-edge-left.svg + cursors/move-edge-right.svg + cursors/move-join.svg dialogs/XsAboutDialog.qml dialogs/XsColourCorrectionDialog.qml dialogs/XsDrawingDialog.qml @@ -389,6 +394,8 @@ dialogs/XsHotkeysDialog.qml dialogs/XsImportSessionDialog.qml dialogs/XsLogDialog.qml + dialogs/XsMediaMoveCopyDialog.qml + dialogs/XsNewSnapshotDialog.qml dialogs/XsNotesDialog.qml dialogs/XsOpenSessionDialog.qml dialogs/XsSaveBeforeDialog.qml @@ -464,6 +471,8 @@ menus/XsPlaylistMenu.qml menus/XsPublishMenu.qml menus/XsRepeatMenu.qml + menus/XsSnapshotMenu.qml + menus/XsSnapshotDirectoryMenu.qml menus/XsTimelineMenu.qml menus/XsViewerContextMenu.qml menus/XsViewerLayoutsMenu.qml @@ -487,14 +496,30 @@ panels/panel_layouts/XsShotgunLayout.qml panels/panel_layouts/XsSplitWidget.qml panels/panel_layouts/XsVerticalPaneDivider.qml + panels/playlist/delegates/XsDelegateChoiceDivider.qml panels/playlist/delegates/XsDelegateChoicePlaylist.qml panels/playlist/delegates/XsDelegateChoiceSubset.qml panels/playlist/delegates/XsDelegateChoiceTimeline.qml - panels/playlist/delegates/XsDelegateChoiceDivider.qml panels/playlist/XsPlaylistsPanelNew.qml panels/playlist/XsSessionBarDivider.qml panels/playlist/XsSessionBarWidget.qml - panels/XsTimelinePanel.qml + panels/playlist/XsSessionPanelHeader.qml + panels/timeline/delegates/XsDelegateStack.qml + panels/timeline/delegates/XsDelegateClip.qml + panels/timeline/delegates/XsDelegateGap.qml + panels/timeline/delegates/XsDelegateAudioTrack.qml + panels/timeline/delegates/XsDelegateVideoTrack.qml + panels/timeline/widgets/XsClipItem.qml + panels/timeline/widgets/XsDragLeft.qml + panels/timeline/widgets/XsMoveClip.qml + panels/timeline/widgets/XsDragRight.qml + panels/timeline/widgets/XsDragBoth.qml + panels/timeline/widgets/XsGapItem.qml + panels/timeline/widgets/XsTrackHeader.qml + panels/timeline/XsTimelinePanel.qml + panels/timeline/XsTimelinePanelHeader.qml + player/XsLightPlayerWidget.qml + player/XsLightPlayerWindow.qml player/XsPlayerWidget.qml player/XsPlayerWindow.qml player/XsPopoutViewerWidget.qml @@ -511,12 +536,12 @@ widgets/XsMediaInfoBarAutoAlign.qml widgets/XsMediaInfoBarItem.qml widgets/XsMediaInfoBarOffset.qml - widgets/XsSourceToolbarButton.qml widgets/XsPluginMenu.qml widgets/XsPluginWidget.qml widgets/XsPythonWidget.qml widgets/XsRewindWidget.qml widgets/XsSnapshotWidget.qml + widgets/XsSourceToolbarButton.qml widgets/XsStepBackWidget.qml widgets/XsStepForwardWidget.qml widgets/XsTimelineDurationWidget.qml @@ -527,9 +552,11 @@ XsStyleGradient.qml xstudio_icon.png xStudio/qmldir + + diff --git a/ui/qml/xstudio/trays/XsMediaToolsTray.qml b/ui/qml/xstudio/trays/XsMediaToolsTray.qml index c8dfc5ed8..d81b6870f 100644 --- a/ui/qml/xstudio/trays/XsMediaToolsTray.qml +++ b/ui/qml/xstudio/trays/XsMediaToolsTray.qml @@ -44,17 +44,35 @@ RowLayout { } } - XsTrayButton { + // this is a mess as we wanted to put the grading tool to the left of the + // notes button ... with re-skin UI this will go away. + XsOrderedModuleAttributesModel { + id: attrs0 + attributesGroupNames: "media_tools_buttons_0" + } + + + ListView { + id: extra_dudes0 Layout.fillHeight: true - prototype: true - text: "Colour" - 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: colourDialog ? colourDialog.visible : false - onClicked: { - toggleColourDialog() - } + Layout.minimumWidth: count * 32 + model: attrs0 + focus: true + orientation: ListView.Horizontal + delegate: + Item { + id: parent_item + width: 32 + height: extra_dudes0.height + property var dynamic_widget + property var qml_code_: qml_code ? qml_code : null + + onQml_code_Changed: { + if (qml_code_) { + dynamic_widget = Qt.createQmlObject(qml_code_, parent_item) + } + } + } } XsTrayButton { diff --git a/ui/qml/xstudio/widgets/XsMediaInfoBarAutoAlign.qml b/ui/qml/xstudio/widgets/XsMediaInfoBarAutoAlign.qml index dca5506a4..6eb2b9ee5 100644 --- a/ui/qml/xstudio/widgets/XsMediaInfoBarAutoAlign.qml +++ b/ui/qml/xstudio/widgets/XsMediaInfoBarAutoAlign.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 diff --git a/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml b/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml index fd886ad2c..41951d046 100644 --- a/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml +++ b/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Controls 2.12 import QtGraphicalEffects 1.12 @@ -5,6 +6,8 @@ import QtQuick.Layouts 1.15 import xStudio 1.0 import xstudio.qml.module 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 XsToolbarItem { @@ -14,6 +17,7 @@ XsToolbarItem { hovered: mouse_area.containsMouse showHighlighted: mouse_area.containsMouse | mouse_area.pressed | (activated != undefined && activated) property int iconsize: XsStyle.menuItemHeight *.66 + property string toolbar_name MouseArea { id: mouse_area @@ -25,17 +29,37 @@ XsToolbarItem { } } - XsModuleAttributes { - id: image_source_value_watcher - attributesGroupNames: "image_source" + XsModuleData { + id: image_source + modelDataName: toolbar_name + "_image_source" + onJsonChanged: { + curr_image_source_property.index = search_recursive("Source", "title") + } } - XsModuleAttributes { - id: audio_source_value_watcher - attributesGroupNames: "audio_source" + + XsModuleData { + id: audio_source + modelDataName: toolbar_name + "_audio_source" + onJsonChanged: { + curr_audio_source_property.index = search_recursive("Source", "title") + } } - property var curr_image_source: image_source_value_watcher.source ? image_source_value_watcher.source : "" - property var curr_audio_source: audio_source_value_watcher.source ? audio_source_value_watcher.audio_source : "" + + XsModelProperty { + id: curr_image_source_property + role: "value" + index: image_source.search_recursive("Source", "title") + } + + XsModelProperty { + id: curr_audio_source_property + role: "value" + index: audio_source.search_recursive("Source", "title") + } + + property var curr_image_source: curr_image_source_property.value + property var curr_audio_source: curr_audio_source_property.value value_text: curr_image_source != "" ? curr_image_source : curr_audio_source != "" ? curr_audio_source : "None" @@ -71,12 +95,7 @@ XsToolbarItem { } color: XsStyle.mainBackground radius: XsStyle.menuRadius - - XsModuleAttributesModel { - id: image_source - attributesGroupNames: "image_source" - } - + ColumnLayout { id: imageColumn @@ -199,11 +218,6 @@ XsToolbarItem { color: XsStyle.menuBorderColor } - XsModuleAttributesModel { - id: audio_source - attributesGroupNames: "audio_source" - } - ColumnLayout { id: audioColumn diff --git a/ui/qml/xstudio/xStudio/qmldir b/ui/qml/xstudio/xStudio/qmldir index a92e5bbb9..d4dc8d0aa 100644 --- a/ui/qml/xstudio/xStudio/qmldir +++ b/ui/qml/xstudio/xStudio/qmldir @@ -13,7 +13,6 @@ XsSessionControls 1.0 ../bars/XsSessionControls.qml XsSessionPanelHeader 1.0 ../panels/playlist/XsSessionPanelHeader.qml XsShortcuts 1.0 ../bars/XsShortcuts.qml XsStatusBar 1.0 ../bars/XsStatusBar.qml -XsTimelinePanelHeader 1.0 ../bars/XsTimelinePanelHeader.qml XsToolBar 1.0 ../bars/XsToolBar.qml XsViewerTabBar 1.0 ../bars/XsViewerTabBar.qml XsViewportTitleBar 1.0 ../bars/XsViewportTitleBar.qml @@ -33,10 +32,11 @@ XsStringRequestDialog 1.0 ../base/dialogs/XsStringRequestDialog.qml XsWindow 1.0 ../base/dialogs/XsWindow.qml XsBoolAttrCheckBox 1.0 ../base/widgets/XsBoolAttrCheckBox.qml +XsBorder 1.0 ../base/widgets/XsBorder.qml XsBusyIndicator 1.0 ../base/widgets/XsBusyIndicator.qml XsButton 1.0 ../base/widgets/XsButton.qml -XsButtonOld 1.1 ../base/widgets/XsButton.qml XsButton 1.1 ../base/widgets/XsButtonNew.qml +XsButtonOld 1.1 ../base/widgets/XsButton.qml XsCheckbox 1.0 ../base/widgets/XsCheckbox.qml XsCheckboxOld 1.1 ../base/widgets/XsCheckbox.qml XsCheckbox 1.1 ../base/widgets/XsCheckBoxNew.qml @@ -50,6 +50,7 @@ XsComboBox 1.1 ../base/widgets/XsComboBoxNew.qml XsComboBoxMultiSelect 1.1 ../base/widgets/XsComboBoxMultiSelect.qml XsComboBoxWithText 1.1 ../base/widgets/XsComboBoxWithText.qml XsDecoratorWidget 1.0 ../base/widgets/XsDecoratorWidget.qml +XsElideLabel 1.0 ../base/widgets/XsElideLabel.qml XsExpandButton 1.0 ../base/widgets/XsExpandButton.qml XsFloatAttrSlider 1.0 ../base/widgets/XsFloatAttrSlider.qml XsFrame 1.1 ../base/widgets/XsFrame.qml @@ -86,6 +87,8 @@ XsTextEdit 1.0 ../base/widgets/XsTextEdit.qml XsTextField 1.1 ../base/widgets/XsTextFieldNew.qml XsTextHoverable 1.1 ../base/widgets/XsTextHoverable.qml XsTextInput 1.0 ../base/widgets/XsTextInput.qml +XsTickWidget 1.0 ../base/widgets/XsTickWidget.qml +XsTimelineCursor 1.0 ../base/widgets/XsTimelineCursor.qml XsTimelineSlider 1.0 ../base/widgets/XsTimelineSlider.qml XsToolbarComboBox 1.0 ../base/widgets/XsToolbarComboBox.qml XsToolbarFloatScrubber 1.0 ../base/widgets/XsToolbarFloatScrubber.qml @@ -110,6 +113,8 @@ XsFeedbackDialog 1.0 ../dialogs/XsFeedbackDialog.qml XsFunctionalFeaturesDialog 1.0 ../dialogs/XsFunctionalFeaturesDialog.qml XsImportSessionDialog 1.0 ../dialogs/XsImportSessionDialog.qml XsLogDialog 1.0 ../dialogs/XsLogDialog.qml +XsMediaMoveCopyDialog 1.0 ../dialogs/XsMediaMoveCopyDialog.qml +XsNewSnapshotDialog 1.0 ../dialogs/XsNewSnapshotDialog.qml XsNotesDialog 1.0 ../dialogs/XsNotesDialog.qml XsOpenSessionDialog 1.0 ../dialogs/XsOpenSessionDialog.qml XsSaveBeforeDialog 1.0 ../dialogs/XsSaveBeforeDialog.qml @@ -138,6 +143,8 @@ XsPlaybackMenu 1.0 ../menus/XsPlaybackMenu.qml XsPlaylistMenu 1.0 ../menus/XsPlaylistMenu.qml XsPublishMenu 1.0 ../menus/XsPublishMenu.qml XsRepeatMenu 1.0 ../menus/XsRepeatMenu.qml +XsSnapshotMenu 1.0 ../menus/XsSnapshotMenu.qml +XsSnapshotDirectoryMenu 1.0 ../menus/XsSnapshotDirectoryMenu.qml XsTimelineMenu 1.0 ../menus/XsTimelineMenu.qml XsViewerContextMenu 1.0 ../menus/XsViewerContextMenu.qml XsViewerLayoutsMenu 1.0 ../menus/XsViewerLayoutsMenu.qml @@ -153,12 +160,26 @@ XsSessionBarDivider 1.0 ../panels/playlist/XsSessionBarDivider.qml XsSessionBarWidget 1.0 ../panels/playlist/XsSessionBarWidget.qml XsPlaylistsPanelNew 1.0 ../panels/playlist/XsPlaylistsPanelNew.qml -XsDelegateChoicePlaylist 1.0 ../panels/playlist/delegates/XsDelegateChoicePlaylist.qml -XsDelegateChoiceDivider 1.0 ../panels/playlist/delegates/XsDelegateChoiceDivider.qml -XsDelegateChoiceSubset 1.0 ../panels/playlist/delegates/XsDelegateChoiceSubset.qml -XsDelegateChoiceTimeline 1.0 ../panels/playlist/delegates/XsDelegateChoiceTimeline.qml +XsDelegateChoicePlaylist 1.0 ../panels/playlist/delegates/XsDelegateChoicePlaylist.qml +XsDelegateChoiceDivider 1.0 ../panels/playlist/delegates/XsDelegateChoiceDivider.qml +XsDelegateChoiceSubset 1.0 ../panels/playlist/delegates/XsDelegateChoiceSubset.qml +XsDelegateChoiceTimeline 1.0 ../panels/playlist/delegates/XsDelegateChoiceTimeline.qml + +XsDelegateClip 1.0 ../panels/timeline/delegates/XsDelegateClip.qml +XsDelegateGap 1.0 ../panels/timeline/delegates/XsDelegateGap.qml +XsDelegateStack 1.0 ../panels/timeline/delegates/XsDelegateStack.qml +XsDelegateAudioTrack 1.0 ../panels/timeline/delegates/XsDelegateAudioTrack.qml +XsDelegateVideoTrack 1.0 ../panels/timeline/delegates/XsDelegateVideoTrack.qml +XsClipItem 1.0 ../panels/timeline/widgets/XsClipItem.qml +XsDragLeft 1.0 ../panels/timeline/widgets/XsDragLeft.qml +XsMoveClip 1.0 ../panels/timeline/widgets/XsMoveClip.qml +XsDragRight 1.0 ../panels/timeline/widgets/XsDragRight.qml +XsDragBoth 1.0 ../panels/timeline/widgets/XsDragBoth.qml +XsGapItem 1.0 ../panels/timeline/widgets/XsGapItem.qml +XsTrackHeader 1.0 ../panels/timeline/widgets/XsTrackHeader.qml +XsTimelinePanel 1.0 ../panels/timeline/XsTimelinePanel.qml +XsTimelinePanelHeader 1.0 ../panels/timeline/XsTimelinePanelHeader.qml -XsTimelinePanel 1.0 ../panels/XsTimelinePanel.qml XsDelegateMedia 1.0 ../panels/media_list/delegates/XsDelegateMedia.qml XsMediaPaneListHeader 1.0 ../panels/media_list/XsMediaPaneListHeader.qml XsMediaPanelListView 1.0 ../panels/media_list/XsMediaPanelListView.qml @@ -173,6 +194,8 @@ XsVerticalPaneDivider 1.0 ../panels/panel_layouts/XsVerticalPaneDivider.qm XsPlayerTabs 1.0 ../player/XsPlayerTabs.qml XsPlayerWidget 1.0 ../player/XsPlayerWidget.qml +XsLightPlayerWidget 1.0 ../player/XsLightPlayerWidget.qml +XsLightPlayerWindow 1.0 ../player/XsLightPlayerWindow.qml XsPlayerWindow 1.0 ../player/XsPlayerWindow.qml XsPopoutViewerWidget 1.0 ../player/XsPopoutViewerWidget.qml XsSessionWidget 1.0 ../player/XsSessionWidget.qml diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..d543c04d7 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,34 @@ +{ + "name": "xstudio", + "version": "1.0.0", + "dependencies": [ + "stduuid", + "reproc", + "nlohmann-json", + "glew", + "freetype", + "pybind11", + "python3", + "spdlog", + "fmt", + "lcms", + "caf", + "opencolorio", + "openimageio", + { + "name": "ffmpeg", + "features": ["all"] + + } + ], + "builtin-baseline": "dafef74af53669ef1cc9015f55e0ce809ead62aa", + "overrides": [ + { "name": "openimageio", "version": "2.4.14.0#3" }, + { "name": "opencolorio", "version": "2.2.1#1" }, + { "name": "caf", "version": "0.18.5" }, + { "name": "fmt", "version": "8.0.1" }, + { "name": "ffmpeg", "version": "5.1.2#6" }, + { "name": "python3", "version": "3.10.7#7" }, + { "name":"boost-modular-build-helper","version":"1.84.0#3" } + ] +} diff --git a/xStudioConfig.cmake.in b/xStudioConfig.cmake.in new file mode 100644 index 000000000..b085bece6 --- /dev/null +++ b/xStudioConfig.cmake.in @@ -0,0 +1,18 @@ +# - Config file for the xStudio package +# It defines the following variables +# xStudio_INCLUDE_DIRS - include directories for xStudio +# xStudio_LIBRARIES - libraries to link against +# xStudio_EXECUTABLE - the bar executable + +# Compute paths +get_filename_component(xStudio_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +set(xStudio_INCLUDE_DIRS "@CONF_INCLUDE_DIRS@") + +# Our library dependencies (contains definitions for IMPORTED targets) +if(NOT TARGET foo AND NOT xStudio_BINARY_DIR) + include("${xStudio_CMAKE_DIR}/xStudioTargets.cmake") +endif() + +# These are IMPORTED targets created by xStudioTargets.cmake +set(xStudio_LIBRARIES foo) +set(xStudio_EXECUTABLE bar) \ No newline at end of file