From 246e335d30a3037bb3540c6347ee7b04b2ea4395 Mon Sep 17 00:00:00 2001 From: Al Crate Date: Mon, 7 Aug 2023 07:44:58 +0100 Subject: [PATCH 01/42] Bug fixes (#51) * Bug fixes Signed-off-by: Al Crate --- CHANGELOG.md | 9 --- CMakeLists.txt | 2 +- docs/build_guides/centos_7.md | 2 +- docs/build_guides/rocky_linux_9_1.md | 2 +- include/xstudio/ui/qml/session_model_ui.hpp | 2 +- src/media/src/media_source_actor.cpp | 3 +- .../dneg/shotgun/src/data_source_shotgun.cpp | 7 +- .../src/qml/Shotgun.1/ShotgunPublishNotes.qml | 2 - .../shotgun/src/qml/Shotgun.1/ShotgunRoot.qml | 19 +++--- .../media_reader/openexr/src/openexr.cpp | 11 ++-- .../src/plugin_manager_actor.cpp | 66 +++++++++---------- .../session/src/session_model_handler_ui.cpp | 44 +++++++++++-- .../session/src/session_model_methods_ui.cpp | 2 +- src/ui/qml/session/src/session_model_ui.cpp | 8 +++ ui/qml/xstudio/menus/XsMediaMenu.qml | 3 + .../panels/playlist/XsPlaylistsPanelNew.qml | 28 ++++++-- 16 files changed, 136 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584ba8795..8b1378917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1 @@ - - - - - - - - - diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ecc79b52..ba40ec9b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12 FATAL_ERROR) -set(XSTUDIO_GLOBAL_VERSION "0.11.0" CACHE STRING "Version string") +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) diff --git a/docs/build_guides/centos_7.md b/docs/build_guides/centos_7.md index 60a435958..32fc7a494 100644 --- a/docs/build_guides/centos_7.md +++ b/docs/build_guides/centos_7.md @@ -46,7 +46,7 @@ Install 5.15 dev tools, using Qt5 online installer, requires login account (free wget https://github.com/nlohmann/json/archive/refs/tags/v3.11.2.tar.gz tar -xf v3.11.2.tar.gz mkdir json-3.11.2/build - cd json-3.7.3/build + cd json-3.11.2/build cmake .. -DJSON_BuildTests=Off make -j $JOBS sudo make install diff --git a/docs/build_guides/rocky_linux_9_1.md b/docs/build_guides/rocky_linux_9_1.md index 1db2a0803..09f9a21e4 100644 --- a/docs/build_guides/rocky_linux_9_1.md +++ b/docs/build_guides/rocky_linux_9_1.md @@ -28,7 +28,7 @@ wget https://github.com/nlohmann/json/archive/refs/tags/v3.11.2.tar.gz tar -xf v3.11.2.tar.gz mkdir json-3.11.2/build - cd json-3.7.3/build + cd json-3.11.2/build cmake .. -DJSON_BuildTests=Off make -j $JOBS sudo make install diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index fd1805297..67e143506 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -210,6 +210,7 @@ class SessionModel : public caf::mixin::actor_object { Q_INVOKABLE void relinkMedia(const QModelIndexList &indexes, const QUrl &path); Q_INVOKABLE void decomposeMedia(const QModelIndexList &indexes); Q_INVOKABLE void rescanMedia(const QModelIndexList &indexes); + Q_INVOKABLE QModelIndex getPlaylistIndex(const QModelIndex &index) const; signals: void bookmarkActorAddrChanged(); @@ -296,7 +297,6 @@ class SessionModel : public caf::mixin::actor_object { const utility::JsonTree &tree, const QPersistentModelIndex &search_hint = QModelIndex()); utility::Uuid refreshId(nlohmann::json &ij); - QModelIndex getPlaylistIndex(const QModelIndex &index) const; QFuture> handleMediaIdDropFuture( const int proposedAction, const utility::JsonStore &drop, const QModelIndex &index); diff --git a/src/media/src/media_source_actor.cpp b/src/media/src/media_source_actor.cpp index c46440838..805c8eb35 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -129,7 +129,8 @@ MediaSourceActor::MediaSourceActor( base_.set_media_reference(mr); - anon_send(actor_cast(this), acquire_media_detail_atom_v, media_reference.rate()); + // special case , when duplicating, as that'll suppy streams. + // anon_send(actor_cast(this), acquire_media_detail_atom_v, media_reference.rate()); init(); } 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 890e275d6..199325d7b 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 @@ -757,7 +757,12 @@ ShotgunDataSourceActor::ShotgunDataSourceActor( // do we need the UI to have spun up before we can issue calls to shotgun... // erm... - [=](use_data_atom, const caf::uri &uri) -> result { + [=](use_data_atom atom, const caf::uri &uri) { + delegate(actor_cast(this), atom, uri, FrameRate()); + }, + [=](use_data_atom, + const caf::uri &uri, + const FrameRate &media_rate) -> result { // check protocol == shotgun.. if (uri.scheme() != "shotgun") return UuidActorVector(); 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 ceed5d469..b7b5c3ce7 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 @@ -181,9 +181,7 @@ XsWindow { ) publishNotesDialog.payload = tmp - // console.log(tmp) publish_func(tmp, playlist_uuid) - // onAccepted: push_playlist_note_promise(data_source, payload, playlist, playlist_uuid, error) } } diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunRoot.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunRoot.qml index 9f0e5d961..d558186b3 100644 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunRoot.qml +++ b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunRoot.qml @@ -992,16 +992,15 @@ Item { let inds = app_window.sessionSelectionModel.selectedIndexes inds.forEach( function (item, index) { - let type = item.model.get(item, "typeRole") - if(type == "Playlist") { - // get playlist name - let uuid = item.model.get(item, "actorUuidRole") - - push_notes_dialog.publishSelected = selected_media - push_notes_dialog.playlist_uuid = uuid - push_notes_dialog.updatePublish() - push_notes_dialog.show() - } + // find playlist.. + let plind = app_window.sessionModel.getPlaylistIndex(item) + // get playlist name + let uuid = item.model.get(plind, "actorUuidRole") + + push_notes_dialog.publishSelected = selected_media + push_notes_dialog.playlist_uuid = uuid + push_notes_dialog.updatePublish() + push_notes_dialog.show() } ) } diff --git a/src/plugin/media_reader/openexr/src/openexr.cpp b/src/plugin/media_reader/openexr/src/openexr.cpp index 04fab4fd8..582f16c15 100644 --- a/src/plugin/media_reader/openexr/src/openexr.cpp +++ b/src/plugin/media_reader/openexr/src/openexr.cpp @@ -580,22 +580,23 @@ xstudio::media::MediaDetail OpenEXRMediaReader::detail(const caf::uri &uri) cons else if (timecode_rate) fr = static_cast(timecode_rate->value()); - if (fr == 0.0) - fr = 24.0; - if (timecode) { + // note if frame rate is no known from metadata we use 24pfs + // as a default tc = utility::Timecode( timecode->value().hours(), timecode->value().minutes(), timecode->value().seconds(), timecode->value().frame(), - fr, + fr == 0.0 ? 24.0 : fr, timecode->value().dropFrame()); } else if (rate) { tc = utility::Timecode("00:00:00:00", fr); } - frd.set_rate(utility::FrameRate(1.0 / fr)); + // if frame rate is not known, return null frame rate so xSTUDIO will + // apply its global frame rate + frd.set_rate(fr == 0.0 ? utility::FrameRate() : utility::FrameRate(1.0 / fr)); std::vector stream_ids; for (int prt = 0; prt < parts; ++prt) { diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index a73027ce3..9ca48dc1e 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -46,6 +46,39 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base delegate(actor_cast(this), json_store::update_atom_v, full); }, + // helper for dealing with URI's + [=](data_source::use_data_atom, + const caf::uri &uri, + const FrameRate &media_rate) -> result { + // send to resident enabled datasource plugins + auto actors = std::vector(); + + for (const auto &i : manager_.factories()) { + if (i.second.factory()->type() == PluginType::PT_DATA_SOURCE and + resident_.count(i.first)) + actors.push_back(resident_[i.first]); + } + + if (actors.empty()) + return UuidActorVector(); + + auto rp = make_response_promise(); + + fan_out_request( + actors, infinite, data_source::use_data_atom_v, uri, media_rate) + .then( + [=](const std::vector results) mutable { + for (const auto &i : results) { + if (not i.empty()) + return rp.deliver(i); + } + rp.deliver(UuidActorVector()); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + + return rp; + }, + // helper for dealing with Media sources back population's [=](data_source::use_data_atom, const caf::actor &media, @@ -166,39 +199,6 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base return rp; }, - // helper for dealing with URI's - [=](data_source::use_data_atom, - const caf::uri &uri, - const FrameRate &media_rate) -> result { - // send to resident enabled datasource plugins - auto actors = std::vector(); - - for (const auto &i : manager_.factories()) { - if (i.second.factory()->type() == PluginType::PT_DATA_SOURCE and - resident_.count(i.first)) - actors.push_back(resident_[i.first]); - } - - if (actors.empty()) - return UuidActorVector(); - - auto rp = make_response_promise(); - - fan_out_request( - actors, infinite, data_source::use_data_atom_v, uri, media_rate) - .then( - [=](const std::vector results) mutable { - for (const auto &i : results) { - if (not i.empty()) - return rp.deliver(i); - } - rp.deliver(UuidActorVector()); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - }, - [=](json_store::update_atom, const JsonStore &js) { try { // this will trash manually enabled/disabled plugins. 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 4acd2eaca..52cf27944 100644 --- a/src/ui/qml/session/src/session_model_handler_ui.cpp +++ b/src/ui/qml/session/src/session_model_handler_ui.cpp @@ -643,7 +643,39 @@ void SessionModel::init(caf::actor_system &_system) { } }, - [=](utility::event_atom, media::add_media_stream_atom, const UuidActor &) {}, + + // NEVER TRIGGERS ? NOT TESTED ! + [=](utility::event_atom, media::add_media_stream_atom, const UuidActor &) { + // spdlog::warn("media::add_media_stream_atom"); + try { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + auto indexes = search_recursive_list( + QVariant::fromValue(QStringFromStd(src_str)), + actorRole, + QModelIndex(), + 0, + 1); + + if (not indexes.empty() and indexes[0].isValid()) { + const nlohmann::json &j = indexToData(indexes[0]); + + // spdlog::warn("media::add_media_stream_atom REQUEST"); + + requestData( + QVariant::fromValue(QStringFromStd(src_str)), + actorRole, + getPlaylistIndex(indexes[0]), + j, + childrenRole); + } else { + spdlog::warn("FAIELD"); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, [=](utility::event_atom, playlist::move_media_atom, @@ -656,18 +688,22 @@ void SessionModel::init(caf::actor_system &_system) { auto index = search_recursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); - spdlog::info("utility::event_atom, utility::move_media_atom {}", src_str); + // spdlog::info("utility::event_atom, utility::move_media_atom {}", + // src_str); // trigger update of model.. if (index.isValid()) { const nlohmann::json &j = indexToData(index); + // spdlog::warn("{}", j.dump(2)); if (j.at("type") == "Subset" or j.at("type") == "Timeline") { - auto media_id = j.at("children").at(0).at("id"); + const auto tree = *(indexToTree(index)->child(0)); + auto media_id = tree.data().at("id"); + requestData( QVariant::fromValue(QUuidFromUuid(media_id)), idRole, index, - j.at("children").at(0), + tree.data(), childrenRole); } } 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 9f17f0830..f65f864b5 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -24,7 +24,7 @@ QVariant SessionModel::playlists() const { auto data = R"([])"_json; try { auto value = R"({"text": null, "uuid":null})"_json; - for (const auto &i : data_) { + for (const auto &i : *(data_.child(0))) { value["text"] = i.data().at("name"); value["uuid"] = i.data().at("actor_uuid"); data.push_back(value); diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index 0686c4300..f5ac923eb 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -194,6 +194,14 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & // spdlog::warn("processChildren {} {} {}", type, ptree->data().dump(2), rj.dump(2)); // spdlog::warn("processChildren {}", tree_to_json( *ptree,"children").dump(2)); // spdlog::warn("processChildren {}", rj.dump(2)); + if (type == "MediaSource" and rj.at(0).at("children").empty() and + rj.at(1).at("children").empty()) { + // spdlog::warn("RETRY {}", rj.dump(2)); + // force retry + emit dataChanged(parent_index, parent_index, roles); + return; + } + try { if (type == "Session" or type == "Container List" or type == "Media" or diff --git a/ui/qml/xstudio/menus/XsMediaMenu.qml b/ui/qml/xstudio/menus/XsMediaMenu.qml index ad3d31816..7d024a774 100644 --- a/ui/qml/xstudio/menus/XsMediaMenu.qml +++ b/ui/qml/xstudio/menus/XsMediaMenu.qml @@ -116,6 +116,9 @@ XsMenu { fakeDisabled: false Repeater { model: app_window.mediaImageSource.streams + onItemAdded: stream_menu.insertItem(index, item) + onItemRemoved: stream_menu.removeItem(item) + XsMenuItem { mytext: modelData.name enabled: true diff --git a/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml b/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml index 4721f1f22..aa0bf2289 100644 --- a/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml +++ b/ui/qml/xstudio/panels/playlist/XsPlaylistsPanelNew.qml @@ -11,6 +11,7 @@ import QuickFuture 1.0 import QuickPromise 1.0 import xStudio 1.1 +import xstudio.qml.helpers 1.0 Rectangle { id: panel @@ -132,14 +133,33 @@ Rectangle { } if("xstudio/media-ids" in data) { - media_move_copy_dialog.data = data + let before = null + let internal_copy = false if(previousItem) { - media_move_copy_dialog.index = previousItem.modelIndex() + before = previousItem.modelIndex() + } + + // does media exist in our parent. + if(before) { + let mi = app_window.sessionModel.search_recursive( + helpers.QVariantFromUuidString(data["xstudio/media-ids"].split("\n")[0]), "idRole" + ) + + if(app_window.sessionModel.getPlaylistIndex(before) == app_window.sessionModel.getPlaylistIndex(mi)) { + internal_copy = true + } + } + + if(internal_copy) { + Future.promise( + app_window.sessionModel.handleDropFuture(Qt.CopyAction, data, before) + ).then(function(quuids){}) } else { - media_move_copy_dialog.index = null + media_move_copy_dialog.data = data + media_move_copy_dialog.index = before + media_move_copy_dialog.open() } - media_move_copy_dialog.open() } else { if(previousItem) { From 14cd9cc6922d9cc50e58aa01408811eeb36962ae Mon Sep 17 00:00:00 2001 From: Nathan Rusch Date: Thu, 21 Sep 2023 11:56:47 -0700 Subject: [PATCH 02/42] FindFFMPEG: Fix version parsing to check for `version_major.h` Signed-off-by: Nathan Rusch --- cmake/modules/FindFFMPEG.cmake | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cmake/modules/FindFFMPEG.cmake b/cmake/modules/FindFFMPEG.cmake index d03741754..85e3fa76d 100644 --- a/cmake/modules/FindFFMPEG.cmake +++ b/cmake/modules/FindFFMPEG.cmake @@ -125,11 +125,23 @@ function (_ffmpeg_find component headername) set("FFMPEG_${component}_FOUND" 1 PARENT_SCOPE) + string(TOUPPER "${component}" component_upper) + + # Check for `version_major.h` + set(version_major "") + set(version_major_header_path "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version_major.h") + if (EXISTS "${version_major_header_path}") + file(STRINGS "${version_major_header_path}" version_major + REGEX "#define *LIB${component_upper}_VERSION_MAJOR ") + endif () + + # Check for `version.h` set(version_header_path "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version.h") if (EXISTS "${version_header_path}") - string(TOUPPER "${component}" component_upper) file(STRINGS "${version_header_path}" version REGEX "#define *LIB${component_upper}_VERSION_(MAJOR|MINOR|MICRO) ") + + set(version "${version_major} ${version}") string(REGEX REPLACE ".*_MAJOR *\([0-9]*\).*" "\\1" major "${version}") string(REGEX REPLACE ".*_MINOR *\([0-9]*\).*" "\\1" minor "${version}") string(REGEX REPLACE ".*_MICRO *\([0-9]*\).*" "\\1" micro "${version}") From b33162c1eaf9b191c157a90f887229df6816ea5e Mon Sep 17 00:00:00 2001 From: adro79 <57686179+adro79@users.noreply.github.com> Date: Wed, 29 Mar 2023 23:13:00 +0200 Subject: [PATCH 03/42] Correct syntax Delete the z in av_malloc_array Signed-off-by: adro79 <57686179+adro79@users.noreply.github.com> Signed-off-by: Michael Kessler --- src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index e25b49eb0..47e8d39e4 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -102,7 +102,7 @@ AVDictionary **init_find_stream_opts(AVFormatContext *avfc, AVDictionary *codec_ AVDictionary **result = nullptr; if (avfc->nb_streams) { - result = (AVDictionary **)av_mallocz_array(avfc->nb_streams, sizeof(*result)); + result = (AVDictionary **)av_malloc_array(avfc->nb_streams, sizeof(*result)); if (result) { for (unsigned int i = 0; i < avfc->nb_streams; i++) From 2c4bcff77080be5c63d041f77a551f7b08199abd Mon Sep 17 00:00:00 2001 From: Mark Reid Date: Fri, 13 Oct 2023 13:56:18 -0700 Subject: [PATCH 04/42] Add missing Imath dependency Signed-off-by: Mark Reid Signed-off-by: Michael Kessler --- src/utility/src/CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/utility/src/CMakeLists.txt b/src/utility/src/CMakeLists.txt index 17c6e3ec8..0a21b08a3 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -1,6 +1,14 @@ +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) + SET(LINK_DEPS caf::core fmt::fmt + Imath::Imath nlohmann_json::nlohmann_json reproc++ spdlog::spdlog @@ -11,6 +19,7 @@ SET(LINK_DEPS SET(STATIC_LINK_DEPS caf::core fmt::fmt + Imath::Imath nlohmann_json::nlohmann_json reproc++ spdlog::spdlog @@ -18,10 +27,5 @@ SET(STATIC_LINK_DEPS uuid ) -find_package(spdlog REQUIRED) -find_package(fmt REQUIRED) -find_package(nlohmann_json REQUIRED) -find_package(PkgConfig REQUIRED) -pkg_search_module(UUID REQUIRED uuid) create_component_static(utility 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") From 57d796f970a18dbfc495fad28c8a8deeaedc39d3 Mon Sep 17 00:00:00 2001 From: Mark Reid Date: Fri, 13 Oct 2023 13:57:50 -0700 Subject: [PATCH 05/42] Added missing glew dependency Signed-off-by: Mark Reid Signed-off-by: Michael Kessler --- src/plugin/colour_pipeline/ocio/src/CMakeLists.txt | 2 ++ src/plugin/media_reader/blank/src/CMakeLists.txt | 3 +++ src/plugin/media_reader/ffmpeg/src/CMakeLists.txt | 3 ++- src/plugin/media_reader/openexr/src/CMakeLists.txt | 2 ++ src/plugin/media_reader/ppm/src/CMakeLists.txt | 3 +++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt b/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt index 92e523a01..2ae01c3ec 100644 --- a/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt +++ b/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(OpenColorIO CONFIG) find_package(OpenEXR) find_package(Imath) +find_package(GLEW REQUIRED) # Temporary hack - OCIO package is not found/defined by 'modern' .cmake # package and as such cmake doesn't distinguish the OCIO headers as system @@ -11,6 +12,7 @@ include_directories(SYSTEM ${OCIO_INCLUDE_DIRS}) SET(LINK_DEPS xstudio::colour_pipeline xstudio::module + GLEW::GLEW OpenColorIO::OpenColorIO OpenEXR::OpenEXR Imath::Imath diff --git a/src/plugin/media_reader/blank/src/CMakeLists.txt b/src/plugin/media_reader/blank/src/CMakeLists.txt index f49af325d..5fb4caf17 100644 --- a/src/plugin/media_reader/blank/src/CMakeLists.txt +++ b/src/plugin/media_reader/blank/src/CMakeLists.txt @@ -1,5 +1,8 @@ +find_package(GLEW REQUIRED) + SET(LINK_DEPS xstudio::media_reader + GLEW::GLEW ) create_plugin_with_alias(media_reader_blank xstudio::media_reader::blank 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt b/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt index ab1e4c3fd..8e3880ae2 100644 --- a/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt +++ b/src/plugin/media_reader/ffmpeg/src/CMakeLists.txt @@ -1,7 +1,7 @@ project(media_reader_ffmpeg VERSION 0.1.0 LANGUAGES CXX) find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale avutil) - +find_package(GLEW REQUIRED) set(SOURCES ffmpeg_stream.cpp @@ -22,6 +22,7 @@ target_compile_options(${PROJECT_NAME} PRIVATE -Wfatal-errors) target_link_libraries(${PROJECT_NAME} PUBLIC xstudio::media_reader + GLEW::GLEW FFMPEG::avcodec FFMPEG::avformat FFMPEG::swscale diff --git a/src/plugin/media_reader/openexr/src/CMakeLists.txt b/src/plugin/media_reader/openexr/src/CMakeLists.txt index 69474bb7e..330fc8d0b 100644 --- a/src/plugin/media_reader/openexr/src/CMakeLists.txt +++ b/src/plugin/media_reader/openexr/src/CMakeLists.txt @@ -1,8 +1,10 @@ find_package(OpenEXR) find_package(Imath) +find_package(GLEW) SET(LINK_DEPS xstudio::media_reader + GLEW::GLEW OpenEXR::OpenEXR Imath::Imath ) diff --git a/src/plugin/media_reader/ppm/src/CMakeLists.txt b/src/plugin/media_reader/ppm/src/CMakeLists.txt index 02545c780..a4be192ab 100644 --- a/src/plugin/media_reader/ppm/src/CMakeLists.txt +++ b/src/plugin/media_reader/ppm/src/CMakeLists.txt @@ -1,5 +1,8 @@ +find_package(GLEW REQUIRED) + SET(LINK_DEPS xstudio::media_reader + GLEW::GLEW ) create_plugin_with_alias(media_reader_ppm xstudio::media_reader::ppm 0.1.0 "${LINK_DEPS}") From 9e7012e3695affe4899f6b081db07e6b33d6411c Mon Sep 17 00:00:00 2001 From: Mark Reid Date: Thu, 23 Nov 2023 12:56:05 -0800 Subject: [PATCH 06/42] Allow setting CAF_ROOT_DIR Signed-off-by: Mark Reid Signed-off-by: Michael Kessler --- cmake/modules/FindCAF.cmake | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmake/modules/FindCAF.cmake b/cmake/modules/FindCAF.cmake index f5e20fdef..b0dc3bac4 100644 --- a/cmake/modules/FindCAF.cmake +++ b/cmake/modules/FindCAF.cmake @@ -18,8 +18,6 @@ # CAF_LIBRARY_$C Library file for component $C # CAF_INCLUDE_DIR_$C Include path for component $C -set(CAF_ROOT_DIR "/user_data/.tmp/caf") - if(CAF_FIND_COMPONENTS STREQUAL "") message(FATAL_ERROR "FindCAF requires at least one COMPONENT.") endif() From b188e51e84aa1066c6289f789527b2a750f8d518 Mon Sep 17 00:00:00 2001 From: Nathan Rusch Date: Thu, 21 Sep 2023 11:40:26 -0700 Subject: [PATCH 07/42] FindFFMPEG: Support linking against static FFMPEG libraries on Linux Signed-off-by: Nathan Rusch Signed-off-by: Michael Kessler --- cmake/modules/FindFFMPEG.cmake | 45 ++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/cmake/modules/FindFFMPEG.cmake b/cmake/modules/FindFFMPEG.cmake index 85e3fa76d..ad5701f70 100644 --- a/cmake/modules/FindFFMPEG.cmake +++ b/cmake/modules/FindFFMPEG.cmake @@ -58,6 +58,11 @@ Note that only components requested with `COMPONENTS` or `OPTIONAL_COMPONENTS` are guaranteed to set these variables or provide targets. #]==] +if (UNIX AND NOT APPLE) + set(LINUX TRUE) + find_package(PkgConfig) +endif () + function (_ffmpeg_find component headername) find_path("FFMPEG_${component}_INCLUDE_DIR" NAMES @@ -119,8 +124,44 @@ function (_ffmpeg_find component headername) add_library("FFMPEG::${component}" UNKNOWN IMPORTED) set_target_properties("FFMPEG::${component}" PROPERTIES IMPORTED_LOCATION "${FFMPEG_${component}_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIR}" - IMPORTED_LINK_INTERFACE_LIBRARIES "${_deps_link}") + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIR}") + + if (LINUX) + # Check if the found component is a static library. + get_filename_component(_ffmpeg_${component}_ext + "${FFMPEG_${component}_LIBRARY}" EXT) + string(COMPARE EQUAL + "${_ffmpeg_${component}_ext}" + ".a" + _ffmpeg_${component}_static) + + if (_ffmpeg_${component}_static) + # This is necessary to link against static ffmpeg libraries. + # See https://www.ffmpeg.org/platform.html#Advanced-linking-configuration + set_target_properties("FFMPEG::${component}" PROPERTIES + INTERFACE_LINK_OPTIONS "-Wl,-Bsymbolic") + + if (PKG_CONFIG_FOUND) + # Use `pkg-config` to find transitive dependencies of the ffmpeg + # libraries, to facilitate proper static linking with codec- + # specific libraries. + pkg_search_module("_ffmpeg_${component}_pkgconfig" + "${CMAKE_STATIC_LIBRARY_PREFIX}${component}") + + if (${_ffmpeg_${component}_pkgconfig_FOUND}) + set_target_properties("FFMPEG::${component}" PROPERTIES + INTERFACE_LINK_DIRECTORIES "${_ffmpeg_${component}_pkgconfig_LIBRARY_DIRS}") + + foreach (_ffmpeg_${component}_dep IN LISTS _ffmpeg_${component}_pkgconfig_LIBRARIES) + list(APPEND _deps_link "${_ffmpeg_${component}_dep}") + endforeach () + list(REMOVE_DUPLICATES _deps_link) + endif () + endif () + endif () + endif () + set_target_properties("FFMPEG::${component}" PROPERTIES + INTERFACE_LINK_LIBRARIES "${_deps_link}") endif () set("FFMPEG_${component}_FOUND" 1 PARENT_SCOPE) From 64d34b0587ae0ed4d74fa32914c31082fc9db7c0 Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Thu, 28 Mar 2024 14:42:57 +0000 Subject: [PATCH 08/42] Update for 0.12 Signed-off-by: Michael Kessler --- CMakeLists.txt | 29 +- NOTICE.TXT | 29 +- README.md | 12 +- cmake/macros.cmake | 17 +- cmake/modules/FindCAF.cmake | 2 +- docs/conf.py | 4 +- extern/include/strict_fstream.hpp | 237 ++ extern/include/zstr.hpp | 502 +++ include/xstudio/atoms.hpp | 167 +- include/xstudio/audio/audio_output.hpp | 51 +- include/xstudio/audio/audio_output_actor.hpp | 223 +- .../audio/linux_audio_output_device.hpp | 52 + include/xstudio/bookmark/bookmark.hpp | 28 +- .../colour_pipeline/colour_operation.hpp | 33 +- .../colour_pipeline/colour_pipeline.hpp | 56 +- .../colour_pipeline/colour_pipeline_actor.hpp | 4 +- .../colour_pipeline/colour_texture.hpp | 26 + .../xstudio/conform/conform_manager_actor.hpp | 40 + include/xstudio/conform/conformer.hpp | 157 + include/xstudio/data_source/data_source.hpp | 2 +- include/xstudio/global_store/global_store.hpp | 12 +- include/xstudio/history/history.hpp | 2 + include/xstudio/history/history_actor.hpp | 123 + include/xstudio/media/media.hpp | 56 +- include/xstudio/media/media_actor.hpp | 9 +- include/xstudio/media_hook/media_hook.hpp | 2 +- .../xstudio/media_metadata/media_metadata.hpp | 2 +- include/xstudio/media_reader/image_buffer.hpp | 36 +- ...edia_detail_and_thumbnail_reader_actor.hpp | 1 - include/xstudio/media_reader/media_reader.hpp | 2 +- .../media_reader/media_reader_actor.hpp | 2 + include/xstudio/module/attribute.hpp | 10 +- include/xstudio/module/module.hpp | 38 +- include/xstudio/playhead/playhead.hpp | 40 +- include/xstudio/playhead/playhead_actor.hpp | 2 - .../playhead/playhead_global_events_actor.hpp | 2 +- include/xstudio/playhead/sub_playhead.hpp | 26 +- include/xstudio/plugin_manager/enums.hpp | 28 +- .../xstudio/plugin_manager/plugin_base.hpp | 99 +- .../xstudio/plugin_manager/plugin_factory.hpp | 5 +- .../xstudio/plugin_manager/plugin_manager.hpp | 4 +- .../xstudio/plugin_manager/plugin_utility.hpp | 2 +- .../xstudio/shotgun_client/shotgun_client.hpp | 26 +- include/xstudio/studio/studio_actor.hpp | 6 + include/xstudio/timeline/clip.hpp | 9 +- include/xstudio/timeline/clip_actor.hpp | 1 + include/xstudio/timeline/gap.hpp | 1 + include/xstudio/timeline/item.hpp | 63 +- include/xstudio/timeline/stack.hpp | 1 + include/xstudio/timeline/stack_actor.hpp | 22 + include/xstudio/timeline/timeline.hpp | 20 + include/xstudio/timeline/timeline_actor.hpp | 16 + include/xstudio/timeline/track.hpp | 1 + include/xstudio/timeline/track_actor.hpp | 51 + include/xstudio/ui/canvas/canvas.hpp | 269 ++ .../xstudio/ui/canvas/canvas_undo_redo.hpp | 48 + include/xstudio/ui/canvas/caption.hpp | 66 + include/xstudio/ui/canvas/handle.hpp | 41 + include/xstudio/ui/canvas/stroke.hpp | 61 + include/xstudio/ui/font.hpp | 5 + .../ui/frontend_model/frontend_model_data.hpp | 2 +- .../ui/model_data/model_data_actor.hpp | 29 +- .../ui/opengl/opengl_canvas_renderer.hpp | 53 + .../ui/opengl/opengl_caption_renderer.hpp | 55 + .../ui/opengl/opengl_offscreen_renderer.hpp | 45 + .../ui/opengl/opengl_stroke_renderer.hpp | 45 + .../ui/opengl/opengl_texthandle_renderer.hpp | 39 + .../ui/opengl/opengl_viewport_renderer.hpp | 4 +- .../xstudio/ui/opengl/shader_program_base.hpp | 3 + include/xstudio/ui/opengl/texture.hpp | 9 +- include/xstudio/ui/qml/actor_object.hpp | 4 +- include/xstudio/ui/qml/bookmark_model_ui.hpp | 3 +- include/xstudio/ui/qml/caf_response_ui.hpp | 13 + include/xstudio/ui/qml/helper_ui.hpp | 78 +- include/xstudio/ui/qml/json_tree_model_ui.hpp | 6 +- include/xstudio/ui/qml/model_data_ui.hpp | 24 +- include/xstudio/ui/qml/module_data_ui.hpp | 26 + include/xstudio/ui/qml/module_menu_ui.hpp | 4 + include/xstudio/ui/qml/module_ui.hpp | 4 +- include/xstudio/ui/qml/playhead_ui.hpp | 4 - include/xstudio/ui/qml/qml_viewport.hpp | 28 +- .../xstudio/ui/qml/qml_viewport_renderer.hpp | 27 +- include/xstudio/ui/qml/session_model_ui.hpp | 132 +- .../xstudio/ui/qml/shotgun_provider_ui.hpp | 3 +- include/xstudio/ui/qml/snapshot_model_ui.hpp | 61 + include/xstudio/ui/qml/studio_ui.hpp | 12 +- .../xstudio/ui/qml/thumbnail_provider_ui.hpp | 8 +- include/xstudio/ui/qt/offscreen_viewport.hpp | 89 +- include/xstudio/ui/viewport/enums.hpp | 1 + .../xstudio/ui/viewport/keypress_monitor.hpp | 2 +- include/xstudio/ui/viewport/shader.hpp | 4 +- include/xstudio/ui/viewport/viewport.hpp | 66 +- .../viewport/viewport_frame_queue_actor.hpp | 2 + .../ui/viewport/viewport_renderer_base.hpp | 13 + include/xstudio/utility/chrono.hpp | 5 +- include/xstudio/utility/container.hpp | 2 + include/xstudio/utility/file_system_item.hpp | 141 + include/xstudio/utility/helpers.hpp | 37 +- include/xstudio/utility/json_store.hpp | 8 +- include/xstudio/utility/serialise_headers.hpp | 1 + include/xstudio/utility/tree.hpp | 7 +- include/xstudio/utility/undo_redo.hpp | 20 + include/xstudio/utility/uuid.hpp | 1 + python/src/xstudio/api/app.py | 54 +- python/src/xstudio/api/module.py | 54 +- python/src/xstudio/api/session/media/media.py | 4 +- .../api/session/playlist/timeline/__init__.py | 20 +- .../api/session/playlist/timeline/clip.py | 20 +- .../api/session/playlist/timeline/gap.py | 20 +- .../api/session/playlist/timeline/stack.py | 20 +- .../api/session/playlist/timeline/track.py | 20 +- python/src/xstudio/connection/__init__.py | 30 +- python/src/xstudio/demo/__init__.py | 1 + python/src/xstudio/demo/dump_shots.py | 29 + python/src/xstudio/plugin/plugin_base.py | 49 +- scripts/linting/license_stub_check | 59 + share/preference/core_audio.json | 4 +- share/preference/core_conform.json | 16 + share/preference/core_plugin_manager.json | 8 +- share/preference/core_session.json | 16 + share/preference/core_snapshot.json | 14 + share/preference/plugin_annotations.json | 16 + .../plugin_colour_pipeline_ocio.json | 8 + .../plugin_data_source_shotbrowser.json | 2449 ++++++++++++ .../plugin_data_source_shotgun.json | 2030 +++++----- share/preference/plugin_grading.json | 77 + share/preference/ui_qml.json | 260 +- share/snippets/demo.json | 12 +- src/CMakeLists.txt | 4 +- src/audio/src/audio_output.cpp | 38 +- src/audio/src/audio_output_actor.cpp | 200 +- src/audio/src/linux_audio_output_device.cpp | 2 +- src/bookmark/src/bookmark.cpp | 8 + src/bookmark/src/bookmark_actor.cpp | 26 +- src/bookmark/src/bookmarks_actor.cpp | 28 +- src/colour_pipeline/src/colour_operation.cpp | 11 +- src/colour_pipeline/src/colour_pipeline.cpp | 160 +- .../src/colour_pipeline_actor.cpp | 48 +- src/conform/src/CMakeLists.txt | 7 + src/conform/src/conform_manager_actor.cpp | 269 ++ src/conform/src/conformer.cpp | 18 + src/conform/test/CMakeLists.txt | 7 + .../source_grading_demo/src/grading_demo.cpp | 28 +- .../src/viewer_solarise_effect.cpp | 21 +- src/demos/glx_minimal_demo/src/main.cpp | 5 +- src/global/src/CMakeLists.txt | 1 + src/global/src/global_actor.cpp | 55 +- src/global_store/src/global_store.cpp | 50 +- src/http_client/src/http_client_actor.cpp | 31 +- src/launch/xstudio/src/CMakeLists.txt | 1 + src/launch/xstudio/src/xstudio.cpp | 150 +- .../src/xstudio_desktop_integration.sh | 9 +- src/media/src/media_actor.cpp | 155 +- src/media/src/media_source_actor.cpp | 174 +- src/media/src/media_stream.cpp | 45 +- src/media/src/media_stream_actor.cpp | 21 +- src/media_hook/src/media_hook_actor.cpp | 15 +- .../src/media_metadata_actor.cpp | 7 +- ...edia_detail_and_thumbnail_reader_actor.cpp | 7 +- src/media_reader/src/media_reader_actor.cpp | 35 +- src/module/src/attribute.cpp | 27 +- src/module/src/module.cpp | 518 ++- src/playhead/src/edit_list_actor.cpp | 4 + src/playhead/src/playhead.cpp | 240 +- src/playhead/src/playhead_actor.cpp | 291 +- .../src/playhead_global_events_actor.cpp | 47 +- src/playhead/src/playhead_selection_actor.cpp | 8 +- src/playhead/src/retime_actor.cpp | 5 + src/playhead/src/sub_playhead.cpp | 647 +++- src/playlist/src/playlist_actor.cpp | 21 +- src/plugin/colour_op/CMakeLists.txt | 3 + .../colour_op/grading/src/CMakeLists.txt | 33 + src/plugin/colour_op/grading/src/grading.cpp | 1017 +++++ src/plugin/colour_op/grading/src/grading.h | 166 + .../grading/src/grading_colour_op.cpp | 552 +++ .../grading/src/grading_colour_op.hpp | 71 + .../colour_op/grading/src/grading_data.cpp | 83 + .../colour_op/grading/src/grading_data.h | 116 + .../grading/src/grading_data_serialiser.cpp | 52 + .../grading/src/grading_data_serialiser.hpp | 43 + .../grading/src/grading_mask_gl_renderer.cpp | 210 + .../grading/src/grading_mask_gl_renderer.h | 67 + .../grading/src/grading_mask_render_data.h | 48 + .../colour_op/grading/src/qml/.clang-tidy | 33 + .../colour_op/grading/src/qml/CMakeLists.txt | 14 + .../src/qml/Grading.1/GradingButton.qml | 67 + .../Grading.1/GradingDialog/GradingDialog.qml | 502 +++ .../GradingDialog/GradingHSlider.qml | 217 ++ .../GradingDialog/GradingSliderGroup.qml | 217 ++ .../GradingDialog/GradingSliderSimple.qml | 40 + .../GradingDialog/GradingVSlider.qml | 137 + .../Grading.1/GradingDialog/GradingWheel.qml | 545 +++ .../grading/src/qml/Grading.1/qmldir | 9 + .../qml/MaskTool.1/MaskDialog/MaskDialog.qml | 1085 ++++++ .../MaskTool.1/MaskDialog/ToolSelector.qml | 150 + .../grading/src/qml/MaskTool.1/qmldir | 3 + .../src/serialisers/1.0/serialiser_1_pt_0.cpp | 32 + .../colour_op/grading/test/CMakeLists.txt | 7 + src/plugin/colour_pipeline/ocio/src/ocio.cpp | 84 +- src/plugin/colour_pipeline/ocio/src/ocio.hpp | 24 +- .../colour_pipeline/ocio/src/ocio_ui.cpp | 159 +- .../colour_pipeline/ocio/src/shaders.hpp | 4 +- .../colour_pipeline/ocio/src/ui_text.hpp | 4 + src/plugin/conform/CMakeLists.txt | 1 + .../conform/dneg/shotgun/src/CMakeLists.txt | 6 + .../dneg/shotgun/src/conform_shotgun.cpp | 46 + .../conform/dneg/shotgun/test/CMakeLists.txt | 7 + .../dneg/ivy/src/data_source_ivy.cpp | 18 +- .../dneg/shotgun/src/data_source_shotgun.cpp | 3399 +---------------- .../dneg/shotgun/src/data_source_shotgun.hpp | 240 +- .../dneg/shotgun/src/data_source_shotgun.tcc | 1188 ++++++ .../src/data_source_shotgun_action.tcc | 292 ++ .../shotgun/src/data_source_shotgun_base.cpp | 179 + .../shotgun/src/data_source_shotgun_base.hpp | 88 + .../src/data_source_shotgun_definitions.hpp | 391 ++ .../src/data_source_shotgun_get_actions.tcc | 787 ++++ .../src/data_source_shotgun_post_actions.tcc | 498 +++ .../src/data_source_shotgun_put_actions.tcc | 157 + .../src/data_source_shotgun_query_engine.cpp | 229 ++ .../src/data_source_shotgun_query_engine.hpp | 72 + .../src/data_source_shotgun_worker.cpp | 260 ++ .../src/data_source_shotgun_worker.hpp | 135 + .../dneg/shotgun/src/qml/CMakeLists.txt | 3 + .../qml/Shotgun.1/DelegateChoicePlaylist.qml | 1 + .../qml/Shotgun.1/DelegateChoiceReference.qml | 77 +- .../src/qml/Shotgun.1/DelegateChoiceShot.qml | 11 +- .../src/qml/Shotgun.1/LeftTreeView.qml | 6 +- .../src/qml/Shotgun.1/QueryListView.qml | 43 +- .../shotgun/src/qml/Shotgun.1/SBLeftPanel.qml | 24 +- .../src/qml/Shotgun.1/SBRightPanel.qml | 7 +- .../src/qml/Shotgun.1/ShotgunAuthenticate.qml | 2 +- .../qml/Shotgun.1/ShotgunBrowserDialog.qml | 2 + .../qml/Shotgun.1/ShotgunCreatePlaylist.qml | 4 +- .../src/qml/Shotgun.1/ShotgunHelpers.js | 4 +- .../shotgun/src/qml/Shotgun.1/ShotgunMenu.qml | 8 +- .../src/qml/Shotgun.1/ShotgunPreferences.qml | 2 +- .../src/qml/Shotgun.1/ShotgunPublishNotes.qml | 8 +- .../shotgun/src/qml/Shotgun.1/ShotgunRoot.qml | 223 +- .../src/qml/Shotgun.1/ShotgunTagDialog.qml | 171 + .../qml/Shotgun.1/ShotgunUpdatePlaylist.qml | 6 +- .../dneg/shotgun/src/qml/Shotgun.1/qmldir | 1 + .../src/qml/data_source_shotgun_query_ui.cpp | 1009 +++++ .../qml/data_source_shotgun_requests_ui.cpp | 1012 +++++ .../src/qml/data_source_shotgun_ui.cpp | 2270 +---------- .../src/qml/data_source_shotgun_ui.hpp | 72 +- .../dneg/shotgun/src/qml/shotgun_model_ui.cpp | 532 +-- .../dneg/shotgun/src/qml/shotgun_model_ui.hpp | 20 +- .../exr_data_window/src/exr_data_window.cpp | 4 +- .../exr_data_window/src/exr_data_window.hpp | 2 +- .../image_boundary/src/image_boundary_hud.cpp | 4 +- .../image_boundary/src/image_boundary_hud.hpp | 2 +- .../hud/pixel_probe/src/pixel_probe.cpp | 62 +- .../hud/pixel_probe/src/pixel_probe.hpp | 12 +- .../media_hook/dneg/dnhook/src/dneg.cpp | 194 +- src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp | 5 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 12 + .../media_reader/ffmpeg/src/ffmpeg_stream.hpp | 6 + .../media_reader/openexr/src/openexr.cpp | 334 +- .../media_reader/openexr/src/openexr.hpp | 2 +- .../openexr/src/simple_exr_sampler.hpp | 7 +- src/plugin/utility/dneg/dnrun/src/dnrun.cpp | 100 +- .../annotations/src/CMakeLists.txt | 2 - .../annotations/src/annotation.cpp | 493 +-- .../annotations/src/annotation.hpp | 245 +- .../src/annotation_opengl_renderer.cpp | 693 +--- .../src/annotation_opengl_renderer.hpp | 77 +- .../src/annotation_render_data.hpp | 31 + .../annotations/src/annotations_tool.cpp | 1356 +++---- .../annotations/src/annotations_tool.hpp | 173 +- .../AnnotationsTool.1/AnnotationsButton.qml | 2 +- .../AnnotationsDialog/AnnotationsDialog.qml | 42 +- .../AnnotationsDialog/DrawCategories.qml | 2 +- .../AnnotationsDialog/ShapeCategories.qml | 2 +- .../AnnotationsDialog/TextCategories.qml | 254 +- .../AnnotationsDialog/ToolSelector.qml | 2 +- .../AnnotationsTextItems.qml | 2 +- .../src/serialisers/1.0/serialiser_1_pt_0.cpp | 156 +- .../src/basic_viewport_masking.cpp | 7 +- .../src/basic_viewport_masking.hpp | 2 +- .../BasicViewportMaskButton.qml | 17 +- .../BasicViewportMaskOverlay.qml | 5 +- src/plugin_manager/src/plugin_base.cpp | 461 +-- src/plugin_manager/src/plugin_manager.cpp | 13 +- .../src/plugin_manager_actor.cpp | 40 +- src/plugin_manager/test/plugin_test.cpp | 2 +- src/python_module/src/py_atoms.cpp | 22 +- src/python_module/src/py_messages.cpp | 13 + src/python_module/src/py_plugin.cpp | 20 +- src/python_module/src/py_register.cpp | 12 +- src/session/src/session_actor.cpp | 74 +- .../src/shotgun_client_actor.cpp | 88 +- src/studio/src/studio_actor.cpp | 80 + .../src/thumbnail_disk_cache_actor.cpp | 48 +- src/timeline/src/clip.cpp | 37 +- src/timeline/src/clip_actor.cpp | 116 +- src/timeline/src/gap.cpp | 18 +- src/timeline/src/gap_actor.cpp | 25 + src/timeline/src/item.cpp | 263 +- src/timeline/src/stack.cpp | 17 +- src/timeline/src/stack_actor.cpp | 455 ++- src/timeline/src/timeline.cpp | 18 +- src/timeline/src/timeline_actor.cpp | 861 +++-- src/timeline/src/track.cpp | 18 +- src/timeline/src/track_actor.cpp | 876 ++++- src/timeline/test/stack_actor_test.cpp | 115 +- src/timeline/test/stack_test.cpp | 55 +- src/timeline/test/timeline_actor_test.cpp | 472 +-- src/timeline/test/track_actor_test.cpp | 97 +- src/timeline/test/track_test.cpp | 109 + src/ui/CMakeLists.txt | 1 + src/ui/base/src/font.cpp | 33 + src/ui/canvas/src/CMakeLists.txt | 19 + src/ui/canvas/src/canvas.cpp | 663 ++++ src/ui/canvas/src/canvas_undo_redo.cpp | 30 + src/ui/canvas/src/caption.cpp | 146 + src/ui/canvas/src/stroke.cpp | 168 + src/ui/canvas/test/CMakeLists.txt | 0 src/ui/model_data/src/model_data_actor.cpp | 492 ++- src/ui/opengl/src/CMakeLists.txt | 1 + src/ui/opengl/src/opengl_canvas_renderer.cpp | 42 + src/ui/opengl/src/opengl_caption_renderer.cpp | 173 + .../opengl/src/opengl_offscreen_renderer.cpp | 81 + src/ui/opengl/src/opengl_stroke_renderer.cpp | 352 ++ src/ui/opengl/src/opengl_text_rendering.cpp | 6 +- .../opengl/src/opengl_texthandle_renderer.cpp | 311 ++ .../opengl/src/opengl_viewport_renderer.cpp | 95 +- src/ui/opengl/src/shader_program_base.cpp | 109 +- src/ui/opengl/src/texture.cpp | 11 + src/ui/qml/bookmark/src/bookmark_model_ui.cpp | 21 +- .../src/embedded_python_ui.cpp | 1 + .../src/global_store_model_ui.cpp | 5 +- src/ui/qml/helper/src/CMakeLists.txt | 2 + src/ui/qml/helper/src/helper_ui.cpp | 11 +- src/ui/qml/helper/src/json_tree_model_ui.cpp | 53 +- src/ui/qml/helper/src/model_data_ui.cpp | 225 +- src/ui/qml/helper/src/model_helper_ui.cpp | 89 +- src/ui/qml/helper/src/module_data_ui.cpp | 19 + src/ui/qml/helper/src/snapshot_model_ui.cpp | 213 ++ src/ui/qml/module/src/module_menu_ui.cpp | 30 + src/ui/qml/module/src/module_ui.cpp | 27 +- src/ui/qml/playhead/src/playhead_ui.cpp | 6 +- src/ui/qml/session/src/CMakeLists.txt | 1 + src/ui/qml/session/src/caf_response_ui.cpp | 219 +- .../qml/session/src/session_model_core_ui.cpp | 557 ++- .../session/src/session_model_handler_ui.cpp | 89 +- .../session/src/session_model_manip_ui.cpp | 603 +-- .../session/src/session_model_methods_ui.cpp | 294 +- .../session/src/session_model_timeline_ui.cpp | 887 +++++ src/ui/qml/session/src/session_model_ui.cpp | 312 +- src/ui/qml/studio/src/studio_ui.cpp | 137 +- src/ui/qml/viewport/src/CMakeLists.txt | 2 +- src/ui/qml/viewport/src/qml_viewport.cpp | 143 +- .../viewport/src/qml_viewport_renderer.cpp | 140 +- .../src/offscreen_viewport.cpp | 816 ++-- .../viewport_widget/src/viewport_widget.cpp | 3 +- src/ui/viewport/src/CMakeLists.txt | 1 + src/ui/viewport/src/keypress_monitor.cpp | 23 +- src/ui/viewport/src/viewport.cpp | 701 ++-- .../src/viewport_frame_queue_actor.cpp | 127 +- src/utility/src/CMakeLists.txt | 13 +- src/utility/src/container.cpp | 8 + src/utility/src/file_system_item.cpp | 196 + src/utility/src/helpers.cpp | 35 +- src/utility/src/json_store.cpp | 35 + src/utility/test/file_system_item_test.cpp | 29 + ui/qml/reskin/assets/icons/new/ad_group.svg | 1 + .../assets/icons/new/arrow_selector_tool.svg | 1 + .../assets/icons/new/arrows_outward.svg | 1 + ui/qml/reskin/assets/icons/new/brush.svg | 1 + ui/qml/reskin/assets/icons/new/build_gang.svg | 1 + .../assets/icons/new/center_focus_strong.svg | 1 + .../reskin/assets/icons/new/content_cut.svg | 1 + .../reskin/assets/icons/new/content_paste.svg | 1 + ui/qml/reskin/assets/icons/new/delete.svg | 1 + ui/qml/reskin/assets/icons/new/disabled.svg | 1 + ui/qml/reskin/assets/icons/new/expand.svg | 1 + ui/qml/reskin/assets/icons/new/expand_all.svg | 1 + .../reskin/assets/icons/new/fast_rewind.svg | 1 + ui/qml/reskin/assets/icons/new/filter.svg | 1 + .../reskin/assets/icons/new/format_size.svg | 1 + ui/qml/reskin/assets/icons/new/ink_pen.svg | 1 + ui/qml/reskin/assets/icons/new/input.svg | 1 + ui/qml/reskin/assets/icons/new/laps.svg | 1 + .../reskin/assets/icons/new/library_music.svg | 1 + ui/qml/reskin/assets/icons/new/list.svg | 1 + .../reskin/assets/icons/new/list_default.svg | 11 + .../reskin/assets/icons/new/list_shotgun.svg | 5 + .../reskin/assets/icons/new/list_subset.svg | 5 + ui/qml/reskin/assets/icons/new/list_view.svg | 1 + ui/qml/reskin/assets/icons/new/more_vert.svg | 1 + ui/qml/reskin/assets/icons/new/movie.svg | 1 + ui/qml/reskin/assets/icons/new/open_in.svg | 1 + .../reskin/assets/icons/new/open_in_new.svg | 1 + ui/qml/reskin/assets/icons/new/open_with.svg | 1 + ui/qml/reskin/assets/icons/new/output.svg | 1 + ui/qml/reskin/assets/icons/new/pan.svg | 1 + ui/qml/reskin/assets/icons/new/pause.svg | 1 + .../reskin/assets/icons/new/photo_camera.svg | 1 + ui/qml/reskin/assets/icons/new/rectangle.svg | 1 + ui/qml/reskin/assets/icons/new/redo.svg | 1 + .../reskin/assets/icons/new/repartition.svg | 1 + ui/qml/reskin/assets/icons/new/repeat.svg | 1 + .../reskin/assets/icons/new/reset_image.svg | 1 + ui/qml/reskin/assets/icons/new/reset_tv.svg | 1 + ui/qml/reskin/assets/icons/new/restart.svg | 1 + ui/qml/reskin/assets/icons/new/search_off.svg | 1 + ui/qml/reskin/assets/icons/new/settings.svg | 1 + ui/qml/reskin/assets/icons/new/skip.svg | 1 + ui/qml/reskin/assets/icons/new/sort.svg | 1 + .../reskin/assets/icons/new/splitscreen.svg | 1 + .../reskin/assets/icons/new/splitscreen2.svg | 1 + .../reskin/assets/icons/new/sticky_note.svg | 1 + ui/qml/reskin/assets/icons/new/sync.svg | 1 + ui/qml/reskin/assets/icons/new/theaters.svg | 1 + ui/qml/reskin/assets/icons/new/trending.svg | 1 + ui/qml/reskin/assets/icons/new/triangle.svg | 1 + ui/qml/reskin/assets/icons/new/tune.svg | 1 + ui/qml/reskin/assets/icons/new/undo.svg | 1 + ui/qml/reskin/assets/icons/new/upload.svg | 1 + .../assets/icons/new/variables_insert.svg | 1 + ui/qml/reskin/assets/icons/new/view.svg | 1 + ui/qml/reskin/assets/icons/new/view_grid.svg | 1 + .../reskin/assets/icons/new/volume_down.svg | 1 + .../reskin/assets/icons/new/volume_mute.svg | 1 + .../assets/icons/new/volume_no_sound.svg | 1 + ui/qml/reskin/assets/icons/new/volume_up.svg | 1 + ui/qml/reskin/assets/icons/new/zoom_in.svg | 1 + .../assets/icons/retired/brush_w500.svg | 1 + .../assets/icons/retired/delete_w500.svg | 1 + .../icons/retired/fast_forward_w500.svg | 1 + .../assets/icons/retired/fast_rewind_w500.svg | 1 + .../assets/icons/retired/open_in_new_w500.svg | 1 + .../assets/icons/retired/open_with_w500.svg | 1 + .../reskin/assets/icons/retired/pan_w500.svg | 1 + .../assets/icons/retired/pause_w500.svg | 1 + .../icons/retired/photo_camera_w500.svg | 1 + .../assets/icons/retired/play_arrow_w500.svg | 1 + .../assets/icons/retired/repeat_w500.svg | 1 + .../assets/icons/retired/restart_w500.svg | 1 + .../assets/icons/retired/search_w500.svg | 1 + .../assets/icons/retired/skip_next_w500.svg | 1 + .../icons/retired/skip_previous_w500.svg | 1 + .../assets/icons/retired/sticky_note_w500.svg | 1 + .../reskin/assets/icons/retired/sync_w500.svg | 1 + .../assets/icons/retired/trending_w500.svg | 1 + .../reskin/assets/icons/retired/tune_w500.svg | 1 + .../assets/icons/retired/view_grid_w500.svg | 1 + .../reskin/assets/icons/retired/view_w500.svg | 1 + .../assets/icons/retired/volume_down_w500.svg | 1 + .../assets/icons/retired/volume_mute_w500.svg | 1 + .../icons/retired/volume_no_sound_w500.svg | 1 + .../assets/icons/retired/volume_up_w500.svg | 1 + .../assets/icons/retired/zoom_in_w500.svg | 1 + ui/qml/reskin/assets/icons/sort_flipped.png | Bin 0 -> 2975 bytes ui/qml/reskin/fonts/Inter/Inter-Black.ttf | Bin 0 -> 316848 bytes ui/qml/reskin/fonts/Inter/Inter-Bold.ttf | Bin 0 -> 316584 bytes ui/qml/reskin/fonts/Inter/Inter-ExtraBold.ttf | Bin 0 -> 317184 bytes .../reskin/fonts/Inter/Inter-ExtraLight.ttf | Bin 0 -> 311232 bytes ui/qml/reskin/fonts/Inter/Inter-Light.ttf | Bin 0 -> 310832 bytes ui/qml/reskin/fonts/Inter/Inter-Medium.ttf | Bin 0 -> 315132 bytes ui/qml/reskin/fonts/Inter/Inter-Regular.ttf | Bin 0 -> 310252 bytes ui/qml/reskin/fonts/Inter/Inter-SemiBold.ttf | Bin 0 -> 316220 bytes ui/qml/reskin/fonts/Inter/Inter-Thin.ttf | Bin 0 -> 310984 bytes ui/qml/reskin/fonts/Inter/OFL.txt | 93 + .../layout_framework/XsLayoutModeBar.qml | 209 + .../layout_framework/XsPanelDivider.qml | 62 +- .../layout_framework/XsPanelSplitter.qml | 136 +- .../layout_framework/XsPanelsMenuButton.qml | 34 +- .../layout_framework/XsViewContainer.qml | 379 +- ui/qml/reskin/main_reskin.qml | 4 +- ui/qml/reskin/qml_reskin.qrc | 222 +- .../session_data/XsMediaListModelData.qml | 45 + .../session_data/XsPlaylistsModelData.qml | 9 + ui/qml/reskin/session_data/XsSessionData.qml | 205 + ui/qml/reskin/views/media/XsMediaHeader.qml | 95 + ui/qml/reskin/views/media/XsMediaItems.qml | 50 + ui/qml/reskin/views/media/XsMedialist.qml | 200 +- .../data_indicators/XsMediaFlagIndicator.qml | 41 + .../data_indicators/XsMediaNotesIndicator.qml | 49 + .../media/data_indicators/XsMediaTextItem.qml | 31 + .../data_indicators/XsMediaThumbnailImage.qml | 47 + .../media/delegates/XsMediaHeaderColumn.qml | 156 + .../media/delegates/XsMediaItemDelegate.qml | 299 +- .../media/delegates/XsMediaSourceSelector.qml | 60 + .../views/playlists/XsPlaylistItems.qml | 57 + ui/qml/reskin/views/playlists/XsPlaylists.qml | 163 +- .../delegates/XsPlaylistDividerDelegate.qml | 163 +- .../delegates/XsPlaylistItemDelegate.qml | 329 +- .../delegates/XsSubsetItemDelegate.qml | 185 + .../delegates/XsTimelineItemDelegate.qml | 189 + ui/qml/reskin/views/timeline/XsTimeline.qml | 278 +- .../views/timeline/XsTimelineEditTools.qml | 58 + .../reskin/views/timeline/XsTimelineMenu.qml | 237 ++ .../reskin/views/timeline/XsTimelinePanel.qml | 1305 +++++++ .../views/timeline/data/XsSortFilterModel.qml | 136 + .../delegates/XsDelegateAudioTrack.qml | 134 + .../timeline/delegates/XsDelegateClip.qml | 217 ++ .../timeline/delegates/XsDelegateGap.qml | 83 + .../timeline/delegates/XsDelegateStack.qml | 421 ++ .../delegates/XsDelegateVideoTrack.qml | 147 + .../delegates/XsTimelineEditToolItems.qml | 16 + .../views/timeline/widgets/XsClipItem.qml | 181 + .../views/timeline/widgets/XsDragBoth.qml | 37 + .../views/timeline/widgets/XsDragLeft.qml | 37 + .../views/timeline/widgets/XsDragRight.qml | 6 + .../views/timeline/widgets/XsElideLabel.qml | 39 + .../views/timeline/widgets/XsGapItem.qml | 90 + .../views/timeline/widgets/XsMoveClip.qml | 38 + .../views/timeline/widgets/XsTickWidget.qml | 80 + .../timeline/widgets/XsTimelineCursor.qml | 61 + .../views/timeline/widgets/XsTrackHeader.qml | 184 + ui/qml/reskin/views/viewport/XsViewport.qml | 144 +- .../views/viewport/XsViewportActionBar.qml | 195 + .../views/viewport/XsViewportInfoBar.qml | 138 + .../views/viewport/XsViewportToolBar.qml | 121 + .../views/viewport/XsViewportTransportBar.qml | 230 ++ .../viewport/widgets/XsViewerMenuButton.qml | 206 + .../widgets/XsViewerSeekEditButton.qml | 259 ++ .../viewport/widgets/XsViewerTextDisplay.qml | 57 + .../viewport/widgets/XsViewerToggleButton.qml | 122 + .../viewport/widgets/XsViewerVolumeButton.qml | 88 + .../widgets/bars_and_tabs/XsSearchBar.qml | 34 +- ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml | 57 + .../widgets/bars_and_tabs/XsTabView.qml | 248 ++ ui/qml/reskin/widgets/buttons/XsNavButton.qml | 104 + .../widgets/buttons/XsPrimaryButton.qml | 69 +- .../reskin/widgets/buttons/XsSearchButton.qml | 55 + .../widgets/buttons/XsSecondaryButton.qml | 36 +- .../reskin/widgets/controls/XsScrollBar.qml | 27 + ui/qml/reskin/widgets/controls/XsSlider.qml | 69 + .../widgets/dialogs/XsOpenSessionDialog.qml | 29 + ui/qml/reskin/widgets/dialogs/XsPopup.qml | 33 + ui/qml/reskin/widgets/labels/XsText.qml | 54 + ui/qml/reskin/widgets/labels/XsTextField.qml | 60 + ui/qml/reskin/widgets/labels/XsToolTip.qml | 56 + ui/qml/reskin/widgets/menus/XsMainMenuBar.qml | 292 +- ui/qml/reskin/widgets/menus/XsMenu.qml | 77 +- ui/qml/reskin/widgets/menus/XsMenuDivider.qml | 4 +- ui/qml/reskin/widgets/menus/XsMenuItem.qml | 114 +- .../reskin/widgets/menus/XsMenuItemToggle.qml | 71 +- .../menus/XsMenuItemToggleWithSettings.qml | 182 + .../widgets/menus/XsMenuMultiChoice.qml | 61 +- ui/qml/reskin/widgets/outputs/XsGridView.qml | 34 + ui/qml/reskin/widgets/outputs/XsImage.qml | 34 + ui/qml/reskin/widgets/outputs/XsListView.qml | 77 + .../widgets/prototypes/new/XsMenuItem.qml | 6 +- .../prototypes/new/XsTopMenuButton.qml | 7 +- .../prototypes/new/XsViewerMenuItem.qml | 6 +- .../widgets/prototypes/old/XsButton.qml | 4 +- ui/qml/reskin/windows/XsSessionWindow.qml | 134 +- ui/qml/reskin/xStudioReskin/XsStyleSheet.qml | 8 +- ui/qml/reskin/xStudioReskin/qmldir | 65 +- ui/qml/xstudio/bars/XsMediaInfoBar.qml | 551 +-- ui/qml/xstudio/bars/XsMenuBar.qml | 4 +- ui/qml/xstudio/bars/XsShortcuts.qml | 31 +- ui/qml/xstudio/bars/XsToolBar.qml | 40 +- .../xstudio/base/core/XsModuleMenuBuilder.qml | 1 + .../xstudio/base/core/XsSortFilterModel.qml | 139 +- .../xstudio/base/dialogs/XsButtonDialog.qml | 2 +- .../base/dialogs/XsModuleAttributesDialog.qml | 6 + .../base/dialogs/XsStringRequestDialog.qml | 2 +- .../base/widgets/XsBoolAttrCheckBox.qml | 1 + ui/qml/xstudio/base/widgets/XsBorder.qml | 62 + .../widgets/XsCheckBoxWithMultiComboBox.qml | 10 +- .../base/widgets/XsComboBoxMultiSelect.qml | 25 +- ui/qml/xstudio/base/widgets/XsElideLabel.qml | 39 + .../xstudio/base/widgets/XsModuleSubMenu.qml | 6 + ui/qml/xstudio/base/widgets/XsSplitView.qml | 6 +- ui/qml/xstudio/base/widgets/XsTickWidget.qml | 80 + .../xstudio/base/widgets/XsTimelineCursor.qml | 61 + ui/qml/xstudio/base/widgets/XsToolbarItem.qml | 9 +- ui/qml/xstudio/core/XsGlobalPreferences.qml | 21 + ui/qml/xstudio/cursors/move-edge-left.svg | 70 + ui/qml/xstudio/cursors/move-edge-right.svg | 1 + ui/qml/xstudio/cursors/move-join.svg | 96 + .../xstudio/dialogs/XsImportSessionDialog.qml | 2 +- .../xstudio/dialogs/XsMediaMoveCopyDialog.qml | 46 + .../xstudio/dialogs/XsNewSnapshotDialog.qml | 164 + ui/qml/xstudio/dialogs/XsNotesDialog.qml | 2 +- .../xstudio/dialogs/XsOpenSessionDialog.qml | 2 +- .../dialogs/XsSaveSelectedSessionDialog.qml | 6 +- .../xstudio/dialogs/XsSaveSessionDialog.qml | 6 +- ui/qml/xstudio/dialogs/XsSettingsDialog.qml | 13 +- ui/qml/xstudio/dialogs/XsSnapshotDialog.qml | 65 +- ui/qml/xstudio/main.qml | 180 +- ui/qml/xstudio/menus/XsMediaMenu.qml | 41 +- ui/qml/xstudio/menus/XsPanelMenu.qml | 2 +- ui/qml/xstudio/menus/XsPlaylistMenu.qml | 12 +- .../xstudio/menus/XsSnapshotDirectoryMenu.qml | 154 + ui/qml/xstudio/menus/XsSnapshotMenu.qml | 97 + ui/qml/xstudio/menus/XsTimelineMenu.qml | 6 + ui/qml/xstudio/menus/XsViewerContextMenu.qml | 1 - .../media_list/XsMediaPanelListView.qml | 2 +- .../media_list/delegates/XsDelegateMedia.qml | 50 +- .../panels/playlist/XsPlaylistsPanelNew.qml | 37 +- .../delegates/XsDelegateChoiceDivider.qml | 6 +- .../delegates/XsDelegateChoicePlaylist.qml | 10 +- .../delegates/XsDelegateChoiceSubset.qml | 8 +- .../delegates/XsDelegateChoiceTimeline.qml | 19 +- .../panels/timeline/XsTimelinePanel.qml | 1395 +++++++ .../panels/timeline/XsTimelinePanelHeader.qml | 54 + .../delegates/XsDelegateAudioTrack.qml | 134 + .../timeline/delegates/XsDelegateClip.qml | 221 ++ .../timeline/delegates/XsDelegateGap.qml | 83 + .../timeline/delegates/XsDelegateStack.qml | 418 ++ .../delegates/XsDelegateVideoTrack.qml | 147 + .../panels/timeline/widgets/XsClipItem.qml | 181 + .../panels/timeline/widgets/XsDragBoth.qml | 37 + .../panels/timeline/widgets/XsDragLeft.qml | 37 + .../panels/timeline/widgets/XsDragRight.qml | 6 + .../panels/timeline/widgets/XsGapItem.qml | 90 + .../panels/timeline/widgets/XsMoveClip.qml | 38 + .../panels/timeline/widgets/XsTrackHeader.qml | 184 + ui/qml/xstudio/player/XsLightPlayerWidget.qml | 228 ++ ui/qml/xstudio/player/XsLightPlayerWindow.qml | 158 + ui/qml/xstudio/player/XsPlayerWidget.qml | 3 +- ui/qml/xstudio/player/XsPlayerWindow.qml | 21 +- .../xstudio/player/XsPopoutViewerWidget.qml | 1 + ui/qml/xstudio/player/XsSessionWidget.qml | 4 +- ui/qml/xstudio/player/XsViewport.qml | 86 +- ui/qml/xstudio/qml.qrc | 37 +- ui/qml/xstudio/trays/XsMediaToolsTray.qml | 38 +- .../xstudio/widgets/XsSourceToolbarButton.qml | 51 +- ui/qml/xstudio/xStudio/qmldir | 37 +- xStudioConfig.cmake.in | 18 + 625 files changed, 50881 insertions(+), 15670 deletions(-) create mode 100644 extern/include/strict_fstream.hpp create mode 100644 extern/include/zstr.hpp create mode 100644 include/xstudio/audio/linux_audio_output_device.hpp create mode 100644 include/xstudio/colour_pipeline/colour_texture.hpp create mode 100644 include/xstudio/conform/conform_manager_actor.hpp create mode 100644 include/xstudio/conform/conformer.hpp create mode 100644 include/xstudio/ui/canvas/canvas.hpp create mode 100644 include/xstudio/ui/canvas/canvas_undo_redo.hpp create mode 100644 include/xstudio/ui/canvas/caption.hpp create mode 100644 include/xstudio/ui/canvas/handle.hpp create mode 100644 include/xstudio/ui/canvas/stroke.hpp create mode 100644 include/xstudio/ui/opengl/opengl_canvas_renderer.hpp create mode 100644 include/xstudio/ui/opengl/opengl_caption_renderer.hpp create mode 100644 include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp create mode 100644 include/xstudio/ui/opengl/opengl_stroke_renderer.hpp create mode 100644 include/xstudio/ui/opengl/opengl_texthandle_renderer.hpp create mode 100644 include/xstudio/ui/qml/module_data_ui.hpp create mode 100644 include/xstudio/ui/qml/snapshot_model_ui.hpp create mode 100644 include/xstudio/utility/file_system_item.hpp create mode 100755 scripts/linting/license_stub_check create mode 100644 share/preference/core_conform.json create mode 100644 share/preference/core_snapshot.json create mode 100644 share/preference/plugin_data_source_shotbrowser.json create mode 100644 share/preference/plugin_grading.json create mode 100644 src/conform/src/CMakeLists.txt create mode 100644 src/conform/src/conform_manager_actor.cpp create mode 100644 src/conform/src/conformer.cpp create mode 100644 src/conform/test/CMakeLists.txt create mode 100644 src/plugin/colour_op/CMakeLists.txt create mode 100644 src/plugin/colour_op/grading/src/CMakeLists.txt create mode 100644 src/plugin/colour_op/grading/src/grading.cpp create mode 100644 src/plugin/colour_op/grading/src/grading.h create mode 100644 src/plugin/colour_op/grading/src/grading_colour_op.cpp create mode 100644 src/plugin/colour_op/grading/src/grading_colour_op.hpp create mode 100644 src/plugin/colour_op/grading/src/grading_data.cpp create mode 100644 src/plugin/colour_op/grading/src/grading_data.h create mode 100644 src/plugin/colour_op/grading/src/grading_data_serialiser.cpp create mode 100644 src/plugin/colour_op/grading/src/grading_data_serialiser.hpp create mode 100644 src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp create mode 100644 src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h create mode 100644 src/plugin/colour_op/grading/src/grading_mask_render_data.h create mode 100644 src/plugin/colour_op/grading/src/qml/.clang-tidy create mode 100644 src/plugin/colour_op/grading/src/qml/CMakeLists.txt create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingButton.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingDialog.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingHSlider.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderGroup.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderSimple.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingVSlider.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingWheel.qml create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.1/qmldir create mode 100644 src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml create mode 100644 src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/ToolSelector.qml create mode 100644 src/plugin/colour_op/grading/src/qml/MaskTool.1/qmldir create mode 100644 src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp create mode 100644 src/plugin/colour_op/grading/test/CMakeLists.txt create mode 100644 src/plugin/conform/CMakeLists.txt create mode 100644 src/plugin/conform/dneg/shotgun/src/CMakeLists.txt create mode 100644 src/plugin/conform/dneg/shotgun/src/conform_shotgun.cpp create mode 100644 src/plugin/conform/dneg/shotgun/test/CMakeLists.txt create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.tcc create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_action.tcc create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.cpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.hpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_definitions.hpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_get_actions.tcc create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_post_actions.tcc create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_put_actions.tcc create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.cpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.hpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.hpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunTagDialog.qml create mode 100644 src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_query_ui.cpp create mode 100644 src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_requests_ui.cpp create mode 100644 src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp create mode 100644 src/ui/canvas/src/CMakeLists.txt create mode 100644 src/ui/canvas/src/canvas.cpp create mode 100644 src/ui/canvas/src/canvas_undo_redo.cpp create mode 100644 src/ui/canvas/src/caption.cpp create mode 100644 src/ui/canvas/src/stroke.cpp create mode 100644 src/ui/canvas/test/CMakeLists.txt create mode 100644 src/ui/opengl/src/opengl_canvas_renderer.cpp create mode 100644 src/ui/opengl/src/opengl_caption_renderer.cpp create mode 100644 src/ui/opengl/src/opengl_offscreen_renderer.cpp create mode 100644 src/ui/opengl/src/opengl_stroke_renderer.cpp create mode 100644 src/ui/opengl/src/opengl_texthandle_renderer.cpp create mode 100644 src/ui/qml/helper/src/module_data_ui.cpp create mode 100644 src/ui/qml/helper/src/snapshot_model_ui.cpp create mode 100644 src/ui/qml/session/src/session_model_timeline_ui.cpp create mode 100644 src/utility/src/file_system_item.cpp create mode 100644 src/utility/test/file_system_item_test.cpp create mode 100644 ui/qml/reskin/assets/icons/new/ad_group.svg create mode 100644 ui/qml/reskin/assets/icons/new/arrow_selector_tool.svg create mode 100644 ui/qml/reskin/assets/icons/new/arrows_outward.svg create mode 100644 ui/qml/reskin/assets/icons/new/brush.svg create mode 100644 ui/qml/reskin/assets/icons/new/build_gang.svg create mode 100644 ui/qml/reskin/assets/icons/new/center_focus_strong.svg create mode 100644 ui/qml/reskin/assets/icons/new/content_cut.svg create mode 100644 ui/qml/reskin/assets/icons/new/content_paste.svg create mode 100644 ui/qml/reskin/assets/icons/new/delete.svg create mode 100644 ui/qml/reskin/assets/icons/new/disabled.svg create mode 100644 ui/qml/reskin/assets/icons/new/expand.svg create mode 100644 ui/qml/reskin/assets/icons/new/expand_all.svg create mode 100644 ui/qml/reskin/assets/icons/new/fast_rewind.svg create mode 100644 ui/qml/reskin/assets/icons/new/filter.svg create mode 100644 ui/qml/reskin/assets/icons/new/format_size.svg create mode 100644 ui/qml/reskin/assets/icons/new/ink_pen.svg create mode 100644 ui/qml/reskin/assets/icons/new/input.svg create mode 100644 ui/qml/reskin/assets/icons/new/laps.svg create mode 100644 ui/qml/reskin/assets/icons/new/library_music.svg create mode 100644 ui/qml/reskin/assets/icons/new/list.svg create mode 100644 ui/qml/reskin/assets/icons/new/list_default.svg create mode 100644 ui/qml/reskin/assets/icons/new/list_shotgun.svg create mode 100644 ui/qml/reskin/assets/icons/new/list_subset.svg create mode 100644 ui/qml/reskin/assets/icons/new/list_view.svg create mode 100644 ui/qml/reskin/assets/icons/new/more_vert.svg create mode 100644 ui/qml/reskin/assets/icons/new/movie.svg create mode 100644 ui/qml/reskin/assets/icons/new/open_in.svg create mode 100644 ui/qml/reskin/assets/icons/new/open_in_new.svg create mode 100644 ui/qml/reskin/assets/icons/new/open_with.svg create mode 100644 ui/qml/reskin/assets/icons/new/output.svg create mode 100644 ui/qml/reskin/assets/icons/new/pan.svg create mode 100644 ui/qml/reskin/assets/icons/new/pause.svg create mode 100644 ui/qml/reskin/assets/icons/new/photo_camera.svg create mode 100644 ui/qml/reskin/assets/icons/new/rectangle.svg create mode 100644 ui/qml/reskin/assets/icons/new/redo.svg create mode 100644 ui/qml/reskin/assets/icons/new/repartition.svg create mode 100644 ui/qml/reskin/assets/icons/new/repeat.svg create mode 100644 ui/qml/reskin/assets/icons/new/reset_image.svg create mode 100644 ui/qml/reskin/assets/icons/new/reset_tv.svg create mode 100644 ui/qml/reskin/assets/icons/new/restart.svg create mode 100644 ui/qml/reskin/assets/icons/new/search_off.svg create mode 100644 ui/qml/reskin/assets/icons/new/settings.svg create mode 100644 ui/qml/reskin/assets/icons/new/skip.svg create mode 100644 ui/qml/reskin/assets/icons/new/sort.svg create mode 100644 ui/qml/reskin/assets/icons/new/splitscreen.svg create mode 100644 ui/qml/reskin/assets/icons/new/splitscreen2.svg create mode 100644 ui/qml/reskin/assets/icons/new/sticky_note.svg create mode 100644 ui/qml/reskin/assets/icons/new/sync.svg create mode 100644 ui/qml/reskin/assets/icons/new/theaters.svg create mode 100644 ui/qml/reskin/assets/icons/new/trending.svg create mode 100644 ui/qml/reskin/assets/icons/new/triangle.svg create mode 100644 ui/qml/reskin/assets/icons/new/tune.svg create mode 100644 ui/qml/reskin/assets/icons/new/undo.svg create mode 100644 ui/qml/reskin/assets/icons/new/upload.svg create mode 100644 ui/qml/reskin/assets/icons/new/variables_insert.svg create mode 100644 ui/qml/reskin/assets/icons/new/view.svg create mode 100644 ui/qml/reskin/assets/icons/new/view_grid.svg create mode 100644 ui/qml/reskin/assets/icons/new/volume_down.svg create mode 100644 ui/qml/reskin/assets/icons/new/volume_mute.svg create mode 100644 ui/qml/reskin/assets/icons/new/volume_no_sound.svg create mode 100644 ui/qml/reskin/assets/icons/new/volume_up.svg create mode 100644 ui/qml/reskin/assets/icons/new/zoom_in.svg create mode 100644 ui/qml/reskin/assets/icons/retired/brush_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/delete_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/fast_forward_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/fast_rewind_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/open_in_new_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/open_with_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/pan_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/pause_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/photo_camera_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/play_arrow_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/repeat_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/restart_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/search_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/skip_next_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/skip_previous_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/sticky_note_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/sync_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/trending_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/tune_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/view_grid_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/view_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/volume_down_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/volume_mute_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/volume_no_sound_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/volume_up_w500.svg create mode 100644 ui/qml/reskin/assets/icons/retired/zoom_in_w500.svg create mode 100644 ui/qml/reskin/assets/icons/sort_flipped.png create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Black.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Bold.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-ExtraBold.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-ExtraLight.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Light.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Medium.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Regular.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-SemiBold.ttf create mode 100644 ui/qml/reskin/fonts/Inter/Inter-Thin.ttf create mode 100644 ui/qml/reskin/fonts/Inter/OFL.txt create mode 100644 ui/qml/reskin/layout_framework/XsLayoutModeBar.qml create mode 100644 ui/qml/reskin/session_data/XsMediaListModelData.qml create mode 100644 ui/qml/reskin/session_data/XsPlaylistsModelData.qml create mode 100644 ui/qml/reskin/session_data/XsSessionData.qml create mode 100644 ui/qml/reskin/views/media/XsMediaHeader.qml create mode 100644 ui/qml/reskin/views/media/XsMediaItems.qml create mode 100644 ui/qml/reskin/views/media/data_indicators/XsMediaFlagIndicator.qml create mode 100644 ui/qml/reskin/views/media/data_indicators/XsMediaNotesIndicator.qml create mode 100644 ui/qml/reskin/views/media/data_indicators/XsMediaTextItem.qml create mode 100644 ui/qml/reskin/views/media/data_indicators/XsMediaThumbnailImage.qml create mode 100644 ui/qml/reskin/views/media/delegates/XsMediaHeaderColumn.qml create mode 100644 ui/qml/reskin/views/media/delegates/XsMediaSourceSelector.qml create mode 100644 ui/qml/reskin/views/playlists/XsPlaylistItems.qml create mode 100644 ui/qml/reskin/views/playlists/delegates/XsSubsetItemDelegate.qml create mode 100644 ui/qml/reskin/views/playlists/delegates/XsTimelineItemDelegate.qml create mode 100644 ui/qml/reskin/views/timeline/XsTimelineEditTools.qml create mode 100644 ui/qml/reskin/views/timeline/XsTimelineMenu.qml create mode 100644 ui/qml/reskin/views/timeline/XsTimelinePanel.qml create mode 100644 ui/qml/reskin/views/timeline/data/XsSortFilterModel.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsDelegateAudioTrack.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsDelegateClip.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsDelegateGap.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsDelegateStack.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsDelegateVideoTrack.qml create mode 100644 ui/qml/reskin/views/timeline/delegates/XsTimelineEditToolItems.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsClipItem.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsDragLeft.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsDragRight.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsElideLabel.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsGapItem.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsMoveClip.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsTickWidget.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsTimelineCursor.qml create mode 100644 ui/qml/reskin/views/timeline/widgets/XsTrackHeader.qml create mode 100644 ui/qml/reskin/views/viewport/XsViewportActionBar.qml create mode 100644 ui/qml/reskin/views/viewport/XsViewportInfoBar.qml create mode 100644 ui/qml/reskin/views/viewport/XsViewportToolBar.qml create mode 100644 ui/qml/reskin/views/viewport/XsViewportTransportBar.qml create mode 100644 ui/qml/reskin/views/viewport/widgets/XsViewerMenuButton.qml create mode 100644 ui/qml/reskin/views/viewport/widgets/XsViewerSeekEditButton.qml create mode 100644 ui/qml/reskin/views/viewport/widgets/XsViewerTextDisplay.qml create mode 100644 ui/qml/reskin/views/viewport/widgets/XsViewerToggleButton.qml create mode 100644 ui/qml/reskin/views/viewport/widgets/XsViewerVolumeButton.qml create mode 100644 ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml create mode 100644 ui/qml/reskin/widgets/bars_and_tabs/XsTabView.qml create mode 100644 ui/qml/reskin/widgets/buttons/XsNavButton.qml create mode 100644 ui/qml/reskin/widgets/buttons/XsSearchButton.qml create mode 100644 ui/qml/reskin/widgets/controls/XsScrollBar.qml create mode 100644 ui/qml/reskin/widgets/controls/XsSlider.qml create mode 100644 ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml create mode 100644 ui/qml/reskin/widgets/dialogs/XsPopup.qml create mode 100644 ui/qml/reskin/widgets/labels/XsText.qml create mode 100644 ui/qml/reskin/widgets/labels/XsTextField.qml create mode 100644 ui/qml/reskin/widgets/labels/XsToolTip.qml create mode 100644 ui/qml/reskin/widgets/menus/XsMenuItemToggleWithSettings.qml create mode 100644 ui/qml/reskin/widgets/outputs/XsGridView.qml create mode 100644 ui/qml/reskin/widgets/outputs/XsImage.qml create mode 100644 ui/qml/reskin/widgets/outputs/XsListView.qml create mode 100644 ui/qml/xstudio/base/widgets/XsBorder.qml create mode 100644 ui/qml/xstudio/base/widgets/XsElideLabel.qml create mode 100644 ui/qml/xstudio/base/widgets/XsTickWidget.qml create mode 100644 ui/qml/xstudio/base/widgets/XsTimelineCursor.qml create mode 100644 ui/qml/xstudio/cursors/move-edge-left.svg create mode 100644 ui/qml/xstudio/cursors/move-edge-right.svg create mode 100644 ui/qml/xstudio/cursors/move-join.svg create mode 100644 ui/qml/xstudio/dialogs/XsMediaMoveCopyDialog.qml create mode 100644 ui/qml/xstudio/dialogs/XsNewSnapshotDialog.qml create mode 100644 ui/qml/xstudio/menus/XsSnapshotDirectoryMenu.qml create mode 100644 ui/qml/xstudio/menus/XsSnapshotMenu.qml create mode 100644 ui/qml/xstudio/panels/timeline/XsTimelinePanel.qml create mode 100644 ui/qml/xstudio/panels/timeline/XsTimelinePanelHeader.qml create mode 100644 ui/qml/xstudio/panels/timeline/delegates/XsDelegateAudioTrack.qml create mode 100644 ui/qml/xstudio/panels/timeline/delegates/XsDelegateClip.qml create mode 100644 ui/qml/xstudio/panels/timeline/delegates/XsDelegateGap.qml create mode 100644 ui/qml/xstudio/panels/timeline/delegates/XsDelegateStack.qml create mode 100644 ui/qml/xstudio/panels/timeline/delegates/XsDelegateVideoTrack.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsClipItem.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsDragLeft.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsDragRight.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsGapItem.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsMoveClip.qml create mode 100644 ui/qml/xstudio/panels/timeline/widgets/XsTrackHeader.qml create mode 100644 ui/qml/xstudio/player/XsLightPlayerWidget.qml create mode 100644 ui/qml/xstudio/player/XsLightPlayerWindow.qml create mode 100644 xStudioConfig.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index ba40ec9b5..73d64905f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,6 @@ if(ENABLE_CLANG_FORMAT) clangformat_setup(${FORMAT_ITEMS}) endif() - if(INSTALL_PYTHON_MODULE) add_subdirectory(python) endif() @@ -148,6 +147,11 @@ if(INSTALL_XSTUDIO) 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 +161,29 @@ 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") 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..697073e0b 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,14 @@ 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. MacOS and Windows compatibility is not available yet but this work is on the roadmap for 2023. -## Building xSTUDIO for Linux - -We provide comprehensive build steps for 3 of the most popular Linux distributions: +We provide comprehensive build steps for 3 of the most popular Linux distributions. * [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 MacOS - -MacOS compatibility is not yet available but it is due in Q3 or Q4 2023. Watch this space! diff --git a/cmake/macros.cmake b/cmake/macros.cmake index c57cf90fe..4da1f3807 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -24,8 +24,8 @@ macro(default_compile_options name) PRIVATE -D__linux__ 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}\" PRIVATE $<$:XSTUDIO_DEBUG=1> @@ -57,9 +57,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> @@ -92,8 +92,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) @@ -191,7 +196,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() 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/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/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/include/xstudio/atoms.hpp b/include/xstudio/atoms.hpp index b2d174984..7d99bc5bc 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; @@ -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..133c8f5d7 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,7 +67,24 @@ 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 +93,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 +101,10 @@ 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_; + bool audio_repitch_ = {false}; + bool audio_scrubbing_ = {false}; + float volume_ = {100.0f}; + bool muted_ = {false}; - module::FloatAttribute *volume_; - module::BooleanAttribute *muted_; }; } // namespace xstudio::audio diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index bf139806e..6746bb5c4 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -8,57 +8,147 @@ 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"); + caf::actor samples_actor) + : caf::event_based_actor(cfg), + playing_(false), + waiting_for_samples_(false), + audio_samples_actor_(samples_actor) { + + spdlog::debug("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 + }, + [=](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 { + + 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; }); + } + + ); + } + + void open_output_device(const utility::JsonStore &prefs) { + try { + output_device_ = std::make_unique(prefs); + } catch (std::exception &e) { + spdlog::debug( + "{} 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(); } + const char *name() const override { return name_.c_str(); } - private: - void open_output_device(const utility::JsonStore &prefs); + 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 +156,103 @@ 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/linux_audio_output_device.hpp b/include/xstudio/audio/linux_audio_output_device.hpp new file mode 100644 index 000000000..14f5b6658 --- /dev/null +++ b/include/xstudio/audio/linux_audio_output_device.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/audio/audio_output_device.hpp" +#include "xstudio/utility/json_store.hpp" + +#include + +namespace xstudio { +namespace audio { + + /** + * @brief LinuxAudioOutputDevice class, low level interface with audio output + * + * @details + * See header for AudioOutputDevice + */ + class LinuxAudioOutputDevice : public AudioOutputDevice { + public: + LinuxAudioOutputDevice(const utility::JsonStore &prefs); + + ~LinuxAudioOutputDevice() 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_; } + + static std::string name() { return "LinuxAudioOutputDevice"; } + + private: + long sample_rate_ = {44100}; + int num_channels_ = {2}; + long buffer_size_ = {2048}; + SampleFormat sample_format_ = {SampleFormat::INT16}; + pa_simple *playback_handle_ = {nullptr}; + const utility::JsonStore config_; + const utility::JsonStore prefs_; + }; +} // namespace audio +} // namespace xstudio diff --git a/include/xstudio/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index ba7b2307b..74a6843ca 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_), @@ -277,6 +284,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 +292,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 +310,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 +318,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/global_store/global_store.hpp b/include/xstudio/global_store/global_store.hpp index b433a4c11..fa43910e4 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"}; @@ -232,6 +232,16 @@ namespace global_store { const bool broacast_change = true) { 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..a787d3710 100644 --- a/include/xstudio/media/media_actor.hpp +++ b/include/xstudio/media/media_actor.hpp @@ -96,7 +96,9 @@ namespace media { const char *name() const override { return NAME.c_str(); } private: + void update_media_status(); + void update_media_detail(); void acquire_detail(const utility::FrameRate &rate, caf::typed_response_promise rp); @@ -132,11 +134,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/image_buffer.hpp b/include/xstudio/media_reader/image_buffer.hpp index df0698ea4..458840e85 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,6 +117,7 @@ namespace media_reader { plugin_blind_data_ = o.plugin_blind_data_; tts_ = o.tts_; frame_id_ = o.frame_id_; + bookmarks_ = o.bookmarks_; return *this; } @@ -140,30 +142,56 @@ 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 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/module.hpp b/include/xstudio/module/module.hpp index 7ec08332d..b4c81aa98 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,9 @@ 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 +188,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 +197,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 +231,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 +267,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 +285,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..dac633740 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: @@ -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..717d5f30b 100644 --- a/include/xstudio/shotgun_client/shotgun_client.hpp +++ b/include/xstudio/shotgun_client/shotgun_client.hpp @@ -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/model_data/model_data_actor.hpp b/include/xstudio/ui/model_data/model_data_actor.hpp index 9f5b26072..c1219fe26 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,12 @@ 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 +109,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 +135,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..e428cf0be --- /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..d30e88065 100644 --- a/include/xstudio/ui/opengl/texture.hpp +++ b/include/xstudio/ui/opengl/texture.hpp @@ -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..94fd04baf 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. diff --git a/include/xstudio/ui/qml/bookmark_model_ui.hpp b/include/xstudio/ui/qml/bookmark_model_ui.hpp index 115ca829b..a27c8cf6f 100644 --- a/include/xstudio/ui/qml/bookmark_model_ui.hpp +++ b/include/xstudio/ui/qml/bookmark_model_ui.hpp @@ -125,7 +125,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/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index 021ebab98..24e072140 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -42,6 +42,41 @@ namespace ui { QVariant mapFromValue(const nlohmann::json &value); nlohmann::json mapFromValue(const QVariant &value); + class 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 ModelProperty : public QObject { Q_OBJECT @@ -136,6 +171,7 @@ namespace ui { [[nodiscard]] QQmlPropertyMap *values() const { return values_; } Q_INVOKABLE void setIndex(const QModelIndex &index); + Q_INVOKABLE void dump(); signals: void indexChanged(); @@ -195,7 +231,9 @@ namespace ui { 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: @@ -394,6 +432,44 @@ 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_; diff --git a/include/xstudio/ui/qml/json_tree_model_ui.hpp b/include/xstudio/ui/qml/json_tree_model_ui.hpp index 9eb84b40d..d3f7538cb 100644 --- a/include/xstudio/ui/qml/json_tree_model_ui.hpp +++ b/include/xstudio/ui/qml/json_tree_model_ui.hpp @@ -36,6 +36,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 +78,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 +153,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; diff --git a/include/xstudio/ui/qml/model_data_ui.hpp b/include/xstudio/ui/qml/model_data_ui.hpp index 8678ae981..21d974c10 100644 --- a/include/xstudio/ui/qml/model_data_ui.hpp +++ b/include/xstudio/ui/qml/model_data_ui.hpp @@ -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,6 +81,7 @@ 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 { @@ -98,7 +103,11 @@ 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 { @@ -107,6 +116,18 @@ class ReskinPanelsModel : public UIModelData { 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 MediaListColumnsModel : public UIModelData { + + Q_OBJECT + + public: + explicit MediaListColumnsModel(QObject *parent = nullptr); }; class MenuModelItem : public caf::mixin::actor_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..bf4cc4a63 --- /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 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..b2c0a31a1 100644 --- a/include/xstudio/ui/qml/module_menu_ui.hpp +++ b/include/xstudio/ui/qml/module_menu_ui.hpp @@ -56,6 +56,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 +87,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 +96,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..34dd14ca9 100644 --- a/include/xstudio/ui/qml/module_ui.hpp +++ b/include/xstudio/ui/qml/module_ui.hpp @@ -31,7 +31,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); @@ -100,7 +100,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..4b0d97992 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -40,8 +40,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 +49,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 +70,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 +93,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 +102,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 +117,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 +130,6 @@ namespace ui { void scaleChanged(float); void playheadChanged(QObject *); void translateChanged(QVector2D); - void colourUnderCursorChanged(); void mouseButtonsChanged(); void mouseChanged(); void onScreenImageLogicalFrameChanged(); @@ -136,6 +140,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 +164,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,6 +171,7 @@ namespace ui { bool no_alpha_channel_ = {false}; bool enable_shortcuts_ = {true}; int viewport_index_ = {0}; + bool is_quick_viewer_ = {false}; }; } // namespace qml diff --git a/include/xstudio/ui/qml/qml_viewport_renderer.hpp b/include/xstudio/ui/qml/qml_viewport_renderer.hpp index d3f95dbcf..c917ef90f 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) { @@ -78,6 +79,18 @@ namespace ui { [[nodiscard]] QString name() const { 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: @@ -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..2939b8524 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -6,6 +6,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/timeline/item.hpp" CAF_PUSH_WARNINGS @@ -14,6 +15,10 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS +// namespace xstudio::timeline { +// class Item; +// } + namespace xstudio::ui::qml { using namespace caf; @@ -28,33 +33,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 +104,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 +117,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 +133,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 +201,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 +247,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 +275,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 +304,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 +338,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 +359,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 +373,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 +396,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 +405,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 +418,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..5a3ff6d65 --- /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 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..d42c09d26 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,14 @@ 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/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..6f111ecf1 100644 --- a/include/xstudio/ui/qt/offscreen_viewport.hpp +++ b/include/xstudio/ui/qt/offscreen_viewport.hpp @@ -22,55 +22,96 @@ 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); + + void make_conversion_lut(); + + thumbnail::ThumbnailBufferPtr + rgb96thumbFromHalfFloatImage(const media_reader::ImageBufPtr &image); - std::shared_ptr viewport_renderer_; - QOpenGLContext *gl_context_ = {nullptr}; - QOffscreenSurface *surface_ = {nullptr}; - QThread *thread_ = {nullptr}; - caf::actor middleman_; + 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..b3051673d 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,37 @@ 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 +319,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 +348,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 +356,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 +380,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/chrono.hpp b/include/xstudio/utility/chrono.hpp index baccf99d1..bcc8b1947 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -17,8 +17,9 @@ 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; + using sysclock = std::chrono::system_clock; + using sys_time_point = sysclock::time_point; + using sys_time_duration = sysclock::duration; inline std::string to_string(const sys_time_point &tp) { auto in_time_t = std::chrono::system_clock::to_time_t(tp); 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/file_system_item.hpp b/include/xstudio/utility/file_system_item.hpp new file mode 100644 index 000000000..b9ebc095a --- /dev/null +++ b/include/xstudio/utility/file_system_item.hpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// container to handle sequences/mov files etc.. +#pragma once + +// #include +#include +#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/helpers.hpp b/include/xstudio/utility/helpers.hpp index 6640991ee..cb18c404f 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -46,6 +46,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 +64,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); @@ -177,27 +180,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); @@ -332,6 +318,17 @@ namespace utility { return false; } + inline bool is_session(const std::string &path) { + fs::path p(path); + std::string ext = to_upper(p.extension()); + 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()); diff --git a/include/xstudio/utility/json_store.hpp b/include/xstudio/utility/json_store.hpp index 5eb976e93..2c5f9d52f 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(); } }; @@ -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()) { 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/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/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/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/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/share/preference/core_audio.json b/share/preference/core_audio.json index b4188be54..a330050e8 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", 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..51e0bbf0a 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.", 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/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/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/audio_output.cpp b/src/audio/src/audio_output.cpp index d0a75f9e2..f34d94ce5 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, @@ -182,8 +151,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); @@ -491,7 +459,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 +490,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..14c185f4d 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -11,162 +11,53 @@ #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" -// include for system (soundcard) audio output -#ifdef __linux__ -#include "linux_audio_output_device.hpp" -#elif __APPLE__ -// TO DO -#elif _WIN32 -// TO DO -#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) { - - 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(); -} + 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"); -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 { - prepare_samples_for_soundcard( - samples, num_samps_to_push, microseconds_delay, num_channels, sample_rate); + [=](utility::get_event_group_atom) -> caf::actor { + return event_group_; + }, - } 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,23 +65,38 @@ 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); + } ); 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..c2c9b32fc 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 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/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..2bd322c75 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..06b0d6aa0 --- /dev/null +++ b/src/conform/src/CMakeLists.txt @@ -0,0 +1,7 @@ +SET(LINK_DEPS + xstudio::utility + xstudio::broadcast + 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..74816f7f7 --- /dev/null +++ b/src/conform/src/conformer.cpp @@ -0,0 +1,18 @@ +#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/main.cpp b/src/demos/glx_minimal_demo/src/main.cpp index c23932321..e5b9faa70 100644 --- a/src/demos/glx_minimal_demo/src/main.cpp +++ b/src/demos/glx_minimal_demo/src/main.cpp @@ -127,7 +127,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 +215,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[]) { diff --git a/src/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index 19750d546..188ff2a92 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC xstudio::module xstudio::audio_output + xstudio::conform xstudio::embedded_python xstudio::event xstudio::global_store diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index 57b671c3b..8aa6af71a 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 +// TO DO +#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,44 @@ 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 + // TO DO +#endif + python_enabled_ = false; connected_ = false; api_enabled_ = false; @@ -248,7 +271,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 +471,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/global_store.cpp b/src/global_store/src/global_store.cpp index 2dd3d5389..e7ebc3bb7 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -68,7 +68,6 @@ 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)) { @@ -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,13 @@ 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. @@ -349,3 +354,36 @@ 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/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index d0eded21e..9b7df7299 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -44,6 +44,7 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::qml::tag xstudio::ui::qml::viewport xstudio::ui::viewport + xstudio::ui::qt::viewport_widget xstudio::utility PUBLIC caf::core diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index ede84c02f..5be2cf685 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -66,13 +66,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 "xstudio/ui/qt/offscreen_viewport.hpp" //NOLINT #include "QuickFuture" @@ -96,6 +99,40 @@ using namespace xstudio; bool shutdown_xstudio = false; +struct ExitTimeoutKiller { + + void start() { + + // 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(); + } + }); + } + + void stop() { + + // 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; + +} exit_timeout_killer; + void handler(int sig) { void *array[10]; size_t size; @@ -139,8 +176,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"}, @@ -224,6 +266,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 +320,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 +382,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 +439,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 +471,9 @@ struct Launcher { playlist, p.value(), not actions["new_instance"], - actions["compare"]); + actions["compare"], + actions["quick_view"]); + media_sent = true; } @@ -457,6 +502,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 +542,7 @@ struct Launcher { { "headless": false, "new_instance": false, + "quick_view": false, "session_name": "", "open_session": false, "debug": false, @@ -555,27 +612,37 @@ 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 { @@ -610,14 +677,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 +698,6 @@ struct Launcher { true); } - UuidActorVector added_media; for (const auto &i : uri_fl) { try { added_media.push_back(request_receive( @@ -655,7 +719,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 +735,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 +833,7 @@ int main(int argc, char **argv) { "Track"); { + try { // create the actor system @@ -867,6 +943,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 +953,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"); @@ -930,6 +1012,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 +1033,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 +1059,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_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/media_actor.cpp b/src/media/src/media_actor.cpp index fa06ab530..bc51c7f77 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -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,28 @@ 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..a5d44eb29 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,16 @@ 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 +518,27 @@ 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"); - } - 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())}); + 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()) + 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 +556,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 +588,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 +627,8 @@ void MediaSourceActor::init() { utility::Uuid(), parent_uuid_, media_type)); + results.back().timecode_ = timecode; + timecode = timecode + 1; } rp.deliver(results); @@ -583,9 +654,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 +715,7 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), meta, - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type)); }, @@ -678,7 +751,7 @@ void MediaSourceActor::init() { base_.reader(), caf::actor_cast(this), utility::JsonStore(), - base_.current(media_type), + base_.uuid(), parent_uuid_, media_type)); } @@ -1163,12 +1236,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 +1282,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 +1410,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 +1444,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,6 +1454,9 @@ 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))); 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_hook/src/media_hook_actor.cpp b/src/media_hook/src/media_hook_actor.cpp index c8189f14c..fb6b50a57 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_) { @@ -194,7 +197,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 +222,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/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/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_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..04fb70dd6 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,27 @@ 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 +188,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 +203,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 +211,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 +220,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 +229,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 +242,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 +251,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 +262,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 +272,46 @@ 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 +390,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 +423,6 @@ caf::message_handler Module::message_handler() { attribute_events_group_, broadcast::join_broadcast_atom_v, subscriber); - return r; } catch (std::exception &e) { @@ -570,6 +636,19 @@ 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 +693,14 @@ 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 +714,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 +789,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 +824,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 +842,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 +900,115 @@ 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 +1181,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 +1207,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 +1243,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 +1295,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 +1381,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 +1434,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 +1558,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 +1577,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..ed133a652 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,29 @@ 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 +169,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 +193,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 +211,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 +250,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 +424,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 +436,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 +473,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 +496,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 +512,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 +635,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 +653,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..50f7c83fe 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) { @@ -1530,6 +1562,7 @@ 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..80e05347f 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,50 @@ 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..b8dd18787 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, @@ -327,13 +389,57 @@ void SubPlayhead::init() { 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 +450,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 +478,25 @@ 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..23d54c779 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -222,6 +222,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); @@ -1919,7 +1920,7 @@ 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 +2001,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 +2015,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..6224a59f6 --- /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..497344c89 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt @@ -0,0 +1,14 @@ +project(grading VERSION 0.1.0 LANGUAGES CXX) + +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) + +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/ocio.cpp b/src/plugin/colour_pipeline/ocio/src/ocio.cpp index d66d833cb..c7e3d8ae4 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(); @@ -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(); @@ -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..6acef9957 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,26 @@ 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 +436,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 +533,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 +607,19 @@ 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..db62b244d 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,3403 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "data_source_shotgun.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/media/media_actor.hpp" -#include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/shotgun_client/shotgun_client_actor.hpp" -#include "xstudio/tag/tag.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "xstudio/utility/chrono.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::shotgun_client; -using namespace xstudio::utility; -using namespace xstudio::global_store; -using namespace std::chrono_literals; - -/*CAF_BEGIN_TYPE_ID_BLOCK(shotgun, xstudio::shotgun_client::shotgun_client_error) -CAF_ADD_ATOM(shotgun, xstudio::shotgun_client, test_atom) -CAF_END_TYPE_ID_BLOCK(shotgun)*/ - -// Datasource should support a common subset of operations that apply to multiple datasources. -// not idea what they are though. -// get and put should try and map from this to the relevant sources. - -// shotgun piggy backs on the shotgun client actor, so most of the work is done in the actor -// class. because shotgun is very flexible, it's hard to write helpers, as entities/properties -// are entirely configurable. but we also don't want to put all the logic into the frontend. as -// python module may want access to this logic. - -// This value helps tune the rate that jobs to build media are processed, if it -// is zero xstudio tends to get overwhelmed when building large playlists, increasing -// the value means xstudio stays interactive at the cost of slowing the overall -#define JOB_DISPATCH_DELAY std::chrono::milliseconds(10) - -class BuildPlaylistMediaJob { - - public: - BuildPlaylistMediaJob( - caf::actor playlist_actor, - const utility::Uuid &media_uuid, - const std::string media_name, - utility::JsonStore sg_data, - utility::FrameRate media_rate, - std::string preferred_visual_source, - std::string preferred_audio_source, - std::shared_ptr event, - std::shared_ptr ordererd_uuids, - utility::Uuid before, - std::string flag_colour, - std::string flag_text, - caf::typed_response_promise response_promise, - std::shared_ptr result, - std::shared_ptr result_count) - : playlist_actor_(std::move(playlist_actor)), - media_uuid_(media_uuid), - media_name_(media_name), - sg_data_(sg_data), - media_rate_(media_rate), - preferred_visual_source_(std::move(preferred_visual_source)), - preferred_audio_source_(std::move(preferred_audio_source)), - event_msg_(std::move(event)), - ordererd_uuids_(std::move(ordererd_uuids)), - before_(std::move(before)), - flag_colour_(std::move(flag_colour)), - flag_text_(std::move(flag_text)), - response_promise_(std::move(response_promise)), - result_(std::move(result)), - result_count_(result_count) { - // increment a shared counter - the counter is shared between - // all the indiviaual Media creation jobs in a single build playlist - // task - (*result_count)++; - } - - BuildPlaylistMediaJob(const BuildPlaylistMediaJob &o) = default; - BuildPlaylistMediaJob() = default; - - ~BuildPlaylistMediaJob() { - // this gets destroyed when the job is done with. - if (media_actor_) { - result_->push_back(UuidActor(media_uuid_, media_actor_)); - } - // decrement the counter - (*result_count_)--; - - if (!(*result_count_)) { - // counter has dropped to zero, all jobs within a single build playlist - // tas are done. Our 'result' member here is in the order that the - // media items were created (asynchronously), rather than the order - // of the final playlist ... so we need to reorder our 'result' to - // match the ordering in the playlist - UuidActorVector reordered; - reordered.reserve(result_->size()); - for (const auto &uuid : (*ordererd_uuids_)) { - for (auto uai = result_->begin(); uai != result_->end(); uai++) { - if ((*uai).uuid() == uuid) { - reordered.push_back(*uai); - result_->erase(uai); - break; - } - } - } - response_promise_.deliver(reordered); - } - } - - caf::actor playlist_actor_; - utility::Uuid media_uuid_; - std::string media_name_; - utility::JsonStore sg_data_; - utility::FrameRate media_rate_; - std::string preferred_visual_source_; - std::string preferred_audio_source_; - std::shared_ptr event_msg_; - std::shared_ptr ordererd_uuids_; - utility::Uuid before_; - std::string flag_colour_; - std::string flag_text_; - caf::typed_response_promise response_promise_; - std::shared_ptr result_; - std::shared_ptr result_count_; - caf::actor media_actor_; -}; - -class ShotgunMediaWorker : public caf::event_based_actor { - public: - ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source); - ~ShotgunMediaWorker() override = default; - - const char *name() const override { return NAME.c_str(); } - - private: - inline static const std::string NAME = "ShotgunMediaWorker"; - caf::behavior make_behavior() override { return behavior_; } - - void add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate); - void add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate, - const UuidActor &movie_source); - void add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const UuidActorVector &srcs); - - private: - caf::behavior behavior_; - caf::actor_addr data_source_; -}; - -void ShotgunMediaWorker::add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate) { - request( - actor_cast(this), - infinite, - media::add_media_source_atom_v, - jsn, - media_rate, - true) - .then( - [=](const UuidActor &movie_source) mutable { - add_media_step_2(rp, media, jsn, media_rate, movie_source); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate, - const UuidActor &movie_source) { - // now get image.. - request( - actor_cast(this), infinite, media::add_media_source_atom_v, jsn, media_rate) - .then( - [=](const UuidActor &image_source) mutable { - // check to see if what we've got.. - // failed... - if (movie_source.uuid().is_null() and image_source.uuid().is_null()) { - spdlog::warn("{} No valid sources {}", __PRETTY_FUNCTION__, jsn.dump(2)); - rp.deliver(false); - } else { - try { - UuidActorVector srcs; - - if (not movie_source.uuid().is_null()) - srcs.push_back(movie_source); - if (not image_source.uuid().is_null()) - srcs.push_back(image_source); - - - add_media_step_3(rp, media, jsn, srcs); - - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const UuidActorVector &srcs) { - request(media, infinite, media::add_media_source_atom_v, srcs) - .then( - [=](const bool) mutable { - rp.deliver(true); - // push metadata to media actor. - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - jsn, - ShotgunMetadataPath + "/version"); - - // dispatch delayed shot data. - try { - auto shotreq = JsonStore(GetShotFromIdJSON); - shotreq["shot_id"] = - jsn.at("relationships").at("entity").at("data").value("id", 0); - - request( - caf::actor_cast(data_source_), - infinite, - get_data_atom_v, - shotreq) - .then( - [=](const JsonStore &jsn) mutable { - try { - if (jsn.count("data")) - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(jsn.at("data")), - ShotgunMetadataPath + "/shot"); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); -} - - -ShotgunMediaWorker::ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source) - : data_source_(std::move(source)), caf::event_based_actor(cfg) { - - // for each input we spawn one media item with upto two media sources. - - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - // movie - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate, - const bool /*movie*/) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_movie").is_null()) { - // spdlog::info("{}", i["attributes"]["sg_path_to_movie"]); - // prescan movie for duration.. - // it may contain slate, which needs trimming.. - // SLOW do we want to be offsetting the movie ? - // if we keep this code is needs threading.. - auto uri = posix_path_to_uri(jsn["attributes"]["sg_path_to_movie"]); - const auto source_uuid = Uuid::generate(); - auto source = spawn( - "SG Movie", uri, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - return rp; - }, - - // frames - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_frames").is_null()) { - FrameList frame_list; - caf::uri uri; - - if (jsn.at("attributes").at("frame_range").is_null()) { - // no frame range specified.. - // try and aquire from filesystem.. - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, true); - } else { - frame_list = FrameList( - jsn.at("attributes").at("frame_range").template get()); - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, false); - } - - const auto source_uuid = Uuid::generate(); - auto source = - frame_list.empty() - ? spawn( - "SG Frames", uri, media_rate, source_uuid) - : spawn( - "SG Frames", uri, frame_list, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - - return rp; - }, - - [=](playlist::add_media_atom, - caf::actor media, - JsonStore jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - - try { - // do stupid stuff, because data integrity is for losers. - // if we've got a movie in the sg_frames property, swap them over. - if (jsn.at("attributes").at("sg_path_to_movie").is_null() and - not jsn.at("attributes").at("sg_path_to_frames").is_null() and - jsn.at("attributes") - .at("sg_path_to_frames") - .template get() - .find_first_of('#') == std::string::npos) { - // movie in image sequence.. - jsn["attributes"]["sg_path_to_movie"] = - jsn.at("attributes").at("sg_path_to_frames"); - jsn["attributes"]["sg_path_to_frames"] = nullptr; - } - - // request movie .. THESE MUST NOT RETURN error on fail. - add_media_step_1(rp, media, jsn, media_rate); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - return rp; - }); -} - - -void ShotgunDataSource::set_authentication_method(const std::string &value) { - if (authentication_method_->value() != value) - authentication_method_->set_value(value); -} -void ShotgunDataSource::set_client_id(const std::string &value) { - if (client_id_->value() != value) - client_id_->set_value(value); -} -void ShotgunDataSource::set_client_secret(const std::string &value) { - if (client_secret_->value() != value) - client_secret_->set_value(value); -} -void ShotgunDataSource::set_username(const std::string &value) { - if (username_->value() != value) - username_->set_value(value); -} -void ShotgunDataSource::set_password(const std::string &value) { - if (password_->value() != value) - password_->set_value(value); -} -void ShotgunDataSource::set_session_token(const std::string &value) { - if (session_token_->value() != value) - session_token_->set_value(value); -} -void ShotgunDataSource::set_authenticated(const bool value) { - if (authenticated_->value() != value) - authenticated_->set_value(value); -} -void ShotgunDataSource::set_timeout(const int value) { - if (timeout_->value() != value) - timeout_->set_value(value); -} - -shotgun_client::AuthenticateShotgun ShotgunDataSource::get_authentication() const { - AuthenticateShotgun auth; - - auth.set_session_uuid(to_string(session_id_)); - - auth.set_authentication_method(authentication_method_->value()); - switch (*(auth.authentication_method())) { - case AM_SCRIPT: - auth.set_client_id(client_id_->value()); - auth.set_client_secret(client_secret_->value()); - break; - case AM_SESSION: - auth.set_session_token(session_token_->value()); - break; - case AM_LOGIN: - auth.set_username(expand_envvars(username_->value())); - auth.set_password(password_->value()); - break; - case AM_UNDEFINED: - default: - break; - } - - return auth; -} - -void ShotgunDataSource::add_attributes() { - - std::vector auth_method_names = { - "client_credentials", "password", "session_token"}; - - module::QmlCodeAttribute *button = add_qml_code_attribute( - "MyCode", - R"( -import Shotgun 1.0 -ShotgunButton {} -)"); - - button->set_role_data(module::Attribute::ToolbarPosition, 1010.0); - button->expose_in_ui_attrs_group("media_tools_buttons"); - - - authentication_method_ = add_string_choice_attribute( - "authentication_method", - "authentication_method", - "password", - auth_method_names, - auth_method_names); - - playlist_notes_action_ = - add_action_attribute("playlist_notes_to_shotgun", "playlist_notes_to_shotgun"); - selected_notes_action_ = - add_action_attribute("selected_notes_to_shotgun", "selected_notes_to_shotgun"); - - client_id_ = add_string_attribute("client_id", "client_id", ""); - client_secret_ = add_string_attribute("client_secret", "client_secret", ""); - username_ = add_string_attribute("username", "username", ""); - password_ = add_string_attribute("password", "password", ""); - session_token_ = add_string_attribute("session_token", "session_token", ""); - - authenticated_ = add_boolean_attribute("authenticated", "authenticated", false); - - // should be int.. - timeout_ = add_float_attribute("timeout", "timeout", 120.0, 10.0, 600.0, 1.0, 0); - - - // by setting static UUIDs on these module we only create them once in the UI - playlist_notes_action_->set_role_data( - module::Attribute::UuidRole, "92c780be-d0bc-462a-b09f-643e8986e2a1"); - playlist_notes_action_->set_role_data( - module::Attribute::Title, "Publish Playlist Notes..."); - playlist_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - playlist_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|Shotgun"})); - - selected_notes_action_->set_role_data( - module::Attribute::UuidRole, "7583a4d0-35d8-4f00-bc32-ae8c2bddc30a"); - selected_notes_action_->set_role_data( - module::Attribute::Title, "Publish Selected Notes..."); - selected_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - selected_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|Shotgun"})); - - authentication_method_->set_role_data( - module::Attribute::UuidRole, "ea7c47b8-a851-4f44-b9f1-3f5b38c11d96"); - client_id_->set_role_data( - module::Attribute::UuidRole, "31925e29-674f-4f03-a861-502a2bc92f78"); - client_secret_->set_role_data( - module::Attribute::UuidRole, "05d18793-ef4c-4753-8b55-1d98788eb727"); - username_->set_role_data( - module::Attribute::UuidRole, "a012c508-a8a7-4438-97ff-05fc707331d0"); - password_->set_role_data( - module::Attribute::UuidRole, "55982b32-3273-4f1c-8164-251d8af83365"); - session_token_->set_role_data( - module::Attribute::UuidRole, "d6fac6a6-a6c9-4ac3-b961-499d9862a886"); - authenticated_->set_role_data( - module::Attribute::UuidRole, "ce708287-222f-46b6-820c-f6dfda592ba9"); - timeout_->set_role_data( - module::Attribute::UuidRole, "9947a178-b5bb-4370-905e-c6687b2d7f41"); - - authentication_method_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_id_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_secret_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - username_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - password_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - session_token_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - authenticated_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - timeout_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - - authentication_method_->set_role_data( - module::Attribute::ToolTip, "Shotgun authentication method."); - - client_id_->set_role_data(module::Attribute::ToolTip, "Shotgun script key."); - client_secret_->set_role_data(module::Attribute::ToolTip, "Shotgun script secret."); - username_->set_role_data(module::Attribute::ToolTip, "Shotgun username."); - password_->set_role_data(module::Attribute::ToolTip, "Shotgun password."); - session_token_->set_role_data(module::Attribute::ToolTip, "Shotgun session token."); - authenticated_->set_role_data(module::Attribute::ToolTip, "Authenticated."); - timeout_->set_role_data(module::Attribute::ToolTip, "Shotgun server timeout."); -} - -void ShotgunDataSource::attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) { - // pass upto actor.. - call_attribute_changed(attr_uuid); -} - -template -void ShotgunDataSourceActor::attribute_changed(const utility::Uuid &attr_uuid) { - // properties changed somewhere. - // update loop ? - if (attr_uuid == data_source_.authentication_method_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.authentication_method_->value(), - "/plugin/data_source/shotgun/authentication/grant_type"); - } - if (attr_uuid == data_source_.client_id_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.client_id_->value(), - "/plugin/data_source/shotgun/authentication/client_id"); - } - // if (attr_uuid == data_source_.client_secret_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.client_secret_->value(), - // "/plugin/data_source/shotgun/authentication/client_secret"); - // } - if (attr_uuid == data_source_.timeout_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.timeout_->value(), "/plugin/data_source/shotgun/server/timeout"); - } - - if (attr_uuid == data_source_.username_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.username_->value(), - "/plugin/data_source/shotgun/authentication/username"); - } - // if (attr_uuid == data_source_.password_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.password_->value(), - // "/plugin/data_source/shotgun/authentication/password"); - // } - if (attr_uuid == data_source_.session_token_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.session_token_->value(), - "/plugin/data_source/shotgun/authentication/session_token"); - } -} - - -template -ShotgunDataSourceActor::ShotgunDataSourceActor( - caf::actor_config &cfg, const utility::JsonStore &) - : caf::event_based_actor(cfg) { - - data_source_.bind_attribute_changed_callback( - [this](auto &&PH1) { attribute_changed(std::forward(PH1)); }); - - spdlog::debug("Created ShotgunDataSourceActor {}", name()); - // print_on_exit(this, "MediaHookActor"); - secret_source_ = actor_cast(this); - - shotgun_ = spawn(); - link_to(shotgun_); - - // we need to recieve authentication updates. - join_event_group(this, shotgun_); - - // we are the source of the secret.. - anon_send(shotgun_, shotgun_authentication_source_atom_v, actor_cast(this)); - - system().registry().put(shotgun_datasource_registry, caf::actor_cast(this)); - - try { - auto prefs = GlobalStoreHelper(system()); - JsonStore j; - join_broadcast(this, prefs.get_group(j)); - update_preferences(j); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), - worker_count_, - [&] { - return system().template spawn( - actor_cast(this)); - }, - caf::actor_pool::round_robin()); - link_to(pool_); - - // data_source_.connect_to_ui(); coz async - data_source_.set_parent_actor_addr(actor_cast(this)); - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(500), - module::connect_to_ui_atom_v); - - behavior_.assign( - [=](utility::name_atom) -> std::string { return name(); }, - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](data_source::use_data_atom, const caf::actor &media, const FrameRate &media_rate) - -> result { return UuidActorVector(); }, - - // no drop support.. - [=](data_source::use_data_atom, const JsonStore &, const FrameRate &, const bool) - -> UuidActorVector { return UuidActorVector(); }, - - [=](data_source::use_data_atom, - const std::string &project, - const utility::Uuid &stalk_dnuuid) { - auto jsre = JsonStore(GetVersionIvyUuidJSON); - jsre["ivy_uuid"] = to_string(stalk_dnuuid); - jsre["job"] = project; - delegate(caf::actor_cast(this), get_data_atom_v, jsre); - }, - - [=](shotgun_projects_atom atom) { delegate(shotgun_, atom); }, - - [=](shotgun_groups_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_schema_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_authentication_source_atom, caf::actor source) { - secret_source_ = actor_cast(source); - }, - - [=](shotgun_authentication_source_atom) -> caf::actor { - return actor_cast(secret_source_); - }, - - [=](shotgun_update_entity_atom atom, - const std::string &entity, - const int record_id, - const JsonStore &body) { delegate(shotgun_, atom, entity, record_id, body); }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail) { delegate(shotgun_, atom, entity, record_id, thumbnail); }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail, - const bool as_buffer) { - delegate(shotgun_, atom, entity, record_id, thumbnail, as_buffer); - }, - - [=](shotgun_upload_atom atom, - const std::string &entity, - const int record_id, - const std::string &field, - const std::string &name, - const std::vector &data, - const std::string &content_type) { - delegate(shotgun_, atom, entity, record_id, field, name, data, content_type); - }, - - // just use the default with jsonstore ? - [=](put_data_atom, const utility::JsonStore &js) -> result { - try { - if (js["entity"] == "Playlist" and js["relationship"] == "Version") { - auto rp = make_response_promise(); - update_playlist_versions(rp, Uuid(js["playlist_uuid"])); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // do we need the UI to have spun up before we can issue calls to shotgun... - // erm... - [=](use_data_atom atom, const caf::uri &uri) { - delegate(actor_cast(this), atom, uri, FrameRate()); - }, - [=](use_data_atom, - const caf::uri &uri, - const FrameRate &media_rate) -> result { - // check protocol == shotgun.. - if (uri.scheme() != "shotgun") - return UuidActorVector(); - - if (to_string(uri.authority()) == "load") { - // need type and id - auto query = uri.query(); - if (query.count("type") and query["type"] == "Version" and query.count("ids")) { - auto ids = split(query["ids"], '|'); - if (ids.empty()) - return UuidActorVector(); - - auto count = std::make_shared(ids.size()); - auto rp = make_response_promise(); - auto results = std::make_shared(); - - for (const auto i : ids) { - try { - auto type = query["type"]; - auto squery = R"({})"_json; - squery["id"] = i; - - request( - caf::actor_cast(this), - std::chrono::seconds( - static_cast(data_source_.timeout_->value())), - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(squery), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](const JsonStore &js) mutable { - // load version.. - request( - caf::actor_cast(this), - infinite, - playlist::add_media_atom_v, - js, - utility::Uuid(), - caf::actor(), - utility::Uuid()) - .then( - [=](const UuidActorVector &uav) mutable { - (*count)--; - - for (const auto &ua : uav) - results->push_back(ua); - - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - (*count)--; - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - if (not(*count)) - rp.deliver(*results); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - (*count)--; - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - return rp; - } else if ( - query.count("type") and query["type"] == "Playlist" and - query.count("ids")) { - // will return an array of playlist actors.. - auto ids = split(query["ids"], '|'); - if (ids.empty()) - return UuidActorVector(); - - auto rp = make_response_promise(); - auto count = std::make_shared(ids.size()); - auto results = std::make_shared(); - - for (const auto i : ids) { - auto id = std::atoi(i.c_str()); - auto js = JsonStore(LoadPlaylistJSON); - js["playlist_id"] = id; - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - js, - caf::actor()) - .then( - [=](const UuidActor &ua) mutable { - // process result to build playlist.. - (*count)--; - results->push_back(ua); - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - (*count)--; - if (not(*count)) - rp.deliver(*results); - }); - } - } else { - spdlog::warn( - "Invalid shotgun action {}, requires type, id", to_string(uri)); - } - } else { - spdlog::warn( - "Invalid shotgun action {} {}", to_string(uri.authority()), to_string(uri)); - } - - return UuidActorVector(); - }, - - [=](use_data_atom, - const utility::JsonStore &js, - const caf::actor &session) -> result { - try { - if (js.at("entity") == "Playlist" and js.count("playlist_id")) { - auto rp = make_response_promise(); - load_playlist(rp, js.at("playlist_id").get(), session); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // just use the default with jsonstore ? - [=](use_data_atom, const utility::JsonStore &js) -> result { - try { - if (js.at("entity") == "Playlist" and js.count("playlist_id")) { - scoped_actor sys{system()}; - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto rp = make_response_promise(); - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - js, - session) - .then( - [=](const UuidActor &) mutable { - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }); - - return rp; - } else if ( - js.at("entity") == "Playlist" and js.at("relationship") == "Version") { - auto rp = make_response_promise(); - refresh_playlist_versions(rp, Uuid(js.at("playlist_uuid"))); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // just use the default with jsonstore ? - - [=](post_data_atom, const utility::JsonStore &js) -> result { - try { - if (js["entity"] == "Note") { - auto rp = make_response_promise(); - create_playlist_notes(rp, js["payload"], JsonStore(js["playlist_uuid"])); - return rp; - } - if (js["entity"] == "Playlist") { - auto rp = make_response_promise(); - create_playlist(rp, js); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - - return make_error(xstudio_error::error, "Invalid operation."); - }, - - [=](shotgun_entity_atom atom, - const std::string &entity, - const int record_id, - const std::vector &fields) { - delegate(shotgun_, atom, entity, record_id, fields); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort) { - delegate(shotgun_, atom, entity, filter, fields, sort); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, filter, fields, sort, page, page_size); - }, - - [=](shotgun_schema_entity_fields_atom atom, - const std::string &entity, - const std::string &field, - const int id) { delegate(shotgun_, atom, entity, field, id); }, - - [=](shotgun_entity_search_atom atom, - const std::string &entity, - const JsonStore &conditions, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, conditions, fields, sort, page, page_size); - }, - - [=](shotgun_text_search_atom atom, - const std::string &text, - const JsonStore &conditions, - const int page, - const int page_size) { - delegate(shotgun_, atom, text, conditions, page, page_size); - }, - - // can't reply via qt mixin.. this is a work around.. - [=](shotgun_acquire_authentication_atom, const bool cancelled) { - if (cancelled) { - data_source_.set_authenticated(false); - for (auto &i : waiting_) - i.deliver( - make_error(xstudio_error::error, "Authentication request cancelled.")); - } else { - auto auth = data_source_.get_authentication(); - if (waiting_.empty()) { - anon_send(shotgun_, shotgun_authenticate_atom_v, auth); - } else { - for (auto &i : waiting_) - i.deliver(auth); - } - } - waiting_.clear(); - }, - - [=](shotgun_acquire_authentication_atom atom, - const std::string &message) -> result { - if (secret_source_ == actor_cast(this)) - return make_error(xstudio_error::error, "No authentication source."); - - auto rp = make_response_promise(); - waiting_.push_back(rp); - data_source_.set_authenticated(false); - anon_send(actor_cast(secret_source_), atom, message); - return rp; - }, - - [=](utility::event_atom, - shotgun_acquire_token_atom, - const std::pair &tokens) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - tokens.second, - "/plugin/data_source/shotgun/authentication/refresh_token", - false); - prefs.save("APPLICATION"); - data_source_.set_authenticated(true); - }, - - [=](playlist::add_media_atom, - const utility::JsonStore &data, - const utility::Uuid &playlist_uuid, - const caf::actor &playlist, - const utility::Uuid &before) -> result> { - auto rp = make_response_promise>(); - add_media_to_playlist(rp, data, playlist_uuid, playlist, before); - return rp; - }, - - [=](playlist::add_media_atom) { - // this message handler is called in a loop until all build media - // tasks in the queue are exhausted - - bool is_ivy_build_task; - - auto build_media_task_data = get_next_build_task(is_ivy_build_task); - while (build_media_task_data) { - - if (is_ivy_build_task) { - - do_add_media_sources_from_ivy(build_media_task_data); - - } else { - - do_add_media_sources_from_shotgun(build_media_task_data); - } - - // N.B. we only get a new build task if the number of incomplete tasks - // already dispatched is less than the number of actors in our - // worker pool - build_media_task_data = get_next_build_task(is_ivy_build_task); - } - }, - - /*[=](playlist::add_media_atom, - caf::actor playlist, - JsonStore versions_to_add, - const utility::Uuid job_uuid, - const utility::Uuid before, - const FrameRate media_rate, - const bool only_movies) { - - if (versions_to_add.empty()) { - // versions_to_add is now empty - deliver on the job uuid - job_response_promise_[job_uuid].deliver(job_result_[job_uuid]); - job_result_.erase(job_result_.find(job_uuid)); - job_response_promise_.erase(job_response_promise_.find(job_uuid)); - return; - } - - while (job_inflight_[job_uuid] < 10) { - // the version to add (first item in 'versions') - JsonStore version(versions_to_add.begin().value()); - // erase it from 'versions', which is used to recursively calling this message - handler versions_to_add.erase(versions_to_add.begin()); job_inflight_[job_uuid]++; - request(pool_, caf::infinite, playlist::add_media_atom_v, version, media_rate, - only_movies).then( - [=](const UuidActor &res) mutable { - request( - playlist, - infinite, - playlist::add_media_atom_v, - res, - before).then( - [=](const UuidActor& r) mutable { - job_result_[job_uuid].push_back(r); - job_inflight_[job_uuid]--; - // continue processing this job - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - job_inflight_[job_uuid]--; - }); - }, - [=](const caf::error &err) mutable { - job_inflight_[job_uuid]--; - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - if (versions_to_add.empty()) break; - } - send(this, playlist::add_media_atom_v, playlist, versions_to_add, job_uuid, before, - media_rate, only_movies); - - },*/ - - // not used. - [=](get_data_atom, - const std::string &entity, - const utility::JsonStore &conditions) -> result { - auto rp = make_response_promise(); - request( - shotgun_, - infinite, - shotgun_entity_search_atom_v, - entity, - conditions, - VersionFields, - std::vector(), - 1, - 30) - .then( - [=](const JsonStore &proj) mutable { rp.deliver(proj); }, - [=](error &err) mutable { rp.deliver(err); }); - return rp; - }, - - [=](get_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - try { - if (js.at("operation") == "VersionFromIvy") { - find_ivy_version( - rp, - js.at("ivy_uuid").get(), - js.at("job").get()); - } else if (js.at("operation") == "GetShotFromId") { - find_shot(rp, js.at("shot_id").get()); - } else if (js.at("operation") == "LinkMedia") { - link_media(rp, utility::Uuid(js.at("playlist_uuid"))); - } else if (js.at("operation") == "DownloadMedia") { - download_media(rp, utility::Uuid(js.at("media_uuid"))); - } else if (js.at("operation") == "MediaCount") { - get_valid_media_count(rp, utility::Uuid(js.at("playlist_uuid"))); - } else if (js.at("operation") == "PrepareNotes") { - UuidVector media_uuids; - for (const auto &i : js.value("media_uuids", std::vector())) - media_uuids.push_back(Uuid(i)); - - prepare_playlist_notes( - rp, - utility::Uuid(js.at("playlist_uuid")), - media_uuids, - js.value("notify_owner", false), - js.value("notify_group_ids", std::vector()), - js.value("combine", false), - js.value("add_time", false), - js.value("add_playlist_name", false), - js.value("add_type", false), - js.value("anno_requires_note", true), - js.value("skip_already_published", false), - js.value("default_type", "")); - } else { - rp.deliver( - make_error(xstudio_error::error, std::string("Invalid operation."))); - } - } catch (const std::exception &err) { - rp.deliver(make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } - return rp; - }, - - [=](json_store::update_atom, - const JsonStore & /*change*/, - const std::string & /*path*/, - const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); - }, - - [=](json_store::update_atom, const JsonStore &js) { - try { - update_preferences(js); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }); -} - -template void ShotgunDataSourceActor::on_exit() { - // maybe on timer.. ? - for (auto &i : waiting_) - i.deliver(make_error(xstudio_error::error, "Password request cancelled.")); - waiting_.clear(); - system().registry().erase(shotgun_datasource_registry); -} - -template void ShotgunDataSourceActor::update_preferences(const JsonStore &js) { - try { - auto grant = preference_value( - js, "/plugin/data_source/shotgun/authentication/grant_type"); - - auto client_id = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_id"); - auto client_secret = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_secret"); - auto username = preference_value( - js, "/plugin/data_source/shotgun/authentication/username"); - auto password = preference_value( - js, "/plugin/data_source/shotgun/authentication/password"); - auto session_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/session_token"); - - auto refresh_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/refresh_token"); - - auto host = - preference_value(js, "/plugin/data_source/shotgun/server/host"); - auto port = preference_value(js, "/plugin/data_source/shotgun/server/port"); - auto protocol = - preference_value(js, "/plugin/data_source/shotgun/server/protocol"); - auto timeout = preference_value(js, "/plugin/data_source/shotgun/server/timeout"); - - - auto cache_dir = expand_envvars( - preference_value(js, "/plugin/data_source/shotgun/download/path")); - auto cache_size = - preference_value(js, "/plugin/data_source/shotgun/download/size"); - - download_cache_.prune_on_exit(true); - download_cache_.target(cache_dir, true); - download_cache_.max_size(cache_size * 1024 * 1024 * 1024); - - auto category = preference_value(js, "/core/bookmark/category"); - category_colours_.clear(); - if (category.is_array()) { - for (const auto &i : category) { - category_colours_[i.value("value", "default")] = i.value("colour", ""); - } - } - - // no op ? - data_source_.set_authentication_method(grant); - data_source_.set_client_id(client_id); - data_source_.set_client_secret(client_secret); - data_source_.set_username(expand_envvars(username)); - data_source_.set_password(password); - data_source_.set_session_token(session_token); - data_source_.set_timeout(timeout); - - // what hppens if we get a sequence of changes... should this be on a timed event ? - // watch out for multiple instances. - auto new_hash = std::hash{}( - grant + username + client_id + host + std::to_string(port) + protocol); - - if (new_hash != changed_hash_) { - changed_hash_ = new_hash; - // set server - anon_send( - shotgun_, - shotgun_host_atom_v, - std::string(fmt::format( - "{}://{}{}", protocol, host, (port ? ":" + std::to_string(port) : "")))); - - auto auth = data_source_.get_authentication(); - if (not refresh_token.empty()) - auth.set_refresh_token(refresh_token); - - anon_send(shotgun_, shotgun_credential_atom_v, auth); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -template -void ShotgunDataSourceActor::update_playlist_versions( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const int playlist_id) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto pl_id = playlist_id; - if (not pl_id) { - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - pl_id = plsg["id"].template get(); - } - - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // foreach medai actor get it's shogtun metadata. - auto jsn = R"({"versions":[]})"_json; - auto ver = R"({"id": 0, "type": "Version"})"_json; - - std::map version_order_map; - // get media detail - int sort_order = 1; - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - auto id = mjson["id"].template get(); - ver["id"] = id; - jsn["versions"].push_back(ver); - version_order_map[id] = sort_order; - - sort_order++; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // update playlist - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - "Playlists", - pl_id, - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - // spdlog::warn("{}", JsonStore(result["data"]).dump(2)); - // update playorder.. - // get PlaylistVersionConnections - scoped_actor sys{system()}; - - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = pl_id; - - try { - auto order = request_receive( - *sys, - shotgun_, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999); - // update all PlaylistVersionConnection's with new sort_order. - for (const auto &i : order["data"]) { - auto version_id = i.at("relationships") - .at("version") - .at("data") - .at("id") - .get(); - auto sort_order = - i.at("attributes").at("sg_sort_order").is_null() - ? 0 - : i.at("attributes").at("sg_sort_order").get(); - // spdlog::warn("{} {}", std::to_string(sort_order), - // std::to_string(version_order_map[version_id])); - if (sort_order != version_order_map[version_id]) { - auto so_jsn = R"({"sg_sort_order": 0})"_json; - so_jsn["sg_sort_order"] = version_order_map[version_id]; - try { - request_receive( - *sys, - shotgun_, - shotgun_update_entity_atom_v, - "PlaylistVersionConnection", - i.at("id").get(), - utility::JsonStore(so_jsn)); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - - if (pl_id != playlist_id) - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - // need to update/add PlaylistVersionConnection's - // on creation the sort_order will be null. - // PlaylistVersionConnection are auto created when adding to playlist. (I think) - // so all we need to do is update.. - - - // get shotgun metadata - // get media actors. - // get media shotgun metadata. - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::refresh_playlist_versions( - caf::typed_response_promise rp, const utility::Uuid &playlist_uuid) { - // grab playlist id, get versions compare/load into playlist - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - auto pl_id = plsg["id"].template get(); - - // this is a list of the media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - - // foreach media actor get it's shogtun metadata. - std::set current_version_ids; - - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - current_version_ids.insert(mjson["id"].template get()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // we got media shotgun ids, plus playlist id - // get current shotgun playlist/versions - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - pl_id, - std::vector()) - .then( - [=](const JsonStore &result) mutable { - try { - scoped_actor sys{system()}; - // update playlist - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - - // gather versions, to get more detail.. - std::vector version_ids; - for (const auto &i : - result.at("data").at("relationships").at("versions").at("data")) { - if (not current_version_ids.count(i.at("id").template get())) - version_ids.emplace_back( - std::to_string(i.at("id").template get())); - } - - if (version_ids.empty()) { - rp.deliver(result); - return; - } - - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get details.. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 1000) - .then( - [=](const JsonStore &result2) mutable { - try { - // got version details. - // we can now just call add versions to playlist.. - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - result2, - playlist_uuid, - playlist, - utility::Uuid()); - - // return this as the result. - rp.deliver(result); - - } catch (const std::exception &err) { - rp.deliver( - make_error(xstudio_error::error, err.what())); - } - }, - - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_playlist( - caf::typed_response_promise rp, const utility::JsonStore &js) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto playlist_uuid = Uuid(js["playlist_uuid"]); - auto project_id = js["project_id"].template get(); - auto code = js["code"].template get(); - auto loc = js["location"].template get(); - auto playlist_type = js["playlist_type"].template get(); - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto jsn = R"({ - "project":{ "type": "Project", "id":null }, - "code": null, - "sg_location": "unknown", - "sg_type": "Dailies", - "sg_date_and_time": null - })"_json; - - jsn["project"]["id"] = project_id; - jsn["code"] = code; - jsn["sg_location"] = loc; - jsn["sg_type"] = playlist_type; - jsn["sg_date_and_time"] = date_time_and_zone(); - - // "2021-08-18T19:00:00Z" - - // need to capture result to embed in playlist and add any media.. - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "playlists", - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - try { - // get new playlist id.. - auto playlist_id = result.at("data").at("id").template get(); - // update shotgun versions from our source playlist. - // return the result.. - update_playlist_versions(rp, playlist_uuid, playlist_id); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - - -template -void ShotgunDataSourceActor::add_media_to_playlist( - caf::typed_response_promise rp, - const utility::JsonStore &data, - utility::Uuid playlist_uuid, - caf::actor playlist, - const utility::Uuid &before) { - // data can be in multiple forms.. - - auto sys = caf::scoped_actor(system()); - - nlohmann::json versions; - try { - versions = data.at("data").at("relationships").at("versions").at("data"); - } catch (...) { - try { - versions = data.at("data"); - } catch (...) { - return rp.deliver(make_error(xstudio_error::error, "Invalid JSON")); - ; - } - } - - if (versions.empty()) - return rp.deliver(std::vector()); - - auto event_msg = std::shared_ptr(); - - - // get uuid for playlist - if (playlist and playlist_uuid.is_null()) { - try { - playlist_uuid = - request_receive(*sys, playlist, utility::uuid_atom_v); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist = caf::actor(); - } - } - - // get playlist for uuid - if (not playlist and not playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist_uuid = utility::Uuid(); - } - } - - // create playlist.. - if (not playlist and playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist_uuid = utility::Uuid::generate(); - playlist = spawn("Shotgun Media", playlist_uuid, session); - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not playlist_uuid.is_null()) { - event_msg = std::make_shared( - "Loading Shotgun Playlist Media {}", - 0, - 0, - versions.size(), // we increment progress once per version loaded - ivy leafs are - // added after progress hits 100% - std::set({playlist_uuid})); - event::send_event(this, *event_msg); - } - - try { - auto media_rate = - request_receive(*sys, playlist, session::media_rate_atom_v); - - std::string flag_text, flag_colour; - if (not data.value("flag_text", "").empty() and - not data.value("flag_colour", "").empty()) { - flag_colour = data.value("flag_colour", ""); - flag_text = data.value("flag_text", ""); - } - - // we need to ensure that media are added to playlist IN ORDER - this - // is a bit fiddly because media are created out of order by the worker - // pool so we use this utility::UuidList to ensure that the playlist builds - // with media in order - auto ordered_uuids = std::make_shared(); - auto result = std::make_shared(); - auto result_count = std::make_shared(0); - - // get a new media item created for each of the names in our list - for (const auto &i : versions) { - - std::string name(i.at("attributes").at("code")); - - // create a task data item, with the raw shotgun data that - // can be used to build the media sources for each media - // item in the playlist - ordered_uuids->push_back(utility::Uuid::generate()); - build_playlist_media_tasks_.emplace_back(std::make_shared( - playlist, - ordered_uuids->back(), - name, // name for the media - JsonStore(i), - media_rate, - data.value("preferred_visual_source", ""), - data.value("preferred_audio_source", ""), - event_msg, - ordered_uuids, - before, - flag_colour, - flag_text, - rp, - result, - result_count)); - } - - // this call starts the work of building the media and consuming - // the jobs in the 'build_playlist_media_tasks_' queue - send(this, playlist::add_media_atom_v); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - if (not playlist_uuid.is_null()) { - event_msg->set_complete(); - event::send_event(this, *event_msg); - } - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::get_valid_media_count( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "") - .then( - [=](std::vector json) mutable { - int count = 0; - for (const auto &i : json) { - try { - if (i["metadata"].count("shotgun")) - count++; - } catch (...) { - } - } - - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = count; - result["result"]["invalid"] = json.size() - count; - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - - -template -void ShotgunDataSourceActor::download_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find media - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto media = - request_receive(*sys, session, playlist::get_media_atom_v, uuid); - - // get metadata, we need version id.. - auto media_metadata = request_receive( - *sys, - media, - json_store::get_json_atom_v, - utility::Uuid(), - "/metadata/shotgun/version"); - - // spdlog::warn("{}", media_metadata.dump(2)); - - auto name = media_metadata.at("attributes").at("code").template get(); - auto job = - media_metadata.at("attributes").at("sg_project_name").template get(); - auto shot = media_metadata.at("relationships") - .at("entity") - .at("data") - .at("name") - .template get(); - auto filepath = download_cache_.target_string() + "/" + name + "-" + job + "-" + shot + - ".dneg.webm"; - - - // check it doesn't already exist.. - if (fs::exists(filepath)) { - // create source and add to media - auto uuid = Uuid::generate(); - auto source = spawn( - "Shotgun Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request(media, infinite, media::add_media_source_atom_v, UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } else { - request( - shotgun_, - infinite, - shotgun_attachment_atom_v, - "version", - media_metadata.at("id").template get(), - "sg_uploaded_movie_webm") - .then( - [=](const std::string &data) mutable { - if (data.size() > 1024 * 15) { - // write to file - std::ofstream o(filepath); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - o << data << std::endl; - o.close(); - - // file written add to media as new source.. - auto uuid = Uuid::generate(); - auto source = spawn( - "Shotgun Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request( - media, - infinite, - media::add_media_source_atom_v, - UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore( - (R"({})"_json)["error"] = to_string(err))); - }); - - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(filepath); - } - spdlog::warn("Failed to open file"); - } - } else { - rp.deliver( - JsonStore((R"({})"_json)["error"] = "Failed to download")); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } - // "content_type": "video/webm", - // "id": 88463162, - // "link_type": "upload", - // "name": "b'tmp_upload_webm_0okvakz6.webm'", - // "type": "Attachment", - // "url": "http://shotgun.dneg.com/file_serve/attachment/88463162" - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(JsonStore((R"({})"_json)["error"] = err.what())); - } -} - -template -void ShotgunDataSourceActor::link_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // scan media for shotgun version / ivy uuid - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "", - true) - .then( - [=](std::vector> json) mutable { - // ivy uuid is stored on source not media.. balls. - auto left = std::make_shared(0); - auto invalid = std::make_shared(0); - for (const auto &i : json) { - try { - if (i.second.is_null() or - not i.second["metadata"].count("shotgun")) { - // request current media source metadata.. - scoped_actor sys{system()}; - auto source_meta = request_receive( - *sys, - i.first.actor(), - json_store::get_json_atom_v, - "/metadata/external/DNeg"); - // we has got it.. - auto ivy_uuid = source_meta.at("Ivy").at("dnuuid"); - auto job = source_meta.at("show"); - auto shot = source_meta.at("shot"); - (*left) += 1; - // spdlog::warn("{} {} {} {}", job, shot, ivy_uuid, *left); - // call back into self ? - // but we need to wait for the final result.. - // maybe in danger of deadlocks... - // now we need to query shotgun.. - // to try and find version from this information. - // this is then used to update the media actor. - auto jsre = JsonStore(GetVersionIvyUuidJSON); - jsre["ivy_uuid"] = ivy_uuid; - jsre["job"] = job; - - request( - caf::actor_cast(this), - infinite, - get_data_atom_v, - jsre) - .then( - [=](const JsonStore &ver) mutable { - // got ver from uuid - (*left)--; - if (ver["payload"].empty()) { - (*invalid)++; - } else { - // push version to media object - scoped_actor sys{system()}; - try { - request_receive( - *sys, - i.first.actor(), - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(ver["payload"]), - ShotgunMetadataPath + "/version"); - } catch (const std::exception &err) { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - err.what()); - } - } - - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*left)--; - (*invalid)++; - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not(*left)) { - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = json.size(); - result["result"]["invalid"] = 0; - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::find_ivy_version( - caf::typed_response_promise rp, - const std::string &uuid, - const std::string &job) { - // find version from supplied details. - - auto version_filter = - FilterBy().And(Text("project.Project.name").is(job), Text("sg_ivy_dnuuid").is(uuid)); - - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_search_atom_v, - "Version", - JsonStore(version_filter), - VersionFields, - std::vector(), - 1, - 1) - .then( - [=](const JsonStore &jsn) mutable { - auto result = JsonStore(R"({"payload":[]})"_json); - if (jsn.count("data") and jsn.at("data").size()) { - result["payload"] = jsn.at("data")[0]; - } - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"payload":[]})"_json)); - }); -} - -template -void ShotgunDataSourceActor::find_shot( - caf::typed_response_promise rp, const int shot_id) { - // find version from supplied details. - if (shot_cache_.count(shot_id)) - rp.deliver(shot_cache_.at(shot_id)); - - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_atom_v, - "Shot", - shot_id, - ShotFields) - .then( - [=](const JsonStore &jsn) mutable { - shot_cache_[shot_id] = jsn; - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"data":{}})"_json)); - }); -} - -template -void ShotgunDataSourceActor::prepare_playlist_notes( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const utility::UuidVector &media_uuids, - const bool notify_owner, - const std::vector notify_group_ids, - const bool combine, - const bool add_time, - const bool add_playlist_name, - const bool add_type, - const bool anno_requires_note, - const bool skip_already_pubished, - const std::string &default_type) { - - auto playlist_name = std::string(); - auto playlist_id = int(0); - auto payload = R"({"payload":[], "valid": 0, "invalid": 0})"_json; - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - // get playlist - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - // get shotgun info from playlist.. - try { - auto sgpl = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - playlist_name = sgpl.at("attributes").at("code").template get(); - playlist_id = sgpl.at("id").template get(); - - } catch (const std::exception &err) { - spdlog::info("No shotgun playlist information"); - } - - // get media for playlist. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // no media so no point.. - // nothing to publish. - if (media.empty()) - return rp.deliver(JsonStore(payload)); - - std::vector media_actors; - - if (not media_uuids.empty()) { - auto lookup = uuidactor_vect_to_map(media); - for (const auto &i : media_uuids) { - if (lookup.count(i)) - media_actors.push_back(lookup[i]); - } - } else { - media_actors = vector_to_caf_actor_vector(media); - } - - // get media shotgun json.. - // we can only publish notes for media that has version information - fan_out_request( - media_actors, - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath, - true) - .then( - [=](std::vector> version_meta) mutable { - auto result = JsonStore(payload); - - scoped_actor sys{system()}; - - std::map> media_map; - UuidVector valid_media; - - // get valid media. - // get all the shotgun info we need to publish - for (const auto &i : version_meta) { - try { - // spdlog::warn("{}", i.second.dump(2)); - const auto &version = i.second.at("version"); - auto jsn = JsonStore(PublishNoteTemplateJSON); - - // project - jsn["payload"]["project"]["id"] = version.at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - - - // playlist link - jsn["payload"]["note_links"][0]["id"] = playlist_id; - - if (version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Sequence") - // shot link - jsn["payload"]["note_links"][1]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - else if ( - version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Shot") - // sequence link - jsn["payload"]["note_links"][2]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - - // version link - jsn["payload"]["note_links"][3]["id"] = version.value("id", 0); - - if (jsn["payload"]["note_links"][3]["id"].get() == 0) - jsn["payload"]["note_links"].erase(3); - if (jsn["payload"]["note_links"][2]["id"].get() == 0) - jsn["payload"]["note_links"].erase(2); - if (jsn["payload"]["note_links"][1]["id"].get() == 0) - jsn["payload"]["note_links"].erase(1); - if (jsn["payload"]["note_links"][0]["id"].get() == 0) - jsn["payload"]["note_links"].erase(0); - - // we don't pass these to shotgun.. - jsn["shot"] = version.at("relationships") - .at("entity") - .at("data") - .at("name") - .get(); - jsn["playlist_name"] = playlist_name; - - if (notify_owner) // 1068 - jsn["payload"]["addressings_to"][0]["id"] = - version.at("relationships") - .at("user") - .at("data") - .at("id") - .get(); - else - jsn["payload"].erase("addressings_to"); - - if (not notify_group_ids.empty()) { - auto grp = R"({ "type": "Group", "id": null})"_json; - for (const auto g : notify_group_ids) { - if (g <= 0) - continue; - - grp["id"] = g; - jsn["payload"]["addressings_cc"].push_back(grp); - } - } - - if (jsn["payload"]["addressings_cc"].empty()) - jsn["payload"].erase("addressings_cc"); - - - media_map[i.first.uuid()] = std::make_pair(i.first, jsn); - valid_media.push_back(i.first.uuid()); - } catch (const std::exception &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - // get bookmark manager. - auto bookmarks = request_receive( - *sys, session, bookmark::get_bookmark_atom_v); - - // // collect media notes if they have shotgun metadata on the media - auto existing_bookmarks = - request_receive>>( - *sys, bookmarks, bookmark::get_bookmarks_atom_v, valid_media); - - // get bookmark detail.. - for (const auto &i : existing_bookmarks) { - // grouped by media item. - // we may want to collapse to unique note_types - std::map>> - notes_by_type; - - for (const auto &j : i.second) { - try { - if (skip_already_pubished) { - auto already_published = false; - try { - // check for shotgun metadata on note. - request_receive( - *sys, - j.actor(), - json_store::get_json_atom_v, - ShotgunMetadataPath + "/note"); - already_published = true; - } catch (...) { - } - - if (already_published) - continue; - } - - auto detail = request_receive( - *sys, j.actor(), bookmark::bookmark_detail_atom_v); - // skip notes with no text unless annotated and - // only_with_annotation is true - auto has_note = detail.note_ and not(*(detail.note_)).empty(); - auto has_anno = - detail.has_annotation_ and *(detail.has_annotation_); - - if (not(has_note or (has_anno and not anno_requires_note))) - continue; - - auto [ua, jsn] = media_map[detail.owner_->uuid()]; - // push to shotgun client.. - jsn["bookmark_uuid"] = j.uuid(); - if (not jsn.count("has_annotation")) - jsn["has_annotation"] = R"([])"_json; - - if (has_anno) { - auto item = - R"({"media_uuid": null, "media_name": null, "media_frame": 0, "timecode_frame": 0})"_json; - item["media_uuid"] = i.first; - item["media_name"] = jsn["shot"]; - item["media_frame"] = detail.start_frame(); - item["timecode_frame"] = - detail.start_timecode_tc().total_frames(); - // requires media actor and first frame of annotation. - jsn["has_annotation"].push_back(item); - } - auto cat = detail.category_ ? *(detail.category_) : ""; - if (not default_type.empty()) - cat = default_type; - - jsn["payload"]["sg_note_type"] = cat; - jsn["payload"]["subject"] = - detail.subject_ ? *(detail.subject_) : ""; - // format note content - std::string content; - - if (add_time) - content += std::string("Frame : ") + - std::to_string( - detail.start_timecode_tc().total_frames()) + - " / " + detail.start_timecode() + " / " + - detail.duration_timecode() + "\n"; - - content += *(detail.note_); - - jsn["payload"]["content"] = content; - - // yeah this is a bit convoluted. - if (not notes_by_type.count(cat)) { - notes_by_type.insert(std::make_pair( - cat, - std::map>( - {{detail.start_frame(), {{jsn}}}}))); - } else { - if (notes_by_type[cat].count(detail.start_frame())) { - notes_by_type[cat][detail.start_frame()].push_back(jsn); - } else { - notes_by_type[cat].insert(std::make_pair( - detail.start_frame(), - std::vector({jsn}))); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - try { - auto merged = JsonStore(); - - // category - for (auto &k : notes_by_type) { - auto category = k.first; - // frame - for (const auto &j : k.second) { - // entry - for (const auto ¬epayload : j.second) { - // spdlog::warn("{}",notepayload.dump(2)); - - if (not merged.is_null() and - (not combine or - merged["payload"]["sg_note_type"] != - notepayload["payload"]["sg_note_type"])) { - // spdlog::warn("{}", merged.dump(2)); - result["payload"].push_back(merged); - merged = JsonStore(); - } - - if (merged.is_null()) { - merged = notepayload; - auto content = std::string(); - if (add_playlist_name and - not merged["playlist_name"] - .get() - .empty()) - content += - "Playlist : " + - std::string(merged["playlist_name"]) + "\n"; - if (add_type) - content += "Note Type : " + - merged["payload"]["sg_note_type"] - .get() + - "\n\n"; - else - content += "\n\n"; - - merged["payload"]["content"] = - content + - merged["payload"]["content"].get(); - - merged.erase("shot"); - merged.erase("playlist_name"); - } else { - merged["payload"]["content"] = - merged["payload"]["content"] - .get() + - "\n\n" + - notepayload["payload"]["content"] - .get(); - merged["has_annotation"].insert( - merged["has_annotation"].end(), - notepayload["has_annotation"].begin(), - notepayload["has_annotation"].end()); - } - } - } - } - - if (not merged.is_null()) - result["payload"].push_back(merged); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - result["valid"] = result["payload"].size(); - - // spdlog::warn("{}", result.dump(2)); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(payload)); - }); - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_playlist_notes( - caf::typed_response_promise rp, - const utility::JsonStore ¬es, - const utility::Uuid &playlist_uuid) { - - const std::string ui(R"( - import xStudio 1.0 - import QtQuick 2.14 - XsLabel { - anchors.fill: parent - font.pixelSize: XsStyle.popupControlFontSize*1.2 - verticalAlignment: Text.AlignVCenter - font.weight: Font.Bold - color: palette.highlight - text: "SG" - } - )"); - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto bookmarks = - request_receive(*sys, session, bookmark::get_bookmark_atom_v); - - auto tags = request_receive(*sys, session, xstudio::tag::get_tag_atom_v); - - auto count = std::make_shared(notes.size()); - auto failed = std::make_shared(0); - auto succeed = std::make_shared(0); - - auto offscreen_renderer = - system().registry().template get(offscreen_viewport_registry); - auto thumbnail_manager = - system().registry().template get(thumbnail_manager_registry); - - for (const auto &j : notes) { - // need to capture result to embed in playlist and add any media.. - // spdlog::warn("{}", j["payload"].dump(2)); - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "notes", - utility::JsonStore(j["payload"])) - .then( - [=](const JsonStore &result) mutable { - (*count)--; - try { - // "errors": [ - // { - // "status": null - // } - // ] - if (not result.at("errors")[0].at("status").is_null()) - throw std::runtime_error(result["errors"].dump(2)); - - // get new playlist id.. - auto note_id = result.at("data").at("id").template get(); - // we have a note... - if (not j["has_annotation"].empty()) { - for (const auto &anno : j["has_annotation"]) { - request( - session, - infinite, - playlist::get_media_atom_v, - utility::Uuid(anno["media_uuid"])) - .then( - [=](const caf::actor &media_actor) mutable { - // spdlog::warn("render annotation {}", - // anno["media_frame"].get()); - request( - offscreen_renderer, - infinite, - ui::viewport:: - render_viewport_to_image_atom_v, - media_actor, - anno["media_frame"].get(), - thumbnail::THUMBNAIL_FORMAT::TF_RGB24, - 0, - true, - true) - .then( - [=](const thumbnail::ThumbnailBufferPtr - &tnail) { - // got buffer. convert to jpg.. - request( - thumbnail_manager, - infinite, - media_reader:: - get_thumbnail_atom_v, - tnail) - .then( - [=](const std::vector< - std::byte> - &jpgbuf) mutable { - // final step... - auto title = std:: - string(fmt::format( - "{}_{}.jpg", - anno["media_" - "name"] - .get< - std:: - string>(), - anno["timecode_" - "frame"] - .get< - int>())); - request( - shotgun_, - infinite, - shotgun_upload_atom_v, - "note", - note_id, - "", - title, - jpgbuf, - "image/jpeg") - .then( - [=](const bool) { - }, - [=](const error & - err) mutable { - spdlog::warn( - "{} " - "Failed" - " uploa" - "d of " - "annota" - "tion " - "{}", - __PRETTY_FUNCTION__, - to_string( - err)); - } - - ); - }, - [=](const error - &err) mutable { - spdlog::warn( - "{} Failed jpeg " - "conversion {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed render annotation " - "{}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed get media {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - } - } - - // spdlog::warn("note {}", result.dump(2)); - // send json to note.. - anon_send( - bookmarks, - json_store::set_json_atom_v, - utility::Uuid(j["bookmark_uuid"]), - utility::JsonStore(result.at("data")), - ShotgunMetadataPath + "/note"); - - xstudio::tag::Tag t; - t.set_type("Decorator"); - t.set_data(ui); - t.set_link(utility::Uuid(j["bookmark_uuid"])); - t.set_unique(to_string(t.link()) + t.type() + t.data()); - - anon_send(tags, xstudio::tag::add_tag_atom_v, t); - - // update shotgun versions from our source playlist. - // return the result.. - // update_playlist_versions(rp, playlist_uuid, playlist_id); - (*succeed)++; - } catch (const std::exception &err) { - (*failed)++; - spdlog::warn( - "{} {} {}", __PRETTY_FUNCTION__, err.what(), result.dump(2)); - } - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }, - [=](error &err) mutable { - spdlog::warn( - "Failed create note entity {} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*count)--; - (*failed)++; - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }); - } - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -// template void -// ShotgunDataSourceActor::refresh_playlist_notes(caf::typed_response_promise -// rp, const utility::Uuid &playlist_uuid) { -// try { -// // find playlist -// scoped_actor sys{system()}; - -// // get session -// auto session = request_receive( -// *sys, -// system().registry().template get(global_registry), -// session::session_atom_v); - -// // get playlist -// auto playlist = request_receive( -// *sys, -// session, -// session::get_playlist_atom_v, -// playlist_uuid); - - -// // get media.. -// auto media = request_receive>( -// *sys, -// playlist, -// playlist::get_media_atom_v); - -// // no media so no point.. -// if(media.empty()) { -// rp.deliver(JsonStore(R"({"data": {"status": "successful"}})"_json)); -// return; -// } - -// auto bookmarks = request_receive( -// *sys, -// session, -// bookmark::get_bookmark_atom_v); - -// // get shotgun playlist id.. -// auto sgpl = request_receive( -// *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath+"/playlist"); -// auto sgpl_id = sgpl["id"].template get(); - -// // get shotgun data.. -// // this calls ourself so need dispatch.. -// auto note_filter = R"( -// { -// "logical_operator": "and", -// "conditions": [ -// ["note_links", "in", {"type":"Playlist", "id":0}] -// ] -// })"_json; - -// note_filter["conditions"][0][2]["id"] = sgpl_id; - -// request(caf::actor_cast(this), -// SHOTGUN_TIMEOUT, -// shotgun_entity_search_atom_v, "Notes", -// JsonStore(note_filter), -// std::vector({"*"}), -// std::vector(), -// 1, 4999).then( -// [=](const JsonStore &jsn) mutable { -// // get metadata to see if they are tagged with version id's -// fan_out_request( -// vpair_second_to_v(media), infinite, json_store::get_json_atom_v, -// utility::Uuid(), ShotgunMetadataPath, true) .then( -// [=](std::vector> vmeta) mutable { -// std::map media_map; -// std::map ver_media_map; -// std::map ver_note_map; - -// // map shot gun versions to media actors. -// for(const auto &i: vmeta){ -// auto ver_id = i.second.value("version", -// R"({})"_json).value("id", 0); -// // spdlog::warn("{} {} {}", ver_id, -// to_string(i.first.first), i.second.dump(2)); if(ver_id){ -// ver_media_map[ver_id] = i.first; -// // add media to map -// media_map[i.first.first] = i.first.second; -// } -// } - -// // map notes to versions, maybe more than one note. -// for(const auto &i : jsn["data"]) { -// for(const auto &j : -// i["relationships"]["note_links"]["data"] ){ -// auto ver_id = j.value("id", 0); -// if(j.value("type", "") == "Version") { -// if(ver_media_map.count(ver_id)) { -// if(not ver_note_map.count(ver_id)) { -// ver_note_map[ver_id] = R"([])"_json; -// } -// ver_note_map[ver_id].push_back(i); -// // spdlog::warn("pushed to {}", ver_id); -// } else { -// // spdlog::warn("No match {}", j.dump(2)); -// } -// break; -// } -// } -// } - -// scoped_actor sys{system()}; -// // collect all note metadata on all media. -// // even if the note exists it's state might have changed.. -// // so we always update. -// auto existing_bookmarks = -// request_receive>>( -// *sys, bookmarks, bookmark::get_bookmarks_atom_v, -// map_key_to_vec(media_map) -// ); - -// // get metadata from existing bookmarks.. -// // do group query on bookmark json.. -// UuidVector meta_bookmarks; -// for(const auto &i: existing_bookmarks) { -// for(const auto &j: i.second) { -// meta_bookmarks.push_back(j.first); -// } -// } -// std::set existing_notes; -// auto bookmark_json = -// request_receive>>(*sys, bookmarks, json_store::get_json_atom_v, -// meta_bookmarks, ShotgunMetadataPath+"/note/id"); for(const -// auto &i: bookmark_json) { -// existing_notes.insert(i.second.get()); -// // spdlog::warn("bookmark sg js {} {}", -// i.second.dump(2),to_string(i.first.first)); -// } - - -// // Create new notes and link to media -// for(const auto &i: ver_note_map) { -// for(const auto &j: i.second) { -// if(existing_notes.count(j["id"].get())) { -// // spdlog::warn("Existing note skipping {}", -// j["id"].get()); continue; -// } - -// // spdlog::warn("{}", j.dump(2)); -// // create bookmark -// auto ba = request_receive( -// *sys, bookmarks, bookmark::add_bookmark_atom_v, -// ver_media_map[i.first] -// ); -// // set json data -// anon_send(ba.second, json_store::set_json_atom_v, -// JsonStore(j), ShotgunMetadataPath+"/note"); - -// bookmark::BookmarkDetail detail; -// try { -// detail.author_ = -// j["relationships"]["created_by"]["data"].value("name","Anonymous"); -// } catch(...){ -// detail.author_ = "Anonymous"; -// } -// detail.category_ = -// j["attributes"].value("sg_note_type", -// "default"); detail.colour_ = -// category_colours_.count(*(detail.category_)) ? -// category_colours_[*(detail.category_)] : ""; -// detail.subject_ = -// j["attributes"].value("subject", ""); -// detail.note_ = -// j["attributes"].value("content", ""); -// detail.created_ = -// to_sys_time_point(j["attributes"].value("created_at", -// "1972-03-19T00:00:00Z")); - -// // set detail -// anon_send(ba.second, -// bookmark::bookmark_detail_atom_v, detail); - -// // spdlog::warn("{} {} {}", i.first, -// j.value("id",0), -// j["attributes"].value("created_at", -// "")); -// } -// } - -// rp.deliver(JsonStore(R"({"data": {"status": -// "successful"}})"_json)); -// }, -// [=](error &err) mutable { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); -// rp.deliver(JsonStore(R"({"data": {"status": -// "unsuccessful"}})"_json)); -// }); - -// }, -// [=](error &err) mutable { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); -// rp.deliver(JsonStore(R"({"data": {"status": "unsuccessful"}})"_json)); -// } -// ); - - -// } catch(const std::exception &err) { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); -// rp.deliver(make_error(xstudio_error::error, err.what())); -// } -// } - -template -void ShotgunDataSourceActor::load_playlist( - caf::typed_response_promise rp, - const int playlist_id, - const caf::actor &session) { - - // this is going to get nesty :() - - // get playlist from shotgun - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - playlist_id, - std::vector()) - .then( - [=](JsonStore pljs) mutable { - // got playlist. - // we can create an new xstudio playlist actor at this point.. - auto playlist = UuidActor(); - try { - if (session) { - scoped_actor sys{system()}; - - auto tmp = request_receive>( - *sys, - session, - session::add_playlist_atom_v, - pljs["data"]["attributes"]["code"].get(), - utility::Uuid(), - false); - - playlist = tmp.second; - - } else { - auto uuid = utility::Uuid::generate(); - auto tmp = spawn( - pljs["data"]["attributes"]["code"].get(), uuid); - playlist = UuidActor(uuid, tmp); - } - - // place holder for shotgun decorators. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(), - "/metadata/shotgun"); - // should really be driven from back end not UI.. - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - - // get version order - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = playlist_id; - - request( - caf::actor_cast(this), - infinite, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999) - .then( - [=](const JsonStore &order) mutable { - std::vector version_ids; - for (const auto &i : order["data"]) - version_ids.emplace_back(std::to_string( - i["relationships"]["version"]["data"].at("id").get())); - - if (version_ids.empty()) - return rp.deliver( - make_error(xstudio_error::error, "No Versions found")); - - // get versions - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get versions ordered by playlist. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](JsonStore &js) mutable { - // munge it.. - auto data = R"([])"_json; - - for (const auto &i : version_ids) { - for (auto &j : js["data"]) { - - // spdlog::warn("{} {}", - // std::to_string(j["id"].get()), i); - if (std::to_string(j["id"].get()) == i) { - data.push_back(j); - break; - } - } - } - - js["data"] = data; - - // add back in - pljs["data"]["relationships"]["versions"] = js; - - // spdlog::warn("{}",pljs.dump(2)); - // now we have a playlist json struct with the versions - // corrrecly ordered, set metadata on playlist.. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(pljs["data"]), - ShotgunMetadataPath + "/playlist"); - - // addDecorator(playlist.uuid) - // addMenusFull(playlist.uuid) - - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - pljs, - playlist.uuid(), - playlist.actor(), - utility::Uuid()); - - rp.deliver(playlist); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); -} - -template -std::shared_ptr -ShotgunDataSourceActor::get_next_build_task(bool &is_ivy_build_task) { - - std::shared_ptr job_info; - // if we already have popped N jobs off the queue that haven't completed - // and N >= worker_count_ we don't pop a job off and instead return a null - // - if (build_tasks_in_flight_ < worker_count_) { - if (!build_playlist_media_tasks_.empty()) { - is_ivy_build_task = false; - job_info = build_playlist_media_tasks_.front(); - build_playlist_media_tasks_.pop_front(); - } else if (!extend_media_with_ivy_tasks_.empty()) { - is_ivy_build_task = true; - job_info = extend_media_with_ivy_tasks_.front(); - extend_media_with_ivy_tasks_.pop_front(); - } - } - return job_info; -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_shotgun( - std::shared_ptr build_media_task_data) { - - // now 'build' the MediaActor via our worker pool to create - // MediaSources and add them - build_tasks_in_flight_++; - - // spawn a media actor - build_media_task_data->media_actor_ = spawn( - build_media_task_data->media_name_, - build_media_task_data->media_uuid_, - UuidActorVector()); - UuidActor ua(build_media_task_data->media_uuid_, build_media_task_data->media_actor_); - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - if (build_media_task_data->event_msg_) { - build_media_task_data->event_msg_->increment_progress(); - event::send_event(this, *(build_media_task_data->event_msg_)); - } - }; - - // now we get our worker pool to build media sources and add them to the - // parent MediaActor using the shotgun query data - request( - pool_, - caf::infinite, - playlist::add_media_atom_v, - build_media_task_data->media_actor_, - build_media_task_data->sg_data_, - build_media_task_data->media_rate_) - .then( - - [=](bool) { - // media sources were constructed successfully - now we can add to - // the playlist, we pass in the overall ordered list of uuids that - // we are building so the playlist can ensure everything is added - // in order, even if they aren't created in the correct order - request( - build_media_task_data->playlist_actor_, - caf::infinite, - playlist::add_media_atom_v, - ua, - *(build_media_task_data->ordererd_uuids_), - build_media_task_data->before_) - .then( - - [=](const UuidActor &) { - if (!build_media_task_data->flag_colour_.empty()) { - anon_send( - build_media_task_data->media_actor_, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional( - build_media_task_data->flag_colour_), - std::optional( - build_media_task_data->flag_text_))); - } - - extend_media_with_ivy_tasks_.emplace_back(build_media_task_data); - continue_processing_job_queue(); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_ivy( - std::shared_ptr ivy_media_task_data) { - - auto ivy = system().registry().template get("IVYDATASOURCE"); - build_tasks_in_flight_++; - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - /* Commented out bevause we're not including ivy leaf addition - in progress indicator now. - if (ivy_media_task_data->event_msg) { - ivy_media_task_data->event_msg->increment_progress(); - event::send_event(this, *(ivy_media_task_data->event_msg)); - }*/ - }; - - - auto good_sources = std::make_shared(); - auto count = std::make_shared(0); - - // this function adds the sources that are 'good' (i.e. were able - // to acquire MediaDetail) to the MediaActor - we only call it - // when we've fully 'built' each MediaSourceActor in our 'sources' - // list -0 see the request/then handler below where it is used - auto finalise = [=]() { - request( - ivy_media_task_data->media_actor_, - infinite, - media::add_media_source_atom_v, - *good_sources) - .then( - [=](const bool) { - // media sources all in media actor. - // we can now select the ones we want.. - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_visual_source_, - media::MT_IMAGE, - true); - - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_audio_source_, - media::MT_AUDIO, - true); - - continue_processing_job_queue(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }; - - // here we get the ivy data source to fetch sources (ivy leafs) using the - // ivy dnuuid for the MediaActor already created from shotgun data - request( - ivy, - infinite, - use_data_atom_v, - ivy_media_task_data->sg_data_.at("attributes").at("sg_project_name").get(), - utility::Uuid(ivy_media_task_data->sg_data_.at("attributes") - .at("sg_ivy_dnuuid") - .get()), - ivy_media_task_data->media_rate_) - .then( - [=](const utility::UuidActorVector &sources) { - // we want to make sure the 'MediaDetail' has been fetched on the - // sources before adding to the parent MediaActor - this means we - // don't build up a massive queue of IO heavy MediaDetail fetches - // but instead deal with them sequentially as each media item is - // added to the playlist - - if (sources.empty()) { - finalise(); - } else { - *count = sources.size(); - } - - for (auto source : sources) { - - // we need to get each source to get its detail to ensure that - // it is readable/valid - request( - source.actor(), - infinite, - media::acquire_media_detail_atom_v, - ivy_media_task_data->media_rate_) - .then( - [=](bool got_media_detail) mutable { - if (got_media_detail) - good_sources->push_back(source); - else - send_exit(source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - - // kill bad source. - send_exit(source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }); - } - }, - - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); -} - +#include "data_source_shotgun.tcc" extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { 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..c354ccc20 --- /dev/null +++ b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp @@ -0,0 +1,260 @@ +// 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..1f0f5ba5a 100644 --- a/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt +++ b/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt @@ -21,6 +21,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/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/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/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/media_hook/dneg/dnhook/src/dneg.cpp b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp index 750eea326..ffa96d09f 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 @@ -422,6 +475,7 @@ class DNegMediaHook : public MediaHook { } else { r["ocio_config"] = "__raw__"; + r["working_space"] = "raw"; } return r; diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp index a77ceb991..fb84eec41 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp @@ -351,7 +351,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)); } } diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index 9a5ccbb74..5e65e5a4d 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -600,6 +600,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 +635,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; 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..8ef861f0a 100644 --- a/src/plugin/media_reader/openexr/src/openexr.cpp +++ b/src/plugin/media_reader/openexr/src/openexr.cpp @@ -92,7 +92,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 +105,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 +222,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 +277,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 +351,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 +393,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 +493,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 +539,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 +551,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 +592,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 +632,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 +718,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 +738,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 +759,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 = - ((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 = + ((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; + 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..d3cb7bc6b 100644 --- a/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp +++ b/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp @@ -12,9 +12,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 +72,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..3a1b37260 100644 --- a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp +++ b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp @@ -345,9 +345,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 +392,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 +413,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 +475,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 +528,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..27fcd4e28 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp @@ -0,0 +1,31 @@ +#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/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..78d7602d3 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 @@ -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_manager/src/plugin_base.cpp b/src/plugin_manager/src/plugin_base.cpp index 6428c195a..54f64aa4a 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); - } - - } 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()); + 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; } - } -} -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()); + // 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..c58b479c4 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -86,16 +86,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 +94,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..ae8ceab7f 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/python_module/src/py_atoms.cpp b/src/python_module/src/py_atoms.cpp index fdc49703f..1c997fab6 100644 --- a/src/python_module/src/py_atoms.cpp +++ b/src/python_module/src/py_atoms.cpp @@ -35,6 +35,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 +66,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 +190,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 +368,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_messages.cpp b/src/python_module/src/py_messages.cpp index d536228ee..ee77b7fc9 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.cpp @@ -99,6 +99,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( diff --git a/src/python_module/src/py_plugin.cpp b/src/python_module/src/py_plugin.cpp index f63273a76..94976c8d1 100644 --- a/src/python_module/src/py_plugin.cpp +++ b/src/python_module/src/py_plugin.cpp @@ -20,13 +20,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/session/src/session_actor.cpp b/src/session/src/session_actor.cpp index 114763f51..8e7709f7f 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, @@ -347,7 +349,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 +362,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 +877,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 +1486,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 +1887,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 +1896,47 @@ 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)) save_path = fs::canonical(save_path); - // 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(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..593a1f5f0 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_, 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_disk_cache_actor.cpp b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp index 739b3ecda..21fee25cd 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; } 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..242c80c51 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -14,6 +14,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 +69,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 +342,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 +356,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 +368,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 +435,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 +460,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 +492,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 +633,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 +642,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 +669,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,8 +723,8 @@ 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); + // spdlog::warn("{}", event.dump(2)); + if (event.at("uuid") == uuid_addr_.first) { switch (static_cast(event.at("action"))) { case IT_ENABLE: @@ -662,6 +733,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 +748,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 +825,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..067a8c81f 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -80,6 +80,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: { @@ -140,7 +142,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 +168,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 +194,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 +253,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 +278,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 +338,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 +371,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 +407,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, @@ -434,7 +500,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 +531,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) { @@ -578,6 +646,13 @@ void TimelineActor::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; + }, + [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) @@ -594,6 +669,16 @@ void TimelineActor::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(); }, + [=](item_atom) -> Item { return base_.item(); }, [=](plugin_manager::enable_atom, const bool value) -> JsonStore { @@ -691,6 +776,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 +859,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; - }, - - [=](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); }); + // send(event_group_, event_atom_v, item_atom_v, hist, true); - 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 +1157,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 +1246,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 +1265,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 +1274,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 +1287,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);*/ - playhead_ = UuidActor(uuid, actor); + // 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); + + 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_; }, @@ -1413,94 +1583,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(); + + jsn["base"] = dup.serialise(); + jsn["actors"] = {}; + auto actor = spawn(jsn, caf::actor()); - request_receive( - *sys, actor, playlist::add_media_atom_v, media_actors, Uuid(), true); + auto hactor = request_receive(*sys, actor, history::history_atom_v); + anon_send(hactor.actor(), plugin_manager::enable_atom_v, false); - return UuidActor(uuid, actor); + for (const auto &i : base_.children()) { + auto ua = request_receive( + *sys, actors_[i.uuid()], utility::duplicate_atom_v); + anon_send(actor, insert_item_atom_v, -1, UuidActorVector({ua})); + } + + // 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 +1772,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)++; @@ -1725,9 +1974,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 { @@ -1945,3 +2193,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/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/canvas/src/CMakeLists.txt b/src/ui/canvas/src/CMakeLists.txt new file mode 100644 index 000000000..995a0fd31 --- /dev/null +++ b/src/ui/canvas/src/CMakeLists.txt @@ -0,0 +1,19 @@ +SET(LINK_DEPS + pthread + xstudio::utility + Imath::Imath + OpenEXR::OpenEXR +) + +find_package(Freetype) +find_package(Imath) +find_package(OpenEXR) + +create_component_with_alias(ui_canvas xstudio::ui::canvas 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/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/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index 58248aed7..9f0ddce1f 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,70 @@ 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 (...) { + } + } + 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; +} + +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 +125,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 +177,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 +215,28 @@ 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 +251,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 +299,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 +339,202 @@ 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()); + // 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)); + + 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 (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 &attribute_data, + const std::string &sort_role, + caf::actor client) { + + auto p = models_.find(model_name); + if (p != models_.end()) { + + // model with this name already exists. Simply add client and send the + // full model state to the client + 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 { + parent_node = find_node_matching_string_field( + parent_node, attr_uuid_role_name, to_string(attribute_uuid)); + + 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 (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 +563,7 @@ void GlobalUIModelData::register_model( client, utility::event_atom_v, model_data_atom_v, + model_name, model_data_as_json(model_name)); } @@ -229,7 +576,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 +637,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 +647,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 +658,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 +676,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 +689,24 @@ 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 +714,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 +769,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, @@ -481,36 +833,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 +852,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 +861,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..e854e9202 100644 --- a/src/ui/opengl/src/CMakeLists.txt +++ b/src/ui/opengl/src/CMakeLists.txt @@ -5,6 +5,7 @@ SET(LINK_DEPS GLEW::GLEW pthread xstudio::ui::base + xstudio::ui::canvas xstudio::utility xstudio::media_reader OpenEXR::OpenEXR 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..d2df464f5 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..271597c1c 100644 --- a/src/ui/opengl/src/texture.cpp +++ b/src/ui/opengl/src/texture.cpp @@ -35,6 +35,8 @@ void GLBlindTex::release() { when_last_used_ = utility::clock::now(); } +GLBlindTex::~GLBlindTex() {} + GLDoubleBufferedTexture::GLDoubleBufferedTexture() { if (using_ssbo_) { @@ -137,6 +139,9 @@ GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { // ensure no copying is in flight if (upload_thread_.joinable()) upload_thread_.join(); + + glDeleteTextures(1, &tex_id_); + glDeleteBuffers(1, &pixel_buf_object_id_); } void GLBlindRGBA8bitTex::resize(const size_t required_size_bytes) { @@ -480,6 +485,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 +577,7 @@ GLSsboTex::GLSsboTex() { glGenBuffers(1, &ssbo_id_); } GLSsboTex::~GLSsboTex() { if (upload_thread_.joinable()) upload_thread_.join(); + glDeleteBuffers(1, &ssbo_id_); } diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index 25379ec13..08f882b09 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. @@ -230,9 +238,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()); @@ -451,6 +457,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/embedded_python/src/embedded_python_ui.cpp b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp index a23a0766f..5edab50e9 100644 --- a/src/ui/qml/embedded_python/src/embedded_python_ui.cpp +++ b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp @@ -223,6 +223,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/global_store/src/global_store_model_ui.cpp b/src/ui/qml/global_store/src/global_store_model_ui.cpp index 4aff43c1d..eaa16d8bf 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; - 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/helper/src/CMakeLists.txt b/src/ui/qml/helper/src/CMakeLists.txt index c56378357..54b16cbe9 100644 --- a/src/ui/qml/helper/src/CMakeLists.txt +++ b/src/ui/qml/helper/src/CMakeLists.txt @@ -10,6 +10,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..0c422eab5 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"); @@ -82,10 +86,13 @@ 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)); + hash = std::hash{}(static_cast( + display_transform_hash + mhash.first + std::to_string(mhash.second))); } catch (const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } 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..9e1025fd9 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -57,10 +57,7 @@ void UIModelData::setModelDataName(QString name) { model_name_ = StdFromQString(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 +65,6 @@ void UIModelData::setModelDataName(QString name) { as_actor()); // process app/user.. - setModelData(data); emit modelDataNameChanged(); } } @@ -79,7 +75,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 +85,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 +96,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 +119,55 @@ 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({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 +180,8 @@ 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") { + j = nlohmann::json::parse(StdFromQString(value.toString())); } else { j = nlohmann::json::parse(QJsonDocument::fromVariant(value) .toJson(QJsonDocument::Compact) @@ -163,6 +198,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 +245,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 +417,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 +426,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 +445,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()); @@ -410,9 +601,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()); diff --git a/src/ui/qml/helper/src/model_helper_ui.cpp b/src/ui/qml/helper/src/model_helper_ui.cpp index 1538f6a9e..2d86f4072 100644 --- a/src/ui/qml/helper/src/model_helper_ui.cpp +++ b/src/ui/qml/helper/src/model_helper_ui.cpp @@ -6,6 +6,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 +218,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 +245,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/module/src/module_menu_ui.cpp b/src/ui/qml/module/src/module_menu_ui.cpp index 36d5c57c8..f9cd1cf55 100644 --- a/src/ui/qml/module/src/module_menu_ui.cpp +++ b/src/ui/qml/module/src/module_menu_ui.cpp @@ -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( diff --git a/src/ui/qml/module/src/module_ui.cpp b/src/ui/qml/module/src/module_ui.cpp index 990ed0dcd..ee4914280 100644 --- a/src/ui/qml/module/src/module_ui.cpp +++ b/src/ui/qml/module/src/module_ui.cpp @@ -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) { @@ -408,12 +412,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 (const std::exception &) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -511,13 +525,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/playhead_ui.cpp b/src/ui/qml/playhead/src/playhead_ui.cpp index e5953703d..d0ce0faf8 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; @@ -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_); diff --git a/src/ui/qml/session/src/CMakeLists.txt b/src/ui/qml/session/src/CMakeLists.txt index 0d7a31a40..8ac241a49 100644 --- a/src/ui/qml/session/src/CMakeLists.txt +++ b/src/ui/qml/session/src/CMakeLists.txt @@ -3,6 +3,7 @@ SET(LINK_DEPS Qt5::Core Qt5::Test xstudio::ui::qml::helper + xstudio::timeline xstudio::utility ) 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/session_model_core_ui.cpp b/src/ui/qml/session/src/session_model_core_ui.cpp index 657b418db..f61e9c194 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()) { + + 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..76fa2b31c 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); @@ -602,15 +673,30 @@ 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); if (type == "Playlist") { target = actorFromString(system(), ij.at("actor")); } else if (type == "Subset") { - target = actorFromIndex(index.parent(), true); + target = actorFromIndex(index.parent(), true); + sub_target = actorFromString(system(), ij.at("actor")); } else if (type == "Timeline") { - target = actorFromIndex(index.parent(), true); + target = actorFromIndex(index.parent(), true); + sub_target = actorFromString(system(), ij.at("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); @@ -650,19 +736,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 +810,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 +886,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 +970,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; @@ -1086,6 +1228,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 +1247,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..193edcaa4 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"; @@ -460,9 +463,21 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & 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 +494,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 +541,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 +570,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 +619,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 +650,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 +702,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 +725,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 +740,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 +758,7 @@ nlohmann::json SessionModel::playlistTreeToJson( "group_actor": null, "actor_uuid": null, "flag": null, + "flag_text": null, "name": null })"_json); @@ -663,7 +769,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 +779,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 +815,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 +883,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 +902,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 +953,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; @@ -881,7 +1017,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 +1036,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/studio_ui.cpp b/src/ui/qml/studio/src/studio_ui.cpp index d11d1b5cb..d1bab527e 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,80 @@ 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 +229,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 +254,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 +301,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 +343,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/viewport/src/CMakeLists.txt b/src/ui/qml/viewport/src/CMakeLists.txt index b08e4ef31..2550a5afd 100644 --- a/src/ui/qml/viewport/src/CMakeLists.txt +++ b/src/ui/qml/viewport/src/CMakeLists.txt @@ -6,8 +6,8 @@ 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::playhead xstudio::utility 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..0519df3e7 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,52 @@ 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/viewport_widget/src/offscreen_viewport.cpp b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp index f61a3a937..5ef9036a2 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -25,110 +25,68 @@ 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; - }); - } - - ~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); + 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; + } - caf::behavior behavior_; - caf::actor offscreen_viewport_; + // 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 make_behavior() override { return behavior_; } -}; + } -OffscreenViewport::OffscreenViewport(QObject *parent) : super(parent) { + static std::map format_to_gl_tex_format = { + {viewport::ImageFormat::RGBA_8, GL_RGBA8}, + {viewport::ImageFormat::RGBA_10_10_10_2, GL_RGBA8}, + {viewport::ImageFormat::RGBA_16, GL_RGBA16}, + {viewport::ImageFormat::RGBA_16F, GL_RGBA16F}, + {viewport::ImageFormat::RGBA_32F, GL_RGBA32F} + }; + + static std::map format_to_gl_pixe_type = { + {viewport::ImageFormat::RGBA_8, GL_UNSIGNED_BYTE}, + {viewport::ImageFormat::RGBA_10_10_10_2, GL_UNSIGNED_BYTE}, + {viewport::ImageFormat::RGBA_16, GL_UNSIGNED_SHORT}, + {viewport::ImageFormat::RGBA_16F, GL_HALF_FLOAT}, + {viewport::ImageFormat::RGBA_32F, GL_FLOAT} + }; + + static std::map format_to_bytes_per_pixel = { + {viewport::ImageFormat::RGBA_8, 4}, + {viewport::ImageFormat::RGBA_10_10_10_2, 4}, + {viewport::ImageFormat::RGBA_16, 8}, + {viewport::ImageFormat::RGBA_16F, 8}, + {viewport::ImageFormat::RGBA_32F, 16} + }; - /*thread_ = new QThread(this); - moveToThread(thread_);*/ +} - /*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 +95,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 +147,86 @@ 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 +239,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 +250,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_); - } -} - -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) { + // gl_context_->makeCurrent(surface_); - 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,7 +300,8 @@ 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)); @@ -288,42 +310,62 @@ void OffscreenViewport::renderSnapshot( '.'); // 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 +389,68 @@ 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_); + } + + tex_width_ = width; + tex_height_ = height; + vid_out_format_ = format; - unsigned int texId, depth_texId; - unsigned int fboId; + 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 +459,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 +473,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 +487,36 @@ 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); +} - 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(); + + // 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 +527,238 @@ 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/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..c6665e9b7 100644 --- a/src/ui/viewport/src/viewport.cpp +++ b/src/ui/viewport/src/viewport.cpp @@ -8,6 +8,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 +105,11 @@ 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 +167,8 @@ 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,7 +180,7 @@ Viewport::Viewport( normalised_pointer_position() * interact_start_projection_matrix_; const float scale_factor = powf( 2.0, - -delta_trans.x * state_.size_.x * + (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; @@ -287,13 +289,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 +320,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 +344,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 +359,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 +391,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 +557,13 @@ 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_) { + for (auto &o: other_viewports_) { anon_send( - other_viewport_, + o, viewport_pan_atom_v, state_.translate_.x, state_.translate_.y); - anon_send(other_viewport_, viewport_scale_atom_v, state_.scale_); + anon_send(o, viewport_scale_atom_v, state_.scale_); } if (state_.translate_.x != 0.0f || state_.translate_.y != 0.0f || @@ -554,8 +574,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 +592,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 +619,7 @@ 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 +669,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 +682,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 +704,9 @@ void Viewport::set_scale(const float scale) { update_matrix(); } -void Viewport::set_size(const float w, const float 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 +788,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 +806,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 +858,25 @@ 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 +893,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 +936,11 @@ 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 +953,61 @@ 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, - caf::actor other_colour_pipeline) { - other_viewport_ = other_view; + bool link) { + if (link) { + + auto p = other_viewports_.begin(); + while (p != other_viewports_.end()) { + if (*p == other_view) { + return; + } + p++; + } - // 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); + + } 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_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 +1043,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 +1081,23 @@ 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 +1105,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 +1197,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 +1227,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 +1245,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 +1268,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 +1294,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 +1343,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 +1377,63 @@ 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 +1468,8 @@ 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,92 @@ 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..9fab647d3 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); } } @@ -457,8 +459,10 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi return timebase::flicks(0); const timebase::flicks video_refresh_period = compute_video_refresh(); + const utility::time_point next_video_refresh_tp = next_video_refresh(video_refresh_period); + caf::scoped_actor sys(system()); try { @@ -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,18 @@ 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 +544,64 @@ 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 +625,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 @@ -583,8 +633,8 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { // Here we try to match out measurement with commong video refresh rates: // 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))); + const int hertz_refresh = 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 +646,31 @@ 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..ea10d06a4 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -6,14 +6,15 @@ find_package(PkgConfig REQUIRED) pkg_search_module(UUID REQUIRED uuid) 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 +22,17 @@ SET(STATIC_LINK_DEPS fmt::fmt Imath::Imath nlohmann_json::nlohmann_json - reproc++ spdlog::spdlog stdc++fs uuid + ZLIB::ZLIB ) +find_package(ZLIB REQUIRED) +find_package(spdlog REQUIRED) +find_package(fmt REQUIRED) +find_package(nlohmann_json REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_search_module(UUID REQUIRED uuid) -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/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/file_system_item.cpp b/src/utility/src/file_system_item.cpp new file mode 100644 index 000000000..475021241 --- /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(entry.path()); + // last_write_ = fs::last_write_time(entry.path()); + name_ = 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(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(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/helpers.cpp b/src/utility/src/helpers.cpp index 33283f2ec..65a05cc70 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -13,8 +13,8 @@ #include -#include -#include +//#include +//#include #include "xstudio/utility/frame_list.hpp" #include "xstudio/utility/helpers.hpp" @@ -37,6 +37,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 @@ -99,6 +104,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 +173,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,7 +221,8 @@ 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) { 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/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/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 0000000000000000000000000000000000000000..f9a9b91ffe45558d302f55a90c31789b839fd852 GIT binary patch literal 2975 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 V!nCKgUYh^_002ovPDHLkV1i*ocrX9} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b27822baea48062bf11617ce763e8d623c3a9769 GIT binary patch literal 316848 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

Jv+`0!b7}B5n-gutriq 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

Z!)9Bt3Egf{~Xji0sPQ(fD|Oc}7GcFAqDbZW_d9 z-QRSeps9fUI?(j3HFB#p+8UjmEw))v${rM4+Fx+#Ta@w#JFd^>t{FOXq-9XffQ~HG ztk$gk>CR|>qBB3MvmyKbeCVDxt=2d1IrQhfWOk}$#{Bs+EQ`oq>l=Fyf4nF6qlfmr zc{drJYKd8}aOSjy*S@$l5l;3IVb5*Xh}PcRjJOwE zIw8S`-~>16)Cg5S!#(=n)A&!Wrz0A9g3Upjv4WW6q(WN_ZqHmLd*&jr`>;GVWY zq5L_tO1%CiR$qHnr|0$Y+^znG=V=_iELW2Z4G|>SAsyG-_z5#7635 zWOX(Y+(O7~Hg@^fQnTL5ttPniZwk&0P`)!ELnm`5CQ?b;&mT#;d(lk)sELFa#dnI5 z?p_cV5E3Cf8b2fxV<(bDa~duQWKzte`2cuI|GIS9E$2r7p8X+!`U|%JhzqhLZ604Z ze936@oF*8*oM`G9IZygpAH;os{J4`H@McCk@d&b7)5dNEMk{1mSed;!T_5!JS6{t7 zW8-?v(^pZi3zxUa@)mau;MB-=p-Eo~;xiEy8vr&tY<1-QZFYP1wi730dr4MS3H#CD z9#7dWmK)4i!?y?kSMPFH-DZT0<&Q2~_ULl;bISbrQ}83(mp+`9_V7~mvz8=Go{ShJ z_6g8teU@l ztRb@|O`J_g{G^Eq=-1M-L1D05H<^3-Nq%8)`V9{V6g+F)cNgGJ)HmthOO8v@>bvym z9tCSoo@HEA*WeJvnT!*y$ArU~W)!<@c;&Yn+8AyA_ajr(z?Y(NR4; zie7f~*!4C^Cuep`U`4|9NwP!vjB^YfhKSi%QK@?tXGBihdh*6wD)-iodXwy+g?-?dtyJ_I-mT|s=*(N>jrBJ`JEW5+JUuu+!sbD@ zyHaU%h%B zt4c$<*2*SYiC`mEFMy@WaJg1_cV!Y1q;$hU9=6|=TkcLLvgz&X*WaG8ZruzG={Cu2 z%1HzB!a#eN=E&DLdWu;>!X};b0?qN?yh0E*rx-~ni4}x{DRSF!F~l$2x2d=wHQm_L z1INW`+Nm$XAZ_Yok~HZu%H_ykZs80NHa0%xyW6Ol6t%X3^Lt_s$n&YF@T7S?Jzbvi zuW72@it3xM=P(huI~}Q{^Ym>f$%a-l6(vP-A@wonQL?Qor>9jz6nAZQk6vHNSjJ3t zfl}afF|2dQq|GbaZx8Tx(^ppf>(wWh<|N-!{6{<5_)KX~(lmvrzpKm2Q0xv?G7;+P z$rpeh7J!$hge+f7O%&LjMvB}Q9z+)BE5^ywmaO+oH*Vf}@;nI|H+k%xqJ~^=P%3C! zkL?EjuK4%!ccr4Azjt{nO*EsY6g))}&*O*uT|5%^o~DrpuD#SjKXmcPjI8{3cmDg` z85?0m!;J$aG^D5KW}c|{xp<<&`61_tr2CL!RA%*9;NY)`=h{=A+(4N%eN{+%u*H$O z>er8lXT_H0=U@dz18^P(%nbDd*zLewvunPq8KLrf?e+3vj7tGcq@J2OJ}%a4xS#ua zxl-2S*%OTX120@J?+M0a&?~>D2|Yiu!F8K{Xg^YJvB4qBG5iv$^emEpDAGtyU>R@` zxWUX7NX?At=0%ul@P=aq8(EhI5GSVsTyNXqj(s&bn~yJC zdsoV$RjZLOu1)KS?2n#ZZ9+n4Cn-fdLPXJ54?Xl%5&K0<`I{m(@7c3CVpBf$w4$CC zV*}5W_FGIaoo0^I$j^I4z!oloklEDo2el?%j$W_~3*eTR8K`g3X!JI>dRb=E3g7c^ zpHv*nyi4$eWe^a`MK_IrtF>BWhqetK4mh93c=27g-&NMbOZLRo0L+<>`8l(TVFChV z{SNxCqsQqt^*e2UAwv$=lK8g?=`K&HyZZoC99uTkEfL)p{GPZM$1mN}KuicwGBxaZ zi&lOt{=%O|$EPfme!X%Y)FDL)sn|P4^vp7tb>|qK@dlBRU7Or}UWy_t0 za^N^FPc6TWm;|lKg`>0C*$l7dBANrYUVLgeB#!M*L2_&T&V+d4v>$>*AXCpWY;yIR z;o@fyv>~)bNbYiKC8d)1NKxm=Z3x@9S^B@(f41V#(Du!%U6WdCeTemS5n-@|u#rSXmNAB*cI~QMPP4mOwdf_#6wRWUAqf@rKx*|O{5?<% z?Oss-KI_&>v#*vLK>8m7MTAiDhJF`j75>MtJC_Z@a3NHiBq-})3%KOW5ri|^B%1Ba zp_kx)g7bsmh8oOa3jgN_sKIXf4dGAM3wU*lfMj0*Vi76sIwO>__wj0?9P4U9L<^!m z5zmAtoH^`8RO&iIicuo~&9NGxsIg0}ffm?o7j?<}=@SB8Ju1kt3^kgqO=1{f8agn3 z7Pbkq#bMAQ=hMedi|8OebS!qUvBXDo;AX8B^6~Xt@-ZHW4(v2zae>8cPK(omy9FC4 z*d_zyG4_W54bl$;)Y19FfF!ms_>9tWh6%^vc!Log_ag%b=-MfD1^2EJ!v$J!tqQR_ zH6(AqfU%d@%KqM};mrWwbI@QhzC56r!R-U)LBNg83`w8`{|+U^gQ9<4GlZq+H)!Cd zh!MYQm_a*CLIyGVh4_|AMXfxLE?r%qqpha{7y7s+6)MQ?NId52oDNdoMm`3U?;;=5 zmfjK4%B+gA%Ahd_Yz*g!tP$u2`i2Ib88BqZDs^PFr8hL3#bAP2V-N>5KoA-~U#|J z{NNYWDsv<(Urc)`3S6v@rRwpm`+RRLpoT_#&*{R$e`A$?l18e%i%ET09}MCD9Dg1H)0)WQVdcjGJKVZjU>CPS`2~n zXeKTYFYa<+ylt8weTZC#vj`4C$jAUkzhZ4PUMcFTCE40&q@~n`;@vuJC~DUMa(Rz! zP!PsU0AxQ>?q=(9=rz|^n;3#YV{r zu2jxi@0>4PRt_^9z>N8OeR$wKEJ~u{LbJO+R`Kb4NqSLv33Tbl3U9^?Gl&ea&N68% zdy}E3yd*3Z7K3gUSP&XH{`?v+v_8O0jANjmMVqKi?5axHxie+au3bw=vXPb=G}ko5 zbJrrZP)KE)L|E^5gKiKju zvIh`?iDOXJr#vAB!QWi5DhzmnnRpko$gyRH=hus+=58~4i{LMeO)m`Yigg!`9xQwX zk!T1rLbNlxl!Qg>`oUoruG&Jv@v^exS{wW9YLTjk$nucxguyypUm+fmV%^~WM-bk6 zn_wX_qcH)13X9$Nv4+S8a?bUly+62k2;18sNzb2XM;GiRtiM5$IPg=B36e#7J0pI4CG>bas3k7dHcyqrNdDpR`nuNQ&S^Wa7=(%O^rjTAMk{@Fs`kwsp)dCUMQ8Q zP*bjUgcXE}(_?sV zCRxNCiYyZWi&11pO$3LM0pEwS6r|1fypKz!_I_|r-Umo~u&*v}(&9;kOiGy)x(Eu~ ziKz*lq`Z<;R?=!AgGSI~{M}vI>C9kd&O#w0ix&yedq2p#=l#8V-v^w%#5gHs^5oQs z6H_NoPMIXeA@7)-w(=Soot;ix?pcXzOCne?F|lG&jw5S3FNVa_-`(L*46^~~?ezCL zJD@i|t9NMTKkki`{2$>jAlYOyti{1t)Ql670Ko&%&62k_|AbSpj=+fwV-|pf^@M%O zEQ;IS?cBF;(cXJV42ij?urME9x-s|cU9@mtw^R5k*w-g`{O;W$xESxQklni`+EG2~Xvi-^F2JQ0u*}%7SKq(Sg zBT>qM{+h^;i~7P|@HB^FceTV4S+MMxsEf^p@I8k`sqpWjO|kT1pmlUN3pS{jIaK8U z0!D&K1H+02J2bo5&dXiR!Y5A- zW5)M>&C=w)Kbc*Av=_J0yG-;5rDU2o#yjw*O9mf~8fu_D)KDY*pf6_{#{m8qPHO5G z)EhB^pe>>8VDkeHG#`BB74`Rne?4%jpt%q?wmtCI!ioy@cVTnEsRMs~Fl51mS9pR9 z_REzjL#$9!W8l;ESH<`w<>^zWf&ui$fDLGtSdd}E1yZv014KR!6?j(zOTnSGEG2YJ!U{P7A@Ya4@D|6cER-&G$D9nE%0Ia`2p2YVt^R%iZj?5EPDRT*lH=m zc}WbYK#hhA@&7@THPJxKwD4{+^A@Hko2BeW-Y>r4jP1%7^WYX<_&WM5 zXRxK~s>xFu-sP%=n!}LB?1&gH<|#SO3xX`ZLF{q{&A4t?>?`kst#N zn}mGo$giPcc@Q*0nATIh1c>FVBJYEtHbaf^h-h(qM0Sv0oOqhuVfax4E=a$8g4kC* z|7m{yr_Zlied<$rJ_%6F^b$!CEezQc?8OH4#S=JD?`RpiS}11Lxd}AUi?JE%4Oj*;j^2Yzl-;sUza)zH&S`^5&^jlk-}>j zYo2xa=@aA^WXDJOpPpLX+jUoi9l{m0mkl7(Z6|0(@AFmVP5CMnq*>Y;Wt2wJz`5I) zfq_@EBMgIV%$XGi&>#&n2vVz47Q1x0*Q2xnU6+(cWRcR2>yfz+hG%28e+mP>k|_x>VuH)r&A*ZPATC$p*Ndwb<9528df+h=V0D ztjWz7BOgQ`y4_pkN!LH%Eo#tWU)2~Kq+N9fwkQ;^CcB`5 zvg-0)Wt6st5$5^_@+7m+(a8s`hSJ0t;9{4jU(`RiDF(sS zE<@SD8taY_8Bv85I?`OU1WKMa3;jUe4M4o_%JvXYud6s@_Ye%H{W}&~=|a zb%`f<_xn~hRrvV{t)c*}qVM>3a9N&8*3JHdb0mX77B$nOE?KD_(lR&_R}B1$PEg8} ztict7bHFvj(!}#q=!Bt8aI-dfc}hCnFyvsyE+l^4`rUfndXFRpvNzSgvQg~hrAxGm zz7(Qp8coCBFKN}KOY9^Yh0-@^5Pm;Gz6f^o8PchZV!z@4QTX;59NK(Q@}-jXA0EH{ z3W~JdjtU2^5`Ah{st2bJj?{!7&j0^M@tDPymY0_fuIGO{{0BE1@n46h!NEbPzJ56n z5c~I;gpyap+udf?Eb6Vtj+#9vv@_?fJJj9THyStjJ)ur_f1eWjLHaxO*A7&i3%{2{+2*HziJkiQFEQKZ zr+Pevc zKKX?0WvsqF`S9H*j`6@wk?qg2t95nk-E&kCoiS_ugE%;H)L@5jNKzE5VYbr&20a{e?y`u@g!e zrI|%iHFBHaF9rmRuR+3lv;kS=wJR@k}5IA_I~mS-A6);k_k0> zYs>NH=RA^9PNDCbR@@pQ7HHNzxtqK|%@XjeG%w7p^a zA&7p8S3+(zA+5u2aJpOO&T>N}D zHS-a`j76&X4pX!lkoia(1-ZDl2&3@?@)SFv(YNyKeoKk6uz27`tQH&0SUlcHzjEU&;lu3! zAYgUkgg|N0!^`d|n6si}144_oE8%jon)g0UC_U7{DlYf3^lz(@3(Z?<_doIhG73?- z6MP5K;pad6Dm(wl6?XC0L+=q!3aPz!P@e2-$kW=pO=pIJf-n`uhFEKR*4i@5^7yKz zun@i&V94w{(xG97Sm|4q-g}uL;)_81jJL?ZSRF@jBV=!@+yo%2 zFW*?m*LgiYFHiD#eI01~V&ZUwU4mMxX>uw1rlpk~|NC3Cq`8rztB0$duF0Dhq)rUq zv0~ApaksKgWKb-coH6$d-ComNLw={%|5Q6aEMn8Fme}-2K}H0&7%V`gx7CQmDA?4Y zd;x~|BKXB{H$Y{vZ`_>!$JVs6cp|TkDNDmfP4-S)j+k9%*}u@b!a8f~qTWR7h9_@Y zuzo_=&J~Lmj_0kbYwRTiQf6hM!yJTS`)_TAs~^G0P0pdgu&ZKV49;2{RRo1Lh#X>#t6|PQA`Gos?9ea~Smvw< zGU{VYtgyRt1ro9*-{UoEh>Pd?5#P4oB8%cwsjcwy?3*P)3Z~L{Np^JKk?1h`CimOc zl*b7fVs^ahp+-tS4jXmztZ-4JO?vU*^L{Hsd)n06DTjBAo;5~P+ItH8r@9A^5d>DC zH()9&Sb*G#Mr)vgsG%JAHWcex3ny-m!OGzz!};Kd&5RXXdXd(XQyZ;)TWyWpHfK+! z&!jz6r?z4Bnrmsp&(zjI_q=nQz4cqh-to)MW-zCWv&)nsk^72gs)e$7P(19=Av!3V zqM!sE?m8UgRf_<}#|bVZo;qh7blx_q?k4iE8bIRNk3G`-A>aZ8AE`KUB1;*aWf!_y zn|Eaq9ek8ktWAVK+UOH1x?zJ^kgN(AJh`pyv zZLWBY^&BRTO)FN;QCp!o>?~tFk3u~C$k#@?kfx?#m~lAjftA-*n|2U7V>tCrN^yhQ zNkx2#Nx^)PS*yOORjc|AYt;svq}cEvNXX6uz!%@}9IKBR0d8L|U=nk2#Q0wne4S)n zH;ldN{jyGt6soZlIS#%JWWs(5hiFeD@@=29m`}QvcauBM4aL&bjz25uz?$Wz9%GwQ zP_K4M{(w!UDFte0Cwu<ixCFnNOv!?b|1{h4)YJ z&^D6G6LWgaQSF@Y*sPqh zD}2y~l*82G~J6FDA>((9R zv;b^XR&Riz2?6-{gu3Yf=fGmHvQmi{BRRPP`g;kZ5aCdWIrD_=e-RoLWS9`B`Cq!adTSnuBL&jDYJ#{~ zpOC-X799KcF|tw~f{Ak&%Cup_rR!rF>}#x$inA&!Bcc~ooj-1V_J!Bgyv)Ghjv1+J z%>JXt_8r7J>4_> z*fD%jtK%lE3(Uz2-M9oXFXE4!RBKN^c=jaavP?eeXX)-XA^bbK`+1yI<8NPM&?}ln{RM2qpvTU{L2E36nBHvvhdDZ_CTo%Yf$pxhJq*_q(PKP}}v9 z5>lclSyah;p{)mPwMZtx^gesM^$L6IRjTAG$04wxp}eZcg}0HnMQZDD;j4y!_Pxee z5T__Ou-Z9iE`AA0A`mE(T&@wLTQE~zFnV&f7V{!`gQi$foFFfvqo6}VC7qHa)bP0c z7g^P33?bQ5HmqG!!*rri>D1Tf{rm)yZBSgoqzH3Q-Hj{99D;gt2O?g*^-X$5hROw< z>ZwKux*ALBh7?|R6Ww*8oK++6jHI2pb(X+DkooK{9xD3z0npJ>F-K}K@Br1{tXSd4$%uDcPcm0}Z)Zi#XkIRtUMZyV|Ie{3UDkOos>TfW31GNU1nX=0)m77-T{ z!!^#hMQt`TS{oQ84%a*YeS9G-g;=GfVSivnkr@Q}M9@gHbpHcW=d#>7_RXWM^aKt1 zmG=Cfv)U#+dFH^g@o^{jQrp8%(7WsrwT1Ge$mrVxT~oq)=wWj4ARQh4=gaJ+e?EC+ z+3N+K#a}<_S1{&_Q!g?#7tM@A+ak3C_&9H-f$2JXBl&ga()YSi6q`{Fr+e&;Oweyp z^I68|5_l4;q>Yj0>(;t(#RkPJ#v!h_U>CPlNeHk?+;Clm$W7I1yj|)Bc=x*8jq~j0 zMDE$fp&UAc_?EF+V-*7N=5W3%3Bec9Li=xD{+qWPah6N@xsQQ&#{dKL33T)yDI#Yp zhvJAvq+m`{#D*?*ut9J?eCFAGcdqlCen;fGDHKT2b?mUmIg0yHAKYDc^z7NALd*Ku zcyHNFgIP6eP(9k%i{~){*$kL?INpoc0GMQcp805uASkxM0Ph{^U}zf+tUI}8bN6U5 z{%6`yyZLf0YE#pQcT)R={b=aJOVQw8|!AOL;^ z)`F0b!{D8y8l_fnWTlAR5O?QMc8MiY>DCP+S6Jjk?m6AlCb@K1>0^3YSR?)-cPLcF z22Q7MD%IAa0U_l^<8Tk)t0D?5(D*ek9u!(@t}8BCq24kVJ^$2PLwnp6dpMKQQuu*6so1`V#` zs!@O2u;}~01YqC{wZYIl;Jd8VOF6usxi@|-7G%$z22KnQD+>ZM-AKzN+X>*aiKn5Kjc4znlQ7n&I_v! zr9_N%53N}k7%@X!-9OZX(4A`VWFE#=j1yP4!Mh)*UP$I}MF;RwajK^X z)}~s7J%d7y3_J=bprvwPf>m-Q1rb-zs@i$6_AtU) zoOqlis?AcczW;e4n8=?s>V)nrxdm?hd@2oA3c!7lyR$_13<@?>^P8FkNmbyGu0a0F z=}m=h6z25lw$y|{Rv4|%AAkCGDP@VV(JRWAA77eZMQ!B^r_EkckyK<^dUSCikMXqn zl;wk?wl9`bI39~dN2$#auWf0pC>}5ZQ68LyjKq2*3C)EJokNMWiJ~L53+A3GDF(j% zPC9|&M0b6aA=vgDlw^7Yve4@(PD7N+sA zzPvXV3!)m0{5Kkj1gcP@z=>UxCVYJbzmQNZk@GObsQQx-51B&?=jko-?F?=~Za>tk zd+A`+-g$1LGMip{vGyJbC+o!E2DQ3eh+#&B8HLCSwMq(9>czl%wYm&f z9XpZpKC&EFZ+I_NcQ8-ERHim!{*{h;!Lv+lk|MQ>m7tC2@ZrPUc+_~w8$v?ZIKk@gLRw-WgxAnAZb6Y^;)r*ozSU@|(tLN_ zam(4~YqBDihp_KIxD{)AN=nSS17@$$3uO1!QFFaVO`aAPxFL5M(Xde;v$n95=^3w| zJ-h13cO-Z6H)r2@wwvHDmiKiGq`ew%Xu6e92#nil+Q2ns}fI+oL|POTA+O> zs<=i^bzb^8{XzCijrr~Ei9tovBhnT{uP;gsTK|I(zh%m_ZFA!{PlC9I3}uz)*yk5M zpr>eJEFBPxByV{!dD9yi9_|$AI%?RMF}^!5|FF{>$t-uQijP|t60$CC`#LoJjGT^S z59PX>v|P@A2nOR52KS9hgf7qm!hDMKWwiOgeQE4q4f4Zv8X8VszI<}hnl)Y>9a=q^ z1J>i?UO9=dv3!}dJX6L8e4c< zzdwbJ5vwRdKq?P~sq7;GvR1}{`!lw(I{#EChIu%do5)o_I2p9`_18~E#3yXY6usO7 z-OJL`rq6IdgqC=Ho!DSdG(Qioaax*aaPi1aY~o7S;c$d48M*KY)>VI%^*p}F#7g{f zex!pC2u*b0hx_$)p39d{ow|Iv=jzL+lao(hMkEp^wT`U-1%jCnk}(&85sQ-t%3(>Y zL`d4Sak+p`x?N=mms7{1;~+{7M(-IpLuz-($8#qBq^1e82fDH*5ao5bArMj!S3n~C z)%AHnpq^k#JGgi2hz2^iI&N!9#ON>t0Xwj6?4m`9<8PaY-oN?g$w~9#s58=1IA%;# zP{f+YZW(>cbgbkIHG>uQ7Dxu;Z(UWrP`!3%t7%BZoo^#X8eh%jY5uP}n}TD6@CYO& zNP|exL=sn(F@=A}Y3zo$rSTX}c;S4r#&ZQZlb;ufsjQJ@ z3zE{#s>~z_jzIEeL7!YeIjztQ2)bl7L+>sYX#K%+KHxT-N$;-Iv1~CQf>kP1Bh+`7 z)aaACngt|KQ3DZur&4WaWtc^9Fi~U=E>!q?a3LJVSstaOE-VYYMV*|*ijm)8u+9ay z+XIV%nS|?@H4ax#(w^nZ_wauKP(@WTGs|Of#(IsRv{=gW^W9k`#e*g2Dwa!G)u42| znAc^0p88bUlhn{i70iQ`BS=QUk!uK1q*ZO&D0puZCx4Hl5o%m%kd7GMvF!ld`%|r z(UN5F3pGL6BS~(?rd)Q0J$SJaeR+9z`F7e$SAHP}#F$uXk>e*V95*&0yfm#iF|jxe z$8@-wXteNSTKZn&6SoWP8o!yR@w1%k|3tf3AQDI^4q9@RoB)b7NEovTgi1uo7k3@mqXc%ub&vXQI0n@TAz&K&v{`n zAaX{P^*L5U`D#5GK}?DbZTv}cY2zst@oa@$r*Crz#IwV-wa1^;b6Uj7c$gMP&LH}u zo~kN@KA-}j9k}nsbCyka{QTRk(S~N$mX$>=>Di+1h_NFowr`G}>!=fuXRuD6>JW}v z!IRorMA}~0bi#f2dNUNMRW(&e$CO7gWt0#<);xEn$zv#(=Y$2O`Q~_UHzzc@8IAU& zxXAg3u)Rb40%spYD%hEr$r2>-sMnO4+%4a`L-gM+ZEY<`(!efv*zUYU!0si_i8?Q zB0;FXKAk-K-A|qa+^;()FBL``)A_Lr-HLdQGG^h*aP>QMqe)*Nl)!Ti@78i2^FVN} zhNkh$qLmRdjrxMx9WUMMYZ>$5p$go5PHz_8#p)f5hH}2o&r1uaOgr2YW(_t{@q{T( zdUIGE86Qbc&t<=T7{;T5A=@8TJP!8m?sHghq^s6v2$k06CkzG70dF)6t)}_A?ritD zQ_yFSN9ud!=q+q{$gb6skI~7s_tt(p82Nd-rt#6?V__d1){N@Q#X6})>ag{8Xt@+D z)@i~GR9b4WJQxhrXt3Gf4R8e7b>uKT;y@XOgN2_7nvGz%m^^J!08xur&aS@x3w!y@ z8H)Y)Wg7py$UJD-hP8`^4qIG6!D_ytaypFk}v5LvX|F*;I#gW(>Yelrtl3q=>t ziPx$yhy+?RK|g%s+>NWRFL`!kAo@eglgsY&4IUCPF5-5PqT^@3l9FHG=h`U_^&dTG z_A=B1mk!}c%C?&EhVw->Rqz`s6pD)qmUoPr9ftTRtUarvE;284=QEiUjz^lAi_k%d zd?dLpNa%oSDN(5sV-V*o)!?Ys8QY=Y(nX0OUIsTcv7KBbv%!(1=FU89f?9nr;@MlU zvRwJ|JlGpw)?U}Y3D!9e2qUqW=Iy*wTk96qk@Eh9$!Sa9J_dQlb*P1QA)~Vei(TxwY|QR$ za=aB|XblSq@(3fmW|`vrt5=`zssx)@4SSYNSb6Z^%A}ke`i*~Co|7}EkPa{?(snOi zyl2ni#k=jF;4LC?9u*JD#k6w&aLp^kezNtgn;=2=1phgXp?caH3AUg{#oDE8^|U9=yC-QhczTQc`7U%Fd}%d{-rw{NK>Q~CXXHXxDR zn(4Dwt(rZ3jU{&Ws${`}ta|*UcErnyF?v;ljQ{Or*A4Mqb@wk_e&~UvOW}9Sza3h> z^nRBA`&n)<^XQr8bbA!dZKA%U#CoJ-Ika!t%1olnmCN=SJ6c;=lD%uOmNnC3lUJcb_2^K6twVfr`i$5g zMh}aktKQxNQOA44f7OB)MAS=bH~Mr%)N5fd#qL031*<%-kvwDg&4`aH`HEl&bbi7axzzgU(27rkaONg| ze2TgfK!svJ*F*>^xDtg5ee{1cqVq_tR;ZQrq|&f}a&3>l!OR>-txsLO@%Gy{uAY$? z>O`Gm>RH3f?d;4eFVoVFY6^U}>zvvM&2tNRWTkK4oK%vwp=1Tplcb$!+5H}+e$z^8 z-pToyR(FGKLaR+MiNt8F<}s4cN;#kNkKM-<3)|Wo585`eTjngiE*TO!*GHhuTGAV5 z>!IYwos+dw6bTN!G+h=qZE@->iPG_F7lK-6ytW{{D1io#OIbE;RjBCR-Ep3xVLOE3 zaN`H;s>iGhq*a(3I_uH2;K1z6_;D*@U06+H8_hZ2$e#E(xPblgekPh~hN9feSzuuX zZz>e!_`n=xs>OOIs1CG%HPgBE0x&UWZA4v%g8cf~(gS0lope-1^T>5p#yIJgfbq#` zYSE0{0g_9|9c%9qoY;Az9D}^_t}c!&OP24N-c_ZRx;RpfEYF-K$8^C2#9HyfKrj{= zc%ex{Mq176JmLVyGr5uIV|}$1q!A%az(W5+o1~`A?5B<^Jv*TzG`@p`5>(T`Dvma7 zpEiHq*y#xEi-UI|xkYT7B)NwBt=R}>Jimnf`{|UIGoSxx(ri}6YC6}f8aXC(v^myr z&tpedvjS_ghu&;$HkQ`kfFumIjmcOnGO}Vu!T@veh8d51Z5)slWhm%5e`H;QNE{?K z2-1cX-gk|gyfub>#m*_6K$!Zxc!B&b5CU?3cHshRIwu?oSsF^TW6qt+f{3Gn- zy@vJyQ>qOghpX}Sigjw|wzYRmbd$n&-U+ww*?`j+(~<*%J&Y5!&tl)RSH+I#83qT4 zZ%!~w_k=6b^t;fMEJqh|rNQ7^?t-4)VvK}I=QeN@KHSo+;n2c{@}U42%*)z+x1k6g z!#BBR3&SV>F2?@EPXC4IFSO(bpuz6DMeVpH@t!3c#u1H6({EEu^=i9RtQ2cm0!x|Y zc^X?o#u~c)4Evb9w|wQ)O|h(V*49W>VM&M-*4CY_&xd6KMlb|P#}zE@&wHCi0Fz`{ zKS7hk&f&3;*ZNL9E0y4^fWASg}dNSo3nZm5BQ4avxe6b?I zL_%WMI%m)@I!I^Fxh3>0#H&%?B&jtoe7hs{L-yCk&0U`%KJ_$`w_mNCBP7?U6$|TE zPMS)0A52=jJI;IheMz7y;$$d z;A${$U|18hL&wL9tdtAdA&ts%{>&7n4U2{l1h5qa(5ALYovJ(1LQ0STN@&x}NvSa+ zHKP^pe)$o5yKEN;f1jTZO$Z^2CZQ6EHkqsE?9oL$@+%{B3tf?)a`~ zcjH6ZuVtBr_(V*YbSK3h{5(7V&0VJ90Ta>0;@%cxtWjsP!UC~5PJpV13n;^Fs0IY> z9_|K>fwj3NKj-0+1bKk3OxaBLl{~1NAoz2eKKO`2O9_aV1#2hYLp1Zya_H|y5}jw~ ze|etOvFg|Jjsnm)V&jvk%67Gp$^wH;Bhz2Y%l^lqJ^v^$$4;6yF2u_z@RN^nuI`?g zzJjt=XHC9CKElnHI4ojda&3i?z*T0D- zekv(VqKW^LAm}2df$bLt!a_}w6Hqrpbe>#v)g}p|k0t^U#5AyJzJ7RzRuE?KnCZRY z5G=gQ&u(+`6rU0AUgo|3ViEy9wqUK3kDHi776-$k6yu4G2lQ;6EJt4~1^&M-B2|`Q zJ#<05d4C0E?GsxcEGrjE($c6wY9$8N?DXlHUidL$%nhSs7D$ zTTvqF(kWQ)j)rs1S^&lb+_#sgrG`d}F_)oXVO}=nJk<8+gr67G_-;c4p(P(mfq16G z$?C@FPBAm(n!sM`i19F;Fp2&UcU>q3-TqAwK!Ij{(^wVEVz zS5nyCRQKowfi3mHGed@t3=K4SCC#pHn7lN4c#81};@zrQUD*{~+GiDMH!tZ?aHGAi2qGtW{bzx!VaX%?B zR5szRsR`L{7YNmA@cOK{$b}E@#0ES0{nn|mBi$wdNd-p;>+s8iIS#-WZk-lgzLL); z08Mhh4&-n#T@;5fLvx|QnSs->X}CjSl=JoFtrCOlsxabbh@NGYzr4xn%b+a~R-~?5 z$ZeVnpH@5d0aWxBJNrdP6FYkuI=Z^W+tZ6hf1iSp;$}M zL?eX%!w%E{)LH;QO_?rY+bdG<--{d5sq5z=OYp)V$H4HJK~gNGu>xc8TW@Y{y~v6i z6>=XnGm?@Q?1+prsYxogcC_IgXARBpJ@P=etcrf@>>yWdn@@#?9uRgeBOp{(!j!nx zf{<`mQS1K7)kHf#J^IK;+lbZk?CNr0Y}~=kmaMkCrp%Hrw{3YVOEptEUeIIf1V8cZ zrUW;RG1tPa`i3FeAWQ)ge}z;AETzr5{;P7q1@4h?27+FRo<)znT`-F zl7av|2K{_pHZ~OLP70|)qr|lvwl!_lt`*ah2lnCsJ+b%R6JqM-M;_U{^`VFQS76(r zvBn$_$iJdOz{fbC?D}<%$gy7Eq&wB;y6*b*jlIphAwAV`bL#CygaTb?Bg%O5$C#0p2pJxA~+j3r$5IEns2Yr!7*4ed=f5eT#cmLpEm zdpi}RWYCr4hYRN-jk9oFM99|`WQ5~%)|G?0S3wowr&*6>gJxicaTyX5I7hI(Rt6ly zO&L--nS0{fjI@2~s42Hf0fmu87ZcpXvePfmaiYBS^W3J68utWu3UHBJ=8n^gl3Rpgq_Y~ z*Luk@Jq6-q^#qHc7Es?rwMvMk60jpkcqA_98BP}Qfu0nlP%6jX$zvw@CX84g&lM|P z!@9k#`eVMo(SxC4W*2+$*=K3?wRh-_(**m{==n(bI?8h`y$=tJY`(u2e-ZEvS*|?H zK5w48dCJ-|$pJ|xS1BbTw-+VK?o@8%Dlv|?uxf2XLZ}>UbXLIlp+;l=(j%+f3tgth zj{^#|Cq>FrKa)3Lr|;}ZLa;SaH&3mW-I3hV8Hm0~eBZ$ciztggT!#QwY4?1Q-1u?k zJ=#rn6QS1!eaFV)h2GbdZKT8ga-^j=M~Yb8zhGuIE?*+h&OBC`uh6|zOmv1_{%7nN zQ8<>-&@E8=KP-+KFNXn#TPS3sdAY14-F8HHttX~UX;L{EJGPYe=ej5&BHE9ya8 zxA1WBL{_SHwNVI!upDusexeY0^BvRf80v12e3GU-_|~3Ltf=6YRg0X*csoT^7?Pm2 zX+X?Eqf#wPl3>1fdw1<<^Hi253&~F`5SuADWEz%Y_OM}?6eqP!zgEiOdhG-bEFUHg z=dPO0aN6W3EGIyF=)tdH+TV7Wefpl1{lH`g5?(72TBx{;9b;du`u3aZ49ebm@?npb zM$}g%yHK5mf3e@A=9&jAMIBiAJSPpP#3~5XT!t8q8P<+!Qq<1J=(cyK?27dCnH&O7 zI630t&sk3JHIn6N)3MU4RY|OY=cmm+3^0bJ?fh};6faZogo9ubC0c+$-zD$@*}eB)#StqSua>2fZrKxWrbLXmGhw-!7r*yz zv^PgBHu{1u;(9bL+Ia!>t#OLirCUVQgFwu8fM)pd%Mo{lpP&IAguLaJx4zP^Lx zMtyB}cxb}-`41*aAG6mgGfDo~c4^*&*ei{cDgpS!o6DDLqX;+|a8M2W7=wF*2S2C9 zLw3%=p{$SWd4bEGr~+CZSm6QSww~QVQxRgslX@P4%KR7F3w0FZLB$u2E~LXFxfr+pvoBP6QWTf8MMKw zVbc%w5#y&M-Xcg7PqFTIe`A%Wo}s&cdzoBcGO3D(IcCLTx1me)r_@dWeq1PLloz$B zC96wQ_#x`vPN}u|R~E4P!1A%2V#Di|)EJu1Ou4seQ?yF&)84unWvMp)f*AHV#;E3T+izx=Hj^oD4 zg`vrPzHnlO1CTR}CLvESF4($nsUR)cvw5aqyujYNn*74U;pTrIy~Gj^0LLVMthHYgmTYSP01?H3Z{3I!5zsZz627~m(!g`bgvZ2p{*-X%Ej zzV*(NzdZzlIL%zN5J}h$Dcu+$mZ1=#Te0;%nJKx(ZGz*T{tXWPrF+->TQIIu- z6j>Dm*mk2iJ%NgluUc4Rtmpmjx)~elArBhTu}h|{S>!@P&Fg^g!%i=wz&C%PyPpLw z_sXxYv#aNXfXTB=t4^o5FL*BFzU|HA^Y9f`{Ed4nd;0Q&q}KsNgiVzSB_MY|9EWWM zzLGm2x?z24XRJnCtvLq4Zu4tUt#fj~TNj4d6p>Pik-0O)g-V}cU%qjbRlN-T$%1Wr zmy(z-7%#G#tL&eUGNEu?lC^0j8FUQ|WjU%7U$cBnv4WiVdgh<}8a+r`_;Zq)?7RLN?V^*)nAON(G z6RjLBmeieL(6MT;Q(*j{_tG>HZdg8id_gR{yEBa2-hT1yq7$#;Y+DpHwXm{rpQC|K z;={*--DgKxCou;EvYhMnb(c8}?Us;8FUFQgFB#NR)HSHhMmTwcXTKfjSKxAN0YG3w zgQK0-Xp>2RMT#@OP*GcrZ9@b;rVT%10*7CvW$FX}W(!&CYwUUU>l1=;&c4TI&3ydc zIWu-0H^)AH@7ywoOl_WE+2$Xbd5(S9;t9pc`9o8mr3vKh`M_gS2M?b5*aM#X3u2sy zL_gL&XOQQw%{rn;G&~NR9Y0+0G9iw27-~8r3^h24{n73cLyhEX%)&8ZFlGTZg3gfu z|MO5ojqHajaL?8G8JKQ?u8s?&x&>@5>f#Qp<)Cid`76{~vlk+U@W?7yAR*Y=wsqtj zI?TrlaOYvRWyMRh6^;ci3@ZP%0U}6q%&MTpi)XK-+L=2AY98WjG z;Eltt2;CKV z^&$p^3b%sy6q3J`+!aBm_U;yPU9+f>s}o$ z(}~}qYOHKlHXL;4-a9Mf84{jlj@OZE*BZj~O|$Qv8@G4%sjb_e!I*rZrppbeeK;7J zshAcXqQVcJ8UUl~U1g~5M_OZRz*1vZ2535P>Z2XE@;P!aOEzuT08816%i-GRx#(op z>n$06%y|EIpKTgBZ<4@iS|=sEa11EiE34DSjNXt2RMMC)0EwQBlI-_~6*wMC5}Co_ly9a-=7gUjaL!8`{V>AAy=f zgLc_iZT%b!*&93f^35QPxX%ofj*E65yu$c(WRd6BaT@5`G;c9??RO-s(%IT??R4Pf zw&+%BU$yd2LDfsGUI<>P^JJ|4V$ zaH0KqearRdahy;w+p;Pre3PXXpx<|2*UJR<$bG%0CQ z?fs_dR4Nxw0BTO_KUW>3&kVPJ3Gb8>aruLL!GbF=mR7b;2BbCAB*y$kYyMizPW^a} zeN&sJPg077$W1XZn`3Bd#)5E5j1x27dWojV9q>No%7i zc2-8TXG8#`Up{x%*F%R~Vx3F=n@fTGwv46WOSH;7IFoZs^g_QpBVo0NJQw|Y+EngBl%QJIs4iB^PuS8~F06L`(ujSB>4K+ld9k|F5J1xONqGR>3bHYEU!u zQ%2QYEwXv7Xbc~@28Hx+KP^kC>Mmrexs+R2i0U%bS~wPH&Wp$sp_#LN#);QqxdyL3 zQob9oxNbv4(XwPU(TD(6zwO;86HDmsU*Do}Wds3aS?c1&BiCHqsq6&DzxVxawU%;j z4;nVwG4=M;r@nt42g&O8?QX|#%eE+XLmR;y7Xp)U^{om zD?od6S1KK96AUX>W(qy!n;N*0V4w_)N{3LUB6bu!ZqQ$MrGrKM`PLqH3TLhGj{N;r zyvL-5V|gHdvco^SlN63bi-dDU0N6ZV=>xt65at0Y23$8mJ(H$E2rl5d1lGRVSYup) z_P9bh0;Ne5+JkgDnl!{sS(+pt0!ir)05PDSxPrph4qVeWb}R7Zrg=$s9#}s-j{Wp- zeN()?P3^c2Wg8!moDRgdBRkK2n6U$Zwu$4`-UF`#Se7ZPyJC8nlZ(u9hesU#rI_jD zseLhSle?NYtj!K{!wjFq;o=CJCJ&3`3`qw&*gCyjziVU9!OGfLcB{vy0kAQ!=fguo zWyR^yCzp&<*p9Crr=c$+?9aG5R?_$xyHEwQ_~CUM4wyW>eFQ;VUa`+>c*rrSn8kQ` z4SJ1ce-iN;xjhN9L4(p+%l>x0fcx5^qNC4B)efWq2ALFf5X6` z`Jt779a>pFt3gwCaG&ue zpD?}lF9^S-I_#>FRQRhwM+#7DGGJaNmWUjYWb9;>RGKCwGN-okHWn+G@ImdZ6>W&? z(a?t6+#H4B5X`B7CguAHXEfCxrv}+rQ*e-#U({I!W!o^oUcsSZ+?@?a0MJYZS);8S zdZGpnl&?F{iURMSwVPhjVBkVei}AP(#m=cAXbN!}7&=l2!+sCC{uFYP#;isUx2o1;(mH_(o^mnJ)Y`m-?}@uZ=2#dUiG@J2iFkWee~cBaM&9& zCMGa~yYIzNfK3(t2bFjYz}xW9s<#Qg7iQK zcI+-3PEOA3*GsRkd2AB?I z1{{BAN>u*_x_@n4ddgJWSWn%)ZL((&4OLtW4c6hbk7^k=bU?vT!?r+wVufAkHTae` zTn4QZ+GufzI#3!OwD|bi=(S6d!f&5SFvj3?4_~`#b?_b0pTnq;`1_zFUD(D4g} zfNn_$VliSH3<39aCHG`&PD3545BkEL%fS4@EawYckps5CIPS_?;Qy*=M@_<;QWf9#GvzaMN z%1{GL)y{#Mra(jm$4c?x-dKFRn%9$%0ScH)7B*wGM8dvXapBCmfXNP4} z?sp3EikcQ0Z4ej31v3{XXL@g&ArXwf9tIqLEz}vXH3-W9iwDO)ISV!=xHoBQOXd(s z&Iscu6`>oW{J1#QEF`v#*qRge7`a^J*sLE;-hJQNTiS#?nzDWI+LX~mo+;}VZ=a&v zF8J5gs!hg7)lv)pMaP&Z=ToeNeFvq^GtawBohsyHo?4Ll?24o_>ld8TXCjzbB|J51 z!RjZ;)nd7kGaS}HC`ma~&zj|GSTQ|u5sa}zHqc~_TGmReCVuh<4aL#!8>Z(v1rvAG z?+f+Bp?V$aiBY$LJjo2d+OQ27to8BM10;vW9bAZ};dnKD_#uUt5mwxag^?-ohZ2DI zM7PRV?T2crP`YF}?3ww7xbFH0IIXYxzCP09R_B{1biaM@%iMiGmabhH5j!<{^pLTi z*X6#wk%~u3(KaX=_!!3=jT7a349+0#6u_MW9O^Yb2WOPN)IK`f$=r^7~E;) zzp%-c0KXxaC4geI&kTFlNeKQ(b_@rIJ< z9a9$imn1*EQYVlncrhKg8WAUsmBAevY6=|&1&(zEA;J}(2n1o1A;pQo0O1G!gW|v5 zMmI;?m$C8vr@W?+^a`URU(<`MQ2nP-_iOc_$lp4tV*bk?k@!^H-14H;OO7TA;d3`M z;p}gS{g~~CyWcn1Pi1PU%)_hQs-1O1P24E12^X@fm(3TEOLbD1b108nhcst;-Z*{} znQ0g$RnNNb$ei>kYU?|+`u`M4tuA0Msm;A;;Jm8$*nSn^0zak&ZCRVaV-&>SyX(Q_ z5AQO?1U^Ul$L?c4HN3(;y7tjIiWs)7)Rz6?@<&F!W)NMFMqw6OH;4Ip#_bg=1bsqvp{yc?$@E&;`XL-l= z?LKyN|Gr15e8lv~@Tr5SSbyKO=g)n=Gdkx=&I`Zomk=HxVsiBKTd;~NV4UrMY{7dK zHBhfOkB(kpzO)pV!M09uCf?flP_0j#>I8|k9^5Qf%T9db(aN5B=N(!@l1EN%ZjQ$T z`}RGQyen+(m^aAd#M|tt?_d#n>YXC;s2(#Tc*kl5PH9j$!pQ+D(V zKD`F;V~=~ETfF29@3mSph)dG^j2ol7Q#TjAhIHr*$Bh4>OZxv| zfTYwIDmHb;{FiPSQvdVlU|V)VkZI{`*8Bh0j%EC}A;L!A2m#6;8=br2T*s7aWcY@d z^3?i;iKC1@32_@%AQ%;5mPEew6D@k>C0LrPsY7iMIuU!ndF+-AF?;6M(x~H?+3Duy zCieVWRIVBYZe=Ou2Ns4}0~;q?C%ITIS~x8$7vqL7s$Qj~H2$3rD5&(1a)H&R@7@JJ ziUqI)r}EIgVkwtUA7R&Ce&*XOa?HrKB(2`mtcK$a8PJf1V+m{Msa1#_s6h?&%`7>V za)e4H7+Lm8g-XiNw+il^I(=(5^rbLZX5*}>;Hm@xZPdW{^{m4x25Kq>S$f5O+$V7{(O87{SrL>&`O?8L3igM*yln@nv8X3Ps!(EC9zvaDlysFKU`iv9w%{Gb z$R1DFU0V2ez`b>-hzG?3(jWY+9yX+#As39$c|vP(2ddVBP&qrhc_Z|Pm*_3x+hE}a z7e!GBuOlh8xi&DvNof>gGsxW78Ch?tYl^KIs6byV@TiI{qDgJ&#GgjNpF4%%2Vg*s zU7kXqt+Y|W)h0o69#QK%5Xs&M1_3TqqNkyigK0T`#Z|dBm@$vxVy52O;8_!I4IUk} zl?<&trC+`eFEJ-kA0LZJYrwK_mzr(-3WklFRrT(w4sNNx4-{_TMnIk3)bCc4(u!LV zoE76pB|VGTGQ0%LjG3n=OEeP*!gZI#R)0DoaZpc@$bWx$D|w4+%h@-&?>r zA$Y__a*tSJp~eaoYpl?p0eYf01v!gi5D5`^?PRP-%8w`q^P0{IghDly(xBH%#S1E6 zp{(z%z=}cCmjuMNhMoc;(kes`7DH7^3t6a&`4_h}WwMf%(uj;dKN97G+bIOW(8PMg z;))eL@JuC8mj2HcqDI=m(C(sH+=zIu4>$v#*2jMx)YS`7d9a{19p6w@3Zr}5C zW!PB12^0LrhOtJtgj?1hdLU)Y#ED}}fq}3hCsK^qF2WKo1Jd4|gUyhvkJURFjJUD~ zfdc`3#g!Ob;rHw^Ohmp((&Z!dg$gJWq}k~=mO8~|NSuob*F>i*O$xm|noFFE3Ri`v ztX>s#M|6?X>heva#*7+2$s1TI(%wJGl=gbnLLow} zvDbQ|JB18{7Ag{w``jt&D%BFH33J2S%8|urDMx7W|HVqRI+N8(CRPjoutxIlRGS4) z{ts<&QVXU0zHQ;VL&{ep@dS7{+)XYBR}leNNFq4M3SF!H6`ZB_0kj;dBOiT*!h_+Z zs?7@4Tp^1iiH87)Rn(3&zHO4B4GNIWh^3Qff1I~a6Djf89YEm`LS#Gz%Vr@Q`e8>3 zU=6H;e}^uh26`_4-f|5v>kkkt2h1j2GeNHVDDR9)1%B}rj-OhXJ-TKxE`@O7(KO}u@&psg$R zQFU{bT#cm@0yWOXtz2WFeJL;t{$}*jQI2D~23O%NKAMW@#;bH7yStji-}1!TsI^Oz z!u_Y(TadD9Rd7JG`kwIguIy8Sa4LJ((}Hj!Ywv0PJ?n%+gJbfA&DWcPFM$-JnbG&} zS6;E?>|bNafdQmTLD-~JAWq$_E$gS`(xmI5{gq$Zbv3S^rfb%H`)7=a9^kUk$uaTd zTI*(8bRu~o6fdW@%)KYh(qn;2;87ZZkZ|8uh`dBg zHf^+USDQoE&h$`#@;V?Cy8)!&a0;V8kL`7!c54?KyJkP8FFPBxb1Zm9Z@Jc)>nM_* ztRm_xDrSzwV3GohsJRI7x0;KYJN8YTDygl#6|kLr*R7eN5w=W%0f=AuzN?P299>ze z)LVrse#Y-Rar|L#ecJ;b0Tz)QeTPzPbjQ6%dLZwEQr7F+gmyy<=<$P6wJ{N+B&j;o zSAY)dqQd;b+{7R;%+26t@H6zpm9z!6x)-}N1-2Ds<>aVIwUGtMn7Yp7g2L zw29`$1VnjLAWHIHA@idv9(v^h{N1f3fi$+Pywo!%U~dscCp?^Kg%s&7QR&ouu( zU!0WS8wrJBgPaJIPn)JN3_=8J?=Y{P+)guPS9T_{Y-3wJi>Sw#x2Tc-ueh%Pi>g}N zp0)R$J%dh(@@o)*3_mF<@h_qxf?|${G72WH6V{i@|84RM}JP&?iIIg6x8z0zY3Unk>dp3J-(&FBFo@pojiN@1tJ=VY;(# zPXNg@u6TlIKl+I1j+phySr=-va-Mnn_M6tMnKNh28UT)3;EZBmG7)#gToL0gl|rzy zfeM##>BmtUo(@UrCmYtIHoHOVIXb^oY@8qF6oBkf%uf}NJ}Mw=Dcz7ItRVF&?dZTv z%HR_y9%HT(F**(0gdc*3scs-Q^aMkO0IllbkAOrErmi_~TdIklap5Qxk>NoB!5GKqXc0 zGUQVZL3IaiRSr^NtY{{NVpZb*a9$A3|KZIy1>wy%Q=7a1zgi6NFQ)WV5 zf91)Sq+WvH=IKs^I8Be9mLT+Ci|T~~d1c+$+3A%0w?0jqCgyeW}R&r8w zgf3Jp9%?asanf++qx{A^=XKU?E!k#6*81&B4UUE>u-D^RBHuR{n%UvV$3wX^I@Dq7 zjCgj}w!_1k@EPfmV;Y89q&T~|`orU|GG+#ITn-{7I^_bo6RpCU*QoY94AFdy>2Ocp zIND$Q9_&zh%;e;u?S}ysjtpKgi~Pk*PCUN-5V4V{DJy2^8z42=Xl3<>k54U$6VpV$j#swIYRwWgyZr@qS(+3pL!0 z92kR5s6!JFv}!mIJc>~JT>OcWnu|OvnmTiCT-~rn*909x5hV(!g(zVZ)#Iu&-A>J;o$|eBQWK zNK|Z+J~`PFp@|tzW4*IF&Si7Ah{Uo8>KJTN6^{Aj_QSJP=m7iHrBahpKuw6X>ypYY z8FU>8f&%VSP(oOZ5Xst%HY&z#0G#^uZPc4VtCT8XLKU==;CNTP*$lrYAe3FGBA!E;h#4;UEB!&x2gi&TgQuHI zv_&O@B`i|X8`9B@jE-b!xEb{*YE{^C)ybz<{8_Nyf>e9?<;#p|H6_^oqkr^@U zai%`lN_J&t-;JoIwVC&xyGGRX9k*vO%DxxrM?zovrcLQ!|FSQ9o2=>eWHFYH(4J4S z_G~+C_FPf7c|}eO{b4`U+kL2$9(9_;0PJagBa(!}xcCbL6sj431)DpffpP?hJ}7;D z&hqgSR^;N*)LUpEp|1@f-u@YS?)w(nepQI6-Yf9zWlSpGIWj=IsB z3l9^qir05(`X-GCp;WIAJxFtURpj8IBRcGQbjL)nYq8c!Mb(Q!DD%XO!6`H;t~#?+nZR^iUdRzQt*|L z!l-1Cy#-AC%t7PAf?w8S#1jU9XB^NB5)VY#FOyAalcDrC|8}CO7{&%Kw)|FxDF*Co zMkowxve7ztU3ep(VqLJ;8gdWdgM~Ev9kS{x-!8KHJ(`WU8sA$_6$q1{P-I~I z7Y}zLNt{i6L>gR-4nhw+fH_9ESs6xXzwyPSZdvv;?{ym)MbQ9R{M>13{hnr23KmkB zR=v&B^vn)Y^&Wk?%kwMx^q+)Wz$Foap((`z-N~?_;%BPm5^cJH{RWDaEo~KW)fn-L zGR%xjIl!pIzY(LQWCFnwMlJ<&Iw{QqT}7#&7vjVg7X8hr;n??}Mni}*V%a5(7iL2C zbEpH3NemPuBXwfVJT-&@#tgwzI{wTPRWrgWk5)8E_CBW`MEVe4Qr`X<-AMmBOdsqL zTB|B|U3{&md7$9l>TZ&B_Fvf7-Yul>(!_r?lN7w$2=3OAXjEGy)QU!w7XmV6;|WNY zF+~aG>v%b=v~n^JPiM>kvYZ25i(KkJU`gRfV2K-xMB`qKkql<`GGGlBZrxhGA?fax zPha0!eb*hjMiy6+nCuzx&~Y4S^gngOkkX}V=Q8EZ0PiPh(ll!|ThJCi!z+^i3%^DL zDHR_%)KvTQfdfz1B%~~wD`wQ4+IQfY+Br)X%_4?c9INHD2j}cN#m`ZiFQ3nk_noSp zosu$pSaZ3&pI?gNMRVnL#>f!9d>OrU+o40-qE@W1-@rPnZ&i!RfwxAwKNMR(vnHD zVe^E&jwb*C+KOxAeCXx#bTF`RY8;udF!TVC)^>7Iiofz9mYc!!@e)S=j7jH6Ljw9| z+z;iTWxA642jz{t+zbbRGdrIHv=OgEvWFQO&4^k6(TMvnBmj%!DWXgZDiU+G2B%E! zeZ*otcXXK=Swi4%jJ~XZ!7t$_v{umXPa`*S#bGk0?i95het07xTOWKNpO9kV79~Th z6LSxdknbA5BY|aQ^m820jzh%w$7)+$Lqna-3fv7bYD8PU>^fWk1o=pKG~3ryy_0J` zSCVZixB|}jY?}Dg?%gL&?B4y9)OLK=uH#Sb-gR6b&xkc?BP?JRe0oRBDWdJKct8&x zz;a122;3DoG%X(XGb~yiIdPwZKIDXbA@K%xN}iI6LT0f!e$Ra;WSipw&McVOHu4lQ zWP-&dcetKqaZOT1qw7Tv7G1)=c>GRuAn?-DME=F)f7niDKR0hpDIqf!#UPKv;y6O$ z7SF^_lX1t>%L$1!#XA0*ziI(+d-`cBZ>};LQaX{kj_h+f7x1biR@pPFda6iCjUEK4 zw3KKchQXAh4=*xW&(Z>R#dAl5&24*aF+qxrEv5v$b5KdfOw0!_VxyL z7loaY>$r=`at{X#(?JGHV6m+!DH7tEXnj>FI~nGT;`0k%fLc10hd4x1Z&Y1>Y2N$p9TAI{cM6imV-iQAfi)v zQuT)d5@Qr0!*(Cme`p)?fZBv7el$IUt5Q1mhZ-A2N`eexxifzykxF!y+j4%okdTv; zfNN86?H|=^p{57H%)z%l+(5Ed9{XXwQYICt7ZR8dJLH<2+Z1-`g`Z$=%8=v5P9Vu4 zXr>W29U(!HBv@?dQEHUhZum2TX@hqW38n2tAVEim6l?X$kgkabU%;pW-Cq}g7*j5- zGk)ix{QYkRdKnUD(wMcmNWBw$Ly${+aG|bg-|iJJ?z^GXXMS9Ghh)YPCBR(s5F3|v^uv*V|RqNx@g zF2gb#k1lAaoH!T7FZGYkTrp+R;_+=`7u>sg$-##gMsAO9Hqln2VcN97sCdi1TO%TF zn#x|SS_aJ~+7U9zTn_sPLb#B-LqS#zM=})V(Xl99IDJZl8)R-Q>ILxPL3+VTa{V&&V+&ygx|XSlG3HS z;{BqhO&&Qw=Nf^TY;`KakPgr+z#|}tF`33;4s;8;YzSnp#mrM!u*nq`J2!5zQ?F7j zhU~9XA`&zcSzND_1ne%2xEe+3F8k*%-U_7b!%ow#E zTH?9caJEl=Xg;$VQrDl``SvEj&Npc?v-3&tgIK85@g7kSs{VsF;p8VUV{x&X3wEyV zLyfV>q8ZUwd4)NC1tYJJb0*I@uOWA1 z!$#*5U$sQ62yaltorBbV z!K@Bfin_pg+0fj`5H|s2SVR8!)FW>a;wb)1)ES%6(9?8d3l6G`HYJMY?jfRJ-< z=?cLxqiOp+R$F1PM{D4=#&IgL6E$G5rD!uOEkV)$-ugJCwCIBong3|1G|@=Nl-PoQ zZ;i|gm80U!+2s?xJSU2o*Dy_z!CIImd&g98aWERXFAHvDlf+?AcK2ZXNUEEMNjA`! zhpeW$Zd~0bz8lU{G-x$-(T*|tg>Vu zNlz$Rq4%1*B4y6<+C1NFPXd;B#d8_Z;`co{KpPbB+fPSOzi$lb{>@6<-ftej4DLkc zh8Cw1NOY4x9>r|Hz*@5PA#yzIhiDB^HPot>S~qT~bx3$jDOEy~>q7Iqmd!~?F?MCY za_Go2iw@@GZI7NXVcw1#QX*tIdQrmsd0KN!Xryw6UXR|h1 zH|w*{Zfeb13KQ-YZo(z7n`2{ls&|;A9TsUAX!uB0Ngtvk{SgHRn{?{guLa?=XKP!X zhc#nnMb26%2+QKV%>p7+g zqK<=!%pWKkT~Ks1llT>9Md+usb%j-p-tA2!VocLeqm+!;w1S%QmQEPaGm2;k_SIhQ z9bmR*EUx4Ihrr^)%*n9i=D{9nb*ghnQdr|$ATHRg)Rb-0#k8~t`Ksk_(1L3C;=B>1 z7{p8dB-|e-6WH;UxVmQATSI%B<7`5!O;;hfj#v%VZ#*d%cZlKkS{PI3LVXW+(Wyc) zZo=19C3pB|jsS-px`sOf808Cd1c-y}sB(v+%FK+bBhk0u7BJP1w^ugPxJKA}6}dh} zu*Hp30^_(FS2f})l@`K32m^=73G5BPL5%4Q0@QE}8YFvx<0A^|j~hA?eHy6b+N7pv zCbmYK6p~!?K3MiQX$`VCPSB^tKU6ZAfML@K)jcPI3)i`!6rVc3$j zDZ5@ao!D!(O`u zZaRxpH3T)E2ov7%evb7coV%Zt7815TV|N01wx;$~tf)drSO5h0sre6MH-EKuTUAOQ znG-ubLYFvh`UL0bm5Hm5J-j7oYM_tX)Cq1|b5W{J3gZC;Q)5@)c=vX!# zJv>zDQ|VS}%8hf}B*RT^QL%D?n7sQDC$U);g8Z2Lg$wIA(||hCF&LpBL}HZT$X;hJ z7jmhymGoQ9_GUIViPc_OEEGaS7BLgEF(wLu!oGtof76wgpfD71b6Q-f~$LwNB)C?oZC_7 zj4as&uWi(W=%;z{qWGm*F&UxQ*o1frc#zcWBvzJ#zCgr?%eB9Lr86mfyv~E|9d5j4 zKliQy9O^#Utr$Df3zNr>pMO{S(PM-h{bFG#G{=RDBL!^&(uc(<#Z887)^*N#jrkv) zF`WDYt~Nu;U7K$+n6oj&+hA@)3QvHOr?YvEc6W3!I-GeJ(Fy)8YN>yuqX0Vwn|@JI zigRWy7{pIKo^mQbQ`)d+%@A_S$~QcnAD0}Iv@l#Thy$6AEypPAgvYK0ULs}yV{5aY+el2FI=zM>X_5nhzo4Z-Ub(eSXlIH9;P};8a*K_ZOwE{`50NaVi2* z!rObAkNWji$ei^)K4Ct=3F~gSVTurm)ma1$dn#{15sI8I>f{2G4@MBSH9oDN4L~Nj z!i+JP-`@tT-Myz*5c1|5Z#ENhzG*R5uIN+~x-UxGO!D&)LgCD$vlHL`S2dH(wk~o` z+T{?enzJxj29g zBYWw;1vU(JESbUb+Rj))YE$FlQrX8dB6M<~(p+`m{Ph!T6DIgd*~u}tcrmDhSJ95V zvX{?~iEtnVqQa9BqJS8%C;Uu`PRN+-mq!*A-#XbVz$<9N2=+l5`~dU6&G8O_nh3nZ z6?UpCZB&rlusmFAO!tpM7iO8rNs>A%CXJBPnX^*o?XF)z&cD$F$^Xr!#ZegGiETobglnL%C18D0X=Rwh0ni0r9)w}Thvnda zV2YicDhP|`#+t|pVM+Ad#q3A)5+9Cf5RN~-{V~BN%m`RHV@!mj`Jx=nX%G7~&Vfa9 z%O#MsfL&tI+*;UwhBEE>VmCs;ISS`0IV`(b4$C9-(uW_@!6(xESq{s8B8MddpzZzF zgZ6x1Gv4ojfv>=Mld9VbWAC0zqG_j}QF%b&=;8k%mj*x;P;WR_2`&-5A2 z=BIMP!lu*b4Qk1>D8sU+a>K&Fn1bChV>oq80TQ#dU#bw}q3XIS6nwy^uogMNF2G`$ zS=_LeL4EN7=$EBnrlAGxB!ZS)>J?(}-DYnU^T5Pd3X<0%yDxDjsrH&l{rSeq7-KcOW=;s9%n| zGB`byb`khjNF)3!pyHBr(mV|}SOfa4aWd;QQV{XflrYmY@f36Mjl{(;Wmn?Q%y`M< zS3HY7kNwkE+JeOldxtj0-l>B)O*}2k3(r1qkoV#texOQVCLxf`vV7Yx%`u)0WS})h zBNQHLp%0`UZUPIG;)#A&=Yxd2OKZQVryqZN-v_YSo$L%L`rmKgMW$}aTCh2ukjX2= zc4A5Vll{P7n5SA`i5EQ-pWA77ZSAw-W_=z)cG z)IyE&xS|#^_}`eq#kNZ($nts=yno-@^z28-Vf*<#uaKGK$hS3n=>Puik*Qm<=jYA7 zdHF~ky6O8*I_fZ%yg|&L8fn4gif`#VA5>Y&Of&8@CEmRW10opK??Y%4HZ&%3DHm36 z5uRum+L@()G=M_o1XRTh2K15=1$QAvxvdHST|+7ud40%J4fKn%XNd8|GbG@JGqmf) zm{pMl=GfaJwu)BC1jeXJZOBj+?b8-s?0J@W)z_1ooANJ`)l#NfvW2@-R@E4KI0eK410U=hnt zo4E!~^`fNbX*am>Sz?=KNYFXb6HhEcp#6=N=YIroW~W(r+4C8l}8J&EM(J26#z$LT`Nq_#og-(&(J3$!XQA0;qu>FUJnU%Xg2u zL@RlDnxp(9Ufw+1FLv|tJe12$)z>qh4Lm$@87mcGjpuiHk)OREo}U4JcB!L$BP%BX zh^89H%b!%sc`Ew<8M`!_4)zS8Vz_`S8W1lreK?#f98!^>+4>Ce_6eKlJ=TA|J>#Bz zaEA5zOp>f1_x>M0=ag=h^qmunK7=3lYIFvml(yI=kTdrT$;CG z|Nadd_6xP@QC?|j=fhditCprtHHLKT&6&C~J!6Vbm@b%+-==Tf3d~8q86TlFChKuz z3!OhXZuJxR21HWRD|mn$}tnKafk9>-XZZ%0}-H>4Jp1B6K!Zh3H73 zu@)7uHj_Db%qqGS(YT9TAiW>dkaHsaT-@NN#X3|adtxloOfYLBEG?|J9C$pvO=O;&8{bU2U2>QWc!QPS!f5ag5AlqaFsk%5IgUk=vt;@Al zqO+{iJ`V~&XACJuHc+HzWCP5m@=Lk_;)@(yI+n-VS54zBbzYNyfPPJGB%hJ=^r|)V z1G@bm^rT&Rv|$TbO5XkjHNjZ@n&a8E!OLgfl~B8}<~G!;64;GkCfcH2T(re^*?mWCLwE22|B(?@YefEU3@ckVN5d|n}p;bh`{Ijqt4v6mJS^%``e%2+r0M7Vrwq( zIlBF`wm+^~cN&)QCQE7B=ClPfh&gKqJHQyhooOMUvlEOz1p%j)=q%&ubJl}ioSbz83>+iUCLDJ;SQyog-PrR(2*psqI(~Orj*z=qz}7KE;I^TCcMV)k>Kd zcA(sldIVpFBn4?Av#@cx$ScjHBMw?73JGN`WbDIt{nUKoANL_BvTAcm%9<&)g>#NY z)kOVG@ICV5zTiq~f3o=d(`B8$ixw`~V+!7K;O0543HRVxJ)ncU8OuhjMuXhk(NPni zn2k8{S=tiS!r_QYW-d-~8X%>d)WTio3erfCh&_UVv_#xno$g0smal}K66#M((<9so zQQlwwoN39n>9Bc__3`~a3E);gNyZ<4j>r{c?R)h67c`?z?4}X%UyzUwo-!qD-j$U8 zMwU``&$IU@oPYafFH+&-li(Bg(_tY#`P;#~rXO|`{ryx8y+9wOzVsZ)q|Io-VyK;Y zSX)I{CYdFV4WVBVsG(42z7&G|p!&J#lcPM7_b0vbZEhX1GhRV8r!u8&`rB)=fh*P$ zNS`zHg}Q!BS{6jH0(bvKCf$6>;$m)SD zma}6Aj^sm(BKe`!M-Eg|TTF7+LQkr65Sw)3u%qB!(K&oi!RPd;_K(Sy-T`vQ+xFhW zyOEJByVmjvNwEHz=Fv|;-%h{%admkanN3n^3s0>eo-h2b$~thtSXbY%d7nK`Os0j> z<)Te5hna6RCZ17Vdy$qNq5svAmpwT#Flt$XPvRNcbiuxJYGSe%soc22dEz|mFIMbb zxw=Z2erK^JW=cFVq_A{LD)0dc78W&MkOCobC*aN^@d)rx6H|f$AC`Ou+YZtOX*1K# z?xTbBOv|c?)03tySvOfS4%LCiPeHa*{{b>?lQK{zc~=lk!?alMy0$M#{0G?wzxr=W z4Yk#1O^OXHnADoK@0sToiWy1iH$+9bQ|sqvtesW#r=dqcdT(wi>eh$Mo|I=Mruijr z6dwKb{y1wHW~Xh5+y%>vo`pnTY>*WVz6bO1tGLmq#tJnw0vSCVr?S#d z*AilZ*fly>7hGYwf{RQ`xbn@8x5!8+|=lIkmKrjsGfpf6JEl%l<_VgY*8{ zzpw%WwTwgqfGZ1s;f4lgfI#A97@p?y-?_tmat#C&Sy*P^pi+zo&}HNNehA1S!J0eP z7i$S)+W_e$tbYt;&OsCYY+(Uz>a&2KWFI>f-nAy(<9?C&6H zaZ`&_HF5UT@rf#MNm^gy)=_iQ%IY zo_Y1z5N10-&BgRXwTg?_IQ|c+$#SiYL=&h>4yesp9}xIe9Dh*LB=U$#y+X4kQt5ar z@X6t=&JFUnq9Wd^xKy(6x5_MLZ#DD{se%DqJstVOvgu#xg28eGGp!>JsHZNRms$Ms z*Z<=N(C(vVM~hLjf?VFdj$X9yrhRH#(E&$W@wv_^zx=RzTrEOC9f>0m0-l0i`s1-9 zwQ~}lJ$Cw8o!#d40oegl?L<=X*@xiO_{L7}`&zJ`+`rR)q2vu^_mkD{64Ql$(j?ke z4Dy7ee_BZ*gaK%avJ($4`hS&`19m3rtm{B1gv!^)lgG2D8J${Quv;-)DE6&}iG07+TsX^aBVM0-r*mlzGV{TZihD5+MdDNF_}-reXw_N4((LS_sJsbq3Za z%s2r2msagntlC=(>bms}*8={o>ajv+1ckPv&$L zwUK4-c3z^3X>TFzrHe0-#P$hGUwJz3&#kjN_Rx=qHMza?n+J$@r*=+8YN4sKr8D)y z8HnngNa0k7xATBYEJ?E(vv{ZGG0bFC^6Kw*d8q-iKz$DpM?53QFJBtFk)!W{2$&=> zZ@~l!3o2V}nU|SQX`X1+5l<8z?&61X?&!cOjEk;!xETBZHS!&4s&2s-O=bQ2-;uHS z6MJ)Ks3rdH)vbNawy!@P=pSje4$?Z(Y@mSa0ab^o?;qq<62@pnKBB`r4io8=;9Uv6 ztp?k+_wL*AUU6@$aow$Pw}*A+Qq9h<&#tc~6C06ruFL%TsSRZVRV4G*ZT%fxd{MCc ztW{~x)ta6*T_i7&8w!a#u?}{&(KGKBeNLOtD69(wOKi>2)I+bBJDr!UOU;<0rG_YQ zxV?*Hb7ab%j`3;D4S5${p8=u46CT1=kT<~oJ~w(iP{6>5fqfd$cxAMJmf7^lHo(+N zek!J=8l8AT`on8v=`+Ra9$(%s+#DGfIo3HMA}mzsM@prJ9(`g% z1Nf#;7e)E@gdz8ZJ5wGs2hB=a4S3sjAb+KPnRl1leM!CvAts}p%Aa@8E zx)`a&;oime2IIHiY=P+%fTG3FdZ(Z@uO6*@dCipTsMY(9mxEnkr ziMOYXTl8Yh9jJjuj?*@&S1rX=YF>Avk=!fj$->ufp;lu#9j2SWd7#NX{=%)}(r!;2 z9=_Ns`_f8eYt=b@Hk_!hgj}XU8h{3UDoQ?QK2Fd*T$LtSt@dqW?ZoK>##BGoMWmq$4a{R?=FC5N8Jc{~qMm5{DQ~`R)@99gg1e45w z(jUn|3W+@X)H_*zYi^&--pyX9ZB#ExLM+2b&POQ|)D^GNmvHGGGKCx(deEHp&M5>< z&AxpNq8MrsKVb&F2juz%ZUO4<#ME$fin~X>y&>)KjI-}Nu()&iJ4AbS?`xU81BnL1 zyse2f%Z_F{8~9^eifN7CQ{%vdo5mRolNYR@AWg(}DXEOwf=CHmYD7$?CfkZlJ{g&&BRN*0f{Zv&Yu2Klbdr9Zkpf#*XoJC&f1; z+`T&M^pWygL(^vaL-}mgG$W0XnNNAm-B^0qzgC2@_mH?|L`iiC7061AGzEy)G$7R!({&_(d^AbN&qE0vgjv4Hj@c94EgGJxop zUVFXLqwo1KWQ=#bjaCiTYt76$0Ap9KJx`lBra}tTrl1XV+7zPTIWR>keWVREY8b*v zY@tM+)JC>l*S{CTJdTAFdzi^-qTbFZ>g z?ZLM#T6p`^H7m!s>m%-bdj95Tx5u6XA5}5U-br*FVynVZI5Kj8>1k1-HSeqOV7 zo!dp7JLsjI8o1;@huDONOCty_7)6-LhtrBOu|v|rT*{;rbzF-e4kW#8o=fh3=U@vN zci<5HmaeA%9e06DdB%TrM)uYq3CxQ6IJ>KKgt&L!`v-cq+tfv0`u_AxQXzvw{|!^- zvDL{>0fE>fci8itSd*}w$dN2+sJM#sgXYJz21$}TD@jKs^p37d%HmR?$>|4paNt+0 z@8n!T;(hAy?aH##!zkBkj^RA83v$~J3yKWnTAQHpgDi#AjmQ`d!3w*QLz;v7Ky<2X zC)6?+$6k_7+P@)*XBZdpssb9iHXk%!2+CGhPVQv;Iw`C zBM>sV=G<;7l=@g63Z+R*9O{)k6>A&*E3%geMR?C5D3htE8hdA!gxRR3Nt?@t1pDw@ z34Sa!0k<2WmYgRR=#?vKDO(2%Ng?c17SwhRt1NQN?zJnKtg2hCT~jfGvTQ7MYS6J- zZ+elw{o56n7*0i(%*P4C+cFCVs}h3g0Hr`(wfKl_RI&HN;#z_*x++p6gboa42(5HL z1j(W7dZ|w|3jLS*gknav)=c9`Gfch}kg_w;AWX_;31~Qh`&xEeHlbOYxZuOzp8e39 z*sY6&ke_0NVA#cr`iVhFWY4SwsZWnoCpruLj`y=LJ~Z%8;Zd^2?yU3!&=O2xk=7*J zGqf1=@OC2MI0n)VO*tP($-p2q@d3ulWjQyQ83c?l2=st^AW&-JXKxl}^Rw0RlF{;9 IAz7pOKk|+GjsO4v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe23eeb9c93a377d0f4ab003f1f77b555d19b1d1 GIT binary patch literal 316584 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 #OJ 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ȇ@(g~$ z{J^}TDmn7Yi&1&1PI_S!gsw8|7)G|KC@bTFvK$4@5=Q+bp11&(Qf^sxjXky;RtB2& zylbx|hhkd{3n&*VyTGLWE;MT@;KPjW>~P z3sdVOHWmrT!W8E-F;JlmB^=sE!m)ukT z5>CYRg(v;f-)5mXYz)zp(j{a-P=%m!FmIBN&5dNaylYGW zdja*PsCQs{2Nkjs@ta&OKD1uMxmzU9x=M4<164zC=?tFwM03u<%Y%pA+Pvh`3H5o` z1|d}&S79xit8MugysPqe|81$MM~C-Zc|MsBP?R=NmFiTe7OFmwU0J;>$Mx@{D_^Sj zf@LpjkqBpl(GXBy)O8oHSqWo_`h#J~+O)3Gn6Z%Hv~%zAU#@=N<=FY-(c=#%4*Mi4 z{`|WBG&GiJF}La7s+M2N)VSXs9d4Y`GbnLa;^3pBThIC|%j7QEtlI~6ZkFsUCYGua zU_M8Z0`|8Mk|o%W8fc#i$qKck(DXt=39f0QtwOGVrMU>V$jN%9)D-fz6pB5;(Hg!Y z{WKxZi!CSA1d&KX?Qs>9;7}AvU=Yds5?)Sq`kpCPe^q=h+;ULe>x6HH{sF!Rp_A1} z$$S9^q0W#r;0}Xb^ACWoWPu^xekEG^^zHojuMGEaF0~6~nt^{(%tLj#cmtZ3jXby^~C`RvS$6wCTlo&gcguB%KQ*V4=HN}hZgi|xwF(`TUfm&-hL%p`1I}cOTKH$BM#lUx=C10|)M;(fqs<9Y9f=;rLt`(Uf2*QT=@9rQB8(riEfI5% zC$$??t$bjGsHx8$|07iej=eFjr4gr0>jM|GBd&ykAbAYK^94AT>5%g(Pl%3Ra8z`O z;fYRc`GWt!wh&v`%E^P;3=gHQtq& zh3K}N@^5-Jo@a~xv2R!k&2Z{$_BslrYIBJd*6br8WN`}a;yB=B`b&6_}6iizXV! ziS1fEh%QY5463K7qoa$h5Qs~!V3AVY2C=yDNJnEpyEUU>{-YL7VTk#5_PurFhY;ps5$5iM|sn4 zZ1CeKtWW19fge>#zZC0&!c<)ZkKZg13KntqmjVv`?(yx2LPy=Z$D$A>+I}}NyIs5N z#36IrwVPY{9E*SOkj0-n$Cp2Rz?Yw6VPmgOn|5{FIQn}m?p=t!;LmUrJah^@7v0}0 zu2!o5M92BUZjjdyX4q(tF57=%@+7HHBPxt+?4d(+tMQX!=c~Z3uENOi1;bF{kWw-1 z`Hz&WP?%|K=_!4uGnP^Rz`EF}TD_?+N}XeIzu6i18($$9nE5_2@cm3MP)6?m(t5(W zmdUk&Wkp@X;Mb90c!GDC{z^=)W=Z@9Dzu)Q|CHNf{qq<9Jd+OgK!yc-Aj73dCMDDC zPzrh5LeeB-bYitJGf@{VWKy<=8Ek_m=~_9#;l{DSlWj2&*&am)Z18y2hul`Se)I9C z5q@G99@MkTcD>h2H=F}6JB9vH)JA6R^(<4sT~sygHPU8YRFo%4P)a743fq}XwgQ~U z=jeD$emo1GkdcirD?;P-x+Fe~q2j`%5YuvDI)<{RNK81qyHuxX9j~v1J zE-v0SID+-n?7{kcxusw|$rTK4C#wFU9YHNF;Lh@%4|iT2e~B^3)=p#4{vwOThDE3v z;x%}I#qNOT_X-tcnh7}07N7>bibsgV+|YhSW3$hdX+7Er6=u53_Uc?6@LhJe%XDO< z(Gxaub>+j?usOP2MZjn4HaWH5;lQ&%cSpCQi1u?>Nhdt{?H9q&&2PWJfoCP_2xTjk z9j#t_okB#AUaRLK%$3>We^7DiB(vEZ_wUr3U?r{(*oX8IYmt9}V-tOzYQ0@-tX5VJ zRvR!SN}N7Ye$R5e#1#prsZvkT^VK$Ma4Lo-E^S1S(Mth!=Sv%|g)eR7vi0tTFKv`` zalRZuhC6@h+J^ZPLo=s)8{pL)&~3+B&feCehVz$TY9neZk2{?&)C8$ zY~9Fz-ZfzxayLLq0`6qB%SjxV6Ru=i(nc$@^S1muipbKMYFv!E0)7?8B} zkw;5*-s{<;bmYCCFg1L5CASW5MI-0z@MS+=bhYP_WIHf%vr`tVp;zR9u9?pr&u7wk<^=nGv)?0y$WjD6o zQpq{1`+g;_zuY=hFv9#=avG z*8Nc-W*IwP=5NB+Ek#)#{~ZTSxCan^@l(iP6gAE;22gH$U0aqJH{Qab zvtD`s-Td%6|@W0CqV z!nLz<4zFRA+2mW_vq`Md2Zv^{>dyykVl^+GXSFu3=bvA`#;<)KPu}{Dny2H12ob1v zJmL=L&B`)wpMB3etYYQF8Zq;uaoYZC&U}-RPje|1krQKHF7=o|K@(QgWYN+sgjPA2 zjG#En4T+^Y4grHLn+o+KZ#2Z@`;?VtFLh1+K{!i6JyslSM(H2q2QjL;)?d}@ntYuE z-P_zT_J`|S)$4qK99D%&i8W*9VOVf>l%RpJabjS2%U03VwQM%j02Wu_)^SLo&1XQ- zox+Iqos&;4=)zWx7*nZk#X3U{73#*ax%!Zg=ZqUTI6<%Hzqq)Vy1(2l)6KA)RHujq z03`^28hI^^_FMyX3Lg-j+Ms+xkD7_i<4&xKfpwb_9^q|JS$UV@0r7_yVfbrc_pXu{ z`U6A3JS`=J4~SWLBDO_xt@42t8x9ZG2QNAtA5h$d|EwCkBf_0#RHe$bv@;3yTWAj9 z4TdMApm<2vQIT^+Zg71^p*3l)G%fBN>Bbh86sH-~ko`~4=4hKM&I)2)wKQ4j#fr1G z6Aq&`-jE_R>{P(xD0(X8Kub4#Ymc6yJ!xc#Cq;Ktv`6&RsaJBIzAxZ&02eQ1i1y?> z1sobaNxqwGUB=5fe-F;|YxrFFuk_1aT17I=5p!o-MIniAXH;%eqwB0lCWX0jdA_=| zM?`l1IwB;mUq=qaSVtoIVh_exHp&Tc>gkm$$*FVON_W7z%{pdxtPA6T%?jDYlE=Bn3zNZ0;-B2aedrJttx_Heu3V4!)89NSo0xa1EzN$!aQ%? z+|4ha=b!S$Z?bjn^MCnXtvs^HR(LUO&J3+Gb91k zTAZ z!e$Iho9E8fR_2TOm9zZfF2-(t&%A~pqhk7iVN=TJ!>&CU@x_8|-_;DNmfEUY|302K zt?73bZ@g5ifmi$bt)d2pgA~NFv~OUQAvlQgva*Z^7Nya{)6GZ)N?@oU9}tyqakYqk zL-i_Jy{>z9vL@Hhv+(n)W}bh**e3qfDgNh$UYYIKo~5f2+YM=?ES@@Lt>MSr))|wY z?Em10YM!O|&tL5|XR=<+Ms^r8m$h#*vQ_H*g%e-&A}a8**(aca4msFc<)vvT2dc$e zS+&3AlV9qVk|Si=q|%wku4R5FKVrVQZw0*-Ic7}6bpF$~!*4cDd}~@t+~nv<6QgT| z)S8-*Fx9Z`MIXbu|FQDfr8>#H}+1Fo`MztHNH8<%O}JtAt^-3;2y zjzq2-IK{8X_Dsupv0F%X4VPU;n)q+B3~NoZB19_42q$%NjQdr`h*)ved- zMAv*H(7XOHpN+lQ6C-VpR6{9>qQpNeKw{qpBt}qmD!aHjU+WX!uU^cbnG9-t)f^ zhqtrVtlE30eh5rGI4%Cz;vUK+bF4AtMcMk{O@ga82}<2P;lYQ$4Ett7yM^Pz8;m)b zrT3-biJot}M2JahA>~J~k8Dz5=6QN}%EN`-8C|#nfIR^hbV7^TC$wF|@Xc|P&3Wmw zPQ|lz{EJJ!^Q}}N;Meo)9b7+8#`W{q1(9*PhKI%k)oUC&Z+?rYi+L-TT~ErN{B|9i z_vyQoqU#-tv7rs9&>=Iel2dCkrr`jywld;3`Uq6DfHP zgI9$@2=??eCaSBh@~%}w-+r@k>6SiH;8-($oaY#ph-qO>Aa2IKQrsL8Y^2hpQ8+ca z7AeOe>dGQyN>wghQh3pF1dSMC8Kd_%ra&W(5E^eJD!Y&d#kxVD|B4-sN9*S6;zaut zk~=$G8x7x>t-boxWwisIq6>$-6aEtg+GiOOWqTMJ;rVzFvj0fHYa?X8Bp;5D{bvM+ z?-T3Tg?wp}&O!WdynaQg9S^jcIL~-W!Lc7-lmM_uDC1!L6d3gb%hY?kec7*57}7m* z#&(z%rBLZj9rJpB{Vb-4Wa#Ou+ROsM0-*^>wCWTvMAM)}98hv)5|pAQJMdFMKEwm# zb1fRRGrd7TAQlHzR+bSwd5{v6$Yg4x@{}d~c{OWFfF%pzyFvLh%^lkzGfIozaevg& zu!ZKWMUh5hwS2B2gH}iZhF2M$HRyBvhFhjG>go%#JeNf;Pe@qSm3k;q>tI^ip;{#@ zpPOo$vwLTE?wsA5kbg5EIeCEjIR>D^0AhiY?w!lBw}j7j2y~`B_Sq`^g(9d{A3k}) zR~JRGgKgwTmds43n9oc*l>EH5cq7fFFp4FU6Lmd=OvowGIhdbBmCYR3$u}?CC+p7T z!>KZj4W7y(9Pk)ZX9ql@GIj-^AASZh05Fp{{+BJCyfIq#J z6aBM|zsmNwaEP_xXrE)0YcChI)Scm?mb!x;$Gn&h8*E9;M8C^9J04RE=%iQ&MR1+* z*lW0pafx_aD%EcJ|SGMD_7?cjaU*)wKXmOFid3&86<~2(}g@H@P#D7>fyA zD0y7dnH#F~6sPQf40+i>B3mM+Ny14yBs_ujDiR*YxD%X=cG*5fl`iG_NSFa19PljT zw*2;D*~<2*N{*Po)?jTh@D0JGc8U+fi0D716f2LPAMzIL0v;~>A95+%vh%#HrnB5t z%2{+4o#Jl!O@(5^kOT^0-y?2|DM?|-O?SXa*%KVfUc9ep>k!sHK^3~MqHWYP7<@k1 ze<456y!1=}9oSqHoQBD=M!6ez(9=)3u$-OtsnxiC@7_5_-fw=4<5dXW%CE+|4SV&< zIWi;kI7?((EkJ4THg9Xvpfo=aICU%=X6dgsSL?KBSc&j<#;)F7UHFE+%{CJ zk+(!Y=y}fxAIF0m0wYot+)>=4I#mA4Sk+hLCj3Kh?Yd2v(5>6pu}VqXFGY9n;$}}o zu>X`pGkv=|nWN?r%GSnb^dARlc$LyOBKC?l4zh>p@HhGh?5Pqj{-{mJ? z82)BCey~IdgVwd%3ztLRV@`v>*v??A@BxW(?=Le#3wwT_&6F(e2^N0^7DHczvXjK( z@AW0V{POv}qSy?{l5D2MvR_DCoKAYmP!K?EgUlvoKl%cfE}r5bO>Pn6-uc!&~RW8N)Ovp34}lVaICzos9Qw?x}G@jyH%YKT`{ z-l9EU?;_?iNjFaTqP(2xf7g)+?AqvrzlWBKvgeKPfABd zYA>aLIWKVivC#)+#`=_G3#z@nDz#tDZ1b9bSn&;qF78uxT*6RqUw+cPTv37v)~rS9 z`gRZiZJOp(P6l;iEN|<-q&hJLiojs#wr_yRmucumt{a6%`j~D#&ocY~Owj6~;_|k= zgiD~e&cj2WUb#mP^WVyMmSFuisKbIX*}gn$V93NMM@5x8fA0rJ)+0975@_BK?_A;X zA!>)o{A?UFQ2^WBUK&QJy>|TWi~-$an^uo*<29?*+05jXqXrGj9?&S$^1JMZMMkFQ zWJLJYDa(Hi9Ue7h!IY@{8ZPK-tz|QG3B49m5u31c_zl`TsHzh`^pjcjw#oG?*R5Ey zORcXgX77Cdj0@{r8D76T#Z1qC8doH0n;?~e?4it)KLg53mr*#0Q;WcmZ8FpAg4BoGQgYVxzKZ)MI!?&I7vA@{Mm)A2>AB_^(Z z-cO+Y5*=h4ag`V?%gDB(0|;yo1i(kje(_NMS6)Lk&QgVMQifO-vL5u<5sTO6^QCN{ zxwMD5)K&Fuwc0h{*o=2Cb^@=fyy6lMkb?|F%t_Ehu?(noGfe_z~2qqu`7^=63V zA9FtTJ9d-3&siJJrruyT_;|}<)^75}k!<9p_xOhyS4NM%!k+N{FAlOU_a3v@lgIh$ z$G`9`huN6!TQg?v@73qP%#1Bv4cWGPJrD^~0%-9P_4j?K)Tj@l8wxpGD9j)Z+|;{v zgl+kjr}C%g@SbMs_iRJ*S8GmWogcz?@*d*0(;s~1i4%BprIfPLGGO)1)V^QsS5}H% zm@XZ?xI$3gCYLQ9mQ>&`)xo=_*SX=oI9?b)2jm`A#EPQt{qmmmmV*iyXTAQ+L zU&WNp*`sQ7X}X~QAh6;R29PX=rt}d*TWIwb%(#V50&JgnicfO*C*l(EHp@6Gm`b zz-K5A@e}I|@9^T$H(F;=W!&nE(s>k+aKjY=5q|I|FQE@2C9Zgsj?!0jO8S-E{5`b> z*46Y!D@L9Y>q@P`-^1eLX1?;;HNKML@s}-(v6R#@&}z-Zy5je-u5L}Cb;XYJJvimg2ExTh%Sw*ig4- zLX(`EVZfnld38l_a;P8=5B}tPS%(p;qdwljaqSN;ji#*Rw@;qnUvFf+ zqi1&+w*@02ye5~&Vh1Oj__Ien#Fm?TnRM*2IrllNd+Q?0W-qZz=DjKGW$NsOQZes^R8Fd1utB<{Ow~ykS)W zRO1YSL0~j0?(xzj&qJ_o7bG!IW{x{QYibV?))Vrn8S@|3q=+J-oVW$B{1#t#L1p{& zy_WPHu%v6REhCN{i(TBM-IQ+K=XF={vgT>l;p#OOvYW9lx844Yv5R~~#pTR(FCIE* zJs~18y)6rAIX0rh$mUH)bllBKEnUX*)^7Xx%`%m&$9D2ZE99gZ*McP_uvVC_;4*nU zm|=z`c>(Xlc6U=YD;3N?s4dmO=EbO;az(9c4$GtC1m@egU2v036S!#@eci@QSLN&9 ze}}7N8(YFMnZWPXQ{Xl7%)8D#fQUHQFz>ADVcr&jd!Nq zP<|XYA?gP!0yWWvEAb~;Syr5-f69*XRZrNKvx<7}iuoqnFh^;|tmZj<9X79^qkQ$> z*pRc&v2e@Hd#t$ms_ZeZmia11-2$YOltB#ATk zGll>7l=U{aEM*a_k~#<0*#=8}zL~fG402?*MCm$;FSzg;D-f*rdOwUPwth zKbc*gawaM1%#;c|-stxke;>x*yo(oD@^j9{e##dOzV^YAPvR0jS@^-VL?!X*bki8Y zgo>tt7>_}`E~X;3B3MAIFbyon<8of)u+?f8wt@9DpJaDfPrlL46AU7bS9E!ZZ>tC~ z+0+9ofWxrw#V(hb^WYrIsGYQ2#{qvbkv-t#!_WV^4wA$rA@P0wR_5fXD~#V zfxEeALfMp*%0}CcpGau2bx9|i7P-YaU`^x9KqPI$Zegsk^!KV?bh^P2GPY_qaJ49&e1 z>srdQv~f27>Yin&;CviRUW@q6HUtQ9(de}q&u^HPzOoC<%m&sAi7D@DYtsEU+kN(1%7DzHvEPL z(c?RKANxj3w92hm(H(?2QU&NeMXZsQBkd=?f>xlWJQ-0ifqlw;`3pNdgDLgFUlx_^ zVjV`Vj$-WNo%{`B6z^;({t|k9xb1L-PP9v+6L7HTF8&F95vUEKe zNNoFxqG@m{+6@)^P$(h6Co?E4Eh^B(M;~nI<5Q}~n!mT9Tzb)5V==1Qd?Ty-r#gqP zwKU{E@yS2L7EKEFx8ip|ijgXg;P@#12nt?i<{q|N@0E8?KlLB@i8WoE=bFQd?hQMM z(I2LQs`IC{OM)+=>TY;6j*h1|nvsb>8>1`Eu9WtqXz}LRmQ`$L@v|pa9gOUAWd7XT zjxIc+>(V}9t&TqA?fDlQ z5!C5)e>ZCcV+I*Ucn}fiPVqd*Ktlx6bn|OmZU#=UW-1Otn5OBN`!B#7aeRjcu=@)F z=~vrbMqyfMU+~WS=EMz=k?SY0FD!TN^F6z^vv$9z0eL&w=Xg!DinglVtc3XKE&2Kt ztm>tUtmcNi_2%A02Y(Y)R8gv1n2JVIqyobk@V&fJ@vQ>>-FeVtiqz&=IcTzTP}J{e zMZN?oen}GJB_CR%_oV1V0-q;6SI)p>^)AojldgWwrZB@pHu%fZyoX`v-r-Zv(%v(C z?+}e`DR!05HD{`k6(6!a%zqqerJT>(&X0XRd|me?{K3JS+i~)U{vhcqes+u6gZ1X? zv9oT*dFTx4;y0wJ^u}Q)$d;&uJL$Fq5SS!juCz6}3cCE<0Fve)LG-3G{1&gqw|%sP zxg9vg-rl$b;yUi(EuO=l&N;#&ZuOkee)_X5C)k^--i>R~JpSF!*(}zW?_0#&+4wuG z_7|+{`RjbfU)=m9zVh3$Y|f(2u-?D0(Kq<~v7b%mYu9t@g00N|0Bbnq;{pA4jOL%5 z;io?aFBe&FnZ7Vpfb8QC8Rdh6h^x1b3IB{JC`v+zsTS!#RKia0Q`fJ)dnsiR^In?8 zAM=Zb3H&SG{mvIG!(w5Bj=wmoF1ony^aJL`iYKu*#m@`J^9S)C}ND->h8$Y#oX<5`#5T4QzF+0}!Rsfz!j5 zrGfX|DQgx-KD-n*khNrIfBT6K#v?Qj`RJ<*kHj$6|LE*h*M_v5_w(AQk9M_bQ}_5^ zrixFn3~qhJ|KhO^eqB+ZR+!gHaE|f$Y5~ zA6ut1ZP;$b!IybZ9v(fNSKjBe4H9av5FRVfzB&s(mQDKm7<$%GI2gJnyYEBb`NYN8t+aX@X(4CO94Pv zk-&&6(@0$mu-!NIt)eFjm5O4hmy%hDdAC{IVR1qJu2_#wFJ3NMKa$Y^Hq`cDw#>d^K;#AHLvo zF0j@^Ph^ZbGcx)3go&qzgEqQ%h4&hemr#x>VMJlbGW(^6|#i_L?_zn%S=H@n0J z-T#%vp6|XknRhpvRek%FQ#+@fpEC8z=;-My+p2oAna3t=rwlYf4Bh_J2SDi?ar<+J zxcw>NyX{_G%ba~6iLVKc?8&BE~62%XSvQ3=OwPKn=Tf$n>8o3n8HIdB;4Dx&=& z-QW4`T`oKD%w@y!@jJnnuqyd*6EfUv{jOx~bgK!!rMq^}WFHs2Urc(x9OHY%=ZEn3O&fx?6E zF0_p}i3m09A7HdyU67@(ech?bwYFfvi3O_)3f;>yI9FiBO45z**v}1vq0nB6Ri9)taSva4cA~!LHED|2vv^$ zYqOoKI;+IC!&x#EDM>hpy(nqfiv_0Wj$_|RGxgC`rM#g~e^@q-_o^sxq1UHV(ibSrTJSSgHOyV;k@9n=-^dCW>`qZTIqn-~PZq+B@R>MD@Mt zf47-EFn&gx&RIPZX1CQ29Q*lAZ+_~HfOmr`)NfFEI3LM>tay`eJARP0yIyf2-}9!a z8EpqgGuP5Ye>t;E0YRc8zYFbbdzUti(o*nheTpoqc9t(&e1+ zc5%&HML`e+i9*4(v?;i;ICuz4(qIv!gVQK5BHnKf3=~6U#YBsWCj_~i#Y`+i<%DeBjlTNpS^+Y;7Ieo`)-N9|h zH!ixs0--lg1kXj)&sRp*V`0UxU z7DP=6|K^(sGpDAf&UJw+W*77O@B$0o(|XP~3)HHO5@$uuIG@yb^sW&dBL=r_8WFIr z(f+7b&EwjI=Uh%2cqY4b=Ab4ZZa9ehK@b-#`Kbi?C?uElh1~?2Xgz|TzC@mge`7iN z)N=!Y-g+-8h8aA^symay7O?FrCXHpyQaeQLSbk%Av%UOs_Z?9qhcPxVt=Hn7eCW_q zOj*eM*dW$4^Gd}57g^vH&i@?PIwY$4DjT_~w!1rMjJ~iP4)P;0KV278s=9$N+B&R^pb@YKX`X%g=_n(1bovZ=@|`_`?ecI}i*CO5$m|fB+QUMtB6# z5mz5nY*#J3&y-dZM<#!AZTp%pLYpW`MEiliTkFPV_eRe9ki3N^9iH47acpl~?@S-> z2FJ#X`73F4=%||2Ta_)}h?O`t<+G2E3{wvBRZzyXcf6{GnA{#jQTj!0^&aJ}c{m02 z#ovpb8fLH6_5O-kobP-7&~TROu7m9YozN^%HqCNem`)N-Y!`4$M~q-^VfY#(pOMK^ z7(N?YA(>7RPVAL<7U&)e_7-kG2gz#rc=FpX!kM0?PqY^%gTynJy_b*Q3BH8Y%ZCfo zNw#0fdJ5Ai7h7{(AvOf}cbNevhS6zsMSYq-#-cvWA1Qrmr`(P?f4oI~H-FSceK&vX z_|!2*JLY^26!mF79|$vvvGaHfa6=eA^r=SN5WFtxJ0Gx%!swARN5+lhY?kh<4Nl*( z!|haxPjPtp4mLq|6BRh|5DQ0~-YAHHbC#}TjX(R8HQK}D+Gcd0wScij*|3n_#{hn z_W9;d``mfNYd-t2_h(o?XRQy_aWH0ZK#lTISo zr_j=}ZUQ`$GB<7GP3FJTc&$4CpD4lrqCHBsjuY?^fbVj7-`?J3IsaAkI}`9VLXaf> zEE}FlfFCS|n7(biv-n@g6jF4ptv_oNMXx|{bi98Xz|??RDD+2xbiEgzRimvMdt+6` ze3{``*5x2>-rTTCyQH4|>J4jljFoN0uKNVnFaCzfzf%7fy_Ei}_ym5o+^p%!UA8_a zI>J<*g}Hki-=9~jdzi}jxnd>DYaJ~u&B^F{ndP?OAo}(armiZ@b75COu&Dwt-H>Ar zgN(KbJQq*f1Z`8K$l6@i;Cj*}{`h|va?TH2y=UE( zkC@VJVc#!XhJ<%CbIVU`pYd~kxL%E}O$Ww5jJvpW&HW5N^ZC2s^OG`GRr9O0b!?Y? z8(MtOV&q%3TY5yd<6pB{tS;8wUh6|+CIp{Sv=C1Z#R?REY`2DF*$$jQ20ZVqEab6j zRkob@wGH+S@d`>0zwjIMFh9#0ifa)+G-*+umW8X=XOFO?Hd;<=uCPaN=!&x6^Tf09 z@<^iu)GsT2PXWTTgK6*OzHuvScbA06FybG+ZtHyS5*O&(u|1y39rH#Qhs z=4r#A$E^LWLeCH3vw8tfl1i~1-TnH+dRNVa%GBqH(1A&k^My>5$MUP z`JO}Rd}WxyU2HRJe$S^-zphDVCNj2xpTBjFAKHT0;k%h>gT@90jZaLT+DPBi{JHX# z_opoEm|L&D75vu!IJx2(e(K;k*7d3Ph~ufL$47W4?j1UGuaeAnvrdrgzA%M972<+C zR3R+35S>^{lpAaK6k*CQ*-D?r)!u37Q@z>P&Nu#qkRbme?`o446-t=+neuLb=Yg|$ zC`I5x{yMx?_LH8!B5ZythDZeibhxOts!mkx*x4P_fTq>h^y=+uL~P@K9)>@3FK(hV zh#Ove=GUXgQ3ElZce~)v_6DjHFY6W^K??d+K4~O04h2bDp*1Xy(9KF zYAe(=LCIZ|CT$)8C&et_unOFSAW1Cw@HJo%Db&L7*}8#F?MX39JPYuOov>jF;h&?s z?1U%3{UU_L9PNc-mU!mcZ5uIGawAFj65an}?mghDI-Wn!yXTyH?geasfH5{ez}}Ez zqc;_$NEZR6DWVh=QBY8^qS$K?QHi}96^$*%9&0SIYmBjLjESOf@AAHT&ZTIQ|1baY z-si!TesU z`Xvcw8O~RVF}2{c6<#jSuMI~M%yRi`#Y6~ZqS%itT`c()*xA*vE898CV{P7|CDV_M z>>ZSlg$+#w+b%8b?IdHki6MW52=}j%-UkvN(>(f&{z9`}3=NtWPMY-`(4HGg#OK#x zedN3+g?8%!Re1NA8{g5AXRGMlM|rbt~;;TKVs3V{BhT3=bldYcl$UC zGYrX!)YfY7nKZ|$&6W=4rV?HPoUC9{b>eX7Xl-K-%rN`-brrPe+k+&&f<$Xc?4fVz zS6Z4+@;m32#u5@+noDn1{ng0y0tvohBl|Iyrn z`?KoQnRUNl?xXy=m|&ZouI{q5~R(_Snau;;*TzaA?~&n9i4=ilaU zCF#V5yIz)xc3)IJ(u+pBjo9_Et~g&}{@x@V9ctfxh^SafKfE1v z_3e>8TnnoIh;BQulSC8oDw(xm1I@Tf%jxQ!2T0%}@?QI3&yf-5=8Sqv)Zju1PidB} zlxHcQAuT{Q`lhhj>d^Stv_gC_U(ky)er^aY$8Dr%z8uHM$+1{k8 zfB*2jx}X2>yt=>A^D6vn;8*wiAL>{4{~zjC(}6v&?r#nJYI^*`^J=-6Z<3@%o2)t9UH$)#J&emQ3$_E?X{_>Q$B3v?o1>^vcr~i)S%Uq#(4GHq20o zwMIJDV6z448@@tBmigb*BU7au&- zIVH1~azXj`b58rV>D<1b5lEfdk_;7i50~FbysMyoiH(RM#(fqDW#Y! zpQNo4^hGOsQ+rb7o`m=dOkd^t$*x7WSMGW++Ni!~#s1^Oreq)8@$?zpz7L)fYtu{f zwJ)%)_A7|(lQE$EW_4Hk+}^eU&y9R>XwHf)^zhHm>H0&*kwVREBG;B{Bye2AkBt%z z=S|rcV?S*9kQ2W^d~Sj}$)BJ{0~|l$L=k=lEd*xrkS$`&<3Y05ncG$SglyvGk|w0j zfv;zMpO{bI9c7OG;qmd|_-)`auycAWMTF^%Qxn!T)gQJle#*f(;vC#3Fsj_jtCP1s zQyUZt;Urqbsw62OhQdkhWD1EwcUJhJ95NJdl&PiwJA`1dcbEt_Hx$Zf771eSs-|c= za6i%sat$bdk=hLmCb*%L%`zx|mn-W-2T)YP4;>J@P$!kK)Jrp_&DuKg%&Wsz0*gP) zRU+3V!`8XAnO-Yl#cP_R37*Kg(;*EEU8ob^u*K}dchkuGia^ava*P%53ESRN=5y)= zM;Bm@r3XpWf@?S*w2>!GrUWibAP9BrFZe)`SnM)ocV5JeQPsjA6AwB6RzB%`^ zc0Cs^eVS{2=EC6di4PduiyGJBCDOi3xvgM_gZIyY@fqkLd zR@(sCI6}E(rcJhQ8Aw+0lM{y)IL5i%y&F3`B`SIXN8`D*#AM4sVz{t)Ui6B{_4M|k zQhIkY+3g!Pc%b8ePQ)zIvww83$E>p>haHS}{r1`R@qbO-1+P$h+btA!AIKoCsKIB37IHjVJIfOEZ zLjkdl?DJAhOdNM+k~QQ{?a!PQPBW*j!m7B13(|_3a?V`KTsV=?O{!_EXex1yq?gF& z@G#z4*Ye>c<1nGgnDe>MSM3a&3YY7o@1|%cRW;y4H*w~q6LUP8@$q;!(xmC|{C@FE zg0%PLCN*dduyuP1 z(rl&1^c@Y#yoO|dwfiEh(aZp#2pr9L0T5q1^ro5~e7fc-*O6>wM0p3Iw4^FJwHT8K zZdq(MR~sP6+Mj990A>uF;3Ov<-!krJ?#!a4KWA(2*R`4Q3t=82&Zaif>zj8Viv-$b zo?5_dKoI;++V(s8d}k24-5=vOQ0n$csoR|Jp|Vga|4vm>tDIN%t=uNd7uAnwxavkB zMhRlhCI`VsuqVaDL%R|82<;>8gXu8k60(5&B(!E-sQEB*Yxn9|OYqOg?TX)nUu609 zP@%?3zEnv8RN?0>kBDv-MI%aQ;}3$mJLSBW6QI~Y(^1CTj9 zAQp|ei&lYEJ=AXG_f>;ta*OzBKWl&iLIOqxs6L1;`ze_;sGa@ajsdI!s(eWARyf8w zHnj0-8IU-$8oCoWp~Ym?HM4%0rD zQUO5Jf-AscO12o{r2!Jj%nc)ysnn1r3Fs4(uEI{p>16GDHKOGtPHX}v1$Ll@*WpYd zEBwqMgl9e>(yC<&(Z`O{%bx>e=eZjqtW- z@92<?ZmhC~RY&a!CMFAAK`a`3EmqeY0kC+~yCz z)P?{r*^giW5WN;8c`Z@_5;K*UBh;C#qY?`8At7>7kRM2A60(Bc`$5|V0G)^91Z~!E zYA3X!cmF8*4TiZ2zvQ6sG>l~urda@h@l|Fz0+7-vsSYGtyv@mWrc7Q%A zCOJJ*e8*0;bC^mTv_A{S9An)lTwEG>k*>aFb(z#qK`n*%IRwGVMqz5$` zEO!xb@Y@8vlo_JUn9SJXX{d6$fd1Mov&PNmh#Ec#$5TqZl-k+DDaoOo*X&d6!KIg;{yc6n=grRPG3-y(Yb zmjR`_7Iz{I+9ghnNMGDn3vOgO9W($C3cHElq)}^$cEA)?O4$g_%;Rj8bWn=Y?xAW3 zP5BMl%R&atXgWP7eUZ15R^!D*FL>*ZW5WaX|Mf$pkR{q@^7hD;GftaR+M=|<| z{r}kiy+`rCawvu^L|g@8U}=T(nXNICRW)I;=A8&j*mb4#vJ=Uj`ZZ}}VA;&ay(I#2 zTVO%m!<|{S%U54bckt@iBuxWwaVqY@I*vUkFP+yxxgt6y7GcRTYsQ;Fbzydv+5wO_ zrc*drOBw8q%=nNJQoqjL!lVWLINOK}_tI$(N(K@n{#{xAfzCg#Bx_Z}cBYP88xinT z>V2xER|z>oVjesq!H4-ug}&mwLfk!WT92V_4uL(|)^FD=Zo$c1SWuTPBW>>{&h&Ii z@8cQe_NAjUsq@PLVp>>8pYH?OI0zN%NqRLV-(a9^Q^O4i8u)A?vX}wBPKp0Zx|Joj ze0GL(*n#AhhV#e7E({D<6x(}DzdB@r{$_fWMT$OQIV@k0gfk0h*-j~k<-!ZeJ<|hs z&_5SLHNq+(T1ri`Z4=v~U<0pY@u+M^qGT79u+Jj|gWbjz(JQgbJg7a=kOuKC6?KsE zp^h5)%I*n|H5^YX8j~g!X9auHvNAfltPG=o9kI9y=cjBf08Q3DyY}@-Gxd7X>cGlBHwG$k)2&Ogt8;u0 z1ectJa@0+;b>=>oEWO?dcKq<~N&PvYt8Pp(2u|qGZ4_8~7%o#is*Yr>yP`v%Y`hSd zW@~4pVE&785^$A?kOVh_-^y&U0c#Y{INJT`Q$pGi-ZiUB>o0qCi5WqF+>aGK;Kl!f zZN^l3Dbd0S(qSSYq^@BddPG!3M@_^cxE#lQb0o5|oUzh*tP(a6DP9JaMO>4>V@*Vn zA>SOdOeDL1L2ikPylZ{P+JZA9CTM!{$2C3K_;G5k;vhXseV*4hAYQXbBVjC1{F$`c zNN=-WV4ulQDt)ZSEO>kl7T*X5n%;EEbGTsdhO6jdao#t#p_@I1JR?mx}ju?y2{*vun#ZEDcF{D z5iQ2-un*@Na>$GFb!_>m0Acosig^K|UvaCn`C<7N($X&Ehs_&dwPpl;`4tWbMm&0i zz)o>BF;Y6;zD)-`c)*{eo0_if<=^{oQ?)^a1g0(XC1llJiX`?Hl_YpG zA!)yV*MN(A)VE?sUs0gXiRniT-=REcUgWIpBzif0{QXu+x3m$p#zu(gdkl9b2?OzC z_1uy8i&2~~N>8Ezg3ZSf;zE4L$4ZgVViH3t*-x;?uF8jXZ%HH8SK+4^405u8(uJfl zn~0=HKu4;{4vyUG?AVCyT#;v5atV~*x3SWBuC@?Zzyz$A@`_RAFKEZTp$|#3jM=?< z&CZxZ{Wzy`60mtQ-L7|$PSo>E>=`zrlYeK@%74KakK{w;1t;mtHD8jYhJop>F2h|M z{H^GljANrWkTxesUDBmul4c$ z-%)yIAIWNJ@7t=sTU!UqIz0?~4xX1V@$dxa(KnX0@Ct9&(z$gH2Q&R%O@?oqwEWiS z$3N0bq!aNeU*zI!qEgahdgjAVOb3Z~U_Vo&BQdkoSIHxpTn^wp{3TI~iFS3-2P4(% z4@LqmQ0Ej`OFRhiSTde2zzl2ro($YaNGbhV^tp3~{&?q(HjhL`FAWJ>70cND06p>j zQToFk3{D8z{8rayJt@}}njowhoK3^3YpBPmg%5$;4H}^8S@9x_?&`z$(E5@t{4}Ks z$^V9NA6aB##jX_k6}N*=N7|f4M65_acOdBDhr-q;rW@CEN6PNUOnNXE<6}_&U4s6K zcL|6F8)Jx=q!}dFkc=U-wLepDn%{uVq~6+}m8Z~;tUGu3!{uqpx#c633qB~6J<3l2 zyrB3Vf_IHHTiN-ewNCVYDt4i&>Rg3INydY=;+k>`-D;w|y(CNeQY0PAx z*bZH)M9`HRy2N5hn@MTMKecdUMUiYdY=MymtA)Y_N;1?|U5<8hD6m3@F=GYlYp{Zt zU%y6Lujm@mg|4?l4x`cB+K-1sclOadqU-4@Lb^vel928_37MOfwNxbg4OY?9;=!n} zEw8gPew^bzF4iitRg9BopmqgmuI)k^X{?C)vFGHWO@mrxIEF+;jr#6=2KwI~>|~|u zKU-a^SQzXi&2*d`Qp-JkU=GUf>{&f91y}EMP(J6I_7hZl# zCtR+Cdx*zJGMzLih%ePHLtl|GMfIJ8nTqw>72Ise96HQvBPb1uIOT$*ZoIK^_SD}kX_p49#8czR(o;m?{k${gH9DCKFh8JRbg(Je(5B>9`m7M<4ip%}uJAZlIF(@av9b)32JfpW~wH}Zi=urHAV@S;!9Jb{nMm!FJ zrq=9s#oSinL*zMVs9d7y#9z{MqDA5Y?Hn4UEV)W^&oyB`1WRO%`08W4_!Yp!Yme4D z0>=4L;S4Vug{L9ANJ5RN5EXKv3(h zBBvI%WQ}t{iqM zQg(ml*|kg7k0^S+J%55n=l+EF+4kv4#L=5cw`zN!fJgcki zgcSI<@l8nJ*QC{{$fLye8!M0YB+xmikiMV=KZVg!>hVyo$%pp|hn9w2-@5%=Xijbj z-8Ojc+!#VS+d0~Xxv4V#JgQ88Mfz->aF;$Pv0&bDn@Gnq-kx(a`;7WF#Ul0Gf-Rm8tDI&o(v&I{q<}*+{yIS`H5piy-GN@hyC4?37riVNQr3 zEUwooW98t*-*s+aGiCeGyxWs#XpcCj(HY`{52IDHHMcZg{OL)f?NY-vuA*X>LxkP< zc&Op&l-|%d@@GPmJ5K{bCt>DF#~!jS#u8gPm^Oh1Q^*X>nN)(7BYC1I%ufoPk*O5l*!MZ(+MwLXdZ~yG{w-~N1c5@Sb{f>6g;HE zNcy$K3$LUmzxqzwMhz#|-9o<#e2^yv=@o`QAXCc`PCf7D9U4y`U7(|q%PtP8&T*XX=E=y*l9VV%n3h^Fh6-#+*F8gU=OU%?IDa%v!nXv9~Um7G?3$--o z-AXj)_^uC0hh;JAhvSTNH9h&5dXd3<)&(vdN*5E?@ce+4^XNW#+e@JFOnbE=bI+(o z4VB|>FI`)?hz!Xe9Xz=Yv2q?AkTL@l+lAe^jE=n5w8R>Lz$SJG!u~>KkN9Ibx>5FlwuZrYJGIb| zL?pKx@{HIQ(lvDLOL}4@wytxb$eDxLRPrK)b88})Q@=X2Evb;SqIh}>%AqS>T>vQ@ z0UV(sk1|BQTV~mn@wtA7c<&wf^a{Pk zM-oSFZN$9%fQTvHz6C?hkXCEHqOW(3en?*(rMK(RBFqfd5)@0(T8UF_#%i31z4ib9 zGfZ(T8?#sra_UL30n9m4O6IO-~ zEKZEr*pcQ879MFP+10VXHGMy^*g(j4bbXUTVzhJYqJe9Zl1m~<9~V!-krrtyedD&# z%O@_RUXF`MT8f|_05U4AGop!8TuUYA74OVE+tJCucB`@4j)fT=(T~9JU7$pM=#NX z`$%w1NkYP!7{|CNePT<7V917J+|?i&rqL*DJTTa#j1SOQDpzW@iSt;VgXJF+AtGmh zxQC=U)DcREG`v#yDh$olfuT?{;77x@nl;^#T*gd|t9I{9yTAsVU-N`6KR4#=gpzXy zi08$N#Pa|l7glGT8B54Hy8KDviqMD^aj}KrA%zJc(|mpN!a}F@^_|vaDG}G@{xr8? zJ#+1bRUhZh`&hJ5V_vV}+@EsS(ekClsqfU#@R@#oGs8ot_T?8MxtjktPtS4w0oh*Q zk2z8on#u#>%8uTR8|Z>?WKK0QMxZ`3fZD8$tt`r(UcdG%l}cns;q*`>qXP$T5=as)}QlZa zwFDTaC9Ab2!clEp9g;80uktIh^g6!+cMGrs+|}TZk?5-wqcuS1^2pTa8V^J(-El+J z3bLl#=y13Me?#kA2edgvTCP7#zqxjvZazmg1TKh)o*yt^{-Brzf!y-s>yM^zhqbNL zbvMzgC$7-r+e?W3Pl-hl5k>KdE5pNAqW!6uih-D7y7qNT6Hc?x6BgWx7>LiBpL&?| zy?%}aLqLoUqt>(^c>_ES(>v=(ap3%@=mi4;7e+_T4^&27Aw9RRrDyLjJhX1yE&aNi zNV}`7<-!CB3oKrFl~+U!G75>xdSy`aaEZ7==?OJYrL0dN(ecJ@U|1r((V(}i2tE=o zOW%#m*>~L{0tnd33!1{?E6!|n~D}hrI~cA(ARh$cemkt zqh{&$mh6n>n-ZKLUedsm12-{bHFt9;${QqmD~05r<@?7;kv7NutXPqam3T z*%_P^_A0x@nlg7W93I1@3OR@<>1J2yxvyikq?0-Qe1=5y4D0NZ>5UYZ9-8a^tCI3H z8%lngG3J&ne3s1hXbtoAexFD(-3*CRxY#C9m&OETPKT&ob;nHg;u?z0%lD9Cp zVjVwi;HjVU7QfD)dV9+J7lY^(r%63}k9LpUr&)fGUb*jWY+)Q32wu8T`Iy@eUY5P$ zaP^6?1v}AYr$_ZTZoLg%(CU6HGO-rKM$aR}9eLf#6O=2;9?yv#GJEzAw-K(O4Pc0U zKr3ryXQs?3JM)kR47RqM+bAA>tDnzi9NQd_QE z!R5}5W3b(mb+GlELH8=yb~@O@D<3PjVGdYIZoqbsG&Sm)lv#_pOu;2UWbVNxeCD)*9hdrpxq+xpFl@{8|G_QcI0WKNviVJ^X9`=#8jtwM&m zdJc8$mJ)cHC?`$%uzk#)e8-Ln(}q}Rb8FKAcp2Q2@WnAKXkjPfXbv<&x%yeH^3JRr zG@7ivs5~Jmb^XvGTzy!{&ZwB7T3SKTM?2##NF|CXj${j*(8y^_P_NjkevoCtK6-8M zNqT-;zx+XRKMFXmATD|?N8Cs7Q!Tb#&atxz9qQ(p*sWV?;AvWpe(XR$96H5MA8b*J z?%;+h`mz9Ss3o|kfW>S}8XD7xYm*oh#*PuJ^9hxEw~3D!7VDkn`WcP)&JG)5Lyv}$ zE_FM6O6L|cPtUXMRh-yon1hbYQ_Hu1QyyX;oe|!GZ=<p#=KWtE%H2QoxspdcVXy~Wy-xeQtq7{KG=r1gwYFi zm6c%3{QM#ABV4q(EoPja*R|*B#NI$QH2}2Com#$aPkFFIbVgVU%};!51tDtcF4(HA zC4bWfM#ko%KC><2B$DHZ`qX){wEwg?$HD7|(qZRM@piDCrW1o$gz{{mi+8=EP3e$4 zV!)+C*z}|Bd}b9OjyJb&jI9Za3o|m}Gf$nT!*qbiQU-|TYYXVRSX}r$fF$pFMSdYG z^XNNm0WfL>+SW2);Bg)0yZ`4m1MqIm)6)794Rn-XVL0&xP@SG^~}A5aUvD<_iras}=y zVoT2}^Q)`ziclT;4Ph~WU337hjDsx-mOWk8HCUzz%i#ejjO*kCNL}d=han%jbz0(n zx-=uJo#QgxZLL~L_a{!<`ZH;~%&}cohNclo-Va(Z!pkX)Jz21F->*66Gb8&?zD4l122!RiL<3?@uV(gp6~ zn9a5(n8em{Iyi=n(0a@C#QoZ35;~-@=HoK${>159ex{Elboi%qIqw8??lE+TbFZ8j z(y|}W5uP%1?#k%+l9q#WdTHy@T#^f#Zqn}3|5a56*JOC>xUwvDI6PD^BZ|U^H|5xb z!0rl)E5d#ey=BDaGpyOR(B*Z$AlQLhw?xt~j#HT$35c`8D~Je|lyp>*=6y3x`GkIl zM1LE;*+U{p8F3?RaGvOmqd&JfkJgNy*VmvJe~!Ad%RbimCxkGLpYP0(9&>Y2z6t=eW!ha^Nk8%c z4sadFOC)*QhRbHW>AO;T?$@b$dR-Gedk(f!sbcn$Iy<%zZbw`_Lkr(_shUonzT-9D zA0$oBOzggtzM%_$dPNt0$Tjb1O7FFFw&7<)?VpjkJ~V1$deWgW&|cJ)^)O4{;uhK- zkjOtkR`;u{hv~*jddelF4c>8*MTi*xn9S-c#)n&thA^9N5}njqEDvDOLW(Y)>P_&M|jl2jp#I zqYl|5U;|;s69=Ny{TA%D(~D21OnFK=Z{JEfJTC?IYcuPoDgop4zvMo_vYajFDg8&e=>1 zSFNJIZQe*<7YpP#X>$DI4btQ&eR}*jX>^o-g97>pvd@y96Rg!SAP~+$;9_R2O{Vj? zMe3OM$;dEB#R+r@`*1xp zo_e{O-nbL#FYR#qw8we|-NJ>$U=N`*mCRd420TR?z;C`GVgU{T6tq;B zSCI-p4*}?PH6W>e)$u>Gnj4apWF7#}J;Y$)Li%ox)*FzB52qkL1+*M@+>{qckcgt3 z6bp@oqpT+bP_vvlDGEn54;UBlp+cgtvAUDs0%4<$3lw~)GFRA$$vprgv=4lN5|vyM zR8wN0kltaV$lkfe`3RoMQq&4X$)!(9!sHSHI6#O&4%e0RZS}KXum6UU-LSyATyfcWEpg&(JbGZn0C=MvGnlcp+`nie8S3Vh@2OYhY zxi=z@Zt9j2b~NJ_-t||u=IUcM;ayir!D9It?6~@HUOm_aDHm@<9@*F<#qVg&Ep#Fn zFE^C-Pf1EPC1wln$w6rOlM%ecbY>tY3{Zzc z_a7#e4^{M>FKvyRAQT`;N@c}I)@py+ z8doH;-E2}NMT109B0T2UexLCJBy))mnY=-G%!9%4Ivxs#CFIj3B-1&633-&O>Q$H9 zD5Y|AE?p?Ddaf7sm*;qoJ!kKUZ}9t*#$BOgBlEz$4P-KF{*%Ttq)HioX`GeYD5Y`$ z&5(e@%q(o>SbBceBPi}~qDWDsu$3R6*$SX|Cq}YSkp!_L)w^gD2DWQ3;c0(%wI0G(jqB4szmIl$_a(EmK zgQ!*Cm1X#zBYj_u_rFz^;r+?XK}4~eo6jv2QZOm9f#+scBG`i%h<{`yf1nw+Y;!Vs8a2~USCew*9Q!k=Pcevz+2uMAr-O!LVthgikDmQ>bW=Whei-WZR z<$xu+Nagubc|KQ&-vsxWnu!xcKZX`;eTKxr#D*AZmARUpb?frSxhW)uOUB)B6HL08 zAOd#yr!`zrL1kx#pW*~8;>YXO)%1kOoT6Ps7LzHc0ws2_mfc>iRrEtMk(g32D$21% z@JvKwN<&ta=>TXm8f~ubhmF|-5<{|S=>vdxKubTbKNjm1aJ53yR)z$~qV3gaJ6f5G z%5*Dli!dV}sWS1Qc?yo`6MHcQ_7|o{$`Y0>dHTxu@~@Qf`~?zAS89i{p9-GnD_q1B zoN~%KCln)M&a=1i5n7RS?GZZXj$Bc+E^o{i)T+!WJ}OegNnl#3p?F<2UY{grW-4c- zC})&sA{j#hT>*`NaJk|*{xM#aFcQa=&)^@^6(j-OeWZvGmxy(AVDv=g+=^h_RJx@i zh?WbRi3-aq(W?liABjrXOwk;Kw5d2w^hb^QIwyXFLe$I?Ov;)stSRnbd_h6HWJwz7 zHa9LdKcBx#w~_&xZgOdSfe`LC(jDco@dbb?4`3$=BZxRQ1dJl=BMksXZw^V2$?t*y zi+yw*uhsDA%XP&mu|&BFC-pdn9RSl7v!2O*6y}Ixu!OZnI!81i|HZSP$nfKn<5$PJ zMt1Z?sFD{q@>E{b`VnqE9eqRS{{P}3KmPFBF?|B;I;1C;mQC;p>uMc8Qak#uWnk>f zq+Hx6jFwyqtfkn1YBX6&3muwfKZ!$6O^@0#qPu@r??^=Ssgh1ljomuTsgEtba;*7k zU!?Aft#1&mU}0<=@2uglQaYbd!+7KtTdGD?=S-lOC) zcTC)UC^-#!{K^Sf7lL&3fNzKr6je^Hh!s}SUxlETFOz8#f~{I5rH7PkHw^e}y5OrNPNMrb$lqcn543EC;(-$#6|G7vhEA!-^cZLF*ulu(VV%#=!;M{|q6 zpa-XsL3Hgj;_(Hk+n9Jvqa`GA8a>z;F(XrMzTC9w`&v2Go)V1Lnj>q>}0(JJ~5QE7h` z72!s$Mcl*gBl`Cr*)8AR&Y`1r3kjy1NwDUR&#FnFuB;*kYu6G3?APC|T}$7s0)3xU zKG18eZUI*bTPeG$%xoUn7SXu^8pwHfv@T9wR`uirMK4YSAH>ZWa|!=)=jO|bquqDhGY zX=2sfT2;qzd}#bQm5gl-<_d`xsLG{VIaeZU@Nj{kaDcZ+iIk z^SXU|$daI-B}4vZ;K2aPgIy-OhAj7+9WgFqR)4TSuJTLqyjrP4;jaiNtCUkSOIiDq zxWmB~o6ye?Ah-7I-?Oi1vwFLMpLcjz_&_h;I@|Qu{1p#n>7hfV^Yh(ZhL21g;p|pW zfTguia1hef^`J2{m$;BA!At>_w!5FZ*KpHk+|SFqKSTc6J<7o$s(ZI6N5?3^Ve0)E z)9+86dLM60B~2W4<=x=Dk4B#GrNUU%8+aT@uJYh%oOK}C6Wdkeh=ujgn59!^M;+|& zLq6qYdssdqASg2N-S4ON|5C3}MSWwZ^W)R}Lb_%6H)`?q{0;lw{3#TZs{;mkxf$i} zJ93En_n7F~VI(-VY3X6e)7cevy_ENbMH>1FRv#R+@3%TiE~oc>eX3E znd{a8dX%6b8}$Bwgoizq9Z1#Q;}0htm$}9ccJ~-EL{JDW6-R|#QEsj=(QfX8pf@C; ztc}_iY%l35V0nrD?D_$TPGmW5#c5H;vS?J%F?u<|FJP!RSvyg1*%LnWLaDYpH$O5q z2^e5AE5ZQ*=7t6U1lwn|DU|Kal-@Ko<7awi)hgofGbh3bDwb+o))UJErNnY0)1Tm1 zC*K9ywi*cPRa-y_zHl(0a^la#0Soz=pS4aTi2J}FB|aPJ-O>Z}?s_c*ngnJU9isdl zYKofzmQ2@LOF9dlVh8H*5(nkwqiKY}>kftyLdD~F)d8+v4!S!4c$O5z%9Z`(1+AD)X3+bO5O6ij#k~w{hZ=O@IHJ#LY)NmI) z)qu1vzJrj*M%b`}C>OpJEc$e(yIOj7QmR9PNdM*@cB0TPvAJ`nPW`(z*lFaEylO;H zx_e!rPicD5{joF3)<-9ZoNJ%1eeI058oQ4y9vPJGQIGGlchtNGlk?wdI$F4PZ?x2~ zVUIR;Q7sqOGwuc^NK=&S$0)CZ+g%he)Vg9iF%iy=%yfRBOs`==-r5Fzla(BH63NSs zHy{Y2=j2_VsML9ps_>=+hfNXg)x?V>PG*)yhL)z$2BK6#Kg)Zd>N7eZJPA+=D}XW9lx+LF>Be#ez?yD(s)bm&szp-7kH()`}DNz zX=di#u$!M}nukZ4rm z$(WQG?6I+6ho3qvh`GV#j6LONG2_UXpp~KC=_m7unhg7v?4Wxe-=s&kug2X-^+)8p zyG~6=n&n~SJ65@dS(N=6lcq3F`dGL zo6(o_mVO=5ThvcX7W4&NGfx1P3g;Qkbdi{bMJB9@CFFj`_Mr3$ei2C)Lo2$FK zd9`Uef2~{L3~j{Zo)K3&^l3X>-?*-zcIe*K7C5EyuZ2XtAn2QLg<~s;95Ny;$CAjf zG=Y$iR!Ip-F_m;w8@r^2rym)WdN?B}&BM4Z89u~gV8?`^Zjl`Yg))fNGxzT{`q|Dc zFUC3aGhfIl@>cYjG$6akBL~ZmJO5Is4;tVgS*Cz&eySz3(H1Vq6FHYvkH(LGwDQtN zHT|Vdh_BNiTiZxy_dy1vh2DGq<+Rs@g|DaG-sT?Wo;z^RWOu*b+i#;g-sp}~Rd-xL zM_KSyDH&$a7>TS7=qA75L~qn@;F17)C?W6{G>zB-OTN(;w;T{dIpP1d*fzZ#>XF9HwfxJRmupx3mN(+be*Z&`K^^VGZ1*Ln zn|5i_@T(2zUlT#W-P2o&X#!6~SF~Xa#|#xYq;%6ZIqBpnLm@?-`l6biqwRx|4*qjg>4E{z@;nTR>-lV#d@`*;SBpzx1Xl zF*O_%6Fjzl1NXF$QAbCCbP;JT_3M*%Q=iO^9i9~D5!pVGOJ6;zletg8plvO@x+LA% zy#2}Oo_@`XD&9@n9Lh~A$eFm*XQE%$0-aLQ8N*hklofPiGt zO?y}wq%8|hK6gMno(qt*5?L=|_O1V@7hy^Y>D77>S9yWUCF}Hdqas8l&QTkRHWn6^ zHZommpKuH#9e+H^L$(>1sJ+@RbBl{}b00DUk-vBMtrOnzq^F%QcaXbVbhMjW6cAzh z)&O*^Dgz$2QwRZNf*(1q37Oa)VRKQ`%Zs){L{c!lbCw)Ynv(=F4tFrAltJZN$gUgO z44Fn`IRNQwcT7*y!((NT{7m%2c*=8Fr_z~yM#O3r&%`hZ8lL&W;jv2_*D+t8l3z+M z59;OpwHH`%3sOn`Q^ZnWBKk$-Hg@Zu(&Gm#^ow=iy+-bJ=?YU#rSUhnrmP2xsz5xyp1pR%#kKmuHYoKheJrF z7Jn{(aL!acktDGS0d&>C$TM|7#*sA4qR=$A2K7lRCV()7It=uH1mbczV~c+F<6dlm z2m-WsG44Twi5#|THy z{ROMXU!8WotRZP#H`2>9yqDAN6$}12dG5Y=%f!a3V{41n^{ksr%}iJkvUy>{?%lhz zYSFH4%VvwmEZMplB(vb(a+}qE)`&C*mdk*x0@#+p3Cs>#_{jyNV~18It@JuI?%VCF zV@#<1sR{1ZQbm4M*XM-%Rg5e7KZTZ9u&cO|t)W=Kq_{%{jw>VW={*CBqRgd-NymuZ zuH%*8an2gWx1p)$4sg@7+d^Vv0R|ElBPc6YOqVgXHO3Gv!(GSAh|AeJr{M+w0&1PO zV|cllpb%NG7V~P6US+SN5^o8wB>IGOU8KK^bSyNO2j9Ra^yE6juNn2VsoXqmI#-}k zaKp4q0j3Q~O#y~kfpOI$0RX0EkfhRmqzAnQyw4yb_i@sJ-joA%;MFLS0J@!&BM=KX zL!2|5nJpL^o`b19yGva{1>5&8okC6B`wp#JmlmQ2`e%s;O>O@@mu{-}%D91tjnG)z zhPz&|V8U8JN&qYUC}T3f4mv3_wZJTrKz^gmc|U4P?m#$jzUudoc<|Wwhh%KHQuR4m$+c7(T0vW0q{8JF&cqmq?%7YovlU(OBHQTt0t;rx zv6Ca=*!JF><)@cT3n~@5kwu<326o5@iWmVHjczTzGp_g2vAZ~2UU!#F?a;@!gHQj^ zz@xVlcZ>*II^QqPpd6qF@^6I60Nq}qZI$V?%1EPI7G(>n)DkXskfaNjwDiR+o^)4^ z3yqkVlR9V4p!C>);7p$0# zql$+lPp+q5Z_()Fbx|O4FuHdDbdZDC?2tQv1+keZr69Io&hAn+&R9Frc1mI)oO#O| z;;_%#;Qbk5l+5>+=raqT0qFUi^RZ;UNRc$os-Y$+wL7P{^-al z&x>3VdUlI-b&BsLh!goYJ!e1Pc86Hazfov!zkRuX@X&8YPd=IQrgMmW*WfOlg6!S1 zFo0elUIUql2Ix3?4cAFKnm>z7W(xk40dQwmsRRS*X&C6H*hY7k=>$o~tOHfrBWws& zqgxwoFt{<()vM(?H@xuonQJB|Pn&pa_MF?qctKvs&HB=ifjvKr*By?#^++t&OgN^BkU3u8jtwRT&R;_$GxCd~CmQJmm z`&jg9Tz-Amu}+d@_YLR&zHks-J?=x{@SXEOeIOT+)?9A>q*mVE}4fmP=rU zXhGIVB|p}xH9)F$rKW@|mrAbIl!$&(Ti0qzgf>#ibyfm;Gn}14r2m~3NoW6~770ov zYg-7hQmZ#~czC14Nh-NngBjn5+lD^hk}!i-g&}?p%vym*{x7vEGX6hlRY2K-F5ouv zi-67@9Xd&LkJi|AA<*$+z`dvIcr#ionoA}3btP@^j;U1g zKv&WPOe5F&Syz%ki}@d=cOL3W2GS|Qw|q7r|5Eb~SIV=~{ztl!A%N^Fz4L2zt$D&^ zspPS)BpB};kZL{Am5ikGgyB-jQ(Z|AN@ntt0r{D(#0#|=NbfwahBHr?CP99oD~Uj@ zxl*l{x{__$aoj_p9^kyvm9#}kj8yVkS8_p{%e@ks;GN%eCFUq`kxJg^N>K0!dmbnRw#MPN|<1&^cUPng)CUcekxchWx0a4{41Xd?t;Ipk9k@!eGCT=_=y!u zUZI#i*5?)dgVcNZg4CmLU&Li*#>J1%Y(!d<`lJqNN$=nu`Zx5(hBke3p7rg{>^RX@Rj7=X%gxQ|?NYneQpR)+Nt8kLeTWlBI|xUbJ}f&L+kzmSuA zY+B)!Dlr(sCjw(TDG4X*B)qW5sy@A2Goi72s^Z+MF8(7itE0T8!g zx>s#|h-*9LTn10-t5V&9Qc}aa3~gN26V6^wn}{tY`=;(p%>1!I=+Y?LUZGx|(N@jd zbA2A&Yh@QzGBj;#EJGF4Y7F0(W-RT8r-h_=G1}`CFWuHq8j5Ouz#a>`>&V^nL#IAl zT>8$8J~xbZO3&5rP!gT8JwD^w+~AC~VEc$}qAH_7*X+xMCkdBxcvic1OLIK?4q7ub zeP`UAC~xk6eskI zF#l26(wU#2Yt=Avmx5c_OGd_yNAK%dM z1_5&hCN8R{ZZJJKgc%1yf~Pl7*I$${a8AIb=8iVKd@Y<>clRJyobBvgUG43hK}kO# z*i51%J3^FHEZOJmKWbR52JM~~0#Y9QqaibrIgidU=vX{t`R3ws5a;5jCSpoPmqW?YDe6iwtYLgMR)YJ?lnR|D12@NYLxk`*+b9n4*M@bOf&W+)ILrDu=NhsaU-ulalV8Rfw110U4GJsl*A8H%aeU>q`1pL$=YC_@ShkR12%1 zRBKLUAO0Z+-9U7QU}HGXfj;|?xS_-YCA(0vymAeH3(kKk(Gw-?!s$ZVl`G^&qr?j( z2T{@rC9R|qZ(WH$57DRg$2*{gzUDOEp0wsb(NF zY$LTKc9@yEO0WLE5E?T3zCb&>--Z-(* zCE_!q+fDjDFNc=h>6wo!0g0j&>bH{X%li}LD1#fAv_EN;J&9@=fJA4)=S&nEou#{G zINl@%d6S9Wtsc{9Dahf^ZvbC5kol57ijpAyDEJal9Bh_KC87LLa3FgJ94M6pV$WeD zm0+}`lD_;=@B^y_evnFH`5oW~R?@QiogEmohA6?HN$)rT@+RpW%u=bOe>G$bk5uA^ zl4epZ43AWT^9vtQ>IRT#-DV_%+E+<0WfWa435(G<;wIhl>6^ueyH1&7t>X)P3T6B`yw-T31 z^?ReS^SbByQoZi)SUso8H+o0pa_)gt&qbv1i-tr@=Pw~dG$ibAP)S-D?5N9_$u96N?~W@#{`Sait@C3}BZTU$W|!+u=n z{z0nE3xEHdhul}HhMk$e@J!;+v-2089!e}d2M%<1iHh(lKbCZ6?xNE}hn-ol;7lU1 z@(hb~cZm-7SdC>uEYBsm$ypaCWM9l?zb0IKlI%S(yLWQ$i4*zz$tYzHvM*#!IR7NM zceeB(_hUE7r$U8}JThopt0Mu5XhrOQ?u_y;opJc@ch)HF-1Mw{2?_hMrk_hArjdaG zAz^-j1Id!aedA{SkeYIK`uP2cFQNhh!XtwEM}Bbmdm1dRroos4BSst;lPvxEIoUfQ z!JGZqEf@Tyod5a1ri~t>P2Gc^(I!oiqnPslK7IbmET7SrF^kctb5kemi;LSgVd}Y2 zGW|mR10u+d`K zU(>aG|38d`7Bd_UK7yiWx3E#ctfa@=t?N>N*Gl`T=5s3BCUKtrFELKN176<6imFpx|$wbGqj} z@!Upw4r`HAA9E1_a_?1*r22TS|LrHwy(IXe_#=r?5Zo+PYt|3Kq2>U3IZbUrEzt921 zNdtG7UL-TG=>Q^egILNNsTb*9AL5VW1_Wnh1P>Tj^_#&`ZxjC1_x$g|Qg5q*QS}=u{m){lvbTC7 zhE?HeqXLH^xQoAc<_yvvE(R@@h^2r2Sq~iJQx7li5f2GJr~{;954aXOV2c0~I&&pd zVc&(l3R&~&&X-4o*w}V0Dbq%Bl;de=L1J1MXDR^y?1Y~=g4HR=rL3) z#zpX=I1&pkQG6qw9nf?B`2%T#<%(H&VdrS>jT1Ks{JToeEWLfN zE6+xL*#K;$WdjI?f$~*0^nZ~bW8>~=Y28)wWBKwG@Sj_a~6Exl`|J3|DF9wewwUryU(BvW6K{A{ShzeQ!=~F7s zZuK5Kq!-Cv2VcHC*k;Jk)*3Rdmai%o^^Bu|p9`GXk-@H-kF$gTOFGjBcG@vE40se; z4Ak0*XmU6C>V9oKJab|$45W(Z&9c2VC_C3uk4sF-eY zXFaIx-3Qc}b&>bbsB))I+q3a#zF0oWcIdmI`QThgar5$wnR2c&PQL{uaR#Poh>|39 zSjDC_O5zQ>KpIFHsEx|=#sq;(_(Mshb8jwsU2yOj(Dh1{S`~}yo5Y;`VQawfR!Ng~ zk41ZT<)jj;Nksb*hTR-Q<3eSeavWC19QR$!cDx+-ApvqXW>d2=T_>{-wk^B&9$Y-Q zcFS6PUPe3RWd*y`kC~x?`X`smf8rZk*)Tzpeu|0c^ z<^S*|n-m+xFYh}ZkD#?G3svJ0D1JWSjjq&dbt*h`*9xCOZC58IYJ}Vq$_W;xcFeWv z1y03+C!}F(#8j2k3PUm0e~xDz7jrrs`DQD?@1L*9wokEndAWZmf9BQBYk-ge+j8IJ zHNa#IRqnBg7>|#p+R5WE!z8N)80zU`4EyR~rrZ;cU#`=%PDpF{*d@Z*QzsDYS|%(R z5)f>TGixq9Q*9z_onU!~zi<}g52++r?Tqke+cfWbJF1BH#w@=U}!v z=L&F(DYje|ar(Jc88<@a=~>*|v^Nsj$Bbp{{iTF4H^^qj32JeSAXn~sZU_vKSyIgT zAW_rtG?kv)CUoR=DhI3ja@R+mWP5Ik<*T!1t%JHr`}%ZX=`MZS)*NG7%D`@L6G$Bxw3}sQr4PVl!z#*h0V2(b-vc3QjuenTkzuF zV~F`sY0Y-R95NeN(GkG20LQ~?DEnBJd^Wc+1!%{qK)x`2f+#2q*#`~_75HC`+La3K z|L&@o?w?!=usL2tR|MvHu1&1v&E3_RLNK9GIYOjisCI#}26hf{&xJ^&o{M;aR8#8n zCAR!N;7_S{i0ddboThRvKA%R4}S;DKSpnkP0z-;O2?}Np3EbF76&GDkT zBB3SS(PO)*v=r%Zc}5g_p(R45D7NBXZ%w5H;ik4GJ8UZXR(!93>I>!2fEy&?e?Lqs zkB@9G>fVZfDYk<4Tb3oz9Xv{H3^#=j{Bb+`9%x^u0Y!&b*mfAXRL-9&1Zzw-PE z@m=AqqK*TA;~<&(`Zy(ueqA$~87>t=*p7)PF$ATetoMG`HTtd)OD2)Dmgjg7E{k0p!PC8V!Qvy&SuXs9v`BJ90F7;ei*1E2HKH7km?`>M_ZmLS-q0*jFkKxfb1ua zv7glbCryf67twcJl)P5i_*^)y_&pal3%7oxrpMr;A135WX zYD$CPzvGiXpa_DmB8Y|HBZ>f8fP%h*n0OlwuXJ+>2s$s>{-uQXijwcYlvrBr;3XJ2 z0wc#-M*ceaEd8J46XR|-Ki^I;H?TP@zB851r=U9gFj*S`E{p&I%Aw%=Acxd4CQ9~v zDdD}MWN$vCVJO**l7nALun~C4q5KjR7L;UtDZw+mAI=K!_baIxZR05K!uHX$(-I8y z>52rUvf;7Viz>3lVjg>D;3#!cGg|kj%pJ{f(HUSPLmm7K(S<9r-wl6JMQy3k_+x`- zKWS~KqTv&6QmgXPZC_!*1l*)tXq9az157(VVNA_xh{pfWr}G+I3|DL2wC?(JwPr=0 z#-_K8+NPag)B95k{fRt)2H-%=P?uw_^TVO)&Z}%SM*i@Zq3~poi;bv`rc@Ok3J>|1 zx<jg zVqU&Lg(v;&Y(x&&=c&?TV-+)ail=Z%;3ikT82h#gziP*f(N!iNTLt_e?s+c0+K$?e zhW*{*cotE)d8BoM5>kRQKR|6g3woj20g zJ?t?%U;2gG|3$)3gqJ@N(8xxQKe&}gf}tdQR|TgC+nQekyj1IO zQu6xT0BxLME`JJ~7Q)fxKdG8gMdHT6{YtpoJRBjashL8!d#DSR1IF1xz{-sqS8CJw zU&9ftzG|5=RSlnA?CtXlQAGjvE?=rNV*U`^ccmK80ibZdivKncZIO@Vwp>@lXxicI zp7G%S?_+M2kLUWWEn-aZATnYO|1V>bW--ey=4ivEsn{IF91Z6TS=s=C+Z8{soc2r$Jcd(_{)=5qNRFBQ5PvU#ZxU@jE`@LMehdVU!d zOWjUq9aoGsEi%mzM_9)qcde_9 zp!E?&;ki0^S7i@r5S&H)V1>nKRKzzbAOMWPK9ZF#P(>Dq86$x)LpFa8WskwY*jDqa zm@Bqg%lT6vUH{~jwY{W&jVojoU<(giP!F*?d^AmYng(2nRZ$jKtHGTWw?}VxtFs8a z0!6M*<{pInuE}}50>AC?Tl;9t<&i|AejWq$7d|hiHHmCYP5L%z+P5(^j_`g$->;=T zYgx7s!F*X)d^etYV(Q1sOml^ZIJHPr=btNvfV+MEA} zmyx>uU%@XR4&)=-IDKtwY)%`=O(+A;W68-${R=x@9l;t5E9NHS8~-bF|LV0y)97x2 zqsEXQ`Hh)2Z7liXb5vlr(MFS4sfoL5lkZlnXwDX}1=dlP3#8cl7;Kb1F| zG>Jc&%8s(bqm@f&uySRiX6`kb@kA!gYPdIRv~pz=90y-O`zl~X15xB)xcpl>m1X%k zI0eM|g8n5Prx*Tj$2~W=N-A3^PBn2s!h(~txK^>HnnFqCA*>0ii>an*%6-!`(A_UB zabB6*Vz-H|+yg`_eytPU?FjN@86%Qd26?3XyHl~vE$a7>=Q?_b@M)yE3js$b{aQGn zX@dA|+r{p#wz0N}G7zL-ptgzOjA^;ZL!5ni4%Z~hlkWU+7s_uSP)A`W9D4hFX=&ok zg$jTzwtVGIA5ak% z2rZ#<_hy#^?$(N!>Rjxk2AuG+^5ga%5J6eJ935RC0poH;bAi3}M>X&xj(i%0PvdWn z9eXqV%$aocbK;G0q#b)>{Dd20$KIH5>Xi9&{Ee~9IPS(opPt@7dH3}4>gjdHyQdF6 zyn0GiPxLkxSQz{Zlsfht|nX`H|{#`Q2j)=%#?m( z!uT8G@XSv?rQaM!Wjw>Ye)0P1%>J& z&;{PlFRHzT_wy8ZKf7eOH1E~0RHxvIKB3*DuBHp3_e!+5e(wLq51O!zZGF}Ee5TgS}+JJ`~aALg9LQd^;=9chF)VqY;( zNieMy>WjxHP~OFMdbu`+=pvTKL{=m>VL;mQP!;H53gWwAV4 zL|uV)zY#0`X-W}G{Cxw-#>%f*&7QN_H?FhU?D-lB9CR!&u}5<==BRI+n_->?X}z|i5(cXECt{dHVByX23+%9j zD$U|cj|K6u0~;abm}JqQOU-rEE;kHRI_ercHU&R^Ebe#=3q1=9tpu>GIpWeBV>z$V0mXM40Y47bJG{F??M-2do^i!H3({6`o6 zYL15G(6Dqq1n^`atz~>&xRCitPaW8gy2^L?;%?<5=8vZV;Jy~XL5}!vg&-DGd^Ce= zEGf6lJ*JMR!HD@y{cyXsv@JNI@DCpnx)aoqx9Y$SU`!2#*)EW=Gt3Qf$2K?9p5^mvY>bfP1GP^mjL)v& zpfEGp7#%aj+c1f@2UdXrklpk=%Z_DngKPxL zxEy44EsW#D1Ik$Hs*Dj2Xx9pvN@wFU@FJJkXn9j^FOl;lmv~osC(%{eFJ0l)T?_Oj zrt{Y?*jaNS^P}34R_*9D{|-jW*_2kQMzKkfPA4PmJ=eSRH)WT6TFe(EI$oj?OoL2m z#o~)i0^(Ls3|F)E25^HG{1ng-gU*2hcQS45i*MPaWt+>&Y5O7`<9l6Hl7yolabN~MaZStGtEqGnx@ zHS3G4*-&K7#-eKE!@|I?Lm>Ys^e>|3xG^1ajD<}U9IeTJ(8tXy& zh5D-pYJnq`Q-26C(WY>XmHHR12^P7|=~6@yZLHM4aGQ0ad3FldSgC)Z z8c=^TB`;i4QsnxjuRx8w1Y?V+u~PrSy;-S$;TkLTFI+Q1s#e4}R_b54%~%T!DO_Wv z{)KC-)W2|zmHHR1u~PrSHCF0hxW-ET3)fhwf8iP{^)FPDPyGwmSgC*E8Y}fLTvJA5 zU!QBOy||-@8Y}fL+?ykEAbk~gv`$6gTR!zK+*^4`S43}C>R-5xmHHR1sZaVuJy(|-#z0HICyVyPm!B()iRqV;h6YS1r z>K!sWc+@tW9gCGg`kVTSvYX`0u~;ukZSq2)wTGNhH{5m*${}5b=R*unWyQ-`85)6MMQb~bYG^uD#1nGwkUA1*Z$enqx6xaenCaNuo+^NWhjVy@Ou@aCm<0;*0L!5pV z3wP>1Vs7BP$LZ;h=XIT(9O^_{+}HljUTlD)e((41X(JV1P32x}`1BXvb0e4d-E9;) zHnip}c01=KyEmgoU|O)xiVxq1}NE$r+$c7E?M z>hS`im~3QnUbA@EytckGhZ)#eYHS#>w0+3(fqV86oN<0;Ywy2d%g<1ncs>2aibEvL zoSUcF7M;*6A+STT|Io)R#`Nho(t9!zV;X0`F!BtEWDoE&lo7fU+!7O70ghV9Lft%u z@?Wu>({!IDS^UjV7t2TUd`fu)kc+Lkb|R|AzL-!6#CNvIpK%+ zDaQwndocH*(A1bGS5gMtCjb5EFWHIgcWnIAg-Ls&roLRb<^4ozB79RC&I0F@jIw2J zu(gj*vLD}0JW0U-OC8pBtn8+nrGlHN$;3qfp|=1|4Kp|cEO~U*xJf=<9220({9-qR z?dhYdp?k_E{FTj4yiDIut!^m;xxRu1nwqD^^r zMy#P6@=ENsm1>@Tbc$+hN_v$!sCUr(@F(F5LQ0n}wU#OX^`7TF*BzF}V?%p$)Pp96 zM=^k{ED+7f{{Ik~`E2joTlGTfRPe0WYXI5Q-^|_uw#_GxQkOe7sqQYo=8!S9|H3YT ztA>Ym32eB*Njg}BJ=t9d|wdlI2%RYO?%h_GZ1_so_)efy8G zY@`SS%jo!u-;{8u98&SgGkb}oR?@5`*OF}nQEoxw2FRo<)gv<|5d zc~;SlS192FUHD9im)ElMzmHncHsShw<~iqPvgx{58*?;US*ks2+a7voFCK`(j}ubp z`6NrQ5KI)jMi8Ek=fMCbaeD5g<54oIdswu6)vDP$SsZ@IlZL&ec00DQBQNgVSi6{( zZ({Q|TtW7Y4Vmotoscda8};%|p8abQ6@T5k|J=@J4_|zO%mBSv7iMHgFrN5q$8OWq zKj$VSk5E>yzuBiuW}MTuk!Rj84UV<>&-K zw~9{id*&qiFr15T^F2!Cz#YQjLP$`WVS2{{&3?9^nTllgm`3SmQ}!|z1`m!*>MTv* z*k$gzgo>$D%iOiz5ubz%L>jq12tU_*;hd|+i4Cmv<@zi^IZz57e-T2QovblvL=V$t zSz}r+h64w;0Pk}_9IWTAm)hEn4^P&A)@a)v+&9h+IH{fz>LCgWhLVzkKG(Ep3G0cN zh^58BrbWs>FWC>@e^0Gn_zg>5FkpJtoQ`o*nlzc3*lzX@)1wzA4lejk%1OR=gy#G$ z4Q0<#=q=}46nYj``7E>S@DAJYW;7j+ypuU(TLWZr>Fu?v?~J2K zUaZZSx77XK0jhOZ*v-OOfKWv`X7X!0A#jq3qJN(uWgKTOm-^HaD+&IlZd%ctFe#+NVrkactCZe`VlOxind z>a;nuWA@Z(^J(ubX!Ec81_r zy*#)^-xayFIX%H@s*yh}tO%z$1hr-v)LD!2C)3kSU#5ISx%x}KU*F6x_{i>ML8~_& zQcfk;^LW=Ln$=EQvUK!}nKW<E-a8xJ<-K-8ov>qk^rW8)#xO9O3->(84 zV4*VUK68{_OG<8Dd+NWLJwC{B!i+ECecFDQ9lv;x{l1C%19ukM)IWA?&qhP08N~Vz zwVe%_D(2v`U>V0EPE&AJ4dmRo_!zWAARJV3j7pBmb3mcv%%{MhDrSpLd=bmicG}KD zO|68@m&k4PYIgq|=+)*C2}z@viG>UAl+xN?CO(}v<8~T34{hCcxN+>)dk2<&>DEH* zrDH6V_pkUWVYHs4)Da0Ic<<+_8gfb@T%qu%df<*t@oSzy&S|%2%>R8NXz2=Zf|RMd zh8fQX9^G@Yz|RegV>TZV$Fi+(J@2VJWm;-*tw#?c?6lM5vG{g!%02B>fsDJQZl-;0 zn|ik=P2B$I*zb9q{D;a`eZ2k~*ql3Ss`JS~oWtrki;$TB3Mv;QVJ~CnRhlQu!fYbK zA^#6tLQKijM?CRwD?0VZOV*tIc-ELQjeR<~@>Wuu?$okOUWcHrfRhILB>Z`SBPd0l%?#EFn~Q&mVwF~0Z*|DH1S)Q(O{E{ z+(^g4!V25L;D7_86Pt@3*!gx)q)Ad`WIGsrjkK92q5;RJZM-(H>9nWYdY#zg-`08R zzVlJklny@ookdsrowkr$4qNkW+WyFyf3F~EL!@s^E1aFM|KXL!@+z>D)S#)C=zz4hrP5$7?#jD zfDQhYJ)L)-rjzi3B5&XdSnAN;`6nX-w@#!-%T}#tHn*z+zh%<^rI_`e4a431 zn>Nm~lJ~en8~O4%6Hn=vtE@s3{5Zzy;dM4#lITA6wlD}fkt?@Z@&SoS8M{tQ-GcUN`XtptBS)x^NnJQ=_)9lUqUbD?fB z0?lxkLHk)pQU98{|1kXgv{_1wK(7WbXdf_t7~9R-Y15S2bXmBkOQv!xXVy&i{>l3B{#_dCKy00p$%;ZNB~221EDIAwW1pA zJ@60HAY6vO|7qpGBleb-dOlM2A=Y(-Fii|FK9ZbGdw#*6GfJ$m7;Mma^gj-(?1)Qp z73}L88cifR(715A2jgiqsSYFuGIFv41@LkOh8ctFFpZ*_!k%qZ{^xksVR@#~HF{|f zNiX5sx<_bCkCf8yF|=QczZ83EJnKRiD1aLI&i{0XdDDf}k5F+ zH_Y+l#|bQ4@DC7js|INvsF|m51Kh?XWnLe$5w5a8SGnn1a8eRXDpEY8C zcAILw;g&fLD!sSNiMG%n+V-Z6{aII-VDquAlMP)|=Ksf7=>9Ib zNf=14+4}Wifr|5$RCx~Paxy`d#mlh|_o%cFBu}M%`?+LH(+w>78ryPs9>pjh zCUz@O&LwH)@)%L4(n^gOE*+NMt3JH0{JyNxaqjGI4M^e0Ufo;WExAuf{g$l+vJ>yD z3Qg`2=-DJd3@_luE2t4E1jB9l9c!-7!Diu7H8}mjgclb~xSNCTH2GeDg+`8&NwFb4 zV-cymXXqgL9Yuhd32qgE+*1*e8o_v1Uc}DXwy|`<7q37U##dC1&ahOrTo@4^l|+;r z**95UMBlRz;a%|1VL?GdhUDc54Ov}jZr%sDFZb6}`V<3sg#nQ#dYa=5D9`v5n&z_# z?615>{29ZTPeGVGFea)@L*_CK{Qd(oh<%Eq^`ZKk*Jr6ja)kOD-n^;|N#-&nJa47; zGJ)Eg7ehBwef~;*d*YXLv&;mr9aa|y{X)!%a_%*ZMe9!$)bb|i|66r2ZOn^Fe9?qF z&=2{Ng?^~l)iOHA0w)A_@#ndz9q0<>`~xX05#+G{n9qphwXo9tVK*zM7{q7V!e@lw zZZu*M%F9pLLv%bd(P3Jk;F`YyvvrKYM$jXRinJElY!eVhYu}*++DwY@v_wcj+5t1M@{YlUPmb zt*dKphuH3RTBJ(SYE4oTWsVSUJVWP&c4T22tUaZ?yN?YSFu!w`S$(>u_ia&f9rt~&XC&cwajZ;xg z&_?sLKQZD_ke;=9An$qo4QFSlOuN}r6y(#NrB8%(nkeL)TnGp#@J`Y z1NM{8H2ZZ6bmaiHS2p;p4 z^HMF-L$T^{9FHaW+d2_AD$TBqQ%7g=NVS-O3O`3vr|l$!^|;--Io6kKO1cccz# zVTN-3pjg`U?nl8rL%E}D_{x(&t}Y-7zF2|Es;mlkSnkeJ#)*e>m#g7Iz^C6`Wx`!` zTzG=;8QdJ!k;_uJZ96j5s%@yk#WrVW_205?&rr|srvIFFt=KdnxJOctdhXq1`4^Y2 z?r#3=!u`4@HL3XRnGfIfYaG3E?Y054PfHy>1*Qk@WdCIKJh(4p`*x~X%fW5Z>8Vqf z`FdXaRNd1pbar(5(Ftx2wb;#_TY~o=iOMAT`=E3UeZDJ%=RJoUNJLZ*P?HLu{}ID^ zSafW?L?MIq6sKeBvD~BVAzqUwW}B+-jS#-gfM!-D6If2%cgi3-alTc)H8q)vV-PWT zd)t3eEAZKnDxM(6MO)aJ`ybf;ZC5Gfkp!}^v%(V-#3HpF^H{$0Dr z%r32_4j4GC)pF^dRqXfdBkacR8@Q1)4x(t7N|qS7YuJE;iIv^Rwu*~Ktp}rb*4@m= z%E~}LK9+v=%jm~uH{kh(!{f+h@|M`7?)f2M)H7O#KFbn&|Kd3Zt_;PbjEot;Oyvjm zCW;R&{{9ou#IArdGjslTA>Cs#MCZzND=iw{uU%^!#414;bMa@LNp<3I;RFk)hZxH| zR1LhVerC6-4%=zmwcD^g&XwJ&xBPF4`ujcg{d=XXtJX8b$0wv`wO*O?x`)ooL~M~M z%3~ItH&%7qcENdutA&di3j?-}x2V_Pp}mOR5~$d-XJo59=CN)LzIcH-J1ozTmfEDF zU3LDhs{X+X*&8wU_DDDE%PW8sR>kAGYTF|t62)*=*T3n~Dzel}(ODk5d*c!-tQb}M3^3o)o{-dEx z-ffL&*s3J4rFrQV!lSazxFxG+D1!AA_Y@k{rs7JHP+a+kD!qHhp7L)vOlQi=Sd7hj z9HtcjeSTOVFGQCStGfAEV?vomK2{?H@n~C!zBTgUq7*p&jvqXPyQRs?5NKw|kT$75 z4x!1Z?SqHK1qBVm%xqR6gHYZ4leUJ1hLbygm- ziFAxI@o?zf% zz{wc*$AW2ea10ev-<_pmLtbucjR z%i5j_bZT7x%dga*b%SWVD@7(nMJ6UiM2yh4%+4;&#zLG3iZ-w!13Y(6G&E=D_ z1DHW>;f)1FGZ%72vPNkfWJi~;-#{3@`N|)E{K<~!a(O%Qd5Z+7BhYf4d`A0&O>4A- z+Q8CMTa61|L9Tt&@a{U%8DRzQJ$%gW;gWdwfqiJ_sCKp&Z^rxU6n1|1Zt|J7SgKyF zuJgK-AsxCD(+E@_oqlK237vxFv~)^{I%Jjbeu+gSZl?w8?&JIHCM~9p3n@CZUYFMX zZdKjq4(RUQp=0~%RonsbWkBpf!5-bFR;XT~v0IJKQ!BVsXoUsbA%zKxRKX-X;Kgb? z=;c!v=-y`F#bEDaslvA4l_C-+sSiAT>hhF7WZX<6@F*E`v{6cyey%!qJS`q@06GID zt8H?dpBpwO{ap5E#D)FMhjjmSD%m6XroA>w^Sk-^pfAt=-Zr`Qm9IRmxgm!M4*FPr zss=$wdV_oeP*PfXhZnf2y(J{+yYr#dY{EP_mV@r9Pw&Pzt2LFhw~oi`ndmmSX;#uM zJiJdEEX>hIs45-K(7C%fIhJvBqS58L29zj95n8#fXDMxPvs)xj9u_x^s0hm5REsuldl>OS9NpOwT<` z&-d2pu|96RM|fX8J=!R;i@3NS&tILw-k|$~;%;fB#KkdcqLhQ;b8$DWEToyLtkqrw zV^kGL0ojm`i#d6(S#_ue>2R?qlOXAL6x+5N?kmuY9qiHGH|#Y0f|~wul&WlCBgC#Q?_!eZ@Y!#t`P~Eb5CEKI@;JIgS==4D5r(H&`y9ngQlJ(DLAyl@&0ocx;xy{I#jP^(>@x#jAa#se$+`_ zHU~$JUd;t}J0Xf%CUyMXrDa{BPH%_a{IN}czlkfOp|K~;kF1Q)lic_W`hgD_cb5V`{FdxF+=3VOyK*aS)@aUXBCqik9BiV zFBC3zE_DScr_CvM)n&sSLq?G3YL!JN(R#Vyd3en_YH#gv<7&arp<};BGjEODO8ze) z_ujrJZ;ifUsd^*RhE{#=IeBE_ltzYifxV$OP)+QCyJt;RFXf7K6*3ZuS zx!6n0qB(#g4k~ES%{B*M#a#+~SDJyV8Vie}&VeZ@fkBCh^f_q6h@haPB-?2e33m}~ zT&J$XhIQ=}XZ-{Zu}VJ4HrS5Ax^mxc)t$q7j1}ygK){W|{~h&Ixqm~A)q2tzsMPvN z|EQ4;MVz|paq9Z2-8-XuYu`XYRSv|3;`mP1bI&k2CZy+}*zO^Ngs4G1gNF>pFLqNs zjuY7f7N=|L9}&^2P47Of{rmQ5jiXs;zHFHzG#tKV@G?#14QvZikMo!LKN_t|TuAo> z1iOqEvf@L#$H#XMjb|&qoZPz5fY?FpIu3{qXdfHfzFka=kaPTaX*PzYl)z{+@osVY z-}0BnzM3dnd8hpMQx6O`Q+o((7eOA?V=(glhK3Fj`V9%~9zzt}BXo%VH})B!)MBh} z*1At$fB#5aU<`qHpJg`h^B1Rx*%N^jhQQ%?m4xoWaY9yNNN7BNod=LXZ)GXwx8Pwr zJI0vb;spz)9q50q{wiBsoU7OW>*CwPUD+7*@5S$fpp}K9-imd>AERUWBU@}+{r|aR z`J)w+te=N?HhHLH&naEevG6q`sz~#T}+#u^~4%lQo}C>zgSJ zxGG+_qAKKX2?)<>SctceQs15n5esZ2!uy)idW;?0qsN#rl)>LK)g0Y7RRu#Z%#nX& z;Wp-7X%6^Ypg9XSHJ}`|`Pi{GFJHVcT}=rNPT~JRhpuX_ifLkH^$pB9xB&fCVLPiz zc0yH%M)n9DmHptlAPg^gL!T_Ui5o!)+=VT2g89=X4xc(BkXzR zGqyi-A9Z}TpQ`+rdz^(LK?-zA8~x)Z#b1~*^-^M^C||!tkJ2vFpqv-f|NOWd3NnCE z0@Q7wS~{pgz1Iw-+?Df?hD)Y&)<3TnC|hMiRi!@8hhce7wa)-|4YQlP^+a;#&cv#2 z&jHJsZ`8-Bvn0Hlm9x9?m3G#6RQ*VKAT%o+BqQrnRo zpLGw2I55sTaNWc%eiQnYWq<5_OV!Jjf%aODUVav7=vVM`(szUEN)>!+4Ng(cHyu(-aQd#s z&}7=iF6re^A}Ue37(0%P?h0p!{pH1?Ro4kj2EQr)V{!Y)m2MIe!hWt zm?^yEL^@aV$Y!g7W>N#lx&8wrX;{ST>sucue0a2-|^KQ`CE2MSFxP^1V**>`)2e?#m zGr+*9EM=#hgdFXBHtJxTxSH)3d|;tc4H=xs&1`h3Qw4O@1+AH<;OplgJBssZtldK-W~3R+BK_5UPY^z5vq053c=T>mk*_M`>u}{C2NCj*XIi$g)%qfkNiMKo$X@jtc;tH4a*f&d|pm zph8(Fj$*w>4Xx3%ywGs47s=Fs^`?y}gTHN7iR#6E2Yv5VN$%B8r=wQIOR~30N8>-? z5rE`)vC>q62&~G4Jf^#+_k~908*mY>Trq7n8dH(fSz~Wu{G9OS>JBSta3nVWm7T^l zqsgySn<5=qKZoSBKI43e{L&?ZY4Ac8CDnxO$s*HWF`XUWOl~LMouKL)Sq^(LG9`F! zpZDPlLzMH#>fS+WYZ#`TuYBO&g<|X=wFffa&U-HWz;yuH2I`ei|9{m@BVHnF;Q4qw0JNx6OYKQt! zJ5W$A?fy71=4bZv`S=$nMi5o_bM(#qZ3WxCOzL(su~TTsf`qsggn`esPLo5rr?+Yr-wBvCAJIq;VNe}1Wng9;sG1iJS#=ZU zv;1Z`r#d09%;VxG%$)===tJp_V*8GUDQl?X_G&>zRzQvG% zM|1~HP?`BFnX+bRT0>d$my+@xHpiRW1-OQF9r=98h^sTk92o6tU%Qoe%K)mHdLeDr zp@iz?8v6sdX^_oMfY8INFh#WCx&nkYssPNvjDrZF?dT*)93GJU>L6K?goA(|lG<03 zcQ5@spJ)NuXP+aN6|64}PmV|?YCU5hF#Vei?DDM@@R~lodF%otAt)8Gbc`tgRx4}# zJG!|H-#a<&`h+oeW`zvy-rA!@&9WZX&riCN;+Zy-8YhhR3XnH*MI(-aQIHu3x~u9j zXAR(_o|GPdf&a&u3hQ_Axw5kYyDUv(m!0yziB;dc;Sz8DPulbD9kLV|CPswccJnX39wjk;v&MhESKwC$4f^*CDC?J=P4a_^lXZ*ScHvxVdepfqy{R zy7uBYYErydO1!l4{R}OO5%qGw##z#I8V_X}A1N<~D^N?=f zMBySgZzx-UI+obykgelhTv>1`DF{dQ!B{)|amu~{&sn5UFKl@qqEeL|MpD%tmFiX` zvaejF6jA+eds5j1r^@)+bD=g|Nalt<%1l;W>XzH9QEDHj#%*hpklj4AS-EfO)T-h- zsL#=l^*c3l?33CE?}0NMuBT0#q}8k}kz0fm8E<3`qVPaQdIrpS^ZJE^kw zPinSmB|GwgiyYC> zKXhgsw$!p0DP4NGS8pjrz*wp-=HBw&h+WXgye66jCawmY3W&Ix);#;>K%qzlY&O5M z2;jY;H*<62K#3(6OviMuCY->h-yCL-H;6P7vWU^CZbYuBeOXU!GwO1VeSiHlX8n8y z=?})tpKzkj>8KOqgwskh;WUQ41#7gaHU}X-hG;BEy_N7_e1h4O8P{}kWFj4pt$XY9 zg$?)Oj30-0=p0_NT)TEXTZwvV!A|JwX8kh$-DYO^ljRuw5<8Gh=;RJPyp;$pqSl^mdb z0d}2g%S}4=$`~

f>>Dr>}pMH28R$5_tXQXm}lDzEwtY1u9)iUtgp+5B50GSXM4|5(6vc7U_v~Lea;%RaN>!+llmS{ zH+|-jlCfde!|s601cyNe%1IDx6UwNz2_Qk-Ho@cvON2G5CBi_%{!c-ehSQkkMOa5W zZV|>e66lb6=9`@vM4kD!6R&aOJRN<65VrN)VU>eB**VpA3h=G&OMNoO?mAEGGKKc( z5>-e4jd6f$Dw|L{41KJXG2UJ*K6#OM~?1l#&g5&rX`seD1;UVolx6 zMBLkpIg;mx`*nKOujEs)QLI08VtrmST?wivN!3C+AnRabC0ldc+&BoAE`VmXB>_Wf zd;bHt@s@*{Rr0J@x3`kbUegBFCm_^SlGw8nI_#nA!dhvR_#V;ayTJ~hGe8H0X~Im& z3pZ5@BAW#g2geSLO;3*{ppEb2+D~=JKH$Uk-ywtJGFz<*fx93)2SkMEE$U*8ne*)=18E>|BbMZ&7pT z(g-%s{aZoUXj6RDOpJpYqP#|>#&6J%P97~sspD8~FrXBW-*f7nXEbs7{slb0i8F=z z_9z!Mo4`k$Qg4n9Fu&47g2Qn)UvyL+nkSaTg$JAYdojp>aj`3EU{Zx?~d_pw&`sn-ZO-?(VPDWV3^3?+z30 zk%#eZzxJY*T`yJ)a9H5NHyc$Ghc$KKfwQ3mawQ>-g=z08UaXS`xRVS!Hi-HP-FWw~ z_xGWBLL-Alh`nw)X_@#KbwW<46VhKZO?}amc=6$gbt8f|DPhKRf)bubk#*6oF`5v< z?G0N(N5sEa3~M1s6e7*#{)A$@vE|?5MdJ=rkTe(jB?Bst^O&##4kKT7GzhGE?cqKI zcmX>x=p{S7b}co{IYQs8VTt53wA-*z4H}Gt9YPp+u)XIf6!7Om~@}ei-4e}pRvwm5&?jVL^Bq!MH^nw02+@Q=ItwO4hWQ)aQdl0?;Zs% zUL|Z?|M=qVJ8(AX#GOrKP4~T1Cmf5;?i(1`nk5|ldD^v6>FI$_np5$una85~MucgP zW@ne+UM7&LX%@q?>j?6G@U;R``T_T_R-`4|!ijJ7h93MvhhxBWLW!bQb z@TRJ)#fl&532jP!>*H3cyq+p`X^^xj-i?i4Qn5!s>6+zj>(0@oKuZ3M?36a9Fs(rl z+NZ@E#<;bq!w$$o`}6Q{Z3Yl02VM4OT^49ll(I?ag>~#BS7DX)qa=5Ikl_qRVttSav1~xTo5)p^R;WRVaf4dYNby%% z=B-vs)_vkg19nVw!)+auH4@ih&U@v{E@(Fkn~K~}-tkSGotxGjFi7saVH)l9q#8Ed7AlY^etDCt&Yz^-E;3$n@lh}N!nmhacxM4TgSvL67I|{o$ zsF{0%CLR+gjwMkbRS+9MY|$C>daNEuW2G%MUn14G(ygJoOQfGEpbkC&uY?v^&?%c3`i$j7|$MO5hjXN)h={KjNdI_ao>ZP^(ZpN0JW_Q;Q#%vkS>tE$9DRv(K z==f{_L%GzD!MtZYZ18)U8rqKLLicSs2xDeEFMrBgV(a^_hjXbiC9<6q0SV(in#~e# zsKV2k@^%(y6N-n;B7gK-lw6q&G}X7IL7|~Ti3a!R9wYRN?GZW%CXXRKVs-O#a>%(= zMBmn}!o&S7T5MIQ{0c&4q?6;W7e4_0+%8xc_@fluWh_bi#=||6@uJjqDX00NHl_{6T~7VEl_E z(^;yZv=eXW+t3ac%r@rafU=udU+F?#8<4l5dFMX0f!6#CE6*0ffQ4@&%n(~>YM959 zh>4m9c~5ON05v3VkNm2|j}vGJY>qQN{HuWw1cs#3tz{Ph`du%b$&vM^N!6+AKpr)JUHJJBLWu+2O;f zRQ!>psQ*svlU0-`juDbLxExzo36B(KAqlR)U>B5Y@^*eK^DsvE%`cTUT!d=5RPN=M zN_5c%r&tVR5;(ykfo~q(6$Bp_0Vyh`D#s~{QcCwqZBt!OtM%*#%aJw4^@||o5~%!E z7Hi63yU_CUYbc&v$azw6~p z8$DM%-}s81fpbyQKNr8*T#NN~>D4MJ<=X~nQXcEg4$)=uu2O~mR1O5s3gSm*^&A>_ zY4EBJEZ`gJ>|S%suVbnw9*vVOe9D#wO{96}K^*=92865UG_FdJ(Mht|NDzzf0O2WT zX~2kAx3@zaU2{?eP%ikFSP%nurRrMuzuCids@=CkL^^r1d+hc4r?XqmBS)H>2Vy^$ z9Ot#n_odmax&uKYgBff!h8tj(Y@- z`-A;(VdCeXl4&vxddLjZHU{)s7rjVXj@8%>sEc8JtEs1qnd35}D+rtVh2QCL?(yTV zy0D#=B*u;&Lkp8f3`wHJ>)6vD!q-o3*zv{egY4;gVU#$kb2rygY`ZBp?`O8i$6Vf4v$3(dtWnygIN-fJ$?E# z^>?;^9})Bfzdxgn`}VQ@&r%k&B5JiDC3!)sRtw~D$`oOgF&cT<<|-ywg}!`A{m-3a zD__23OD|E64IC0XYWB>z?e*Fra371MQ@Wr8g;b4zvxyf=xt&4-YZ@FxN zxF;RAHpIlB9J+yggwsM#><)iB=%|Tb7gc2fcgN#bo61n}9-&%0iPp26y^CK@r{)wJ z)pKNqDSODAvlp+`zfhjeP}WRqJ{YSo~h@y&1}{@_%Q8W8n|*p zmb8jGEo6_bf4t5fEyQfzxpHOownZC1Zdi&d%f6)>K5kgNW$l$Kci5f93(5KV$E)PB z=z7P-0~$`6u6@DAO@tsDb)FAhi)NyHC&a$4IkG`TNt7#oO z^CFA=wneaQ+1F#_skquP7bdTna)HXvyTH~ztbLDdK0ga?37R`V$0o4L<*t2@ds$rj zT)yJk=YxB=N?tWw!k}6pdP!YafRrIKSC&_XLKidR_lMZib+kEZ$f#rzLut!;;A9a+l<^B(jdS1W$K^G!IjJTXdv9|RC&_-D zhW!Br47fW8ubP9!M1W?vZvxvv$f_Cke-nRn>zd$|2Tlb0HqY4c^Jo_xU%k0YJp%6N>t9;RMpQlKX8tF%^l z?M$Vq`?@fg7+i9OwcWOLL0MGegY)?9QQv+WwSBzv=xxz;YYp6H2(fkk@JNG1r~taE zj&m^BYSo~8{H(kGxw`H?rdPqa8m+r8x#`ocaUL{{TB}FEIGm~|m)-Sgn#*`Ecib({ zQ5p4)d67**SL1;-7eK^B2yVQ2$2jau*(aG!y=^lcj*X+0tUZ*q4;c8}qW zw`cdp)%cU9v%ZoJMiJP*_R!tc;*qWqP1_veR~7S^B^_p~MaVC4wD5EnRxg@@?l8h{ zpQXSLAB?*`OI>o$Q+n*63F(7l(}itg2M?M+G-1%-v0uIk8)yO^;Ki{M$l<-z>b>!Z z`iHg$$HxbUBqS&YLlY80gW}`0Gc8|~!j(gMsD&Enn z!;kFEkHgDV>YB7xNE&cuZ5^L)9V@-3;XfZ{2k+KA%T6DnuKhPf)*O6bc6)2ZnPf@X(| z+aEY96p3mZBVnz67rC(Cl!04Px=iS_bp-yb5AgJ8M*$w5?SQ#Mv>$02WKmr7Vz!fT z!10aKo2Dgi*^*qpMT`2{j|)>ruiDz8L7nfg1%}X{G)*ca>+~}<8Y8^(G=b=1n7NA} zFEdD9Du97?{ru{p7wyOO-_>c*V(Y5WsSD9VAbJ?4_As0GfQ$7XO0=TOn~BTvOyjsu zFaRh}GTXSEm+u4aW(pOHU4=0y!8gZ6SQ zYx@!*YQKCwP|xRs&zJ3cYkMv7-CEz%(q7QS<~3zZcl?7_>qe6=i!2BBTw8$?14+aRfYXxmpmi z-G8YCd6A|Na&S7{UuOXf(4l&CA z*V}c+MR9fQy)(PB3pR>~6-BBjNMBG?kRoEIi-4el3Wx%B?4n}B-ceBz5j(M0?7bu! z6MKm;<0{8bdO1%}+CZDeJG~+i`}fmbkC)T( z)9J9NokV)hjqSHMA|gLEXk3qlvg%yo31WM41pOGB*Q@u^n98h; zG4?&$Hq5&|0v)e^I}qVQsiUN8cEn@X?1&eG5hhy;$$TCBsn{_bk(Ck*2T5kZt_`C% zMt5Lhi`qkywHY%M_G@1*N4)tP5&;vet@PLf_$gT>->vzDc#cYp%=B+UNK8@Y@S}bg zoHoAV-jgf!twVY=u&e9gmf$&5as+noGsDrXlR)$F?8-;np{yRZ>%6$?1!J-U^OB7-ibCqGD zr+8F5ADJ+a3W2!jD-Z|@%w+!w!WkcU)(RN!lPAE=nfn_;DbQiVSNm!^&E2e7t zp%xaRTD+@e*82Kd*x<~kxdz*oj@%|u&83SA4^==5P#G_(Y2(im+A&Nip49r2E2>V1 zSIiSrXdCSgxzgZ4#Zk?2bW#kEeTc14&3wyNE_h0)r$s4PJSPFZqM-2Kh0c@97K>%d zZ2(w#oEB^(CNUZnYEJ3U1MEYGOoJ=(B}t2Mk$^i?bH@}7E63i2|Bo&)9+|Nw-an21 z=44m-E!w-9Oy=Dpbqp>y(TkL}O#!8>t;3KJ;QK=rOfoB$_71FLte|I%oyR{~O6`Rn zpR>%mlwSa`tcqubzTFHPw*yemyPzL9+PHBpkEYd!ak42lBKq6xp z1W13r`ygZhyIVnO?-l6)fngB~RXDe`D)^lIlTxKy&n2YK^s_bXB+ z-gvbY!)GPte*bDH&yL$9L3vmM6gXi4G4&9R$hsKL)zfcbQKDv+@j%R(HG>YiVCKkT z^RGs@hCX-}y(%2xk6j{;K8_?JP|M` zwlD>7bs{E&rNj{YWdCL+*}q8}I{61;v^ccN4$fmMnJ}vv7gYHlkZp66 z5+@$1@@8lfOa`e~qL>5C2Pnmpti zX02k?ycLWWg^HgaG#pztB^MIKA+nofjmlVV{;;liu!N@2P&$C-PfA`ZEb$wZ*V(LD zGqa9e{R;}}SvITZi|uVGT7D3YmO3X9QkD`R^jW(zmX(;fgXAQ485q~j4vjo9xqa&3 zHn!d$G)`#Fe#@MkIypU0|EI=C*gS|e2D$w5C4)vdFFKvS1SCJT{9t7^Oml84w4$#; zbIEoKvOU8YnFX<%?vPLqN7KsNLDq!0xW=vS>+I<2LdZ#l_MCGA>$C~JT_c;HYuV6c z@Th=6z0us;g#@}y z=8`}OvpZu;C8TLj$?gX*6OeBk9f~SRL5zCBy`(uAa!b*}#%V*pZSSY%(z`Dwy)DZi zYn8Jz$B&vaGA4Zn4dKQb^MQ2lhXySOH{#*y1wZGgGk1+O?b4Zf*RbQ;Fdi!Q$06EZ zlqQhq70|udOCG@xQrO0WxUr!xH!u_fTt%mOde<#$?DXJR&eBSqm(6a zn@D2-wZ-)5wxp%~8y@D3NSCF~W9wT;;ZO!PX47<}yMNFpwvL)u9l>u*H zl`3V&iqeSAv?T7%VfW^4E$h1eaysYE)JZQ2Qpd-J`gL|Naldgq_tan#=`0_sXNLtr z3f&_~nY*yFV5eccdnRofbeE)1>_m#B>YWJprJabAHG)9}HeLNxz-qya9~5>K?-VsF z-qD3uu8{Qaz5~4at5}!5*XHTc1!CB+K?ryiQG+W!WJ+N&bJ@SxP&^A0F==U9?1<9MwKy+BiK#X&~Cr|P6qxql34f7A3fz< zPLrTNx0RELx5#0-O#G;l{aI${`<{}-ju!~9oK0Ob&aI3}+?>iec8V+1qHDh?MAA-N4I2b=`KzZ58&ln6l}Inw`!gmoJd z^SaS)$4S_oUr6}DjqoW5C2iC<#9+~RI*;=7(nFH)=j0T!mjrg3y=Z6V+GU>o{Y!{! z{xtf>g#+}Wcv+(nrvAS55wu8u>}*M59CG%)UberN_@C^jBg#b3l@Rf;&dvDq?vaM%``ypiSmPL7`Pgye8g2!COyD zgZtUe`Fk^xl3Im%uX-^05F@8ocu-p?| zcZoR+NDAWW_sG;3sAWfF%hnB69gtLX1}p@-CgaJnebm;LZKs|vW3+>|s2;RFQaWh| zv1YO&NVI&R#VgYH#93PK)Z!UkeU=47s%5eIFd;|sE#S(T>_28Vcttqr6>)%=EQR2- z_PN~-`uNJQ#l-kPBxLJe%j6#y(x(@1(C^o*CJx_SCXE)+3&pEf6>ccV-$?pdCdNnh zH6=YK{I=!riy=Of&gX4=J&w?wrg6~;3C%EkK^lca^H`}$eqXDSDWgX~{zhHUQ%u{2 z4{+k7u~k)baNbcNw%WMy>zh0vap%v`4G)_*-)~*KWXskii?)&; zp4kJ_M>YU5GO6Q$uQ~ zI!{YX8L#G5jenx+&knyz>aDYXLK4rP!1hqycOF_$7wN zdAjzA{W|*M>hQB9UfX!=nUQhDnRCbNOw2gPsx6)}xQPkJ_B97tI~f*LKb-qajsKfU zDgM2IOC}e+(ZJ?g6*W`;TI%mb1^;e>ufR=r=$~uzZv$U@9eQ% z=BMl@#>>vr;_rXFLAL^07Ta?t@Is)PiXybN#QhDcUTraAtUPpco-F`Ov?(nhO^+NW zjaQBTc=-E1GsgjJ7&~~cIyidn(G-g8w`zHv9#L^1in3hk~pfXcF zPUAqIBMhP0!V1N0SX>~ACkf^ER(z~b2<$5dpK-`WCB1Me|L zuIn@L6mKmaxQBlZae<7ghzqXm^Kumz{8T)F%Oj8Q&oRzV`fdMRuKiPp1exS}Zl&@b zW4JPb3UlAq&O;}Mk`&@ht{rsx2&ut9Zhm;BHNpSlYFx3FN$UPWK9IWMH8VK@$Mbn{^y@a_a%J} zD1w_Q^U1|ETy~w9tV~b31Z?+Ry$_mPhi|AYl}^Q9oQc&s&_@z&h;l_%+NxD)St|;BoLl(#v~c#J=lbR5 z_8XiteV~)Kx08bxY(*H~LvF&G-~b0=dBt>$|2c~8ux&BJoot~!%|)6eiV*&)(inHf zD0@IJ44$BPOkSq##8i5LP~KIYy%|_s>emckrpvfAb(5vHkGYN#s43t?DkfQ;gF-FAsH^L`;?FkE`~G#+&@9n$YS9rnL;NtA!}(O%Tvtd zJ+-?(QU`jXbP#*n)C4@HH8e$Ui^``eE2}GvTwju{X~DN))q?S)e;a=8F_W-hZaC)SBoLy zFWRa}g#1{SXl{|(QoDq3EyeGN3v0oO4uT)e!0Ak9nxR8zJ{u)=Z15=TOwC0{|9-t9 zy<7sMkrL~j*eBM_xii5ST1c-K|F%&$gK-kjp%w-VLmxUA^)F7I7EWk5>ep59rsHcU z@eUhi-}fcRQ9U3(*MLh-ph*S;>m+STvUarc>rkg($sm03B@II}hJ{589V*uDH9R96%5YRZ zS=a)12@%-({!%PGMkOto#-Cv_pWhePGoZ@9UucAvbKu*sg|S_-M+dhKs=nDesOqM3 zpz?jq@VSu|EiBZYHGAbnI=gt0g|)-yhBvXd@boYY&5Lw#QOgwNPem2|sAWzvt6baZ zs5MqZt#-q3vI4Ku4X!rQEB_!f@6sRV#dHdYVbh6a=Noi& z5Pd*WI+G^!C*)WeB^rvyw2P|;87!BG2QfD{BWlfyBS+{E;@;zDI=Krd#|`N63#87J zxfe*-bO1)b2^L~QmFt)KWOgDD8&2H1bz(bMT6JzGSS-%SSTn7=j~e!z04dwk6oYws zmsimEGZDg4!u{krY*NRD0O@U(GK6NL%b>1s6dz$})nQ3`f<@)i1*CGp5^Au7{3$vuBEK%8CgjZ$T0@#72Gmnq z0{zB|ilt0msB{nwXjAFV%L*`T0FqgY2C{~@W2bsqK9 zmCGb)V}sC;8>T#uwRSf2QZZ_M?Yz(60pokAtM}(=jd>832PH8-}Ey z#fb||C@B8Zj}^(G^J8Z03CrsM8+F(|dfYL>>ZnO=5JFt~yi)O7%lRO8HADn?*yt28FAigyiE}aB$;Ddfp zViK8S`?A!H2t6y40oexTJZGuIb&2k|jsW_?Ou+rp9 zB{6v>Z2~9sZWuk-!9LqL8d?zxVYp~+n2#o9@*=2dOlKjnJ=p47gQB5K%021WY zlI@xY+PbottE7Z*}5axTCxB`p8O;*Hdsob&IPir)McW2{7a zrF0&{=3{QAQ=UlW*IasVj2gFS(lsi0&IlV_cHov6cI~+aXlP})Fj#anWP%pc8e;UT zzqH%46K$ze`a`a{ef=he^#W|OMiGPI>(}oW{ae@*zDQML@)YD!7iFk0$U?Q5!3|7p zbQa;7~FW8yqPxT&6C>(SvPOeTsr2-6EcI0mT^RYjx$MGr1TneCTt2xl|N1fOLsdhB(Z#` zbhhMVL#4wjbG-{pFHw)_={)X3MJDND`4rkaDAkKuS*H6Z99z?u8?DjEJ-hU3rr;3+ zsD^FQ;bprS+Z{C?@}@Lg-n2Yjc|(+Q8ur(U zl}HMICzs7f1r~iR8(k(i5)PmV{MB+1n%y0m0=k{~SI5&h^ob3)U)Udta11!yIYl0#unsdhZU~`!9=ePlHxu4!tj)mdvtRr_xk&A0xm7P;Wj$&L+MGiAOYz(~A zP;OHBx?h-ro~~O1$D99edbZB&oB5%uAtBV#%!XWu1(I?1tBD(57#cJ1#1cpiw{UP& zpFt$GcT~!euNxhY8#0S0v$i1`$M4;elDc&rI9P0Vfy6zqzE9#W%%^Y4M&##Q7;K$# ze)7o7ZCV+_f>{gb{Ng#`B>NZY(qE7wY_~R8k0hNi3|$#r-Fs;0*ip7NqsI@K3AQ?X zqFU~FP1?dm%;{H$>BIHhm_CC^YD{Db8O*7i`}ynNK5gwq(qJKAPLzzUzcBm5NvW_Q z-ICD9-%*uvoHS+(bQ=lA3gd6_pm1^1Xg8rmvz^*@Cq7(ru7TL0@@<%i~vpe%-69k(#%o0HvHG@qcnqP0p4E&b)<$xz| zY4y^nDlmVzS+q<(2l2wJNejN7ZDYrN5uVOjXO`OcbvCv!oSofypkrNYbMa@oZcJDM zbtC(UZaLffwE(%@-H zQTm@OOc2Z^{^q|Jn6$xxdUW)^7?!|Wi1vm2A!fbK#~B7e^#ozxfv7VOnymwT8$io3 zztWSi+HL=eeb1cW-f}#N7ZXSuy7MfFy4B)G5_yj9N;@{LMgNnNx$fYG*kbSx(>{|w zR6NtY0+zOrzSsb;!V8%8Lc)Im7F!w*BSpg|s~sne2YOZbnzSz{peKK&F=x1Z(kAub zDC>k{lSmKd^TPUjy6p^?PVDF{U>!iggj`0*{gEYePYY?jh}nYTf!7v)8Tl?H24y zpCQeR^+*%qr@2;8Fmran8sBaq9W>Y0ubVYnJc#ng=_c4`}Q;F(}k5jFfko4UdJ0DG>IVVq{B zyqYLAosG8T4&cM|!D3QE6{3H#qLG^L+mX#uy5{xWh z{&<_EhQH&ry17W}$UpOygZtAZ>R>MdSHefUfJvBudz+Qym4F0CK}3o?aOg`M!QViBw@T=4=if2C&OqRN#k$`{WV zHX9{|NgR%XAeN}E|>A;fkJDo zyUGu*|Kwiuv%}WsJ~}o1Nh7+Ej-p#8rlwAsoRT_G-gxpL<|Sj$WR)+CKyZ|A!ZWkN zfMnf?9F`FgmY$9mjKvEE;|0tI6r-Sh#sgDejnch9cTyD>k}0XF6A3~7Q^d{0jGU(< zHLoV64FZ-nc~IIU;SV@94eOPaRf`2;c^Z*1ECPPVArsFLkg!~d2@le zPr`>}D5JIoKiP?wJ|r<;SvjZOnt=E$LfUloCZt`@cKUxh_iUPyAAyS=K7{!6!0mu& z7&SJl-MWv%R%`2?zP2sRjcV4Y39WjQrubseKsCeACBWX&+Nh?ghG88`OSpG7H83%3 z0#D%xa1u=`R7=plOz<`*c1pEENi%cB#ks%EAR}{COCCapTj$}o4^a(QVTpDlaBBsU zdRVIAan+Uc8BR{pnZlB1Cusy$ITJ*!nWj%dSg5_qIY8I;jp71XbefaTkmM?IsZpH^ zv9znW%EQSoE!nM%dQKxwAjwGNQc#_XnWeH~Br)U7#D}FUm*+@QUV&ViAqhK~;6-Xn zP^pxa_2?6lHF*m8?Kr(#xSAz~E;-_dACRXD9*ELEV9FlAo0SzV$H?o+(_rQPg!b7| z_}K#-%r9z+#oB1B9VU1Oj&*M8&re|WG#A(r%%M@2-obN+n>42@XciZMGnlG##M*B; z5U9(A)tH8Dm~0nj$-pUV58a*xhr*f}?e5Ka4EOc6BrbJGv^+)`8Xfm^U(k>tz<)ko z5LRn{&^?K1W(V*>rv^dL0`ccr_k1;TSn^edZ*8?+m|_k>o|)i79s2^a!Zg+DR}(K=UrRREX-L3N5P8kDjx^At@fIUU<8?&P#F zkzpf}0$Kz#wr<{6S?oI}Yw+6MKYyG5!_3h7B~9XYET20eHmZN8nzeK4SpYVU5t8X# zWfo*A<|oQcg8yyp+$71!=F3ZsavwK;e!`+qa+mXTb$2Hl9+CYE`cC#Gdx*EIhnq5M z`iw4PgIl$6cdhyY1M3U)bnuB%IW`&3u)c4|vvI)XibfQFBhDvF{rp2a^pEiD z*F4Q;jgqFh>OsI{UkUx|*don4m4Sb>>44(o&j7p0kqu)8+BjyrhXXVAMlD|HYvBp8 z>Hkmbmuj(hH~D_#rruGTMvmSTb!w;Dze!+tSYW_cD$p{$ zssjDD4>cOLBPn@jx>3>XU7qo6je3s-xEDDF{a}TTo`M?4!T4gT7Y4@I>3}A)Dj@1J zh?V&wLs{)~8y6m@l)jWexYHBdDfB_tY?pJ`+m%!q?f zUhyU)yHVQkNe}6f|2?sCoH_W@Q3Z6hO_oJ!g?ebLUI3;GQl z)4oofz_Iq>0W}Rf_v+dsMA4&dn^`ta;lW(XU7~gB!Y;((-p1Onb{epAm|4o!q+JO; zJH;f9ime?z2CF*Nu)nm$fNSP@F*dC*Eo1pfvf{8VjhTM`ng#55^!{J(YYIgMR z+KZ6T@PI($d@|Z*moVtSqOFS#OwU!fi0Za3IJS1p!HenMmOW7GPQV?V4G&}I(L3pa z3s|DGQ>&RAud#i=ouylM_MCsp`e8olXn44CKLRZ7q!V|p-2D?k@VIf*4$2ZMAfB9r z7GpIvw$fNVc)$>02uk_*<)%TgUQoiKW(tWwyh&p^-&5M`+Qm)PT;km^QP-mw(MVTy z(q$8sxLF}F0nDu#du}(qzM5YD#{Dk$<9+2&&YL~==?`xjiyT@SjK)-#aD2!erb?a2 zi%g`2RMY1R9A6J67XZhfzmd9-YYfNdD=Lu>r!O%BW0gehf*fFGXe^#9q9#QTc;!Rk zcsK4@*Q)p9?M+b6XLw5IQ^Cp(GlYo(tOD>ejfo8>p!nYoQA^m8$OaI)k!db8B_e-b z4SCepebfUIx$+PpnS+4 zVT%)E6Z$*bx9~rI?)&dmGjkiZsGmDLBssXr_@Fi8}DFc={% z6A~Dv(?94n9atS85a_h7wjR0=esG`Ec0kW@!J8K?-+!0)@NsNIhx}wLo z;a+MlWk7@0W=86mpu{<|Q(Cw{XV=oCQR=|tsoh4k^LAoGj46*w4bL&D&=38|CZV}f zCyZ#s2L)4`sv9Y4HLdH?K^V>lO_`&TZ&C2htues;@Ze=MMb!Yu7$nT?ubUOViI>G~ zTo5%|Oy0~35pS{~NluHirg9^8?ZQ}ikL{z5!B|k9n@Wo`(>yUA!C&V;PHy$;nd)=} z`VHOASmU&414lZ~?%#W8yPCDy53}qJr+IY4I9)XCg#qK1`Hv>pl>(U z&e}aN?VHqkX`A|PONelf$?zG_u6AS=7eNK5R`SA{^4zH;0@Y~(CD5;5DuI;d(q_+IVNz=%`w;W0N8~^6U~HJk)ky)_!1HM*oOw)N02jTlQ-raB$o=PW)p zJNnX$m1J42)w%5?@Fuo#NiY?9Hb%O0hRkdXs zwr+l5JselO7d{44{QY~iPl)nPXr5xbVgyrM76}O#i{lp5X&V6LWJH?1L$+(Aq_o@x zUssi@Fj`75%&Ij&e7(UdUTT+ z=zFZ?^v%X(=TFvC=3l)_u!4%N!$^2X*Y3gg4XiR@0A|~(r)@)P^7otdZRzs|4-7Jz z`}S*M;bhUQeX>Q<)`yyR7|^t-Yhw$){*9Zo;@V;*<}3Cn;>7o8Js#(>W+;&>qy*E&)WL{wVp)GHM@(5+ut-u?c4Ep( zm8x6kkbsV=Lz+~1`~RSCktgt7EX{aL!yqVY7mF!7@^@8qN2;w2Y_#hv}M+DPVcEjz3YQ zEUsAnvh-K7>CrJ2l$c9L%qMr%UQvNn?R7=3I$nQ8r#kivNQIo8jxA><@~+~2GC36y zI`w={H~lxha>4S&9TsJ|4y--%6nC%xos)CYmp0B^)K(eK)rP~WM%+j7&tLHmQ*rnOu$8%^3ZEfPpQB9>(y%n>R8YF3hh8Dh z#&l&c2~@Z7C&a&vw?CN_9nC`o&Zq!I;PCI;;M9j)gB+%2l z6Nm~vo&jVM3H0z`#qjY6bZ0UPS+*P_=^kn8+Q;9pmA>U%SWB`NbpAUZ0rZ%8!a&+a zwM}OXtyqfnbCbB7Kck(iqsRO$!r~eFWAluy729@}Z%MbMKRD&TMTEuQ5n*w)xZJvT zxY{eCck|d?x(JJ4v_TWTRC`6oXbXgf5>refkAKcw`!5-73*x|N>*V~2(Z*1PFy9`Zx5RFm}VGhq1F+Dl^!}lY2!3gCm3DAKf z4xQNzl(iBNEwfg`dK{-c3kWt6fMv@cJe2or)-HPN=4SfxZ0ZVlvzhv`Ro{cb$VlM=5=DfC-Ng=*gGC1Jt{rZ+z)VukG*Yp{tW8@97&nBXQtr=87&14lkOaMc9~VAjz+Q zs(BoDtO!AKR67NNE?sY{gZ-NEuH=?nOL9Y#KA3Liyn>&BJh%iwPV!d~`Py|vzI+wc ztnrTZ92wCz)^h}(B41iwm+VofEBl>OB!76aoaV12WYtPaSFY5`mXiU_@d3$c(m&|b zPVh?7SYTx`(Ur*5fI~L`b<@9F();VJCCu#@)feGBO^D$H8V{z8pE^cO$nbEVzUu6* ztJo_LiDfwv){(1Cv5uS;_xcyzhDYUWF_nyyuPLwT)F*K85a)!Tfdgqi0tv|Ul{mWSwABl+o_aqkBQZWJE52_Qw=6Rue#D6Qgv=2;rwvd? zb`Ej0ciS~3*)gnVXiJ2rb?P#BaF?zF2XRbG9Xc@IKG|xhHw%I^3-3AmU%#i}7w>WERrQ`>KF$Brd!RiX zi%GiIU>;UW54=5iqDml{z_B*U7SMbh3VV`7G8U59iuddL0a6#^$OzDsaKqNvj#uy>FJZ zs&L*3nbM-7yiFAF2fq#rhqQ>0fznFSW#DJo>jM$~)9M9(2K-mnDo{m8tLuMToIbGk zVWlk-nsL*W?=V22%Ji@_HRA-f1mz3O$o|GO)D}rD39Yz=(m1F}VxmjZswxS*29)vs zs>M<+(m+EFWIR12l-ZK)jcKrqITrZ0B(nE{5eH9cpy|Nci%k+~mb`yw(%NBU{~YD` zsoBnbdRVZj0citilXYqRkQVA}OemuKgP1tA9NB-`7l~z$V8`DDjvr7-X7OZc{79Zc zW8{-bj}PyJBk0*DG)@u9-4m3GZ8BNKJq#*X+#4|2)JPh=f3?&n@QsWYBA|3LT|8-d z35EI?ru&=naKecAnT#S$<8`L{^eH62W2AlLg1!h?o%=K0N}J~ur6J_=mcLl_8)}m_ zC+9iGwHwzxZ%A=p2(d$`f#M;ASUZ$!CL443%&VA*L9tC6t{5kI>G&!)LFc$qx5X)% zi$?Sk_p9c_BsiU%I(X1zPLrLQHhF4FYPRymhe@hQ;tR$d2+hdQn80=e5kIrg?VGrv z%G2_@(jF`Y6_6Ha1iyfpvLxjyn-)dBUo`RF`P0dRHg#Q`K)9uu-<>@gmrw+ByP`)< zSbA6|Z(`aDTTxgiWcZougTTMF-wv=%kC6_nksi{S@J>pbV;LvBQ5-=dRIz5!r6AFOPH4&VpwI|!Dq1OS(2~j*ujTdNWVe*O!k5>a7wM|G zEwBHq@|k>8MWEtP#kyy>LNl9d=P3#m@p3z1wnEN7lHpT%id>PwYD9TT@f#Z;+IjF* zlfqKSzvX{oDTL!lflt*QTqC7$Tv3UXmyj}Bku0S=pm;B(SfxuTKcIhN74EYqO87^3 zB40a?8?5XrJuy@FR{2cj8~kzud8O%pIWB)9|yn?;vmrar|?g)Zex{<@TsH5ZYU^K#BX8H378$2rb{URB0amdGRVTcKDiKgd}y zh9{i!G31Q7!)DuHHSqa4^2ts3hR9}4{k?GglhJ&hxM65Q&t%ua?5=a&3p^ij77O3b zcPOOVMbqCE&bnDYYCz99aSjt!v}?O77@>P)yKoB0Q6{&y~Qb6i^DBRkML+5blW#4Jse%lemmc9Rp%b# zT@dGmbatXYHf-xa$jm?17Qf6UvB&AR*Xj6Ud@1!g@EvJ>X;pA|*4W5|n*%hFW47hA z-+s)x5$W8tVP_}*OBuXH(3$&b>z<66aDUM}@C;{BIW2&vtc7U3u{hyw#+u4Q`NPmT zHhli(SqwLY73Gik(PU%W1X~|7b#~B(Q_**6C~V6Pm6B5#gocvTE|Q-IbIAdOz}S`uEe2O3a(412 z-JMs@pT6Q{hoHXQ8a_Wj-W#pSo4)duZ$LsCcg{aQ=O*2A_#}zE|C&Var(H^l2F?vm z-#6?gaV^W z`I(+QIndT8#Jzj0hrCwh*~cGeq#Dse$yIac-+Uo@va|k6UPJLQ!YJ>-%7VA z)@jZsEe@rR0>>2|OXer|N4a>lX-ywp*}3)l{MVJ^NiX7fWZ}<-ZF<Gm(3xt2P3hgK2orR1 z8nH4VHH=BKX^6e$KAqk-7B-OX`yMS{@zVidP~RMQx^&r3duSm%byKs-t@uA(88-tLTc@sKOl8YYmr8_@`|Oha`rFPsBU=$0d;2D&0 zsrTT;C`b@EExGO7dn?%Da%cdt&JJ=uM_{WKZ;KIh8Fkymow(`Bds z(-oH55;zD72OCr1u1jEnxAw8(won&XQqW}vHe_ml5xPJO5x7vou94oLpA-@C4TKUN z2${?fE^)DFG-hre0p)?^ttUt0yTpse0w*WZih>q21xf^WCv2=Uyb6A$-7I0<+n?kX zJb($Y(_QjTg6w!I;~mdoF4EDE1U-mQT9Bqn+uk=m+p%B!Uae7Rn06AC7}#oTd(W zO+WtFYW}UU5Lq|Fc@;Z`vBq_kbz$T8|4>e1$?Mv>E^BM_i8VZWvrZsxN(m!lYYz(v zAwvZ{J(Xwa8aauVs&#_SBN-r^ZhoDsp@nGQ-lQUz8^)?NWK7;IVmS7;W+CY(r|WgC zfH+0-;m9f)tw3T)KV8z1Lq_M_q96YEEdb9WrMdJG+KfKTr={=Uz`-v10U1pA+2T8? zwQ|y46XlPN^KOsDEWZKTa+0bMliL5Cq<@oas&jhJw&UHS&Tcxg$C$>LoglI0#F*q< zyGSR}Uw@~oFL7)3mM$X=PHjI!>MuTCdXD%WehHHPk6YUb&2(*CZ*tXuP5gT*sVS$=8ry&^)C* z`8;`0xux)+a=&Ic*~vFV4(Bi#GceG3@ITyTogqXaBk*!ut)->J+6q+EY7+~bRoNQO zAZ~WQ+K_}WFF2njsZLdTjue$~M*E4$vZ?glCGiDUk2E7izdWT;^se}Z*vFMj>6&$I zF1=1>?W=)2o&c0SofIz7(4M|vH-@;mKCALrmS*Xwr@ zaKSQeXI|#PGa;8|)7#I}Z_~$lq{U^Ej=^Drx?V0j9=5L(GWm}(SmzPfwDME z;xR0WN#Z}&?klz6QMIpDSvQ$k*F&alE|+>;?q;cE{h*U|84;9CZoNH^o13W==SRZo z*)z5EN&`!!CHZDQFhKc&6tU0T1GHAw7%tLGkHzmKI77X z)-i2dquXq6LMKdqR1&|6nC&HS0T@w~8#?^^`K0OJoB9#$^@bbrMXS;uzS8pa@Efv& zG|eGR$d0#Hj?o==b05(IrB7G~azg^v%m|yZCH%5^x1M3a9ZjfVz>@Oa>CcJn>`NAL z`<7>xmVuKjyC!bqLkx=0YhW=O<6LP>t`TGU+k28j$I*OJTd5(n-_mLCD`>7vS#Aft zb6-iPt%P4hV<-k2NLGhvS01WuxPY*-gS#806%VwWpoMvaPeFGuHNF%`1>Is+ew0fP z3zprx$7_G5-)7aPtAD@RD68H>x|sawK}WgX+r9n8+7tKM2fgqORM`}gmrOTJ%{wD% zZfqd^9Z?gA7Qor$9Z_6Fv0C z)m3UEwHvT}MwuQ*mj&MA?(Qrj5gSKGFO0rM+Nj(4)@#tgt4$lU$R4_an5sG!77_o6csr2__)fIXzkINQ{ zA5rbuO_cjba@ltMu)#c%)`$^)h_u{gOD|YVe@wqoc7GeS=wN?`#0U?q_T$fjtJYrm5+OUh>muQchpAN=rx zCS7ds=@)J2r$@F|c<6qp*e9;mZh})0sak-vK<>BT>EpfH{jqaI1*b&I(mL4o>A%B*%a5{)sYF+6MKauWF=cdJ$E{ytB9nl9x zixgLCS7^hS?Cx)7qtn6Zywub)A1I!SU)z1j!ExS)+kaDDl)k5rR`%4VhM{+Y*RarK z3AGHRcb*sgwNu#{w3kOcIhb&g2Yo#oABLlAiK>6{H;I`AnNfB}+U>3=3Ys=Ou=Tc^ zwBb{eMm1{GXx_G~{m_VIeHAqf6ei~IHqnm;YlF0|XxAIs^B-bynrTLZHA?Bu_u3Wu zJ0bLi)&ps7;!Zru0lZf@1j|exY;0`hb%)b;=6xISwQvHJ&hzl72%RU+YwTy#)R>up zfAOZJTl-B*y5RIo2})V?KC{F*t`CGDNUvw~s@ zmMsl)Pl@)#vGP0FcZktrF3oeHm%^q0Nc$cNz^BC?3M#au&-JH(*;O$U90> ze$}-tyJj|6vOXHHw`7sjNabNL4wF{e(V53@f>jJQD?~9#)8tovC4Cu17jRALb4|%9 z%}464`BQcEPd`J;xYsnNqOsx$6J-Ibg@|eLd4`MSw&2Jy?6Nz73pljvGO;09r)5^i zvOu;+A;jk@aSzu8@175cokiWP0m?rP$S|a2&e!2?-4u@ZrP$e6Ek6a|Wjbd2Q}jkL zfx_A=tw;BvZF+SxZ`q(#OkqgsqL>z&v88*`?c+(U+x&hF3qiE)~=blaoOx9E}Q#T`gU z!$Q*VLg$R5nNb@-nu`)nh%XJ`G@NZbepSA5UM}WjiuxCEk73f5IR#PwH2_j#dy17-X!FZNQHw&;bn8VHSwC>;T~bvV|lmV(nle6!sv! z+(w|sXS#f}I~T(E7isW$dF}>rE@X~*$IhZ&lV->*D(}NX|0;4-EPROVXQ{_SaXtSS zXEfqZY7M8AKTx`2OnRY$gM3g^Q_6SO81nBCpbY;UMPP!XsN+f;8mk~n7Bkpd7yQeQ zjR^K<=>hEB60rwSLZBmGFQIU}ViMtLBwXkACaJ8T2wIm)W3yQ?r^}mEJ|OE*%xk)0 zEBi@|fhmK@3y~&sULS&jO_SSjy*fO~p zKZ_fws~yQCa;l+u#UK&AtaQ+`E pjVyK_kxo9cQ-2UU^-Gbv4@;@jIY%kA?mpt8$aVJzk)QDU{{UBU-YozC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..874b1b0dd7c63f46240530a710ccd503d58d866d GIT binary patch literal 317184 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
!x2YkZQFYkfXG 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

9^l#G`%67Bvu2q&xtL(J0Ho|mhT}g zR?MZ3PSb%8-3R!L9Pl5aI`-uHbv8SnZzdr>7hNH}Ph5IKBaxH`3v}cg68e3MzS|dO zoj4T=VCLHE;dklX>BQ<%y`WybM@C#a_I>X|TOpud$0VkxoGHv+ofQKxiSrfnFp>YU zdY@E7ztmXssN3JxS)Rh2F8-m=$(Mv+>BZBAv7Q0dv;F) zy&=I;oj@MdH56?KfXAL}eE1Og(Qb%Oj<^r-&Khv^rxQ`jTSQjjrM>n+9fsG zFCYE zW^|eEc*{bQm&gM7dnY=@`FC<){nJ&)V6E=t`_$WyXzVtSm&}Asn|48p-?(4|Pbs8D z-21iSrs9<|rhHXg9OBohD=g=pLK@8m%af_i_EOX(J<*JSu>3)kkr_%KoG3lq0Pq8h z(I(N%s7KuMT?a|@+VKO*V(++m`gt2Q@rLSjhwP$jYBeG2*Aqup5^c>-YZ?yQGBms> z#>ppmpo3BS{yt%@rK?Ey!b0q0>ya_cOhPVX^&PgrR0Ixk<+17F4}Pk)>{ED87iben z`bpaSlyK%-YJO7n1zn^~LFR#%#V!S(>8)lO>C^&dfn$3s{RVgV| z7a)In2;RC9O(4t%iV&05k_{N@noS-!1oNKFwxx%icM~6SR})z67oX57Zr7qj6hI+( zt77DvdV2urXliN+6Ef*b?vYNro#|oA887Kvb=T^|#k=~pN{IJ^RGlU`(mQI3SPD0E z!!!#aZYhL;LV?B;XKq5?UI=d26ybqxkop=r5M{CSs1U8y*AV}fR1Xs#Xd~nwg7AYj zLbUk!i_Y=mE!S#-Xk{MVKrQI;-A*LfZpITb0-B*XaOuHB zs{sk#tX^V?s*ikAXO@v3DHU5Iri8LF33l2;PvF)KV4P>an4Xk)aOpt8d$Rx%??rL0 zE>D-nMEI^Iw$g?KWZ$3BuuMzD196L^R~?-cawhzs?(4C;`@en~SifGtQ2%LBiv~4h z1|)@eQYg?>FlDs2M^hz;b5byWQ<=vPYTZ`SEG1#}pu(!r2`=A-AJTmtkw31d%k~G< z;%VbnpVg~x7ZmMO+-J#PRlRzuCe0DI!iW0mn(Nx3WiRV?y^BNZ-=V&>$D}h?bb0zS z0d%>p1FpG>GyPDGuVIG02bWG?Y;TgHfavpZu7T>t?3|zh!v-RmhoQGgdW4;IBj!a8 zWsUvJf0I-gZf@r2Xy#XZZBZ**V|x#Wu3PAt=P&4)O`Yvs?VH%Os<=_?=iJG;b@z!R z!8szaga4Q@g;OJw4$`2wki5iBh74sY|LC~XJ>K8!AvBU_)j%c+vXWRl|Jl=*i`CfoB^lQJ{O;B zpK5Nt@ia7#d_}*kNl`6j5-l*ccrioy$goUlr|M~m7pQs?jBK>*Vv#4i5M2x7x-;9V zkb`u+xRHy-yOFX<^%9L<4?Y4=m6@d(S0L;=QsqGemlfuZIf9w>x#Kx{YBRY859=P? z)7<0w7+D&%AG|s&V|iMKZS*ARN2iY?hS&HbT9zG=PA6mB(z=s*NohPsLM>Vaj@vep z8eXGsS+Fu4LWP#$?Mj)Ig?8lC4APNM2&VKYRX{_TR3eXRcv(A=y@m&y_MUcT&T&$I z<~(|zCev5#zb75Hv>lN=bex+K0S-03Vu$8SNs~)s5713_B5u(=PuKa8ETalyav~ya z!_df8AlI1---(+Hy2-s5R2O?^*Fy4s;Zj!lGV@DKGj)2@Zd?Izv9#0tXKz5R894{_>{f-oRz7`iKo6~m0fCP|wxm0(B zg_)Z)e86nwI|vG9+TDD3EplLMH}Aqf%f!LlGHgmS6pDHpJ%Y*d8r#S@$%5FJV1E)} z#8OV@2Dk`?H2G^Gg?%;Sk;K~+ZfA}p50P!pk}#z4c4j3VIZj{D=^Q5&bfTBOpnNpl zLArArWyfH-*ocsG^xuoER4m3)4Q@9^3FC${pn}Z}W5UZKunI-zm(ZW6CDH*Wy`Vpr zk$BEqr>I@Ut);J6E<{17uHmD(?bS8hIQoz@uSChO0m_BJ-rx?}g9+6v-HQ}8J()e% zQr{%T_{2AWhB(mh^ym3x0O$GkAo-1!DVseeg0>%f={$P-hH@a^gWImJUpi@w=sycs zz)3+N_Rwx8gGh7w5Thtc9I7gZQg5e(x`|OVTCq(UMNzu@s&Z*`vGWKL)0#m5Q1cB| cOBu$Y0O0CO4lx2goX^8kk_-LLr0@LKGF{DGDhK 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

1I|svCvU|R`iQp*F3zg>=ixMX$T~z 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�dQ42l5tk(k!kqW$Z9oaaPl9Brl>h&Mw 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

-!3)&^3_);e$#d3!A`}>2A0`eCa`SL&sov0 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ŹD@)<7ltOtn$SSm;LMTrPEA?sR43d= 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�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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5e4851f0ab7e0268da6ce903306e2f871ee19821 GIT binary patch literal 310252 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 z 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%0D1lG~DE2ff7BeoTnFxKM!4172W=IjHdV9zf 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 literal 0 HcmV?d00001 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..c39f412c3 --- /dev/null +++ b/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml @@ -0,0 +1,209 @@ +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..ca68f289c 100644 --- a/ui/qml/reskin/layout_framework/XsPanelDivider.qml +++ b/ui/qml/reskin/layout_framework/XsPanelDivider.qml @@ -1,58 +1,76 @@ 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..182064234 100644 --- a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml +++ b/ui/qml/reskin/layout_framework/XsPanelSplitter.qml @@ -5,64 +5,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 +93,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 +102,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 +127,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..b927efe98 100644 --- a/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml +++ b/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml @@ -7,40 +7,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..4040573cb 100644 --- a/ui/qml/reskin/layout_framework/XsViewContainer.qml +++ b/ui/qml/reskin/layout_framework/XsViewContainer.qml @@ -5,165 +5,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..e69078816 --- /dev/null +++ b/ui/qml/reskin/session_data/XsMediaListModelData.qml @@ -0,0 +1,45 @@ +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..46e22f0c1 --- /dev/null +++ b/ui/qml/reskin/session_data/XsPlaylistsModelData.qml @@ -0,0 +1,9 @@ +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..3981eaf40 --- /dev/null +++ b/ui/qml/reskin/session_data/XsSessionData.qml @@ -0,0 +1,205 @@ +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..5a37861db --- /dev/null +++ b/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml @@ -0,0 +1,37 @@ +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..93565f586 100644 --- a/ui/qml/reskin/views/viewport/XsViewport.qml +++ b/ui/qml/reskin/views/viewport/XsViewport.qml @@ -1,24 +1,150 @@ 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..a5967d051 --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportActionBar.qml @@ -0,0 +1,195 @@ +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..bf9e23c8b --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml @@ -0,0 +1,138 @@ +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..39aab5e3f --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportToolBar.qml @@ -0,0 +1,121 @@ +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..2b0ed1d33 --- /dev/null +++ b/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml @@ -0,0 +1,230 @@ +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..0133f3059 --- /dev/null +++ b/ui/qml/reskin/widgets/dialogs/XsPopup.qml @@ -0,0 +1,33 @@ +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..299c8a237 100644 --- a/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml +++ b/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml @@ -8,11 +8,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 +30,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 +271,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..5509e692a 100644 --- a/ui/qml/reskin/widgets/menus/XsMenu.qml +++ b/ui/qml/reskin/widgets/menus/XsMenu.qml @@ -6,38 +6,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 +41,8 @@ Popup { delegate: chooser DelegateChooser { - id: chooser - role: "menu_item_type" + role: "menu_item_type" DelegateChoice { roleValue: "button" @@ -72,7 +57,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 +77,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/XsMenuDivider.qml b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml index 4bc60ef28..dbf7dd310 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml @@ -3,9 +3,11 @@ 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..bb07c9180 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuItem.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuItem.qml @@ -10,7 +10,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 +20,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 +177,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 +200,6 @@ Item { } } - Component.onCompleted: { make_submenus() } @@ -166,26 +227,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..f8806a126 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml @@ -13,27 +13,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/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..9d4c2c76e 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: { 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/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..97ef3db2b 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml @@ -42,7 +42,7 @@ DelegateChoice { Component.onCompleted: { // grab children - control.DelegateModel.model.srcModel.get(modelIndex(), "childrenRole") + control.DelegateModel.model.srcModel.fetchMore(modelIndex()) } function modelIndex() { @@ -87,7 +87,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 +149,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..fdf4772cc 100644 --- a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml +++ b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml @@ -41,7 +41,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 +97,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 +159,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..4e33954a2 --- /dev/null +++ b/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml @@ -0,0 +1,37 @@ +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..9f4ef5655 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 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/XsSourceToolbarButton.qml b/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml index fd886ad2c..eff998a32 100644 --- a/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml +++ b/ui/qml/xstudio/widgets/XsSourceToolbarButton.qml @@ -5,6 +5,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 +16,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 +28,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 +94,7 @@ XsToolbarItem { } color: XsStyle.mainBackground radius: XsStyle.menuRadius - - XsModuleAttributesModel { - id: image_source - attributesGroupNames: "image_source" - } - + ColumnLayout { id: imageColumn @@ -199,11 +217,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/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 From 93ec1ea42d3fbb14f2f167e4bc21116c395764fc Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Tue, 2 Apr 2024 15:53:00 +0100 Subject: [PATCH 09/42] Adding missing license stub text Signed-off-by: Michael Kessler --- CHANGELOG.md | 9 +++++++++ scripts/linting/tidy_message_handlers | 2 +- src/conform/src/conformer.cpp | 1 + .../annotations/src/annotation_render_data.hpp | 1 + src/ui/qml/helper/src/model_helper_ui.cpp | 1 + src/ui/viewport/src/viewport.cpp | 1 + src/utility/src/CMakeLists.txt | 8 +------- src/utility/test/managed_dir_test.cpp | 1 + ui/qml/reskin/layout_framework/XsLayoutModeBar.qml | 1 + ui/qml/reskin/layout_framework/XsPanelDivider.qml | 1 + ui/qml/reskin/layout_framework/XsPanelSplitter.qml | 1 + ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml | 1 + ui/qml/reskin/layout_framework/XsViewContainer.qml | 1 + ui/qml/reskin/session_data/XsMediaListModelData.qml | 1 + ui/qml/reskin/session_data/XsPlaylistsModelData.qml | 1 + ui/qml/reskin/session_data/XsSessionData.qml | 1 + ui/qml/reskin/views/timeline/XsTimelineMenu.qml | 1 + ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml | 1 + ui/qml/reskin/views/viewport/XsViewport.qml | 1 + ui/qml/reskin/views/viewport/XsViewportActionBar.qml | 1 + ui/qml/reskin/views/viewport/XsViewportInfoBar.qml | 1 + ui/qml/reskin/views/viewport/XsViewportToolBar.qml | 1 + ui/qml/reskin/views/viewport/XsViewportTransportBar.qml | 1 + ui/qml/reskin/widgets/dialogs/XsPopup.qml | 1 + ui/qml/reskin/widgets/menus/XsMainMenuBar.qml | 1 + ui/qml/reskin/widgets/menus/XsMenu.qml | 1 + ui/qml/reskin/widgets/menus/XsMenuChoice.qml | 1 + ui/qml/reskin/widgets/menus/XsMenuDivider.qml | 1 + ui/qml/reskin/widgets/menus/XsMenuItem.qml | 1 + ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml | 1 + .../widgets/menus/XsMenuItemToggleWithSettings.qml | 1 + ui/qml/reskin/widgets/menus/XsMenuMultiChoice.qml | 1 + .../panels/playlist/delegates/XsDelegateChoiceSubset.qml | 1 + .../playlist/delegates/XsDelegateChoiceTimeline.qml | 1 + ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml | 1 + ui/qml/xstudio/widgets/XsMediaInfoBarAutoAlign.qml | 1 + ui/qml/xstudio/widgets/XsSourceToolbarButton.qml | 1 + 37 files changed, 45 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1378917..584ba8795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,10 @@ + + + + + + + + + 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/src/conform/src/conformer.cpp b/src/conform/src/conformer.cpp index 74816f7f7..19ef82ebd 100644 --- a/src/conform/src/conformer.cpp +++ b/src/conform/src/conformer.cpp @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #include "xstudio/conform/conformer.hpp" using namespace xstudio; diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp index 27fcd4e28..ef2c8448a 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #pragma once #include "xstudio/utility/blind_data.hpp" diff --git a/src/ui/qml/helper/src/model_helper_ui.cpp b/src/ui/qml/helper/src/model_helper_ui.cpp index 2d86f4072..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" diff --git a/src/ui/viewport/src/viewport.cpp b/src/ui/viewport/src/viewport.cpp index c6665e9b7..68151da6c 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 diff --git a/src/utility/src/CMakeLists.txt b/src/utility/src/CMakeLists.txt index ea10d06a4..aecf5158d 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -3,6 +3,7 @@ find_package(fmt REQUIRED) find_package(Imath REQUIRED) find_package(nlohmann_json REQUIRED) find_package(PkgConfig REQUIRED) +find_package(ZLIB REQUIRED) pkg_search_module(UUID REQUIRED uuid) SET(LINK_DEPS @@ -28,11 +29,4 @@ SET(STATIC_LINK_DEPS ZLIB::ZLIB ) -find_package(ZLIB REQUIRED) -find_package(spdlog REQUIRED) -find_package(fmt REQUIRED) -find_package(nlohmann_json REQUIRED) -find_package(PkgConfig REQUIRED) -pkg_search_module(UUID REQUIRED uuid) - create_component_static(utility 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file diff --git a/src/utility/test/managed_dir_test.cpp b/src/utility/test/managed_dir_test.cpp index 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/layout_framework/XsLayoutModeBar.qml b/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml index c39f412c3..bb34ed520 100644 --- a/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml +++ b/ui/qml/reskin/layout_framework/XsLayoutModeBar.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 diff --git a/ui/qml/reskin/layout_framework/XsPanelDivider.qml b/ui/qml/reskin/layout_framework/XsPanelDivider.qml index ca68f289c..1a3f76e3f 100644 --- a/ui/qml/reskin/layout_framework/XsPanelDivider.qml +++ b/ui/qml/reskin/layout_framework/XsPanelDivider.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xStudioReskin 1.0 diff --git a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml b/ui/qml/reskin/layout_framework/XsPanelSplitter.qml index 182064234..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 diff --git a/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml b/ui/qml/reskin/layout_framework/XsPanelsMenuButton.qml index b927efe98..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 diff --git a/ui/qml/reskin/layout_framework/XsViewContainer.qml b/ui/qml/reskin/layout_framework/XsViewContainer.qml index 4040573cb..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 diff --git a/ui/qml/reskin/session_data/XsMediaListModelData.qml b/ui/qml/reskin/session_data/XsMediaListModelData.qml index e69078816..b3b9c2d26 100644 --- a/ui/qml/reskin/session_data/XsMediaListModelData.qml +++ b/ui/qml/reskin/session_data/XsMediaListModelData.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xstudio.qml.session 1.0 diff --git a/ui/qml/reskin/session_data/XsPlaylistsModelData.qml b/ui/qml/reskin/session_data/XsPlaylistsModelData.qml index 46e22f0c1..ebce36425 100644 --- a/ui/qml/reskin/session_data/XsPlaylistsModelData.qml +++ b/ui/qml/reskin/session_data/XsPlaylistsModelData.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xstudio.qml.session 1.0 diff --git a/ui/qml/reskin/session_data/XsSessionData.qml b/ui/qml/reskin/session_data/XsSessionData.qml index 3981eaf40..ad7252a2a 100644 --- a/ui/qml/reskin/session_data/XsSessionData.qml +++ b/ui/qml/reskin/session_data/XsSessionData.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xstudio.qml.session 1.0 diff --git a/ui/qml/reskin/views/timeline/XsTimelineMenu.qml b/ui/qml/reskin/views/timeline/XsTimelineMenu.qml index 810a28009..c1846b0e8 100644 --- a/ui/qml/reskin/views/timeline/XsTimelineMenu.qml +++ b/ui/qml/reskin/views/timeline/XsTimelineMenu.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import xstudio.qml.models 1.0 import xStudioReskin 1.0 diff --git a/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml b/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml index 5a37861db..9ec2a8452 100644 --- a/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml +++ b/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Shapes 1.12 import xStudioReskin 1.0 diff --git a/ui/qml/reskin/views/viewport/XsViewport.qml b/ui/qml/reskin/views/viewport/XsViewport.qml index 93565f586..a4a6a55e8 100644 --- a/ui/qml/reskin/views/viewport/XsViewport.qml +++ b/ui/qml/reskin/views/viewport/XsViewport.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Layouts 1.15 diff --git a/ui/qml/reskin/views/viewport/XsViewportActionBar.qml b/ui/qml/reskin/views/viewport/XsViewportActionBar.qml index a5967d051..bcd5a473b 100644 --- a/ui/qml/reskin/views/viewport/XsViewportActionBar.qml +++ b/ui/qml/reskin/views/viewport/XsViewportActionBar.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Layouts 1.15 // import QtQml.Models 2.14 diff --git a/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml b/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml index bf9e23c8b..6a8523b7c 100644 --- a/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml +++ b/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Layouts 1.15 // import QtQml.Models 2.14 diff --git a/ui/qml/reskin/views/viewport/XsViewportToolBar.qml b/ui/qml/reskin/views/viewport/XsViewportToolBar.qml index 39aab5e3f..d58b161ed 100644 --- a/ui/qml/reskin/views/viewport/XsViewportToolBar.qml +++ b/ui/qml/reskin/views/viewport/XsViewportToolBar.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Layouts 1.15 import Qt.labs.qmlmodels 1.0 diff --git a/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml b/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml index 2b0ed1d33..772694618 100644 --- a/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml +++ b/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQml.Models 2.14 diff --git a/ui/qml/reskin/widgets/dialogs/XsPopup.qml b/ui/qml/reskin/widgets/dialogs/XsPopup.qml index 0133f3059..0e4f24b45 100644 --- a/ui/qml/reskin/widgets/dialogs/XsPopup.qml +++ b/ui/qml/reskin/widgets/dialogs/XsPopup.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Controls 2.15 diff --git a/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml b/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml index 299c8a237..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 diff --git a/ui/qml/reskin/widgets/menus/XsMenu.qml b/ui/qml/reskin/widgets/menus/XsMenu.qml index 5509e692a..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 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 dbf7dd310..901350266 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import xStudioReskin 1.0 diff --git a/ui/qml/reskin/widgets/menus/XsMenuItem.qml b/ui/qml/reskin/widgets/menus/XsMenuItem.qml index bb07c9180..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 diff --git a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml b/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml index f8806a126..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 diff --git a/ui/qml/reskin/widgets/menus/XsMenuItemToggleWithSettings.qml b/ui/qml/reskin/widgets/menus/XsMenuItemToggleWithSettings.qml index 24ce08333..2b457d8cf 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuItemToggleWithSettings.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuItemToggleWithSettings.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/XsMenuMultiChoice.qml b/ui/qml/reskin/widgets/menus/XsMenuMultiChoice.qml index 8606117bd..bc171e134 100644 --- a/ui/qml/reskin/widgets/menus/XsMenuMultiChoice.qml +++ b/ui/qml/reskin/widgets/menus/XsMenuMultiChoice.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/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceSubset.qml index 97ef3db2b..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 diff --git a/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml b/ui/qml/xstudio/panels/playlist/delegates/XsDelegateChoiceTimeline.qml index fdf4772cc..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 diff --git a/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml b/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml index 4e33954a2..612ac109c 100644 --- a/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml +++ b/ui/qml/xstudio/panels/timeline/widgets/XsDragBoth.qml @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 import QtQuick 2.12 import QtQuick.Shapes 1.12 import xStudio 1.0 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 eff998a32..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 From 44469557899cb60b51407fd8579a80ac6f54e46d Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Mon, 6 May 2024 20:43:11 -0700 Subject: [PATCH 10/42] Current Progress of Windows Port Signed-off-by: Michael Kessler --- .gitignore | 1 + CHANGELOG.md | 9 - CMakeLists.txt | 86 ++- CMakePresets.json | 18 + README.md | 13 +- cmake/macros.cmake | 57 +- cmake/modules/FindDbgHelp.cmake | 19 + cmake/modules/FindOpenEXR.cmake | 4 +- cmake/modules/FindSphinx.cmake | 5 +- cmake/modules/vcpkg.cmake | 615 ++++++++++++++++++ .../media/images/Qt5_select_components.png | Bin 0 -> 93322 bytes docs/build_guides/media/images/setup_Qt5.png | Bin 0 -> 47701 bytes .../media/images/setup_ffmpeg.png | Bin 0 -> 94335 bytes docs/build_guides/windows.md | 67 ++ extern/include/QuickFuture | 2 +- extern/quickfuture/CMakeLists.txt | 15 + extern/quickpromise/CMakeLists.txt | 73 ++- include/xstudio/atoms.hpp | 2 +- include/xstudio/audio/audio_output_device.hpp | 2 +- include/xstudio/bookmark/bookmark.hpp | 4 + include/xstudio/event/event.hpp | 2 +- include/xstudio/media_reader/buffer.hpp | 20 +- include/xstudio/media_reader/enums.hpp | 1 + include/xstudio/media_reader/image_buffer.hpp | 26 +- .../xstudio/module/attribute_role_data.hpp | 2 +- .../xstudio/shotgun_client/shotgun_client.hpp | 8 +- include/xstudio/ui/keyboard.hpp | 132 ++++ include/xstudio/ui/qml/actor_object.hpp | 7 +- include/xstudio/ui/qml/bookmark_model_ui.hpp | 50 +- include/xstudio/ui/qml/embedded_python_ui.hpp | 48 +- include/xstudio/ui/qml/event_ui.hpp | 49 +- .../xstudio/ui/qml/global_store_model_ui.hpp | 46 +- include/xstudio/ui/qml/helper_ui.hpp | 95 ++- include/xstudio/ui/qml/hotkey_ui.hpp | 50 +- include/xstudio/ui/qml/json_tree_model_ui.hpp | 47 +- include/xstudio/ui/qml/log_ui.hpp | 47 +- include/xstudio/ui/qml/model_data_ui.hpp | 52 +- include/xstudio/ui/qml/module_menu_ui.hpp | 44 +- include/xstudio/ui/qml/module_ui.hpp | 48 +- include/xstudio/ui/qml/qml_viewport.hpp | 46 +- include/xstudio/ui/qml/session_model_ui.hpp | 43 +- include/xstudio/utility/caf_helpers.hpp | 5 +- include/xstudio/utility/chrono.hpp | 15 +- include/xstudio/utility/exports.hpp | 2 +- include/xstudio/utility/helpers.hpp | 104 ++- include/xstudio/utility/json_store.hpp | 6 +- include/xstudio/utility/lock_file.hpp | 71 +- include/xstudio/utility/logging.hpp | 5 + .../xstudio/utility/remote_session_file.hpp | 6 + include/xstudio/utility/sequence.hpp | 11 +- include/xstudio/utility/string_helpers.hpp | 22 +- python/CMakeLists.txt | 3 + scripts/qt_install/CMakeLists.txt | 2 + scripts/setup/setup_dev_env.ps1 | 30 + share/fonts/CMakeLists.txt | 13 +- share/preference/CMakeLists.txt | 13 +- share/preference/core_session.json | 4 +- share/preference/core_thumbnail.json | 4 +- share/snippets/CMakeLists.txt | 13 +- src/audio/src/CMakeLists.txt | 41 +- src/audio/src/audio_output.cpp | 78 ++- src/audio/src/audio_output_actor.cpp | 8 + src/audio/src/linux_audio_output_device.cpp | 2 +- src/audio/src/linux_audio_output_device.hpp | 2 +- src/audio/src/windows_audio_output_device.cpp | 332 ++++++++++ src/audio/src/windows_audio_output_device.hpp | 59 ++ src/broadcast/src/CMakeLists.txt | 1 - src/demos/glx_minimal_demo/src/CMakeLists.txt | 4 +- src/demos/glx_minimal_demo/src/main.cpp | 4 +- src/embedded_python/src/CMakeLists.txt | 20 +- src/embedded_python/src/embedded_python.cpp | 2 +- .../src/embedded_python_actor.cpp | 21 +- src/global/src/CMakeLists.txt | 9 +- src/global_store/src/CMakeLists.txt | 7 +- src/launch/xstudio/src/CMakeLists.txt | 57 +- src/launch/xstudio/src/xstudio.bat.in | 26 + src/launch/xstudio/src/xstudio.cpp | 56 +- src/media/src/media_actor.cpp | 16 +- src/media/src/media_source_actor.cpp | 2 +- src/media_cache/src/media_cache_actor.cpp | 5 +- src/media_hook/src/CMakeLists.txt | 7 +- src/media_metadata/src/CMakeLists.txt | 8 +- src/media_metadata/src/media_metadata.cpp | 2 + src/media_reader/src/CMakeLists.txt | 8 +- src/media_reader/src/media_reader.cpp | 2 + src/playlist/src/playlist_actor.cpp | 15 + .../colour_pipeline/ocio/src/CMakeLists.txt | 2 +- src/plugin/colour_pipeline/ocio/src/ocio.cpp | 4 +- .../dneg/shotgun/src/qml/CMakeLists.txt | 7 +- .../hud/exr_data_window/src/CMakeLists.txt | 1 + .../hud/image_boundary/src/CMakeLists.txt | 1 + src/plugin/hud/pixel_probe/src/CMakeLists.txt | 1 + .../hud/pixel_probe/src/qml/CMakeLists.txt | 4 + .../media_hook/dneg/dnhook/src/CMakeLists.txt | 1 + .../ffprobe/src/ffprobe_lib.cpp | 8 +- .../media_metadata/openexr/src/openexr.cpp | 2 +- .../media_reader/ffmpeg/src/CMakeLists.txt | 18 +- src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp | 26 +- src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp | 1 + .../ffmpeg/src/ffmpeg_decoder.cpp | 10 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 74 ++- .../media_reader/openexr/src/openexr.cpp | 4 +- .../openexr/src/simple_exr_sampler.hpp | 1 + src/plugin/utility/dneg/dnrun/src/dnrun.cpp | 2 + .../annotations/src/qml/CMakeLists.txt | 4 + .../src/basic_viewport_masking.cpp | 2 +- .../src/qml/CMakeLists.txt | 4 + src/plugin_manager/src/CMakeLists.txt | 6 +- src/plugin_manager/src/plugin_manager.cpp | 77 ++- src/pyside2_module/src/CMakeLists.txt | 8 +- src/python_module/src/CMakeLists.txt | 13 +- src/python_module/src/py_atoms.cpp | 2 + src/python_module/src/py_context.cpp | 4 +- src/python_module/src/py_link.cpp | 2 + src/python_module/src/py_messages.cpp | 4 +- src/python_module/src/py_playhead.cpp | 2 + src/python_module/src/py_plugin.cpp | 2 + .../src/py_remote_session_file.cpp | 2 + src/python_module/src/py_types.cpp | 2 + src/python_module/src/py_ui.cpp | 2 + src/python_module/src/py_utility.cpp | 2 + src/python_module/src/py_xstudio.cpp | 2 + src/scanner/src/scanner_actor.cpp | 14 +- src/session/src/session_actor.cpp | 9 + .../src/shotgun_client_actor.cpp | 71 +- .../src/thumbnail_disk_cache_actor.cpp | 9 +- src/timeline/src/CMakeLists.txt | 18 +- src/timeline/src/item.cpp | 35 +- src/timeline/src/timeline_actor.cpp | 75 ++- src/ui/base/src/CMakeLists.txt | 14 +- src/ui/base/src/keyboard.cpp | 132 ---- src/ui/model_data/src/CMakeLists.txt | 2 + src/ui/model_data/src/model_data_actor.cpp | 2 +- src/ui/opengl/src/CMakeLists.txt | 12 +- src/ui/opengl/src/gl_debug_utils.cpp | 4 + src/ui/opengl/src/texture.cpp | 52 +- src/ui/qml/CMakeLists.txt | 7 +- src/ui/qml/bookmark/src/bookmark_model_ui.cpp | 4 +- src/ui/qml/embedded_python/src/CMakeLists.txt | 1 + .../src/embedded_python_ui.cpp | 2 +- src/ui/qml/helper/src/CMakeLists.txt | 1 + src/ui/qml/helper/src/helper_ui.cpp | 4 +- src/ui/qml/helper/src/model_data_ui.cpp | 7 + src/ui/qml/module/src/module_menu_ui.cpp | 4 +- src/ui/qml/module/src/module_ui.cpp | 42 +- src/ui/qml/playhead/src/playhead_ui.cpp | 2 +- src/ui/qml/quickfuture/src/CMakeLists.txt | 1 + src/ui/qml/quickfuture/src/QuickFuture | 2 +- src/ui/qml/quickfuture/src/qffuture.h | 77 ++- src/ui/qml/quickfuture/src/qfvariantwrapper.h | 311 ++++++++- src/ui/qml/quickfuture/src/qmldir | 2 +- src/ui/qml/quickfuture/src/quickfuture.h | 34 +- .../qml/quickfuture/src/quickfuture.qmltypes | 2 +- src/ui/qml/session/src/CMakeLists.txt | 1 + src/ui/qml/session/src/session_model_ui.cpp | 16 +- src/ui/qml/studio/src/CMakeLists.txt | 1 + src/ui/qml/tag/src/CMakeLists.txt | 1 + src/ui/qml/tag/src/tag_ui.cpp | 2 +- src/ui/qml/viewport/src/CMakeLists.txt | 1 + src/ui/qt/CMakeLists.txt | 6 +- src/ui/qt/viewport_widget/src/CMakeLists.txt | 5 +- .../src/offscreen_viewport.cpp | 4 + src/ui/viewport/src/fps_monitor.cpp | 2 +- src/utility/src/CMakeLists.txt | 17 +- src/utility/src/chrono.cpp | 4 + src/utility/src/edit_list.cpp | 4 + src/utility/src/frame_list.cpp | 13 +- src/utility/src/frame_rate_and_duration.cpp | 8 +- src/utility/src/helpers.cpp | 74 ++- src/utility/src/remote_session_file.cpp | 10 +- src/utility/src/sequence.cpp | 57 +- ui/qml/xstudio/base/dialogs/XsWindow.qml | 2 +- .../xstudio/dialogs/XsOpenSessionDialog.qml | 3 +- ui/qml/xstudio/extern/QuickPromise | 2 +- ui/qml/xstudio/qml.qrc | 4 +- vcpkg.json | 25 + 176 files changed, 3944 insertions(+), 555 deletions(-) create mode 100644 CMakePresets.json create mode 100644 cmake/modules/FindDbgHelp.cmake create mode 100644 cmake/modules/vcpkg.cmake create mode 100644 docs/build_guides/media/images/Qt5_select_components.png create mode 100644 docs/build_guides/media/images/setup_Qt5.png create mode 100644 docs/build_guides/media/images/setup_ffmpeg.png create mode 100644 docs/build_guides/windows.md create mode 100644 extern/quickfuture/CMakeLists.txt create mode 100644 scripts/qt_install/CMakeLists.txt create mode 100644 scripts/setup/setup_dev_env.ps1 create mode 100644 src/audio/src/windows_audio_output_device.cpp create mode 100644 src/audio/src/windows_audio_output_device.hpp create mode 100644 src/launch/xstudio/src/xstudio.bat.in create mode 100644 vcpkg.json 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/CHANGELOG.md b/CHANGELOG.md index 584ba8795..8b1378917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1 @@ - - - - - - - - - diff --git a/CMakeLists.txt b/CMakeLists.txt index 73d64905f..67082d727 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,21 @@ 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.27) + option(BUILD_TESTING "Build tests" OFF) option(INSTALL_PYTHON_MODULE "Install python module" ON) option(INSTALL_XSTUDIO "Install xstudio" ON) @@ -15,6 +26,7 @@ option(FORCE_COLORED_OUTPUT "Always produce ANSI-colored output (GNU/Clang only) option(OPTIMIZE_FOR_NATIVE "Build with -march=native" OFF) option(BUILD_RESKIN "Build xstudio reskin binary" ON) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") set(STUDIO_PLUGINS "" CACHE STRING "Enable compilation of SITE plugins") @@ -30,7 +42,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 +64,26 @@ 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) +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) @@ -123,7 +145,53 @@ if(ENABLE_CLANG_TIDY) endif() -find_package(nlohmann_json REQUIRED) +if(WIN32) + ADD_DEFINITIONS(-DNOMINMAX) +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(CMAKE_CXX_STANDARD 20) + add_compile_options(/permissive-) + set(VCPKG_INTEGRATION ON) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS 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 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") + # enable UUID System Generator + add_definitions(-DUUID_SYSTEM_GENERATOR=ON) + # build quickpromise + add_subdirectory("extern/quickpromise") + add_subdirectory("extern/quickfuture") + + # 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() + +find_package(nlohmann_json CONFIG REQUIRED) include(CTest) if(ENABLE_CLANG_FORMAT) @@ -187,3 +255,9 @@ if(INSTALL_XSTUDIO) 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..08666d6eb --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,18 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "windows", + "generator": "Visual Studio 16 2019", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", + "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", + "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", + "BUILD_DOCS": "OFF" + } + } + ] +} diff --git a/README.md b/README.md index 697073e0b..5f6563ffc 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,21 @@ 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 +## Building xSTUDIO for MS Windows -This release of xSTUDIO can be built on various Linux flavours. MacOS and Windows compatibility is not available yet but this work is on the roadmap for 2023. +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). -We provide comprehensive build steps for 3 of the most popular Linux distributions. +This release of xSTUDIO can be built on various Linux flavours. MacOS compatibility is not available yet but this work is on the roadmap for 2023. + +We provide comprehensive build steps for 4 of the most popular Linux distributions. * [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) +* [Windows](docs/build_guides/windows.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 MacOS + +MacOS compatibility is not yet available but it is due in Q3 or Q4 2023. Watch this space! diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 4da1f3807..b9397a2eb 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -6,11 +6,15 @@ macro(default_compile_options name) # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> - PRIVATE $<$:-Wno-unused-function> + PRIVATE $<$: + $<$:-Wno-unused-function> + $<$:/wd4100> + > # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> - PRIVATE $<$:-Wextra> - PRIVATE $<$:-Wpedantic> + $<$:PRIVATE $<$:-Wno-unused-function>> + $<$:PRIVATE $<$:-Wextra>> + $<$:PRIVATE $<$:-Wpedantic>> # PRIVATE ${GTEST_CFLAGS} ) @@ -21,7 +25,9 @@ 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}\" PUBLIC PROJECT_VERSION=\"${PROJECT_VERSION}\" @@ -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 $<$:-Wextra>> + $<$:PRIVATE $<$:-Wpedantic>> PRIVATE ${GTEST_CFLAGS} ) target_compile_features(${name} - PUBLIC cxx_std_17 + PUBLIC cxx_std_20 ) target_compile_definitions(${name} @@ -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} @@ -106,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} @@ -236,6 +242,17 @@ macro(create_plugin_with_alias NAME ALIASNAME VERSION DEPS) PUBLIC ${DEPS} ) + if(WIN32) #TODO: Determine if we need to keep this limited to win32. + # We don't want the vcpkg install. + + #This will unfortunately also install the plugin in the /bin directory. TODO: Figure out how to omit the plugin itself. + install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin ) + + + _install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin) + endif() + + set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) endmacro() @@ -261,6 +278,10 @@ macro(create_component_with_alias NAME ALIASNAME VERSION DEPS) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + # Generate export header + include(GenerateExportHeader) + generate_export_header(${PROJECT_NAME}) + endmacro() macro(create_component_static NAME VERSION DEPS STATICDEPS) @@ -367,6 +388,15 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + # 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 + ) + + # Generate export header + include(GenerateExportHeader) + generate_export_header(${PROJECT_NAME}) + endmacro() macro(build_studio_plugins STUDIO) @@ -389,3 +419,10 @@ macro(build_studio_plugins STUDIO) endmacro() +macro(set_python_to_proper_build_type) + if(WIN32) + target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:Py_DEBUG>") + endif() +endmacro() + + 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 0000000000000000000000000000000000000000..c6c81078cefe4d5432abab3b0370873bfbadb66d GIT binary patch literal 93322 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

}i3DzpKCHi8aCA89i0he4~md@N~8=b!si#4Byim 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 Hiuiv37Vo2? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71e1d6aae844f7bae8c6867485512137cae28157 GIT binary patch literal 47701 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 E3oNgi-~a#s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a7fb0cf39bee454a1d455e6e963e16cbb0a16021 GIT binary patch literal 94335 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 KqvX16$o~M)!FFW; literal 0 HcmV?d00001 diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md new file mode 100644 index 000000000..7ca42da35 --- /dev/null +++ b/docs/build_guides/windows.md @@ -0,0 +1,67 @@ +# Build XStudio on Windows + +## Install basic tools +--- + +To build XStudio on Windows, you need to install some basic tools. The Windows build uses VCPKG to install third-party dependencies. Follow these steps to set up the development environment: + +1. First, ensure that you are using an [administrative](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/) shell. + +2. Install MS Visual Studio 2019 (Community Edition is fine) [Visual Studio Legacy Installs](https://visualstudio.microsoft.com/vs/older-downloads/). + +3. Restart your machine after Visual Studio finishes installing + +4. Execute the script `setup_dev_env.ps1` located in the [script/setup](/scripts/setup/setup_dev_env.ps1) folder. + +## Install Qt 5.15 +--- + +To install Qt 5.15, follow these steps: + +1. [Download](https://www.qt.io/download-qt-installer-oss?hsCtaTracking=99d9dd4f-5681-48d2-b096-470725510d34%7C074ddad0-fdef-4e53-8aa8-5e8a876d6ab4) the Qt Windows installer. +2. Execute the installer. +3. During the installation, select the components as shown in the following picture: + + ![Qt Components](/docs/build_guides/media/images/Qt5_select_components.png) + +4. The installation may take some time. You can grab a cup of coffee while it completes. +5. Note that you will need a valid username and password to download Qt. The installer allows you to add or remove components in the future. + +## Install FFMPEG +--- +**Important**: The current supported version of FFMPEG is 5.1. + +There are two options to install FFMPEG: + +1. Using VCPKG. +2. Using a prebuilt binary. + +### Install FFMPEG with a prebuilt binary +You can locate prebuilt FFMPEG binaries that fit your licensing criteria [here](https://ffmpeg.org/download.html#build-windows). + +Download and extract the archive to your local machine. + +## Build XStudio using CMake GUI and Visual Studio 2019 +--- + +To build XStudio using CMake GUI, follow these steps: + +1. Configure the CMakePresets.json to your appropriate paths. + ![Qt5 CMake location](/docs/build_guides/media/images/setup_Qt5.png) + ![FFMPEG ROOT](/docs/build_guides/media/images/setup_ffmpeg.png) +2. Launch CMake-GUI. +3. Select the source root path in CMake-GUI +4. Select the "windows" preset +5. Select the build location to be in your source root path in a folder called build +6. Click on the "Configure" button. +7. Define Visual Studio 2019 and select the X64 architecture. +8. Click on the "Generate" button. +9. Click on the "Open Project" button. +10. Choose your Build Type (Debug/Release) +11. Select the INSTALL target and build. + +# Known Caveats + +* Python interpreter is not set up properly +* Audio does not work and may have lots of debugging messages still in-tact. +* Some tools or plugins may not be fully functional. \ No newline at end of file diff --git a/extern/include/QuickFuture b/extern/include/QuickFuture index a936bb7ab..f93058bea 120000 --- a/extern/include/QuickFuture +++ b/extern/include/QuickFuture @@ -1 +1 @@ -../quickfuture/src/QuickFuture \ No newline at end of file +#include "../quickfuture/src/QuickFuture" \ No newline at end of file diff --git a/extern/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt new file mode 100644 index 000000000..28c206b75 --- /dev/null +++ b/extern/quickfuture/CMakeLists.txt @@ -0,0 +1,15 @@ +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 STATIC ${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) \ No newline at end of file diff --git a/extern/quickpromise/CMakeLists.txt b/extern/quickpromise/CMakeLists.txt index 1e7e5b9eb..9333d7d31 100644 --- a/extern/quickpromise/CMakeLists.txt +++ b/extern/quickpromise/CMakeLists.txt @@ -1,24 +1,61 @@ -# -# 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}) + +find_package(Qt5 COMPONENTS Core Quick REQUIRED) -set(HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qppromise.h - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/qptimer.h - ${CMAKE_CURRENT_SOURCE_DIR}/cpp/QuickPromise +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) +else() + install(TARGETS quickpromise DESTINATION ${INSTALL_ROOT}) +endif() + +if(${STATIC}) +# +else() + install(FILES ${QML_FILES} DESTINATION ${QML_INSTALL_DIR}) +endif() + diff --git a/include/xstudio/atoms.hpp b/include/xstudio/atoms.hpp index 7d99bc5bc..04de419cd 100644 --- a/include/xstudio/atoms.hpp +++ b/include/xstudio/atoms.hpp @@ -133,7 +133,7 @@ namespace ui { class PointerEvent; struct Signature; namespace viewport { - struct GPUShader; + class GPUShader; typedef std::shared_ptr GPUShaderPtr; } // namespace viewport diff --git a/include/xstudio/audio/audio_output_device.hpp b/include/xstudio/audio/audio_output_device.hpp index d19f4b50f..42c36970d 100644 --- a/include/xstudio/audio/audio_output_device.hpp +++ b/include/xstudio/audio/audio_output_device.hpp @@ -59,7 +59,7 @@ class AudioOutputDevice { * block while the soundcard consumes samples, depending on the implementation of * the subclass. */ - virtual void push_samples(const void *sample_data, const long num_samples) = 0; + virtual void push_samples(const void *sample_data, const long num_samples, int channel_count) = 0; /** * @brief Query the audio pipeline delay from the last sample in the soundcard diff --git a/include/xstudio/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index 74a6843ca..e4450a201 100644 --- a/include/xstudio/bookmark/bookmark.hpp +++ b/include/xstudio/bookmark/bookmark.hpp @@ -237,7 +237,11 @@ namespace bookmark { std::string created() const { +#ifdef _WIN32 + auto dt = (created_ ? *created_ : std::chrono::high_resolution_clock::now()); +#elif auto dt = (created_ ? *created_ : std::chrono::system_clock::now()); +#endif return utility::to_string(dt); } 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/media_reader/buffer.hpp b/include/xstudio/media_reader/buffer.hpp index ee9387ce3..e93b6d794 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" @@ -62,11 +63,18 @@ namespace media_reader { error_state_ = HAS_ERROR; } - 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 BufferData { + struct BufferDeleter { + void operator()(byte *ptr) const { operator delete[](ptr); } + }; + + 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 +88,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 458840e85..cc7d6349c 100644 --- a/include/xstudio/media_reader/image_buffer.hpp +++ b/include/xstudio/media_reader/image_buffer.hpp @@ -124,9 +124,27 @@ namespace media_reader { ~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_; } @@ -195,4 +213,4 @@ namespace media_reader { }; } // namespace media_reader -} // namespace xstudio \ No newline at end of file +} // namespace xstudio 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/shotgun_client/shotgun_client.hpp b/include/xstudio/shotgun_client/shotgun_client.hpp index 717d5f30b..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; 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/qml/actor_object.hpp b/include/xstudio/ui/qml/actor_object.hpp index 94fd04baf..6a3e3fe94 100644 --- a/include/xstudio/ui/qml/actor_object.hpp +++ b/include/xstudio/ui/qml/actor_object.hpp @@ -54,7 +54,12 @@ 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 a27c8cf6f..5cfc20452 100644 --- a/include/xstudio/ui/qml/bookmark_model_ui.hpp +++ b/include/xstudio/ui/qml/bookmark_model_ui.hpp @@ -1,6 +1,50 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#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 */ + + #include #include @@ -20,7 +64,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 +88,7 @@ class BookmarkCategoryModel : public JSONTreeModel { }; -class BookmarkFilterModel : public QSortFilterProxyModel { +class BOOKMARK_QML_EXPORT BookmarkFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY( @@ -93,7 +137,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 diff --git a/include/xstudio/ui/qml/embedded_python_ui.hpp b/include/xstudio/ui/qml/embedded_python_ui.hpp index c1860e685..bc01c477c 100644 --- a/include/xstudio/ui/qml/embedded_python_ui.hpp +++ b/include/xstudio/ui/qml/embedded_python_ui.hpp @@ -1,6 +1,50 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#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 */ + + #include #include @@ -86,7 +130,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 +185,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..daec9bba4 100644 --- a/include/xstudio/ui/qml/event_ui.hpp +++ b/include/xstudio/ui/qml/event_ui.hpp @@ -1,6 +1,49 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#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 */ + #include #include @@ -18,7 +61,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 +116,7 @@ namespace ui { event::Event event_; }; - class EventAttrs : public QQmlPropertyMap { + class EVENT_QML_EXPORT EventAttrs : public QQmlPropertyMap { Q_OBJECT @@ -83,7 +126,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..3f1bc4c8b 100644 --- a/include/xstudio/ui/qml/global_store_model_ui.hpp +++ b/include/xstudio/ui/qml/global_store_model_ui.hpp @@ -1,6 +1,50 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#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 */ + + #include #include "xstudio/ui/qml/json_tree_model_ui.hpp" @@ -18,7 +62,7 @@ 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 24e072140..34a018f09 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include #include #include @@ -77,7 +119,42 @@ namespace ui { int count_{0}; }; - 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) @@ -119,7 +196,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) @@ -158,7 +235,7 @@ namespace ui { }; - class ModelPropertyMap : public QObject { + class HELPER_QML_EXPORT ModelPropertyMap : public QObject { Q_OBJECT Q_PROPERTY(QQmlPropertyMap *values READ values NOTIFY valuesChanged) @@ -193,7 +270,7 @@ namespace ui { QQmlPropertyMap *values_{nullptr}; }; - class ModelNestedPropertyMap : public ModelPropertyMap { + class HELPER_QML_EXPORT ModelNestedPropertyMap : public ModelPropertyMap { Q_OBJECT public: @@ -226,7 +303,7 @@ 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: @@ -475,7 +552,7 @@ namespace ui { QQmlEngine *engine_; }; - class CursorPosProvider : public QObject { + class HELPER_QML_EXPORT CursorPosProvider : public QObject { Q_OBJECT public: @@ -485,7 +562,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) @@ -527,7 +604,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) @@ -573,7 +650,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 diff --git a/include/xstudio/ui/qml/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index cf15804ae..6e4b7a715 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include #include @@ -21,7 +63,7 @@ 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 +105,7 @@ namespace ui { }; - class HotkeyUI : public QMLActor { + class VIEWPORT_QML_EXPORT HotkeyUI : public QMLActor { Q_OBJECT @@ -149,7 +191,7 @@ namespace ui { utility::Uuid hotkey_uuid_; }; - class HotkeyReferenceUI : public QMLActor { + class VIEWPORT_QML_EXPORT HotkeyReferenceUI : public QMLActor { Q_OBJECT @@ -180,4 +222,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 d3f7538cb..3b95dcfc0 100644 --- a/include/xstudio/ui/qml/json_tree_model_ui.hpp +++ b/include/xstudio/ui/qml/json_tree_model_ui.hpp @@ -1,5 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + +#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 */ + #include #include #include @@ -15,7 +58,7 @@ CAF_POP_WARNINGS 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) @@ -174,7 +217,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..09f116fb1 100644 --- a/include/xstudio/ui/qml/log_ui.hpp +++ b/include/xstudio/ui/qml/log_ui.hpp @@ -3,6 +3,49 @@ #include + +#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 */ + #include "spdlog/common.h" #include "spdlog/details/log_msg.h" #include "spdlog/details/synchronous_factory.h" @@ -26,7 +69,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 +113,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 21d974c10..37f066374 100644 --- a/include/xstudio/ui/qml/model_data_ui.hpp +++ b/include/xstudio/ui/qml/model_data_ui.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include #include "xstudio/ui/qml/helper_ui.hpp" @@ -17,7 +59,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 @@ -84,7 +126,7 @@ class UIModelData : public caf::mixin::actor_object { bool foobarred_ = {false}; }; -class MenusModelData : public UIModelData { +class HELPER_QML_EXPORT MenusModelData : public UIModelData { Q_OBJECT @@ -92,7 +134,7 @@ class MenusModelData : public UIModelData { explicit MenusModelData(QObject *parent = nullptr); }; -class ViewsModelData : public UIModelData { +class HELPER_QML_EXPORT ViewsModelData : public UIModelData { Q_OBJECT @@ -110,7 +152,7 @@ class ViewsModelData : public UIModelData { QVariant view_qml_source(QString view_name); }; -class ReskinPanelsModel : public UIModelData { +class HELPER_QML_EXPORT ReskinPanelsModel : public UIModelData { Q_OBJECT @@ -130,7 +172,7 @@ class MediaListColumnsModel : public UIModelData { explicit MediaListColumnsModel(QObject *parent = nullptr); }; -class MenuModelItem : public caf::mixin::actor_object { +class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object { Q_OBJECT diff --git a/include/xstudio/ui/qml/module_menu_ui.hpp b/include/xstudio/ui/qml/module_menu_ui.hpp index b2c0a31a1..7e49b9be8 100644 --- a/include/xstudio/ui/qml/module_menu_ui.hpp +++ b/include/xstudio/ui/qml/module_menu_ui.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include #include @@ -21,7 +63,7 @@ namespace ui { class ModuleAttrsToQMLShim; - class ModuleMenusModel : public QAbstractListModel { + class MODULE_QML_EXPORT ModuleMenusModel : public QAbstractListModel { Q_OBJECT diff --git a/include/xstudio/ui/qml/module_ui.hpp b/include/xstudio/ui/qml/module_ui.hpp index 34dd14ca9..5db91e310 100644 --- a/include/xstudio/ui/qml/module_ui.hpp +++ b/include/xstudio/ui/qml/module_ui.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include #include @@ -21,7 +63,7 @@ namespace xstudio { namespace ui { namespace qml { - class ModuleAttrsDirect : public QQmlPropertyMap { + class MODULE_QML_EXPORT ModuleAttrsDirect : public QQmlPropertyMap { Q_OBJECT @@ -70,7 +112,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 +131,7 @@ namespace ui { }; - class ModuleAttrsModel : public QAbstractListModel { + class MODULE_QML_EXPORT ModuleAttrsModel : public QAbstractListModel { Q_OBJECT diff --git a/include/xstudio/ui/qml/qml_viewport.hpp b/include/xstudio/ui/qml/qml_viewport.hpp index 4b0d97992..396f4ae83 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -1,6 +1,48 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ + #include CAF_PUSH_WARNINGS @@ -29,7 +71,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) @@ -176,4 +218,4 @@ namespace ui { } // namespace qml } // namespace ui -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index 2939b8524..198444343 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -1,5 +1,46 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#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 */ #include @@ -22,7 +63,7 @@ CAF_POP_WARNINGS 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 diff --git a/include/xstudio/utility/caf_helpers.hpp b/include/xstudio/utility/caf_helpers.hpp index ca85d7ea2..075c65fee 100644 --- a/include/xstudio/utility/caf_helpers.hpp +++ b/include/xstudio/utility/caf_helpers.hpp @@ -30,9 +30,12 @@ namespace utility { struct absolute_receive_timeout { public: 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; diff --git a/include/xstudio/utility/chrono.hpp b/include/xstudio/utility/chrono.hpp index bcc8b1947..66934cfc0 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -17,16 +17,27 @@ namespace utility { using time_point = clock::time_point; using milliseconds = std::chrono::milliseconds; - using sysclock = std::chrono::system_clock; +#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); @@ -83,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/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/helpers.hpp b/include/xstudio/utility/helpers.hpp index cb18c404f..5e50fbb5a 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -23,6 +23,10 @@ #include "xstudio/caf_error.hpp" #include "xstudio/caf_utility/caf_setup.hpp" +#ifdef _WIN32 +#include +#endif + namespace xstudio { namespace utility { @@ -254,31 +258,87 @@ namespace utility { inline std::string xstudio_root(const std::string &append_path) { auto root = get_env("XSTUDIO_ROOT"); + + #ifdef _WIN32 + char filename[MAX_PATH]; + DWORD nSize = _countof(filename); + DWORD result = GetModuleFileNameA(NULL, filename, nSize); + + std::string fallback_root; + if (result == 0) { + spdlog::debug("Unable to determine executable path from Windows API, falling back " + "to standard methods"); + } else { + spdlog::warn(std::string(filename)); + 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 + fallback_root = std::string(BINARY_DIR); + #endif + + std::string path = - (root ? (*root) + append_path : std::string(BINARY_DIR) + append_path); - return path; - } + (root ? (*root) + append_path : fallback_root + append_path); - inline std::string remote_session_path() { - auto root = get_env("HOME"); - std::string path = (root ? (*root) + "/.config/DNEG/xstudio/sessions" : ""); return path; } - 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; +inline std::string remote_session_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 = "") { + 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; + } } - 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; + return path.string(); +} + +inline std::string snippets_path(const std::string &append_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) { return preference_path(to_lower(context) + ".json"); } @@ -299,7 +359,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; } @@ -311,7 +371,11 @@ namespace utility { inline bool is_file_supported(const caf::uri &uri) { fs::path p(uri_to_posix_path(uri)); +#ifdef _WIN32 + std::string ext = to_upper(p.extension().string()); // Convert path extension to string +#else std::string ext = to_upper(p.extension()); +#endif for (const auto &i : supported_extensions) if (i == ext) return true; @@ -331,7 +395,11 @@ namespace utility { inline bool is_timeline_supported(const caf::uri &uri) { fs::path p(uri_to_posix_path(uri)); +#ifdef _WIN32 + std::string ext = to_upper_path(p.extension()); +#else std::string ext = to_upper(p.extension()); +#endif for (const auto &i : supported_timeline_extensions) if (i == ext) return true; @@ -452,4 +520,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 2c5f9d52f..cda5ddfe3 100644 --- a/include/xstudio/utility/json_store.hpp +++ b/include/xstudio/utility/json_store.hpp @@ -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); } @@ -305,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..c2ceccee9 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,7 @@ namespace utility { [[nodiscard]] bool unlock() { if (locked_ and owned_ and not borrowed_) { // unlock we no longer own file. - unlink(uri_to_posix_path(lock_file()).c_str()); + _unlink(uri_to_posix_path(lock_file()).c_str()); 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..c2e0e7a92 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/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index d6be083c4..06abe8450 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#ifdef _WIN32 +#include +#endif +#include #include #include #include @@ -11,7 +15,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #include @@ -186,6 +192,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()); @@ -196,7 +214,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 +326,4 @@ namespace utility { } // namespace utility -} // namespace xstudio \ No newline at end of file +} // namespace xstudio 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/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/scripts/setup/setup_dev_env.ps1 b/scripts/setup/setup_dev_env.ps1 new file mode 100644 index 000000000..a78883521 --- /dev/null +++ b/scripts/setup/setup_dev_env.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy Bypass -Scope Process -Force; + +## Install Chocolatey +Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + +Write-Host "=== Creating your XStudio development environment! ===" + +Write-Host "====> Installing Choco packages..." +choco --version +choco feature enable -name=exitOnRebootDetected +choco install ChocolateyGUI -y + +# core components +Write-Host "====> Installing core components..." +choco install powershell-core -y +choco install git -y +choco install cmake -y +choco install ninja -y +choco install doxygen.install -y + +# ides +Write-Host "====> Installing IDEs..." +#choco install visualstudio2019buildtools -y +#choco install visualstudio2019community -y --package-parameters "--includeRecommended --locale en-US --passive --add Microsoft.VisualStudio.Component.CoreEditor --add Microsoft.VisualStudio.Workload.NetWeb --add Microsoft.VisualStudio.Workload.Azure --add Microsoft.VisualStudio.Workload.NetCoreTools --add Microsoft.VisualStudio.Workload.Python --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.NativeGame --add Microsoft.VisualStudio.Workload.NativeCrossPlat --add Component.GitHub.VisualStudio --add Component.Incredibuild --add Microsoft.VisualStudio.Workload.VisualStudioExtension --add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.Universal" +#choco install visualstudio2019professional -y --package-parameters "--includeRecommended --locale en-US --passive --add Microsoft.VisualStudio.Component.CoreEditor --add Microsoft.VisualStudio.Workload.NetWeb --add Microsoft.VisualStudio.Workload.Azure --add Microsoft.VisualStudio.Workload.NetCoreTools --add Microsoft.VisualStudio.Workload.Python --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.NativeGame --add Microsoft.VisualStudio.Workload.NativeCrossPlat --add Component.GitHub.VisualStudio --add Microsoft.VisualStudio.Workload.VisualStudioExtension --add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.Universal" + +refreshenv + +Write-Host "=== Your XStudio development environment is ready to use! Enjoy! ===" 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_session.json b/share/preference/core_session.json index 51e0bbf0a..43896d03b 100644 --- a/share/preference/core_session.json +++ b/share/preference/core_session.json @@ -102,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_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/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/src/audio/src/CMakeLists.txt b/src/audio/src/CMakeLists.txt index 51a870ff5..ba0d6ff19 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,17 @@ 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(UNIX) + list(APPEND LINK_DEPS 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 f34d94ce5..9fd006667 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -129,14 +129,26 @@ void AudioOutputControl::prepare_samples_for_soundcard( const int num_channels, const int sample_rate) { + spdlog::info("Preparing samples for soundcard."); + try { - v.resize(num_samps_to_push * num_channels); + v.resize(num_samps_to_push * num_channels); + spdlog::info("[xstudio] Resized vector 'v' to size: {}", v.size()); + memset(v.data(), 0, v.size() * sizeof(int16_t)); + spdlog::info( + "[xstudio] Set all values in 'v' to 0 using memset for a total size of {} bytes.", + v.size() * sizeof(int16_t)); int16_t *d = v.data(); long n = num_samps_to_push; long num_samps_pushed = 0; + spdlog::info( + "[xstudio] Set 'd' pointer to v.data(), 'n' to {}, and 'num_samps_pushed' to {}", + n, + num_samps_pushed); + if (muted()) return; @@ -145,6 +157,8 @@ void AudioOutputControl::prepare_samples_for_soundcard( if (!current_buf_ && sample_data_.size()) { + spdlog::info("Next buffer for playback is being selected."); + // when is the next sample that we copy into the buffer going to get played? // We assume there are 'samples_in_soundcard_buffer + num_samps_pushed' audio // samples already either in our soundcard buffer or that are already in our @@ -157,6 +171,7 @@ void AudioOutputControl::prepare_samples_for_soundcard( if (current_buf_) { + spdlog::info("Selected a buffer for playback."); current_buf_pos_ = 0; // is audio playback stable ? i.e. is the next sample buffer @@ -171,11 +186,14 @@ 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; } @@ -190,13 +208,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) { @@ -205,6 +226,7 @@ void AudioOutputControl::prepare_samples_for_soundcard( static_volume_adjust(v, vol / 100.0f); } last_vol = vol; + */ } catch (std::exception &e) { spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -217,7 +239,14 @@ void AudioOutputControl::queue_samples_for_playing( const bool forwards, const float velocity) { + spdlog::info( + "Queueing samples for playing. Playing: {}, Direction: {}, Velocity: {}", + playing ? "Yes" : "No", + forwards ? "Forwards" : "Backwards", + velocity); + if (!playing) { + spdlog::info("Not playing, exiting queue_samples_for_playing."); return; } @@ -235,8 +264,15 @@ void AudioOutputControl::queue_samples_for_playing( (previous_buf_ && previous_buf_->media_key() == audio_frame->media_key()) || (current_buf_ && current_buf_->media_key() == audio_frame->media_key()) || !audio_frame->num_samples()) - continue; + { + 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 @@ -249,19 +285,28 @@ 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) { + spdlog::info( + "Respeeding audio buffer due to audio repitch and non-standard velocity."); audio_frame = super_simple_respeed_audio_buffer(audio_frame, fabs(velocity)); } if (!forwards) { + spdlog::info("Reversing audio buffer for backwards playback."); media_reader::AudioBufPtr reversed( new media_reader::AudioBuffer(audio_frame->params())); @@ -284,9 +329,11 @@ void AudioOutputControl::queue_samples_for_playing( reversed->set_display_timestamp_seconds(audio_frame->display_timestamp_seconds()); } else { + spdlog::info("Queueing audio frame for forwards playback."); sample_data_[when_to_sound_audio] = audio_frame; } } + spdlog::info("Finished queueing samples for playing."); } void AudioOutputControl::clear_queued_samples() { @@ -419,6 +466,16 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( const int num_channels, const int fade_in_out) { + spdlog::info("Starting copy operation."); + spdlog::info( + "Initial values - Current Buffer Position: {}, Samples to Copy: {}, Samples Pushed: " + "{}, Num Channels: {}, Fade Setting: {}", + current_buf_position, + num_samples_to_copy, + num_samps_pushed, + num_channels, + fade_in_out); + static std::vector fade_coeffs; if (fade_coeffs.empty()) { fade_coeffs.resize(FADE_FUNC_SAMPS); @@ -428,6 +485,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( } if (fade_in_out == AudioOutputControl::NoFade) { + spdlog::info("Copy operation with no fade."); if (((long)current_buf->num_samples() - (long)current_buf_position) > num_samples_to_copy) { @@ -442,6 +500,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( current_buf_position += num_samples_to_copy; num_samples_to_copy = 0; } else { + spdlog::info("Copy operation with fading."); // fewer samples left in current buffer than the soundcard wants memcpy( stream, @@ -478,6 +537,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( num_samples_to_copy), 0l); + spdlog::info("Copy without fading for {} samples.", bpos); memcpy(stream, tt, bpos * num_channels * sizeof(T)); num_samples_to_copy -= bpos; num_samps_pushed += bpos; @@ -502,6 +562,12 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( } } } + spdlog::info( + "Completed copy operation. New Buffer Position: {}, Samples Left to Copy: {}, Total " + "Samples Pushed: {}", + current_buf_position, + num_samples_to_copy, + num_samps_pushed); } diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index 14c185f4d..60f453f37 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -11,6 +11,14 @@ #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" +// include for system (soundcard) audio output +#ifdef __linux__ +#include "linux_audio_output_device.hpp" +#endif +#ifdef _WIN32 +#include "windows_audio_output_device.hpp" +#endif + using namespace caf; using namespace xstudio::audio; using namespace xstudio::utility; diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index c2c9b32fc..000cfbc48 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -86,7 +86,7 @@ long LinuxAudioOutputDevice::latency_microseconds() { } -void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { +void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long num_samples, int channel_count) { int error; if (playback_handle_ && diff --git a/src/audio/src/linux_audio_output_device.hpp b/src/audio/src/linux_audio_output_device.hpp index be6463a30..014089b60 100644 --- a/src/audio/src/linux_audio_output_device.hpp +++ b/src/audio/src/linux_audio_output_device.hpp @@ -27,7 +27,7 @@ namespace audio { long desired_samples() override; - void push_samples(const void *sample_data, const long num_samples) override; + void push_samples(const void *sample_data, const long num_samples, int channel_count) override; long latency_microseconds() override; 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..77a98e23a --- /dev/null +++ b/src/audio/src/windows_audio_output_device.cpp @@ -0,0 +1,332 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include + +#include "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() { + if (render_client_) { + render_client_ = nullptr; + } + if (audio_client_) { + audio_client_->Stop(); + audio_client_ = nullptr; + } +} + +HRESULT WindowsAudioOutputDevice::initializeAudioClient( + const std::wstring &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 == L"default") { + CComPtr device_collection; + UINT device_count = 0; + hr = device_enumerator->EnumAudioEndpoints( + eRender, DEVICE_STATE_ACTIVE, &device_collection); + if (FAILED(hr)) { + return hr; + } + + hr = device_collection->GetCount(&device_count); + if (FAILED(hr) || device_count == 0) { + return E_FAIL; // or some suitable error + } + + // For this example, we're just taking the first device + CComPtr first_device; + hr = device_collection->Item(0, &first_device); + if (FAILED(hr)) { + return hr; + } + + // Print the device name + CComPtr property_store; + hr = first_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"Device Name: %s\n", var_name.pwszVal); + PropVariantClear(&var_name); // always clear the PROPVARIANT to release any + // memory it might've allocated + } + } + + LPWSTR device_id = nullptr; + hr = first_device->GetId(&device_id); + if (FAILED(hr)) { + return hr; + } + + hr = device_enumerator->GetDevice(device_id, &audio_device); + ::CoTaskMemFree(device_id); // free the memory for the ID + } else { + // Get the audio-render device based on the provided sound_card + hr = device_enumerator->GetDevice(sound_card.c_str(), &audio_device); + } + + if (FAILED(hr)) { + return hr; + } + + // 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; + } + + // 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); + spdlog::info("Channel Mask: {}", pExtensible->dwChannelMask); + // Add more fields if needed + } + + // 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_->InitializeSharedAudioStream( + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + MINP, + wavefmt, + nullptr // session GUID + ); + + // Free the mix format and wave format after usage + CoTaskMemFree(pMixFormat); + CoTaskMemFree(wavefmt); + + return hr; +} + +void WindowsAudioOutputDevice::connect_to_soundcard() { + + sample_rate_ = 48000; //default values + num_channels_ = 2; + std::wstring sound_card(L"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 + } + + spdlog::info("Connected to soundcard"); +} + +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"); + } + return bufferSize; +} + +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) { + + // 1. Function Entry & Initial Parameters + spdlog::info( + "Entering push_samples with {} samples, {} channels.", num_samples, channel_count); + + 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; + } + + // 2. Buffer Size Info + spdlog::info("Buffer frame count from WASAPI: {}", buffer_framecount); + + // 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; + } + + // 3. Padding Info + spdlog::info("Current padding from WASAPI: {}", pad); + + // 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; + } + + // 4. Write and Availability Info + spdlog::info( + "Frames available to write: {}. Frames attempting to write: {}", + available_frames, + frames_to_write); + + // Get a buffer from WASAPI for our audio data. + BYTE *buffer; + hr = render_client_->GetBuffer(frames_to_write * channel_count, &buffer); + if (FAILED(hr)) { + spdlog::error("GetBuffer failed with HRESULT: 0x{:08x}", hr); + throw std::runtime_error("Failed to get buffer from WASAPI"); + } + + // 5. Successful Buffer Retrieval + spdlog::info("Successfully retrieved buffer from WASAPI for writing."); + + // 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 * channel_count, 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"); + } + + // 6. Successful Buffer Release + spdlog::info("Successfully released buffer to WASAPI for playback."); + + // 7. Function Exit + spdlog::info("Exiting push_samples."); +} diff --git a/src/audio/src/windows_audio_output_device.hpp b/src/audio/src/windows_audio_output_device.hpp new file mode 100644 index 000000000..b9dd6513e --- /dev/null +++ b/src/audio/src/windows_audio_output_device.hpp @@ -0,0 +1,59 @@ +// 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, int channel_count) 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_; + const utility::JsonStore config_; + const utility::JsonStore prefs_; + + HRESULT initializeAudioClient( + const std::wstring &sound_card = L"", + long sample_rate = 48000, + int num_channels = 2); + + }; +} // namespace audio +} // namespace xstudio 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/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 e5b9faa70..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 @@ -576,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..1fdd21858 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,8 @@ 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) diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index 8ff56e19a..c5b90e592 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -30,7 +30,7 @@ EmbeddedPython::EmbeddedPython(const JsonStore &jsn, EmbeddedPythonActor *parent void EmbeddedPython::setup() { try { if (not Py_IsInitialized()) { - spdlog::debug("py::initialize_interpreter"); + spdlog::info("py::initialize_interpreter"); py::initialize_interpreter(); inited_ = true; } diff --git a/src/embedded_python/src/embedded_python_actor.cpp b/src/embedded_python/src/embedded_python_actor.cpp index 78df1a626..bad1d2dcb 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" @@ -25,9 +27,18 @@ using namespace caf; using namespace pybind11::literals; 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/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index 188ff2a92..4f3da75c4 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -8,9 +8,13 @@ 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 @@ -37,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_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/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index 9b7df7299..7f043fa64 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) +endif() default_options(${PROJECT_NAME}) @@ -38,6 +48,7 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::qml::log xstudio::ui::qml::module xstudio::ui::qml::playhead + xstudio::ui::model_data xstudio::ui::qml::quickfuture xstudio::ui::qml::session xstudio::ui::qml::studio @@ -50,30 +61,48 @@ target_link_libraries(${PROJECT_NAME} 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 ) +if(WIN32) + target_link_libraries(${PROJECT_NAME} PRIVATE dbghelp) +endif() + set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - OUTPUT_NAME "${PROJECT_NAME}.bin" + if(WIN32) + OUTPUT_NAME "${PROJECT_NAME}" + VS_DEBUGGER_ENVIRONMENT XSTUDIO_ROOT=${CMAKE_BINARY_DIR}/bin/$<$:Debug>$<$:Release> + else() + OUTPUT_NAME "${PROJECT_NAME}.bin" + endif() LINK_DEPENDS_NO_SHARED true ) 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 5be2cf685..289530b05 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 @@ -77,6 +81,7 @@ CAF_POP_WARNINGS #include "xstudio/ui/qml/thumbnail_provider_ui.hpp" #include "xstudio/ui/qt/offscreen_viewport.hpp" //NOLINT +//TODO: Ahead Fix #include "QuickFuture" Q_DECLARE_METATYPE(QUrl) @@ -133,6 +138,37 @@ struct ExitTimeoutKiller { } 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; @@ -143,8 +179,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); @@ -252,8 +290,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, @@ -658,7 +699,7 @@ struct Launcher { files.push_back(p); continue; } - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { } // add to scan list.. @@ -855,13 +896,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.. diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index bc51c7f77..fbe8ff682 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")); } @@ -330,9 +330,13 @@ void MediaActor::init() { const FrameList &frame_list, const utility::FrameRate &rate) -> result { auto rp = make_response_promise(); - +#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 = @@ -392,7 +396,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 +409,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")); } diff --git a/src/media/src/media_source_actor.cpp b/src/media/src/media_source_actor.cpp index a5d44eb29..855abd8ac 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -1460,7 +1460,7 @@ void MediaSourceActor::get_media_pointers_for_frames( 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_cache/src/media_cache_actor.cpp b/src/media_cache/src/media_cache_actor.cpp index 00e48aab7..0285e181f 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; +#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_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_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_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/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index 23d54c779..e7a1cbc7b 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -76,8 +76,13 @@ void blocking_loader( const FrameList &frame_list = i.second; 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 = @@ -358,8 +363,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 = @@ -416,8 +426,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 = 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 c7e3d8ae4..e65c62b3c 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio.cpp @@ -403,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() } @@ -946,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)); 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 1f0f5ba5a..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) 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/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/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/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_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index 47e8d39e4..9a8600796 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -16,14 +16,16 @@ 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 +104,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)); +#elif 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 fb84eec41..c1062cc76 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_ = 48000; + //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(); } @@ -399,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 5e65e5a4d..4f1f61a61 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -7,7 +7,9 @@ #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 +117,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 +148,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::powf(2, 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::powf(2, bitdepth - 8); Imath::M33f scale; scale[0][0] = 1.f * max_cv / (range[1] - range[0]); @@ -161,7 +163,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::powf(2, bitdepth - 8); jsn["yuv_offsets"] = {"ivec3", 1, offset[0], offset[1], offset[2]}; } } @@ -501,6 +503,8 @@ FFMpegStream::convert_av_frame_to_thumbnail(const size_t size_hint) { AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_sample_rate) { + spdlog::info("Entering get_ffmpeg_frame_as_xstudio_audio."); + AudioBufPtr audio_buffer(new AudioBuffer()); audio_buffer->allocate( soundcard_sample_rate, // sample rate @@ -509,6 +513,12 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ audio::SampleFormat::INT16 // format ); + spdlog::info( + "Allocated AudioBuffer with sample rate: {}, channels: {}, format: {}.", + soundcard_sample_rate, + 2, + "INT16"); + // N.B. We aren't supporting 'planar' audio data in xstudio (yet) - if a source codec_ // supplies planar audio ffmpeg will convert to interleaved which is what soundcards want switch (audio_buffer->sample_format()) { @@ -533,6 +543,10 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ default: throw media_corrupt_error("Audio buffer format is not set."); } + + spdlog::info( + "Determined target sample format: {}.", av_get_sample_fmt_name(target_sample_format_)); + target_sample_rate_ = audio_buffer->sample_rate(); target_audio_channels_ = audio_buffer->num_channels(); @@ -540,7 +554,16 @@ 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); + + spdlog::info("Resampled audio data size: {} samples.", audio_buffer->num_samples()); + + spdlog::info("Exiting get_ffmpeg_frame_as_xstudio_audio."); return audio_buffer; } @@ -694,6 +717,13 @@ int64_t FFMpegStream::frame_to_pts(int frame) const { size_t FFMpegStream::resample_audio( AVFrame *frame, AudioBufPtr &audio_buffer, int offset_into_output_buffer) { + spdlog::info("Resampling audio frame with the following attributes:"); + spdlog::info("Sample rate: {}", frame->sample_rate); + spdlog::info("Format: {}", av_get_sample_fmt_name((AVSampleFormat)frame->format)); + spdlog::info("Channels: {}", frame->channels); + spdlog::info("Number of samples: {}", frame->nb_samples); + spdlog::info("Channel layout: {}", frame->channel_layout); + // N.B. this method is based loosely on the audio resampling in ffplay.c in ffmpeg source const int64_t target_channel_layout = av_get_default_channel_layout(2); @@ -762,6 +792,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); @@ -777,6 +834,8 @@ size_t FFMpegStream::resample_audio( int converted_n_samps = swr_convert(audio_resampler_ctx_, &out, out_count, in, frame->nb_samples); + spdlog::info("Converted {} samples.", converted_n_samps); + if (offset_into_output_buffer == -1) { audio_buffer->set_num_samples(audio_buffer->num_samples() + converted_n_samps); } @@ -785,6 +844,11 @@ size_t FFMpegStream::resample_audio( throw media_corrupt_error("swr_convert() failed"); } + spdlog::info( + "Resampled audio data size: {} bytes", + converted_n_samps * target_audio_channels_ * + av_get_bytes_per_sample(target_sample_format_)); + return converted_n_samps * target_audio_channels_ * av_get_bytes_per_sample(target_sample_format_); } @@ -877,4 +941,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/openexr/src/openexr.cpp b/src/plugin/media_reader/openexr/src/openexr.cpp index 8ef861f0a..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 @@ -816,4 +818,4 @@ PixelInfo OpenEXRMediaReader::exr_buffer_pixel_picker( } // else 1 channel, assume luminance return r; -} \ No newline at end of file +} diff --git a/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp b/src/plugin/media_reader/openexr/src/simple_exr_sampler.hpp index d3cb7bc6b..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 { diff --git a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp index 3a1b37260..0aed07b56 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 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/basic_viewport_mask/src/basic_viewport_masking.cpp b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp index 78d7602d3..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; 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_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index c58b479c4..b0d3fdce9 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,50 @@ 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::warn("Loading Plugins"); + for (const auto &path : plugin_paths_) { + spdlog::warn(path); 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; + spdlog::warn(entry.path().string()); +#ifdef __linux__ // only want .so // clear any errors.. dlerror(); @@ -47,6 +79,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 +119,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 +141,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) 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..fa384e93d 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 1c997fab6..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 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 ee77b7fc9..9145d5b6c 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.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 @@ -168,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 94976c8d1..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" 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..ccb4b7368 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; } @@ -176,16 +176,28 @@ ScanHelperActor::ScanHelperActor(caf::actor_config &cfg) : caf::event_based_acto try { if (fs::is_regular_file(entry.status())) { // 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()); +#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 8e7709f7f..46f3fd31c 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -334,7 +334,12 @@ 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( @@ -1898,7 +1903,11 @@ void SessionActor::save_json_to( auto save_path = uri_to_posix_path(ppath); if (resolve_link && fs::exists(save_path) && fs::is_symlink(save_path)) +#ifdef _WIN32 + save_path = fs::canonical(save_path).string(); +#else save_path = fs::canonical(save_path); +#endif // compress data. diff --git a/src/shotgun_client/src/shotgun_client_actor.cpp b/src/shotgun_client/src/shotgun_client_actor.cpp index 593a1f5f0..63a63c5fb 100644 --- a/src/shotgun_client/src/shotgun_client_actor.cpp +++ b/src/shotgun_client/src/shotgun_client_actor.cpp @@ -1577,40 +1577,43 @@ 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/thumbnail/src/thumbnail_disk_cache_actor.cpp b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp index 21fee25cd..81daaf6d2 100644 --- a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp +++ b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp @@ -313,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()); } @@ -354,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/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/item.cpp b/src/timeline/src/item.cpp index 242c80c51..bcd93c304 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/timeline/item.hpp" #include "xstudio/utility/helpers.hpp" @@ -742,8 +743,8 @@ bool Item::process_event(const utility::JsonStore &event) { case IT_ACTIVE: set_active_range_direct(event.at("value")); has_active_range_ = event.at("value2"); - break; - case IT_AVAIL: + break; + case IT_AVAIL: set_available_range_direct(event.at("value")); has_available_range_ = event.at("value2"); break; @@ -790,29 +791,29 @@ bool Item::process_event(const utility::JsonStore &event) { } } break; - case IT_ADDR: + case IT_ADDR: if (event.at("value").is_null()) - set_actor_addr_direct(caf::actor_addr()); - else + set_actor_addr_direct(caf::actor_addr()); + else set_actor_addr_direct(string_to_actor_addr(event.at("value"))); - break; - case IA_NONE: - default: - break; - } - if (item_event_callback_) - item_event_callback_(event, *this); - } else { + break; + case IA_NONE: + default: + break; + } + if (item_event_callback_) + item_event_callback_(event, *this); + } else { // child ? for (auto &i : *this) { if (i.process_event(event)) return true; - } + } - return false; - } + return false; + } return true; -} + } void Item::bind_item_event_func(ItemEventFunc fn, const bool recursive) { recursive_bind_ = recursive; diff --git a/src/timeline/src/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index 067a8c81f..8b2960d4d 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" @@ -107,6 +109,7 @@ void TimelineActor::item_event_callback(const utility::JsonStore &event, Item &i } } +#ifdef BUILD_OTIO namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; @@ -491,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) @@ -641,7 +646,14 @@ 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); +#elif anon_send(history_, history::log_atom_v, sysclock::now(), jsn); +#endif } return jsn; }, @@ -664,7 +676,14 @@ 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; }, @@ -685,7 +704,15 @@ void TimelineActor::init() { 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; }, @@ -734,17 +761,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); }, @@ -1563,6 +1607,16 @@ 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 @@ -1957,11 +2011,13 @@ void TimelineActor::init() { [=](playhead::get_selection_atom) -> UuidList { return UuidList{base_.uuid()}; }, [=](playhead::get_selection_atom, caf::actor requester) { - anon_send( - requester, - utility::event_atom_v, - playhead::selection_changed_atom_v, - UuidList{base_.uuid()}); +#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) {}, @@ -1986,13 +2042,14 @@ void TimelineActor::init() { auto rp = make_response_promise(); // purge timeline.. ? +#ifdef BUILD_OTIO spawn( timeline_importer, rp, caf::actor_cast(playlist_), UuidActor(base_.uuid(), actor_cast(this)), data); - +#endif return rp; }); } 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/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/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 9f0ddce1f..ef5e83b34 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -794,7 +794,7 @@ void GlobalUIModelData::insert_into_menu_model( menu_model_data = find_node_matching_string_field( menu_model_data, "name", parent_menus.front()); 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; } diff --git a/src/ui/opengl/src/CMakeLists.txt b/src/ui/opengl/src/CMakeLists.txt index e854e9202..3d3eaa1cb 100644 --- a/src/ui/opengl/src/CMakeLists.txt +++ b/src/ui/opengl/src/CMakeLists.txt @@ -3,7 +3,6 @@ SET(LINK_DEPS OpenGL::GL OpenGL::GLU GLEW::GLEW - pthread xstudio::ui::base xstudio::ui::canvas xstudio::utility @@ -12,15 +11,24 @@ SET(LINK_DEPS 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/texture.cpp b/src/ui/opengl/src/texture.cpp index 271597c1c..c1dec8933 100644 --- a/src/ui/opengl/src/texture.cpp +++ b/src/ui/opengl/src/texture.cpp @@ -31,7 +31,9 @@ class DebugTimer { } // namespace void GLBlindTex::release() { - mutex_.unlock(); + //if linux + //mutex_.unlock(); + //endif when_last_used_ = utility::clock::now(); } @@ -136,6 +138,7 @@ void GLDoubleBufferedTexture::upload_next( void GLDoubleBufferedTexture::release() { current_->release(); } GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { + //if linux? TODO: Merged this in but might be problematic for Windows, do check. // ensure no copying is in flight if (upload_thread_.joinable()) upload_thread_.join(); @@ -220,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; } @@ -256,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(); @@ -282,15 +292,19 @@ 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(); +void GLBlindRGBA8bitTex::bind(int tex_index, Imath::V2i &dims) { + + // mutex_.lock(); + //std::unique_lock lck(mutex_); dims.x = tex_width_; dims.y = tex_height_; @@ -298,9 +312,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. @@ -683,4 +697,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..d8338d891 100644 --- a/src/ui/qml/CMakeLists.txt +++ b/src/ui/qml/CMakeLists.txt @@ -8,9 +8,10 @@ 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) diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index 08f882b09..b54c28c39 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -211,7 +211,7 @@ BookmarkModel::getJSONFuture(const QModelIndex &index, const QString &path) cons 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()); } @@ -329,7 +329,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(); } 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 5edab50e9..29afd2696 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(); diff --git a/src/ui/qml/helper/src/CMakeLists.txt b/src/ui/qml/helper/src/CMakeLists.txt index 54b16cbe9..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 ) diff --git a/src/ui/qml/helper/src/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index 0c422eab5..cfbc4940e 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -93,7 +93,7 @@ QString xstudio::ui::qml::getThumbnailURL( *sys, colour_pipe, colour_pipeline::display_colour_transform_hash_atom_v, mp); hash = std::hash{}(static_cast( display_transform_hash + mhash.first + std::to_string(mhash.second))); - } catch (const std::exception &err) { + } catch ([[maybe_unused]] const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -106,7 +106,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/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 9e1025fd9..440c67a4c 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -119,10 +119,17 @@ 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) { +#ifdef _WIN32 + emit dataChanged( + idx, + idx, + QVector({static_cast(Roles::LASTROLE + static_cast(i))})); +#else emit dataChanged( idx, idx, QVector({Roles::LASTROLE + static_cast(i)})); +#endif break; } } diff --git a/src/ui/qml/module/src/module_menu_ui.cpp b/src/ui/qml/module/src/module_menu_ui.cpp index f9cd1cf55..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; } @@ -540,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 ee4914280..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; @@ -265,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(); + } } } @@ -399,7 +401,7 @@ void ModuleAttrsModel::update_attribute_from_backend( } row++; } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { } } @@ -427,7 +429,7 @@ ModuleAttrsToQMLShim::~ModuleAttrsToQMLShim() { try { request_receive( *sys, attrs_events_actor_group_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &) { + } catch ([[maybe_unused]] const std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } diff --git a/src/ui/qml/playhead/src/playhead_ui.cpp b/src/ui/qml/playhead/src/playhead_ui.cpp index d0ce0faf8..6497feee6 100644 --- a/src/ui/qml/playhead/src/playhead_ui.cpp +++ b/src/ui/qml/playhead/src/playhead_ui.cpp @@ -441,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 index f98e15582..244341899 100644 --- a/src/ui/qml/quickfuture/src/CMakeLists.txt +++ b/src/ui/qml/quickfuture/src/CMakeLists.txt @@ -1,5 +1,6 @@ SET(LINK_DEPS Qt5::Core + Qt5::Quick ) SET(EXTRAMOC diff --git a/src/ui/qml/quickfuture/src/QuickFuture b/src/ui/qml/quickfuture/src/QuickFuture index 3ae515624..216bd6d88 120000 --- a/src/ui/qml/quickfuture/src/QuickFuture +++ b/src/ui/qml/quickfuture/src/QuickFuture @@ -1 +1 @@ -../../../../../extern/quickfuture/src/QuickFuture \ No newline at end of file +#include "../../../../../extern/quickfuture/src/QuickFuture" \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/qffuture.h b/src/ui/qml/quickfuture/src/qffuture.h index b9fe99793..7253b49ec 120000 --- a/src/ui/qml/quickfuture/src/qffuture.h +++ b/src/ui/qml/quickfuture/src/qffuture.h @@ -1 +1,76 @@ -../../../../../extern/quickfuture/src/qffuture.h \ No newline at end of file +#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/src/ui/qml/quickfuture/src/qfvariantwrapper.h b/src/ui/qml/quickfuture/src/qfvariantwrapper.h index 950128958..1807bb3d8 120000 --- a/src/ui/qml/quickfuture/src/qfvariantwrapper.h +++ b/src/ui/qml/quickfuture/src/qfvariantwrapper.h @@ -1 +1,310 @@ -../../../../../extern/quickfuture/src/qfvariantwrapper.h \ No newline at end of file +#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/src/ui/qml/quickfuture/src/qmldir b/src/ui/qml/quickfuture/src/qmldir index 56238483d..8693fc6e0 120000 --- a/src/ui/qml/quickfuture/src/qmldir +++ b/src/ui/qml/quickfuture/src/qmldir @@ -1 +1 @@ -../../../../../extern/quickfuture/src/qmldir \ No newline at end of file +include "../../../../../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 index 1f07cca8f..516980d37 120000 --- a/src/ui/qml/quickfuture/src/quickfuture.h +++ b/src/ui/qml/quickfuture/src/quickfuture.h @@ -1 +1,33 @@ -../../../../../extern/quickfuture/src/quickfuture.h \ No newline at end of file +#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/src/ui/qml/quickfuture/src/quickfuture.qmltypes b/src/ui/qml/quickfuture/src/quickfuture.qmltypes index e8b96a211..84ef0cd06 120000 --- a/src/ui/qml/quickfuture/src/quickfuture.qmltypes +++ b/src/ui/qml/quickfuture/src/quickfuture.qmltypes @@ -1 +1 @@ -../../../../../extern/quickfuture/src/quickfuture.qmltypes \ No newline at end of file +#include "../../../../../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 8ac241a49..6e74a5388 100644 --- a/src/ui/qml/session/src/CMakeLists.txt +++ b/src/ui/qml/session/src/CMakeLists.txt @@ -5,6 +5,7 @@ SET(LINK_DEPS xstudio::ui::qml::helper xstudio::timeline xstudio::utility + xstudio::session ) SET(EXTRAMOC diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index 193edcaa4..02cdaf6fe 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -455,9 +455,18 @@ 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(unsigned long(children.size())), + mediaCountRole); + + } else { + setData(parent_index.parent(), QVariant::fromValue(0), mediaCountRole); + } } emit dataChanged(parent_index, parent_index, roles); @@ -993,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()) { diff --git a/src/ui/qml/studio/src/CMakeLists.txt b/src/ui/qml/studio/src/CMakeLists.txt index 39ae256f4..fcde09b3f 100644 --- a/src/ui/qml/studio/src/CMakeLists.txt +++ b/src/ui/qml/studio/src/CMakeLists.txt @@ -3,6 +3,7 @@ SET(LINK_DEPS Qt5::Core xstudio::ui::qml::helper xstudio::utility + xstudio::session ) SET(EXTRAMOC 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/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 2550a5afd..51400dff8 100644 --- a/src/ui/qml/viewport/src/CMakeLists.txt +++ b/src/ui/qml/viewport/src/CMakeLists.txt @@ -9,6 +9,7 @@ SET(LINK_DEPS xstudio::playhead xstudio::ui::opengl::viewport xstudio::ui::viewport + xstudio::ui::qml::helper xstudio::ui::qml::playhead xstudio::utility ) 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 5ef9036a2..c3c1f940a 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -306,7 +306,11 @@ void OffscreenViewport::renderSnapshot(const int width, const int height, const 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") { 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/utility/src/CMakeLists.txt b/src/utility/src/CMakeLists.txt index aecf5158d..3429b7161 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -2,9 +2,14 @@ find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(Imath REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(PkgConfig REQUIRED) find_package(ZLIB REQUIRED) -pkg_search_module(UUID REQUIRED uuid) +if(WIN32) + # Not Required +elseif(UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_search_module(UUID REQUIRED uuid) +endif() + SET(LINK_DEPS PUBLIC @@ -13,7 +18,6 @@ SET(LINK_DEPS Imath::Imath nlohmann_json::nlohmann_json spdlog::spdlog - stdc++fs uuid ZLIB::ZLIB ) @@ -24,9 +28,14 @@ SET(STATIC_LINK_DEPS Imath::Imath nlohmann_json::nlohmann_json 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}") \ No newline at end of file diff --git a/src/utility/src/chrono.cpp b/src/utility/src/chrono.cpp index 2fda69403..26431e435 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/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/frame_list.cpp b/src/utility/src/frame_list.cpp index 9159d1ddc..f032366b0 100644 --- a/src/utility/src/frame_list.cpp +++ b/src/utility/src/frame_list.cpp @@ -51,7 +51,7 @@ int FrameGroup::frame(const size_t index, const bool implied, const bool valid) if (implied) { if (valid) { // find previous valid frame. - _frame = ((index / step_) * step_) + start_; + _frame = (int)((index / step_) * step_) + start_; } else _frame = start_ + index; } else { @@ -261,14 +261,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 65a05cc70..41d003bfc 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -1,15 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __linux__ #define __USE_POSIX - +#include +#include +#include +#include +#endif #include #include #include #include -#include #include -#include -#include -#include #include @@ -21,6 +22,10 @@ #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; @@ -78,7 +83,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; @@ -229,9 +234,21 @@ 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 + // 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 + } + + // Now, replace forward slashes with backslashes + std::replace(path.begin(), path.end(), '/', '\\'); +#endif return path; } return ""; @@ -360,7 +377,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()); @@ -425,9 +449,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); @@ -456,14 +486,22 @@ 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(entry.path().string()); +#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()); @@ -510,12 +548,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; @@ -531,6 +577,7 @@ std::string xstudio::utility::get_user_name() { result = pw->pw_name; } } +#endif return result; } @@ -576,6 +623,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; @@ -588,6 +643,7 @@ std::string xstudio::utility::get_login_name() { result = pw->pw_name; } } +#endif return result; } diff --git a/src/utility/src/remote_session_file.cpp b/src/utility/src/remote_session_file.cpp index 9f6454e45..7dad932ec 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,7 @@ fs::path RemoteSessionFile::filepath() const { return p; } -pid_t RemoteSessionFile::get_pid() const { return getpid(); } +pid_t RemoteSessionFile::get_pid() const { return _getpid(); } bool RemoteSessionFile::create_session_file() { @@ -146,7 +150,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..1f36ac08c 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,15 +74,41 @@ 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_() {} Sequence::Sequence(const std::string name) : name_(std::move(name)), frames_() {} @@ -118,6 +159,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 +176,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 +360,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 +419,4 @@ std::vector sequences_from_entries( return sequences; } -} // namespace xstudio::utility \ No newline at end of file +} // namespace xstudio::utility 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/dialogs/XsOpenSessionDialog.qml b/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml index 9d4c2c76e..b8c6f3a95 100644 --- a/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml +++ b/ui/qml/xstudio/dialogs/XsOpenSessionDialog.qml @@ -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/extern/QuickPromise b/ui/qml/xstudio/extern/QuickPromise index 9352e9364..5399b0dd6 120000 --- a/ui/qml/xstudio/extern/QuickPromise +++ b/ui/qml/xstudio/extern/QuickPromise @@ -1 +1 @@ -../../../../extern/quickpromise/qml/QuickPromise \ No newline at end of file +#include "../../../../extern/quickpromise/qml/QuickPromise" \ No newline at end of file diff --git a/ui/qml/xstudio/qml.qrc b/ui/qml/xstudio/qml.qrc index 9f4ef5655..82442c087 100644 --- a/ui/qml/xstudio/qml.qrc +++ b/ui/qml/xstudio/qml.qrc @@ -552,9 +552,11 @@ XsStyleGradient.qml xstudio_icon.png xStudio/qmldir + + diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..797b1f137 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,25 @@ +{ + "name": "xstudio", + "version": "1.0.0", + "dependencies": [ + "stduuid", + "reproc", + "nlohmann-json", + "glew", + "freetype", + "pybind11", + "spdlog", + "fmt", + "lcms", + "caf", + "opencolorio", + "openimageio" + ], + "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" } + ] +} From abf4087ab84ccefc6743ad94ad37f37fbc1ea9ac Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 7 May 2024 16:25:32 -0700 Subject: [PATCH 11/42] Fix python install and portability. Signed-off-by: Michael Kessler --- CMakeLists.txt | 2 +- src/embedded_python/src/CMakeLists.txt | 6 ++++++ src/embedded_python/src/embedded_python.cpp | 1 - src/embedded_python/src/python310._pth | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/embedded_python/src/python310._pth diff --git a/CMakeLists.txt b/CMakeLists.txt index 67082d727..69d150e5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,7 +171,7 @@ if(USE_VCPKG) 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 sphinx breathe sphinx-rtd-theme opentimelineio importlib_metadata zipp + COMMAND "${CMAKE_COMMAND}" -E env "PATH=${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3" python.exe -m pip install sphinx breathe sphinx-rtd-theme OpenTimelineIO importlib_metadata zipp RESULT_VARIABLE PIP_RESULT ) if(PIP_RESULT) diff --git a/src/embedded_python/src/CMakeLists.txt b/src/embedded_python/src/CMakeLists.txt index 1fdd21858..88d406b4c 100644 --- a/src/embedded_python/src/CMakeLists.txt +++ b/src/embedded_python/src/CMakeLists.txt @@ -38,3 +38,9 @@ target_link_libraries(${PROJECT_NAME} ) 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 c5b90e592..449b2d883 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -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/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 From 5771523d1daf2dfe7f266c3b8e28fd82f114cddd Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 7 May 2024 16:26:03 -0700 Subject: [PATCH 12/42] Remove portability debugging output. Signed-off-by: Michael Kessler --- include/xstudio/utility/helpers.hpp | 1 - src/plugin_manager/src/plugin_manager.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 5e50fbb5a..e3a0cdcbe 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -269,7 +269,6 @@ namespace utility { spdlog::debug("Unable to determine executable path from Windows API, falling back " "to standard methods"); } else { - spdlog::warn(std::string(filename)); auto exePath = fs::path(filename); // The first parent path gets us to the bin directory, the second gets us to the level above bin. diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index b0d3fdce9..b0adc2436 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -50,14 +50,12 @@ size_t PluginManager::load_plugins() { spdlog::warn("Loading Plugins"); for (const auto &path : plugin_paths_) { - spdlog::warn(path); try { // read dir content.. for (const auto &entry : fs::directory_iterator(path)) { if (not fs::is_regular_file(entry.status()) or not(entry.path().extension() == ".so" || entry.path().extension() == ".dll")) continue; - spdlog::warn(entry.path().string()); #ifdef __linux__ // only want .so From 9dad87484fba3d47cfbde79a6464c38d0f0f0515 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 7 May 2024 16:26:24 -0700 Subject: [PATCH 13/42] Fix thumbnail cache pathing Signed-off-by: Michael Kessler --- src/thumbnail/src/thumbnail.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 9c38750245b126f49c0817f0b6a03d9f90e72416 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 7 May 2024 16:49:35 -0700 Subject: [PATCH 14/42] Demote warnings into debug messages. Signed-off-by: Michael Kessler --- src/embedded_python/src/embedded_python.cpp | 2 +- src/plugin_manager/src/plugin_manager.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index 449b2d883..84a309e4a 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -30,7 +30,7 @@ EmbeddedPython::EmbeddedPython(const JsonStore &jsn, EmbeddedPythonActor *parent void EmbeddedPython::setup() { try { if (not Py_IsInitialized()) { - spdlog::info("py::initialize_interpreter"); + spdlog::debug("py::initialize_interpreter"); py::initialize_interpreter(); inited_ = true; } diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index b0adc2436..b6a97f6b6 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -47,7 +47,7 @@ PluginManager::PluginManager(std::list plugin_paths) size_t PluginManager::load_plugins() { // scan for .so or .dll for each path. size_t loaded = 0; - spdlog::warn("Loading Plugins"); + spdlog::debug("Loading Plugins"); for (const auto &path : plugin_paths_) { try { From b316e84713fec39489e83ed1309c293251247f46 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 17 May 2024 03:41:37 -0700 Subject: [PATCH 15/42] Post-merge intermediate work Signed-off-by: Michael Kessler --- CMakeLists.txt | 4 +- CMakePresets.json | 4 +- cmake/macros.cmake | 14 +- extern/reproc | 1 - extern/reproc-14.2.4/.clang-format | 22 - extern/reproc-14.2.4/.clang-tidy | 40 - extern/reproc-14.2.4/.editorconfig | 9 - .../.github/workflows/codeql-analysis.yml | 43 - .../reproc-14.2.4/.github/workflows/main.yml | 130 -- .../reproc-14.2.4/.github/workflows/vsenv.ps1 | 15 - extern/reproc-14.2.4/CHANGELOG.md | 1076 ----------------- extern/reproc-14.2.4/CMakeLists.txt | 36 - extern/reproc-14.2.4/LICENSE | 21 - extern/reproc-14.2.4/README.md | 301 ----- extern/reproc-14.2.4/cmake/reproc.cmake | 408 ------- extern/reproc-14.2.4/reproc++/CMakeLists.txt | 22 - .../reproc++/examples/background.cpp | 89 -- .../reproc-14.2.4/reproc++/examples/drain.cpp | 76 -- .../reproc++/examples/forward.cpp | 83 -- .../reproc-14.2.4/reproc++/examples/run.cpp | 24 - .../reproc++/include/reproc++/arguments.hpp | 59 - .../include/reproc++/detail/array.hpp | 53 - .../include/reproc++/detail/type_traits.hpp | 18 - .../reproc++/include/reproc++/drain.hpp | 152 --- .../reproc++/include/reproc++/env.hpp | 76 -- .../reproc++/include/reproc++/export.hpp | 21 - .../reproc++/include/reproc++/input.hpp | 37 - .../reproc++/include/reproc++/reproc.hpp | 223 ---- .../reproc++/include/reproc++/run.hpp | 41 - .../reproc++/reproc++-config.cmake.in | 13 - extern/reproc-14.2.4/reproc++/reproc++.pc.in | 13 - extern/reproc-14.2.4/reproc++/src/reproc.cpp | 168 --- extern/reproc-14.2.4/reproc/CMakeLists.txt | 62 - extern/reproc-14.2.4/reproc/examples/drain.c | 62 - extern/reproc-14.2.4/reproc/examples/env.c | 21 - extern/reproc-14.2.4/reproc/examples/parent.c | 42 - extern/reproc-14.2.4/reproc/examples/path.c | 18 - extern/reproc-14.2.4/reproc/examples/poll.c | 107 -- extern/reproc-14.2.4/reproc/examples/read.c | 108 -- extern/reproc-14.2.4/reproc/examples/run.c | 19 - .../reproc/include/reproc/drain.h | 79 -- .../reproc/include/reproc/export.h | 21 - .../reproc/include/reproc/reproc.h | 530 -------- .../reproc-14.2.4/reproc/include/reproc/run.h | 27 - .../reproc/reproc-config.cmake.in | 12 - extern/reproc-14.2.4/reproc/reproc.pc.in | 12 - extern/reproc-14.2.4/reproc/resources/argv.c | 10 - .../reproc-14.2.4/reproc/resources/deadline.c | 7 - extern/reproc-14.2.4/reproc/resources/env.c | 13 - extern/reproc-14.2.4/reproc/resources/io.c | 15 - .../reproc-14.2.4/reproc/resources/overflow.c | 14 - extern/reproc-14.2.4/reproc/resources/path.c | 10 - extern/reproc-14.2.4/reproc/resources/pid.c | 15 - extern/reproc-14.2.4/reproc/resources/sleep.h | 18 - extern/reproc-14.2.4/reproc/resources/stop.c | 7 - .../reproc/resources/working-directory.c | 21 - extern/reproc-14.2.4/reproc/src/clock.h | 5 - extern/reproc-14.2.4/reproc/src/clock.posix.c | 17 - .../reproc-14.2.4/reproc/src/clock.windows.c | 10 - extern/reproc-14.2.4/reproc/src/drain.c | 121 -- extern/reproc-14.2.4/reproc/src/error.h | 25 - extern/reproc-14.2.4/reproc/src/error.posix.c | 31 - .../reproc-14.2.4/reproc/src/error.windows.c | 58 - extern/reproc-14.2.4/reproc/src/handle.h | 20 - .../reproc-14.2.4/reproc/src/handle.posix.c | 42 - .../reproc-14.2.4/reproc/src/handle.windows.c | 23 - extern/reproc-14.2.4/reproc/src/init.h | 5 - extern/reproc-14.2.4/reproc/src/init.posix.c | 10 - .../reproc-14.2.4/reproc/src/init.windows.c | 24 - extern/reproc-14.2.4/reproc/src/macro.h | 11 - extern/reproc-14.2.4/reproc/src/options.c | 137 --- extern/reproc-14.2.4/reproc/src/options.h | 7 - extern/reproc-14.2.4/reproc/src/pipe.h | 46 - extern/reproc-14.2.4/reproc/src/pipe.posix.c | 141 --- .../reproc-14.2.4/reproc/src/pipe.windows.c | 261 ---- extern/reproc-14.2.4/reproc/src/process.h | 66 - .../reproc-14.2.4/reproc/src/process.posix.c | 499 -------- .../reproc/src/process.windows.c | 506 -------- extern/reproc-14.2.4/reproc/src/redirect.c | 164 --- extern/reproc-14.2.4/reproc/src/redirect.h | 25 - .../reproc-14.2.4/reproc/src/redirect.posix.c | 79 -- .../reproc/src/redirect.windows.c | 113 -- extern/reproc-14.2.4/reproc/src/reproc.c | 695 ----------- extern/reproc-14.2.4/reproc/src/run.c | 54 - extern/reproc-14.2.4/reproc/src/strv.c | 88 -- extern/reproc-14.2.4/reproc/src/strv.h | 7 - extern/reproc-14.2.4/reproc/src/utf.h | 13 - extern/reproc-14.2.4/reproc/src/utf.posix.c | 3 - extern/reproc-14.2.4/reproc/src/utf.windows.c | 39 - extern/reproc-14.2.4/reproc/test/argv.c | 33 - extern/reproc-14.2.4/reproc/test/assert.h | 43 - extern/reproc-14.2.4/reproc/test/deadline.c | 10 - extern/reproc-14.2.4/reproc/test/env.c | 36 - extern/reproc-14.2.4/reproc/test/fork.c | 39 - extern/reproc-14.2.4/reproc/test/io.c | 74 -- extern/reproc-14.2.4/reproc/test/overflow.c | 17 - extern/reproc-14.2.4/reproc/test/path.c | 41 - extern/reproc-14.2.4/reproc/test/pid.c | 37 - extern/reproc-14.2.4/reproc/test/stop.c | 32 - .../reproc/test/working-directory.c | 29 - include/xstudio/ui/qml/helper_ui.hpp | 84 +- include/xstudio/ui/qml/json_tree_model_ui.hpp | 44 +- include/xstudio/ui/qml/model_data_ui.hpp | 46 +- include/xstudio/ui/qml/module_data_ui.hpp | 2 +- include/xstudio/ui/qml/snapshot_model_ui.hpp | 2 +- include/xstudio/ui/qml/tag_ui.hpp | 46 +- include/xstudio/utility/file_system_item.hpp | 3 + include/xstudio/utility/helpers.hpp | 13 +- include/xstudio/utility/string_helpers.hpp | 11 + src/conform/src/CMakeLists.txt | 2 + src/launch/xstudio/src/xstudio.cpp | 13 +- src/playhead/src/playhead_actor.cpp | 2 +- src/playhead/src/sub_playhead.cpp | 8 +- src/plugin_manager/src/plugin_base.cpp | 2 +- src/session/src/session_actor.cpp | 2 +- src/timeline/src/item.cpp | 2 +- src/ui/canvas/src/CMakeLists.txt | 16 +- src/ui/model_data/src/model_data_actor.cpp | 6 +- src/ui/qml/playhead/src/playhead_ui.cpp | 4 +- src/ui/qml/session/src/CMakeLists.txt | 2 + .../qml/session/src/session_model_core_ui.cpp | 2 +- src/ui/qml/studio/src/CMakeLists.txt | 1 + src/utility/src/file_system_item.cpp | 8 +- src/utility/src/logging.cpp | 2 +- 124 files changed, 146 insertions(+), 8455 deletions(-) delete mode 120000 extern/reproc delete mode 100644 extern/reproc-14.2.4/.clang-format delete mode 100644 extern/reproc-14.2.4/.clang-tidy delete mode 100644 extern/reproc-14.2.4/.editorconfig delete mode 100644 extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml delete mode 100644 extern/reproc-14.2.4/.github/workflows/main.yml delete mode 100644 extern/reproc-14.2.4/.github/workflows/vsenv.ps1 delete mode 100644 extern/reproc-14.2.4/CHANGELOG.md delete mode 100644 extern/reproc-14.2.4/CMakeLists.txt delete mode 100644 extern/reproc-14.2.4/LICENSE delete mode 100644 extern/reproc-14.2.4/README.md delete mode 100644 extern/reproc-14.2.4/cmake/reproc.cmake delete mode 100644 extern/reproc-14.2.4/reproc++/CMakeLists.txt delete mode 100644 extern/reproc-14.2.4/reproc++/examples/background.cpp delete mode 100644 extern/reproc-14.2.4/reproc++/examples/drain.cpp delete mode 100644 extern/reproc-14.2.4/reproc++/examples/forward.cpp delete mode 100644 extern/reproc-14.2.4/reproc++/examples/run.cpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/detail/array.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/drain.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/env.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/export.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/input.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/reproc.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/include/reproc++/run.hpp delete mode 100644 extern/reproc-14.2.4/reproc++/reproc++-config.cmake.in delete mode 100644 extern/reproc-14.2.4/reproc++/reproc++.pc.in delete mode 100644 extern/reproc-14.2.4/reproc++/src/reproc.cpp delete mode 100644 extern/reproc-14.2.4/reproc/CMakeLists.txt delete mode 100644 extern/reproc-14.2.4/reproc/examples/drain.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/env.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/parent.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/path.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/poll.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/read.c delete mode 100644 extern/reproc-14.2.4/reproc/examples/run.c delete mode 100644 extern/reproc-14.2.4/reproc/include/reproc/drain.h delete mode 100644 extern/reproc-14.2.4/reproc/include/reproc/export.h delete mode 100644 extern/reproc-14.2.4/reproc/include/reproc/reproc.h delete mode 100644 extern/reproc-14.2.4/reproc/include/reproc/run.h delete mode 100644 extern/reproc-14.2.4/reproc/reproc-config.cmake.in delete mode 100644 extern/reproc-14.2.4/reproc/reproc.pc.in delete mode 100644 extern/reproc-14.2.4/reproc/resources/argv.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/deadline.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/env.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/io.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/overflow.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/path.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/pid.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/sleep.h delete mode 100644 extern/reproc-14.2.4/reproc/resources/stop.c delete mode 100644 extern/reproc-14.2.4/reproc/resources/working-directory.c delete mode 100644 extern/reproc-14.2.4/reproc/src/clock.h delete mode 100644 extern/reproc-14.2.4/reproc/src/clock.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/clock.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/drain.c delete mode 100644 extern/reproc-14.2.4/reproc/src/error.h delete mode 100644 extern/reproc-14.2.4/reproc/src/error.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/error.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/handle.h delete mode 100644 extern/reproc-14.2.4/reproc/src/handle.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/handle.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/init.h delete mode 100644 extern/reproc-14.2.4/reproc/src/init.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/init.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/macro.h delete mode 100644 extern/reproc-14.2.4/reproc/src/options.c delete mode 100644 extern/reproc-14.2.4/reproc/src/options.h delete mode 100644 extern/reproc-14.2.4/reproc/src/pipe.h delete mode 100644 extern/reproc-14.2.4/reproc/src/pipe.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/pipe.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/process.h delete mode 100644 extern/reproc-14.2.4/reproc/src/process.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/process.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/redirect.c delete mode 100644 extern/reproc-14.2.4/reproc/src/redirect.h delete mode 100644 extern/reproc-14.2.4/reproc/src/redirect.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/redirect.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/src/reproc.c delete mode 100644 extern/reproc-14.2.4/reproc/src/run.c delete mode 100644 extern/reproc-14.2.4/reproc/src/strv.c delete mode 100644 extern/reproc-14.2.4/reproc/src/strv.h delete mode 100644 extern/reproc-14.2.4/reproc/src/utf.h delete mode 100644 extern/reproc-14.2.4/reproc/src/utf.posix.c delete mode 100644 extern/reproc-14.2.4/reproc/src/utf.windows.c delete mode 100644 extern/reproc-14.2.4/reproc/test/argv.c delete mode 100644 extern/reproc-14.2.4/reproc/test/assert.h delete mode 100644 extern/reproc-14.2.4/reproc/test/deadline.c delete mode 100644 extern/reproc-14.2.4/reproc/test/env.c delete mode 100644 extern/reproc-14.2.4/reproc/test/fork.c delete mode 100644 extern/reproc-14.2.4/reproc/test/io.c delete mode 100644 extern/reproc-14.2.4/reproc/test/overflow.c delete mode 100644 extern/reproc-14.2.4/reproc/test/path.c delete mode 100644 extern/reproc-14.2.4/reproc/test/pid.c delete mode 100644 extern/reproc-14.2.4/reproc/test/stop.c delete mode 100644 extern/reproc-14.2.4/reproc/test/working-directory.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 69d150e5b..206da4b41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.12 FATAL_ERROR) option(USE_VCPKG "Use Vcpkg for package management" OFF) if(WIN32) set(USE_VCPKG ON) + set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") endif() if (USE_VCPKG) @@ -14,7 +15,7 @@ set(XSTUDIO_GLOBAL_NAME xStudio) project(${XSTUDIO_GLOBAL_NAME} VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) -cmake_policy(VERSION 3.27) +cmake_policy(VERSION 3.26) option(BUILD_TESTING "Build tests" OFF) option(INSTALL_PYTHON_MODULE "Install python module" ON) @@ -73,6 +74,7 @@ set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) if(WIN32) set(CMAKE_CXX_STANDARD 20) + add_compile_definitions($<$:_ITERATOR_DEBUG_LEVEL=0>) else() set(CMAKE_CXX_STANDARD 17) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 08666d6eb..b689bc75e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -11,7 +11,9 @@ "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF" + "BUILD_DOCS": "OFF", + "CMAKE_BUILD_PARALLEL_LEVEL": "64", + "CMAKE_AUTOGEN_PARALLEL": "64" } } ] diff --git a/cmake/macros.cmake b/cmake/macros.cmake index b9397a2eb..1f5372e50 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -278,6 +278,11 @@ 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}) @@ -387,11 +392,12 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) ) 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 - ) + ## 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 + #) # Generate export header include(GenerateExportHeader) 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-14.2.4/.clang-format deleted file mode 100644 index df06971c7..000000000 --- a/extern/reproc-14.2.4/.clang-format +++ /dev/null @@ -1,22 +0,0 @@ ---- -BasedOnStyle: LLVM ---- -Language: Cpp -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortFunctionsOnASingleLine: Empty -AlwaysBreakTemplateDeclarations: Yes -BinPackParameters: false -BreakBeforeBraces: Custom -BraceWrapping: - AfterFunction: true - SplitEmptyRecord: false -ConstructorInitializerAllOnOneLineOrOnePerLine: true -Cpp11BracedListStyle: false -FixNamespaceComments: false -ForEachMacros: ['STRV_FOREACH', 'NULSTR_FOREACH'] -IndentCaseLabels: true -IndentPPDirectives: BeforeHash -PenaltyBreakAssignment: 100 -PenaltyBreakBeforeFirstCallParameter: 100 -SpaceAfterCStyleCast: true -SpaceBeforeParens: ControlStatementsExceptForEachMacros diff --git a/extern/reproc-14.2.4/.clang-tidy b/extern/reproc-14.2.4/.clang-tidy deleted file mode 100644 index 35bb58f09..000000000 --- a/extern/reproc-14.2.4/.clang-tidy +++ /dev/null @@ -1,40 +0,0 @@ ---- -Checks: -' -*, --altera-*, --android-cloexec-fopen, --android-cloexec-pipe, --bugprone-easily-swappable-parameters, --bugprone-exception-escape, --bugprone-reserved-identifier, --cert-*, --clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, --clang-diagnostic-unused-command-line-argument, --concurrency-mt-unsafe, --cppcoreguidelines-avoid-c-arrays, --cppcoreguidelines-avoid-magic-numbers, --cppcoreguidelines-macro-usage, --cppcoreguidelines-owning-memory, --cppcoreguidelines-pro-bounds-array-to-pointer-decay, --cppcoreguidelines-pro-bounds-pointer-arithmetic, --cppcoreguidelines-pro-type-reinterpret-cast, --cppcoreguidelines-pro-type-vararg, --cppcoreguidelines-special-member-functions, --fuchsia-*, --llvmlibc-*, --google-*, --hicpp-*, --llvm-else-after-return, --llvm-header-guard, --llvm-namespace-comment, --misc-no-recursion, --modernize-avoid-c-arrays, --modernize-use-trailing-return-type, --performance-no-int-to-ptr, --readability-else-after-return, --readability-function-cognitive-complexity, --readability-magic-numbers, -' -HeaderFilterRegex: '.*reproc\+\+.*$' -... diff --git a/extern/reproc-14.2.4/.editorconfig b/extern/reproc-14.2.4/.editorconfig deleted file mode 100644 index b1a1858f7..000000000 --- a/extern/reproc-14.2.4/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -trim_trailing_whitespace = true -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true diff --git a/extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml b/extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 33a895a9f..000000000 --- a/extern/reproc-14.2.4/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - analyze: - name: Analyze ${{ matrix.os }} - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - language: [cpp] - os: [ubuntu-20.04, macos-latest, windows-latest] - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - - name: Configure - run: > - cmake - -B build - -DREPROC++=ON - -DREPROC_TEST=ON - -DREPROC_EXAMPLES=ON - - - name: Build - run: cmake --build build - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/extern/reproc-14.2.4/.github/workflows/main.yml b/extern/reproc-14.2.4/.github/workflows/main.yml deleted file mode 100644 index 40004546f..000000000 --- a/extern/reproc-14.2.4/.github/workflows/main.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - ci: - name: ${{ matrix.os }}-${{ matrix.compiler }} - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - compiler: gcc - - - os: ubuntu-latest - compiler: clang - - - os: windows-latest - compiler: cl - - - os: windows-latest - compiler: clang-cl - - - os: windows-latest - compiler: clang - - - os: windows-latest - compiler: gcc - - - os: macos-latest - compiler: gcc - - - os: macos-latest - compiler: clang - - steps: - - uses: actions/checkout@v1 - - - name: Install (Ubuntu) - if: runner.os == 'Linux' - run: | - sudo apt-get install -y --no-install-recommends ninja-build clang-tidy - - if [ "${{ matrix.compiler }}" = "gcc" ]; then - echo CC=gcc >> $GITHUB_ENV - echo CXX=g++ >> $GITHUB_ENV - else - echo CC=clang >> $GITHUB_ENV - echo CXX=clang++ >> $GITHUB_ENV - fi - - - name: Install (macOS) - if: runner.os == 'macOS' - run: | - brew install ninja - sudo ln -s /usr/local/opt/llvm/bin/clang-tidy /usr/local/bin/clang-tidy - - if [ "${{ matrix.compiler }}" = "gcc" ]; then - echo CC=gcc >> $GITHUB_ENV - echo CXX=g++ >> $GITHUB_ENV - else - echo CC=clang >> $GITHUB_ENV - echo CXX=clang++ >> $GITHUB_ENV - fi - - - name: Install (Windows) - if: runner.os == 'Windows' - run: | - Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') - scoop install ninja llvm --global - - if ("${{ matrix.compiler }}" -eq "gcc") { - echo CC=gcc | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - echo CXX=g++ | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - } elseif ("${{ matrix.compiler }}" -eq "clang") { - echo CC=clang | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - echo CXX=clang++ | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - } else { - echo CC=${{ matrix.compiler }} | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - echo CXX=${{ matrix.compiler }} | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - } - - # We add the output directories to the PATH to make sure the tests and - # examples can find the reproc and reproc++ DLL's. - $env:PATH += ";$pwd\build\reproc\lib" - $env:PATH += ";$pwd\build\reproc++\lib" - - # Make all PATH additions made by scoop and ourselves global. - echo "PATH=$env:PATH" | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 - - if ("${{ matrix.compiler }}".endswith("cl")) { - & .github\workflows\vsenv.ps1 -arch x64 -hostArch x64 - } - - # We build reproc as a shared library to verify all the necessary symbols - # are exported. - - # YAML folded multiline strings ('>') require the same indentation for all - # lines in order to turn newlines into spaces. - - - name: Configure - run: > - cmake - -B build - -G Ninja - -DCMAKE_BUILD_TYPE=Release - -DBUILD_SHARED_LIBS=ON - -DREPROC++=ON - -DREPROC_TEST=ON - -DREPROC_EXAMPLES=ON - -DREPROC_WARNINGS=ON - -DREPROC_WARNINGS_AS_ERRORS=ON - -DREPROC_TIDY=ON - -DREPROC_SANITIZERS=ON - - - name: Build - run: cmake --build build - - - name: Test - run: cmake --build build --target test - env: - CTEST_OUTPUT_ON_FAILURE: ON diff --git a/extern/reproc-14.2.4/.github/workflows/vsenv.ps1 b/extern/reproc-14.2.4/.github/workflows/vsenv.ps1 deleted file mode 100644 index 35b90f14d..000000000 --- a/extern/reproc-14.2.4/.github/workflows/vsenv.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -param ( - [string]$arch = "x64", - [string]$hostArch = "x64" - ) - -$vswherePath = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -$vsInstallationPath = & "$vswherePath" -latest -products * -property installationPath -$vsDevCmdPath = "`"$vsInstallationPath\Common7\Tools\vsdevcmd.bat`"" -$command = "$vsDevCmdPath -no_logo -arch=$arch -host_arch=$hostArch" - -# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt -& "${env:COMSPEC}" /s /c "$command && set" | ForEach-Object { - $name, $value = $_ -split '=', 2 - Add-Content -Path $env:GITHUB_ENV -Encoding utf8 "$name=$value" -} diff --git a/extern/reproc-14.2.4/CHANGELOG.md b/extern/reproc-14.2.4/CHANGELOG.md deleted file mode 100644 index a4821f625..000000000 --- a/extern/reproc-14.2.4/CHANGELOG.md +++ /dev/null @@ -1,1076 +0,0 @@ -# Changelog - -## 14.2.4 - -- Bugfix: Fix a memory leak in `reproc_start()` on Windows (thanks @AokiYuune). -- Bugfix: Fix a memory leak in reproc++ `array` class move constructor. -- Allow passing zero-sized array's to reproc's `input` option (thanks @lightray22). - -## 14.2.3 - -- Bugfix: Fix sign of EWOULDBLOCK error returned from `reproc_read`. - -## 14.2.2 - -- Bugfix: Disallow using `fork` option when using `reproc_run`. - -## 14.2.1 - -- Bugfix: `reproc_run` now handles forked child processes correctly. -- Bugfix: Sinks of different types can now be passed to `reproc::drain`. -- Bugfix: Processes on Windows returning negative exit codes don't cause asserts - anymore. -- Bugfix: Dependency on librt on POSIX (except osx) systems is now explicit in - CMake. -- Bugfix: Added missing stdout redirect option to reproc++. - -## 14.2.0 - -- Added `reproc_pid`/`process::pid` to get the pid of the process -- Fixed compilation error when including reproc/drain.h in C++ code -- Added missing extern "C" block to reproc/run.h - -## 14.1.0 - -- `reproc_run`/`reproc::run` now return the exit code of - `reproc_stop`/`process::stop` even if the deadline is exceeded. -- A bug where deadlines wouldn't work was fixed. - -## 14.0.0 - -- Renamed `environment` to `env`. -- Added configurable behavior to `env` option. Extra environment variables are - now added via `env.extra`. Extra environment variables now extend the parent - environment by default instead of replacing it. - -## 13.0.1 - -- Bugfix: Reset the `events` parameter of every event source if a deadline - expires when calling `reproc_poll`. -- Bugfix: Return 1 from `reproc_poll` if a deadline expires. -- Bugfix: Don't block in `reproc_read` on Windows if the `nonblocking` option - was specified. - -## 13.0.0 - -- Allow passing empty event sources to `reproc_poll`. - - If the `process` member of a `reproc_event_source` object is `NULL`, - `reproc_poll` ignores the event source. - -- Return zero from `reproc_poll` if the given timeout expires instead of - `REPROC_ETIMEDOUT`. - - In reproc, we follow the general pattern that we don't modify output arguments - if an error occurs. However, when `reproc_poll` errors, we still want to set - all events of the given event sources to zero. To signal that we're modifying - the output arguments if the timeout expires, we return zero instead of - `REPROC_ETIMEDOUT`. This is also more consistent with `poll` and `WSAPoll` - which have the same behaviour. - -- If one or more events occur, return the number of processes with events from - `reproc_poll`. - -## 12.0.0 - -### reproc - -- Put pipes in blocking mode by default. - - This allows using `reproc_read` and `reproc_write` directly without having to - figure out `reproc_poll`. - -- Add `nonblocking` option. - - Allows putting pipes back in nonblocking mode if needed. - -- Child process stderr streams are now redirected to the parent stderr stream by - default. - - Because pipes are blocking again by default, there's a (small) chance of - deadlocks if we redirect both stdout and stderr to pipes. Redirecting stderr - to the parent by default avoids that issue. - - The other (bigger) issue is that if we redirect stderr to a pipe, there's a - good chance users might forget to read from it and discard valuable error - output from the child process. - - By redirecting to the parent stderr by default, it's immediately noticeable - when a child process is not behaving according to expectations as its error - output will appear directly on the parent process stderr stream. Users can - then still decide to explicitly discard the output from the stderr stream if - needed. - -- Turn `timeout` option into an argument for `reproc_poll`. - - While deadlines can differ per process, the timeout is very likely to always - be the same so we make it an argument to the only function that uses it, - namely `reproc_poll`. - -- In `reproc_drain`, call `sink` once with an empty buffer when a stream is - closed. - - This allows sinks to handle stream closure if needed. - -- Change sinks to return `int` instead of `bool`. If a `sink` returns a non-zero - value, `reproc_drain` exits immediately with the same value. - - This change allows sinks to return their own errors without having to store - extra state. - -- `reproc_sink_string` now returns `REPROC_ENOMEM` from `reproc_drain` if a - memory allocation fails and no longer frees any output that was read - previously. - - This allows the user to still do something with the remaining output even if a - memory allocation failed. On the flipside, it is now required to always call - `reproc_free` after calling `reproc_drain` with a sink string, even if it - fails. - -- Renamed sink.h to drain.h. - - Reflect that sink.h contains `reproc_drain` by renaming it to drain.h. - -- Add `REPROC_REDIRECT_PATH` and shorthand `path` options. - -### reproc++ - -- Equivalent changes as those done for reproc. - -- Remove use of reprocxx. - - Meson gained support for CMake subprojects containing targets with special - characters so we rename directories and CMake targets back to reproc++. - -## 11.0.0 - -### General - -- Compilation now happens with compiler extensions disabled (`-std=c99` and - `-std=c++11`). - -### reproc - -- Add `inherit` and `discard` options as shorthands to set all members of the - `redirect` options to `REPROC_REDIRECT_INHERIT` and `REPROC_REDIRECT_DISCARD` - respectively. - -- Add `reproc_run` and `reproc_run_ex` which allows running a process using only - a single function. - - Running a simple process with reproc required calling `reproc_new`, - `reproc_start`, `reproc_wait` and optionally `reproc_drain` with all the - associated error handling. `reproc_run` encapsulates all this boilerplate. - -- Add `input` option that writes the given to the child process stdin pipe - before starting the process. - - This allows passing input to the process when using `reproc_run`. - -- Add `deadline` option that specifies a point in time beyond which - `reproc_poll` will return `REPROC_ETIMEDOUT`. - -- Add `REPROC_DEADLINE` that makes `reproc_wait` wait until the deadline - specified in the `deadline` option has expired. - - By default, if the `deadline` option is set, `reproc_destroy` waits until the - deadline expires before sending a `SIGTERM` signal to the child process. - -- Add (POSIX only) `fork` option that makes `reproc_start` a safe alternative to - `fork` with support for all of reproc's other features. - -- Return the amount of bytes written from `reproc_write` and stop handling - partial writes. - - Now that reproc uses nonblocking pipes, it doesn't make sense to handle - partial writes anymore. The `input` option can be used as an alternative. - `reproc_start` keeps writing until all data from `input` is written to the - child process stdin pipe or until a call to `reproc_write` returns an error. - -- Add `reproc_poll` to query child process events of one or more child - processes. - - We now support polling multiple child processes for events. `reproc_poll` - mimicks the POSIX `poll` function but instead of pollfds, it takes a list of - event sources which consist out of a process, the events we're interested in - and an output field which is filled in by `reproc_poll` that contains the - events that occurred for that child process. - -- Stop reading from both stdout and stderr in `reproc_read`. - - Because we now have `reproc_poll`, `reproc_read` was simplified to again read - from the given stream which is passed again as an argument. To avoid - deadlocks, call `reproc_poll` to figure out the first stream that has data - available to read. - -- Support polling for process exit events. - - By adding `REPROC_EVENT_EXIT` to the list of interested events, we can poll - for child process exit events. - -- Add a dependency on Winsock2 on Windows. - - To implement `reproc_poll`, we redirect to sockets on Windows which allows us - to use `WSAPoll` which is required to implement `reproc_poll`. Using sockets - on Windows requires linking with the `ws2_32` library. This dependency is - automatically handled by CMake and pkg-config. - -- Move `in`, `out` and `err` options out of `stdio` directly into `redirect`. - - This reduces the amount of boilerplate when redirecting streams. - -- Rename `REPROC_REDIRECT_INHERIT` to `REPROC_REDIRECT_PARENT`. - - `REPROC_REDIRECT_PARENT` more clearly indicates that we're redirecting to the - parent's corresponding standard stream. - -- Add support for redirecting to operating system handles. - - This allows redirecting to operating system-specific handles. On POSIX - systems, this feature expects file descriptors. On Windows, it expects - `HANDLE`s or `SOCKET`s. - -- Add support for redirecting to `FILE *`s. - - This gives users a cross-platform way to redirect standard streams to files. - -- Add support for redirecting stderr to stdout. - - For high-traffic scenarios, it doesn't make sense to allocate a separate pipe - for stderr if its output is only going to be combined with the output of - stdout. By using `REPROC_REDIRECT_STDOUT` for stderr, its output is written - directly to stdout by the child process. - -- Turn `redirect`'s `in`, `out` and `err` options into instances of the new - `reproc_redirecŧ` struct. - - An enum didn't cut it anymore for the new file and handle redirect options - since those require extra fields to allow specifying which file or handle to - redirect to. - -- Add `redirect.file` option as a shorthand for redirecting stdout and stderr to - the same file. - -### reproc++ - -- reproc++ includes mostly the same changes done to reproc so we only document - the differences. - -- Add `fork` method instead of `fork` option. - - Adding a `fork` option would have required changing `start` to return - `std::pair` to allow determining whether we're in the - parent or the child process after a fork. However, the bool return value would - only be valid when the fork option was enabled. Thus, this approach would - unnecessarily complicate all other use cases of `start` that don't require - `fork`. To solve the issue, we made `fork` a separate method instead. - -## 10.0.3 - -### reproc - -- Fixed issue where `reproc_wait` would assert when invoked with a timeout of - zero on POSIX. - -- Fixed issue where `reproc_wait` would not return `REPROC_ETIMEDOUT` when - invoked with a timeout of zero on POSIX. - -## 10.0.2 - -- Update CMake project version. - -## 10.0.1 - -### reproc - -- Pass `timeout` once via `reproc_options` instead of passing it via - `reproc_read`, `reproc_write` and `reproc_drain`. - -### reproc++ - -- Pass `timeout` once via `reproc::options` instead of passing it via - `process::read`, `process::write` and `reproc::drain`. - -## 10.0.0 - -### reproc - -- Remove `reproc_parse`. - - Instead of checking for `REPROC_EPIPE` (previously - `REPROC_ERROR_STREAM_CLOSED`), simply check if the given parser has a full - message available. If it doesn't, the output streams closed unexpectedly. - -- Remove `reproc_running` and `reproc_exit_status`. - - When calling `reproc_running`, it would wait with a zero timeout if the - process was still running and check if the wait timed out. However, a call to - wait can fail for other reasons as well which were all ignored by - `reproc_running`. Instead of `reproc_running`, A call to `reproc_wait` with a - timeout of zero should be used to check if a process is still running. - `reproc_wait` now also returns the exit status if the process exits or has - already exited which removes the need for `reproc_exit_status`. - -- Read from both stdout and stderr in `reproc_read` to avoid deadlocks and - indicate which stream `reproc_read` was read from. - - Previously, users would indicate the stream they wanted to read from when - calling `reproc_read`. However, this lead to issues with programs that write - to both stdout and stderr as a user wouldn't know whether stdout or stderr - would have output available to read. Reading from only the stdout stream - didn't work as the parent could be blocked on reading from stdout while the - child was simultaneously blocked on writing to stderr leading to a deadlock. - To get around this, users had to start up a separate thread to read from both - stdout and stderr at the same time which was a lot of extra work just to get - the output of external programs that write to both stdout and stderr. Now, - reproc takes care of avoiding the deadlock by checking which of stdout/stderr - can be read from, doing the actual read and indicating to the user which - stream was read from. - - Practically, instead of passing `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR` to - `reproc_read`, you now pass a pointer to a `REPROC_STREAM` variable instead - which `reproc_read` will set to `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR` - depending on which stream it read from. - - If both streams have been closed by the child process, `reproc_read` returns - `REPROC_EPIPE`. - - Because of the changes to `reproc_read`, `reproc_drain` now also reads from - both stdout and stderr and indicates the stream that was read from to the - given sink function via an extra argument passed to the sink. - -- Read the output of both stdout and stderr into a single contiguous - null-terminated string in `reproc_sink_string`. - -- Remove the `bytes_written` parameter of `reproc_write`. - - `reproc_write` now always writes `size` bytes to the standard input of the - child process. Partial writes do not have to be handled by users anymore and - are instead handled by reproc internally. - -- Define `_GNU_SOURCE` and `_WIN32_WINNT` only in the implementation files that - need them. - - This helps keep track of where we're using functionality that requires extra - definitions and makes building reproc in all kinds of build systems simpler as - the compiler invocations to build reproc get smaller as a result. - -- Change the error handling in the public API to return negative `errno` (POSIX) - or `GetLastError` (Windows) style values. `REPROC_ERROR` is replaced by extern - constants that are assigned the correct error value based on the platform - reproc is built for. Instead of returning `REPROC_ERROR`, most functions in - reproc's API now return `int` when they can fail. Because system errors are - now returned directly, there's no need anymore for `REPROC_ERROR` and - `reproc_error_system` and they has been removed. - - Error handling before 10.0.0: - - ```c - REPROC_ERROR error = reproc_start(...); - if (error) { - goto finish; - } - - finish: - if (error) { - fprintf(stderr, "%s", reproc_strerror(error)); - } - ``` - - Error handling from 10.0.0 onwards: - - ```c - int r = reproc_start(...); - if (r < 0) { - goto finish; - } - - finish: - if (r < 0) { - fprintf(stderr, "%s", reproc_strerror(r)); - } - ``` - -- Hide the internals of `reproc_t`. - - Instances of `reproc_t` are now allocated on the heap by calling `reproc_new`. - `reproc_destroy` releases the memory allocated by `reproc_new`. - -- Take optional arguments via the `reproc_options` struct in `reproc_start`. - - When using designated initializers, calls to `reproc_start` are now much more - readable than before. Using a struct also makes it much easier to set all - options to their default values (`reproc_options options = { 0 };`). Finally, - we can add more options in further releases without requiring existing users - to change their code. - -- Support redirecting the child process standard streams to `/dev/null` (POSIX) - or `NUL` (Windows) in `reproc_start` via the `redirect` field in - `reproc_options`. - - This is especially useful when you're not interested in the output of a child - process as redirecting to `/dev/null` doesn't require regularly flushing the - output pipes of the process to prevent deadlocks as is the case when - redirecting to pipes. - -- Support redirecting the child process standard streams to the parent process - standard streams in `reproc_starŧ` via the `redirect` field in - `reproc_options`. - - This is useful when you want to interleave child process output with the - parent process output. - -- Modify `reproc_start` and `reproc_destroy` to work like the reproc++ `process` - class constructor and destructor. - - The `stop_actions` field in `reproc_options` can be used to define up to three - stop actions that are executed when `reproc_destroy` is called if the child - process is still running. If no explicit stop actions are given, - `reproc_destroy` defaults to waiting indefinitely for the child process to - exit. - -- Return the amount of bytes read from `reproc_read` if it succeeds. - - This is made possible by the new error handling scheme. Because errors are all - negative values, we can use the positive range of an `int` as the normal - return value if no errors occur. - -- Return the exit status from `reproc_wait` and `reproc_stop` if they succeed. - - Same reasoning as above. If the child process has already exited, - `reproc_wait` and `reproc_stop` simply returns the exit status again. - -- Do nothing when `NULL` is passed to `reproc_destroy` and always return `NULL` - from `reproc_destroy`. - - This allows `reproc_destroy` to be safely called on the same instance multiple - times when assigning the result of `reproc_destroy` to the same instance - (`process = reproc_destroy(process)`). - -- Take stop actions via the `reproc_stop_actions` struct in `reproc_stop`. - - This makes it easier to store stop action configurations both in and outside - of reproc. - -- Add 256 to signal exit codes returned by `reproc_wait` and `reproc_stop`. - - This prevents conflicts with normal exit codes. - -- Add `REPROC_SIGTERM` and `REPROC_SIGKILL` constants to match against signal - exit codes. - - These also work on Windows and correspond to the exit codes returned by - sending the `CTRL-BREAK` signal and calling `TerminateProcess` respectively. - -- Rename `REPROC_CLEANUP` to `REPROC_STOP`. - - Naming the enum after the function it is passed to (`reproc_stop`) is simpler - than using a different name. - -- Rewrite tests in C using CTest and `assert` and remove doctest. - - Doctest is a great library but we don't really lose anything major by - replacing it with CTest and asserts. On the other hand, we lose a dependency, - don't need to download stuff from CMake anymore and tests compile - significantly faster. - - Tests are now executed by running `cmake --build build --target test`. - -- Return `REPROC_EINVAL` from public API functions when passed invalid - arguments. - -- Make `reproc_strerror` thread-safe. - -- Move `reproc_drain` to sink.h. - -- Make `reproc_drain` take a separate sink for each output stream. Sinks are now - passed via the `reproc_sink` type. - - Using separate sinks for both output streams allows for a lot more - flexibility. To use a single sink for both output streams, simply pass the - same sink to both the `out` and `err` arguments of `reproc_drain`. - -- Turn `reproc_sink_string` and `reproc_sink_discard` into functions that return - sinks and hide the actual functions in sink.c. - -- Add `reproc_free` to sink.h which must be used to free memory allocated by - `reproc_sink_string`. - - This avoids issues with allocating across module (DLL) boundaries on Windows. - -- Support passing timeouts to `reproc_read`, `reproc_write` and `reproc_drain`. - - Pass `REPROC_INFINITE` as the timeout to retain the old behaviour. - -- Use `int` to represent timeout values. - -- Renamed `stop_actions` field of `reproc_options` to `stop`. - -### reproc++ - -- Remove `process::parse`, `process::exit_status` and `process::running`. - - Consequence of the equivalents in reproc being removed. - -- Take separate `out` and `err` arguments in the `sink::string` and - `sink::ostream` constructors that receive output from the stdout and stderr - streams of the child process respectively. - - To combine the output from the stdout and stderr streams, simply pass the same - `string` or `ostream` to both the `out` and `err` arguments. - -- Modify `process::read` to return a tuple of the stream read from, the amount - of bytes read and an error code. The stream read from and amount of bytes read - are only valid if `process::read` succeeds. - - `std::tie` can be used pre-C++17 to assign the tuple's contents to separate - variables. - -- Modify `process::wait` and `process::stop` to return a pair of exit status and - error code. The exit status is only valid if `process::wait` or - `process::stop` succeeds. - -- Alias `reproc::error` to `std::errc`. - - As OS errors are now used everywhere, we can simply use `std::errc` for all - error handling instead of defining our own error code. - -- Add `signal::terminate` and `signal::kill` constants. - - These are aliases for `REPROC_SIGTERM` and `REPROC_SIGKILL` respectively. - -- Inline all sink implementations in sink.hpp. - -- Add `sink::thread_safe::string` which is a thread-safe version of - `sink::string`. - -- Move `process::drain` out of the `process` class and move it to sink.hpp. - - `process.drain(...)` becomes `reproc::drain(process, ...)`. - -- Make `reproc::drain` take a separate sink for each output stream. - - Same reasoning as `reproc_drain`. - -- Modify all included sinks to support the new `reproc::drain` behaviour. - -- Support passing timeouts to `process::read`, `process::write` and - `reproc::drain`. - - They still default to waiting indefinitely which matches their old behaviour. - -- Renamed `stop_actions` field of `reproc::options` to `stop`. - -### CMake - -- Drop required CMake version to CMake 3.12. -- Add CMake 3.16 as a supported CMake version. -- Build reproc++ with `-pthread` when `REPROC_MULTITHREADED` is enabled. - - See https://github.com/DaanDeMeyer/reproc/issues/24 for more information. - -- Add `REPROC_WARNINGS` option (default: `OFF`) to build with compiler warnings. -- Add `REPROC_DEVELOP` option (default: `OFF`) which enables a lot of options to - simplify developing reproc. - - By default, most of reproc's CMake options are disabled to make including - reproc in other projects as simple as possible. However, when working on - reproc, we usually wants most options enabled instead. To make enabling all - options simpler, `REPROC_DEVELOP` was added from which most other options take - their default value. As a result, enabling `REPROC_DEVELOP` enables all - options related to developing reproc. Additionally, `REPROC_DEVELOP` takes its - initial value from an environment variable of the same name so it can be set - once and always take effect whenever running CMake on reproc's source tree. - -- Add `REPROC_OBJECT_LIBRARIES` option to build CMake object libraries. - - In CMake, linking a library against a static library doesn't actually copy the - object files from the static library into the library. Instead, both static - libraries have to be installed and depended on by the final executable. By - using CMake object libraries, the object files are copied into the depending - static library and no extra artifacts are produced. - -- Enable `REPROC_INSTALL` by default unless `REPROC_OBJECT_LIBRARIES` is - enabled. - - As `REPROC_OBJECT_LIBRARIES` can now be used to depend on reproc without - generating extra artifacts, we assume that users not using - `REPROC_OBJECT_LIBRARIES` will want to install the produced artifacts. - -- Rename reproc++ to reprocxx inside the CMake build files. - - This was done to allow using reproc as a Meson subproject. Meson doesn't - accept the '+' character in target names so we use 'x' instead. - -- Modify the export headers so that the only extra define necessary is - `REPROC_SHARED` when using reproc as a shared library on Windows. - - Naturally, this define is added as a CMake usage requirement and doesn't have - to be worried about when using reproc via `add_subdirectory` or - `find_package`. - -## 9.0.0 - -### General - -- Drop support for Windows XP. - -- Add support for custom environments. - - `reproc_start` and `process::start` now take an extra `environment` parameter - that allows specifying custom environments. - - **IMPORTANT**: The `environment` parameter was inserted before the - `working_directory` parameter so make sure to update existing usages of - `reproc_start` and `process::start` so that the `environment` and - `working_directory` arguments are specified in the correct order. - - To keep the previous behaviour, pass `nullptr` as the environment to - `reproc_start`/`process::start` or use the `process::start` overload without - the `environment` parameter. - -- Remove `argc` parameter from `reproc_start` and `process::start`. - - We can trivially calculate `argc` internally in reproc since `argv` is - required to end with a `NULL` value. - -- Improve implementation of `reproc_wait` with a timeout on POSIX systems. - - Instead of spawning a new process to implement the timeout, we now use - `sigtimedwait` on Linux and `kqueue` on macOS to wait on `SIGCHLD` signals and - check if the process we're waiting on has exited after each received `SIGCHLD` - signal. - -- Remove `vfork` usage. - - Clang analyzer was indicating a host of errors in our usage of `vfork`. We - also discovered tests were behaving differently on macOS depending on whether - `vfork` was enabled or disabled. As we do not have the expertise to verify if - `vfork` is working correctly, we opt to remove it. - -- Ensure passing a custom working directory and a relative executable path - behaves consistently on all supported platforms. - - Previously, calling `reproc_start` with a relative executable path combined - with a custom working directory would behave differently depending on which - platform the code was executed on. On POSIX systems, the relative executable - path would be resolved relative to the custom working directory. On Windows, - the relative executable path would be resolved relative to the parent process - working directory. Now, relative executable paths are always resolved relative - to the parent process working directory. - -- Reimplement `reproc_drain`/`process::drain` in terms of - `reproc_parse`/`process::parse`. - - Like `reproc_parse` and `process::parse`, `reproc_drain` and `process::drain` - are now guaranteed to always be called once with an empty buffer before - reading any actual data. - - We now also guarantee that the initial empty buffer is not `NULL` or `nullptr` - so the received data and size can always be safely passed to `memcpy`. - -- Add MinGW support. - - MinGW CI builds were also added to prevent regressions in MinGW support. - -### reproc - -- Update `reproc_strerror` to return the actual system error string of the error - code returned by `reproc_system_error` instead of "system error" when passed - `REPROC_ERROR_SYSTEM` as argument. - - This should make debugging reproc errors a lot easier. - -- Add `reproc_sink_string` in `sink.h`, a sink that stores all process output in - a single null-terminated C string. - -- Add `reproc_sink_discard` in `sink.h`, a sink that discards all process - output. - -### reproc++ - -- Move sinks into `sink` namespace and remove `_sink` suffix from all sinks. - -- Add `discard` sink that discards all output read from a stream. - - This is useful when a child process produces a lot of output that we're not - interested in and cannot handle the output stream being closed or full. When - this is the case, simply start a thread that drains the stream with a - `discard` sink. - -- Update `process::start` to work with any kind of string type. - - Every string type that implements a `size` method and the index operator can - now be passed in a container to `process::start`. `working_directory` now - takes a `const char *` instead of a `std::string *`. - -- Fix compilation error when using `process::parse`. - -## 8.0.1 - -- Correctly escape arguments on Windows. - - See [#18](https://github.com/DaanDeMeyer/reproc/issues/18) for more - information. - -## 8.0.0 - -- Change `reproc_parse` and `reproc_drain` argument order. - - `context` is now the last argument instead of the first. - -- Use `uint8_t *` as buffer type instead of `char *` or `void *` - - `uint8_t *` more clearly indicates reproc is working with buffers of bytes - than `char *` and `void *`. We choose `uint8_t *` over `char *` to avoid - errors caused by passing data read by reproc directly to functions that expect - null-terminated strings (data read by reproc is not null-terminated). - -## 7.0.0 - -### General - -- Rework error handling. - - Trying to abstract platform-specific errors in `REPROC_ERROR` and - `reproc::errc` turned out to be harder than expected. On POSIX it remains very - hard to figure out which errors actually have a chance of happening and - matching `reproc::errc` values to `std::errc` values is also ambiguous and - prone to errors. On Windows, there's hardly any documentation on which system - errors functions can return so 90% of the time we were just returning - `REPROC_UNKNOWN_ERROR`. Furthermore, many operating system errors will be - fatal for most users and we suspect they'll all be handled similarly (stopping - the application or retrying). - - As a result, in this release we stop trying to abstract system errors in - reproc. All system errors in `REPROC_ERROR` were replaced by a single value - (`REPROC_ERROR_SYSTEM`). `reproc::errc` was renamed to `reproc::error` and - turned into an error code instead of an error condition and only contains the - reproc-specific errors. - - reproc users can still retrieve the specific system error using - `reproc_system_error`. - - reproc++ users can still match against specific system errors using the - `std::errc` error condition enum - () or print a string - presentation of the error using the `message` method of `std::error_code`. - - All values from `REPROC_ERROR` are now prefixed with `REPROC_ERROR` instead of - `REPROC` which helps reduce clutter in code completion. - -- Azure Pipelines CI now includes Visual Studio 2019. - -- Various smaller improvements and fixes. - -### CMake - -- Introduce `REPROC_MULTITHREADED` to configure whether reproc should link - against pthreads. - - By default, `REPROC_MULTITHREADED` is enabled to prevent accidental undefined - behaviour caused by forgetting to enable `REPROC_MULTITHREADED`. Advanced - users might want to disable `REPROC_MULTITHREADED` when they know for certain - their code won't use more than a single thread. - -- doctest is now downloaded at configure time instead of being vendored inside - the reproc repository. - - doctest is only downloaded if `REPROC_TEST` is enabled. - -## 6.0.0 - -### General - -- Added Azure Pipelines CI. - - Azure Pipelines provides 10 parallel jobs which is more than Travis and - Appveyor combined. If it turns out to be reliable Appveyor and Travis will - likely be dropped in the future. For now, all three are enabled. - -- Code cleanup and refactoring. - -### CMake - -- Renamed `REPROC_TESTS` to `REPROC_TEST`. -- Renamed test executable from `tests` to `test`. - -### reproc - -- Renamed `reproc_type` to `reproc_t`. - - We chose `reproc_type` initially because `_t` belongs to POSIX but we switch - to using `_t` because `reproc` is a sufficiently unique name that we don't - have to worry about naming conflicts. - -- reproc now keeps track of whether a process has exited and its exit status. - - Keeping track of whether the child process has exited allows us to remove the - restriction that `reproc_wait`, `reproc_terminate`, `reproc_kill` and - `reproc_stop` cannot be called again on the same process after completing - successfully once. Now, if the process has already exited, these methods don't - do anything and return `REPROC_SUCCESS`. - -- Added `reproc_running` to allow checking whether a child process is still - running. - -- Added `reproc_exit_status` to allow querying the exit status of a process - after it has exited. - -- `reproc_wait` and `reproc_stop` lost their `exit_status` output parameter. - - Use `reproc_exit_status` instead to retrieve the exit status. - -### reproc++ - -- Added `process::running` and `process::exit_status`. - - These delegate to `reproc_running` and `reproc_exit_status` respectively. - -- `process::wait` and `process::stop` lost their `exit_status` output parameter. - - Use `process::exit_status` instead. - -## 5.0.1 - -### reproc++ - -- Fixed compilation error caused by defining `reproc::process`'s move assignment - operator as default in the header which is not allowed when a - `std::unique_ptr` member of an incomplete type is present. - -## 5.0.0 - -### General - -- Added and rewrote implementation documentation. -- General refactoring and simplification of the source code. - -### CMake - -- Raised minimum CMake version to 3.13. - - Tests are now added to a single target `reproc-tests` in each subdirectory - included with `add_subdirectory`. Dependencies required to run the added tests - are added to `reproc-tests` with `target_link_libraries`. Before CMake 3.13, - `target_link_libraries` could not modify targets created outside of the - current directory which is why CMake 3.13 is needed. - -- `REPROC_CI` was renamed to `REPROC_WARNINGS_AS_ERRORS`. - - This is a side effect of upgrading cddm. The variable was renamed in cddm to - more clearly indicate its purpose. - -- Removed namespace from reproc's targets. - - To link against reproc or reproc++, you now have to link against the target - without a namespace prefix: - - ```cmake - find_package(reproc) # or add_subdirectory(external/reproc) - target_link_libraries(myapp PRIVATE reproc) - - find_package(reproc++) # or add_subdirectory(external/reproc++) - target_link_libraries(myapp PRIVATE reproc++) - ``` - - This change was made because of a change in cddm (a collection of CMake - functions to make setting up new projects easier) that removed namespacing and - aliases of library targets in favor of namespacing within the target name - itself. This change was made because the original target can still conflict - with other targets even after adding an alias. This can cause problems when - using generic names for targets inside the library itself. An example - clarifies the problem: - - Imagine reproc added a target for working with processes asynchronously. In - the previous naming scheme, we'd do the following in reproc's CMake build - files: - - ```cmake - add_library(async "") - add_library(reproc::async ALIAS async) - ``` - - However, there's a non-negligible chance that someone using reproc might also - have a target named async which would result in a conflict when using reproc - with `add_subdirectory` since there'd be two targets with the same name. With - the new naming scheme, we'd do the following instead: - - ```cmake - add_library(reproc-async "") - ``` - - This has almost zero chance of conflicting with user's target names. The - advantage is that with this scheme we can use common target names without - conflicting with user's target names which was not the case with the previous - naming scheme. - -### reproc - -- Removed undefined behaviour in Windows implementation caused by casting an int - to an unsigned int. - -- Added a note to `reproc_start` docs about the behaviour of using a executable - path relative to the working directory combined with a custom working - directory for the child process on different platforms. - -- We now retrieve the file descriptor limit in the parent process (using - `sysconf`) instead of in the child process because `sysconf` is not guaranteed - to be async-signal-safe which all functions called in a child process after - forking should be. - -- Fixed compilation issue when `ATTRIBUTE_LIST_FOUND` was undefined (#15). - -### reproc++ - -- Generified `process::start` so it works with any container of `std::string` - satisfying the - [SequenceContainer](https://en.cppreference.com/w/cpp/named_req/SequenceContainer) - interface. - -## 4.0.0 - -### General - -- Internal improvements and documentation fixes. - -### reproc - -- Added `reproc_parse` which mimics reproc++'s `process::parse`. -- Added `reproc_drain` which mimics reproc++'s `process::drain` along with an - example that explains how to use it. - - Because C doesn't support lambda's, both of these functions take a function - pointer and an extra context argument which is passed to the function pointer - each time it is called. The context argument can be used to store any data - needed by the given function pointer. - -### reproc++ - -- Renamed the `process::read` overload which takes a parser to `process::parse`. - - This breaking change was done to keep consistency with reproc where we added - `reproc_parse`. We couldn't add another `reproc_read` since C doesn't support - overloading so we made the decision to rename `process::read` to - `process::parse` instead. - -- Changed `process::drain` sinks to return a boolean instead of `void`. - - Before this change, the only way to stop draining a process was to throw an - exception from the sink. By changing sinks to return `bool`, a sink can tell - `drain` to stop if an error occurs by returning `false`. The error itself can - be stored in the sink if needed. - -## 3.1.3 - -### CMake - -- Update project version in CMakeLists.txt from 3.0.0 to the actual latest - version (3.1.3). - -## 3.1.2 - -### pkg-config - -- Fix pkg-config install prefix. - -## 3.1.0 - -### CMake - -- Added `REPROC_INSTALL_PKGCONFIG` to control whether pkg-config files are - installed or not (default: `ON`). - - The vcpkg package manager has no need for the pkg-config files so we added an - option to disable installing them. - -- Added `REPROC_INSTALL_CMAKECONFIGDIR` and `REPROC_INSTALL_PKGCONFIGDIR` to - control where cmake config files and pkg-config files are installed - respectively (default: `${CMAKE_INSTALL_LIBDIR}/cmake` and - `${CMAKE_INSTALL_LIBDIR}/pkgconfig`). - - reproc already uses the values from `GNUInstallDirs` when generating its - install rules which are cache variables that be overridden by users. However, - `GNUInstallDirs` does not include variables for the installation directories - of CMake config files and pkg-config files. vcpkg requires cmake config files - to be installed to a different directory than the directory reproc used until - now. These options were added to allow vcpkg to control where the config files - are installed to. - -## 3.0.0 - -### General - -- Removed support for Doxygen (and as a result `REPROC_DOCS`). - - All the Doxygen directives made the header docstrings rather hard to read - directly. Doxygen's output was also too complicated for a simple library such - as reproc. Finally, Doxygen doesn't really provide any intuitive support for - documenting a set of libraries. I have an idea for a Doxygen alternative using - libclang and cmark but I'm not sure when I'll be able to implement it. - -### CMake - -- Renamed `REPROCXX` option to `REPROC++`. - - `REPROCXX` was initially chosen because CMake didn't recommend using anything - other than letters and underscores for variable names. However, `REPROC++` - turns out to work without any problems so we use it since it's the expected - name for an option to build reproc++. - -- Stopped modifying the default `CMAKE_INSTALL_PREFIX` on Windows. - - In 2.0.0, when installing to the default `CMAKE_INSTALL_PREFIX`, you would end - up with `C:\Program Files (x86)\reproc` and `C:\Program Files (x86)\reproc++` - when installing reproc. In 3.0.0, the default `CMAKE_INSTALL_PREFIX` isn't - modified anymore and all libraries are installed to `CMAKE_INSTALL_PREFIX` in - exactly the same way as they are on UNIX systems (include and lib - subdirectories directly beneath the installation directory). Sticking to the - defaults makes it easy to include reproc in various package managers such as - vcpkg. - -### reproc - -- `reproc_terminate` and `reproc_kill` don't call `reproc_wait` internally - anymore. `reproc_stop` has been changed to call `reproc_wait` after calling - `reproc_terminate` or `reproc_kill` so it still behaves the same. - - Previously, calling `reproc_terminate` on a list of processes would only call - `reproc_terminate` on the next process after the previous process had exited - or the timeout had expired. This made terminating multiple processes take - longer than required. By removing the `reproc_wait` call from - `reproc_terminate`, users can first call `reproc_terminate` on all processes - before waiting for each of them with `reproc_wait` which makes terminating - multiple processes much faster. - -- Default to using `vfork` instead of `fork` on POSIX systems. - - This change was made to increase `reproc_start`'s performance when the parent - process is using a large amount of memory. In these scenario's, `vfork` can be - a lot faster than `fork`. Care is taken to make sure signal handlers in the - child don't corrupt the state of the parent process. This change induces an - extra constraint in that `set*id` functions cannot be called while a call to - `reproc_start` is in process, but this situation is rare enough that the - tradeoff for better performance seems worth it. - - A dependency on pthreads had to be added in order to safely use `vfork` (we - needed access to `pthread_sigmask`). The CMake and pkg-config files have been - updated to automatically find pthreads so users don't have to find it - themselves. - -- Renamed `reproc_error_to_string` to `reproc_strerror`. - - The C standard library has `strerror` for retrieving a string representation - of an error. By using the same function name (prefixed with reproc) for a - function that does the same for reproc's errors, new users will immediately - know what the function does. - -### reproc++ - -- reproc++ now takes timeouts as `std::chrono::duration` values (more specific - `reproc::milliseconds`) instead of unsigned ints. - - Taking the `reproc::milliseconds` type explains a lot more about the expected - argument than taking an unsigned int. C++14 also added chrono literals which - make constructing `reproc::milliseconds` values a lot more concise - (`reproc::milliseconds(2000)` => `2000ms`). diff --git a/extern/reproc-14.2.4/CMakeLists.txt b/extern/reproc-14.2.4/CMakeLists.txt deleted file mode 100644 index 07786f1f7..000000000 --- a/extern/reproc-14.2.4/CMakeLists.txt +++ /dev/null @@ -1,36 +0,0 @@ -cmake_minimum_required(VERSION 3.12...3.21) - -project( - reproc - VERSION 14.2.4 - DESCRIPTION "Cross-platform C99/C++11 process library" - HOMEPAGE_URL "https://github.com/DaanDeMeyer/reproc" - LANGUAGES C -) - -# Common options and functions separated for easier reuse in other projects. -include(cmake/reproc.cmake) - -option(REPROC++ "Build reproc++" ${REPROC_DEVELOP}) -option( - REPROC_MULTITHREADED - "Use `pthread_sigmask` and link against the system's thread library" - ON -) - -if(REPROC_MULTITHREADED) - set(THREADS_PREFER_PTHREAD_FLAG ON) - find_package(Threads REQUIRED) - set(REPROC_THREAD_LIBRARY ${CMAKE_THREAD_LIBS_INIT}) -endif() - -set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -fpic -march=nehalem") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -march=nehalem") -set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") - -add_subdirectory(reproc) - -if(REPROC++) - enable_language(CXX) - add_subdirectory(reproc++) -endif() diff --git a/extern/reproc-14.2.4/LICENSE b/extern/reproc-14.2.4/LICENSE deleted file mode 100644 index 80ebfacf5..000000000 --- a/extern/reproc-14.2.4/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) Daan De Meyer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/extern/reproc-14.2.4/README.md b/extern/reproc-14.2.4/README.md deleted file mode 100644 index 62cc299ee..000000000 --- a/extern/reproc-14.2.4/README.md +++ /dev/null @@ -1,301 +0,0 @@ -# reproc - -- [What is reproc?](#what-is-reproc) -- [Features](#features) -- [Questions](#questions) -- [Installation](#installation) -- [Dependencies](#dependencies) -- [CMake options](#cmake-options) -- [Documentation](#documentation) -- [Error handling](#error-handling) -- [Multithreading](#multithreading) -- [Gotchas](#gotchas) - -## What is reproc? - -reproc (Redirected Process) is a cross-platform C/C++ library that simplifies -starting, stopping and communicating with external programs. The main use case -is executing command line applications directly from C or C++ code and -retrieving their output. - -reproc consists out of two libraries: reproc and reproc++. reproc is a C99 -library that contains the actual code for working with external programs. -reproc++ depends on reproc and adapts its API to an idiomatic C++11 API. It also -adds a few extras that simplify working with external programs from C++. - -## Features - -- Start any program directly from C or C++ code. -- Communicate with a program via its standard streams. -- Wait for a program to exit or forcefully stop it yourself. When forcefully - stopping a process you can either allow the process to clean up its resources - or stop it immediately. -- The core library (reproc) is written in C99. An optional C++11 wrapper library - (reproc++) with extra features is available for use in C++ applications. -- Multiple installation methods. Either build reproc as part of your project or - use a system installed version of reproc. - -## Usage - -```c -#include - -int main(void) -{ - const char *args[] = { "echo", "Hello, world!", NULL }; - return reproc_run(args, (reproc_options) { 0 }); -} -``` - -## Questions - -If you have any questions after reading the readme and documentation you can -either make an issue or ask questions directly in the reproc -[gitter](https://gitter.im/reproc/Lobby) channel. - -## Installation - -**Note: Building reproc requires CMake 3.12 or higher.** - -There are multiple ways to get reproc into your project. One way is to build -reproc as part of your project using CMake. To do this, we first have to get the -reproc source code into the project. This can be done using any of the following -options: - -- When using CMake 3.11 or later, you can use the CMake `FetchContent` API to - download reproc when running CMake. See - for an - example. -- Another option is to include reproc's repository as a git submodule. - - provides more information. -- A very simple solution is to just include reproc's source code in your - repository. You can download a zip of the source code without the git history - and add it to your repository in a separate directory. - -After including reproc's source code in your project, it can be built from the -root CMakeLists.txt file as follows: - -```cmake -add_subdirectory() # For example: add_subdirectory(external/reproc) -``` - -CMake options can be specified before calling `add_subdirectory`: - -```cmake -set(REPROC++ ON) -add_subdirectory() -``` - -**Note: If the option has already been cached in a previous CMake run, you'll -have to clear CMake's cache to apply the new default value.** - -For more information on configuring reproc's build, see -[CMake options](#cmake-options). - -You can also depend on an installed version of reproc. You can either build and -install reproc yourself or install reproc via a package manager. reproc is -available in the following package repositories: - -- Arch User Repository () -- vcpkg (https://github.com/microsoft/vcpkg/tree/master/ports/reproc) - -If using a package manager is not an option, you can build and install reproc -from source (CMake 3.13+): - -```sh -cmake -B build -cmake --build build -cmake --install build -``` - -Enable the `REPROC_TEST` option and build the `test` target to run the tests -(CMake 3.13+): - -```sh -cmake -B build -DREPROC_TEST=ON -cmake --build build -cmake --build build --target test -``` - -After installing reproc your build system will have to find it. reproc provides -both CMake config files and pkg-config files to simplify finding a reproc -installation using CMake and pkg-config respectively. Note that reproc and -reproc++ are separate libraries and as a result have separate config files as -well. Make sure to search for the one you want to use. - -To find an installed version of reproc using CMake: - -```cmake -find_package(reproc) # Find reproc. -find_package(reproc++) # Find reproc++. -``` - -After building reproc as part of your project or finding a installed version of -reproc, you can link against it from within your CMakeLists.txt file as follows: - -```cmake -target_link_libraries(myapp reproc) # Link against reproc. -target_link_libraries(myapp reproc++) # Link against reproc++. -``` - -From Meson 0.53.2 onwards, reproc can be included as a CMake subproject in Meson -build scripts. See https://mesonbuild.com/CMake-module.html for more -information. - -## Dependencies - -By default, reproc has a dependency on pthreads on POSIX systems (`-pthread`) -and a dependency on Winsock 2.2 on Windows systems (`-lws2_32`). CMake and -pkg-config handle these dependencies automatically. - -## CMake options - -reproc's build can be configured using the following CMake options: - -### User - -- `REPROC++`: Build reproc++ (default: `${REPROC_DEVELOP}`) -- `REPROC_TEST`: Build tests (default: `${REPROC_DEVELOP}`) - - Run the tests by running the `test` binary which can be found in the build - directory after building reproc. - -- `REPROC_EXAMPLES`: Build examples (default: `${REPROC_DEVELOP}`) - - The resulting binaries will be located in the examples folder of each project - subdirectory in the build directory after building reproc. - -### Advanced - -- `REPROC_OBJECT_LIBRARIES`: Build CMake object libraries (default: - `${REPROC_DEVELOP}`) - - This is useful to directly include reproc in another library. When building - reproc as a static or shared library, it has to be installed alongside the - consuming library which makes distributing the consuming library harder. When - using object libraries, reproc's object files are included directly into the - consuming library and no extra installation is necessary. - - **Note: reproc's object libraries will only link correctly from CMake 3.14 - onwards.** - - **Note: This option overrides `BUILD_SHARED_LIBS`.** - -- `REPROC_INSTALL`: Generate installation rules (default: `ON` unless - `REPROC_OBJECT_LIBRARIES` is enabled) -- `REPROC_INSTALL_CMAKECONFIGDIR`: CMake config files installation directory - (default: `${CMAKE_INSTALL_LIBDIR}/cmake`) -- `REPROC_INSTALL_PKGCONFIG`: Install pkg-config files (default: `ON`) -- `REPROC_INSTALL_PKGCONFIGDIR`: pkg-config files installation directory - (default: `${CMAKE_INSTALL_LIBDIR}/pkgconfig`) - -- `REPROC_MULTITHREADED`: Use `pthread_sigmask` and link against the system's - thread library (default: `ON`) - -### Developer - -- `REPROC_DEVELOP`: Configure option default values for development (default: - `OFF` unless the `REPROC_DEVELOP` environment variable is set) -- `REPROC_SANITIZERS`: Build with sanitizers (default: `${REPROC_DEVELOP}`) -- `REPROC_TIDY`: Run clang-tidy when building (default: `${REPROC_DEVELOP}`) -- `REPROC_WARNINGS`: Enable compiler warnings (default: `${REPROC_DEVELOP}`) -- `REPROC_WARNINGS_AS_ERRORS`: Add -Werror or equivalent to the compile flags - and clang-tidy (default: `OFF`) - -## Documentation - -Each function and class is documented extensively in its header file. Examples -can be found in the examples subdirectory of [reproc](reproc/examples) and -[reproc++](reproc++/examples). - -## Error handling - -On failure, Most functions in reproc's API return a negative `errno` (POSIX) or -`GetLastError` (Windows) style error code. For actionable errors, reproc -provides constants (`REPROC_ETIMEDOUT`, `REPROC_EPIPE`, ...) that can be used to -match against the error without having to write platform-specific code. To get a -string representation of an error, pass it to `reproc_strerror`. - -reproc++'s API integrates with the C++ standard library error codes mechanism -(`std::error_code` and `std::error_condition`). Most methods in reproc++'s API -return `std::error_code` values that contain the actual system error that -occurred. You can test against these error codes using values from the -`std::errc` enum. - -See the examples for more information on how to handle errors when using reproc. - -## Multithreading - -Don't call the same operation on the same child process from more than one -thread at the same time. For example: reading and writing to a child process -from different threads is fine but waiting on the same child process from two -different threads at the same time will result in issues. - -## Gotchas - -- (POSIX) It is strongly recommended to not call `waitpid` on pids of processes - started by reproc. - - reproc uses `waitpid` to wait until a process has exited. Unfortunately, - `waitpid` cannot be called twice on the same process. This means that - `reproc_wait` won't work correctly if `waitpid` has already been called on a - child process beforehand outside of reproc. - -- It is strongly recommended to make sure each child process actually exits - using `reproc_wait` or `reproc_stop`. - - On POSIX, a child process that has exited is a zombie process until the parent - process waits on it using `waitpid`. A zombie process takes up resources and - can be seen as a resource leak so it is important to make sure all processes - exit correctly in a timely fashion. - -- It is strongly recommended to try terminating a child process by waiting for - it to exit or by calling `reproc_terminate` before resorting to `reproc_kill`. - - When using `reproc_kill` the child process does not receive a chance to - perform cleanup which could result in resources being leaked. Chief among - these leaks is that the child process will not be able to stop its own child - processes. Always try to let a child process exit normally by calling - `reproc_terminate` before calling `reproc_kill`. `reproc_stop` is a handy - helper function that can be used to perform multiple stop actions in a row - with timeouts inbetween. - -- (POSIX) It is strongly recommended to ignore the `SIGPIPE` signal in the - parent process. - - On POSIX, writing to a closed stdin pipe of a child process will terminate the - parent process with the `SIGPIPE` signal by default. To avoid this, the - `SIGPIPE` signal has to be ignored in the parent process. If the `SIGPIPE` - signal is ignored `reproc_write` will return `REPROC_EPIPE` as expected when - writing to a closed stdin pipe. - -- While `reproc_terminate` allows the child process to perform cleanup it is up - to the child process to correctly clean up after itself. reproc only sends a - termination signal to the child process. The child process itself is - responsible for cleaning up its own child processes and other resources. - -- (Windows) `reproc_kill` is not guaranteed to kill a child process immediately - on Windows. For more information, read the Remarks section in the - documentation of the Windows `TerminateProcess` function that reproc uses to - kill child processes on Windows. - -- Child processes spawned via reproc inherit a single extra file handle which is - used to wait for the child process to exit. If the child process closes this - file handle manually, reproc will wrongly detect the child process has exited. - If this handle is further inherited by other processes that outlive the child - process, reproc will detect the child process is still running even if it has - exited. If data is written to this handle, reproc will also wrongly detect the - child process has exited. - -- (Windows) It's not possible to detect if a child process closes its stdout or - stderr stream before exiting. The parent process will only be notified that a - child process output stream is closed once that child process exits. - -- (Windows) reproc assumes that Windows creates sockets that are usable as file - system objects. More specifically, the default sockets returned by `WSASocket` - should have the `XP1_IFS_HANDLES ` flag set. This might not be the case if - there are external LSP providers installed on a Windows machine. If this is - the case, we recommend removing the software that's providing the extra - service providers since they're deprecated and should not be used anymore (see - https://docs.microsoft.com/en-us/windows/win32/winsock/categorizing-layered-service-providers-and-applications). diff --git a/extern/reproc-14.2.4/cmake/reproc.cmake b/extern/reproc-14.2.4/cmake/reproc.cmake deleted file mode 100644 index d0efafc04..000000000 --- a/extern/reproc-14.2.4/cmake/reproc.cmake +++ /dev/null @@ -1,408 +0,0 @@ -include(CheckCCompilerFlag) -include(CMakePackageConfigHelpers) -include(GenerateExportHeader) -include(GNUInstallDirs) - -# Developer options - -option(REPROC_DEVELOP "Enable all developer options" $ENV{REPROC_DEVELOP}) -option(REPROC_TEST "Build tests" ${REPROC_DEVELOP}) -option(REPROC_EXAMPLES "Build examples" ${REPROC_DEVELOP}) -option(REPROC_WARNINGS "Enable compiler warnings" ${REPROC_DEVELOP}) -option(REPROC_TIDY "Run clang-tidy when building" ${REPROC_DEVELOP}) - -option( - REPROC_SANITIZERS - "Build with sanitizers on configurations that support it" - ${REPROC_DEVELOP} -) - -option( - REPROC_WARNINGS_AS_ERRORS - "Add -Werror or equivalent to the compile flags and clang-tidy" -) - -mark_as_advanced( - REPROC_TIDY - REPROC_SANITIZERS - REPROC_WARNINGS_AS_ERRORS -) - -# Installation options - -option(REPROC_OBJECT_LIBRARIES "Build CMake object libraries" ${REPROC_DEVELOP}) - -if(NOT REPROC_OBJECT_LIBRARIES) - set(REPROC_INSTALL_DEFAULT ON) -endif() - -option(REPROC_INSTALL "Generate installation rules" ${REPROC_INSTALL_DEFAULT}) -option(REPROC_INSTALL_PKGCONFIG "Install pkg-config files" ON) - -set( - REPROC_INSTALL_CMAKECONFIGDIR - ${CMAKE_INSTALL_LIBDIR}/cmake - CACHE STRING "CMake config files installation directory" -) - -set( - REPROC_INSTALL_PKGCONFIGDIR - ${CMAKE_INSTALL_LIBDIR}/pkgconfig - CACHE STRING "pkg-config files installation directory" -) - -mark_as_advanced( - REPROC_OBJECT_LIBRARIES - REPROC_INSTALL - REPROC_INSTALL_PKGCONFIG - REPROC_INSTALL_CMAKECONFIGDIR - REPROC_INSTALL_PKGCONFIGDIR -) - -# Testing - -if(REPROC_TEST) - enable_testing() -endif() - -# Build type - -if(REPROC_DEVELOP AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) - set_property( - CACHE CMAKE_BUILD_TYPE - PROPERTY STRINGS Debug Release MinSizeRel RelWithDebInfo - ) -endif() - -# clang-tidy - -if(REPROC_TIDY) - find_program(REPROC_TIDY_PROGRAM clang-tidy) - mark_as_advanced(REPROC_TIDY_PROGRAM) - - if(NOT REPROC_TIDY_PROGRAM) - message(FATAL_ERROR "clang-tidy not found") - endif() - - if(REPROC_WARNINGS_AS_ERRORS) - set(REPROC_TIDY_PROGRAM ${REPROC_TIDY_PROGRAM} -warnings-as-errors=*) - endif() -endif() - -# Functions - -function(reproc_common TARGET LANGUAGE NAME DIRECTORY) - if(LANGUAGE STREQUAL C) - set(STANDARD 99) - target_compile_features(${TARGET} PUBLIC c_std_99) - else() - # clang-tidy uses the MSVC standard library instead of MinGW's standard - # library so we have to use C++14 (because MSVC headers use C++14). - if(MINGW AND REPROC_TIDY) - set(STANDARD 14) - else() - set(STANDARD 11) - endif() - - target_compile_features(${TARGET} PUBLIC cxx_std_11) - endif() - - set_target_properties(${TARGET} PROPERTIES - ${LANGUAGE}_STANDARD ${STANDARD} - ${LANGUAGE}_STANDARD_REQUIRED ON - ${LANGUAGE}_EXTENSIONS OFF - OUTPUT_NAME "${NAME}" - RUNTIME_OUTPUT_DIRECTORY "${DIRECTORY}" - ARCHIVE_OUTPUT_DIRECTORY "${DIRECTORY}" - LIBRARY_OUTPUT_DIRECTORY "${DIRECTORY}" - ) - - if(REPROC_TIDY AND REPROC_TIDY_PROGRAM) - set_property( - TARGET ${TARGET} - # `REPROC_TIDY_PROGRAM` is a list so we surround it with quotes to pass it - # as a single argument. - PROPERTY ${LANGUAGE}_CLANG_TIDY "${REPROC_TIDY_PROGRAM}" - ) - endif() - - # Common development flags (warnings + sanitizers + colors) - - if(REPROC_WARNINGS) - if(MSVC) - check_c_compiler_flag(/permissive- REPROC_HAVE_PERMISSIVE) - - target_compile_options(${TARGET} PRIVATE - /nologo # Silence MSVC compiler version output. - $<$:/WX> # -Werror - $<$:/permissive-> - ) - - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0) - # CMake 3.15 does not add /W3 to the compiler flags by default anymore - # so we add /W4 instead. - target_compile_options(${TARGET} PRIVATE /W4) - endif() - - if(LANGUAGE STREQUAL C) - # Disable MSVC warnings that flag C99 features as non-standard. - target_compile_options(${TARGET} PRIVATE /wd4204 /wd4221) - endif() - else() - target_compile_options(${TARGET} PRIVATE - -Wall - -Wextra - -pedantic - -Wconversion - -Wsign-conversion - $<$:-Werror> - $<$:-pedantic-errors> - ) - - if(LANGUAGE STREQUAL C OR CMAKE_CXX_COMPILER_ID MATCHES Clang) - target_compile_options(${TARGET} PRIVATE -Wmissing-prototypes) - endif() - endif() - - if(WIN32) - target_compile_definitions(${TARGET} PRIVATE _CRT_SECURE_NO_WARNINGS) - endif() - - target_compile_options(${TARGET} PRIVATE - $<$<${LANGUAGE}_COMPILER_ID:GNU>:-fdiagnostics-color> - $<$<${LANGUAGE}_COMPILER_ID:Clang>:-fcolor-diagnostics> - ) - endif() - - if(REPROC_SANITIZERS AND NOT MSVC AND NOT MINGW) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0) - set_property( - TARGET ${TARGET} - PROPERTY MSVC_RUNTIME_LIBRARY MultiThreaded - ) - endif() - - target_compile_options(${TARGET} PRIVATE - -fsanitize=address,undefined - -fno-omit-frame-pointer - ) - - target_link_libraries(${TARGET} PRIVATE -fsanitize=address,undefined) - endif() -endfunction() - -function(reproc_library TARGET LANGUAGE) - if(REPROC_OBJECT_LIBRARIES) - add_library(${TARGET} OBJECT) - else() - add_library(${TARGET}) - endif() - - reproc_common(${TARGET} ${LANGUAGE} "" lib) - - if(BUILD_SHARED_LIBS AND NOT REPROC_OBJECT_LIBRARIES) - # Enable -fvisibility=hidden and -fvisibility-inlines-hidden. - set_target_properties(${TARGET} PROPERTIES - ${LANGUAGE}_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN true - ) - - # clang-tidy errors with: unknown argument: '-fno-keep-inline-dllexport' - # when enabling `VISIBILITY_INLINES_HIDDEN` on MinGW so we disable it when - # running clang-tidy on MinGW. - if(MINGW AND REPROC_TIDY) - set_property(TARGET ${TARGET} PROPERTY VISIBILITY_INLINES_HIDDEN false) - endif() - - # Disable CMake's default export definition. - set_property(TARGET ${TARGET} PROPERTY DEFINE_SYMBOL "") - - string(TOUPPER ${TARGET} TARGET_UPPER) - string(REPLACE + X TARGET_SANITIZED ${TARGET_UPPER}) - - target_compile_definitions(${TARGET} PRIVATE ${TARGET_SANITIZED}_BUILDING) - if(WIN32) - target_compile_definitions(${TARGET} PUBLIC ${TARGET_SANITIZED}_SHARED) - endif() - endif() - - # Make sure we follow the popular naming convention for shared libraries on - # UNIX systems. - set_target_properties(${TARGET} PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} - ) - - # Only use the headers from the repository when building. When installing we - # want to use the install location of the headers (e.g. /usr/include) as the - # include directory instead. - target_include_directories(${TARGET} PUBLIC - $ - ) - - # Adapted from https://codingnest.com/basic-cmake-part-2/. - # Each library is installed separately (with separate config files). - - if(REPROC_INSTALL) - - # Headers - - install( - DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT ${TARGET}-development - ) - - # Library - - install( - TARGETS ${TARGET} - EXPORT ${TARGET}-targets - RUNTIME - DESTINATION ${CMAKE_INSTALL_BINDIR} - COMPONENT ${TARGET}-runtime - LIBRARY - DESTINATION ${CMAKE_INSTALL_LIBDIR} - COMPONENT ${TARGET}-runtime - NAMELINK_COMPONENT ${TARGET}-development - ARCHIVE - DESTINATION ${CMAKE_INSTALL_LIBDIR} - COMPONENT ${TARGET}-development - INCLUDES - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - ) - - if(NOT APPLE) - set_property( - TARGET ${TARGET} - PROPERTY INSTALL_RPATH $ORIGIN - ) - endif() - - # CMake config - - configure_package_config_file( - ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}-config.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake - INSTALL_DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} - ) - - write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake - COMPATIBILITY SameMajorVersion - ) - - install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake - DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} - COMPONENT ${TARGET}-development - ) - - install( - EXPORT ${TARGET}-targets - DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} - COMPONENT ${TARGET}-development - ) - - # pkg-config - - if(REPROC_INSTALL_PKGCONFIG) - configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}.pc.in - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc - @ONLY - ) - - install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc - DESTINATION ${REPROC_INSTALL_PKGCONFIGDIR} - COMPONENT ${TARGET}-development - ) - endif() - endif() -endfunction() - -function(reproc_test TARGET NAME LANGUAGE) - if(NOT REPROC_TEST) - return() - endif() - - if(LANGUAGE STREQUAL C) - set(EXTENSION c) - else() - set(EXTENSION cpp) - endif() - - add_executable(${TARGET}-test-${NAME} test/${NAME}.${EXTENSION}) - - reproc_common(${TARGET}-test-${NAME} ${LANGUAGE} ${NAME} test) - target_link_libraries(${TARGET}-test-${NAME} PRIVATE ${TARGET}) - - if(MINGW) - target_compile_definitions(${TARGET}-test-${NAME} PRIVATE - __USE_MINGW_ANSI_STDIO=1 # Add %zu on Mingw - ) - endif() - - add_test(NAME ${TARGET}-test-${NAME} COMMAND ${TARGET}-test-${NAME}) - - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c) - target_compile_definitions(${TARGET}-test-${NAME} PRIVATE - RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources" - ) - - if (NOT TARGET ${TARGET}-resource-${NAME}) - add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c) - reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources) - endif() - - # Make sure the test resource is available when running the test. - add_dependencies(${TARGET}-test-${NAME} ${TARGET}-resource-${NAME}) - endif() -endfunction() - -function(reproc_example TARGET NAME LANGUAGE) - cmake_parse_arguments(OPT "" "" "ARGS;DEPENDS" ${ARGN}) - if(NOT REPROC_EXAMPLES) - return() - endif() - - if(LANGUAGE STREQUAL C) - set(EXTENSION c) - else() - set(EXTENSION cpp) - endif() - - add_executable(${TARGET}-example-${NAME} examples/${NAME}.${EXTENSION}) - - reproc_common(${TARGET}-example-${NAME} ${LANGUAGE} ${NAME} examples) - target_link_libraries(${TARGET}-example-${NAME} PRIVATE ${TARGET} ${OPT_DEPENDS}) - - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c) - target_compile_definitions(${TARGET}-example-${NAME} PRIVATE - RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources" - ) - - if (NOT TARGET ${TARGET}-resource-${NAME}) - add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c) - reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources) - endif() - - # Make sure the example resource is available when running the example. - add_dependencies(${TARGET}-example-${NAME} ${TARGET}-resource-${NAME}) - endif() - - if(REPROC_TEST) - if(NOT DEFINED OPT_ARGS) - set(OPT_ARGS cmake --help) - endif() - - add_test( - NAME ${TARGET}-example-${NAME} - COMMAND ${TARGET}-example-${NAME} ${OPT_ARGS} - ) - endif() -endfunction() diff --git a/extern/reproc-14.2.4/reproc++/CMakeLists.txt b/extern/reproc-14.2.4/reproc++/CMakeLists.txt deleted file mode 100644 index f0c5efa8a..000000000 --- a/extern/reproc-14.2.4/reproc++/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -reproc_library(reproc++ CXX) - -target_link_libraries(reproc++ PRIVATE - reproc - $<$:Threads::Threads> -) - -target_sources( - reproc++ - PRIVATE src/reproc.cpp - # We manually propagate reproc's object files until CMake adds support for - # doing it automatically. - INTERFACE $<$:$> -) - -reproc_example(reproc++ drain CXX) -reproc_example(reproc++ forward CXX) -reproc_example(reproc++ run CXX) - -if(REPROC_MULTITHREADED) - reproc_example(reproc++ background CXX DEPENDS Threads::Threads) -endif() diff --git a/extern/reproc-14.2.4/reproc++/examples/background.cpp b/extern/reproc-14.2.4/reproc++/examples/background.cpp deleted file mode 100644 index 560bf130d..000000000 --- a/extern/reproc-14.2.4/reproc++/examples/background.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include -#include -#include - -#include -#include - -static int fail(std::error_code ec) -{ - std::cerr << ec.message(); - return ec.value(); -} - -// The background example reads the output of a child process in a background -// thread and shows how to access the current output in the main thread while -// the background thread is still running. - -// Like the forward example it forwards its arguments to a child process and -// prints the child process output on stdout. -int main(int argc, const char **argv) -{ - if (argc <= 1) { - std::cerr << "No arguments provided. Example usage: " - << "./background cmake --help"; - return EXIT_FAILURE; - } - - reproc::process process; - - reproc::stop_actions stop = { - { reproc::stop::terminate, reproc::milliseconds(5000) }, - { reproc::stop::kill, reproc::milliseconds(2000) }, - {} - }; - - reproc::options options; - options.stop = stop; - - std::error_code ec = process.start(argv + 1, options); - - if (ec == std::errc::no_such_file_or_directory) { - std::cerr << "Program not found. Make sure it's available from the PATH."; - return ec.value(); - } else if (ec) { - return fail(ec); - } - - // We need a mutex along with `output` to prevent the main thread and - // background thread from modifying `output` at the same time (`std::string` - // is not thread safe). - std::string output; - std::mutex mutex; - - auto drain_async = std::async(std::launch::async, [&process, &output, - &mutex]() { - // `sink::thread_safe::string` locks a given mutex before appending to the - // given string, allowing working with the string across multiple threads if - // the mutex is locked in the other threads as well. - reproc::sink::thread_safe::string sink(output, mutex); - return reproc::drain(process, sink, sink); - }); - - // Show new output every 2 seconds. - while (drain_async.wait_for(std::chrono::seconds(2)) != - std::future_status::ready) { - std::lock_guard lock(mutex); - std::cout << output; - // Clear output that's already been flushed to `std::cout`. - output.clear(); - } - - // Flush any remaining output of `process`. - std::cout << output; - - // Check if any errors occurred in the background thread. - ec = drain_async.get(); - if (ec) { - return fail(ec); - } - - int status = 0; - std::tie(status, ec) = process.stop(options.stop); - if (ec) { - return fail(ec); - } - - return status; -} diff --git a/extern/reproc-14.2.4/reproc++/examples/drain.cpp b/extern/reproc-14.2.4/reproc++/examples/drain.cpp deleted file mode 100644 index 77837059f..000000000 --- a/extern/reproc-14.2.4/reproc++/examples/drain.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include -#include - -#include -#include - -static int fail(std::error_code ec) -{ - std::cerr << ec.message(); - return ec.value(); -} - -// Uses `reproc::drain` to show the output of the given command. -int main(int argc, const char **argv) -{ - if (argc <= 1) { - std::cerr << "No arguments provided. Example usage: " - << "./drain cmake --help"; - return EXIT_FAILURE; - } - - reproc::process process; - - // reproc++ uses error codes to report errors. If exceptions are preferred, - // convert `std::error_code`'s to exceptions using `std::system_error`. - std::error_code ec = process.start(argv + 1); - - // reproc++ converts system errors to `std::error_code`'s of the system - // category. These can be matched against using values from the `std::errc` - // error condition. See https://en.cppreference.com/w/cpp/error/errc for more - // information. - if (ec == std::errc::no_such_file_or_directory) { - std::cerr << "Program not found. Make sure it's available from the PATH."; - return ec.value(); - } else if (ec) { - return fail(ec); - } - - // `reproc::drain` reads from the stdout and stderr streams of `process` until - // both are closed or an error occurs. Providing it with a string sink for a - // specific stream makes it store all output of that stream in the string - // passed to the string sink. Passing the same sink to both the `out` and - // `err` arguments of `reproc::drain` causes the stdout and stderr output to - // get stored in the same string. - std::string output; - reproc::sink::string sink(output); - // By default, reproc only redirects stdout to a pipe and not stderr so we - // pass `reproc::sink::null` as the sink for stderr here. We could also pass - // `sink` but it wouldn't receive any data from stderr. - ec = reproc::drain(process, sink, reproc::sink::null); - if (ec) { - return fail(ec); - } - - std::cout << output << std::flush; - - // It's easy to define your own sinks as well. Take a look at `drain.hpp` in - // the repository to see how `sink::string` and other sinks are implemented. - // The documentation of `reproc::drain` also provides more information on the - // requirements a sink should fulfill. - - // By default, The `process` destructor waits indefinitely for the child - // process to exit to ensure proper cleanup. See the forward example for - // information on how this can be configured. However, when relying on the - // `process` destructor, we cannot check the exit status of the process so it - // usually makes sense to explicitly wait for the process to exit and check - // its exit status. - - int status = 0; - std::tie(status, ec) = process.wait(reproc::infinite); - if (ec) { - return fail(ec); - } - - return status; -} diff --git a/extern/reproc-14.2.4/reproc++/examples/forward.cpp b/extern/reproc-14.2.4/reproc++/examples/forward.cpp deleted file mode 100644 index b0d7feb1c..000000000 --- a/extern/reproc-14.2.4/reproc++/examples/forward.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include -#include - -static int fail(std::error_code ec) -{ - std::cerr << ec.message(); - return ec.value(); -} - -// The forward example forwards the program arguments to a child process and -// prints its output on stdout. -// -// Example: "./forward cmake --help" will print CMake's help output. -// -// This program can be used to verify that manually executing a command and -// executing it using reproc produces the same output. -int main(int argc, const char **argv) -{ - if (argc <= 1) { - std::cerr << "No arguments provided. Example usage: ./forward cmake --help"; - return EXIT_FAILURE; - } - - reproc::process process; - - // Stop actions can be passed to both `process::start` (via `options`) and - // `process::stop`. Stop actions passed to `process::start` are passed to - // `process::stop` in the `process` destructor. This can be used to make sure - // that a child process is always stopped correctly when its corresponding - // `process` instance is destroyed. - // - // Any program can be started with forward so we make sure the child process - // is cleaned up correctly by specifying `reproc::terminate` which sends - // `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waits five seconds. We also - // add the `reproc::kill` flag which sends `SIGKILL` (POSIX) or calls - // `TerminateProcess` (Windows) if the process hasn't exited after five - // seconds and waits two more seconds for the child process to exit. - // - // If the `stop_actions` struct passed to `process::start` is - // default-initialized, the `process` destructor will wait indefinitely for - // the child process to exit. - // - // Note that C++14 has chrono literals which allows - // `reproc::milliseconds(5000)` to be replaced with `5000ms`. - reproc::stop_actions stop = { - { reproc::stop::noop, reproc::milliseconds(0) }, - { reproc::stop::terminate, reproc::milliseconds(5000) }, - { reproc::stop::kill, reproc::milliseconds(2000) } - }; - - reproc::options options; - options.stop = stop; - - // We have the child process inherit the parent's standard streams so the - // child process reads directly from the stdin and writes directly to the - // stdout/stderr of the parent process. - options.redirect.parent = true; - - // Exclude `argv[0]` which is the current program's name. - std::error_code ec = process.start(argv + 1, options); - - if (ec == std::errc::no_such_file_or_directory) { - std::cerr << "Program not found. Make sure it's available from the PATH."; - return ec.value(); - } else if (ec) { - return fail(ec); - } - - // Call `process::stop` manually so we can access the exit status. We add - // `reproc::wait` with a timeout of ten seconds to give the process time to - // exit on its own before sending `SIGTERM`. - options.stop.first = { reproc::stop::wait, reproc::milliseconds(10000) }; - - int status = 0; - std::tie(status, ec) = process.stop(options.stop); - if (ec) { - return fail(ec); - } - - return status; -} diff --git a/extern/reproc-14.2.4/reproc++/examples/run.cpp b/extern/reproc-14.2.4/reproc++/examples/run.cpp deleted file mode 100644 index b88c59523..000000000 --- a/extern/reproc-14.2.4/reproc++/examples/run.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#include - -// Equivalent to reproc's run example but implemented using reproc++. -int main(int argc, const char **argv) -{ - (void) argc; - - int status = -1; - std::error_code ec; - - reproc::options options; - options.redirect.parent = true; - options.deadline = reproc::milliseconds(5000); - - std::tie(status, ec) = reproc::run(argv + 1, options); - - if (ec) { - std::cerr << ec.message() << std::endl; - } - - return ec ? ec.value() : status; -} diff --git a/extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp deleted file mode 100644 index c542fafc0..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/arguments.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/detail/array.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/detail/array.hpp deleted file mode 100644 index a4081471a..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/detail/array.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp deleted file mode 100644 index 553f12755..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/detail/type_traits.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/drain.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/drain.hpp deleted file mode 100644 index 90ad2efa9..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/drain.hpp +++ /dev/null @@ -1,152 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/env.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/env.hpp deleted file mode 100644 index 144f41dc9..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/env.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/export.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/export.hpp deleted file mode 100644 index 3eb0af0e6..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/export.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/input.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/input.hpp deleted file mode 100644 index e69049d26..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/input.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/reproc.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/reproc.hpp deleted file mode 100644 index ab6f1394a..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/reproc.hpp +++ /dev/null @@ -1,223 +0,0 @@ -#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-14.2.4/reproc++/include/reproc++/run.hpp b/extern/reproc-14.2.4/reproc++/include/reproc++/run.hpp deleted file mode 100644 index 196121f72..000000000 --- a/extern/reproc-14.2.4/reproc++/include/reproc++/run.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#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-14.2.4/reproc++/reproc++-config.cmake.in deleted file mode 100644 index 4406ad310..000000000 --- a/extern/reproc-14.2.4/reproc++/reproc++-config.cmake.in +++ /dev/null @@ -1,13 +0,0 @@ -@PACKAGE_INIT@ - -set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@) - -include(CMakeFindDependencyMacro) -find_dependency(reproc @PROJECT_VERSION@) - -if(REPROC_MULTITHREADED) - set(THREADS_PREFER_PTHREAD_FLAG ON) - find_dependency(Threads) -endif() - -include(${CMAKE_CURRENT_LIST_DIR}/@TARGET@-targets.cmake) diff --git a/extern/reproc-14.2.4/reproc++/reproc++.pc.in b/extern/reproc-14.2.4/reproc++/reproc++.pc.in deleted file mode 100644 index d1648c982..000000000 --- a/extern/reproc-14.2.4/reproc++/reproc++.pc.in +++ /dev/null @@ -1,13 +0,0 @@ -prefix=@CMAKE_INSTALL_PREFIX@ -exec_prefix=${prefix} -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ - -Name: @TARGET@ -Description: @PROJECT_DESCRIPTION@ -URL: @PROJECT_HOMEPAGE_URL@ -Version: @PROJECT_VERSION@ -Cflags: -I${includedir} -Libs: -L${libdir} -l@TARGET@ -Libs.private: @REPROC_THREAD_LIBRARY@ -Requires.private: reproc = @PROJECT_VERSION@ diff --git a/extern/reproc-14.2.4/reproc++/src/reproc.cpp b/extern/reproc-14.2.4/reproc++/src/reproc.cpp deleted file mode 100644 index e4eed1a73..000000000 --- a/extern/reproc-14.2.4/reproc++/src/reproc.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include - -#include - -namespace reproc { - -namespace signal { - -const int kill = REPROC_SIGKILL; -const int terminate = REPROC_SIGTERM; - -} - -const milliseconds infinite = milliseconds(REPROC_INFINITE); -const milliseconds deadline = milliseconds(REPROC_DEADLINE); - -static std::error_code error_code_from(int r) -{ - if (r >= 0) { - return {}; - } - - if (r == REPROC_EPIPE) { - // https://github.com/microsoft/STL/pull/406 - return { static_cast(std::errc::broken_pipe), - std::generic_category() }; - } - - return { -r, std::system_category() }; -} - -static reproc_stop_actions reproc_stop_actions_from(stop_actions stop) -{ - return { - { static_cast(stop.first.action), stop.first.timeout.count() }, - { static_cast(stop.second.action), - stop.second.timeout.count() }, - { static_cast(stop.third.action), stop.third.timeout.count() } - }; -} - -static reproc_redirect reproc_redirect_from(redirect redirect) -{ - return { static_cast(redirect.type), redirect.handle, - redirect.file, redirect.path }; -} - -static reproc_options reproc_options_from(const options &options, bool fork) -{ - return { - options.working_directory, - { static_cast(options.env.behavior), options.env.extra.data() }, - { reproc_redirect_from(options.redirect.in), - reproc_redirect_from(options.redirect.out), - reproc_redirect_from(options.redirect.err), options.redirect.parent, - options.redirect.discard, options.redirect.file, options.redirect.path }, - reproc_stop_actions_from(options.stop), - options.deadline.count(), - { options.input.data(), options.input.size() }, - options.nonblocking, - fork - }; -} - -process::process() : impl_(reproc_new(), reproc_destroy) {} -process::~process() noexcept = default; - -process::process(process &&other) noexcept = default; -process &process::operator=(process &&other) noexcept = default; - -std::error_code process::start(const arguments &arguments, - const options &options) noexcept -{ - reproc_options reproc_options = reproc_options_from(options, false); - int r = reproc_start(impl_.get(), arguments.data(), reproc_options); - return error_code_from(r); -} - -std::pair process::fork(const options &options) noexcept -{ - reproc_options reproc_options = reproc_options_from(options, true); - int r = reproc_start(impl_.get(), nullptr, reproc_options); - return { r == 0, error_code_from(r) }; -} - -std::pair process::poll(int interests, - milliseconds timeout) -{ - event::source source{ *this, interests, 0 }; - std::error_code ec = ::reproc::poll(&source, 1, timeout); - return { source.events, ec }; -} - -std::pair -process::read(stream stream, uint8_t *buffer, size_t size) noexcept -{ - int r = reproc_read(impl_.get(), static_cast(stream), buffer, - size); - return { r, error_code_from(r) }; -} - -std::pair process::write(const uint8_t *buffer, - size_t size) noexcept -{ - int r = reproc_write(impl_.get(), buffer, size); - return { r, error_code_from(r) }; -} - -std::error_code process::close(stream stream) noexcept -{ - int r = reproc_close(impl_.get(), static_cast(stream)); - return error_code_from(r); -} - -std::pair process::wait(milliseconds timeout) noexcept -{ - int r = reproc_wait(impl_.get(), timeout.count()); - return { r, error_code_from(r) }; -} - -std::error_code process::terminate() noexcept -{ - int r = reproc_terminate(impl_.get()); - return error_code_from(r); -} - -std::error_code process::kill() noexcept -{ - int r = reproc_kill(impl_.get()); - return error_code_from(r); -} - -std::pair process::stop(stop_actions stop) noexcept -{ - int r = reproc_stop(impl_.get(), reproc_stop_actions_from(stop)); - return { r, error_code_from(r) }; -} - -std::pair process::pid() noexcept -{ - int r = reproc_pid(impl_.get()); - return { r, error_code_from(r) }; -} - -std::error_code -poll(event::source *sources, size_t num_sources, milliseconds timeout) -{ - auto *reproc_sources = new reproc_event_source[num_sources]; - - for (size_t i = 0; i < num_sources; i++) { - reproc_sources[i] = { sources[i].process.impl_.get(), sources[i].interests, - 0 }; - } - - int r = reproc_poll(reproc_sources, num_sources, timeout.count()); - - if (r >= 0) { - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = reproc_sources[i].events; - } - } - - delete[] reproc_sources; - - return error_code_from(r); -} - -} diff --git a/extern/reproc-14.2.4/reproc/CMakeLists.txt b/extern/reproc-14.2.4/reproc/CMakeLists.txt deleted file mode 100644 index 949cc88c0..000000000 --- a/extern/reproc-14.2.4/reproc/CMakeLists.txt +++ /dev/null @@ -1,62 +0,0 @@ -if(WIN32) - set(REPROC_WINSOCK_LIBRARY ws2_32) -elseif(NOT APPLE) - set(REPROC_RT_LIBRARY rt) # clock_gettime -endif() - -reproc_library(reproc C) - -if(REPROC_MULTITHREADED) - target_compile_definitions(reproc PRIVATE REPROC_MULTITHREADED) - target_link_libraries(reproc PRIVATE Threads::Threads) -endif() - -if(WIN32) - set(PLATFORM windows) - target_compile_definitions(reproc PRIVATE WIN32_LEAN_AND_MEAN) - target_link_libraries(reproc PRIVATE ${REPROC_WINSOCK_LIBRARY}) -else() - set(PLATFORM posix) - if(NOT APPLE) - target_link_libraries(reproc PRIVATE ${REPROC_RT_LIBRARY}) - endif() -endif() - -target_sources(reproc PRIVATE - src/clock.${PLATFORM}.c - src/drain.c - src/error.${PLATFORM}.c - src/handle.${PLATFORM}.c - src/init.${PLATFORM}.c - src/options.c - src/pipe.${PLATFORM}.c - src/process.${PLATFORM}.c - src/redirect.${PLATFORM}.c - src/redirect.c - src/reproc.c - src/run.c - src/strv.c - src/utf.${PLATFORM}.c -) - -reproc_test(reproc argv C) -reproc_test(reproc deadline C) -reproc_test(reproc env C) -reproc_test(reproc io C) -reproc_test(reproc overflow C) -reproc_test(reproc path C) -reproc_test(reproc stop C) -reproc_test(reproc working-directory C) -reproc_test(reproc pid C) - -if(UNIX) - reproc_test(reproc fork C) -endif() - -reproc_example(reproc drain C) -reproc_example(reproc env C ARGS PROJECT=REPROC) -reproc_example(reproc path C) -reproc_example(reproc poll C) -reproc_example(reproc read C) -reproc_example(reproc parent C) -reproc_example(reproc run C) diff --git a/extern/reproc-14.2.4/reproc/examples/drain.c b/extern/reproc-14.2.4/reproc/examples/drain.c deleted file mode 100644 index b4c84c6c6..000000000 --- a/extern/reproc-14.2.4/reproc/examples/drain.c +++ /dev/null @@ -1,62 +0,0 @@ -#include - -#include -#include - -// Shows the output of the given command using `reproc_drain`. -int main(int argc, const char **argv) -{ - (void) argc; - - reproc_t *process = NULL; - char *output = NULL; - int r = REPROC_ENOMEM; - - process = reproc_new(); - if (process == NULL) { - goto finish; - } - - r = reproc_start(process, argv + 1, (reproc_options){ 0 }); - if (r < 0) { - goto finish; - } - - r = reproc_close(process, REPROC_STREAM_IN); - if (r < 0) { - goto finish; - } - - // `reproc_drain` reads from a child process and passes the output to the - // given sinks. A sink consists of a function pointer and a context pointer - // which is always passed to the function. reproc provides several built-in - // sinks such as `reproc_sink_string` which stores all provided output in the - // given string. Passing the same sink to both output streams makes sure the - // output from both streams is combined into a single string. - reproc_sink sink = reproc_sink_string(&output); - // By default, reproc only redirects stdout to a pipe and not stderr so we - // pass `REPROC_SINK_NULL` as the sink for stderr here. We could also pass - // `sink` but it wouldn't receive any data from stderr. - r = reproc_drain(process, sink, REPROC_SINK_NULL); - if (r < 0) { - goto finish; - } - - printf("%s", output); - - r = reproc_wait(process, REPROC_INFINITE); - if (r < 0) { - goto finish; - } - -finish: - // Memory allocated by `reproc_sink_string` must be freed with `reproc_free`. - reproc_free(output); - reproc_destroy(process); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/examples/env.c b/extern/reproc-14.2.4/reproc/examples/env.c deleted file mode 100644 index a2382fa46..000000000 --- a/extern/reproc-14.2.4/reproc/examples/env.c +++ /dev/null @@ -1,21 +0,0 @@ -#include - -#include - -// Runs a binary as a child process that prints all its environment variables to -// stdout and exits. Additional environment variables (in the format A=B) passed -// via the command line are added to the child process environment variables. -int main(int argc, const char **argv) -{ - (void) argc; - - const char *args[] = { RESOURCE_DIRECTORY "/env", NULL }; - - int r = reproc_run(args, (reproc_options){ .env.extra = argv + 1 }); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/examples/parent.c b/extern/reproc-14.2.4/reproc/examples/parent.c deleted file mode 100644 index ab357f30b..000000000 --- a/extern/reproc-14.2.4/reproc/examples/parent.c +++ /dev/null @@ -1,42 +0,0 @@ -#include - -#include - -// Forwards the provided command to `reproc_start` and redirects the standard -// streams of the child process to the standard streams of the parent process. -int main(int argc, const char **argv) -{ - if (argc <= 1) { - fprintf(stderr, - "No arguments provided. Example usage: ./inherit cmake --help"); - return EXIT_FAILURE; - } - - reproc_t *process = NULL; - int r = REPROC_ENOMEM; - - process = reproc_new(); - if (process == NULL) { - goto finish; - } - - r = reproc_start(process, argv + 1, - (reproc_options){ .redirect.parent = true }); - if (r < 0) { - goto finish; - } - - r = reproc_wait(process, REPROC_INFINITE); - if (r < 0) { - goto finish; - } - -finish: - reproc_destroy(process); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/examples/path.c b/extern/reproc-14.2.4/reproc/examples/path.c deleted file mode 100644 index ed8421774..000000000 --- a/extern/reproc-14.2.4/reproc/examples/path.c +++ /dev/null @@ -1,18 +0,0 @@ -#include - -#include - -// Redirects the output of the given command to the reproc.out file. -int main(int argc, const char **argv) -{ - (void) argc; - - int r = reproc_run(argv + 1, - (reproc_options){ .redirect.path = "reproc.out" }); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/examples/poll.c b/extern/reproc-14.2.4/reproc/examples/poll.c deleted file mode 100644 index b0bd212f2..000000000 --- a/extern/reproc-14.2.4/reproc/examples/poll.c +++ /dev/null @@ -1,107 +0,0 @@ -#ifdef _WIN32 - #include -static void millisleep(long ms) -{ - Sleep((DWORD) ms); -} -static int getpid() -{ - return (int) GetCurrentProcessId(); -} -#else - #define _POSIX_C_SOURCE 200809L - #include - #include -static inline void millisleep(long ms) -{ - nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000, - .tv_nsec = ((ms) % 1000L) * 1000000 }, - NULL); -} -#endif - -#include -#include - -#include - -enum { NUM_CHILDREN = 20 }; - -static int parent(const char *program) -{ - reproc_event_source children[NUM_CHILDREN] = { { 0 } }; - int r = -1; - - for (int i = 0; i < NUM_CHILDREN; i++) { - reproc_t *process = reproc_new(); - - const char *args[] = { program, "child", NULL }; - - r = reproc_start(process, args, (reproc_options){ .nonblocking = true }); - if (r < 0) { - goto finish; - } - - children[i].process = process; - children[i].interests = REPROC_EVENT_OUT; - } - - for (;;) { - r = reproc_poll(children, NUM_CHILDREN, REPROC_INFINITE); - if (r < 0) { - r = r == REPROC_EPIPE ? 0 : r; - goto finish; - } - - for (int i = 0; i < NUM_CHILDREN; i++) { - if (children[i].process == NULL || !children[i].events) { - continue; - } - - uint8_t output[4096]; - r = reproc_read(children[i].process, REPROC_STREAM_OUT, output, - sizeof(output)); - if (r == REPROC_EPIPE) { - // `reproc_destroy` returns `NULL`. Event sources with their process set - // to `NULL` are ignored by `reproc_poll`. - children[i].process = reproc_destroy(children[i].process); - continue; - } - - if (r < 0) { - goto finish; - } - - output[r] = '\0'; - printf("%s\n", output); - } - } - -finish: - for (int i = 0; i < NUM_CHILDREN; i++) { - reproc_destroy(children[i].process); - } - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} - -static int child(void) -{ - srand(((unsigned int) getpid())); - int ms = rand() % NUM_CHILDREN * 4; // NOLINT - millisleep(ms); - printf("Process %i slept %i milliseconds.", getpid(), ms); - return EXIT_SUCCESS; -} - -// Starts a number of child processes that each sleep a random amount of -// milliseconds before printing a message and exiting. The parent process polls -// each of the child processes and prints their messages to stdout. -int main(int argc, const char **argv) -{ - return argc > 1 && strcmp(argv[1], "child") == 0 ? child() : parent(argv[0]); -} diff --git a/extern/reproc-14.2.4/reproc/examples/read.c b/extern/reproc-14.2.4/reproc/examples/read.c deleted file mode 100644 index 7e874649f..000000000 --- a/extern/reproc-14.2.4/reproc/examples/read.c +++ /dev/null @@ -1,108 +0,0 @@ -#include -#include - -#include - -// Prints the output of the given command using `reproc_read`. Usually, using -// `reproc_run` or `reproc_drain` is a better solution when dealing with a -// single child process. -int main(int argc, const char **argv) -{ - (void) argc; - - // `reproc_t` stores necessary information between calls to reproc's API. - reproc_t *process = NULL; - char *output = NULL; - size_t size = 0; - int r = REPROC_ENOMEM; - - process = reproc_new(); - if (process == NULL) { - goto finish; - } - - // `reproc_start` takes a child process instance (`reproc_t`), argv and - // a set of options including the working directory and environment of the - // child process. If the working directory is `NULL` the working directory of - // the parent process is used. If the environment is `NULL`, the environment - // of the parent process is used. - r = reproc_start(process, argv + 1, (reproc_options){ 0 }); - - // On failure, reproc's API functions return a negative `errno` (POSIX) or - // `GetLastError` (Windows) style error code. To check against common error - // codes, reproc provides cross platform constants such as `REPROC_EPIPE` and - // `REPROC_ETIMEDOUT`. - if (r < 0) { - goto finish; - } - - // Close the stdin stream since we're not going to write any input to the - // child process. - r = reproc_close(process, REPROC_STREAM_IN); - if (r < 0) { - goto finish; - } - - // Read the entire output of the child process. I've found this pattern to be - // the most readable when reading the entire output of a child process. The - // while loop keeps running until an error occurs in `reproc_read` (the child - // process closing its output stream is also reported as an error). - for (;;) { - uint8_t buffer[4096]; - r = reproc_read(process, REPROC_STREAM_OUT, buffer, sizeof(buffer)); - if (r < 0) { - break; - } - - // On success, `reproc_read` returns the amount of bytes read. - size_t bytes_read = (size_t) r; - - // Increase the size of `output` to make sure it can hold the new output. - // This is definitely not the most performant way to grow a buffer so keep - // that in mind. Add 1 to size to leave space for the NUL terminator which - // isn't included in `output_size`. - char *result = realloc(output, size + bytes_read + 1); - if (result == NULL) { - r = REPROC_ENOMEM; - goto finish; - } - - output = result; - - // Copy new data into `output`. - memcpy(output + size, buffer, bytes_read); - output[size + bytes_read] = '\0'; - size += bytes_read; - } - - // Check that the while loop stopped because the output stream of the child - // process was closed and not because of any other error. - if (r != REPROC_EPIPE) { - goto finish; - } - - printf("%s", output); - - // Wait for the process to exit. This should always be done since some systems - // (POSIX) don't clean up system resources allocated to a child process until - // the parent process explicitly waits for it after it has exited. - r = reproc_wait(process, REPROC_INFINITE); - if (r < 0) { - goto finish; - } - -finish: - free(output); - - // Clean up all the resources allocated to the child process (including the - // memory allocated by `reproc_new`). Unless custom stop actions are passed to - // `reproc_start`, `reproc_destroy` will first wait indefinitely for the child - // process to exit. - reproc_destroy(process); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/examples/run.c b/extern/reproc-14.2.4/reproc/examples/run.c deleted file mode 100644 index 6ea0c81cf..000000000 --- a/extern/reproc-14.2.4/reproc/examples/run.c +++ /dev/null @@ -1,19 +0,0 @@ -#include - -#include - -// Start a process from the arguments given on the command line. Inherit the -// parent's standard streams and allow the process to run for maximum 5 seconds -// before terminating it. -int main(int argc, const char **argv) -{ - (void) argc; - - int r = reproc_run(argv + 1, (reproc_options){ .deadline = 5000 }); - - if (r < 0) { - fprintf(stderr, "%s\n", reproc_strerror(r)); - } - - return abs(r); -} diff --git a/extern/reproc-14.2.4/reproc/include/reproc/drain.h b/extern/reproc-14.2.4/reproc/include/reproc/drain.h deleted file mode 100644 index 355208e68..000000000 --- a/extern/reproc-14.2.4/reproc/include/reproc/drain.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/*! Used by `reproc_drain` to provide data to the caller. Each time data is -read, `function` is called with `context`. If a sink returns a non-zero value, -`reproc_drain` will return immediately with the same value. */ -typedef struct reproc_sink { - int (*function)(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context); - void *context; -} reproc_sink; - -/*! Pass `REPROC_SINK_NULL` as the sink for output streams that have not been -redirected to a pipe. */ -REPROC_EXPORT extern const reproc_sink REPROC_SINK_NULL; - -/*! -Reads from the child process stdout and stderr until an error occurs or both -streams are closed. The `out` and `err` sinks receive the output from stdout and -stderr respectively. The same sink may be passed to both `out` and `err`. - -`reproc_drain` always starts by calling both sinks once with an empty buffer and -`stream` set to `REPROC_STREAM_IN` to give each sink the chance to process all -output from the previous call to `reproc_drain` one by one. - -When a stream is closed, its corresponding `sink` is called once with `size` set -to zero. - -Note that his function returns 0 instead of `REPROC_EPIPE` when both output -streams of the child process are closed. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int -reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err); - -/*! -Appends the output of a process (stdout and stderr) to the value of `output`. -`output` must point to either `NULL` or a NUL-terminated string. - -Calls `realloc` as necessary to make space in `output` to store the output of -the child process. Make sure to always call `reproc_free` on the value of -`output` after calling `reproc_drain` (even if it fails). - -Because the resulting sink does not store the output size, `strlen` is called -each time data is read to calculate the current size of the output. This might -cause performance problems when draining processes that produce a lot of output. - -Similarly, this sink will not work on processes that have NUL terminators in -their output because `strlen` is used to calculate the current output size. - -Returns `REPROC_ENOMEM` if a call to `realloc` fails. `output` will contain any -output read from the child process, preceeded by whatever was stored in it at -the moment its corresponding sink was passed to `reproc_drain`. - -The `drain` example shows how to use `reproc_sink_string`. -``` -*/ -REPROC_EXPORT reproc_sink reproc_sink_string(char **output); - -/*! Discards the output of a process. */ -REPROC_EXPORT reproc_sink reproc_sink_discard(void); - -/*! Calls `free` on `ptr` and returns `NULL`. Use this function to free memory -allocated by `reproc_sink_string`. This avoids issues with allocating across -module (DLL) boundaries on Windows. */ -REPROC_EXPORT void *reproc_free(void *ptr); - -#ifdef __cplusplus -} -#endif diff --git a/extern/reproc-14.2.4/reproc/include/reproc/export.h b/extern/reproc-14.2.4/reproc/include/reproc/export.h deleted file mode 100644 index 8f558c25f..000000000 --- a/extern/reproc-14.2.4/reproc/include/reproc/export.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#ifndef REPROC_EXPORT - #ifdef _WIN32 - #ifdef REPROC_SHARED - #ifdef REPROC_BUILDING - #define REPROC_EXPORT __declspec(dllexport) - #else - #define REPROC_EXPORT __declspec(dllimport) - #endif - #else - #define REPROC_EXPORT - #endif - #else - #ifdef REPROC_BUILDING - #define REPROC_EXPORT __attribute__((visibility("default"))) - #else - #define REPROC_EXPORT - #endif - #endif -#endif diff --git a/extern/reproc-14.2.4/reproc/include/reproc/reproc.h b/extern/reproc-14.2.4/reproc/include/reproc/reproc.h deleted file mode 100644 index 9c45c0923..000000000 --- a/extern/reproc-14.2.4/reproc/include/reproc/reproc.h +++ /dev/null @@ -1,530 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/*! Used to store information about a child process. `reproc_t` is an opaque -type and can be allocated and released via `reproc_new` and `reproc_destroy` -respectively. */ -typedef struct reproc_t reproc_t; - -/*! reproc error naming follows POSIX errno naming prefixed with `REPROC`. */ - -/*! An invalid argument was passed to an API function */ -REPROC_EXPORT extern const int REPROC_EINVAL; -/*! A timeout value passed to an API function expired. */ -REPROC_EXPORT extern const int REPROC_ETIMEDOUT; -/*! The child process closed one of its streams (and in the case of -stdout/stderr all of the data remaining in that stream has been read). */ -REPROC_EXPORT extern const int REPROC_EPIPE; -/*! A memory allocation failed. */ -REPROC_EXPORT extern const int REPROC_ENOMEM; -/*! A call to `reproc_read` or `reproc_write` would have blocked. */ -REPROC_EXPORT extern const int REPROC_EWOULDBLOCK; - -/*! Signal exit status constants. */ - -REPROC_EXPORT extern const int REPROC_SIGKILL; -REPROC_EXPORT extern const int REPROC_SIGTERM; - -/*! Tells a function that takes a timeout value to wait indefinitely. */ -REPROC_EXPORT extern const int REPROC_INFINITE; -/*! Tells `reproc_wait` to wait until the deadline passed to `reproc_start` -expires. */ -REPROC_EXPORT extern const int REPROC_DEADLINE; - -/*! Stream identifiers used to indicate which stream to act on. */ -typedef enum { - /*! stdin */ - REPROC_STREAM_IN, - /*! stdout */ - REPROC_STREAM_OUT, - /*! stderr */ - REPROC_STREAM_ERR, -} REPROC_STREAM; - -/*! Used to tell reproc where to redirect the streams of the child process. */ -typedef enum { - /*! Use the default redirect behavior, see the documentation for `redirect` in - `reproc_options`. */ - REPROC_REDIRECT_DEFAULT, - /*! Redirect to a pipe. */ - REPROC_REDIRECT_PIPE, - /*! Redirect to the corresponding stream from the parent process. */ - REPROC_REDIRECT_PARENT, - /*! Redirect to /dev/null (or NUL on Windows). */ - REPROC_REDIRECT_DISCARD, - /*! Redirect to child process stdout. Only valid for stderr. */ - REPROC_REDIRECT_STDOUT, - /*! Redirect to a handle (fd on Linux, HANDLE/SOCKET on Windows). */ - REPROC_REDIRECT_HANDLE, - /*! Redirect to a `FILE *`. */ - REPROC_REDIRECT_FILE, - /*! Redirect to a specific path. */ - REPROC_REDIRECT_PATH, -} REPROC_REDIRECT; - -/*! Used to tell `reproc_stop` how to stop a child process. */ -typedef enum { - /*! noop (no operation) */ - REPROC_STOP_NOOP, - /*! `reproc_wait` */ - REPROC_STOP_WAIT, - /*! `reproc_terminate` */ - REPROC_STOP_TERMINATE, - /*! `reproc_kill` */ - REPROC_STOP_KILL, -} REPROC_STOP; - -typedef struct reproc_stop_action { - REPROC_STOP action; - int timeout; -} reproc_stop_action; - -typedef struct reproc_stop_actions { - reproc_stop_action first; - reproc_stop_action second; - reproc_stop_action third; -} reproc_stop_actions; - -// clang-format off - -#define REPROC_STOP_ACTIONS_NULL (reproc_stop_actions) { \ - { REPROC_STOP_NOOP, 0 }, \ - { REPROC_STOP_NOOP, 0 }, \ - { REPROC_STOP_NOOP, 0 }, \ -} - -// clang-format on - -#if defined(_WIN32) -typedef void *reproc_handle; // `HANDLE` -#else -typedef int reproc_handle; // fd -#endif - -typedef struct reproc_redirect { - /*! Type of redirection. */ - REPROC_REDIRECT type; - /*! - Redirect a stream to an operating system handle. The given handle must be in - blocking mode ( `O_NONBLOCK` and `OVERLAPPED` handles are not supported). - - Note that reproc does not take ownership of the handle. The user is - responsible for closing the handle after passing it to `reproc_start`. Since - the operating system will copy the handle to the child process, the handle - can be closed immediately after calling `reproc_start` if the handle is not - needed in the parent process anymore. - - If `handle` is set, `type` must be unset or set to `REPROC_REDIRECT_HANDLE` - and `file`, `path` must be unset. - */ - reproc_handle handle; - /*! - Redirect a stream to a file stream. - - Note that reproc does not take ownership of the file. The user is - responsible for closing the file after passing it to `reproc_start`. Just - like with `handles`, the operating system will copy the file handle to the - child process so the file can be closed immediately after calling - `reproc_start` if it isn't needed anymore by the parent process. - - Any file passed to `file.in` must have been opened in read mode. Likewise, - any files passed to `file.out` or `file.err` must have been opened in write - mode. - - If `file` is set, `type` must be unset or set to `REPROC_REDIRECT_FILE` and - `handle`, `path` must be unset. - */ - FILE *file; - /*! - Redirect a stream to a given path. - - reproc will create or open the file at the given path. Depending on the - stream, the file is opened in read or write mode. - - If `path` is set, `type` must be unset or set to `REPROC_REDIRECT_PATH` and - `handle`, `file` must be unset. - */ - const char *path; -} reproc_redirect; - -typedef enum { - REPROC_ENV_EXTEND, - REPROC_ENV_EMPTY, -} REPROC_ENV; - -typedef struct reproc_options { - /*! - `working_directory` specifies the working directory for the child process. If - `working_directory` is `NULL`, the child process runs in the working directory - of the parent process. - */ - const char *working_directory; - - struct { - /*! - `behavior` specifies whether the child process should start with a copy of - the parent process environment variables or an empty environment. By - default, the child process starts with a copy of the parent's environment - variables (`REPROC_ENV_EXTEND`). If `behavior` is set to `REPROC_ENV_EMPTY`, - the child process starts with an empty environment. - */ - REPROC_ENV behavior; - /*! - `extra` is an array of UTF-8 encoded, NUL-terminated strings that specifies - extra environment variables for the child process. It has the following - layout: - - - All elements except the final element must be of the format `NAME=VALUE`. - - The final element must be `NULL`. - - Example: ["IP=127.0.0.1", "PORT=8080", `NULL`] - - If `env` is `NULL`, no extra environment variables are added to the - environment of the child process. - */ - const char *const *extra; - } env; - /*! - `redirect` specifies where to redirect the streams from the child process. - - By default each stream is redirected to a pipe which can be written to (stdin) - or read from (stdout/stderr) using `reproc_write` and `reproc_read` - respectively. - */ - struct { - /*! - `in`, `out` and `err` specify where to redirect the standard I/O streams of - the child process. When not set, `in` and `out` default to - `REPROC_REDIRECT_PIPE` while `err` defaults to `REPROC_REDIRECT_PARENT`. - */ - reproc_redirect in; - reproc_redirect out; - reproc_redirect err; - /*! - Use `REPROC_REDIRECT_PARENT` instead of `REPROC_REDIRECT_PIPE` when `type` - is unset. - - When this option is set, `discard`, `file` and `path` must be unset. - */ - bool parent; - /*! - Use `REPROC_REDIRECT_DISCARD` instead of `REPROC_REDIRECT_PIPE` when `type` - is unset. - - When this option is set, `parent`, `file` and `path` must be unset. - */ - bool discard; - /*! - Shorthand for redirecting stdout and stderr to the same file. - - If this option is set, `out`, `err`, `parent`, `discard` and `path` must be - unset. - */ - FILE *file; - /*! - Shorthand for redirecting stdout and stderr to the same path. - - If this option is set, `out`, `err`, `parent`, `discard` and `file` must be - unset. - */ - const char *path; - } redirect; - /*! - Stop actions that are passed to `reproc_stop` in `reproc_destroy` to stop the - child process. See `reproc_stop` for more information on how `stop` is - interpreted. - */ - reproc_stop_actions stop; - /*! - Maximum allowed duration in milliseconds the process is allowed to run in - milliseconds. If the deadline is exceeded, Any ongoing and future calls to - `reproc_poll` return `REPROC_ETIMEDOUT`. - - Note that only `reproc_poll` takes the deadline into account. More - specifically, if the `nonblocking` option is not enabled, `reproc_read` and - `reproc_write` can deadlock waiting on the child process to perform I/O. If - this is a problem, enable the `nonblocking` option and use `reproc_poll` - together with a deadline/timeout to avoid any deadlocks. - - If `REPROC_DEADLINE` is passed as the timeout to `reproc_wait`, it waits until - the deadline expires. - - When `deadline` is zero, no deadline is set for the process. - */ - int deadline; - /*! - `input` is written to the stdin pipe before the child process is started. - - Because `input` is written to the stdin pipe before the process starts, - `input.size` must be smaller than the system's default pipe size (64KB). - - If `input` is set, the stdin pipe is closed after `input` is written to it. - - If `redirect.in` is set, this option may not be set. - */ - struct { - const uint8_t *data; - size_t size; - } input; - /*! - This option can only be used on POSIX systems. If enabled on Windows, an error - will be returned. - - If `fork` is enabled, `reproc_start` forks a child process and returns 0 in - the child process and > 0 in the parent process. In the child process, only - `reproc_destroy` may be called on the `reproc_t` instance to free its - associated memory. - - When `fork` is enabled. `argv` must be `NULL` when calling `reproc_start`. - */ - bool fork; - /*! - Put pipes created by reproc in nonblocking mode. This makes `reproc_read` and - `reproc_write` nonblocking operations. If needed, use `reproc_poll` to wait - until streams becomes readable/writable. - */ - bool nonblocking; -} reproc_options; - -enum { - /*! Data can be written to stdin. */ - REPROC_EVENT_IN = 1 << 0, - /*! Data can be read from stdout. */ - REPROC_EVENT_OUT = 1 << 1, - /*! Data can be read from stderr. */ - REPROC_EVENT_ERR = 1 << 2, - /*! The process finished running. */ - REPROC_EVENT_EXIT = 1 << 3, - /*! The deadline of the process expired. This event is added by default to the - list of interested events. */ - REPROC_EVENT_DEADLINE = 1 << 4, -}; - -typedef struct reproc_event_source { - /*! Process to poll for events. */ - reproc_t *process; - /*! Events of the process that we're interested in. Takes a combo of - `REPROC_EVENT` flags. */ - int interests; - /*! Combo of `REPROC_EVENT` flags that indicate the events that occurred. This - field is filled in by `reproc_poll`. */ - int events; -} reproc_event_source; - -/*! Allocate a new `reproc_t` instance on the heap. */ -REPROC_EXPORT reproc_t *reproc_new(void); - -/*! -Starts the process specified by `argv` in the given working directory and -redirects its input, output and error streams. - -If this function does not return an error the child process will have started -running and can be inspected using the operating system's tools for process -inspection (e.g. ps on Linux). - -Every successful call to this function should be followed by a successful call -to `reproc_wait` or `reproc_stop` and a call to `reproc_destroy`. If an error -occurs during `reproc_start` all allocated resources are cleaned up before -`reproc_start` returns and no further action is required. - -`argv` is an array of UTF-8 encoded, NUL-terminated strings that specifies the -program to execute along with its arguments. It has the following layout: - -- The first element indicates the executable to run as a child process. This can -be an absolute path, a path relative to the working directory of the parent -process or the name of an executable located in the PATH. It cannot be `NULL`. -- The following elements indicate the whitespace delimited arguments passed to -the executable. None of these elements can be `NULL`. -- The final element must be `NULL`. - -Example: ["cmake", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", `NULL`] -*/ -REPROC_EXPORT int reproc_start(reproc_t *process, - const char *const *argv, - reproc_options options); - -/*! -Returns the process ID of the child or `REPROC_EINVAL` on error. - -Note that if `reproc_wait` has been called successfully on this process already, -the returned pid will be that of the just ended child process. The operating -system will have cleaned up the resources allocated to the process -and the operating system is free to reuse the same pid for another process. - -Generally, only pass the result of this function to system calls that need a -valid pid if `reproc_wait` hasn't been called successfully on the process yet. -*/ -REPROC_EXPORT int reproc_pid(reproc_t *process); - -/*! -Polls each process in `sources` for its corresponding events in `interests` and -stores events that occurred for each process in `events`. If an event source -process member is `NULL`, the event source is ignored. - -Pass `REPROC_INFINITE` to `timeout` to have `reproc_poll` wait forever for an -event to occur. - -If one or more events occur, returns the number of processes with events. If the -timeout expires, returns zero. Returns `REPROC_EPIPE` if none of the sources -have valid pipes remaining that can be polled. - -Actionable errors: -- `REPROC_EPIPE` -*/ -REPROC_EXPORT int -reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout); - -/*! -Reads up to `size` bytes into `buffer` from the child process output stream -indicated by `stream`. - -Actionable errors: -- `REPROC_EPIPE` -- `REPROC_EWOULDBLOCK` -*/ -REPROC_EXPORT int reproc_read(reproc_t *process, - REPROC_STREAM stream, - uint8_t *buffer, - size_t size); - -/*! -Writes up to `size` bytes from `buffer` to the standard input (stdin) of the -child process. - -(POSIX) By default, writing to a closed stdin pipe terminates the parent process -with the `SIGPIPE` signal. `reproc_write` will only return `REPROC_EPIPE` if -this signal is ignored by the parent process. - -Returns the amount of bytes written. If `buffer` is `NULL` and `size` is zero, -this function returns 0. - -If the standard input of the child process wasn't opened with -`REPROC_REDIRECT_PIPE`, this function returns `REPROC_EPIPE` unless `buffer` is -`NULL` and `size` is zero. - -Actionable errors: -- `REPROC_EPIPE` -- `REPROC_EWOULDBLOCK` -*/ -REPROC_EXPORT int -reproc_write(reproc_t *process, const uint8_t *buffer, size_t size); - -/*! -Closes the child process standard stream indicated by `stream`. - -This function is necessary when a child process reads from stdin until it is -closed. After writing all the input to the child process using `reproc_write`, -the standard input stream can be closed using this function. -*/ -REPROC_EXPORT int reproc_close(reproc_t *process, REPROC_STREAM stream); - -/*! -Waits `timeout` milliseconds for the child process to exit. If the child process -has already exited or exits within the given timeout, its exit status is -returned. - -If `timeout` is 0, the function will only check if the child process is still -running without waiting. If `timeout` is `REPROC_INFINITE`, this function will -wait indefinitely for the child process to exit. If `timeout` is -`REPROC_DEADLINE`, this function waits until the deadline passed to -`reproc_start` expires. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int reproc_wait(reproc_t *process, int timeout); - -/*! -Sends the `SIGTERM` signal (POSIX) or the `CTRL-BREAK` signal (Windows) to the -child process. Remember that successful calls to `reproc_wait` and -`reproc_destroy` are required to make sure the child process is completely -cleaned up. -*/ -REPROC_EXPORT int reproc_terminate(reproc_t *process); - -/*! -Sends the `SIGKILL` signal to the child process (POSIX) or calls -`TerminateProcess` (Windows) on the child process. Remember that successful -calls to `reproc_wait` and `reproc_destroy` are required to make sure the child -process is completely cleaned up. -*/ -REPROC_EXPORT int reproc_kill(reproc_t *process); - -/*! -Simplifies calling combinations of `reproc_wait`, `reproc_terminate` and -`reproc_kill`. The function executes each specified step and waits (using -`reproc_wait`) until the corresponding timeout expires before continuing with -the next step. - -Example: - -Wait 10 seconds for the child process to exit on its own before sending -`SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waiting five more seconds for -the child process to exit. - -```c -REPROC_ERROR error = reproc_stop(process, - REPROC_STOP_WAIT, 10000, - REPROC_STOP_TERMINATE, 5000, - REPROC_STOP_NOOP, 0); -``` - -Call `reproc_wait`, `reproc_terminate` and `reproc_kill` directly if you need -extra logic such as logging between calls. - -`stop` can contain up to three stop actions that instruct this function how the -child process should be stopped. The first element of each stop action specifies -which action should be called on the child process. The second element of each -stop actions specifies how long to wait after executing the operation indicated -by the first element. - -When `stop` is 3x `REPROC_STOP_NOOP`, `reproc_destroy` will wait until the -deadline expires (or forever if there is no deadline). If the process is still -running after the deadline expires, `reproc_stop` then calls `reproc_terminate` -and waits forever for the process to exit. - -Note that when a stop action specifies `REPROC_STOP_WAIT`, the function will -just wait for the specified timeout instead of performing an action to stop the -child process. - -If the child process has already exited or exits during the execution of this -function, its exit status is returned. - -Actionable errors: -- `REPROC_ETIMEDOUT` -*/ -REPROC_EXPORT int reproc_stop(reproc_t *process, reproc_stop_actions stop); - -/*! -Release all resources associated with `process` including the memory allocated -by `reproc_new`. Calling this function before a succesfull call to `reproc_wait` -can result in resource leaks. - -Does nothing if `process` is an invalid `reproc_t` instance and always returns -an invalid `reproc_t` instance (`NULL`). By assigning the result of -`reproc_destroy` to the instance being destroyed, it can be safely called -multiple times on the same instance. - -Example: `process = reproc_destroy(process)`. -*/ -REPROC_EXPORT reproc_t *reproc_destroy(reproc_t *process); - -/*! -Returns a string describing `error`. This string must not be modified by the -caller. -*/ -REPROC_EXPORT const char *reproc_strerror(int error); - -#ifdef __cplusplus -} -#endif diff --git a/extern/reproc-14.2.4/reproc/include/reproc/run.h b/extern/reproc-14.2.4/reproc/include/reproc/run.h deleted file mode 100644 index 5d8deffb5..000000000 --- a/extern/reproc-14.2.4/reproc/include/reproc/run.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/*! Sets `options.redirect.parent = true` unless `discard` is set and calls -`reproc_run_ex` with `REPROC_SINK_NULL` for the `out` and `err` sinks. */ -REPROC_EXPORT int reproc_run(const char *const *argv, reproc_options options); - -/*! -Wrapper function that starts a process with the given arguments, drain its -output and waits until it exits. Have a look at its (trivial) implementation and -the documentation of the functions it calls to see exactly what it does: -https://github.com/DaanDeMeyer/reproc/blob/master/reproc/src/run.c -*/ -REPROC_EXPORT int reproc_run_ex(const char *const *argv, - reproc_options options, - reproc_sink out, - reproc_sink err); - -#ifdef __cplusplus -} -#endif diff --git a/extern/reproc-14.2.4/reproc/reproc-config.cmake.in b/extern/reproc-14.2.4/reproc/reproc-config.cmake.in deleted file mode 100644 index b3ff831bc..000000000 --- a/extern/reproc-14.2.4/reproc/reproc-config.cmake.in +++ /dev/null @@ -1,12 +0,0 @@ -@PACKAGE_INIT@ - -set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@) - -include(CMakeFindDependencyMacro) - -if(REPROC_MULTITHREADED) - set(THREADS_PREFER_PTHREAD_FLAG ON) - find_dependency(Threads) -endif() - -include(${CMAKE_CURRENT_LIST_DIR}/@TARGET@-targets.cmake) diff --git a/extern/reproc-14.2.4/reproc/reproc.pc.in b/extern/reproc-14.2.4/reproc/reproc.pc.in deleted file mode 100644 index e2502aaac..000000000 --- a/extern/reproc-14.2.4/reproc/reproc.pc.in +++ /dev/null @@ -1,12 +0,0 @@ -prefix=@CMAKE_INSTALL_PREFIX@ -exec_prefix=${prefix} -includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ - -Name: @TARGET@ -Description: @PROJECT_DESCRIPTION@ -URL: @PROJECT_HOMEPAGE_URL@ -Version: @PROJECT_VERSION@ -Cflags: -I${includedir} -Libs: -L${libdir} -l@TARGET@ -Libs.private: @REPROC_THREAD_LIBRARY@ @REPROC_WINSOCK_LIBRARY@ @REPROC_RT_LIBRARY@ diff --git a/extern/reproc-14.2.4/reproc/resources/argv.c b/extern/reproc-14.2.4/reproc/resources/argv.c deleted file mode 100644 index 47f0861ae..000000000 --- a/extern/reproc-14.2.4/reproc/resources/argv.c +++ /dev/null @@ -1,10 +0,0 @@ -#include - -int main(int argc, const char **argv) -{ - for (int i = 0; i < argc; i++) { - printf("%s\n", argv[i]); - } - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/deadline.c b/extern/reproc-14.2.4/reproc/resources/deadline.c deleted file mode 100644 index 7454e569e..000000000 --- a/extern/reproc-14.2.4/reproc/resources/deadline.c +++ /dev/null @@ -1,7 +0,0 @@ -#include "sleep.h" - -int main(void) -{ - millisleep(25000); - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/env.c b/extern/reproc-14.2.4/reproc/resources/env.c deleted file mode 100644 index 4bd9ed781..000000000 --- a/extern/reproc-14.2.4/reproc/resources/env.c +++ /dev/null @@ -1,13 +0,0 @@ -#include - -int main(int argc, const char **argv, const char **envp) -{ - (void) argc; - (void) argv; - - for (size_t i = 0; envp[i] != NULL; i++) { - printf("%s\n", envp[i]); - } - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/io.c b/extern/reproc-14.2.4/reproc/resources/io.c deleted file mode 100644 index 9a4e5ee79..000000000 --- a/extern/reproc-14.2.4/reproc/resources/io.c +++ /dev/null @@ -1,15 +0,0 @@ -#include - -int main(void) -{ - char input[8096]; - - if (fgets(input, sizeof(input), stdin) == NULL) { - return 1; - } - - fprintf(stdout, "%s", input); - fprintf(stderr, "%s", input); - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/overflow.c b/extern/reproc-14.2.4/reproc/resources/overflow.c deleted file mode 100644 index 3f8821dd4..000000000 --- a/extern/reproc-14.2.4/reproc/resources/overflow.c +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include - -int main() -{ - char buffer[8192]; - - for (int i = 0; i < 200; i++) { - FILE *stream = rand() % 2 ? stdout : stderr; // NOLINT - fprintf(stream, "%s", buffer); - } - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/path.c b/extern/reproc-14.2.4/reproc/resources/path.c deleted file mode 100644 index 3bcf25b4a..000000000 --- a/extern/reproc-14.2.4/reproc/resources/path.c +++ /dev/null @@ -1,10 +0,0 @@ -#include - -int main(int argc, const char **argv) -{ - (void) argc; - - printf("%s", argv[0]); - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/pid.c b/extern/reproc-14.2.4/reproc/resources/pid.c deleted file mode 100644 index 33e0bef76..000000000 --- a/extern/reproc-14.2.4/reproc/resources/pid.c +++ /dev/null @@ -1,15 +0,0 @@ -#include - -#ifdef _WIN32 - #include - #define getpid (int) GetCurrentProcessId -#else - #include -#endif - -int main(void) -{ - printf("%d", getpid()); - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/sleep.h b/extern/reproc-14.2.4/reproc/resources/sleep.h deleted file mode 100644 index a53f851b7..000000000 --- a/extern/reproc-14.2.4/reproc/resources/sleep.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#ifdef _WIN32 - #include -static inline void millisleep(long ms) -{ - Sleep((DWORD) ms); -} -#else - #define _POSIX_C_SOURCE 200809L - #include -static inline void millisleep(long ms) -{ - nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000, - .tv_nsec = ((ms) % 1000L) * 1000000 }, - NULL); -} -#endif diff --git a/extern/reproc-14.2.4/reproc/resources/stop.c b/extern/reproc-14.2.4/reproc/resources/stop.c deleted file mode 100644 index 7454e569e..000000000 --- a/extern/reproc-14.2.4/reproc/resources/stop.c +++ /dev/null @@ -1,7 +0,0 @@ -#include "sleep.h" - -int main(void) -{ - millisleep(25000); - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/resources/working-directory.c b/extern/reproc-14.2.4/reproc/resources/working-directory.c deleted file mode 100644 index 5bc25c70e..000000000 --- a/extern/reproc-14.2.4/reproc/resources/working-directory.c +++ /dev/null @@ -1,21 +0,0 @@ -#include - -#if defined(_WIN32) - #include - #define getcwd _getcwd -#else - #include -#endif - -int main() -{ - char working_directory[8096]; - - if (getcwd(working_directory, sizeof(working_directory)) == NULL) { - return 1; - } - - printf("%s", working_directory); - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/src/clock.h b/extern/reproc-14.2.4/reproc/src/clock.h deleted file mode 100644 index 460e03bb3..000000000 --- a/extern/reproc-14.2.4/reproc/src/clock.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -int64_t now(void); diff --git a/extern/reproc-14.2.4/reproc/src/clock.posix.c b/extern/reproc-14.2.4/reproc/src/clock.posix.c deleted file mode 100644 index 8d22a3b27..000000000 --- a/extern/reproc-14.2.4/reproc/src/clock.posix.c +++ /dev/null @@ -1,17 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "clock.h" - -#include - -#include "error.h" - -int64_t now(void) -{ - struct timespec timespec = { 0 }; - - int r = clock_gettime(CLOCK_REALTIME, ×pec); - ASSERT_UNUSED(r == 0); - - return timespec.tv_sec * 1000 + timespec.tv_nsec / 1000000; -} diff --git a/extern/reproc-14.2.4/reproc/src/clock.windows.c b/extern/reproc-14.2.4/reproc/src/clock.windows.c deleted file mode 100644 index 3130f851f..000000000 --- a/extern/reproc-14.2.4/reproc/src/clock.windows.c +++ /dev/null @@ -1,10 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "clock.h" - -#include - -int64_t now(void) -{ - return (int64_t) GetTickCount64(); -} diff --git a/extern/reproc-14.2.4/reproc/src/drain.c b/extern/reproc-14.2.4/reproc/src/drain.c deleted file mode 100644 index 37e836afa..000000000 --- a/extern/reproc-14.2.4/reproc/src/drain.c +++ /dev/null @@ -1,121 +0,0 @@ -#include - -#include -#include - -#include "error.h" -#include "macro.h" - -int reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(out.function); - ASSERT_EINVAL(err.function); - - const uint8_t initial = 0; - int r = -1; - - // 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 one by one before reading from the child - // process again. - - r = out.function(REPROC_STREAM_IN, &initial, 0, out.context); - if (r != 0) { - return r; - } - - r = err.function(REPROC_STREAM_IN, &initial, 0, err.context); - if (r != 0) { - return r; - } - - uint8_t buffer[4096]; - - for (;;) { - reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR, - 0 }; - - r = reproc_poll(&source, 1, REPROC_INFINITE); - if (r < 0) { - r = r == REPROC_EPIPE ? 0 : r; - break; - } - - if (source.events & REPROC_EVENT_DEADLINE) { - r = REPROC_ETIMEDOUT; - break; - } - - REPROC_STREAM stream = source.events & REPROC_EVENT_OUT ? REPROC_STREAM_OUT - : REPROC_STREAM_ERR; - - r = reproc_read(process, stream, buffer, ARRAY_SIZE(buffer)); - if (r < 0 && r != REPROC_EPIPE) { - break; - } - - size_t bytes_read = r == REPROC_EPIPE ? 0 : (size_t) r; - reproc_sink sink = stream == REPROC_STREAM_OUT ? out : err; - - r = sink.function(stream, buffer, bytes_read, sink.context); - if (r != 0) { - break; - } - } - - return r; -} - -static int sink_string(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context) -{ - (void) stream; - - char **string = (char **) context; - size_t string_size = *string == NULL ? 0 : strlen(*string); - - char *r = (char *) realloc(*string, string_size + size + 1); - if (r == NULL) { - return REPROC_ENOMEM; - } - - *string = r; - memcpy(*string + string_size, buffer, size); - (*string)[string_size + size] = '\0'; - - return 0; -} - -reproc_sink reproc_sink_string(char **output) -{ - return (reproc_sink){ sink_string, output }; -} - -static int sink_discard(REPROC_STREAM stream, - const uint8_t *buffer, - size_t size, - void *context) -{ - (void) stream; - (void) buffer; - (void) size; - (void) context; - - return 0; -} - -reproc_sink reproc_sink_discard(void) -{ - return (reproc_sink){ sink_discard, NULL }; -} - -const reproc_sink REPROC_SINK_NULL = { sink_discard, NULL }; - -void *reproc_free(void *ptr) -{ - free(ptr); - return NULL; -} diff --git a/extern/reproc-14.2.4/reproc/src/error.h b/extern/reproc-14.2.4/reproc/src/error.h deleted file mode 100644 index bfc031117..000000000 --- a/extern/reproc-14.2.4/reproc/src/error.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#define ASSERT(expression) assert(expression) - -// Avoid unused assignment warnings in release mode when the result of an -// assignment is only used in an assert statement. -#define ASSERT_UNUSED(expression) \ - do { \ - (void) !(expression); \ - ASSERT((expression)); \ - } while (0) - -// Returns `r` if `expression` is false. -#define ASSERT_RETURN(expression, r) \ - do { \ - if (!(expression)) { \ - return (r); \ - } \ - } while (0) - -#define ASSERT_EINVAL(expression) ASSERT_RETURN(expression, REPROC_EINVAL) - -const char *error_string(int error); diff --git a/extern/reproc-14.2.4/reproc/src/error.posix.c b/extern/reproc-14.2.4/reproc/src/error.posix.c deleted file mode 100644 index 7c1a7c9bb..000000000 --- a/extern/reproc-14.2.4/reproc/src/error.posix.c +++ /dev/null @@ -1,31 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "error.h" - -#include -#include -#include - -#include - -#include "macro.h" - -const int REPROC_EINVAL = -EINVAL; -const int REPROC_EPIPE = -EPIPE; -const int REPROC_ETIMEDOUT = -ETIMEDOUT; -const int REPROC_ENOMEM = -ENOMEM; -const int REPROC_EWOULDBLOCK = -EWOULDBLOCK; - -enum { ERROR_STRING_MAX_SIZE = 512 }; - -const char *error_string(int error) -{ - static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; - - int r = strerror_r(abs(error), string, ARRAY_SIZE(string)); - if (r != 0) { - return "Failed to retrieve error string"; - } - - return string; -} diff --git a/extern/reproc-14.2.4/reproc/src/error.windows.c b/extern/reproc-14.2.4/reproc/src/error.windows.c deleted file mode 100644 index b8d82343e..000000000 --- a/extern/reproc-14.2.4/reproc/src/error.windows.c +++ /dev/null @@ -1,58 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "error.h" - -#include -#include -#include -#include - -#include - -#include "macro.h" - -const int REPROC_EINVAL = -ERROR_INVALID_PARAMETER; -const int REPROC_EPIPE = -ERROR_BROKEN_PIPE; -const int REPROC_ETIMEDOUT = -WAIT_TIMEOUT; -const int REPROC_ENOMEM = -ERROR_NOT_ENOUGH_MEMORY; -const int REPROC_EWOULDBLOCK = -WSAEWOULDBLOCK; - -enum { ERROR_STRING_MAX_SIZE = 512 }; - -const char *error_string(int error) -{ - wchar_t *wstring = NULL; - int r = -1; - - wstring = malloc(sizeof(wchar_t) * ERROR_STRING_MAX_SIZE); - if (wstring == NULL) { - return "Failed to allocate memory for error string"; - } - - // We don't expect message sizes larger than the maximum possible int. - r = (int) FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, (DWORD) abs(error), - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), wstring, - ERROR_STRING_MAX_SIZE, NULL); - if (r == 0) { - free(wstring); - return "Failed to retrieve error string"; - } - - static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; - - r = WideCharToMultiByte(CP_UTF8, 0, wstring, -1, string, ARRAY_SIZE(string), - NULL, NULL); - free(wstring); - if (r == 0) { - return "Failed to convert error string to UTF-8"; - } - - // Remove trailing whitespace and period. - if (r >= 4) { - string[r - 4] = '\0'; - } - - return string; -} diff --git a/extern/reproc-14.2.4/reproc/src/handle.h b/extern/reproc-14.2.4/reproc/src/handle.h deleted file mode 100644 index a6ac49bde..000000000 --- a/extern/reproc-14.2.4/reproc/src/handle.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include - -#if defined(_WIN32) -typedef void *handle_type; // `HANDLE` -#else -typedef int handle_type; // fd -#endif - -extern const handle_type HANDLE_INVALID; - -// Sets the `FD_CLOEXEC` flag on the file descriptor. POSIX only. -int handle_cloexec(handle_type handle, bool enable); - -// Closes `handle` if it is not an invalid handle and returns an invalid handle. -// Does not overwrite the last system error if an error occurs while closing -// `handle`. -handle_type handle_destroy(handle_type handle); diff --git a/extern/reproc-14.2.4/reproc/src/handle.posix.c b/extern/reproc-14.2.4/reproc/src/handle.posix.c deleted file mode 100644 index 1c9ce104c..000000000 --- a/extern/reproc-14.2.4/reproc/src/handle.posix.c +++ /dev/null @@ -1,42 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "handle.h" - -#include -#include -#include - -#include "error.h" - -const int HANDLE_INVALID = -1; - -int handle_cloexec(int handle, bool enable) -{ - int r = -1; - - r = fcntl(handle, F_GETFD, 0); - if (r < 0) { - return -errno; - } - - r = enable ? r | FD_CLOEXEC : r & ~FD_CLOEXEC; - - r = fcntl(handle, F_SETFD, r); - if (r < 0) { - return -errno; - } - - return 0; -} - -int handle_destroy(int handle) -{ - if (handle == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - int r = close(handle); - ASSERT_UNUSED(r == 0); - - return HANDLE_INVALID; -} diff --git a/extern/reproc-14.2.4/reproc/src/handle.windows.c b/extern/reproc-14.2.4/reproc/src/handle.windows.c deleted file mode 100644 index e0cd500dc..000000000 --- a/extern/reproc-14.2.4/reproc/src/handle.windows.c +++ /dev/null @@ -1,23 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "handle.h" - -#include - -#include "error.h" - -const HANDLE HANDLE_INVALID = INVALID_HANDLE_VALUE; // NOLINT - -// `handle_cloexec` is POSIX-only. - -HANDLE handle_destroy(HANDLE handle) -{ - if (handle == NULL || handle == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - int r = CloseHandle(handle); - ASSERT_UNUSED(r != 0); - - return HANDLE_INVALID; -} diff --git a/extern/reproc-14.2.4/reproc/src/init.h b/extern/reproc-14.2.4/reproc/src/init.h deleted file mode 100644 index 704e237af..000000000 --- a/extern/reproc-14.2.4/reproc/src/init.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -int init(void); - -void deinit(void); diff --git a/extern/reproc-14.2.4/reproc/src/init.posix.c b/extern/reproc-14.2.4/reproc/src/init.posix.c deleted file mode 100644 index e4b44a2e3..000000000 --- a/extern/reproc-14.2.4/reproc/src/init.posix.c +++ /dev/null @@ -1,10 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "init.h" - -int init(void) -{ - return 0; -} - -void deinit(void) {} diff --git a/extern/reproc-14.2.4/reproc/src/init.windows.c b/extern/reproc-14.2.4/reproc/src/init.windows.c deleted file mode 100644 index 8357b7c21..000000000 --- a/extern/reproc-14.2.4/reproc/src/init.windows.c +++ /dev/null @@ -1,24 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "init.h" - -#include - -#include "error.h" - -int init(void) -{ - WSADATA data; - int r = WSAStartup(MAKEWORD(2, 2), &data); - return -r; -} - -void deinit(void) -{ - int saved = WSAGetLastError(); - - int r = WSACleanup(); - ASSERT_UNUSED(r == 0); - - WSASetLastError(saved); -} diff --git a/extern/reproc-14.2.4/reproc/src/macro.h b/extern/reproc-14.2.4/reproc/src/macro.h deleted file mode 100644 index c746360f0..000000000 --- a/extern/reproc-14.2.4/reproc/src/macro.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) - -#define MIN(a, b) (a) < (b) ? (a) : (b) - -#if defined(_WIN32) && !defined(__MINGW32__) - #define THREAD_LOCAL __declspec(thread) -#else - #define THREAD_LOCAL __thread -#endif diff --git a/extern/reproc-14.2.4/reproc/src/options.c b/extern/reproc-14.2.4/reproc/src/options.c deleted file mode 100644 index 5a94d81ab..000000000 --- a/extern/reproc-14.2.4/reproc/src/options.c +++ /dev/null @@ -1,137 +0,0 @@ -#include "options.h" - -#include "error.h" - -static bool redirect_is_set(reproc_redirect redirect) -{ - return redirect.type || redirect.handle || redirect.file || redirect.path; -} - -static int parse_redirect(reproc_redirect *redirect, - REPROC_STREAM stream, - bool parent, - bool discard, - FILE *file, - const char *path) -{ - ASSERT(redirect); - - if (file) { - ASSERT_EINVAL(!redirect_is_set(*redirect)); - ASSERT_EINVAL(!parent && !discard && !path); - redirect->type = REPROC_REDIRECT_FILE; - redirect->file = file; - } - - if (path) { - ASSERT_EINVAL(!redirect_is_set(*redirect)); - ASSERT_EINVAL(!parent && !discard && !file); - redirect->type = REPROC_REDIRECT_PATH; - redirect->path = path; - } - - if (redirect->type == REPROC_REDIRECT_HANDLE || redirect->handle) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_HANDLE); - ASSERT_EINVAL(redirect->handle); - ASSERT_EINVAL(!redirect->file && !redirect->path); - redirect->type = REPROC_REDIRECT_HANDLE; - } - - if (redirect->type == REPROC_REDIRECT_FILE || redirect->file) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_FILE); - ASSERT_EINVAL(redirect->file); - ASSERT_EINVAL(!redirect->handle && !redirect->path); - redirect->type = REPROC_REDIRECT_FILE; - } - - if (redirect->type == REPROC_REDIRECT_PATH || redirect->path) { - ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || - redirect->type == REPROC_REDIRECT_PATH); - ASSERT_EINVAL(redirect->path); - ASSERT_EINVAL(!redirect->handle && !redirect->file); - redirect->type = REPROC_REDIRECT_PATH; - } - - if (redirect->type == REPROC_REDIRECT_DEFAULT) { - if (parent) { - ASSERT_EINVAL(!discard); - redirect->type = REPROC_REDIRECT_PARENT; - } else if (discard) { - ASSERT_EINVAL(!parent); - redirect->type = REPROC_REDIRECT_DISCARD; - } else { - redirect->type = stream == REPROC_STREAM_ERR ? REPROC_REDIRECT_PARENT - : REPROC_REDIRECT_PIPE; - } - } - - return 0; -} - -reproc_stop_actions parse_stop_actions(reproc_stop_actions stop) -{ - bool is_noop = stop.first.action == REPROC_STOP_NOOP && - stop.second.action == REPROC_STOP_NOOP && - stop.third.action == REPROC_STOP_NOOP; - - if (is_noop) { - stop.first.action = REPROC_STOP_WAIT; - stop.first.timeout = REPROC_DEADLINE; - stop.second.action = REPROC_STOP_TERMINATE; - stop.second.timeout = REPROC_INFINITE; - } - - return stop; -} - -int parse_options(reproc_options *options, const char *const *argv) -{ - ASSERT(options); - - int r = -1; - - r = parse_redirect(&options->redirect.in, REPROC_STREAM_IN, - options->redirect.parent, options->redirect.discard, NULL, - NULL); - if (r < 0) { - return r; - } - - r = parse_redirect(&options->redirect.out, REPROC_STREAM_OUT, - options->redirect.parent, options->redirect.discard, - options->redirect.file, options->redirect.path); - if (r < 0) { - return r; - } - - r = parse_redirect(&options->redirect.err, REPROC_STREAM_ERR, - options->redirect.parent, options->redirect.discard, - options->redirect.file, options->redirect.path); - if (r < 0) { - return r; - } - - if (options->input.data != NULL) { - ASSERT_EINVAL(options->redirect.in.type == REPROC_REDIRECT_PIPE); - } - - if (options->input.size > 0) { - ASSERT_EINVAL(options->input.data != NULL); - } - - if (options->fork) { - ASSERT_EINVAL(argv == NULL); - } else { - ASSERT_EINVAL(argv != NULL && argv[0] != NULL); - } - - if (options->deadline == 0) { - options->deadline = REPROC_INFINITE; - } - - options->stop = parse_stop_actions(options->stop); - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/src/options.h b/extern/reproc-14.2.4/reproc/src/options.h deleted file mode 100644 index 9e244e4e9..000000000 --- a/extern/reproc-14.2.4/reproc/src/options.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include - -reproc_stop_actions parse_stop_actions(reproc_stop_actions stop); - -int parse_options(reproc_options *options, const char *const *argv); diff --git a/extern/reproc-14.2.4/reproc/src/pipe.h b/extern/reproc-14.2.4/reproc/src/pipe.h deleted file mode 100644 index 6735e3a58..000000000 --- a/extern/reproc-14.2.4/reproc/src/pipe.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include -#include - -#ifdef _WIN64 -typedef uint64_t pipe_type; // `SOCKET` -#elif _WIN32 -typedef uint32_t pipe_type; // `SOCKET` -#else -typedef int pipe_type; // fd -#endif - -extern const pipe_type PIPE_INVALID; - -extern const short PIPE_EVENT_IN; -extern const short PIPE_EVENT_OUT; - -typedef struct { - pipe_type pipe; - short interests; - short events; -} pipe_event_source; - -// Creates a new anonymous pipe. `parent` and `child` are set to the parent and -// child endpoint of the pipe respectively. -int pipe_init(pipe_type *read, pipe_type *write); - -// Sets `pipe` to nonblocking mode. -int pipe_nonblocking(pipe_type pipe, bool enable); - -// Reads up to `size` bytes into `buffer` from the pipe indicated by `pipe` and -// returns the amount of bytes read. -int pipe_read(pipe_type pipe, uint8_t *buffer, size_t size); - -// Writes up to `size` bytes from `buffer` to the pipe indicated by `pipe` and -// returns the amount of bytes written. -int pipe_write(pipe_type pipe, const uint8_t *buffer, size_t size); - -// Polls the given event sources for events. -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout); - -int pipe_shutdown(pipe_type pipe); - -pipe_type pipe_destroy(pipe_type pipe); diff --git a/extern/reproc-14.2.4/reproc/src/pipe.posix.c b/extern/reproc-14.2.4/reproc/src/pipe.posix.c deleted file mode 100644 index f6d289ef4..000000000 --- a/extern/reproc-14.2.4/reproc/src/pipe.posix.c +++ /dev/null @@ -1,141 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "pipe.h" - -#include -#include -#include -#include -#include -#include - -#include "error.h" -#include "handle.h" - -const int PIPE_INVALID = -1; - -const short PIPE_EVENT_IN = POLLIN; -const short PIPE_EVENT_OUT = POLLOUT; - -int pipe_init(int *read, int *write) -{ - ASSERT(read); - ASSERT(write); - - int pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - r = pipe(pair); - if (r < 0) { - r = -errno; - goto finish; - } - - r = handle_cloexec(pair[0], true); - if (r < 0) { - goto finish; - } - - r = handle_cloexec(pair[1], true); - if (r < 0) { - goto finish; - } - - *read = pair[0]; - *write = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_nonblocking(int pipe, bool enable) -{ - int r = -1; - - r = fcntl(pipe, F_GETFL, 0); - if (r < 0) { - return -errno; - } - - r = enable ? r | O_NONBLOCK : r & ~O_NONBLOCK; - - r = fcntl(pipe, F_SETFL, r); - - return r < 0 ? -errno : 0; -} - -int pipe_read(int pipe, uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - - int r = (int) read(pipe, buffer, size); - - if (r == 0) { - // `read` returns 0 to indicate the other end of the pipe was closed. - return -EPIPE; - } - - return r < 0 ? -errno : r; -} - -int pipe_write(int pipe, const uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - - int r = (int) write(pipe, buffer, size); - - return r < 0 ? -errno : r; -} - -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT(num_sources <= INT_MAX); - - struct pollfd *pollfds = NULL; - int r = -1; - - pollfds = calloc(num_sources, sizeof(struct pollfd)); - if (pollfds == NULL) { - r = -errno; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - pollfds[i].fd = sources[i].pipe; - pollfds[i].events = sources[i].interests; - } - - r = poll(pollfds, (nfds_t) num_sources, timeout); - if (r < 0) { - r = -errno; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = pollfds[i].revents; - } - -finish: - free(pollfds); - - return r; -} - -int pipe_shutdown(int pipe) -{ - (void) pipe; - return 0; -} - -int pipe_destroy(int pipe) -{ - return handle_destroy(pipe); -} diff --git a/extern/reproc-14.2.4/reproc/src/pipe.windows.c b/extern/reproc-14.2.4/reproc/src/pipe.windows.c deleted file mode 100644 index bb355be31..000000000 --- a/extern/reproc-14.2.4/reproc/src/pipe.windows.c +++ /dev/null @@ -1,261 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "pipe.h" - -#include -#include -#include -#include - -#include "error.h" -#include "handle.h" -#include "macro.h" - -const SOCKET PIPE_INVALID = INVALID_SOCKET; - -const short PIPE_EVENT_IN = POLLIN; -const short PIPE_EVENT_OUT = POLLOUT; - -// Inspired by https://gist.github.com/geertj/4325783. -static int socketpair(int domain, int type, int protocol, SOCKET *out) -{ - ASSERT(out); - - SOCKET server = PIPE_INVALID; - SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - server = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); - if (server == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - SOCKADDR_IN localhost = { 0 }; - localhost.sin_family = AF_INET; - localhost.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK); - localhost.sin_port = 0; - - r = bind(server, (SOCKADDR *) &localhost, sizeof(localhost)); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - r = listen(server, 1); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - SOCKADDR_STORAGE name = { 0 }; - int size = sizeof(name); - r = getsockname(server, (SOCKADDR *) &name, &size); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - pair[0] = WSASocketW(domain, type, protocol, NULL, 0, 0); - if (pair[0] == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - struct { - WSAPROTOCOL_INFOW data; - int size; - } info = { { 0 }, sizeof(WSAPROTOCOL_INFOW) }; - - r = getsockopt(pair[0], SOL_SOCKET, SO_PROTOCOL_INFOW, (char *) &info.data, - &info.size); - if (r < 0) { - goto finish; - } - - // We require the returned sockets to be usable as Windows file handles. This - // might not be the case if extra LSP providers are installed. - - if (!(info.data.dwServiceFlags1 & XP1_IFS_HANDLES)) { - r = -ERROR_NOT_SUPPORTED; - goto finish; - } - - r = pipe_nonblocking(pair[0], true); - if (r < 0) { - goto finish; - } - - r = connect(pair[0], (SOCKADDR *) &name, size); - if (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK) { - r = -WSAGetLastError(); - goto finish; - } - - r = pipe_nonblocking(pair[0], false); - if (r < 0) { - goto finish; - } - - pair[1] = accept(server, NULL, NULL); - if (pair[1] == INVALID_SOCKET) { - r = -WSAGetLastError(); - goto finish; - } - - out[0] = pair[0]; - out[1] = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(server); - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_init(SOCKET *read, SOCKET *write) -{ - ASSERT(read); - ASSERT(write); - - SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - // Use sockets instead of pipes so we can use `WSAPoll` which only works with - // sockets. - r = socketpair(AF_INET, SOCK_STREAM, 0, pair); - if (r < 0) { - goto finish; - } - - r = SetHandleInformation((HANDLE) pair[0], HANDLE_FLAG_INHERIT, 0); - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - r = SetHandleInformation((HANDLE) pair[1], HANDLE_FLAG_INHERIT, 0); - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - // Make the connection unidirectional to better emulate a pipe. - - r = shutdown(pair[0], SD_SEND); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - r = shutdown(pair[1], SD_RECEIVE); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - *read = pair[0]; - *write = pair[1]; - - pair[0] = PIPE_INVALID; - pair[1] = PIPE_INVALID; - -finish: - pipe_destroy(pair[0]); - pipe_destroy(pair[1]); - - return r; -} - -int pipe_nonblocking(SOCKET pipe, bool enable) -{ - u_long mode = enable; - int r = ioctlsocket(pipe, (long) FIONBIO, &mode); - return r < 0 ? -WSAGetLastError() : 0; -} - -int pipe_read(SOCKET pipe, uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - ASSERT(size <= INT_MAX); - - int r = recv(pipe, (char *) buffer, (int) size, 0); - - if (r == 0) { - return -ERROR_BROKEN_PIPE; - } - - return r < 0 ? -WSAGetLastError() : r; -} - -int pipe_write(SOCKET pipe, const uint8_t *buffer, size_t size) -{ - ASSERT(pipe != PIPE_INVALID); - ASSERT(buffer); - ASSERT(size <= INT_MAX); - - int r = send(pipe, (const char *) buffer, (int) size, 0); - - return r < 0 ? -WSAGetLastError() : r; -} - -int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT(num_sources <= INT_MAX); - - WSAPOLLFD *pollfds = NULL; - int r = -1; - - pollfds = calloc(num_sources, sizeof(WSAPOLLFD)); - if (pollfds == NULL) { - r = -ERROR_NOT_ENOUGH_MEMORY; - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - pollfds[i].fd = sources[i].pipe; - pollfds[i].events = sources[i].interests; - } - - r = WSAPoll(pollfds, (ULONG) num_sources, timeout); - if (r < 0) { - r = -WSAGetLastError(); - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = pollfds[i].revents; - } - -finish: - free(pollfds); - - return r; -} - -int pipe_shutdown(SOCKET pipe) -{ - if (pipe == PIPE_INVALID) { - return 0; - } - - int r = shutdown(pipe, SD_SEND); - return r < 0 ? -WSAGetLastError() : 0; -} - -SOCKET pipe_destroy(SOCKET pipe) -{ - if (pipe == PIPE_INVALID) { - return PIPE_INVALID; - } - - int r = closesocket(pipe); - ASSERT_UNUSED(r == 0); - - return PIPE_INVALID; -} diff --git a/extern/reproc-14.2.4/reproc/src/process.h b/extern/reproc-14.2.4/reproc/src/process.h deleted file mode 100644 index b1455a786..000000000 --- a/extern/reproc-14.2.4/reproc/src/process.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include "handle.h" - -#include - -#include - -#if defined(_WIN32) -typedef void *process_type; // `HANDLE` -#else -typedef int process_type; // `pid_t` -#endif - -extern const process_type PROCESS_INVALID; - -struct process_options { - // If `NULL`, the child process inherits the environment of the current - // process. - struct { - REPROC_ENV behavior; - const char *const *extra; - } env; - // If not `NULL`, the working directory of the child process is set to - // `working_directory`. - const char *working_directory; - // The standard streams of the child process are redirected to the `in`, `out` - // and `err` handles. If a handle is `HANDLE_INVALID`, the corresponding child - // process standard stream is closed. The `exit` handle is simply inherited by - // the child process. - struct { - handle_type in; - handle_type out; - handle_type err; - handle_type exit; - } handle; -}; - -// Spawns a child process that executes the command stored in `argv`. -// -// If `argv` is `NULL` on POSIX, `exec` is not called after fork and this -// function returns 0 in the child process and > 0 in the parent process. On -// Windows, if `argv` is `NULL`, an error is returned. -// -// The process handle of the new child process is assigned to `process`. -int process_start(process_type *process, - const char *const *argv, - struct process_options options); - -// Returns the process ID associated with the given handle. On posix systems the -// handle is the process ID and so its returned directly. On WIN32 the process -// ID is returned from GetProcessId on the pointer. -int process_pid(process_type process); - -// Returns the process's exit status if it has finished running. -int process_wait(process_type process); - -// Sends the `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) signal to the process -// indicated by `process`. -int process_terminate(process_type process); - -// Sends the `SIGKILL` signal to `process` (POSIX) or calls `TerminateProcess` -// on `process` (Windows). -int process_kill(process_type process); - -process_type process_destroy(process_type process); diff --git a/extern/reproc-14.2.4/reproc/src/process.posix.c b/extern/reproc-14.2.4/reproc/src/process.posix.c deleted file mode 100644 index 0f0fe0d51..000000000 --- a/extern/reproc-14.2.4/reproc/src/process.posix.c +++ /dev/null @@ -1,499 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "process.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "error.h" -#include "macro.h" -#include "pipe.h" -#include "strv.h" - -const pid_t PROCESS_INVALID = -1; - -static int signal_mask(int how, const sigset_t *newmask, sigset_t *oldmask) -{ - int r = -1; - -#if defined(REPROC_MULTITHREADED) - // `pthread_sigmask` returns positive errno values so we negate them. - r = -pthread_sigmask(how, newmask, oldmask); -#else - r = sigprocmask(how, newmask, oldmask); - r = r < 0 ? -errno : 0; -#endif - - return r; -} - -// Returns true if the NUL-terminated string indicated by `path` is a relative -// path. A path is relative if any character except the first is a forward slash -// ('/'). -static bool path_is_relative(const char *path) -{ - return strlen(path) > 0 && path[0] != '/' && strchr(path + 1, '/') != NULL; -} - -// Prepends the NUL-terminated string indicated by `path` with the current -// working directory. The caller is responsible for freeing the result of this -// function. If an error occurs, `NULL` is returned and `errno` is set to -// indicate the error. -static char *path_prepend_cwd(const char *path) -{ - ASSERT(path); - - size_t path_size = strlen(path); - size_t cwd_size = PATH_MAX; - - // We always allocate sufficient space for `path` but do not include this - // space in `cwd_size` so we can be sure that when `getcwd` succeeds there is - // sufficient space left in `cwd` to append `path`. - - // +2 reserves space to add a NUL terminator and potentially a missing '/' - // after the current working directory. - char *cwd = calloc(cwd_size + path_size + 2, sizeof(char)); - if (cwd == NULL) { - return cwd; - } - - while (getcwd(cwd, cwd_size) == NULL) { - if (errno != ERANGE) { - free(cwd); - return NULL; - } - - cwd_size += PATH_MAX; - - char *result = realloc(cwd, cwd_size + path_size + 1); - if (result == NULL) { - free(cwd); - return result; - } - - cwd = result; - } - - cwd_size = strlen(cwd); - - // Add a forward slash after `cwd` if there is none. - if (cwd[cwd_size - 1] != '/') { - cwd[cwd_size] = '/'; - cwd[cwd_size + 1] = '\0'; - cwd_size++; - } - - // We've made sure there's sufficient space left in `cwd` to add `path` and a - // NUL terminator. - memcpy(cwd + cwd_size, path, path_size); - cwd[cwd_size + path_size] = '\0'; - - return cwd; -} - -static const int MAX_FD_LIMIT = 1024 * 1024; - -static int get_max_fd(void) -{ - struct rlimit limit = { 0 }; - - int r = getrlimit(RLIMIT_NOFILE, &limit); - if (r < 0) { - return -errno; - } - - rlim_t soft = limit.rlim_cur; - - if (soft == RLIM_INFINITY || soft > INT_MAX) { - return INT_MAX; - } - - return (int) (soft - 1); -} - -static bool fd_in_set(int fd, const int *fd_set, size_t size) -{ - for (size_t i = 0; i < size; i++) { - if (fd == fd_set[i]) { - return true; - } - } - - return false; -} - -static pid_t process_fork(const int *except, size_t num_except) -{ - struct { - sigset_t old; - sigset_t new; - } mask; - - int r = -1; - - // We don't want signal handlers of the parent to run in the child process so - // we block all signals before forking. - - r = sigfillset(&mask.new); - if (r < 0) { - return -errno; - } - - r = signal_mask(SIG_SETMASK, &mask.new, &mask.old); - if (r < 0) { - return r; - } - - struct { - int read; - int write; - } pipe = { PIPE_INVALID, PIPE_INVALID }; - - r = pipe_init(&pipe.read, &pipe.write); - if (r < 0) { - return r; - } - - r = fork(); - if (r < 0) { - // `fork` error. - - r = -errno; // Save `errno`. - - int q = signal_mask(SIG_SETMASK, &mask.new, &mask.old); - ASSERT_UNUSED(q == 0); - - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - - return r; - } - - if (r > 0) { - // Parent process - - pid_t child = r; - - // From now on, the child process might have started so we don't report - // errors from `signal_mask` and `read`. This puts the responsibility - // for cleaning up the process in the hands of the caller. - - int q = signal_mask(SIG_SETMASK, &mask.old, &mask.old); - ASSERT_UNUSED(q == 0); - - // Close the error pipe write end on the parent's side so `read` will return - // when it is closed on the child side as well. - pipe_destroy(pipe.write); - - int child_errno = 0; - q = (int) read(pipe.read, &child_errno, sizeof(child_errno)); - ASSERT_UNUSED(q >= 0); - - if (child_errno > 0) { - // If the child writes to the error pipe and exits, we're certain the - // child process exited on its own and we can report errors as usual. - r = waitpid(child, NULL, 0); - ASSERT(r < 0 || r == child); - - r = r < 0 ? -errno : -child_errno; - } - - pipe_destroy(pipe.read); - - return r < 0 ? r : child; - } - - // Child process - - // Reset all signal handlers so they don't run in the child process. By - // default, a child process inherits the parent's signal handlers but we - // override this as most signal handlers won't be written in a way that they - // can deal with being run in a child process. - - struct sigaction action = { .sa_handler = SIG_DFL }; - - r = sigemptyset(&action.sa_mask); - if (r < 0) { - r = -errno; - goto finish; - } - - // NSIG is not standardized so we use a fixed limit instead. - for (int signal = 0; signal < 32; signal++) { - r = sigaction(signal, &action, NULL); - if (r < 0 && errno != EINVAL) { - r = -errno; - goto finish; - } - } - - // Reset the child's signal mask to the default signal mask. By default, a - // child process inherits the parent's signal mask (even over an `exec` call) - // but we override this as most processes won't be written in a way that they - // can deal with starting with a custom signal mask. - - r = sigemptyset(&mask.new); - if (r < 0) { - r = -errno; - goto finish; - } - - r = signal_mask(SIG_SETMASK, &mask.new, NULL); - if (r < 0) { - goto finish; - } - - // Not all file descriptors might have been created with the `FD_CLOEXEC` - // flag so we manually close all file descriptors to prevent file descriptors - // leaking into the child process. - - r = get_max_fd(); - if (r < 0) { - goto finish; - } - - int max_fd = r; - - if (max_fd > MAX_FD_LIMIT) { - // Refuse to try to close too many file descriptors. - r = -EMFILE; - goto finish; - } - - for (int i = 0; i < max_fd; i++) { - // Make sure we don't close the error pipe file descriptors twice. - if (i == pipe.read || i == pipe.write) { - continue; - } - - if (fd_in_set(i, except, num_except)) { - continue; - } - - // Check if `i` is a valid file descriptor before trying to close it. - r = fcntl(i, F_GETFD); - if (r >= 0) { - handle_destroy(i); - } - } - - r = 0; - -finish: - if (r < 0) { - (void) !write(pipe.write, &errno, sizeof(errno)); - _exit(EXIT_FAILURE); - } - - pipe_destroy(pipe.write); - pipe_destroy(pipe.read); - - return 0; -} - -int process_start(pid_t *process, - const char *const *argv, - struct process_options options) -{ - ASSERT(process); - - if (argv != NULL) { - ASSERT(argv[0] != NULL); - } - - struct { - int read; - int write; - } pipe = { PIPE_INVALID, PIPE_INVALID }; - char *program = NULL; - char **env = NULL; - int r = -1; - - // We create an error pipe to receive errors from the child process. - r = pipe_init(&pipe.read, &pipe.write); - if (r < 0) { - goto finish; - } - - if (argv != NULL) { - // We prepend the parent working directory to `program` if it is a - // relative path so that it will always be searched for relative to the - // parent working directory even after executing `chdir`. - program = options.working_directory && path_is_relative(argv[0]) - ? path_prepend_cwd(argv[0]) - : strdup(argv[0]); - if (program == NULL) { - r = -errno; - goto finish; - } - } - - extern char **environ; // NOLINT - char *const *parent = options.env.behavior == REPROC_ENV_EMPTY ? NULL - : environ; - env = strv_concat(parent, options.env.extra); - if (env == NULL) { - goto finish; - } - - int except[] = { options.handle.in, options.handle.out, options.handle.err, - pipe.read, pipe.write, options.handle.exit }; - - r = process_fork(except, ARRAY_SIZE(except)); - if (r < 0) { - goto finish; - } - - if (r == 0) { - // Redirect stdin, stdout and stderr. - - int redirect[] = { options.handle.in, options.handle.out, - options.handle.err }; - - for (int i = 0; i < (int) ARRAY_SIZE(redirect); i++) { - // `i` corresponds to the standard stream we need to redirect. - r = dup2(redirect[i], i); - if (r < 0) { - r = -errno; - goto child; - } - - // Make sure we don't accidentally cloexec the standard streams of the - // child process when we're inheriting the parent standard streams. If we - // don't call `exec`, the caller is responsible for closing the redirect - // and exit handles. - if (redirect[i] != i) { - // Make sure the pipe is closed when we call exec. - r = handle_cloexec(redirect[i], true); - if (r < 0) { - goto child; - } - } - } - - // Make sure the `exit` file descriptor is inherited. - - r = handle_cloexec(options.handle.exit, false); - if (r < 0) { - goto child; - } - - if (options.working_directory != NULL) { - r = chdir(options.working_directory); - if (r < 0) { - r = -errno; - goto child; - } - } - - // `environ` is carried over calls to `exec`. - environ = env; - - if (argv != NULL) { - ASSERT(program); - - r = execvp(program, (char *const *) argv); - if (r < 0) { - r = -errno; - goto child; - } - } - - env = NULL; - - child: - if (r < 0) { - (void) !write(pipe.write, &errno, sizeof(errno)); - _exit(EXIT_FAILURE); - } - - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - free(program); - strv_free(env); - - return 0; - } - - pid_t child = r; - - // Close the error pipe write end on the parent's side so `read` will return - // when it is closed on the child side as well. - pipe.write = pipe_destroy(pipe.write); - - int child_errno = 0; - r = (int) read(pipe.read, &child_errno, sizeof(child_errno)); - ASSERT_UNUSED(r >= 0); - - if (child_errno > 0) { - r = waitpid(child, NULL, 0); - r = r < 0 ? -errno : -child_errno; - goto finish; - } - - *process = child; - r = 0; - -finish: - pipe_destroy(pipe.read); - pipe_destroy(pipe.write); - free(program); - strv_free(env); - - return r < 0 ? r : 1; -} - -static int parse_status(int status) -{ - return WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status) + 128; -} - -int process_pid(process_type process) -{ - return process; -} - -int process_wait(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int status = 0; - int r = waitpid(process, &status, 0); - if (r < 0) { - return -errno; - } - - ASSERT(r == process); - - return parse_status(status); -} - -int process_terminate(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int r = kill(process, SIGTERM); - return r < 0 ? -errno : 0; -} - -int process_kill(pid_t process) -{ - ASSERT(process != PROCESS_INVALID); - - int r = kill(process, SIGKILL); - return r < 0 ? -errno : 0; -} - -pid_t process_destroy(pid_t process) -{ - // `waitpid` already cleans up the process for us. - (void) process; - return PROCESS_INVALID; -} diff --git a/extern/reproc-14.2.4/reproc/src/process.windows.c b/extern/reproc-14.2.4/reproc/src/process.windows.c deleted file mode 100644 index 666f3cb77..000000000 --- a/extern/reproc-14.2.4/reproc/src/process.windows.c +++ /dev/null @@ -1,506 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "process.h" - -#include -#include -#include - -#include "error.h" -#include "macro.h" -#include "utf.h" - -const HANDLE PROCESS_INVALID = INVALID_HANDLE_VALUE; // NOLINT - -static const DWORD CREATION_FLAGS = - // Create each child process in a new process group so we don't send - // `CTRL-BREAK` signals to more than one child process in - // `process_terminate`. - CREATE_NEW_PROCESS_GROUP | - // Create each child process with a Unicode environment as we accept any - // UTF-16 encoded environment (including Unicode characters). Create each - CREATE_UNICODE_ENVIRONMENT | - // Create each child with an extended STARTUPINFOEXW structure so we can - // specify which handles should be inherited. - EXTENDED_STARTUPINFO_PRESENT; - -// Argument escaping implementation is based on the following blog post: -// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ - -static bool argument_should_escape(const char *argument) -{ - ASSERT(argument); - - bool should_escape = false; - - for (size_t i = 0; i < strlen(argument); i++) { - should_escape = should_escape || argument[i] == ' ' || - argument[i] == '\t' || argument[i] == '\n' || - argument[i] == '\v' || argument[i] == '\"'; - } - - return should_escape; -} - -static size_t argument_escaped_size(const char *argument) -{ - ASSERT(argument); - - size_t argument_size = strlen(argument); - - if (!argument_should_escape(argument)) { - return argument_size; - } - - size_t size = 2; // double quotes - - for (size_t i = 0; i < argument_size; i++) { - size_t num_backslashes = 0; - - while (i < argument_size && argument[i] == '\\') { - i++; - num_backslashes++; - } - - if (i == argument_size) { - size += num_backslashes * 2; - } else if (argument[i] == '"') { - size += num_backslashes * 2 + 2; - } else { - size += num_backslashes + 1; - } - } - - return size; -} - -static size_t argument_escape(char *dest, const char *argument) -{ - ASSERT(dest); - ASSERT(argument); - - size_t argument_size = strlen(argument); - - if (!argument_should_escape(argument)) { - strcpy(dest, argument); // NOLINT - return argument_size; - } - - const char *begin = dest; - - *dest++ = '"'; - - for (size_t i = 0; i < argument_size; i++) { - size_t num_backslashes = 0; - - while (i < argument_size && argument[i] == '\\') { - i++; - num_backslashes++; - } - - if (i == argument_size) { - memset(dest, '\\', num_backslashes * 2); - dest += num_backslashes * 2; - } else if (argument[i] == '"') { - memset(dest, '\\', num_backslashes * 2 + 1); - dest += num_backslashes * 2 + 1; - *dest++ = '"'; - } else { - memset(dest, '\\', num_backslashes); - dest += num_backslashes; - *dest++ = argument[i]; - } - } - - *dest++ = '"'; - - return (size_t)(dest - begin); -} - -static char *argv_join(const char *const *argv) -{ - ASSERT(argv); - - // Determine the size of the concatenated string first. - size_t joined_size = 1; // Count the NUL terminator. - for (int i = 0; argv[i] != NULL; i++) { - joined_size += argument_escaped_size(argv[i]); - - if (argv[i + 1] != NULL) { - joined_size++; // Count whitespace. - } - } - - char *joined = calloc(joined_size, sizeof(char)); - if (joined == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - char *current = joined; - for (int i = 0; argv[i] != NULL; i++) { - current += argument_escape(current, argv[i]); - - // We add a space after each argument in the joined arguments string except - // for the final argument. - if (argv[i + 1] != NULL) { - *current++ = ' '; - } - } - - *current = '\0'; - - return joined; -} - -static size_t env_join_size(const char *const *env) -{ - ASSERT(env); - - size_t joined_size = 1; // Count the NUL terminator. - for (int i = 0; env[i] != NULL; i++) { - joined_size += strlen(env[i]) + 1; // Count the NUL terminator. - } - - return joined_size; -} - -static char *env_join(const char *const *env) -{ - ASSERT(env); - - char *joined = calloc(env_join_size(env), sizeof(char)); - if (joined == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - char *current = joined; - for (int i = 0; env[i] != NULL; i++) { - size_t to_copy = strlen(env[i]) + 1; // Include NUL terminator. - memcpy(current, env[i], to_copy); - current += to_copy; - } - - *current = '\0'; - - return joined; -} - -static const DWORD NUM_ATTRIBUTES = 1; - -static LPPROC_THREAD_ATTRIBUTE_LIST setup_attribute_list(HANDLE *handles, - size_t num_handles) -{ - ASSERT(handles); - - int r = -1; - - // Make sure all the given handles can be inherited. - for (size_t i = 0; i < num_handles; i++) { - r = SetHandleInformation(handles[i], HANDLE_FLAG_INHERIT, - HANDLE_FLAG_INHERIT); - if (r == 0) { - return NULL; - } - } - - // Get the required size for `attribute_list`. - SIZE_T attribute_list_size = 0; - r = InitializeProcThreadAttributeList(NULL, NUM_ATTRIBUTES, 0, - &attribute_list_size); - if (r == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - return NULL; - } - - LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = malloc(attribute_list_size); - if (attribute_list == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - r = InitializeProcThreadAttributeList(attribute_list, NUM_ATTRIBUTES, 0, - &attribute_list_size); - if (r == 0) { - free(attribute_list); - return NULL; - } - - // Add the handles to be inherited to `attribute_list`. - r = UpdateProcThreadAttribute(attribute_list, 0, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, - num_handles * sizeof(HANDLE), NULL, NULL); - if (r == 0) { - DeleteProcThreadAttributeList(attribute_list); - free(attribute_list); - return NULL; - } - - return attribute_list; -} - -#define NULSTR_FOREACH(i, l) \ - for ((i) = (l); (i) && *(i) != L'\0'; (i) = wcschr((i), L'\0') + 1) - -static wchar_t *env_concat(const wchar_t *a, const wchar_t *b) -{ - const wchar_t *i = NULL; - size_t size = 1; - wchar_t *c = NULL; - - NULSTR_FOREACH(i, a) { - size += wcslen(i) + 1; - } - - NULSTR_FOREACH(i, b) { - size += wcslen(i) + 1; - } - - wchar_t *r = calloc(size, sizeof(wchar_t)); - if (!r) { - return NULL; - } - - c = r; - - NULSTR_FOREACH(i, a) { - wcscpy(c, i); - c += wcslen(i) + 1; - } - - NULSTR_FOREACH(i, b) { - wcscpy(c, i); - c += wcslen(i) + 1; - } - - *c = L'\0'; - - return r; -} - -static wchar_t *env_setup(REPROC_ENV behavior, const char *const *extra) -{ - wchar_t *env_parent_wstring = NULL; - char *env_extra = NULL; - wchar_t *env_extra_wstring = NULL; - wchar_t *env_wstring = NULL; - - if (behavior == REPROC_ENV_EXTEND) { - env_parent_wstring = GetEnvironmentStringsW(); - } - - if (extra != NULL) { - env_extra = env_join(extra); - if (env_extra == NULL) { - goto finish; - } - - size_t joined_size = env_join_size(extra); - ASSERT(joined_size <= INT_MAX); - - env_extra_wstring = utf16_from_utf8(env_extra, (int) joined_size); - if (env_extra_wstring == NULL) { - goto finish; - } - } - - env_wstring = env_concat(env_parent_wstring, env_extra_wstring); - if (env_wstring == NULL) { - goto finish; - } - -finish: - FreeEnvironmentStringsW(env_parent_wstring); - free(env_extra); - free(env_extra_wstring); - - return env_wstring; -} - -int process_start(HANDLE *process, - const char *const *argv, - struct process_options options) -{ - ASSERT(process); - - if (argv == NULL) { - return -ERROR_CALL_NOT_IMPLEMENTED; - } - - ASSERT(argv[0] != NULL); - - char *command_line = NULL; - wchar_t *command_line_wstring = NULL; - wchar_t *env_wstring = NULL; - wchar_t *working_directory_wstring = NULL; - LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = NULL; - PROCESS_INFORMATION info = { PROCESS_INVALID, HANDLE_INVALID, 0, 0 }; - int r = -1; - - // Join `argv` to a whitespace delimited string as required by - // `CreateProcessW`. - command_line = argv_join(argv); - if (command_line == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Convert UTF-8 to UTF-16 as required by `CreateProcessW`. - command_line_wstring = utf16_from_utf8(command_line, -1); - if (command_line_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Idem for `working_directory` if it isn't `NULL`. - if (options.working_directory != NULL) { - working_directory_wstring = utf16_from_utf8(options.working_directory, -1); - if (working_directory_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - } - - env_wstring = env_setup(options.env.behavior, options.env.extra); - if (env_wstring == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - // Windows Vista added the `STARTUPINFOEXW` structure in which we can put a - // list of handles that should be inherited. Only these handles are inherited - // by the child process. Other code in an application that calls - // `CreateProcess` without passing a `STARTUPINFOEXW` struct containing the - // handles it should inherit can still unintentionally inherit handles meant - // for a reproc child process. See https://stackoverflow.com/a/2345126 for - // more information. - HANDLE handles[] = { options.handle.exit, options.handle.in, - options.handle.out, options.handle.err }; - size_t num_handles = ARRAY_SIZE(handles); - - if (options.handle.out == options.handle.err) { - // CreateProcess doesn't like the same handle being specified twice in the - // `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute. - num_handles--; - } - - attribute_list = setup_attribute_list(handles, num_handles); - if (attribute_list == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - STARTUPINFOEXW extended_startup_info = { - .StartupInfo = { .cb = sizeof(extended_startup_info), - .dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, - // `STARTF_USESTDHANDLES` - .hStdInput = options.handle.in, - .hStdOutput = options.handle.out, - .hStdError = options.handle.err, - // `STARTF_USESHOWWINDOW`. Make sure the console window of - // the child process isn't visible. See - // https://github.com/DaanDeMeyer/reproc/issues/6 and - // https://github.com/DaanDeMeyer/reproc/pull/7 for more - // information. - .wShowWindow = SW_HIDE }, - .lpAttributeList = attribute_list - }; - - LPSTARTUPINFOW startup_info_address = &extended_startup_info.StartupInfo; - - // Child processes inherit the error mode of their parents. To avoid child - // processes creating error dialogs we set our error mode to not create error - // dialogs temporarily which is inherited by the child process. - DWORD previous_error_mode = SetErrorMode(SEM_NOGPFAULTERRORBOX); - - SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), - .bInheritHandle = false, - .lpSecurityDescriptor = NULL }; - - r = CreateProcessW(NULL, command_line_wstring, &do_not_inherit, - &do_not_inherit, true, CREATION_FLAGS, env_wstring, - working_directory_wstring, startup_info_address, &info); - - SetErrorMode(previous_error_mode); - - if (r == 0) { - r = -(int) GetLastError(); - goto finish; - } - - *process = info.hProcess; - r = 0; - -finish: - free(command_line); - free(command_line_wstring); - free(env_wstring); - free(working_directory_wstring); - DeleteProcThreadAttributeList(attribute_list); - free(attribute_list); - handle_destroy(info.hThread); - - return r < 0 ? r : 1; -} - -int process_pid(process_type process) -{ - ASSERT(process); - return (int) GetProcessId(process); -} - -int process_wait(HANDLE process) -{ - ASSERT(process); - - int r = -1; - - r = (int) WaitForSingleObject(process, INFINITE); - if ((DWORD) r == WAIT_FAILED) { - return -(int) GetLastError(); - } - - DWORD status = 0; - r = GetExitCodeProcess(process, &status); - if (r == 0) { - return -(int) GetLastError(); - } - - // `GenerateConsoleCtrlEvent` causes a process to exit with this exit code. - // Because `GenerateConsoleCtrlEvent` has roughly the same semantics as - // `SIGTERM`, we map its exit code to `SIGTERM`. - if (status == 3221225786) { - status = (DWORD) REPROC_SIGTERM; - } - - return (int) status; -} - -int process_terminate(HANDLE process) -{ - ASSERT(process && process != PROCESS_INVALID); - - // `GenerateConsoleCtrlEvent` can only be called on a process group. To call - // `GenerateConsoleCtrlEvent` on a single child process it has to be put in - // its own process group (which we did when starting the child process). - BOOL r = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(process)); - - return r == 0 ? -(int) GetLastError() : 0; -} - -int process_kill(HANDLE process) -{ - ASSERT(process && process != PROCESS_INVALID); - - // We use 137 (`SIGKILL`) as the exit status because it is the same exit - // status as a process that is stopped with the `SIGKILL` signal on POSIX - // systems. - BOOL r = TerminateProcess(process, (DWORD) REPROC_SIGKILL); - - return r == 0 ? -(int) GetLastError() : 0; -} - -HANDLE process_destroy(HANDLE process) -{ - return handle_destroy(process); -} diff --git a/extern/reproc-14.2.4/reproc/src/redirect.c b/extern/reproc-14.2.4/reproc/src/redirect.c deleted file mode 100644 index 2c25d13e4..000000000 --- a/extern/reproc-14.2.4/reproc/src/redirect.c +++ /dev/null @@ -1,164 +0,0 @@ -#include "redirect.h" - -#include "error.h" - -static int redirect_pipe(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - bool nonblocking) -{ - ASSERT(parent); - ASSERT(child); - - pipe_type pipe[] = { PIPE_INVALID, PIPE_INVALID }; - int r = -1; - - r = pipe_init(&pipe[0], &pipe[1]); - if (r < 0) { - goto finish; - } - - r = pipe_nonblocking(stream == REPROC_STREAM_IN ? pipe[1] : pipe[0], - nonblocking); - if (r < 0) { - goto finish; - } - - *parent = stream == REPROC_STREAM_IN ? pipe[1] : pipe[0]; - *child = stream == REPROC_STREAM_IN ? (handle_type) pipe[0] - : (handle_type) pipe[1]; - -finish: - if (r < 0) { - pipe_destroy(pipe[0]); - pipe_destroy(pipe[1]); - } - - return r; -} - -int redirect_init(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - reproc_redirect redirect, - bool nonblocking, - handle_type out) -{ - ASSERT(parent); - ASSERT(child); - - int r = REPROC_EINVAL; - - switch (redirect.type) { - - case REPROC_REDIRECT_DEFAULT: - ASSERT(false); - break; - - case REPROC_REDIRECT_PIPE: - r = redirect_pipe(parent, child, stream, nonblocking); - break; - - case REPROC_REDIRECT_PARENT: - r = redirect_parent(child, stream); - if (r == REPROC_EPIPE) { - // Discard if the corresponding parent stream is closed. - r = redirect_discard(child, stream); - } - - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_DISCARD: - r = redirect_discard(child, stream); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_HANDLE: - ASSERT(redirect.handle); - - r = 0; - - *child = redirect.handle; - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_FILE: - ASSERT(redirect.file); - - r = redirect_file(child, redirect.file); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_STDOUT: - ASSERT(stream == REPROC_STREAM_ERR); - ASSERT(out != HANDLE_INVALID); - - r = 0; - - *child = out; - *parent = PIPE_INVALID; - - break; - - case REPROC_REDIRECT_PATH: - ASSERT(redirect.path); - - r = redirect_path(child, stream, redirect.path); - if (r < 0) { - break; - } - - *parent = PIPE_INVALID; - - break; - } - - return r; -} - -handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type) -{ - if (child == HANDLE_INVALID) { - return HANDLE_INVALID; - } - - switch (type) { - case REPROC_REDIRECT_DEFAULT: - ASSERT(false); - break; - case REPROC_REDIRECT_PIPE: - // We know `handle` is a pipe if `REDIRECT_PIPE` is used so the cast is - // safe. This little hack prevents us from having to introduce a generic - // handle type. - pipe_destroy((pipe_type) child); - break; - case REPROC_REDIRECT_DISCARD: - case REPROC_REDIRECT_PATH: - handle_destroy(child); - break; - case REPROC_REDIRECT_PARENT: - case REPROC_REDIRECT_FILE: - case REPROC_REDIRECT_HANDLE: - case REPROC_REDIRECT_STDOUT: - break; - } - - return HANDLE_INVALID; -} diff --git a/extern/reproc-14.2.4/reproc/src/redirect.h b/extern/reproc-14.2.4/reproc/src/redirect.h deleted file mode 100644 index 4cee19cd7..000000000 --- a/extern/reproc-14.2.4/reproc/src/redirect.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#include "handle.h" -#include "pipe.h" - -int redirect_init(pipe_type *parent, - handle_type *child, - REPROC_STREAM stream, - reproc_redirect redirect, - bool nonblocking, - handle_type out); - -handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type); - -// Internal prototypes - -int redirect_parent(handle_type *child, REPROC_STREAM stream); - -int redirect_discard(handle_type *child, REPROC_STREAM stream); - -int redirect_file(handle_type *child, FILE *file); - -int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path); diff --git a/extern/reproc-14.2.4/reproc/src/redirect.posix.c b/extern/reproc-14.2.4/reproc/src/redirect.posix.c deleted file mode 100644 index 943fe9eb7..000000000 --- a/extern/reproc-14.2.4/reproc/src/redirect.posix.c +++ /dev/null @@ -1,79 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "redirect.h" - -#include -#include -#include - -#include "error.h" -#include "pipe.h" - -static FILE *stream_to_file(REPROC_STREAM stream) -{ - switch (stream) { - case REPROC_STREAM_IN: - return stdin; - case REPROC_STREAM_OUT: - return stdout; - case REPROC_STREAM_ERR: - return stderr; - } - - return NULL; -} - -int redirect_parent(int *child, REPROC_STREAM stream) -{ - ASSERT(child); - - FILE *file = stream_to_file(stream); - if (file == NULL) { - return -EINVAL; - } - - int r = fileno(file); - if (r < 0) { - return errno == EBADF ? -EPIPE : -errno; - } - - *child = r; // `r` contains the duplicated file descriptor. - - return 0; -} - -int redirect_discard(int *child, REPROC_STREAM stream) -{ - return redirect_path(child, stream, "/dev/null"); -} - -int redirect_file(int *child, FILE *file) -{ - ASSERT(child); - - int r = fileno(file); - if (r < 0) { - return -errno; - } - - *child = r; - - return 0; -} - -int redirect_path(int *child, REPROC_STREAM stream, const char *path) -{ - ASSERT(child); - ASSERT(path); - - int mode = stream == REPROC_STREAM_IN ? O_RDONLY : O_WRONLY; - - int r = open(path, mode | O_CREAT | O_CLOEXEC, 0640); - if (r < 0) { - return -errno; - } - - *child = r; - - return 0; -} diff --git a/extern/reproc-14.2.4/reproc/src/redirect.windows.c b/extern/reproc-14.2.4/reproc/src/redirect.windows.c deleted file mode 100644 index c634145e4..000000000 --- a/extern/reproc-14.2.4/reproc/src/redirect.windows.c +++ /dev/null @@ -1,113 +0,0 @@ -#define _WIN32_WINNT _WIN32_WINNT_VISTA - -#include "redirect.h" - -#include -#include -#include - -#include "error.h" -#include "pipe.h" -#include "utf.h" - -static DWORD stream_to_id(REPROC_STREAM stream) -{ - switch (stream) { - case REPROC_STREAM_IN: - return STD_INPUT_HANDLE; - case REPROC_STREAM_OUT: - return STD_OUTPUT_HANDLE; - case REPROC_STREAM_ERR: - return STD_ERROR_HANDLE; - } - - return 0; -} - -int redirect_parent(HANDLE *child, REPROC_STREAM stream) -{ - ASSERT(child); - - DWORD id = stream_to_id(stream); - if (id == 0) { - return -ERROR_INVALID_PARAMETER; - } - - HANDLE *handle = GetStdHandle(id); - if (handle == INVALID_HANDLE_VALUE) { - return -(int) GetLastError(); - } - - if (handle == NULL) { - return -ERROR_BROKEN_PIPE; - } - - *child = handle; - - return 0; -} - -enum { FILE_NO_TEMPLATE = 0 }; - -int redirect_discard(HANDLE *child, REPROC_STREAM stream) -{ - return redirect_path(child, stream, "NUL"); -} - -int redirect_file(HANDLE *child, FILE *file) -{ - ASSERT(child); - ASSERT(file); - - int r = _fileno(file); - if (r < 0) { - return -ERROR_INVALID_HANDLE; - } - - intptr_t result = _get_osfhandle(r); - if (result == -1) { - return -ERROR_INVALID_HANDLE; - } - - *child = (HANDLE) result; - - return 0; -} - -int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path) -{ - ASSERT(child); - ASSERT(path); - - DWORD mode = stream == REPROC_STREAM_IN ? GENERIC_READ : GENERIC_WRITE; - HANDLE handle = HANDLE_INVALID; - int r = -1; - - wchar_t *wpath = utf16_from_utf8(path, -1); - if (wpath == NULL) { - r = -(int) GetLastError(); - goto finish; - } - - SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), - .bInheritHandle = false, - .lpSecurityDescriptor = NULL }; - - handle = CreateFileW(wpath, mode, FILE_SHARE_READ | FILE_SHARE_WRITE, - &do_not_inherit, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, - (HANDLE) FILE_NO_TEMPLATE); - if (handle == INVALID_HANDLE_VALUE) { - r = -(int) GetLastError(); - goto finish; - } - - *child = handle; - handle = HANDLE_INVALID; - r = 0; - -finish: - free(wpath); - handle_destroy(handle); - - return r; -} diff --git a/extern/reproc-14.2.4/reproc/src/reproc.c b/extern/reproc-14.2.4/reproc/src/reproc.c deleted file mode 100644 index 4104717d3..000000000 --- a/extern/reproc-14.2.4/reproc/src/reproc.c +++ /dev/null @@ -1,695 +0,0 @@ -#include - -#include - -#include "clock.h" -#include "error.h" -#include "handle.h" -#include "init.h" -#include "macro.h" -#include "options.h" -#include "pipe.h" -#include "process.h" -#include "redirect.h" - -struct reproc_t { - process_type handle; - - struct { - pipe_type in; - pipe_type out; - pipe_type err; - pipe_type exit; - } pipe; - - int status; - reproc_stop_actions stop; - int64_t deadline; - bool nonblocking; - - struct { - pipe_type out; - pipe_type err; - } child; -}; - -enum { - STATUS_NOT_STARTED = -1, - STATUS_IN_PROGRESS = -2, - STATUS_IN_CHILD = -3, -}; - -#define SIGOFFSET 128 - -const int REPROC_SIGKILL = SIGOFFSET + 9; -const int REPROC_SIGTERM = SIGOFFSET + 15; - -const int REPROC_INFINITE = -1; -const int REPROC_DEADLINE = -2; - -static int setup_input(pipe_type *pipe, const uint8_t *data, size_t size) -{ - if (data == NULL) { - ASSERT(size == 0); - return 0; - } - - ASSERT(pipe && *pipe != PIPE_INVALID); - - // `reproc_write` only needs the child process stdin pipe to be initialized. - size_t written = 0; - int r = -1; - - // Make sure we don't block indefinitely when `input` is bigger than the - // size of the pipe. - r = pipe_nonblocking(*pipe, true); - if (r < 0) { - return r; - } - - while (written < size) { - r = pipe_write(*pipe, data + written, size - written); - if (r < 0) { - return r; - } - - ASSERT(written + (size_t) r <= size); - written += (size_t) r; - } - - *pipe = pipe_destroy(*pipe); - - return 0; -} - -static int expiry(int timeout, int64_t deadline) -{ - if (timeout == REPROC_INFINITE && deadline == REPROC_INFINITE) { - return REPROC_INFINITE; - } - - if (deadline == REPROC_INFINITE) { - return timeout; - } - - int64_t n = now(); - - if (n >= deadline) { - return REPROC_DEADLINE; - } - - // `deadline` exceeds `now` by at most a full `int` so the cast is safe. - int remaining = (int) (deadline - n); - - if (timeout == REPROC_INFINITE) { - return remaining; - } - - return MIN(timeout, remaining); -} - -static size_t find_earliest_deadline(reproc_event_source *sources, - size_t num_sources) -{ - ASSERT(sources); - ASSERT(num_sources > 0); - - size_t earliest = 0; - int min = REPROC_INFINITE; - - for (size_t i = 0; i < num_sources; i++) { - reproc_t *process = sources[i].process; - - if (process == NULL) { - continue; - } - - int current = expiry(REPROC_INFINITE, process->deadline); - - if (current == REPROC_DEADLINE) { - return i; - } - - if (min == REPROC_INFINITE || current < min) { - earliest = i; - min = current; - } - } - - return earliest; -} - -reproc_t *reproc_new(void) -{ - reproc_t *process = malloc(sizeof(reproc_t)); - if (process == NULL) { - return NULL; - } - - *process = (reproc_t){ .handle = PROCESS_INVALID, - .pipe = { .in = PIPE_INVALID, - .out = PIPE_INVALID, - .err = PIPE_INVALID, - .exit = PIPE_INVALID }, - .child = { .out = PIPE_INVALID, .err = PIPE_INVALID }, - .status = STATUS_NOT_STARTED, - .deadline = REPROC_INFINITE }; - - return process; -} - -int reproc_start(reproc_t *process, - const char *const *argv, - reproc_options options) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status == STATUS_NOT_STARTED); - - struct { - handle_type in; - handle_type out; - handle_type err; - pipe_type exit; - } child = { HANDLE_INVALID, HANDLE_INVALID, HANDLE_INVALID, PIPE_INVALID }; - int r = -1; - - r = init(); - if (r < 0) { - return r; // Make sure we can always call `deinit` in `finish`. - } - - r = parse_options(&options, argv); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.in, &child.in, REPROC_STREAM_IN, - options.redirect.in, options.nonblocking, HANDLE_INVALID); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.out, &child.out, REPROC_STREAM_OUT, - options.redirect.out, options.nonblocking, HANDLE_INVALID); - if (r < 0) { - goto finish; - } - - r = redirect_init(&process->pipe.err, &child.err, REPROC_STREAM_ERR, - options.redirect.err, options.nonblocking, child.out); - if (r < 0) { - goto finish; - } - - r = pipe_init(&process->pipe.exit, &child.exit); - if (r < 0) { - goto finish; - } - - r = setup_input(&process->pipe.in, options.input.data, options.input.size); - if (r < 0) { - goto finish; - } - - struct process_options process_options = { - .env = { .behavior = options.env.behavior, .extra = options.env.extra }, - .working_directory = options.working_directory, - .handle = { .in = child.in, - .out = child.out, - .err = child.err, - .exit = (handle_type) child.exit } - }; - - r = process_start(&process->handle, argv, process_options); - if (r < 0) { - goto finish; - } - - if (r > 0) { - process->stop = options.stop; - - if (options.deadline != REPROC_INFINITE) { - process->deadline = now() + options.deadline; - } - - process->nonblocking = options.nonblocking; - } - -finish: - // Either an error has ocurred or the child pipe endpoints have been copied to - // the stdin/stdout/stderr streams of the child process. Either way, they can - // be safely closed. - redirect_destroy(child.in, options.redirect.in.type); - - // See `reproc_poll` for why we do this. - -#ifdef _WIN32 - if (r < 0 || options.redirect.out.type != REPROC_REDIRECT_PIPE) { - child.out = redirect_destroy(child.out, options.redirect.out.type); - } - - if (r < 0 || options.redirect.err.type != REPROC_REDIRECT_PIPE) { - child.err = redirect_destroy(child.err, options.redirect.err.type); - } -#else - child.out = redirect_destroy(child.out, options.redirect.out.type); - child.err = redirect_destroy(child.err, options.redirect.err.type); -#endif - - pipe_destroy(child.exit); - - if (r < 0) { - process->handle = process_destroy(process->handle); - process->pipe.in = pipe_destroy(process->pipe.in); - process->pipe.out = pipe_destroy(process->pipe.out); - process->pipe.err = pipe_destroy(process->pipe.err); - process->pipe.exit = pipe_destroy(process->pipe.exit); - deinit(); - } else if (r == 0) { - process->handle = PROCESS_INVALID; - // `process_start` has already taken care of closing the handles for us. - process->pipe.in = PIPE_INVALID; - process->pipe.out = PIPE_INVALID; - process->pipe.err = PIPE_INVALID; - process->pipe.exit = PIPE_INVALID; - process->status = STATUS_IN_CHILD; - } else { - process->child.out = (pipe_type) child.out; - process->child.err = (pipe_type) child.err; - process->status = STATUS_IN_PROGRESS; - } - - return r; -} - -enum { PIPES_PER_SOURCE = 4 }; - -static bool contains_valid_pipe(pipe_event_source *sources, size_t num_sources) -{ - for (size_t i = 0; i < num_sources; i++) { - if (sources[i].pipe != PIPE_INVALID) { - return true; - } - } - - return false; -} - -int reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout) -{ - ASSERT_EINVAL(sources); - ASSERT_EINVAL(num_sources > 0); - - size_t earliest = find_earliest_deadline(sources, num_sources); - int64_t deadline = sources[earliest].process == NULL - ? REPROC_INFINITE - : sources[earliest].process->deadline; - - int first = expiry(timeout, deadline); - size_t num_pipes = num_sources * PIPES_PER_SOURCE; - int r = REPROC_ENOMEM; - - if (first == REPROC_DEADLINE) { - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = 0; - } - - sources[earliest].events = REPROC_EVENT_DEADLINE; - return 1; - } - - pipe_event_source *pipes = calloc(num_pipes, sizeof(pipe_event_source)); - if (pipes == NULL) { - return r; - } - - for (size_t i = 0; i < num_pipes; i++) { - pipes[i].pipe = PIPE_INVALID; - } - - for (size_t i = 0; i < num_sources; i++) { - size_t j = i * PIPES_PER_SOURCE; - reproc_t *process = sources[i].process; - int interests = sources[i].interests; - - if (process == NULL) { - continue; - } - - bool in = interests & REPROC_EVENT_IN; - pipes[j + 0].pipe = in ? process->pipe.in : PIPE_INVALID; - pipes[j + 0].interests = PIPE_EVENT_OUT; - - bool out = interests & REPROC_EVENT_OUT; - pipes[j + 1].pipe = out ? process->pipe.out : PIPE_INVALID; - pipes[j + 1].interests = PIPE_EVENT_IN; - - bool err = interests & REPROC_EVENT_ERR; - pipes[j + 2].pipe = err ? process->pipe.err : PIPE_INVALID; - pipes[j + 2].interests = PIPE_EVENT_IN; - - bool exit = (interests & REPROC_EVENT_EXIT) || - (interests & REPROC_EVENT_OUT && - process->child.out != PIPE_INVALID) || - (interests & REPROC_EVENT_ERR && - process->child.err != PIPE_INVALID); - pipes[j + 3].pipe = exit ? process->pipe.exit : PIPE_INVALID; - pipes[j + 3].interests = PIPE_EVENT_IN; - } - - if (!contains_valid_pipe(pipes, num_pipes)) { - r = REPROC_EPIPE; - goto finish; - } - - r = pipe_poll(pipes, num_pipes, first); - if (r < 0) { - goto finish; - } - - for (size_t i = 0; i < num_sources; i++) { - sources[i].events = 0; - } - - if (r == 0 && first != timeout) { - // Differentiate between timeout and deadline expiry. Deadline expiry is an - // event, timeouts are not. - sources[earliest].events = REPROC_EVENT_DEADLINE; - r = 1; - } else if (r > 0) { - // Convert pipe events to process events. - for (size_t i = 0; i < num_pipes; i++) { - if (pipes[i].pipe == PIPE_INVALID) { - continue; - } - - if (pipes[i].events > 0) { - // Index in a set of pipes determines the process pipe and thus the - // process event. - // 0 = stdin pipe => REPROC_EVENT_IN - // 1 = stdout pipe => REPROC_EVENT_OUT - // ... - int event = 1 << (i % PIPES_PER_SOURCE); - sources[i / PIPES_PER_SOURCE].events |= event; - } - } - - r = 0; - - // Count the number of processes with events. - for (size_t i = 0; i < num_sources; i++) { - r += sources[i].events > 0; - } - - // On Windows, when redirecting to sockets, we keep the child handles alive - // in the parent process (see `reproc_start`). We do this because Windows - // doesn't correctly flush redirected socket handles when a child process - // exits. This can lead to data loss where the parent process doesn't - // receive all output of the child process. To get around this, we keep an - // extra handle open in the parent process which we close correctly when we - // detect the child process has exited. Detecting whether a child process - // has exited happens via another inherited socket, but here there's no - // danger of data loss because no data is received over this socket. - - bool again = false; - - for (size_t i = 0; i < num_sources; i++) { - if (!(sources[i].events & REPROC_EVENT_EXIT)) { - continue; - } - - reproc_t *process = sources[i].process; - - if (process->child.out == PIPE_INVALID && - process->child.err == PIPE_INVALID) { - continue; - } - - r = pipe_shutdown(process->child.out); - if (r < 0) { - goto finish; - } - - r = pipe_shutdown(process->child.err); - if (r < 0) { - goto finish; - } - - process->child.out = pipe_destroy(process->child.out); - process->child.err = pipe_destroy(process->child.err); - again = true; - } - - // If we've closed handles, we poll again so we can include any new close - // events that occurred because we closed handles. - - if (again) { - r = reproc_poll(sources, num_sources, timeout); - if (r < 0) { - goto finish; - } - } - } - -finish: - free(pipes); - - return r; -} - -int reproc_read(reproc_t *process, - REPROC_STREAM stream, - uint8_t *buffer, - size_t size) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(stream == REPROC_STREAM_OUT || stream == REPROC_STREAM_ERR); - ASSERT_EINVAL(buffer); - - pipe_type *pipe = stream == REPROC_STREAM_OUT ? &process->pipe.out - : &process->pipe.err; - pipe_type child = stream == REPROC_STREAM_OUT ? process->child.out - : process->child.err; - int r = -1; - - if (*pipe == PIPE_INVALID) { - return REPROC_EPIPE; - } - - // If we've kept extra handles open in the parent, make sure we use - // `reproc_poll` which closes the extra handles we keep open when the child - // process exits. If we don't, `pipe_read` will block forever because the - // extra handles we keep open in the parent would never be closed. - if (child != PIPE_INVALID) { - int event = stream == REPROC_STREAM_OUT ? REPROC_EVENT_OUT - : REPROC_EVENT_ERR; - reproc_event_source source = { process, event, 0 }; - r = reproc_poll(&source, 1, process->nonblocking ? 0 : REPROC_INFINITE); - if (r <= 0) { - return r == 0 ? REPROC_EWOULDBLOCK : r; - } - } - - r = pipe_read(*pipe, buffer, size); - - if (r == REPROC_EPIPE) { - *pipe = pipe_destroy(*pipe); - } - - return r; -} - -int reproc_write(reproc_t *process, const uint8_t *buffer, size_t size) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - - if (buffer == NULL) { - // Allow `NULL` buffers but only if `size == 0`. - ASSERT_EINVAL(size == 0); - return 0; - } - - if (process->pipe.in == PIPE_INVALID) { - return REPROC_EPIPE; - } - - int r = pipe_write(process->pipe.in, buffer, size); - - if (r == REPROC_EPIPE) { - process->pipe.in = pipe_destroy(process->pipe.in); - } - - return r; -} - -int reproc_close(reproc_t *process, REPROC_STREAM stream) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - - switch (stream) { - case REPROC_STREAM_IN: - process->pipe.in = pipe_destroy(process->pipe.in); - return 0; - case REPROC_STREAM_OUT: - process->pipe.out = pipe_destroy(process->pipe.out); - return 0; - case REPROC_STREAM_ERR: - process->pipe.err = pipe_destroy(process->pipe.err); - return 0; - } - - return REPROC_EINVAL; -} - -int reproc_wait(reproc_t *process, int timeout) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - int r = -1; - - if (process->status >= 0) { - return process->status; - } - - if (timeout == REPROC_DEADLINE) { - timeout = expiry(REPROC_INFINITE, process->deadline); - // If the deadline has expired, `expiry` returns `REPROC_DEADLINE` which - // means we'll only check if the process is still running. - if (timeout == REPROC_DEADLINE) { - timeout = 0; - } - } - - ASSERT(process->pipe.exit != PIPE_INVALID); - - pipe_event_source source = { .pipe = process->pipe.exit, - .interests = PIPE_EVENT_IN }; - - r = pipe_poll(&source, 1, timeout); - if (r <= 0) { - return r == 0 ? REPROC_ETIMEDOUT : r; - } - - r = process_wait(process->handle); - if (r < 0) { - return r; - } - - process->pipe.exit = pipe_destroy(process->pipe.exit); - - return process->status = r; -} - -int reproc_terminate(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - if (process->status >= 0) { - return 0; - } - - return process_terminate(process->handle); -} - -int reproc_kill(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - if (process->status >= 0) { - return 0; - } - - return process_kill(process->handle); -} - -int reproc_stop(reproc_t *process, reproc_stop_actions stop) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - stop = parse_stop_actions(stop); - - reproc_stop_action actions[] = { stop.first, stop.second, stop.third }; - int r = -1; - - for (size_t i = 0; i < ARRAY_SIZE(actions); i++) { - r = REPROC_EINVAL; // NOLINT - - switch (actions[i].action) { - case REPROC_STOP_NOOP: - r = 0; - continue; - case REPROC_STOP_WAIT: - r = 0; - break; - case REPROC_STOP_TERMINATE: - r = reproc_terminate(process); - break; - case REPROC_STOP_KILL: - r = reproc_kill(process); - break; - } - - // Stop if `reproc_terminate` or `reproc_kill` fail. - if (r < 0) { - break; - } - - r = reproc_wait(process, actions[i].timeout); - if (r != REPROC_ETIMEDOUT) { - break; - } - } - - return r; -} - -int reproc_pid(reproc_t *process) -{ - ASSERT_EINVAL(process); - ASSERT_EINVAL(process->status != STATUS_IN_CHILD); - ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); - - return process_pid(process->handle); -} - -reproc_t *reproc_destroy(reproc_t *process) -{ - ASSERT_RETURN(process, NULL); - - if (process->status == STATUS_IN_PROGRESS) { - reproc_stop(process, process->stop); - } - - process_destroy(process->handle); - pipe_destroy(process->pipe.in); - pipe_destroy(process->pipe.out); - pipe_destroy(process->pipe.err); - pipe_destroy(process->pipe.exit); - - pipe_destroy(process->child.out); - pipe_destroy(process->child.err); - - if (process->status != STATUS_NOT_STARTED) { - deinit(); - } - - free(process); - - return NULL; -} - -const char *reproc_strerror(int error) -{ - return error_string(error); -} diff --git a/extern/reproc-14.2.4/reproc/src/run.c b/extern/reproc-14.2.4/reproc/src/run.c deleted file mode 100644 index fbdc3c41d..000000000 --- a/extern/reproc-14.2.4/reproc/src/run.c +++ /dev/null @@ -1,54 +0,0 @@ -#include - -#include - -#include "error.h" - -int reproc_run(const char *const *argv, reproc_options options) -{ - if (!options.redirect.discard && !options.redirect.file && - !options.redirect.path) { - options.redirect.parent = true; - } - - return reproc_run_ex(argv, options, REPROC_SINK_NULL, REPROC_SINK_NULL); -} - -int reproc_run_ex(const char *const *argv, - reproc_options options, // lgtm [cpp/large-parameter] - reproc_sink out, - reproc_sink err) -{ - reproc_t *process = NULL; - int r = REPROC_ENOMEM; - - // There's no way for `reproc_run_ex` to inform the caller whether we're in - // the forked process or the parent process so let's not allow forking when - // using `reproc_run_ex`. - ASSERT_EINVAL(!options.fork); - - process = reproc_new(); - if (process == NULL) { - goto finish; - } - - r = reproc_start(process, argv, options); - if (r < 0) { - goto finish; - } - - r = reproc_drain(process, out, err); - if (r < 0) { - goto finish; - } - - r = reproc_stop(process, options.stop); - if (r < 0) { - goto finish; - } - -finish: - reproc_destroy(process); - - return r; -} diff --git a/extern/reproc-14.2.4/reproc/src/strv.c b/extern/reproc-14.2.4/reproc/src/strv.c deleted file mode 100644 index 210b4d4c5..000000000 --- a/extern/reproc-14.2.4/reproc/src/strv.c +++ /dev/null @@ -1,88 +0,0 @@ -#include "strv.h" - -#include -#include -#include - -#include "error.h" - -static char *str_dup(const char *s) -{ - ASSERT_RETURN(s, NULL); - - char *r = malloc(strlen(s) + 1); - if (!r) { - return NULL; - } - - strcpy(r, s); // NOLINT - - return r; -} - -char **strv_concat(char *const *a, const char *const *b) -{ - char *const *i = NULL; - const char *const *j = NULL; - size_t size = 1; - size_t c = 0; - - STRV_FOREACH(i, a) { - size++; - } - - STRV_FOREACH(j, b) { - size++; - } - - char **r = calloc(size, sizeof(char *)); - if (!r) { - goto finish; - } - - STRV_FOREACH(i, a) { - r[c] = str_dup(*i); - if (!r[c]) { - goto finish; - } - - c++; - } - - STRV_FOREACH(j, b) { - r[c] = str_dup(*j); - if (!r[c]) { - goto finish; - } - - c++; - } - - r[c++] = NULL; - -finish: - if (c < size) { - STRV_FOREACH(i, r) { - free(*i); - } - - free(r); - - return NULL; - } - - return r; -} - -char **strv_free(char **l) -{ - char **s = NULL; - - STRV_FOREACH(s, l) { - free(*s); - } - - free(l); - - return NULL; -} diff --git a/extern/reproc-14.2.4/reproc/src/strv.h b/extern/reproc-14.2.4/reproc/src/strv.h deleted file mode 100644 index d5b273821..000000000 --- a/extern/reproc-14.2.4/reproc/src/strv.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#define STRV_FOREACH(s, l) for ((s) = (l); (s) && *(s); (s)++) - -char **strv_concat(char *const *a, const char *const *b); - -char **strv_free(char **l); diff --git a/extern/reproc-14.2.4/reproc/src/utf.h b/extern/reproc-14.2.4/reproc/src/utf.h deleted file mode 100644 index d6a96b395..000000000 --- a/extern/reproc-14.2.4/reproc/src/utf.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -// `size` represents the entire size of `string`, including NUL-terminators. We -// take the entire size because strings like the environment string passed to -// CreateProcessW includes multiple NUL-terminators so we can't always rely on -// `strlen` to calculate the string length for us. See the lpEnvironment -// documentation of CreateProcessW: -// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw -// Pass -1 as the size to have `utf16_from_utf8` calculate the size until (and -// including) the first NUL terminator. -wchar_t *utf16_from_utf8(const char *string, int size); diff --git a/extern/reproc-14.2.4/reproc/src/utf.posix.c b/extern/reproc-14.2.4/reproc/src/utf.posix.c deleted file mode 100644 index cdb71a394..000000000 --- a/extern/reproc-14.2.4/reproc/src/utf.posix.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "utf.h" - -// `utf16_from_utf8` is Windows-only. diff --git a/extern/reproc-14.2.4/reproc/src/utf.windows.c b/extern/reproc-14.2.4/reproc/src/utf.windows.c deleted file mode 100644 index 92f1e08dd..000000000 --- a/extern/reproc-14.2.4/reproc/src/utf.windows.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "utf.h" - -#include -#include -#include - -#include "error.h" - -wchar_t *utf16_from_utf8(const char *string, int size) -{ - ASSERT(string); - - // Determine wstring size (`MultiByteToWideChar` returns the required size if - // its last two arguments are `NULL` and 0). - int r = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string, size, NULL, - 0); - if (r == 0) { - return NULL; - } - - // `MultiByteToWideChar` does not return negative values so the cast to - // `size_t` is safe. - wchar_t *wstring = calloc((size_t) r, sizeof(wchar_t)); - if (wstring == NULL) { - SetLastError(ERROR_NOT_ENOUGH_MEMORY); - return NULL; - } - - // Now we pass our allocated string and its size as the last two arguments - // instead of `NULL` and 0 which makes `MultiByteToWideChar` actually perform - // the conversion. - r = MultiByteToWideChar(CP_UTF8, 0, string, size, wstring, r); - if (r == 0) { - free(wstring); - return NULL; - } - - return wstring; -} diff --git a/extern/reproc-14.2.4/reproc/test/argv.c b/extern/reproc-14.2.4/reproc/test/argv.c deleted file mode 100644 index 0c383c0ac..000000000 --- a/extern/reproc-14.2.4/reproc/test/argv.c +++ /dev/null @@ -1,33 +0,0 @@ -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/argv", "\"argument 1\"", - "\"argument 2\"", NULL }; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink); - ASSERT_OK(r); - ASSERT(output != NULL); - - const char *current = output; - - for (size_t i = 0; i < 3; i++) { - size_t size = strlen(argv[i]); - - ASSERT_GE_SIZE(strlen(current), size); - ASSERT_EQ_MEM(current, argv[i], size); - - current += size; - current += *current == '\r'; - current += *current == '\n'; - } - - ASSERT_EQ_SIZE(strlen(current), (size_t) 0); - - reproc_free(output); -} diff --git a/extern/reproc-14.2.4/reproc/test/assert.h b/extern/reproc-14.2.4/reproc/test/assert.h deleted file mode 100644 index ea0d0a9c8..000000000 --- a/extern/reproc-14.2.4/reproc/test/assert.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include -#include - -#define ASSERT(expression) ASSERT_MSG(expression, "%s", "") -#define ASSERT_OK(r) ASSERT_MSG(r >= 0, "%s", reproc_strerror(r)) - -#define ASSERT_EQ_MEM(left, right, size) \ - ASSERT_MSG(memcmp(left, right, size) == 0, "\"%.*s\" == \"%.*s\"", \ - (int) size, left, (int) size, right) - -#define ASSERT_EQ_STR(left, right) \ - ASSERT_MSG(strcmp(left, right) == 0, "%s == %s", left, right) - -#define ASSERT_GE_SIZE(left, right) \ - ASSERT_MSG(left >= right, "%zu >= %zu", left, right) - -#define ASSERT_EQ_SIZE(left, right) \ - ASSERT_MSG(left == right, "%zu == %zu", left, right) - -#define ASSERT_EQ_INT(left, right) \ - ASSERT_MSG(left == right, "%i == %i", left, right) - -#ifdef _WIN32 - #define ABORT() exit(EXIT_FAILURE) -#else - // Use `abort` so we get a coredump. - #define ABORT() abort() -#endif - -#define ASSERT_MSG(expression, format, ...) \ - do { \ - if (!(expression)) { \ - fprintf(stderr, "%s:%u: Assertion '%s' (" format ") failed", __FILE__, \ - __LINE__, #expression, __VA_ARGS__); \ - \ - fflush(stderr); \ - \ - ABORT(); \ - } \ - } while (0) diff --git a/extern/reproc-14.2.4/reproc/test/deadline.c b/extern/reproc-14.2.4/reproc/test/deadline.c deleted file mode 100644 index 017cced62..000000000 --- a/extern/reproc-14.2.4/reproc/test/deadline.c +++ /dev/null @@ -1,10 +0,0 @@ -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/deadline", NULL }; - int r = reproc_run(argv, (reproc_options){ .deadline = 100 }); - ASSERT(r == REPROC_SIGTERM); -} diff --git a/extern/reproc-14.2.4/reproc/test/env.c b/extern/reproc-14.2.4/reproc/test/env.c deleted file mode 100644 index 74a508dca..000000000 --- a/extern/reproc-14.2.4/reproc/test/env.c +++ /dev/null @@ -1,36 +0,0 @@ -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/env", NULL }; - const char *envp[] = { "IP=127.0.0.1", "PORT=8080", NULL }; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - r = reproc_run_ex(argv, - (reproc_options){ .env.behavior = REPROC_ENV_EMPTY, - .env.extra = envp }, - sink, sink); - ASSERT_OK(r); - ASSERT(output != NULL); - - const char *current = output; - - for (size_t i = 0; i < 2; i++) { - size_t size = strlen(envp[i]); - - ASSERT_GE_SIZE(strlen(current), size); - ASSERT_EQ_MEM(current, envp[i], size); - - current += size; - current += *current == '\r'; - current += *current == '\n'; - } - - ASSERT_EQ_SIZE(strlen(current), (size_t) 0); - - reproc_free(output); -} diff --git a/extern/reproc-14.2.4/reproc/test/fork.c b/extern/reproc-14.2.4/reproc/test/fork.c deleted file mode 100644 index 33a8ba4c9..000000000 --- a/extern/reproc-14.2.4/reproc/test/fork.c +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include - -#include - -#include "assert.h" - -int main(void) -{ - reproc_t *process = reproc_new(); - const char *MESSAGE = "reproc stands for REdirected PROCess!"; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - r = reproc_start(process, NULL, (reproc_options){ .fork = true }); - - if (r == 0) { - printf("%s", MESSAGE); - fclose(stdout); // `_exit` doesn't flush stdout. - _exit(EXIT_SUCCESS); - } - - ASSERT_OK(r); - - r = reproc_drain(process, sink, sink); - ASSERT_OK(r); - - ASSERT(output != NULL); - ASSERT_EQ_STR(output, MESSAGE); - - r = reproc_wait(process, REPROC_INFINITE); - ASSERT_OK(r); - - reproc_destroy(process); - - reproc_free(output); -} diff --git a/extern/reproc-14.2.4/reproc/test/io.c b/extern/reproc-14.2.4/reproc/test/io.c deleted file mode 100644 index e65af9c11..000000000 --- a/extern/reproc-14.2.4/reproc/test/io.c +++ /dev/null @@ -1,74 +0,0 @@ -#include -#include - -#include "assert.h" - -#define MESSAGE "reproc stands for REdirected PROCess" - -static void io() -{ - int r = -1; - - reproc_t *process = reproc_new(); - ASSERT(process); - - const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL }; - - r = reproc_start(process, argv, - (reproc_options){ - .redirect.err.type = REPROC_REDIRECT_STDOUT }); - ASSERT_OK(r); - - r = reproc_write(process, (uint8_t *) MESSAGE, strlen(MESSAGE)); - ASSERT_OK(r); - ASSERT_EQ_INT(r, (int) strlen(MESSAGE)); - - r = reproc_close(process, REPROC_STREAM_IN); - ASSERT_OK(r); - - char *out = NULL; - r = reproc_drain(process, reproc_sink_string(&out), REPROC_SINK_NULL); - ASSERT_OK(r); - - ASSERT(out != NULL); - ASSERT_EQ_STR(out, MESSAGE MESSAGE); - - r = reproc_wait(process, REPROC_INFINITE); - ASSERT_OK(r); - - reproc_destroy(process); - - reproc_free(out); -} - -static void timeout(void) -{ - int r = -1; - - reproc_t *process = reproc_new(); - ASSERT(process); - - const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL }; - - r = reproc_start(process, argv, (reproc_options){ 0 }); - ASSERT_OK(r); - - reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR, - 0 }; - r = reproc_poll(&source, 1, 200); - ASSERT(r == 0); - - r = reproc_close(process, REPROC_STREAM_IN); - ASSERT_OK(r); - - r = reproc_poll(&source, 1, 200); - ASSERT_OK(r); - - reproc_destroy(process); -} - -int main(void) -{ - io(); - timeout(); -} diff --git a/extern/reproc-14.2.4/reproc/test/overflow.c b/extern/reproc-14.2.4/reproc/test/overflow.c deleted file mode 100644 index a93f9432d..000000000 --- a/extern/reproc-14.2.4/reproc/test/overflow.c +++ /dev/null @@ -1,17 +0,0 @@ -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/overflow", NULL }; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink); - ASSERT_OK(r); - ASSERT(output != NULL); - - reproc_free(output); -} diff --git a/extern/reproc-14.2.4/reproc/test/path.c b/extern/reproc-14.2.4/reproc/test/path.c deleted file mode 100644 index 23319cfd4..000000000 --- a/extern/reproc-14.2.4/reproc/test/path.c +++ /dev/null @@ -1,41 +0,0 @@ -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/path", NULL }; - int r = -1; - - r = reproc_run(argv, (reproc_options){ .redirect.path = "path.txt" }); - ASSERT_OK(r); - - FILE *file = fopen("path.txt", "rb"); - ASSERT(file != NULL); - - r = fseek(file, 0, SEEK_END); - ASSERT_OK(r); - - r = (int) ftell(file); - ASSERT_OK(r); - - size_t size = (size_t) r; - char *string = malloc(size + 1); - ASSERT(string != NULL); - - rewind(file); - r = (int) fread(string, sizeof(char), size, file); - ASSERT_EQ_INT(r, (int) size); - - string[r] = '\0'; - - r = fclose(file); - ASSERT_OK(r); - - r = remove("path.txt"); - ASSERT_OK(r); - - ASSERT_EQ_STR(string, argv[0]); - - free(string); -} diff --git a/extern/reproc-14.2.4/reproc/test/pid.c b/extern/reproc-14.2.4/reproc/test/pid.c deleted file mode 100644 index cabc519a3..000000000 --- a/extern/reproc-14.2.4/reproc/test/pid.c +++ /dev/null @@ -1,37 +0,0 @@ -#include -#include - -#include -#include - -#include "assert.h" - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/pid", NULL }; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - reproc_t *process = reproc_new(); - ASSERT(process); - - ASSERT(reproc_pid(process) == REPROC_EINVAL); - - r = reproc_start(process, argv, (reproc_options){ 0 }); - ASSERT_OK(r); - - r = reproc_drain(process, sink, sink); - ASSERT_OK(r); - ASSERT(output != NULL); - - ASSERT(reproc_pid(process) == strtol(output, NULL, 10)); - - r = reproc_wait(process, REPROC_INFINITE); - ASSERT_OK(r); - - ASSERT(reproc_pid(process) == strtol(output, NULL, 10)); - - reproc_destroy(process); - reproc_free(output); -} diff --git a/extern/reproc-14.2.4/reproc/test/stop.c b/extern/reproc-14.2.4/reproc/test/stop.c deleted file mode 100644 index cbf744408..000000000 --- a/extern/reproc-14.2.4/reproc/test/stop.c +++ /dev/null @@ -1,32 +0,0 @@ -#include - -#include "assert.h" - -static void stop(REPROC_STOP action, int status) -{ - int r = -1; - - reproc_t *process = reproc_new(); - ASSERT(process); - - const char *argv[] = { RESOURCE_DIRECTORY "/stop", NULL }; - - r = reproc_start(process, argv, (reproc_options){ 0 }); - ASSERT_OK(r); - - r = reproc_wait(process, 50); - ASSERT(r == REPROC_ETIMEDOUT); - - reproc_stop_actions stop = { .first = { action, 500 } }; - - r = reproc_stop(process, stop); - ASSERT_EQ_INT(r, status); - - reproc_destroy(process); -} - -int main(void) -{ - stop(REPROC_STOP_TERMINATE, REPROC_SIGTERM); - stop(REPROC_STOP_KILL, REPROC_SIGKILL); -} diff --git a/extern/reproc-14.2.4/reproc/test/working-directory.c b/extern/reproc-14.2.4/reproc/test/working-directory.c deleted file mode 100644 index 55515f46f..000000000 --- a/extern/reproc-14.2.4/reproc/test/working-directory.c +++ /dev/null @@ -1,29 +0,0 @@ -#include - -#include "assert.h" - -static void replace(char *string, char old, char new) -{ - for (size_t i = 0; i < strlen(string); i++) { - string[i] = (char) (string[i] == old ? new : string[i]); - } -} - -int main(void) -{ - const char *argv[] = { RESOURCE_DIRECTORY "/working-directory", NULL }; - char *output = NULL; - reproc_sink sink = reproc_sink_string(&output); - int r = -1; - - r = reproc_run_ex(argv, - (reproc_options){ .working_directory = RESOURCE_DIRECTORY }, - sink, sink); - ASSERT_OK(r); - ASSERT(output != NULL); - - replace(output, '\\', '/'); - ASSERT_EQ_STR(output, RESOURCE_DIRECTORY); - - reproc_free(output); -} diff --git a/include/xstudio/ui/qml/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index 34a018f09..b6cec1bdf 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -1,47 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ #include #include @@ -54,6 +13,8 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include "helper_qml_export.h" + CAF_PUSH_WARNINGS #include #include @@ -84,41 +45,6 @@ namespace ui { QVariant mapFromValue(const nlohmann::json &value); nlohmann::json mapFromValue(const QVariant &value); - class 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 ModelRowCount : public QObject { Q_OBJECT @@ -286,7 +212,7 @@ namespace ui { QString default_role_ = {"defaultValueRole"}; }; - class CafSystemObject : public QObject { + class HELPER_QML_EXPORT CafSystemObject : public QObject { Q_OBJECT @@ -397,7 +323,7 @@ namespace ui { return jsn; } - class Helpers : public QObject { + class HELPER_QML_EXPORT Helpers : public QObject { Q_OBJECT public: @@ -673,7 +599,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/json_tree_model_ui.hpp b/include/xstudio/ui/qml/json_tree_model_ui.hpp index 3b95dcfc0..435758edf 100644 --- a/include/xstudio/ui/qml/json_tree_model_ui.hpp +++ b/include/xstudio/ui/qml/json_tree_model_ui.hpp @@ -1,48 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ - #include #include #include @@ -56,6 +14,8 @@ CAF_POP_WARNINGS #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/tree.hpp" +#include "helper_qml_export.h" + namespace xstudio::ui::qml { class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { diff --git a/include/xstudio/ui/qml/model_data_ui.hpp b/include/xstudio/ui/qml/model_data_ui.hpp index 37f066374..0ad95e8da 100644 --- a/include/xstudio/ui/qml/model_data_ui.hpp +++ b/include/xstudio/ui/qml/model_data_ui.hpp @@ -1,53 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ - #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/ui/qml/tag_ui.hpp" CAF_PUSH_WARNINGS @@ -164,7 +122,7 @@ class HELPER_QML_EXPORT ReskinPanelsModel : public UIModelData { Q_INVOKABLE void duplicate_layout(QModelIndex panel_index); }; -class MediaListColumnsModel : public UIModelData { +class HELPER_QML_EXPORT MediaListColumnsModel : public UIModelData { Q_OBJECT diff --git a/include/xstudio/ui/qml/module_data_ui.hpp b/include/xstudio/ui/qml/module_data_ui.hpp index bf4cc4a63..4986a59e1 100644 --- a/include/xstudio/ui/qml/module_data_ui.hpp +++ b/include/xstudio/ui/qml/module_data_ui.hpp @@ -15,7 +15,7 @@ CAF_POP_WARNINGS namespace xstudio::ui::qml { using namespace caf; -class ModulesModelData : public UIModelData { +class HELPER_QML_EXPORT ModulesModelData : public UIModelData { Q_OBJECT diff --git a/include/xstudio/ui/qml/snapshot_model_ui.hpp b/include/xstudio/ui/qml/snapshot_model_ui.hpp index 5a3ff6d65..3a171d52f 100644 --- a/include/xstudio/ui/qml/snapshot_model_ui.hpp +++ b/include/xstudio/ui/qml/snapshot_model_ui.hpp @@ -16,7 +16,7 @@ CAF_POP_WARNINGS namespace xstudio::ui::qml { using namespace caf; -class SnapshotModel : public JSONTreeModel { +class HELPER_QML_EXPORT SnapshotModel : public JSONTreeModel { Q_OBJECT Q_PROPERTY(QVariant paths READ paths WRITE setPaths NOTIFY pathsChanged) diff --git a/include/xstudio/ui/qml/tag_ui.hpp b/include/xstudio/ui/qml/tag_ui.hpp index 094a2fe6d..ae1f9aa6d 100644 --- a/include/xstudio/ui/qml/tag_ui.hpp +++ b/include/xstudio/ui/qml/tag_ui.hpp @@ -2,6 +2,50 @@ #pragma once #pragma once +#pragma once + +#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 */ + #include #include @@ -85,7 +129,7 @@ namespace ui { void reset(); }; - class TagManagerUI : public QMLActor { + class TAG_QML_EXPORT TagManagerUI : public QMLActor { Q_OBJECT diff --git a/include/xstudio/utility/file_system_item.hpp b/include/xstudio/utility/file_system_item.hpp index b9ebc095a..a20202150 100644 --- a/include/xstudio/utility/file_system_item.hpp +++ b/include/xstudio/utility/file_system_item.hpp @@ -3,7 +3,10 @@ #pragma once // #include +#ifdef _WIN32 +#else #include +#endif #include #include #include diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index e3a0cdcbe..952298eae 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -90,6 +90,16 @@ 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; @@ -276,6 +286,7 @@ namespace utility { 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 @@ -383,7 +394,7 @@ inline std::string snippets_path(const std::string &append_path = "") { inline bool is_session(const std::string &path) { fs::path p(path); - std::string ext = to_upper(p.extension()); + std::string ext = to_upper(path_to_string(p.extension())); for (const auto &i : session_extensions) if (i == ext) return true; diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index 06abe8450..45b93cf39 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -182,6 +182,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; diff --git a/src/conform/src/CMakeLists.txt b/src/conform/src/CMakeLists.txt index 06b0d6aa0..cdf381bf0 100644 --- a/src/conform/src/CMakeLists.txt +++ b/src/conform/src/CMakeLists.txt @@ -1,6 +1,8 @@ SET(LINK_DEPS xstudio::utility xstudio::broadcast + xstudio::json_store + xstudio::global_store caf::core ) diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index 289530b05..3578e542f 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -107,6 +107,11 @@ bool shutdown_xstudio = false; struct ExitTimeoutKiller { void start() { +#ifdef _WIN32 + spdlog::debug("ExitTimeoutKiller start ignored"); + } +#else + // lock the mutex ... clean_actor_system_exit.lock(); @@ -124,17 +129,23 @@ struct ExitTimeoutKiller { } }); } +#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; diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index 50f7c83fe..4b7e24ee5 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -1561,7 +1561,7 @@ void PlayheadActor::update_duration(caf::typed_response_promiseset_value(duration); send(event_group_, utility::event_atom_v, duration_frames_atom_v, duration); }, diff --git a/src/playhead/src/sub_playhead.cpp b/src/playhead/src/sub_playhead.cpp index b8dd18787..5e4404548 100644 --- a/src/playhead/src/sub_playhead.cpp +++ b/src/playhead/src/sub_playhead.cpp @@ -211,13 +211,13 @@ void SubPlayhead::init() { return rp; }, - [=](duration_frames_atom atom) -> result { + [=](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(); + auto rp = make_response_promise(); request(caf::actor_cast(this), infinite, source_atom_v) .then( [=](caf::actor) mutable { @@ -372,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 { @@ -386,7 +386,7 @@ void SubPlayhead::init() { } return *(p->second); } - return make_error(xstudio_error::error, "No Frames"); + return make_error(xstudio_error::error, "No frames"); }, [=](media::get_media_pointer_atom) -> result { diff --git a/src/plugin_manager/src/plugin_base.cpp b/src/plugin_manager/src/plugin_base.cpp index 54f64aa4a..e0d470a9b 100644 --- a/src/plugin_manager/src/plugin_base.cpp +++ b/src/plugin_manager/src/plugin_base.cpp @@ -242,7 +242,7 @@ 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); - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } diff --git a/src/session/src/session_actor.cpp b/src/session/src/session_actor.cpp index 46f3fd31c..7f5b944c6 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -1911,7 +1911,7 @@ void SessionActor::save_json_to( // compress data. - if (to_lower(fs::path(save_path).extension()) == ".xsz") { + 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); diff --git a/src/timeline/src/item.cpp b/src/timeline/src/item.cpp index bcd93c304..9de919bd5 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -726,7 +726,7 @@ void Item::redo(const utility::JsonStore &event) { bool Item::process_event(const utility::JsonStore &event) { // spdlog::warn("{}", event.dump(2)); - if (event.at("uuid") == uuid_addr_.first) { + if (Uuid(event.at("uuid")) == uuid_addr_.first) { switch (static_cast(event.at("action"))) { case IT_ENABLE: set_enabled_direct(event.at("value")); diff --git a/src/ui/canvas/src/CMakeLists.txt b/src/ui/canvas/src/CMakeLists.txt index 995a0fd31..240b4d906 100644 --- a/src/ui/canvas/src/CMakeLists.txt +++ b/src/ui/canvas/src/CMakeLists.txt @@ -1,18 +1,26 @@ SET(LINK_DEPS - pthread xstudio::utility Imath::Imath OpenEXR::OpenEXR + ui_base ) -find_package(Freetype) +if(UNIX) + list(APPEND LINK_DEPS pthread) +endif() + +if(WIN32) + find_package(freetype CONFIG REQUIRED) +else() + find_package(Freetype) + include_directories("${FREETYPE_INCLUDE_DIRS}") +endif() + find_package(Imath) find_package(OpenEXR) create_component_with_alias(ui_canvas xstudio::ui::canvas 0.1.0 "${LINK_DEPS}") -include_directories("${FREETYPE_INCLUDE_DIRS}") - target_link_libraries(${PROJECT_NAME} PRIVATE freetype diff --git a/src/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index ef5e83b34..774a6dbc9 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -355,7 +355,7 @@ void GlobalUIModelData::set_data( broadcast_whole_model_data(model_name); } - } catch (std::exception &e) { + } catch ([[maybe_unused]]std::exception &e) { // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -434,7 +434,7 @@ void GlobalUIModelData::set_data( } } - } catch (std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -493,7 +493,7 @@ void GlobalUIModelData::insert_attribute_data_into_model( } } - } catch (std::exception &e) { + } 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]; diff --git a/src/ui/qml/playhead/src/playhead_ui.cpp b/src/ui/qml/playhead/src/playhead_ui.cpp index 6497feee6..d66097ad7 100644 --- a/src/ui/qml/playhead/src/playhead_ui.cpp +++ b/src/ui/qml/playhead/src/playhead_ui.cpp @@ -97,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_ = @@ -232,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) { diff --git a/src/ui/qml/session/src/CMakeLists.txt b/src/ui/qml/session/src/CMakeLists.txt index 6e74a5388..e525aa794 100644 --- a/src/ui/qml/session/src/CMakeLists.txt +++ b/src/ui/qml/session/src/CMakeLists.txt @@ -3,6 +3,7 @@ SET(LINK_DEPS Qt5::Core Qt5::Test xstudio::ui::qml::helper + xstudio::ui::qml::tag xstudio::timeline xstudio::utility xstudio::session @@ -11,6 +12,7 @@ SET(LINK_DEPS SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/session_model_ui.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/caf_response_ui.hpp" + #"${ROOT_DIR}/include/xstudio/ui/qml/tag_ui.hpp" ) create_qml_component(session 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") 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 f61e9c194..224f8ca67 100644 --- a/src/ui/qml/session/src/session_model_core_ui.cpp +++ b/src/ui/qml/session/src/session_model_core_ui.cpp @@ -748,7 +748,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { if (did >= 0 && did <= 9) { const std::string key = fmt::format("metadata_set{}", did); if (j.count(key)) { - if (j.at(key).is_null()) { + if (j.at(key).is_null() && !metadata_sets_.empty()) { requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), diff --git a/src/ui/qml/studio/src/CMakeLists.txt b/src/ui/qml/studio/src/CMakeLists.txt index fcde09b3f..58853a88e 100644 --- a/src/ui/qml/studio/src/CMakeLists.txt +++ b/src/ui/qml/studio/src/CMakeLists.txt @@ -4,6 +4,7 @@ SET(LINK_DEPS xstudio::ui::qml::helper xstudio::utility xstudio::session + xstudio::ui::qt::viewport_widget ) SET(EXTRAMOC diff --git a/src/utility/src/file_system_item.cpp b/src/utility/src/file_system_item.cpp index 475021241..1b15f2c1b 100644 --- a/src/utility/src/file_system_item.cpp +++ b/src/utility/src/file_system_item.cpp @@ -12,9 +12,9 @@ FileSystemItem::FileSystemItem(const fs::directory_entry &entry) : FileSystemIte else type_ = FSIT_DIRECTORY; - path_ = posix_path_to_uri(entry.path()); + path_ = posix_path_to_uri(path_to_string(entry.path())); // last_write_ = fs::last_write_time(entry.path()); - name_ = entry.path().filename(); + name_ = path_to_string(entry.path().filename()); } bool FileSystemItem::scan(const int depth, const bool ignore_last_write) { @@ -65,7 +65,7 @@ bool FileSystemItem::scan(const int depth, const bool ignore_last_write) { try { if (ignore_entry_callback_ == nullptr or not ignore_entry_callback_(entry)) { - auto cpath = posix_path_to_uri(entry.path()); + auto cpath = posix_path_to_uri(path_to_string(entry.path())); // is new entry ? FileSystemItems::iterator it = end(); @@ -170,7 +170,7 @@ 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(entry.path().extension()); + auto ext = to_lower(path_to_string(entry.path().extension())); if (ext != ".xst" and ext != ".xsz") result = true; } diff --git a/src/utility/src/logging.cpp b/src/utility/src/logging.cpp index 6ddfeca71..9f2a9e6a0 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); From 75c5cd031e7d4a2c15f649186f25da81fd8abf41 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 21 May 2024 12:51:03 -0700 Subject: [PATCH 16/42] Intermediate commit including the merge. Signed-off-by: Michael Kessler --- cmake/macros.cmake | 10 ++++++---- vcpkg.json | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 1f5372e50..e98322388 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -281,7 +281,7 @@ macro(create_component_with_alias NAME ALIASNAME VERSION DEPS) if(_WIN32) set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) - endif(WIN32) + endif(_WIN32) # Generate export header include(GenerateExportHeader) @@ -387,6 +387,10 @@ 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} ) @@ -399,9 +403,7 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) # PUBLIC ${CMAKE_BINARY_DIR} # Include the build directory #) - # Generate export header - include(GenerateExportHeader) - generate_export_header(${PROJECT_NAME}) + endmacro() diff --git a/vcpkg.json b/vcpkg.json index 797b1f137..cd126c1ea 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "xstudio", "version": "1.0.0", - "dependencies": [ + "dependencies": [ "stduuid", "reproc", "nlohmann-json", From 9c6061ec92c0c9bf2ad2e715885e8f457eba6e20 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 24 May 2024 13:30:50 -0700 Subject: [PATCH 17/42] Further updates to fix merge Signed-off-by: Michael Kessler --- CMakeLists.txt | 2 + cmake/macros.cmake | 6 +- extern/reproc/.clang-format | 22 + extern/reproc/.clang-tidy | 40 + extern/reproc/.editorconfig | 9 + .../.github/workflows/codeql-analysis.yml | 43 + extern/reproc/.github/workflows/main.yml | 130 ++ extern/reproc/.github/workflows/vsenv.ps1 | 15 + extern/reproc/CHANGELOG.md | 1076 +++++++++++++++++ extern/reproc/CMakeLists.txt | 36 + extern/reproc/LICENSE | 21 + extern/reproc/README.md | 301 +++++ extern/reproc/cmake/reproc.cmake | 408 +++++++ extern/reproc/reproc++/CMakeLists.txt | 22 + .../reproc/reproc++/examples/background.cpp | 89 ++ extern/reproc/reproc++/examples/drain.cpp | 76 ++ extern/reproc/reproc++/examples/forward.cpp | 83 ++ extern/reproc/reproc++/examples/run.cpp | 24 + .../reproc++/include/reproc++/arguments.hpp | 59 + .../include/reproc++/detail/array.hpp | 53 + .../include/reproc++/detail/type_traits.hpp | 18 + .../reproc++/include/reproc++/drain.hpp | 152 +++ .../reproc/reproc++/include/reproc++/env.hpp | 76 ++ .../reproc++/include/reproc++/export.hpp | 21 + .../reproc++/include/reproc++/input.hpp | 37 + .../reproc++/include/reproc++/reproc.hpp | 223 ++++ .../reproc/reproc++/include/reproc++/run.hpp | 41 + .../reproc/reproc++/reproc++-config.cmake.in | 13 + extern/reproc/reproc++/reproc++.pc.in | 13 + extern/reproc/reproc++/src/reproc.cpp | 168 +++ extern/reproc/reproc/CMakeLists.txt | 62 + extern/reproc/reproc/examples/drain.c | 62 + extern/reproc/reproc/examples/env.c | 21 + extern/reproc/reproc/examples/parent.c | 42 + extern/reproc/reproc/examples/path.c | 18 + extern/reproc/reproc/examples/poll.c | 107 ++ extern/reproc/reproc/examples/read.c | 108 ++ extern/reproc/reproc/examples/run.c | 19 + extern/reproc/reproc/include/reproc/drain.h | 79 ++ extern/reproc/reproc/include/reproc/export.h | 21 + extern/reproc/reproc/include/reproc/reproc.h | 530 ++++++++ extern/reproc/reproc/include/reproc/run.h | 27 + extern/reproc/reproc/reproc-config.cmake.in | 12 + extern/reproc/reproc/reproc.pc.in | 12 + extern/reproc/reproc/resources/argv.c | 10 + extern/reproc/reproc/resources/deadline.c | 7 + extern/reproc/reproc/resources/env.c | 13 + extern/reproc/reproc/resources/io.c | 15 + extern/reproc/reproc/resources/overflow.c | 14 + extern/reproc/reproc/resources/path.c | 10 + extern/reproc/reproc/resources/pid.c | 15 + extern/reproc/reproc/resources/sleep.h | 18 + extern/reproc/reproc/resources/stop.c | 7 + .../reproc/resources/working-directory.c | 21 + extern/reproc/reproc/src/clock.h | 5 + extern/reproc/reproc/src/clock.posix.c | 17 + extern/reproc/reproc/src/clock.windows.c | 10 + extern/reproc/reproc/src/drain.c | 121 ++ extern/reproc/reproc/src/error.h | 25 + extern/reproc/reproc/src/error.posix.c | 31 + extern/reproc/reproc/src/error.windows.c | 58 + extern/reproc/reproc/src/handle.h | 20 + extern/reproc/reproc/src/handle.posix.c | 42 + extern/reproc/reproc/src/handle.windows.c | 23 + extern/reproc/reproc/src/init.h | 5 + extern/reproc/reproc/src/init.posix.c | 10 + extern/reproc/reproc/src/init.windows.c | 24 + extern/reproc/reproc/src/macro.h | 11 + extern/reproc/reproc/src/options.c | 137 +++ extern/reproc/reproc/src/options.h | 7 + extern/reproc/reproc/src/pipe.h | 46 + extern/reproc/reproc/src/pipe.posix.c | 141 +++ extern/reproc/reproc/src/pipe.windows.c | 261 ++++ extern/reproc/reproc/src/process.h | 66 + extern/reproc/reproc/src/process.posix.c | 499 ++++++++ extern/reproc/reproc/src/process.windows.c | 506 ++++++++ extern/reproc/reproc/src/redirect.c | 164 +++ extern/reproc/reproc/src/redirect.h | 25 + extern/reproc/reproc/src/redirect.posix.c | 79 ++ extern/reproc/reproc/src/redirect.windows.c | 113 ++ extern/reproc/reproc/src/reproc.c | 695 +++++++++++ extern/reproc/reproc/src/run.c | 54 + extern/reproc/reproc/src/strv.c | 88 ++ extern/reproc/reproc/src/strv.h | 7 + extern/reproc/reproc/src/utf.h | 13 + extern/reproc/reproc/src/utf.posix.c | 3 + extern/reproc/reproc/src/utf.windows.c | 39 + extern/reproc/reproc/test/argv.c | 33 + extern/reproc/reproc/test/assert.h | 43 + extern/reproc/reproc/test/deadline.c | 10 + extern/reproc/reproc/test/env.c | 36 + extern/reproc/reproc/test/fork.c | 39 + extern/reproc/reproc/test/io.c | 74 ++ extern/reproc/reproc/test/overflow.c | 17 + extern/reproc/reproc/test/path.c | 41 + extern/reproc/reproc/test/pid.c | 37 + extern/reproc/reproc/test/stop.c | 32 + extern/reproc/reproc/test/working-directory.c | 29 + src/caf_utility/src/CMakeLists.txt | 4 +- src/caf_utility/src/caf_setup.cpp | 1 - .../media_metadata/ffprobe/src/ffprobe.cpp | 8 +- src/ui/model_data/src/model_data_actor.cpp | 18 +- src/ui/qml/bookmark/src/bookmark_model_ui.cpp | 3 +- src/ui/qml/bookmark/src/export.h | 42 + .../src/include/bookmark_qml_export.h | 42 + .../src/embedded_python_ui.cpp | 3 +- src/ui/qml/embedded_python/src/export.h | 42 + .../src/include/embedded_python_qml_export.h | 42 + src/ui/qml/event/src/export.h | 42 + .../qml/event/src/include/event_qml_export.h | 42 + src/ui/qml/global_store/src/export.h | 42 + .../src/include/global_store_qml_export.h | 42 + src/ui/qml/helper/src/helper_ui.cpp | 3 +- .../helper/src/include/helper_qml_export.h | 42 + src/ui/qml/helper/src/model_data_ui.cpp | 33 +- src/ui/qml/json_store/src/export.h | 42 + .../src/include/json_store_qml_export.h | 42 + src/ui/qml/log/src/export.h | 42 + src/ui/qml/log/src/include/log_qml_export.h | 42 + src/ui/qml/module/src/export.h | 42 + .../module/src/include/module_qml_export.h | 42 + src/ui/qml/playhead/src/export.h | 42 + .../src/include/playhead_qml_export.h | 42 + src/ui/qml/quickfuture/src/export.h | 42 + .../src/include/quickfuture_qml_export.h | 42 + src/ui/qml/session/src/export.h | 42 + .../session/src/include/session_qml_export.h | 42 + .../session/src/session_model_methods_ui.cpp | 8 +- src/ui/qml/studio/src/export.h | 42 + .../studio/src/include/studio_qml_export.h | 42 + src/ui/qml/tag/src/export.h | 42 + src/ui/qml/tag/src/include/tag_qml_export.h | 42 + src/ui/qml/viewport/src/export.h | 42 + .../src/include/viewport_qml_export.h | 42 + src/utility/src/helpers.cpp | 9 + src/utility/src/json_store.cpp | 4 +- vcpkg | 1 + 137 files changed, 9462 insertions(+), 30 deletions(-) create mode 100644 extern/reproc/.clang-format create mode 100644 extern/reproc/.clang-tidy create mode 100644 extern/reproc/.editorconfig create mode 100644 extern/reproc/.github/workflows/codeql-analysis.yml create mode 100644 extern/reproc/.github/workflows/main.yml create mode 100644 extern/reproc/.github/workflows/vsenv.ps1 create mode 100644 extern/reproc/CHANGELOG.md create mode 100644 extern/reproc/CMakeLists.txt create mode 100644 extern/reproc/LICENSE create mode 100644 extern/reproc/README.md create mode 100644 extern/reproc/cmake/reproc.cmake create mode 100644 extern/reproc/reproc++/CMakeLists.txt create mode 100644 extern/reproc/reproc++/examples/background.cpp create mode 100644 extern/reproc/reproc++/examples/drain.cpp create mode 100644 extern/reproc/reproc++/examples/forward.cpp create mode 100644 extern/reproc/reproc++/examples/run.cpp create mode 100644 extern/reproc/reproc++/include/reproc++/arguments.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/detail/array.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/detail/type_traits.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/drain.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/env.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/export.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/input.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/reproc.hpp create mode 100644 extern/reproc/reproc++/include/reproc++/run.hpp create mode 100644 extern/reproc/reproc++/reproc++-config.cmake.in create mode 100644 extern/reproc/reproc++/reproc++.pc.in create mode 100644 extern/reproc/reproc++/src/reproc.cpp create mode 100644 extern/reproc/reproc/CMakeLists.txt create mode 100644 extern/reproc/reproc/examples/drain.c create mode 100644 extern/reproc/reproc/examples/env.c create mode 100644 extern/reproc/reproc/examples/parent.c create mode 100644 extern/reproc/reproc/examples/path.c create mode 100644 extern/reproc/reproc/examples/poll.c create mode 100644 extern/reproc/reproc/examples/read.c create mode 100644 extern/reproc/reproc/examples/run.c create mode 100644 extern/reproc/reproc/include/reproc/drain.h create mode 100644 extern/reproc/reproc/include/reproc/export.h create mode 100644 extern/reproc/reproc/include/reproc/reproc.h create mode 100644 extern/reproc/reproc/include/reproc/run.h create mode 100644 extern/reproc/reproc/reproc-config.cmake.in create mode 100644 extern/reproc/reproc/reproc.pc.in create mode 100644 extern/reproc/reproc/resources/argv.c create mode 100644 extern/reproc/reproc/resources/deadline.c create mode 100644 extern/reproc/reproc/resources/env.c create mode 100644 extern/reproc/reproc/resources/io.c create mode 100644 extern/reproc/reproc/resources/overflow.c create mode 100644 extern/reproc/reproc/resources/path.c create mode 100644 extern/reproc/reproc/resources/pid.c create mode 100644 extern/reproc/reproc/resources/sleep.h create mode 100644 extern/reproc/reproc/resources/stop.c create mode 100644 extern/reproc/reproc/resources/working-directory.c create mode 100644 extern/reproc/reproc/src/clock.h create mode 100644 extern/reproc/reproc/src/clock.posix.c create mode 100644 extern/reproc/reproc/src/clock.windows.c create mode 100644 extern/reproc/reproc/src/drain.c create mode 100644 extern/reproc/reproc/src/error.h create mode 100644 extern/reproc/reproc/src/error.posix.c create mode 100644 extern/reproc/reproc/src/error.windows.c create mode 100644 extern/reproc/reproc/src/handle.h create mode 100644 extern/reproc/reproc/src/handle.posix.c create mode 100644 extern/reproc/reproc/src/handle.windows.c create mode 100644 extern/reproc/reproc/src/init.h create mode 100644 extern/reproc/reproc/src/init.posix.c create mode 100644 extern/reproc/reproc/src/init.windows.c create mode 100644 extern/reproc/reproc/src/macro.h create mode 100644 extern/reproc/reproc/src/options.c create mode 100644 extern/reproc/reproc/src/options.h create mode 100644 extern/reproc/reproc/src/pipe.h create mode 100644 extern/reproc/reproc/src/pipe.posix.c create mode 100644 extern/reproc/reproc/src/pipe.windows.c create mode 100644 extern/reproc/reproc/src/process.h create mode 100644 extern/reproc/reproc/src/process.posix.c create mode 100644 extern/reproc/reproc/src/process.windows.c create mode 100644 extern/reproc/reproc/src/redirect.c create mode 100644 extern/reproc/reproc/src/redirect.h create mode 100644 extern/reproc/reproc/src/redirect.posix.c create mode 100644 extern/reproc/reproc/src/redirect.windows.c create mode 100644 extern/reproc/reproc/src/reproc.c create mode 100644 extern/reproc/reproc/src/run.c create mode 100644 extern/reproc/reproc/src/strv.c create mode 100644 extern/reproc/reproc/src/strv.h create mode 100644 extern/reproc/reproc/src/utf.h create mode 100644 extern/reproc/reproc/src/utf.posix.c create mode 100644 extern/reproc/reproc/src/utf.windows.c create mode 100644 extern/reproc/reproc/test/argv.c create mode 100644 extern/reproc/reproc/test/assert.h create mode 100644 extern/reproc/reproc/test/deadline.c create mode 100644 extern/reproc/reproc/test/env.c create mode 100644 extern/reproc/reproc/test/fork.c create mode 100644 extern/reproc/reproc/test/io.c create mode 100644 extern/reproc/reproc/test/overflow.c create mode 100644 extern/reproc/reproc/test/path.c create mode 100644 extern/reproc/reproc/test/pid.c create mode 100644 extern/reproc/reproc/test/stop.c create mode 100644 extern/reproc/reproc/test/working-directory.c create mode 100644 src/ui/qml/bookmark/src/export.h create mode 100644 src/ui/qml/bookmark/src/include/bookmark_qml_export.h create mode 100644 src/ui/qml/embedded_python/src/export.h create mode 100644 src/ui/qml/embedded_python/src/include/embedded_python_qml_export.h create mode 100644 src/ui/qml/event/src/export.h create mode 100644 src/ui/qml/event/src/include/event_qml_export.h create mode 100644 src/ui/qml/global_store/src/export.h create mode 100644 src/ui/qml/global_store/src/include/global_store_qml_export.h create mode 100644 src/ui/qml/helper/src/include/helper_qml_export.h create mode 100644 src/ui/qml/json_store/src/export.h create mode 100644 src/ui/qml/json_store/src/include/json_store_qml_export.h create mode 100644 src/ui/qml/log/src/export.h create mode 100644 src/ui/qml/log/src/include/log_qml_export.h create mode 100644 src/ui/qml/module/src/export.h create mode 100644 src/ui/qml/module/src/include/module_qml_export.h create mode 100644 src/ui/qml/playhead/src/export.h create mode 100644 src/ui/qml/playhead/src/include/playhead_qml_export.h create mode 100644 src/ui/qml/quickfuture/src/export.h create mode 100644 src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h create mode 100644 src/ui/qml/session/src/export.h create mode 100644 src/ui/qml/session/src/include/session_qml_export.h create mode 100644 src/ui/qml/studio/src/export.h create mode 100644 src/ui/qml/studio/src/include/studio_qml_export.h create mode 100644 src/ui/qml/tag/src/export.h create mode 100644 src/ui/qml/tag/src/include/tag_qml_export.h create mode 100644 src/ui/qml/viewport/src/export.h create mode 100644 src/ui/qml/viewport/src/include/viewport_qml_export.h create mode 160000 vcpkg diff --git a/CMakeLists.txt b/CMakeLists.txt index 206da4b41..bc38b1550 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ option(USE_VCPKG "Use Vcpkg for package management" OFF) if(WIN32) set(USE_VCPKG ON) set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") + add_compile_options($<$:/MP>) + endif() if (USE_VCPKG) diff --git a/cmake/macros.cmake b/cmake/macros.cmake index e98322388..ccf68ef06 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -429,7 +429,11 @@ endmacro() macro(set_python_to_proper_build_type) if(WIN32) - target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:Py_DEBUG>") + #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:-DPy_DEBUG") + #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:PYTHON_IS_DEBUG=0>") + #add_compile_definitions(PY_NO_LINK_LIB) + set_property(TARGET ${PROJECT_NAME} PROPERTY EXCLUDE_FROM_DEFAULT_BUILD_DEBUG TRUE) + endif() endmacro() diff --git a/extern/reproc/.clang-format b/extern/reproc/.clang-format new file mode 100644 index 000000000..df06971c7 --- /dev/null +++ b/extern/reproc/.clang-format @@ -0,0 +1,22 @@ +--- +BasedOnStyle: LLVM +--- +Language: Cpp +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortFunctionsOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +BinPackParameters: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true + SplitEmptyRecord: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +Cpp11BracedListStyle: false +FixNamespaceComments: false +ForEachMacros: ['STRV_FOREACH', 'NULSTR_FOREACH'] +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +PenaltyBreakAssignment: 100 +PenaltyBreakBeforeFirstCallParameter: 100 +SpaceAfterCStyleCast: true +SpaceBeforeParens: ControlStatementsExceptForEachMacros diff --git a/extern/reproc/.clang-tidy b/extern/reproc/.clang-tidy new file mode 100644 index 000000000..35bb58f09 --- /dev/null +++ b/extern/reproc/.clang-tidy @@ -0,0 +1,40 @@ +--- +Checks: +' +*, +-altera-*, +-android-cloexec-fopen, +-android-cloexec-pipe, +-bugprone-easily-swappable-parameters, +-bugprone-exception-escape, +-bugprone-reserved-identifier, +-cert-*, +-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, +-clang-diagnostic-unused-command-line-argument, +-concurrency-mt-unsafe, +-cppcoreguidelines-avoid-c-arrays, +-cppcoreguidelines-avoid-magic-numbers, +-cppcoreguidelines-macro-usage, +-cppcoreguidelines-owning-memory, +-cppcoreguidelines-pro-bounds-array-to-pointer-decay, +-cppcoreguidelines-pro-bounds-pointer-arithmetic, +-cppcoreguidelines-pro-type-reinterpret-cast, +-cppcoreguidelines-pro-type-vararg, +-cppcoreguidelines-special-member-functions, +-fuchsia-*, +-llvmlibc-*, +-google-*, +-hicpp-*, +-llvm-else-after-return, +-llvm-header-guard, +-llvm-namespace-comment, +-misc-no-recursion, +-modernize-avoid-c-arrays, +-modernize-use-trailing-return-type, +-performance-no-int-to-ptr, +-readability-else-after-return, +-readability-function-cognitive-complexity, +-readability-magic-numbers, +' +HeaderFilterRegex: '.*reproc\+\+.*$' +... diff --git a/extern/reproc/.editorconfig b/extern/reproc/.editorconfig new file mode 100644 index 000000000..b1a1858f7 --- /dev/null +++ b/extern/reproc/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true diff --git a/extern/reproc/.github/workflows/codeql-analysis.yml b/extern/reproc/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..33a895a9f --- /dev/null +++ b/extern/reproc/.github/workflows/codeql-analysis.yml @@ -0,0 +1,43 @@ +name: "CodeQL" + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + analyze: + name: Analyze ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + language: [cpp] + os: [ubuntu-20.04, macos-latest, windows-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Configure + run: > + cmake + -B build + -DREPROC++=ON + -DREPROC_TEST=ON + -DREPROC_EXAMPLES=ON + + - name: Build + run: cmake --build build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/extern/reproc/.github/workflows/main.yml b/extern/reproc/.github/workflows/main.yml new file mode 100644 index 000000000..40004546f --- /dev/null +++ b/extern/reproc/.github/workflows/main.yml @@ -0,0 +1,130 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + ci: + name: ${{ matrix.os }}-${{ matrix.compiler }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + + - os: ubuntu-latest + compiler: clang + + - os: windows-latest + compiler: cl + + - os: windows-latest + compiler: clang-cl + + - os: windows-latest + compiler: clang + + - os: windows-latest + compiler: gcc + + - os: macos-latest + compiler: gcc + + - os: macos-latest + compiler: clang + + steps: + - uses: actions/checkout@v1 + + - name: Install (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get install -y --no-install-recommends ninja-build clang-tidy + + if [ "${{ matrix.compiler }}" = "gcc" ]; then + echo CC=gcc >> $GITHUB_ENV + echo CXX=g++ >> $GITHUB_ENV + else + echo CC=clang >> $GITHUB_ENV + echo CXX=clang++ >> $GITHUB_ENV + fi + + - name: Install (macOS) + if: runner.os == 'macOS' + run: | + brew install ninja + sudo ln -s /usr/local/opt/llvm/bin/clang-tidy /usr/local/bin/clang-tidy + + if [ "${{ matrix.compiler }}" = "gcc" ]; then + echo CC=gcc >> $GITHUB_ENV + echo CXX=g++ >> $GITHUB_ENV + else + echo CC=clang >> $GITHUB_ENV + echo CXX=clang++ >> $GITHUB_ENV + fi + + - name: Install (Windows) + if: runner.os == 'Windows' + run: | + Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') + scoop install ninja llvm --global + + if ("${{ matrix.compiler }}" -eq "gcc") { + echo CC=gcc | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + echo CXX=g++ | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + } elseif ("${{ matrix.compiler }}" -eq "clang") { + echo CC=clang | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + echo CXX=clang++ | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + } else { + echo CC=${{ matrix.compiler }} | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + echo CXX=${{ matrix.compiler }} | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + } + + # We add the output directories to the PATH to make sure the tests and + # examples can find the reproc and reproc++ DLL's. + $env:PATH += ";$pwd\build\reproc\lib" + $env:PATH += ";$pwd\build\reproc++\lib" + + # Make all PATH additions made by scoop and ourselves global. + echo "PATH=$env:PATH" | Add-Content -Path $env:GITHUB_ENV -Encoding utf8 + + if ("${{ matrix.compiler }}".endswith("cl")) { + & .github\workflows\vsenv.ps1 -arch x64 -hostArch x64 + } + + # We build reproc as a shared library to verify all the necessary symbols + # are exported. + + # YAML folded multiline strings ('>') require the same indentation for all + # lines in order to turn newlines into spaces. + + - name: Configure + run: > + cmake + -B build + -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=ON + -DREPROC++=ON + -DREPROC_TEST=ON + -DREPROC_EXAMPLES=ON + -DREPROC_WARNINGS=ON + -DREPROC_WARNINGS_AS_ERRORS=ON + -DREPROC_TIDY=ON + -DREPROC_SANITIZERS=ON + + - name: Build + run: cmake --build build + + - name: Test + run: cmake --build build --target test + env: + CTEST_OUTPUT_ON_FAILURE: ON diff --git a/extern/reproc/.github/workflows/vsenv.ps1 b/extern/reproc/.github/workflows/vsenv.ps1 new file mode 100644 index 000000000..35b90f14d --- /dev/null +++ b/extern/reproc/.github/workflows/vsenv.ps1 @@ -0,0 +1,15 @@ +param ( + [string]$arch = "x64", + [string]$hostArch = "x64" + ) + +$vswherePath = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" +$vsInstallationPath = & "$vswherePath" -latest -products * -property installationPath +$vsDevCmdPath = "`"$vsInstallationPath\Common7\Tools\vsdevcmd.bat`"" +$command = "$vsDevCmdPath -no_logo -arch=$arch -host_arch=$hostArch" + +# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt +& "${env:COMSPEC}" /s /c "$command && set" | ForEach-Object { + $name, $value = $_ -split '=', 2 + Add-Content -Path $env:GITHUB_ENV -Encoding utf8 "$name=$value" +} diff --git a/extern/reproc/CHANGELOG.md b/extern/reproc/CHANGELOG.md new file mode 100644 index 000000000..a4821f625 --- /dev/null +++ b/extern/reproc/CHANGELOG.md @@ -0,0 +1,1076 @@ +# Changelog + +## 14.2.4 + +- Bugfix: Fix a memory leak in `reproc_start()` on Windows (thanks @AokiYuune). +- Bugfix: Fix a memory leak in reproc++ `array` class move constructor. +- Allow passing zero-sized array's to reproc's `input` option (thanks @lightray22). + +## 14.2.3 + +- Bugfix: Fix sign of EWOULDBLOCK error returned from `reproc_read`. + +## 14.2.2 + +- Bugfix: Disallow using `fork` option when using `reproc_run`. + +## 14.2.1 + +- Bugfix: `reproc_run` now handles forked child processes correctly. +- Bugfix: Sinks of different types can now be passed to `reproc::drain`. +- Bugfix: Processes on Windows returning negative exit codes don't cause asserts + anymore. +- Bugfix: Dependency on librt on POSIX (except osx) systems is now explicit in + CMake. +- Bugfix: Added missing stdout redirect option to reproc++. + +## 14.2.0 + +- Added `reproc_pid`/`process::pid` to get the pid of the process +- Fixed compilation error when including reproc/drain.h in C++ code +- Added missing extern "C" block to reproc/run.h + +## 14.1.0 + +- `reproc_run`/`reproc::run` now return the exit code of + `reproc_stop`/`process::stop` even if the deadline is exceeded. +- A bug where deadlines wouldn't work was fixed. + +## 14.0.0 + +- Renamed `environment` to `env`. +- Added configurable behavior to `env` option. Extra environment variables are + now added via `env.extra`. Extra environment variables now extend the parent + environment by default instead of replacing it. + +## 13.0.1 + +- Bugfix: Reset the `events` parameter of every event source if a deadline + expires when calling `reproc_poll`. +- Bugfix: Return 1 from `reproc_poll` if a deadline expires. +- Bugfix: Don't block in `reproc_read` on Windows if the `nonblocking` option + was specified. + +## 13.0.0 + +- Allow passing empty event sources to `reproc_poll`. + + If the `process` member of a `reproc_event_source` object is `NULL`, + `reproc_poll` ignores the event source. + +- Return zero from `reproc_poll` if the given timeout expires instead of + `REPROC_ETIMEDOUT`. + + In reproc, we follow the general pattern that we don't modify output arguments + if an error occurs. However, when `reproc_poll` errors, we still want to set + all events of the given event sources to zero. To signal that we're modifying + the output arguments if the timeout expires, we return zero instead of + `REPROC_ETIMEDOUT`. This is also more consistent with `poll` and `WSAPoll` + which have the same behaviour. + +- If one or more events occur, return the number of processes with events from + `reproc_poll`. + +## 12.0.0 + +### reproc + +- Put pipes in blocking mode by default. + + This allows using `reproc_read` and `reproc_write` directly without having to + figure out `reproc_poll`. + +- Add `nonblocking` option. + + Allows putting pipes back in nonblocking mode if needed. + +- Child process stderr streams are now redirected to the parent stderr stream by + default. + + Because pipes are blocking again by default, there's a (small) chance of + deadlocks if we redirect both stdout and stderr to pipes. Redirecting stderr + to the parent by default avoids that issue. + + The other (bigger) issue is that if we redirect stderr to a pipe, there's a + good chance users might forget to read from it and discard valuable error + output from the child process. + + By redirecting to the parent stderr by default, it's immediately noticeable + when a child process is not behaving according to expectations as its error + output will appear directly on the parent process stderr stream. Users can + then still decide to explicitly discard the output from the stderr stream if + needed. + +- Turn `timeout` option into an argument for `reproc_poll`. + + While deadlines can differ per process, the timeout is very likely to always + be the same so we make it an argument to the only function that uses it, + namely `reproc_poll`. + +- In `reproc_drain`, call `sink` once with an empty buffer when a stream is + closed. + + This allows sinks to handle stream closure if needed. + +- Change sinks to return `int` instead of `bool`. If a `sink` returns a non-zero + value, `reproc_drain` exits immediately with the same value. + + This change allows sinks to return their own errors without having to store + extra state. + +- `reproc_sink_string` now returns `REPROC_ENOMEM` from `reproc_drain` if a + memory allocation fails and no longer frees any output that was read + previously. + + This allows the user to still do something with the remaining output even if a + memory allocation failed. On the flipside, it is now required to always call + `reproc_free` after calling `reproc_drain` with a sink string, even if it + fails. + +- Renamed sink.h to drain.h. + + Reflect that sink.h contains `reproc_drain` by renaming it to drain.h. + +- Add `REPROC_REDIRECT_PATH` and shorthand `path` options. + +### reproc++ + +- Equivalent changes as those done for reproc. + +- Remove use of reprocxx. + + Meson gained support for CMake subprojects containing targets with special + characters so we rename directories and CMake targets back to reproc++. + +## 11.0.0 + +### General + +- Compilation now happens with compiler extensions disabled (`-std=c99` and + `-std=c++11`). + +### reproc + +- Add `inherit` and `discard` options as shorthands to set all members of the + `redirect` options to `REPROC_REDIRECT_INHERIT` and `REPROC_REDIRECT_DISCARD` + respectively. + +- Add `reproc_run` and `reproc_run_ex` which allows running a process using only + a single function. + + Running a simple process with reproc required calling `reproc_new`, + `reproc_start`, `reproc_wait` and optionally `reproc_drain` with all the + associated error handling. `reproc_run` encapsulates all this boilerplate. + +- Add `input` option that writes the given to the child process stdin pipe + before starting the process. + + This allows passing input to the process when using `reproc_run`. + +- Add `deadline` option that specifies a point in time beyond which + `reproc_poll` will return `REPROC_ETIMEDOUT`. + +- Add `REPROC_DEADLINE` that makes `reproc_wait` wait until the deadline + specified in the `deadline` option has expired. + + By default, if the `deadline` option is set, `reproc_destroy` waits until the + deadline expires before sending a `SIGTERM` signal to the child process. + +- Add (POSIX only) `fork` option that makes `reproc_start` a safe alternative to + `fork` with support for all of reproc's other features. + +- Return the amount of bytes written from `reproc_write` and stop handling + partial writes. + + Now that reproc uses nonblocking pipes, it doesn't make sense to handle + partial writes anymore. The `input` option can be used as an alternative. + `reproc_start` keeps writing until all data from `input` is written to the + child process stdin pipe or until a call to `reproc_write` returns an error. + +- Add `reproc_poll` to query child process events of one or more child + processes. + + We now support polling multiple child processes for events. `reproc_poll` + mimicks the POSIX `poll` function but instead of pollfds, it takes a list of + event sources which consist out of a process, the events we're interested in + and an output field which is filled in by `reproc_poll` that contains the + events that occurred for that child process. + +- Stop reading from both stdout and stderr in `reproc_read`. + + Because we now have `reproc_poll`, `reproc_read` was simplified to again read + from the given stream which is passed again as an argument. To avoid + deadlocks, call `reproc_poll` to figure out the first stream that has data + available to read. + +- Support polling for process exit events. + + By adding `REPROC_EVENT_EXIT` to the list of interested events, we can poll + for child process exit events. + +- Add a dependency on Winsock2 on Windows. + + To implement `reproc_poll`, we redirect to sockets on Windows which allows us + to use `WSAPoll` which is required to implement `reproc_poll`. Using sockets + on Windows requires linking with the `ws2_32` library. This dependency is + automatically handled by CMake and pkg-config. + +- Move `in`, `out` and `err` options out of `stdio` directly into `redirect`. + + This reduces the amount of boilerplate when redirecting streams. + +- Rename `REPROC_REDIRECT_INHERIT` to `REPROC_REDIRECT_PARENT`. + + `REPROC_REDIRECT_PARENT` more clearly indicates that we're redirecting to the + parent's corresponding standard stream. + +- Add support for redirecting to operating system handles. + + This allows redirecting to operating system-specific handles. On POSIX + systems, this feature expects file descriptors. On Windows, it expects + `HANDLE`s or `SOCKET`s. + +- Add support for redirecting to `FILE *`s. + + This gives users a cross-platform way to redirect standard streams to files. + +- Add support for redirecting stderr to stdout. + + For high-traffic scenarios, it doesn't make sense to allocate a separate pipe + for stderr if its output is only going to be combined with the output of + stdout. By using `REPROC_REDIRECT_STDOUT` for stderr, its output is written + directly to stdout by the child process. + +- Turn `redirect`'s `in`, `out` and `err` options into instances of the new + `reproc_redirecŧ` struct. + + An enum didn't cut it anymore for the new file and handle redirect options + since those require extra fields to allow specifying which file or handle to + redirect to. + +- Add `redirect.file` option as a shorthand for redirecting stdout and stderr to + the same file. + +### reproc++ + +- reproc++ includes mostly the same changes done to reproc so we only document + the differences. + +- Add `fork` method instead of `fork` option. + + Adding a `fork` option would have required changing `start` to return + `std::pair` to allow determining whether we're in the + parent or the child process after a fork. However, the bool return value would + only be valid when the fork option was enabled. Thus, this approach would + unnecessarily complicate all other use cases of `start` that don't require + `fork`. To solve the issue, we made `fork` a separate method instead. + +## 10.0.3 + +### reproc + +- Fixed issue where `reproc_wait` would assert when invoked with a timeout of + zero on POSIX. + +- Fixed issue where `reproc_wait` would not return `REPROC_ETIMEDOUT` when + invoked with a timeout of zero on POSIX. + +## 10.0.2 + +- Update CMake project version. + +## 10.0.1 + +### reproc + +- Pass `timeout` once via `reproc_options` instead of passing it via + `reproc_read`, `reproc_write` and `reproc_drain`. + +### reproc++ + +- Pass `timeout` once via `reproc::options` instead of passing it via + `process::read`, `process::write` and `reproc::drain`. + +## 10.0.0 + +### reproc + +- Remove `reproc_parse`. + + Instead of checking for `REPROC_EPIPE` (previously + `REPROC_ERROR_STREAM_CLOSED`), simply check if the given parser has a full + message available. If it doesn't, the output streams closed unexpectedly. + +- Remove `reproc_running` and `reproc_exit_status`. + + When calling `reproc_running`, it would wait with a zero timeout if the + process was still running and check if the wait timed out. However, a call to + wait can fail for other reasons as well which were all ignored by + `reproc_running`. Instead of `reproc_running`, A call to `reproc_wait` with a + timeout of zero should be used to check if a process is still running. + `reproc_wait` now also returns the exit status if the process exits or has + already exited which removes the need for `reproc_exit_status`. + +- Read from both stdout and stderr in `reproc_read` to avoid deadlocks and + indicate which stream `reproc_read` was read from. + + Previously, users would indicate the stream they wanted to read from when + calling `reproc_read`. However, this lead to issues with programs that write + to both stdout and stderr as a user wouldn't know whether stdout or stderr + would have output available to read. Reading from only the stdout stream + didn't work as the parent could be blocked on reading from stdout while the + child was simultaneously blocked on writing to stderr leading to a deadlock. + To get around this, users had to start up a separate thread to read from both + stdout and stderr at the same time which was a lot of extra work just to get + the output of external programs that write to both stdout and stderr. Now, + reproc takes care of avoiding the deadlock by checking which of stdout/stderr + can be read from, doing the actual read and indicating to the user which + stream was read from. + + Practically, instead of passing `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR` to + `reproc_read`, you now pass a pointer to a `REPROC_STREAM` variable instead + which `reproc_read` will set to `REPROC_STREAM_OUT` or `REPROC_STREAM_ERR` + depending on which stream it read from. + + If both streams have been closed by the child process, `reproc_read` returns + `REPROC_EPIPE`. + + Because of the changes to `reproc_read`, `reproc_drain` now also reads from + both stdout and stderr and indicates the stream that was read from to the + given sink function via an extra argument passed to the sink. + +- Read the output of both stdout and stderr into a single contiguous + null-terminated string in `reproc_sink_string`. + +- Remove the `bytes_written` parameter of `reproc_write`. + + `reproc_write` now always writes `size` bytes to the standard input of the + child process. Partial writes do not have to be handled by users anymore and + are instead handled by reproc internally. + +- Define `_GNU_SOURCE` and `_WIN32_WINNT` only in the implementation files that + need them. + + This helps keep track of where we're using functionality that requires extra + definitions and makes building reproc in all kinds of build systems simpler as + the compiler invocations to build reproc get smaller as a result. + +- Change the error handling in the public API to return negative `errno` (POSIX) + or `GetLastError` (Windows) style values. `REPROC_ERROR` is replaced by extern + constants that are assigned the correct error value based on the platform + reproc is built for. Instead of returning `REPROC_ERROR`, most functions in + reproc's API now return `int` when they can fail. Because system errors are + now returned directly, there's no need anymore for `REPROC_ERROR` and + `reproc_error_system` and they has been removed. + + Error handling before 10.0.0: + + ```c + REPROC_ERROR error = reproc_start(...); + if (error) { + goto finish; + } + + finish: + if (error) { + fprintf(stderr, "%s", reproc_strerror(error)); + } + ``` + + Error handling from 10.0.0 onwards: + + ```c + int r = reproc_start(...); + if (r < 0) { + goto finish; + } + + finish: + if (r < 0) { + fprintf(stderr, "%s", reproc_strerror(r)); + } + ``` + +- Hide the internals of `reproc_t`. + + Instances of `reproc_t` are now allocated on the heap by calling `reproc_new`. + `reproc_destroy` releases the memory allocated by `reproc_new`. + +- Take optional arguments via the `reproc_options` struct in `reproc_start`. + + When using designated initializers, calls to `reproc_start` are now much more + readable than before. Using a struct also makes it much easier to set all + options to their default values (`reproc_options options = { 0 };`). Finally, + we can add more options in further releases without requiring existing users + to change their code. + +- Support redirecting the child process standard streams to `/dev/null` (POSIX) + or `NUL` (Windows) in `reproc_start` via the `redirect` field in + `reproc_options`. + + This is especially useful when you're not interested in the output of a child + process as redirecting to `/dev/null` doesn't require regularly flushing the + output pipes of the process to prevent deadlocks as is the case when + redirecting to pipes. + +- Support redirecting the child process standard streams to the parent process + standard streams in `reproc_starŧ` via the `redirect` field in + `reproc_options`. + + This is useful when you want to interleave child process output with the + parent process output. + +- Modify `reproc_start` and `reproc_destroy` to work like the reproc++ `process` + class constructor and destructor. + + The `stop_actions` field in `reproc_options` can be used to define up to three + stop actions that are executed when `reproc_destroy` is called if the child + process is still running. If no explicit stop actions are given, + `reproc_destroy` defaults to waiting indefinitely for the child process to + exit. + +- Return the amount of bytes read from `reproc_read` if it succeeds. + + This is made possible by the new error handling scheme. Because errors are all + negative values, we can use the positive range of an `int` as the normal + return value if no errors occur. + +- Return the exit status from `reproc_wait` and `reproc_stop` if they succeed. + + Same reasoning as above. If the child process has already exited, + `reproc_wait` and `reproc_stop` simply returns the exit status again. + +- Do nothing when `NULL` is passed to `reproc_destroy` and always return `NULL` + from `reproc_destroy`. + + This allows `reproc_destroy` to be safely called on the same instance multiple + times when assigning the result of `reproc_destroy` to the same instance + (`process = reproc_destroy(process)`). + +- Take stop actions via the `reproc_stop_actions` struct in `reproc_stop`. + + This makes it easier to store stop action configurations both in and outside + of reproc. + +- Add 256 to signal exit codes returned by `reproc_wait` and `reproc_stop`. + + This prevents conflicts with normal exit codes. + +- Add `REPROC_SIGTERM` and `REPROC_SIGKILL` constants to match against signal + exit codes. + + These also work on Windows and correspond to the exit codes returned by + sending the `CTRL-BREAK` signal and calling `TerminateProcess` respectively. + +- Rename `REPROC_CLEANUP` to `REPROC_STOP`. + + Naming the enum after the function it is passed to (`reproc_stop`) is simpler + than using a different name. + +- Rewrite tests in C using CTest and `assert` and remove doctest. + + Doctest is a great library but we don't really lose anything major by + replacing it with CTest and asserts. On the other hand, we lose a dependency, + don't need to download stuff from CMake anymore and tests compile + significantly faster. + + Tests are now executed by running `cmake --build build --target test`. + +- Return `REPROC_EINVAL` from public API functions when passed invalid + arguments. + +- Make `reproc_strerror` thread-safe. + +- Move `reproc_drain` to sink.h. + +- Make `reproc_drain` take a separate sink for each output stream. Sinks are now + passed via the `reproc_sink` type. + + Using separate sinks for both output streams allows for a lot more + flexibility. To use a single sink for both output streams, simply pass the + same sink to both the `out` and `err` arguments of `reproc_drain`. + +- Turn `reproc_sink_string` and `reproc_sink_discard` into functions that return + sinks and hide the actual functions in sink.c. + +- Add `reproc_free` to sink.h which must be used to free memory allocated by + `reproc_sink_string`. + + This avoids issues with allocating across module (DLL) boundaries on Windows. + +- Support passing timeouts to `reproc_read`, `reproc_write` and `reproc_drain`. + + Pass `REPROC_INFINITE` as the timeout to retain the old behaviour. + +- Use `int` to represent timeout values. + +- Renamed `stop_actions` field of `reproc_options` to `stop`. + +### reproc++ + +- Remove `process::parse`, `process::exit_status` and `process::running`. + + Consequence of the equivalents in reproc being removed. + +- Take separate `out` and `err` arguments in the `sink::string` and + `sink::ostream` constructors that receive output from the stdout and stderr + streams of the child process respectively. + + To combine the output from the stdout and stderr streams, simply pass the same + `string` or `ostream` to both the `out` and `err` arguments. + +- Modify `process::read` to return a tuple of the stream read from, the amount + of bytes read and an error code. The stream read from and amount of bytes read + are only valid if `process::read` succeeds. + + `std::tie` can be used pre-C++17 to assign the tuple's contents to separate + variables. + +- Modify `process::wait` and `process::stop` to return a pair of exit status and + error code. The exit status is only valid if `process::wait` or + `process::stop` succeeds. + +- Alias `reproc::error` to `std::errc`. + + As OS errors are now used everywhere, we can simply use `std::errc` for all + error handling instead of defining our own error code. + +- Add `signal::terminate` and `signal::kill` constants. + + These are aliases for `REPROC_SIGTERM` and `REPROC_SIGKILL` respectively. + +- Inline all sink implementations in sink.hpp. + +- Add `sink::thread_safe::string` which is a thread-safe version of + `sink::string`. + +- Move `process::drain` out of the `process` class and move it to sink.hpp. + + `process.drain(...)` becomes `reproc::drain(process, ...)`. + +- Make `reproc::drain` take a separate sink for each output stream. + + Same reasoning as `reproc_drain`. + +- Modify all included sinks to support the new `reproc::drain` behaviour. + +- Support passing timeouts to `process::read`, `process::write` and + `reproc::drain`. + + They still default to waiting indefinitely which matches their old behaviour. + +- Renamed `stop_actions` field of `reproc::options` to `stop`. + +### CMake + +- Drop required CMake version to CMake 3.12. +- Add CMake 3.16 as a supported CMake version. +- Build reproc++ with `-pthread` when `REPROC_MULTITHREADED` is enabled. + + See https://github.com/DaanDeMeyer/reproc/issues/24 for more information. + +- Add `REPROC_WARNINGS` option (default: `OFF`) to build with compiler warnings. +- Add `REPROC_DEVELOP` option (default: `OFF`) which enables a lot of options to + simplify developing reproc. + + By default, most of reproc's CMake options are disabled to make including + reproc in other projects as simple as possible. However, when working on + reproc, we usually wants most options enabled instead. To make enabling all + options simpler, `REPROC_DEVELOP` was added from which most other options take + their default value. As a result, enabling `REPROC_DEVELOP` enables all + options related to developing reproc. Additionally, `REPROC_DEVELOP` takes its + initial value from an environment variable of the same name so it can be set + once and always take effect whenever running CMake on reproc's source tree. + +- Add `REPROC_OBJECT_LIBRARIES` option to build CMake object libraries. + + In CMake, linking a library against a static library doesn't actually copy the + object files from the static library into the library. Instead, both static + libraries have to be installed and depended on by the final executable. By + using CMake object libraries, the object files are copied into the depending + static library and no extra artifacts are produced. + +- Enable `REPROC_INSTALL` by default unless `REPROC_OBJECT_LIBRARIES` is + enabled. + + As `REPROC_OBJECT_LIBRARIES` can now be used to depend on reproc without + generating extra artifacts, we assume that users not using + `REPROC_OBJECT_LIBRARIES` will want to install the produced artifacts. + +- Rename reproc++ to reprocxx inside the CMake build files. + + This was done to allow using reproc as a Meson subproject. Meson doesn't + accept the '+' character in target names so we use 'x' instead. + +- Modify the export headers so that the only extra define necessary is + `REPROC_SHARED` when using reproc as a shared library on Windows. + + Naturally, this define is added as a CMake usage requirement and doesn't have + to be worried about when using reproc via `add_subdirectory` or + `find_package`. + +## 9.0.0 + +### General + +- Drop support for Windows XP. + +- Add support for custom environments. + + `reproc_start` and `process::start` now take an extra `environment` parameter + that allows specifying custom environments. + + **IMPORTANT**: The `environment` parameter was inserted before the + `working_directory` parameter so make sure to update existing usages of + `reproc_start` and `process::start` so that the `environment` and + `working_directory` arguments are specified in the correct order. + + To keep the previous behaviour, pass `nullptr` as the environment to + `reproc_start`/`process::start` or use the `process::start` overload without + the `environment` parameter. + +- Remove `argc` parameter from `reproc_start` and `process::start`. + + We can trivially calculate `argc` internally in reproc since `argv` is + required to end with a `NULL` value. + +- Improve implementation of `reproc_wait` with a timeout on POSIX systems. + + Instead of spawning a new process to implement the timeout, we now use + `sigtimedwait` on Linux and `kqueue` on macOS to wait on `SIGCHLD` signals and + check if the process we're waiting on has exited after each received `SIGCHLD` + signal. + +- Remove `vfork` usage. + + Clang analyzer was indicating a host of errors in our usage of `vfork`. We + also discovered tests were behaving differently on macOS depending on whether + `vfork` was enabled or disabled. As we do not have the expertise to verify if + `vfork` is working correctly, we opt to remove it. + +- Ensure passing a custom working directory and a relative executable path + behaves consistently on all supported platforms. + + Previously, calling `reproc_start` with a relative executable path combined + with a custom working directory would behave differently depending on which + platform the code was executed on. On POSIX systems, the relative executable + path would be resolved relative to the custom working directory. On Windows, + the relative executable path would be resolved relative to the parent process + working directory. Now, relative executable paths are always resolved relative + to the parent process working directory. + +- Reimplement `reproc_drain`/`process::drain` in terms of + `reproc_parse`/`process::parse`. + + Like `reproc_parse` and `process::parse`, `reproc_drain` and `process::drain` + are now guaranteed to always be called once with an empty buffer before + reading any actual data. + + We now also guarantee that the initial empty buffer is not `NULL` or `nullptr` + so the received data and size can always be safely passed to `memcpy`. + +- Add MinGW support. + + MinGW CI builds were also added to prevent regressions in MinGW support. + +### reproc + +- Update `reproc_strerror` to return the actual system error string of the error + code returned by `reproc_system_error` instead of "system error" when passed + `REPROC_ERROR_SYSTEM` as argument. + + This should make debugging reproc errors a lot easier. + +- Add `reproc_sink_string` in `sink.h`, a sink that stores all process output in + a single null-terminated C string. + +- Add `reproc_sink_discard` in `sink.h`, a sink that discards all process + output. + +### reproc++ + +- Move sinks into `sink` namespace and remove `_sink` suffix from all sinks. + +- Add `discard` sink that discards all output read from a stream. + + This is useful when a child process produces a lot of output that we're not + interested in and cannot handle the output stream being closed or full. When + this is the case, simply start a thread that drains the stream with a + `discard` sink. + +- Update `process::start` to work with any kind of string type. + + Every string type that implements a `size` method and the index operator can + now be passed in a container to `process::start`. `working_directory` now + takes a `const char *` instead of a `std::string *`. + +- Fix compilation error when using `process::parse`. + +## 8.0.1 + +- Correctly escape arguments on Windows. + + See [#18](https://github.com/DaanDeMeyer/reproc/issues/18) for more + information. + +## 8.0.0 + +- Change `reproc_parse` and `reproc_drain` argument order. + + `context` is now the last argument instead of the first. + +- Use `uint8_t *` as buffer type instead of `char *` or `void *` + + `uint8_t *` more clearly indicates reproc is working with buffers of bytes + than `char *` and `void *`. We choose `uint8_t *` over `char *` to avoid + errors caused by passing data read by reproc directly to functions that expect + null-terminated strings (data read by reproc is not null-terminated). + +## 7.0.0 + +### General + +- Rework error handling. + + Trying to abstract platform-specific errors in `REPROC_ERROR` and + `reproc::errc` turned out to be harder than expected. On POSIX it remains very + hard to figure out which errors actually have a chance of happening and + matching `reproc::errc` values to `std::errc` values is also ambiguous and + prone to errors. On Windows, there's hardly any documentation on which system + errors functions can return so 90% of the time we were just returning + `REPROC_UNKNOWN_ERROR`. Furthermore, many operating system errors will be + fatal for most users and we suspect they'll all be handled similarly (stopping + the application or retrying). + + As a result, in this release we stop trying to abstract system errors in + reproc. All system errors in `REPROC_ERROR` were replaced by a single value + (`REPROC_ERROR_SYSTEM`). `reproc::errc` was renamed to `reproc::error` and + turned into an error code instead of an error condition and only contains the + reproc-specific errors. + + reproc users can still retrieve the specific system error using + `reproc_system_error`. + + reproc++ users can still match against specific system errors using the + `std::errc` error condition enum + () or print a string + presentation of the error using the `message` method of `std::error_code`. + + All values from `REPROC_ERROR` are now prefixed with `REPROC_ERROR` instead of + `REPROC` which helps reduce clutter in code completion. + +- Azure Pipelines CI now includes Visual Studio 2019. + +- Various smaller improvements and fixes. + +### CMake + +- Introduce `REPROC_MULTITHREADED` to configure whether reproc should link + against pthreads. + + By default, `REPROC_MULTITHREADED` is enabled to prevent accidental undefined + behaviour caused by forgetting to enable `REPROC_MULTITHREADED`. Advanced + users might want to disable `REPROC_MULTITHREADED` when they know for certain + their code won't use more than a single thread. + +- doctest is now downloaded at configure time instead of being vendored inside + the reproc repository. + + doctest is only downloaded if `REPROC_TEST` is enabled. + +## 6.0.0 + +### General + +- Added Azure Pipelines CI. + + Azure Pipelines provides 10 parallel jobs which is more than Travis and + Appveyor combined. If it turns out to be reliable Appveyor and Travis will + likely be dropped in the future. For now, all three are enabled. + +- Code cleanup and refactoring. + +### CMake + +- Renamed `REPROC_TESTS` to `REPROC_TEST`. +- Renamed test executable from `tests` to `test`. + +### reproc + +- Renamed `reproc_type` to `reproc_t`. + + We chose `reproc_type` initially because `_t` belongs to POSIX but we switch + to using `_t` because `reproc` is a sufficiently unique name that we don't + have to worry about naming conflicts. + +- reproc now keeps track of whether a process has exited and its exit status. + + Keeping track of whether the child process has exited allows us to remove the + restriction that `reproc_wait`, `reproc_terminate`, `reproc_kill` and + `reproc_stop` cannot be called again on the same process after completing + successfully once. Now, if the process has already exited, these methods don't + do anything and return `REPROC_SUCCESS`. + +- Added `reproc_running` to allow checking whether a child process is still + running. + +- Added `reproc_exit_status` to allow querying the exit status of a process + after it has exited. + +- `reproc_wait` and `reproc_stop` lost their `exit_status` output parameter. + + Use `reproc_exit_status` instead to retrieve the exit status. + +### reproc++ + +- Added `process::running` and `process::exit_status`. + + These delegate to `reproc_running` and `reproc_exit_status` respectively. + +- `process::wait` and `process::stop` lost their `exit_status` output parameter. + + Use `process::exit_status` instead. + +## 5.0.1 + +### reproc++ + +- Fixed compilation error caused by defining `reproc::process`'s move assignment + operator as default in the header which is not allowed when a + `std::unique_ptr` member of an incomplete type is present. + +## 5.0.0 + +### General + +- Added and rewrote implementation documentation. +- General refactoring and simplification of the source code. + +### CMake + +- Raised minimum CMake version to 3.13. + + Tests are now added to a single target `reproc-tests` in each subdirectory + included with `add_subdirectory`. Dependencies required to run the added tests + are added to `reproc-tests` with `target_link_libraries`. Before CMake 3.13, + `target_link_libraries` could not modify targets created outside of the + current directory which is why CMake 3.13 is needed. + +- `REPROC_CI` was renamed to `REPROC_WARNINGS_AS_ERRORS`. + + This is a side effect of upgrading cddm. The variable was renamed in cddm to + more clearly indicate its purpose. + +- Removed namespace from reproc's targets. + + To link against reproc or reproc++, you now have to link against the target + without a namespace prefix: + + ```cmake + find_package(reproc) # or add_subdirectory(external/reproc) + target_link_libraries(myapp PRIVATE reproc) + + find_package(reproc++) # or add_subdirectory(external/reproc++) + target_link_libraries(myapp PRIVATE reproc++) + ``` + + This change was made because of a change in cddm (a collection of CMake + functions to make setting up new projects easier) that removed namespacing and + aliases of library targets in favor of namespacing within the target name + itself. This change was made because the original target can still conflict + with other targets even after adding an alias. This can cause problems when + using generic names for targets inside the library itself. An example + clarifies the problem: + + Imagine reproc added a target for working with processes asynchronously. In + the previous naming scheme, we'd do the following in reproc's CMake build + files: + + ```cmake + add_library(async "") + add_library(reproc::async ALIAS async) + ``` + + However, there's a non-negligible chance that someone using reproc might also + have a target named async which would result in a conflict when using reproc + with `add_subdirectory` since there'd be two targets with the same name. With + the new naming scheme, we'd do the following instead: + + ```cmake + add_library(reproc-async "") + ``` + + This has almost zero chance of conflicting with user's target names. The + advantage is that with this scheme we can use common target names without + conflicting with user's target names which was not the case with the previous + naming scheme. + +### reproc + +- Removed undefined behaviour in Windows implementation caused by casting an int + to an unsigned int. + +- Added a note to `reproc_start` docs about the behaviour of using a executable + path relative to the working directory combined with a custom working + directory for the child process on different platforms. + +- We now retrieve the file descriptor limit in the parent process (using + `sysconf`) instead of in the child process because `sysconf` is not guaranteed + to be async-signal-safe which all functions called in a child process after + forking should be. + +- Fixed compilation issue when `ATTRIBUTE_LIST_FOUND` was undefined (#15). + +### reproc++ + +- Generified `process::start` so it works with any container of `std::string` + satisfying the + [SequenceContainer](https://en.cppreference.com/w/cpp/named_req/SequenceContainer) + interface. + +## 4.0.0 + +### General + +- Internal improvements and documentation fixes. + +### reproc + +- Added `reproc_parse` which mimics reproc++'s `process::parse`. +- Added `reproc_drain` which mimics reproc++'s `process::drain` along with an + example that explains how to use it. + + Because C doesn't support lambda's, both of these functions take a function + pointer and an extra context argument which is passed to the function pointer + each time it is called. The context argument can be used to store any data + needed by the given function pointer. + +### reproc++ + +- Renamed the `process::read` overload which takes a parser to `process::parse`. + + This breaking change was done to keep consistency with reproc where we added + `reproc_parse`. We couldn't add another `reproc_read` since C doesn't support + overloading so we made the decision to rename `process::read` to + `process::parse` instead. + +- Changed `process::drain` sinks to return a boolean instead of `void`. + + Before this change, the only way to stop draining a process was to throw an + exception from the sink. By changing sinks to return `bool`, a sink can tell + `drain` to stop if an error occurs by returning `false`. The error itself can + be stored in the sink if needed. + +## 3.1.3 + +### CMake + +- Update project version in CMakeLists.txt from 3.0.0 to the actual latest + version (3.1.3). + +## 3.1.2 + +### pkg-config + +- Fix pkg-config install prefix. + +## 3.1.0 + +### CMake + +- Added `REPROC_INSTALL_PKGCONFIG` to control whether pkg-config files are + installed or not (default: `ON`). + + The vcpkg package manager has no need for the pkg-config files so we added an + option to disable installing them. + +- Added `REPROC_INSTALL_CMAKECONFIGDIR` and `REPROC_INSTALL_PKGCONFIGDIR` to + control where cmake config files and pkg-config files are installed + respectively (default: `${CMAKE_INSTALL_LIBDIR}/cmake` and + `${CMAKE_INSTALL_LIBDIR}/pkgconfig`). + + reproc already uses the values from `GNUInstallDirs` when generating its + install rules which are cache variables that be overridden by users. However, + `GNUInstallDirs` does not include variables for the installation directories + of CMake config files and pkg-config files. vcpkg requires cmake config files + to be installed to a different directory than the directory reproc used until + now. These options were added to allow vcpkg to control where the config files + are installed to. + +## 3.0.0 + +### General + +- Removed support for Doxygen (and as a result `REPROC_DOCS`). + + All the Doxygen directives made the header docstrings rather hard to read + directly. Doxygen's output was also too complicated for a simple library such + as reproc. Finally, Doxygen doesn't really provide any intuitive support for + documenting a set of libraries. I have an idea for a Doxygen alternative using + libclang and cmark but I'm not sure when I'll be able to implement it. + +### CMake + +- Renamed `REPROCXX` option to `REPROC++`. + + `REPROCXX` was initially chosen because CMake didn't recommend using anything + other than letters and underscores for variable names. However, `REPROC++` + turns out to work without any problems so we use it since it's the expected + name for an option to build reproc++. + +- Stopped modifying the default `CMAKE_INSTALL_PREFIX` on Windows. + + In 2.0.0, when installing to the default `CMAKE_INSTALL_PREFIX`, you would end + up with `C:\Program Files (x86)\reproc` and `C:\Program Files (x86)\reproc++` + when installing reproc. In 3.0.0, the default `CMAKE_INSTALL_PREFIX` isn't + modified anymore and all libraries are installed to `CMAKE_INSTALL_PREFIX` in + exactly the same way as they are on UNIX systems (include and lib + subdirectories directly beneath the installation directory). Sticking to the + defaults makes it easy to include reproc in various package managers such as + vcpkg. + +### reproc + +- `reproc_terminate` and `reproc_kill` don't call `reproc_wait` internally + anymore. `reproc_stop` has been changed to call `reproc_wait` after calling + `reproc_terminate` or `reproc_kill` so it still behaves the same. + + Previously, calling `reproc_terminate` on a list of processes would only call + `reproc_terminate` on the next process after the previous process had exited + or the timeout had expired. This made terminating multiple processes take + longer than required. By removing the `reproc_wait` call from + `reproc_terminate`, users can first call `reproc_terminate` on all processes + before waiting for each of them with `reproc_wait` which makes terminating + multiple processes much faster. + +- Default to using `vfork` instead of `fork` on POSIX systems. + + This change was made to increase `reproc_start`'s performance when the parent + process is using a large amount of memory. In these scenario's, `vfork` can be + a lot faster than `fork`. Care is taken to make sure signal handlers in the + child don't corrupt the state of the parent process. This change induces an + extra constraint in that `set*id` functions cannot be called while a call to + `reproc_start` is in process, but this situation is rare enough that the + tradeoff for better performance seems worth it. + + A dependency on pthreads had to be added in order to safely use `vfork` (we + needed access to `pthread_sigmask`). The CMake and pkg-config files have been + updated to automatically find pthreads so users don't have to find it + themselves. + +- Renamed `reproc_error_to_string` to `reproc_strerror`. + + The C standard library has `strerror` for retrieving a string representation + of an error. By using the same function name (prefixed with reproc) for a + function that does the same for reproc's errors, new users will immediately + know what the function does. + +### reproc++ + +- reproc++ now takes timeouts as `std::chrono::duration` values (more specific + `reproc::milliseconds`) instead of unsigned ints. + + Taking the `reproc::milliseconds` type explains a lot more about the expected + argument than taking an unsigned int. C++14 also added chrono literals which + make constructing `reproc::milliseconds` values a lot more concise + (`reproc::milliseconds(2000)` => `2000ms`). diff --git a/extern/reproc/CMakeLists.txt b/extern/reproc/CMakeLists.txt new file mode 100644 index 000000000..07786f1f7 --- /dev/null +++ b/extern/reproc/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.12...3.21) + +project( + reproc + VERSION 14.2.4 + DESCRIPTION "Cross-platform C99/C++11 process library" + HOMEPAGE_URL "https://github.com/DaanDeMeyer/reproc" + LANGUAGES C +) + +# Common options and functions separated for easier reuse in other projects. +include(cmake/reproc.cmake) + +option(REPROC++ "Build reproc++" ${REPROC_DEVELOP}) +option( + REPROC_MULTITHREADED + "Use `pthread_sigmask` and link against the system's thread library" + ON +) + +if(REPROC_MULTITHREADED) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + set(REPROC_THREAD_LIBRARY ${CMAKE_THREAD_LIBS_INIT}) +endif() + +set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -fpic -march=nehalem") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -march=nehalem") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") + +add_subdirectory(reproc) + +if(REPROC++) + enable_language(CXX) + add_subdirectory(reproc++) +endif() diff --git a/extern/reproc/LICENSE b/extern/reproc/LICENSE new file mode 100644 index 000000000..80ebfacf5 --- /dev/null +++ b/extern/reproc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Daan De Meyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extern/reproc/README.md b/extern/reproc/README.md new file mode 100644 index 000000000..62cc299ee --- /dev/null +++ b/extern/reproc/README.md @@ -0,0 +1,301 @@ +# reproc + +- [What is reproc?](#what-is-reproc) +- [Features](#features) +- [Questions](#questions) +- [Installation](#installation) +- [Dependencies](#dependencies) +- [CMake options](#cmake-options) +- [Documentation](#documentation) +- [Error handling](#error-handling) +- [Multithreading](#multithreading) +- [Gotchas](#gotchas) + +## What is reproc? + +reproc (Redirected Process) is a cross-platform C/C++ library that simplifies +starting, stopping and communicating with external programs. The main use case +is executing command line applications directly from C or C++ code and +retrieving their output. + +reproc consists out of two libraries: reproc and reproc++. reproc is a C99 +library that contains the actual code for working with external programs. +reproc++ depends on reproc and adapts its API to an idiomatic C++11 API. It also +adds a few extras that simplify working with external programs from C++. + +## Features + +- Start any program directly from C or C++ code. +- Communicate with a program via its standard streams. +- Wait for a program to exit or forcefully stop it yourself. When forcefully + stopping a process you can either allow the process to clean up its resources + or stop it immediately. +- The core library (reproc) is written in C99. An optional C++11 wrapper library + (reproc++) with extra features is available for use in C++ applications. +- Multiple installation methods. Either build reproc as part of your project or + use a system installed version of reproc. + +## Usage + +```c +#include + +int main(void) +{ + const char *args[] = { "echo", "Hello, world!", NULL }; + return reproc_run(args, (reproc_options) { 0 }); +} +``` + +## Questions + +If you have any questions after reading the readme and documentation you can +either make an issue or ask questions directly in the reproc +[gitter](https://gitter.im/reproc/Lobby) channel. + +## Installation + +**Note: Building reproc requires CMake 3.12 or higher.** + +There are multiple ways to get reproc into your project. One way is to build +reproc as part of your project using CMake. To do this, we first have to get the +reproc source code into the project. This can be done using any of the following +options: + +- When using CMake 3.11 or later, you can use the CMake `FetchContent` API to + download reproc when running CMake. See + for an + example. +- Another option is to include reproc's repository as a git submodule. + + provides more information. +- A very simple solution is to just include reproc's source code in your + repository. You can download a zip of the source code without the git history + and add it to your repository in a separate directory. + +After including reproc's source code in your project, it can be built from the +root CMakeLists.txt file as follows: + +```cmake +add_subdirectory() # For example: add_subdirectory(external/reproc) +``` + +CMake options can be specified before calling `add_subdirectory`: + +```cmake +set(REPROC++ ON) +add_subdirectory() +``` + +**Note: If the option has already been cached in a previous CMake run, you'll +have to clear CMake's cache to apply the new default value.** + +For more information on configuring reproc's build, see +[CMake options](#cmake-options). + +You can also depend on an installed version of reproc. You can either build and +install reproc yourself or install reproc via a package manager. reproc is +available in the following package repositories: + +- Arch User Repository () +- vcpkg (https://github.com/microsoft/vcpkg/tree/master/ports/reproc) + +If using a package manager is not an option, you can build and install reproc +from source (CMake 3.13+): + +```sh +cmake -B build +cmake --build build +cmake --install build +``` + +Enable the `REPROC_TEST` option and build the `test` target to run the tests +(CMake 3.13+): + +```sh +cmake -B build -DREPROC_TEST=ON +cmake --build build +cmake --build build --target test +``` + +After installing reproc your build system will have to find it. reproc provides +both CMake config files and pkg-config files to simplify finding a reproc +installation using CMake and pkg-config respectively. Note that reproc and +reproc++ are separate libraries and as a result have separate config files as +well. Make sure to search for the one you want to use. + +To find an installed version of reproc using CMake: + +```cmake +find_package(reproc) # Find reproc. +find_package(reproc++) # Find reproc++. +``` + +After building reproc as part of your project or finding a installed version of +reproc, you can link against it from within your CMakeLists.txt file as follows: + +```cmake +target_link_libraries(myapp reproc) # Link against reproc. +target_link_libraries(myapp reproc++) # Link against reproc++. +``` + +From Meson 0.53.2 onwards, reproc can be included as a CMake subproject in Meson +build scripts. See https://mesonbuild.com/CMake-module.html for more +information. + +## Dependencies + +By default, reproc has a dependency on pthreads on POSIX systems (`-pthread`) +and a dependency on Winsock 2.2 on Windows systems (`-lws2_32`). CMake and +pkg-config handle these dependencies automatically. + +## CMake options + +reproc's build can be configured using the following CMake options: + +### User + +- `REPROC++`: Build reproc++ (default: `${REPROC_DEVELOP}`) +- `REPROC_TEST`: Build tests (default: `${REPROC_DEVELOP}`) + + Run the tests by running the `test` binary which can be found in the build + directory after building reproc. + +- `REPROC_EXAMPLES`: Build examples (default: `${REPROC_DEVELOP}`) + + The resulting binaries will be located in the examples folder of each project + subdirectory in the build directory after building reproc. + +### Advanced + +- `REPROC_OBJECT_LIBRARIES`: Build CMake object libraries (default: + `${REPROC_DEVELOP}`) + + This is useful to directly include reproc in another library. When building + reproc as a static or shared library, it has to be installed alongside the + consuming library which makes distributing the consuming library harder. When + using object libraries, reproc's object files are included directly into the + consuming library and no extra installation is necessary. + + **Note: reproc's object libraries will only link correctly from CMake 3.14 + onwards.** + + **Note: This option overrides `BUILD_SHARED_LIBS`.** + +- `REPROC_INSTALL`: Generate installation rules (default: `ON` unless + `REPROC_OBJECT_LIBRARIES` is enabled) +- `REPROC_INSTALL_CMAKECONFIGDIR`: CMake config files installation directory + (default: `${CMAKE_INSTALL_LIBDIR}/cmake`) +- `REPROC_INSTALL_PKGCONFIG`: Install pkg-config files (default: `ON`) +- `REPROC_INSTALL_PKGCONFIGDIR`: pkg-config files installation directory + (default: `${CMAKE_INSTALL_LIBDIR}/pkgconfig`) + +- `REPROC_MULTITHREADED`: Use `pthread_sigmask` and link against the system's + thread library (default: `ON`) + +### Developer + +- `REPROC_DEVELOP`: Configure option default values for development (default: + `OFF` unless the `REPROC_DEVELOP` environment variable is set) +- `REPROC_SANITIZERS`: Build with sanitizers (default: `${REPROC_DEVELOP}`) +- `REPROC_TIDY`: Run clang-tidy when building (default: `${REPROC_DEVELOP}`) +- `REPROC_WARNINGS`: Enable compiler warnings (default: `${REPROC_DEVELOP}`) +- `REPROC_WARNINGS_AS_ERRORS`: Add -Werror or equivalent to the compile flags + and clang-tidy (default: `OFF`) + +## Documentation + +Each function and class is documented extensively in its header file. Examples +can be found in the examples subdirectory of [reproc](reproc/examples) and +[reproc++](reproc++/examples). + +## Error handling + +On failure, Most functions in reproc's API return a negative `errno` (POSIX) or +`GetLastError` (Windows) style error code. For actionable errors, reproc +provides constants (`REPROC_ETIMEDOUT`, `REPROC_EPIPE`, ...) that can be used to +match against the error without having to write platform-specific code. To get a +string representation of an error, pass it to `reproc_strerror`. + +reproc++'s API integrates with the C++ standard library error codes mechanism +(`std::error_code` and `std::error_condition`). Most methods in reproc++'s API +return `std::error_code` values that contain the actual system error that +occurred. You can test against these error codes using values from the +`std::errc` enum. + +See the examples for more information on how to handle errors when using reproc. + +## Multithreading + +Don't call the same operation on the same child process from more than one +thread at the same time. For example: reading and writing to a child process +from different threads is fine but waiting on the same child process from two +different threads at the same time will result in issues. + +## Gotchas + +- (POSIX) It is strongly recommended to not call `waitpid` on pids of processes + started by reproc. + + reproc uses `waitpid` to wait until a process has exited. Unfortunately, + `waitpid` cannot be called twice on the same process. This means that + `reproc_wait` won't work correctly if `waitpid` has already been called on a + child process beforehand outside of reproc. + +- It is strongly recommended to make sure each child process actually exits + using `reproc_wait` or `reproc_stop`. + + On POSIX, a child process that has exited is a zombie process until the parent + process waits on it using `waitpid`. A zombie process takes up resources and + can be seen as a resource leak so it is important to make sure all processes + exit correctly in a timely fashion. + +- It is strongly recommended to try terminating a child process by waiting for + it to exit or by calling `reproc_terminate` before resorting to `reproc_kill`. + + When using `reproc_kill` the child process does not receive a chance to + perform cleanup which could result in resources being leaked. Chief among + these leaks is that the child process will not be able to stop its own child + processes. Always try to let a child process exit normally by calling + `reproc_terminate` before calling `reproc_kill`. `reproc_stop` is a handy + helper function that can be used to perform multiple stop actions in a row + with timeouts inbetween. + +- (POSIX) It is strongly recommended to ignore the `SIGPIPE` signal in the + parent process. + + On POSIX, writing to a closed stdin pipe of a child process will terminate the + parent process with the `SIGPIPE` signal by default. To avoid this, the + `SIGPIPE` signal has to be ignored in the parent process. If the `SIGPIPE` + signal is ignored `reproc_write` will return `REPROC_EPIPE` as expected when + writing to a closed stdin pipe. + +- While `reproc_terminate` allows the child process to perform cleanup it is up + to the child process to correctly clean up after itself. reproc only sends a + termination signal to the child process. The child process itself is + responsible for cleaning up its own child processes and other resources. + +- (Windows) `reproc_kill` is not guaranteed to kill a child process immediately + on Windows. For more information, read the Remarks section in the + documentation of the Windows `TerminateProcess` function that reproc uses to + kill child processes on Windows. + +- Child processes spawned via reproc inherit a single extra file handle which is + used to wait for the child process to exit. If the child process closes this + file handle manually, reproc will wrongly detect the child process has exited. + If this handle is further inherited by other processes that outlive the child + process, reproc will detect the child process is still running even if it has + exited. If data is written to this handle, reproc will also wrongly detect the + child process has exited. + +- (Windows) It's not possible to detect if a child process closes its stdout or + stderr stream before exiting. The parent process will only be notified that a + child process output stream is closed once that child process exits. + +- (Windows) reproc assumes that Windows creates sockets that are usable as file + system objects. More specifically, the default sockets returned by `WSASocket` + should have the `XP1_IFS_HANDLES ` flag set. This might not be the case if + there are external LSP providers installed on a Windows machine. If this is + the case, we recommend removing the software that's providing the extra + service providers since they're deprecated and should not be used anymore (see + https://docs.microsoft.com/en-us/windows/win32/winsock/categorizing-layered-service-providers-and-applications). diff --git a/extern/reproc/cmake/reproc.cmake b/extern/reproc/cmake/reproc.cmake new file mode 100644 index 000000000..d0efafc04 --- /dev/null +++ b/extern/reproc/cmake/reproc.cmake @@ -0,0 +1,408 @@ +include(CheckCCompilerFlag) +include(CMakePackageConfigHelpers) +include(GenerateExportHeader) +include(GNUInstallDirs) + +# Developer options + +option(REPROC_DEVELOP "Enable all developer options" $ENV{REPROC_DEVELOP}) +option(REPROC_TEST "Build tests" ${REPROC_DEVELOP}) +option(REPROC_EXAMPLES "Build examples" ${REPROC_DEVELOP}) +option(REPROC_WARNINGS "Enable compiler warnings" ${REPROC_DEVELOP}) +option(REPROC_TIDY "Run clang-tidy when building" ${REPROC_DEVELOP}) + +option( + REPROC_SANITIZERS + "Build with sanitizers on configurations that support it" + ${REPROC_DEVELOP} +) + +option( + REPROC_WARNINGS_AS_ERRORS + "Add -Werror or equivalent to the compile flags and clang-tidy" +) + +mark_as_advanced( + REPROC_TIDY + REPROC_SANITIZERS + REPROC_WARNINGS_AS_ERRORS +) + +# Installation options + +option(REPROC_OBJECT_LIBRARIES "Build CMake object libraries" ${REPROC_DEVELOP}) + +if(NOT REPROC_OBJECT_LIBRARIES) + set(REPROC_INSTALL_DEFAULT ON) +endif() + +option(REPROC_INSTALL "Generate installation rules" ${REPROC_INSTALL_DEFAULT}) +option(REPROC_INSTALL_PKGCONFIG "Install pkg-config files" ON) + +set( + REPROC_INSTALL_CMAKECONFIGDIR + ${CMAKE_INSTALL_LIBDIR}/cmake + CACHE STRING "CMake config files installation directory" +) + +set( + REPROC_INSTALL_PKGCONFIGDIR + ${CMAKE_INSTALL_LIBDIR}/pkgconfig + CACHE STRING "pkg-config files installation directory" +) + +mark_as_advanced( + REPROC_OBJECT_LIBRARIES + REPROC_INSTALL + REPROC_INSTALL_PKGCONFIG + REPROC_INSTALL_CMAKECONFIGDIR + REPROC_INSTALL_PKGCONFIGDIR +) + +# Testing + +if(REPROC_TEST) + enable_testing() +endif() + +# Build type + +if(REPROC_DEVELOP AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) + set_property( + CACHE CMAKE_BUILD_TYPE + PROPERTY STRINGS Debug Release MinSizeRel RelWithDebInfo + ) +endif() + +# clang-tidy + +if(REPROC_TIDY) + find_program(REPROC_TIDY_PROGRAM clang-tidy) + mark_as_advanced(REPROC_TIDY_PROGRAM) + + if(NOT REPROC_TIDY_PROGRAM) + message(FATAL_ERROR "clang-tidy not found") + endif() + + if(REPROC_WARNINGS_AS_ERRORS) + set(REPROC_TIDY_PROGRAM ${REPROC_TIDY_PROGRAM} -warnings-as-errors=*) + endif() +endif() + +# Functions + +function(reproc_common TARGET LANGUAGE NAME DIRECTORY) + if(LANGUAGE STREQUAL C) + set(STANDARD 99) + target_compile_features(${TARGET} PUBLIC c_std_99) + else() + # clang-tidy uses the MSVC standard library instead of MinGW's standard + # library so we have to use C++14 (because MSVC headers use C++14). + if(MINGW AND REPROC_TIDY) + set(STANDARD 14) + else() + set(STANDARD 11) + endif() + + target_compile_features(${TARGET} PUBLIC cxx_std_11) + endif() + + set_target_properties(${TARGET} PROPERTIES + ${LANGUAGE}_STANDARD ${STANDARD} + ${LANGUAGE}_STANDARD_REQUIRED ON + ${LANGUAGE}_EXTENSIONS OFF + OUTPUT_NAME "${NAME}" + RUNTIME_OUTPUT_DIRECTORY "${DIRECTORY}" + ARCHIVE_OUTPUT_DIRECTORY "${DIRECTORY}" + LIBRARY_OUTPUT_DIRECTORY "${DIRECTORY}" + ) + + if(REPROC_TIDY AND REPROC_TIDY_PROGRAM) + set_property( + TARGET ${TARGET} + # `REPROC_TIDY_PROGRAM` is a list so we surround it with quotes to pass it + # as a single argument. + PROPERTY ${LANGUAGE}_CLANG_TIDY "${REPROC_TIDY_PROGRAM}" + ) + endif() + + # Common development flags (warnings + sanitizers + colors) + + if(REPROC_WARNINGS) + if(MSVC) + check_c_compiler_flag(/permissive- REPROC_HAVE_PERMISSIVE) + + target_compile_options(${TARGET} PRIVATE + /nologo # Silence MSVC compiler version output. + $<$:/WX> # -Werror + $<$:/permissive-> + ) + + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0) + # CMake 3.15 does not add /W3 to the compiler flags by default anymore + # so we add /W4 instead. + target_compile_options(${TARGET} PRIVATE /W4) + endif() + + if(LANGUAGE STREQUAL C) + # Disable MSVC warnings that flag C99 features as non-standard. + target_compile_options(${TARGET} PRIVATE /wd4204 /wd4221) + endif() + else() + target_compile_options(${TARGET} PRIVATE + -Wall + -Wextra + -pedantic + -Wconversion + -Wsign-conversion + $<$:-Werror> + $<$:-pedantic-errors> + ) + + if(LANGUAGE STREQUAL C OR CMAKE_CXX_COMPILER_ID MATCHES Clang) + target_compile_options(${TARGET} PRIVATE -Wmissing-prototypes) + endif() + endif() + + if(WIN32) + target_compile_definitions(${TARGET} PRIVATE _CRT_SECURE_NO_WARNINGS) + endif() + + target_compile_options(${TARGET} PRIVATE + $<$<${LANGUAGE}_COMPILER_ID:GNU>:-fdiagnostics-color> + $<$<${LANGUAGE}_COMPILER_ID:Clang>:-fcolor-diagnostics> + ) + endif() + + if(REPROC_SANITIZERS AND NOT MSVC AND NOT MINGW) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15.0) + set_property( + TARGET ${TARGET} + PROPERTY MSVC_RUNTIME_LIBRARY MultiThreaded + ) + endif() + + target_compile_options(${TARGET} PRIVATE + -fsanitize=address,undefined + -fno-omit-frame-pointer + ) + + target_link_libraries(${TARGET} PRIVATE -fsanitize=address,undefined) + endif() +endfunction() + +function(reproc_library TARGET LANGUAGE) + if(REPROC_OBJECT_LIBRARIES) + add_library(${TARGET} OBJECT) + else() + add_library(${TARGET}) + endif() + + reproc_common(${TARGET} ${LANGUAGE} "" lib) + + if(BUILD_SHARED_LIBS AND NOT REPROC_OBJECT_LIBRARIES) + # Enable -fvisibility=hidden and -fvisibility-inlines-hidden. + set_target_properties(${TARGET} PROPERTIES + ${LANGUAGE}_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN true + ) + + # clang-tidy errors with: unknown argument: '-fno-keep-inline-dllexport' + # when enabling `VISIBILITY_INLINES_HIDDEN` on MinGW so we disable it when + # running clang-tidy on MinGW. + if(MINGW AND REPROC_TIDY) + set_property(TARGET ${TARGET} PROPERTY VISIBILITY_INLINES_HIDDEN false) + endif() + + # Disable CMake's default export definition. + set_property(TARGET ${TARGET} PROPERTY DEFINE_SYMBOL "") + + string(TOUPPER ${TARGET} TARGET_UPPER) + string(REPLACE + X TARGET_SANITIZED ${TARGET_UPPER}) + + target_compile_definitions(${TARGET} PRIVATE ${TARGET_SANITIZED}_BUILDING) + if(WIN32) + target_compile_definitions(${TARGET} PUBLIC ${TARGET_SANITIZED}_SHARED) + endif() + endif() + + # Make sure we follow the popular naming convention for shared libraries on + # UNIX systems. + set_target_properties(${TARGET} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + + # Only use the headers from the repository when building. When installing we + # want to use the install location of the headers (e.g. /usr/include) as the + # include directory instead. + target_include_directories(${TARGET} PUBLIC + $ + ) + + # Adapted from https://codingnest.com/basic-cmake-part-2/. + # Each library is installed separately (with separate config files). + + if(REPROC_INSTALL) + + # Headers + + install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + COMPONENT ${TARGET}-development + ) + + # Library + + install( + TARGETS ${TARGET} + EXPORT ${TARGET}-targets + RUNTIME + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT ${TARGET}-runtime + LIBRARY + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT ${TARGET}-runtime + NAMELINK_COMPONENT ${TARGET}-development + ARCHIVE + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT ${TARGET}-development + INCLUDES + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ) + + if(NOT APPLE) + set_property( + TARGET ${TARGET} + PROPERTY INSTALL_RPATH $ORIGIN + ) + endif() + + # CMake config + + configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake + INSTALL_DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} + ) + + write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake + COMPATIBILITY SameMajorVersion + ) + + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-config-version.cmake + DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} + COMPONENT ${TARGET}-development + ) + + install( + EXPORT ${TARGET}-targets + DESTINATION ${REPROC_INSTALL_CMAKECONFIGDIR}/${TARGET} + COMPONENT ${TARGET}-development + ) + + # pkg-config + + if(REPROC_INSTALL_PKGCONFIG) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc + @ONLY + ) + + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.pc + DESTINATION ${REPROC_INSTALL_PKGCONFIGDIR} + COMPONENT ${TARGET}-development + ) + endif() + endif() +endfunction() + +function(reproc_test TARGET NAME LANGUAGE) + if(NOT REPROC_TEST) + return() + endif() + + if(LANGUAGE STREQUAL C) + set(EXTENSION c) + else() + set(EXTENSION cpp) + endif() + + add_executable(${TARGET}-test-${NAME} test/${NAME}.${EXTENSION}) + + reproc_common(${TARGET}-test-${NAME} ${LANGUAGE} ${NAME} test) + target_link_libraries(${TARGET}-test-${NAME} PRIVATE ${TARGET}) + + if(MINGW) + target_compile_definitions(${TARGET}-test-${NAME} PRIVATE + __USE_MINGW_ANSI_STDIO=1 # Add %zu on Mingw + ) + endif() + + add_test(NAME ${TARGET}-test-${NAME} COMMAND ${TARGET}-test-${NAME}) + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c) + target_compile_definitions(${TARGET}-test-${NAME} PRIVATE + RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources" + ) + + if (NOT TARGET ${TARGET}-resource-${NAME}) + add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c) + reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources) + endif() + + # Make sure the test resource is available when running the test. + add_dependencies(${TARGET}-test-${NAME} ${TARGET}-resource-${NAME}) + endif() +endfunction() + +function(reproc_example TARGET NAME LANGUAGE) + cmake_parse_arguments(OPT "" "" "ARGS;DEPENDS" ${ARGN}) + if(NOT REPROC_EXAMPLES) + return() + endif() + + if(LANGUAGE STREQUAL C) + set(EXTENSION c) + else() + set(EXTENSION cpp) + endif() + + add_executable(${TARGET}-example-${NAME} examples/${NAME}.${EXTENSION}) + + reproc_common(${TARGET}-example-${NAME} ${LANGUAGE} ${NAME} examples) + target_link_libraries(${TARGET}-example-${NAME} PRIVATE ${TARGET} ${OPT_DEPENDS}) + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources/${NAME}.c) + target_compile_definitions(${TARGET}-example-${NAME} PRIVATE + RESOURCE_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/resources" + ) + + if (NOT TARGET ${TARGET}-resource-${NAME}) + add_executable(${TARGET}-resource-${NAME} resources/${NAME}.c) + reproc_common(${TARGET}-resource-${NAME} C ${NAME} resources) + endif() + + # Make sure the example resource is available when running the example. + add_dependencies(${TARGET}-example-${NAME} ${TARGET}-resource-${NAME}) + endif() + + if(REPROC_TEST) + if(NOT DEFINED OPT_ARGS) + set(OPT_ARGS cmake --help) + endif() + + add_test( + NAME ${TARGET}-example-${NAME} + COMMAND ${TARGET}-example-${NAME} ${OPT_ARGS} + ) + endif() +endfunction() diff --git a/extern/reproc/reproc++/CMakeLists.txt b/extern/reproc/reproc++/CMakeLists.txt new file mode 100644 index 000000000..f0c5efa8a --- /dev/null +++ b/extern/reproc/reproc++/CMakeLists.txt @@ -0,0 +1,22 @@ +reproc_library(reproc++ CXX) + +target_link_libraries(reproc++ PRIVATE + reproc + $<$:Threads::Threads> +) + +target_sources( + reproc++ + PRIVATE src/reproc.cpp + # We manually propagate reproc's object files until CMake adds support for + # doing it automatically. + INTERFACE $<$:$> +) + +reproc_example(reproc++ drain CXX) +reproc_example(reproc++ forward CXX) +reproc_example(reproc++ run CXX) + +if(REPROC_MULTITHREADED) + reproc_example(reproc++ background CXX DEPENDS Threads::Threads) +endif() diff --git a/extern/reproc/reproc++/examples/background.cpp b/extern/reproc/reproc++/examples/background.cpp new file mode 100644 index 000000000..560bf130d --- /dev/null +++ b/extern/reproc/reproc++/examples/background.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include + +#include +#include + +static int fail(std::error_code ec) +{ + std::cerr << ec.message(); + return ec.value(); +} + +// The background example reads the output of a child process in a background +// thread and shows how to access the current output in the main thread while +// the background thread is still running. + +// Like the forward example it forwards its arguments to a child process and +// prints the child process output on stdout. +int main(int argc, const char **argv) +{ + if (argc <= 1) { + std::cerr << "No arguments provided. Example usage: " + << "./background cmake --help"; + return EXIT_FAILURE; + } + + reproc::process process; + + reproc::stop_actions stop = { + { reproc::stop::terminate, reproc::milliseconds(5000) }, + { reproc::stop::kill, reproc::milliseconds(2000) }, + {} + }; + + reproc::options options; + options.stop = stop; + + std::error_code ec = process.start(argv + 1, options); + + if (ec == std::errc::no_such_file_or_directory) { + std::cerr << "Program not found. Make sure it's available from the PATH."; + return ec.value(); + } else if (ec) { + return fail(ec); + } + + // We need a mutex along with `output` to prevent the main thread and + // background thread from modifying `output` at the same time (`std::string` + // is not thread safe). + std::string output; + std::mutex mutex; + + auto drain_async = std::async(std::launch::async, [&process, &output, + &mutex]() { + // `sink::thread_safe::string` locks a given mutex before appending to the + // given string, allowing working with the string across multiple threads if + // the mutex is locked in the other threads as well. + reproc::sink::thread_safe::string sink(output, mutex); + return reproc::drain(process, sink, sink); + }); + + // Show new output every 2 seconds. + while (drain_async.wait_for(std::chrono::seconds(2)) != + std::future_status::ready) { + std::lock_guard lock(mutex); + std::cout << output; + // Clear output that's already been flushed to `std::cout`. + output.clear(); + } + + // Flush any remaining output of `process`. + std::cout << output; + + // Check if any errors occurred in the background thread. + ec = drain_async.get(); + if (ec) { + return fail(ec); + } + + int status = 0; + std::tie(status, ec) = process.stop(options.stop); + if (ec) { + return fail(ec); + } + + return status; +} diff --git a/extern/reproc/reproc++/examples/drain.cpp b/extern/reproc/reproc++/examples/drain.cpp new file mode 100644 index 000000000..77837059f --- /dev/null +++ b/extern/reproc/reproc++/examples/drain.cpp @@ -0,0 +1,76 @@ +#include +#include + +#include +#include + +static int fail(std::error_code ec) +{ + std::cerr << ec.message(); + return ec.value(); +} + +// Uses `reproc::drain` to show the output of the given command. +int main(int argc, const char **argv) +{ + if (argc <= 1) { + std::cerr << "No arguments provided. Example usage: " + << "./drain cmake --help"; + return EXIT_FAILURE; + } + + reproc::process process; + + // reproc++ uses error codes to report errors. If exceptions are preferred, + // convert `std::error_code`'s to exceptions using `std::system_error`. + std::error_code ec = process.start(argv + 1); + + // reproc++ converts system errors to `std::error_code`'s of the system + // category. These can be matched against using values from the `std::errc` + // error condition. See https://en.cppreference.com/w/cpp/error/errc for more + // information. + if (ec == std::errc::no_such_file_or_directory) { + std::cerr << "Program not found. Make sure it's available from the PATH."; + return ec.value(); + } else if (ec) { + return fail(ec); + } + + // `reproc::drain` reads from the stdout and stderr streams of `process` until + // both are closed or an error occurs. Providing it with a string sink for a + // specific stream makes it store all output of that stream in the string + // passed to the string sink. Passing the same sink to both the `out` and + // `err` arguments of `reproc::drain` causes the stdout and stderr output to + // get stored in the same string. + std::string output; + reproc::sink::string sink(output); + // By default, reproc only redirects stdout to a pipe and not stderr so we + // pass `reproc::sink::null` as the sink for stderr here. We could also pass + // `sink` but it wouldn't receive any data from stderr. + ec = reproc::drain(process, sink, reproc::sink::null); + if (ec) { + return fail(ec); + } + + std::cout << output << std::flush; + + // It's easy to define your own sinks as well. Take a look at `drain.hpp` in + // the repository to see how `sink::string` and other sinks are implemented. + // The documentation of `reproc::drain` also provides more information on the + // requirements a sink should fulfill. + + // By default, The `process` destructor waits indefinitely for the child + // process to exit to ensure proper cleanup. See the forward example for + // information on how this can be configured. However, when relying on the + // `process` destructor, we cannot check the exit status of the process so it + // usually makes sense to explicitly wait for the process to exit and check + // its exit status. + + int status = 0; + std::tie(status, ec) = process.wait(reproc::infinite); + if (ec) { + return fail(ec); + } + + return status; +} diff --git a/extern/reproc/reproc++/examples/forward.cpp b/extern/reproc/reproc++/examples/forward.cpp new file mode 100644 index 000000000..b0d7feb1c --- /dev/null +++ b/extern/reproc/reproc++/examples/forward.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include + +static int fail(std::error_code ec) +{ + std::cerr << ec.message(); + return ec.value(); +} + +// The forward example forwards the program arguments to a child process and +// prints its output on stdout. +// +// Example: "./forward cmake --help" will print CMake's help output. +// +// This program can be used to verify that manually executing a command and +// executing it using reproc produces the same output. +int main(int argc, const char **argv) +{ + if (argc <= 1) { + std::cerr << "No arguments provided. Example usage: ./forward cmake --help"; + return EXIT_FAILURE; + } + + reproc::process process; + + // Stop actions can be passed to both `process::start` (via `options`) and + // `process::stop`. Stop actions passed to `process::start` are passed to + // `process::stop` in the `process` destructor. This can be used to make sure + // that a child process is always stopped correctly when its corresponding + // `process` instance is destroyed. + // + // Any program can be started with forward so we make sure the child process + // is cleaned up correctly by specifying `reproc::terminate` which sends + // `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waits five seconds. We also + // add the `reproc::kill` flag which sends `SIGKILL` (POSIX) or calls + // `TerminateProcess` (Windows) if the process hasn't exited after five + // seconds and waits two more seconds for the child process to exit. + // + // If the `stop_actions` struct passed to `process::start` is + // default-initialized, the `process` destructor will wait indefinitely for + // the child process to exit. + // + // Note that C++14 has chrono literals which allows + // `reproc::milliseconds(5000)` to be replaced with `5000ms`. + reproc::stop_actions stop = { + { reproc::stop::noop, reproc::milliseconds(0) }, + { reproc::stop::terminate, reproc::milliseconds(5000) }, + { reproc::stop::kill, reproc::milliseconds(2000) } + }; + + reproc::options options; + options.stop = stop; + + // We have the child process inherit the parent's standard streams so the + // child process reads directly from the stdin and writes directly to the + // stdout/stderr of the parent process. + options.redirect.parent = true; + + // Exclude `argv[0]` which is the current program's name. + std::error_code ec = process.start(argv + 1, options); + + if (ec == std::errc::no_such_file_or_directory) { + std::cerr << "Program not found. Make sure it's available from the PATH."; + return ec.value(); + } else if (ec) { + return fail(ec); + } + + // Call `process::stop` manually so we can access the exit status. We add + // `reproc::wait` with a timeout of ten seconds to give the process time to + // exit on its own before sending `SIGTERM`. + options.stop.first = { reproc::stop::wait, reproc::milliseconds(10000) }; + + int status = 0; + std::tie(status, ec) = process.stop(options.stop); + if (ec) { + return fail(ec); + } + + return status; +} diff --git a/extern/reproc/reproc++/examples/run.cpp b/extern/reproc/reproc++/examples/run.cpp new file mode 100644 index 000000000..b88c59523 --- /dev/null +++ b/extern/reproc/reproc++/examples/run.cpp @@ -0,0 +1,24 @@ +#include + +#include + +// Equivalent to reproc's run example but implemented using reproc++. +int main(int argc, const char **argv) +{ + (void) argc; + + int status = -1; + std::error_code ec; + + reproc::options options; + options.redirect.parent = true; + options.deadline = reproc::milliseconds(5000); + + std::tie(status, ec) = reproc::run(argv + 1, options); + + if (ec) { + std::cerr << ec.message() << std::endl; + } + + return ec ? ec.value() : status; +} 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/reproc++/reproc++-config.cmake.in b/extern/reproc/reproc++/reproc++-config.cmake.in new file mode 100644 index 000000000..4406ad310 --- /dev/null +++ b/extern/reproc/reproc++/reproc++-config.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@) + +include(CMakeFindDependencyMacro) +find_dependency(reproc @PROJECT_VERSION@) + +if(REPROC_MULTITHREADED) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_dependency(Threads) +endif() + +include(${CMAKE_CURRENT_LIST_DIR}/@TARGET@-targets.cmake) diff --git a/extern/reproc/reproc++/reproc++.pc.in b/extern/reproc/reproc++/reproc++.pc.in new file mode 100644 index 000000000..d1648c982 --- /dev/null +++ b/extern/reproc/reproc++/reproc++.pc.in @@ -0,0 +1,13 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @TARGET@ +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@TARGET@ +Libs.private: @REPROC_THREAD_LIBRARY@ +Requires.private: reproc = @PROJECT_VERSION@ diff --git a/extern/reproc/reproc++/src/reproc.cpp b/extern/reproc/reproc++/src/reproc.cpp new file mode 100644 index 000000000..e4eed1a73 --- /dev/null +++ b/extern/reproc/reproc++/src/reproc.cpp @@ -0,0 +1,168 @@ +#include + +#include + +namespace reproc { + +namespace signal { + +const int kill = REPROC_SIGKILL; +const int terminate = REPROC_SIGTERM; + +} + +const milliseconds infinite = milliseconds(REPROC_INFINITE); +const milliseconds deadline = milliseconds(REPROC_DEADLINE); + +static std::error_code error_code_from(int r) +{ + if (r >= 0) { + return {}; + } + + if (r == REPROC_EPIPE) { + // https://github.com/microsoft/STL/pull/406 + return { static_cast(std::errc::broken_pipe), + std::generic_category() }; + } + + return { -r, std::system_category() }; +} + +static reproc_stop_actions reproc_stop_actions_from(stop_actions stop) +{ + return { + { static_cast(stop.first.action), stop.first.timeout.count() }, + { static_cast(stop.second.action), + stop.second.timeout.count() }, + { static_cast(stop.third.action), stop.third.timeout.count() } + }; +} + +static reproc_redirect reproc_redirect_from(redirect redirect) +{ + return { static_cast(redirect.type), redirect.handle, + redirect.file, redirect.path }; +} + +static reproc_options reproc_options_from(const options &options, bool fork) +{ + return { + options.working_directory, + { static_cast(options.env.behavior), options.env.extra.data() }, + { reproc_redirect_from(options.redirect.in), + reproc_redirect_from(options.redirect.out), + reproc_redirect_from(options.redirect.err), options.redirect.parent, + options.redirect.discard, options.redirect.file, options.redirect.path }, + reproc_stop_actions_from(options.stop), + options.deadline.count(), + { options.input.data(), options.input.size() }, + options.nonblocking, + fork + }; +} + +process::process() : impl_(reproc_new(), reproc_destroy) {} +process::~process() noexcept = default; + +process::process(process &&other) noexcept = default; +process &process::operator=(process &&other) noexcept = default; + +std::error_code process::start(const arguments &arguments, + const options &options) noexcept +{ + reproc_options reproc_options = reproc_options_from(options, false); + int r = reproc_start(impl_.get(), arguments.data(), reproc_options); + return error_code_from(r); +} + +std::pair process::fork(const options &options) noexcept +{ + reproc_options reproc_options = reproc_options_from(options, true); + int r = reproc_start(impl_.get(), nullptr, reproc_options); + return { r == 0, error_code_from(r) }; +} + +std::pair process::poll(int interests, + milliseconds timeout) +{ + event::source source{ *this, interests, 0 }; + std::error_code ec = ::reproc::poll(&source, 1, timeout); + return { source.events, ec }; +} + +std::pair +process::read(stream stream, uint8_t *buffer, size_t size) noexcept +{ + int r = reproc_read(impl_.get(), static_cast(stream), buffer, + size); + return { r, error_code_from(r) }; +} + +std::pair process::write(const uint8_t *buffer, + size_t size) noexcept +{ + int r = reproc_write(impl_.get(), buffer, size); + return { r, error_code_from(r) }; +} + +std::error_code process::close(stream stream) noexcept +{ + int r = reproc_close(impl_.get(), static_cast(stream)); + return error_code_from(r); +} + +std::pair process::wait(milliseconds timeout) noexcept +{ + int r = reproc_wait(impl_.get(), timeout.count()); + return { r, error_code_from(r) }; +} + +std::error_code process::terminate() noexcept +{ + int r = reproc_terminate(impl_.get()); + return error_code_from(r); +} + +std::error_code process::kill() noexcept +{ + int r = reproc_kill(impl_.get()); + return error_code_from(r); +} + +std::pair process::stop(stop_actions stop) noexcept +{ + int r = reproc_stop(impl_.get(), reproc_stop_actions_from(stop)); + return { r, error_code_from(r) }; +} + +std::pair process::pid() noexcept +{ + int r = reproc_pid(impl_.get()); + return { r, error_code_from(r) }; +} + +std::error_code +poll(event::source *sources, size_t num_sources, milliseconds timeout) +{ + auto *reproc_sources = new reproc_event_source[num_sources]; + + for (size_t i = 0; i < num_sources; i++) { + reproc_sources[i] = { sources[i].process.impl_.get(), sources[i].interests, + 0 }; + } + + int r = reproc_poll(reproc_sources, num_sources, timeout.count()); + + if (r >= 0) { + for (size_t i = 0; i < num_sources; i++) { + sources[i].events = reproc_sources[i].events; + } + } + + delete[] reproc_sources; + + return error_code_from(r); +} + +} diff --git a/extern/reproc/reproc/CMakeLists.txt b/extern/reproc/reproc/CMakeLists.txt new file mode 100644 index 000000000..949cc88c0 --- /dev/null +++ b/extern/reproc/reproc/CMakeLists.txt @@ -0,0 +1,62 @@ +if(WIN32) + set(REPROC_WINSOCK_LIBRARY ws2_32) +elseif(NOT APPLE) + set(REPROC_RT_LIBRARY rt) # clock_gettime +endif() + +reproc_library(reproc C) + +if(REPROC_MULTITHREADED) + target_compile_definitions(reproc PRIVATE REPROC_MULTITHREADED) + target_link_libraries(reproc PRIVATE Threads::Threads) +endif() + +if(WIN32) + set(PLATFORM windows) + target_compile_definitions(reproc PRIVATE WIN32_LEAN_AND_MEAN) + target_link_libraries(reproc PRIVATE ${REPROC_WINSOCK_LIBRARY}) +else() + set(PLATFORM posix) + if(NOT APPLE) + target_link_libraries(reproc PRIVATE ${REPROC_RT_LIBRARY}) + endif() +endif() + +target_sources(reproc PRIVATE + src/clock.${PLATFORM}.c + src/drain.c + src/error.${PLATFORM}.c + src/handle.${PLATFORM}.c + src/init.${PLATFORM}.c + src/options.c + src/pipe.${PLATFORM}.c + src/process.${PLATFORM}.c + src/redirect.${PLATFORM}.c + src/redirect.c + src/reproc.c + src/run.c + src/strv.c + src/utf.${PLATFORM}.c +) + +reproc_test(reproc argv C) +reproc_test(reproc deadline C) +reproc_test(reproc env C) +reproc_test(reproc io C) +reproc_test(reproc overflow C) +reproc_test(reproc path C) +reproc_test(reproc stop C) +reproc_test(reproc working-directory C) +reproc_test(reproc pid C) + +if(UNIX) + reproc_test(reproc fork C) +endif() + +reproc_example(reproc drain C) +reproc_example(reproc env C ARGS PROJECT=REPROC) +reproc_example(reproc path C) +reproc_example(reproc poll C) +reproc_example(reproc read C) +reproc_example(reproc parent C) +reproc_example(reproc run C) diff --git a/extern/reproc/reproc/examples/drain.c b/extern/reproc/reproc/examples/drain.c new file mode 100644 index 000000000..b4c84c6c6 --- /dev/null +++ b/extern/reproc/reproc/examples/drain.c @@ -0,0 +1,62 @@ +#include + +#include +#include + +// Shows the output of the given command using `reproc_drain`. +int main(int argc, const char **argv) +{ + (void) argc; + + reproc_t *process = NULL; + char *output = NULL; + int r = REPROC_ENOMEM; + + process = reproc_new(); + if (process == NULL) { + goto finish; + } + + r = reproc_start(process, argv + 1, (reproc_options){ 0 }); + if (r < 0) { + goto finish; + } + + r = reproc_close(process, REPROC_STREAM_IN); + if (r < 0) { + goto finish; + } + + // `reproc_drain` reads from a child process and passes the output to the + // given sinks. A sink consists of a function pointer and a context pointer + // which is always passed to the function. reproc provides several built-in + // sinks such as `reproc_sink_string` which stores all provided output in the + // given string. Passing the same sink to both output streams makes sure the + // output from both streams is combined into a single string. + reproc_sink sink = reproc_sink_string(&output); + // By default, reproc only redirects stdout to a pipe and not stderr so we + // pass `REPROC_SINK_NULL` as the sink for stderr here. We could also pass + // `sink` but it wouldn't receive any data from stderr. + r = reproc_drain(process, sink, REPROC_SINK_NULL); + if (r < 0) { + goto finish; + } + + printf("%s", output); + + r = reproc_wait(process, REPROC_INFINITE); + if (r < 0) { + goto finish; + } + +finish: + // Memory allocated by `reproc_sink_string` must be freed with `reproc_free`. + reproc_free(output); + reproc_destroy(process); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/examples/env.c b/extern/reproc/reproc/examples/env.c new file mode 100644 index 000000000..a2382fa46 --- /dev/null +++ b/extern/reproc/reproc/examples/env.c @@ -0,0 +1,21 @@ +#include + +#include + +// Runs a binary as a child process that prints all its environment variables to +// stdout and exits. Additional environment variables (in the format A=B) passed +// via the command line are added to the child process environment variables. +int main(int argc, const char **argv) +{ + (void) argc; + + const char *args[] = { RESOURCE_DIRECTORY "/env", NULL }; + + int r = reproc_run(args, (reproc_options){ .env.extra = argv + 1 }); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/examples/parent.c b/extern/reproc/reproc/examples/parent.c new file mode 100644 index 000000000..ab357f30b --- /dev/null +++ b/extern/reproc/reproc/examples/parent.c @@ -0,0 +1,42 @@ +#include + +#include + +// Forwards the provided command to `reproc_start` and redirects the standard +// streams of the child process to the standard streams of the parent process. +int main(int argc, const char **argv) +{ + if (argc <= 1) { + fprintf(stderr, + "No arguments provided. Example usage: ./inherit cmake --help"); + return EXIT_FAILURE; + } + + reproc_t *process = NULL; + int r = REPROC_ENOMEM; + + process = reproc_new(); + if (process == NULL) { + goto finish; + } + + r = reproc_start(process, argv + 1, + (reproc_options){ .redirect.parent = true }); + if (r < 0) { + goto finish; + } + + r = reproc_wait(process, REPROC_INFINITE); + if (r < 0) { + goto finish; + } + +finish: + reproc_destroy(process); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/examples/path.c b/extern/reproc/reproc/examples/path.c new file mode 100644 index 000000000..ed8421774 --- /dev/null +++ b/extern/reproc/reproc/examples/path.c @@ -0,0 +1,18 @@ +#include + +#include + +// Redirects the output of the given command to the reproc.out file. +int main(int argc, const char **argv) +{ + (void) argc; + + int r = reproc_run(argv + 1, + (reproc_options){ .redirect.path = "reproc.out" }); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/examples/poll.c b/extern/reproc/reproc/examples/poll.c new file mode 100644 index 000000000..b0bd212f2 --- /dev/null +++ b/extern/reproc/reproc/examples/poll.c @@ -0,0 +1,107 @@ +#ifdef _WIN32 + #include +static void millisleep(long ms) +{ + Sleep((DWORD) ms); +} +static int getpid() +{ + return (int) GetCurrentProcessId(); +} +#else + #define _POSIX_C_SOURCE 200809L + #include + #include +static inline void millisleep(long ms) +{ + nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000, + .tv_nsec = ((ms) % 1000L) * 1000000 }, + NULL); +} +#endif + +#include +#include + +#include + +enum { NUM_CHILDREN = 20 }; + +static int parent(const char *program) +{ + reproc_event_source children[NUM_CHILDREN] = { { 0 } }; + int r = -1; + + for (int i = 0; i < NUM_CHILDREN; i++) { + reproc_t *process = reproc_new(); + + const char *args[] = { program, "child", NULL }; + + r = reproc_start(process, args, (reproc_options){ .nonblocking = true }); + if (r < 0) { + goto finish; + } + + children[i].process = process; + children[i].interests = REPROC_EVENT_OUT; + } + + for (;;) { + r = reproc_poll(children, NUM_CHILDREN, REPROC_INFINITE); + if (r < 0) { + r = r == REPROC_EPIPE ? 0 : r; + goto finish; + } + + for (int i = 0; i < NUM_CHILDREN; i++) { + if (children[i].process == NULL || !children[i].events) { + continue; + } + + uint8_t output[4096]; + r = reproc_read(children[i].process, REPROC_STREAM_OUT, output, + sizeof(output)); + if (r == REPROC_EPIPE) { + // `reproc_destroy` returns `NULL`. Event sources with their process set + // to `NULL` are ignored by `reproc_poll`. + children[i].process = reproc_destroy(children[i].process); + continue; + } + + if (r < 0) { + goto finish; + } + + output[r] = '\0'; + printf("%s\n", output); + } + } + +finish: + for (int i = 0; i < NUM_CHILDREN; i++) { + reproc_destroy(children[i].process); + } + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} + +static int child(void) +{ + srand(((unsigned int) getpid())); + int ms = rand() % NUM_CHILDREN * 4; // NOLINT + millisleep(ms); + printf("Process %i slept %i milliseconds.", getpid(), ms); + return EXIT_SUCCESS; +} + +// Starts a number of child processes that each sleep a random amount of +// milliseconds before printing a message and exiting. The parent process polls +// each of the child processes and prints their messages to stdout. +int main(int argc, const char **argv) +{ + return argc > 1 && strcmp(argv[1], "child") == 0 ? child() : parent(argv[0]); +} diff --git a/extern/reproc/reproc/examples/read.c b/extern/reproc/reproc/examples/read.c new file mode 100644 index 000000000..7e874649f --- /dev/null +++ b/extern/reproc/reproc/examples/read.c @@ -0,0 +1,108 @@ +#include +#include + +#include + +// Prints the output of the given command using `reproc_read`. Usually, using +// `reproc_run` or `reproc_drain` is a better solution when dealing with a +// single child process. +int main(int argc, const char **argv) +{ + (void) argc; + + // `reproc_t` stores necessary information between calls to reproc's API. + reproc_t *process = NULL; + char *output = NULL; + size_t size = 0; + int r = REPROC_ENOMEM; + + process = reproc_new(); + if (process == NULL) { + goto finish; + } + + // `reproc_start` takes a child process instance (`reproc_t`), argv and + // a set of options including the working directory and environment of the + // child process. If the working directory is `NULL` the working directory of + // the parent process is used. If the environment is `NULL`, the environment + // of the parent process is used. + r = reproc_start(process, argv + 1, (reproc_options){ 0 }); + + // On failure, reproc's API functions return a negative `errno` (POSIX) or + // `GetLastError` (Windows) style error code. To check against common error + // codes, reproc provides cross platform constants such as `REPROC_EPIPE` and + // `REPROC_ETIMEDOUT`. + if (r < 0) { + goto finish; + } + + // Close the stdin stream since we're not going to write any input to the + // child process. + r = reproc_close(process, REPROC_STREAM_IN); + if (r < 0) { + goto finish; + } + + // Read the entire output of the child process. I've found this pattern to be + // the most readable when reading the entire output of a child process. The + // while loop keeps running until an error occurs in `reproc_read` (the child + // process closing its output stream is also reported as an error). + for (;;) { + uint8_t buffer[4096]; + r = reproc_read(process, REPROC_STREAM_OUT, buffer, sizeof(buffer)); + if (r < 0) { + break; + } + + // On success, `reproc_read` returns the amount of bytes read. + size_t bytes_read = (size_t) r; + + // Increase the size of `output` to make sure it can hold the new output. + // This is definitely not the most performant way to grow a buffer so keep + // that in mind. Add 1 to size to leave space for the NUL terminator which + // isn't included in `output_size`. + char *result = realloc(output, size + bytes_read + 1); + if (result == NULL) { + r = REPROC_ENOMEM; + goto finish; + } + + output = result; + + // Copy new data into `output`. + memcpy(output + size, buffer, bytes_read); + output[size + bytes_read] = '\0'; + size += bytes_read; + } + + // Check that the while loop stopped because the output stream of the child + // process was closed and not because of any other error. + if (r != REPROC_EPIPE) { + goto finish; + } + + printf("%s", output); + + // Wait for the process to exit. This should always be done since some systems + // (POSIX) don't clean up system resources allocated to a child process until + // the parent process explicitly waits for it after it has exited. + r = reproc_wait(process, REPROC_INFINITE); + if (r < 0) { + goto finish; + } + +finish: + free(output); + + // Clean up all the resources allocated to the child process (including the + // memory allocated by `reproc_new`). Unless custom stop actions are passed to + // `reproc_start`, `reproc_destroy` will first wait indefinitely for the child + // process to exit. + reproc_destroy(process); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/examples/run.c b/extern/reproc/reproc/examples/run.c new file mode 100644 index 000000000..6ea0c81cf --- /dev/null +++ b/extern/reproc/reproc/examples/run.c @@ -0,0 +1,19 @@ +#include + +#include + +// Start a process from the arguments given on the command line. Inherit the +// parent's standard streams and allow the process to run for maximum 5 seconds +// before terminating it. +int main(int argc, const char **argv) +{ + (void) argc; + + int r = reproc_run(argv + 1, (reproc_options){ .deadline = 5000 }); + + if (r < 0) { + fprintf(stderr, "%s\n", reproc_strerror(r)); + } + + return abs(r); +} diff --git a/extern/reproc/reproc/include/reproc/drain.h b/extern/reproc/reproc/include/reproc/drain.h new file mode 100644 index 000000000..355208e68 --- /dev/null +++ b/extern/reproc/reproc/include/reproc/drain.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! Used by `reproc_drain` to provide data to the caller. Each time data is +read, `function` is called with `context`. If a sink returns a non-zero value, +`reproc_drain` will return immediately with the same value. */ +typedef struct reproc_sink { + int (*function)(REPROC_STREAM stream, + const uint8_t *buffer, + size_t size, + void *context); + void *context; +} reproc_sink; + +/*! Pass `REPROC_SINK_NULL` as the sink for output streams that have not been +redirected to a pipe. */ +REPROC_EXPORT extern const reproc_sink REPROC_SINK_NULL; + +/*! +Reads from the child process stdout and stderr until an error occurs or both +streams are closed. The `out` and `err` sinks receive the output from stdout and +stderr respectively. The same sink may be passed to both `out` and `err`. + +`reproc_drain` always starts by calling both sinks once with an empty buffer and +`stream` set to `REPROC_STREAM_IN` to give each sink the chance to process all +output from the previous call to `reproc_drain` one by one. + +When a stream is closed, its corresponding `sink` is called once with `size` set +to zero. + +Note that his function returns 0 instead of `REPROC_EPIPE` when both output +streams of the child process are closed. + +Actionable errors: +- `REPROC_ETIMEDOUT` +*/ +REPROC_EXPORT int +reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err); + +/*! +Appends the output of a process (stdout and stderr) to the value of `output`. +`output` must point to either `NULL` or a NUL-terminated string. + +Calls `realloc` as necessary to make space in `output` to store the output of +the child process. Make sure to always call `reproc_free` on the value of +`output` after calling `reproc_drain` (even if it fails). + +Because the resulting sink does not store the output size, `strlen` is called +each time data is read to calculate the current size of the output. This might +cause performance problems when draining processes that produce a lot of output. + +Similarly, this sink will not work on processes that have NUL terminators in +their output because `strlen` is used to calculate the current output size. + +Returns `REPROC_ENOMEM` if a call to `realloc` fails. `output` will contain any +output read from the child process, preceeded by whatever was stored in it at +the moment its corresponding sink was passed to `reproc_drain`. + +The `drain` example shows how to use `reproc_sink_string`. +``` +*/ +REPROC_EXPORT reproc_sink reproc_sink_string(char **output); + +/*! Discards the output of a process. */ +REPROC_EXPORT reproc_sink reproc_sink_discard(void); + +/*! Calls `free` on `ptr` and returns `NULL`. Use this function to free memory +allocated by `reproc_sink_string`. This avoids issues with allocating across +module (DLL) boundaries on Windows. */ +REPROC_EXPORT void *reproc_free(void *ptr); + +#ifdef __cplusplus +} +#endif diff --git a/extern/reproc/reproc/include/reproc/export.h b/extern/reproc/reproc/include/reproc/export.h new file mode 100644 index 000000000..8f558c25f --- /dev/null +++ b/extern/reproc/reproc/include/reproc/export.h @@ -0,0 +1,21 @@ +#pragma once + +#ifndef REPROC_EXPORT + #ifdef _WIN32 + #ifdef REPROC_SHARED + #ifdef REPROC_BUILDING + #define REPROC_EXPORT __declspec(dllexport) + #else + #define REPROC_EXPORT __declspec(dllimport) + #endif + #else + #define REPROC_EXPORT + #endif + #else + #ifdef REPROC_BUILDING + #define REPROC_EXPORT __attribute__((visibility("default"))) + #else + #define REPROC_EXPORT + #endif + #endif +#endif diff --git a/extern/reproc/reproc/include/reproc/reproc.h b/extern/reproc/reproc/include/reproc/reproc.h new file mode 100644 index 000000000..9c45c0923 --- /dev/null +++ b/extern/reproc/reproc/include/reproc/reproc.h @@ -0,0 +1,530 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! Used to store information about a child process. `reproc_t` is an opaque +type and can be allocated and released via `reproc_new` and `reproc_destroy` +respectively. */ +typedef struct reproc_t reproc_t; + +/*! reproc error naming follows POSIX errno naming prefixed with `REPROC`. */ + +/*! An invalid argument was passed to an API function */ +REPROC_EXPORT extern const int REPROC_EINVAL; +/*! A timeout value passed to an API function expired. */ +REPROC_EXPORT extern const int REPROC_ETIMEDOUT; +/*! The child process closed one of its streams (and in the case of +stdout/stderr all of the data remaining in that stream has been read). */ +REPROC_EXPORT extern const int REPROC_EPIPE; +/*! A memory allocation failed. */ +REPROC_EXPORT extern const int REPROC_ENOMEM; +/*! A call to `reproc_read` or `reproc_write` would have blocked. */ +REPROC_EXPORT extern const int REPROC_EWOULDBLOCK; + +/*! Signal exit status constants. */ + +REPROC_EXPORT extern const int REPROC_SIGKILL; +REPROC_EXPORT extern const int REPROC_SIGTERM; + +/*! Tells a function that takes a timeout value to wait indefinitely. */ +REPROC_EXPORT extern const int REPROC_INFINITE; +/*! Tells `reproc_wait` to wait until the deadline passed to `reproc_start` +expires. */ +REPROC_EXPORT extern const int REPROC_DEADLINE; + +/*! Stream identifiers used to indicate which stream to act on. */ +typedef enum { + /*! stdin */ + REPROC_STREAM_IN, + /*! stdout */ + REPROC_STREAM_OUT, + /*! stderr */ + REPROC_STREAM_ERR, +} REPROC_STREAM; + +/*! Used to tell reproc where to redirect the streams of the child process. */ +typedef enum { + /*! Use the default redirect behavior, see the documentation for `redirect` in + `reproc_options`. */ + REPROC_REDIRECT_DEFAULT, + /*! Redirect to a pipe. */ + REPROC_REDIRECT_PIPE, + /*! Redirect to the corresponding stream from the parent process. */ + REPROC_REDIRECT_PARENT, + /*! Redirect to /dev/null (or NUL on Windows). */ + REPROC_REDIRECT_DISCARD, + /*! Redirect to child process stdout. Only valid for stderr. */ + REPROC_REDIRECT_STDOUT, + /*! Redirect to a handle (fd on Linux, HANDLE/SOCKET on Windows). */ + REPROC_REDIRECT_HANDLE, + /*! Redirect to a `FILE *`. */ + REPROC_REDIRECT_FILE, + /*! Redirect to a specific path. */ + REPROC_REDIRECT_PATH, +} REPROC_REDIRECT; + +/*! Used to tell `reproc_stop` how to stop a child process. */ +typedef enum { + /*! noop (no operation) */ + REPROC_STOP_NOOP, + /*! `reproc_wait` */ + REPROC_STOP_WAIT, + /*! `reproc_terminate` */ + REPROC_STOP_TERMINATE, + /*! `reproc_kill` */ + REPROC_STOP_KILL, +} REPROC_STOP; + +typedef struct reproc_stop_action { + REPROC_STOP action; + int timeout; +} reproc_stop_action; + +typedef struct reproc_stop_actions { + reproc_stop_action first; + reproc_stop_action second; + reproc_stop_action third; +} reproc_stop_actions; + +// clang-format off + +#define REPROC_STOP_ACTIONS_NULL (reproc_stop_actions) { \ + { REPROC_STOP_NOOP, 0 }, \ + { REPROC_STOP_NOOP, 0 }, \ + { REPROC_STOP_NOOP, 0 }, \ +} + +// clang-format on + +#if defined(_WIN32) +typedef void *reproc_handle; // `HANDLE` +#else +typedef int reproc_handle; // fd +#endif + +typedef struct reproc_redirect { + /*! Type of redirection. */ + REPROC_REDIRECT type; + /*! + Redirect a stream to an operating system handle. The given handle must be in + blocking mode ( `O_NONBLOCK` and `OVERLAPPED` handles are not supported). + + Note that reproc does not take ownership of the handle. The user is + responsible for closing the handle after passing it to `reproc_start`. Since + the operating system will copy the handle to the child process, the handle + can be closed immediately after calling `reproc_start` if the handle is not + needed in the parent process anymore. + + If `handle` is set, `type` must be unset or set to `REPROC_REDIRECT_HANDLE` + and `file`, `path` must be unset. + */ + reproc_handle handle; + /*! + Redirect a stream to a file stream. + + Note that reproc does not take ownership of the file. The user is + responsible for closing the file after passing it to `reproc_start`. Just + like with `handles`, the operating system will copy the file handle to the + child process so the file can be closed immediately after calling + `reproc_start` if it isn't needed anymore by the parent process. + + Any file passed to `file.in` must have been opened in read mode. Likewise, + any files passed to `file.out` or `file.err` must have been opened in write + mode. + + If `file` is set, `type` must be unset or set to `REPROC_REDIRECT_FILE` and + `handle`, `path` must be unset. + */ + FILE *file; + /*! + Redirect a stream to a given path. + + reproc will create or open the file at the given path. Depending on the + stream, the file is opened in read or write mode. + + If `path` is set, `type` must be unset or set to `REPROC_REDIRECT_PATH` and + `handle`, `file` must be unset. + */ + const char *path; +} reproc_redirect; + +typedef enum { + REPROC_ENV_EXTEND, + REPROC_ENV_EMPTY, +} REPROC_ENV; + +typedef struct reproc_options { + /*! + `working_directory` specifies the working directory for the child process. If + `working_directory` is `NULL`, the child process runs in the working directory + of the parent process. + */ + const char *working_directory; + + struct { + /*! + `behavior` specifies whether the child process should start with a copy of + the parent process environment variables or an empty environment. By + default, the child process starts with a copy of the parent's environment + variables (`REPROC_ENV_EXTEND`). If `behavior` is set to `REPROC_ENV_EMPTY`, + the child process starts with an empty environment. + */ + REPROC_ENV behavior; + /*! + `extra` is an array of UTF-8 encoded, NUL-terminated strings that specifies + extra environment variables for the child process. It has the following + layout: + + - All elements except the final element must be of the format `NAME=VALUE`. + - The final element must be `NULL`. + + Example: ["IP=127.0.0.1", "PORT=8080", `NULL`] + + If `env` is `NULL`, no extra environment variables are added to the + environment of the child process. + */ + const char *const *extra; + } env; + /*! + `redirect` specifies where to redirect the streams from the child process. + + By default each stream is redirected to a pipe which can be written to (stdin) + or read from (stdout/stderr) using `reproc_write` and `reproc_read` + respectively. + */ + struct { + /*! + `in`, `out` and `err` specify where to redirect the standard I/O streams of + the child process. When not set, `in` and `out` default to + `REPROC_REDIRECT_PIPE` while `err` defaults to `REPROC_REDIRECT_PARENT`. + */ + reproc_redirect in; + reproc_redirect out; + reproc_redirect err; + /*! + Use `REPROC_REDIRECT_PARENT` instead of `REPROC_REDIRECT_PIPE` when `type` + is unset. + + When this option is set, `discard`, `file` and `path` must be unset. + */ + bool parent; + /*! + Use `REPROC_REDIRECT_DISCARD` instead of `REPROC_REDIRECT_PIPE` when `type` + is unset. + + When this option is set, `parent`, `file` and `path` must be unset. + */ + bool discard; + /*! + Shorthand for redirecting stdout and stderr to the same file. + + If this option is set, `out`, `err`, `parent`, `discard` and `path` must be + unset. + */ + FILE *file; + /*! + Shorthand for redirecting stdout and stderr to the same path. + + If this option is set, `out`, `err`, `parent`, `discard` and `file` must be + unset. + */ + const char *path; + } redirect; + /*! + Stop actions that are passed to `reproc_stop` in `reproc_destroy` to stop the + child process. See `reproc_stop` for more information on how `stop` is + interpreted. + */ + reproc_stop_actions stop; + /*! + Maximum allowed duration in milliseconds the process is allowed to run in + milliseconds. If the deadline is exceeded, Any ongoing and future calls to + `reproc_poll` return `REPROC_ETIMEDOUT`. + + Note that only `reproc_poll` takes the deadline into account. More + specifically, if the `nonblocking` option is not enabled, `reproc_read` and + `reproc_write` can deadlock waiting on the child process to perform I/O. If + this is a problem, enable the `nonblocking` option and use `reproc_poll` + together with a deadline/timeout to avoid any deadlocks. + + If `REPROC_DEADLINE` is passed as the timeout to `reproc_wait`, it waits until + the deadline expires. + + When `deadline` is zero, no deadline is set for the process. + */ + int deadline; + /*! + `input` is written to the stdin pipe before the child process is started. + + Because `input` is written to the stdin pipe before the process starts, + `input.size` must be smaller than the system's default pipe size (64KB). + + If `input` is set, the stdin pipe is closed after `input` is written to it. + + If `redirect.in` is set, this option may not be set. + */ + struct { + const uint8_t *data; + size_t size; + } input; + /*! + This option can only be used on POSIX systems. If enabled on Windows, an error + will be returned. + + If `fork` is enabled, `reproc_start` forks a child process and returns 0 in + the child process and > 0 in the parent process. In the child process, only + `reproc_destroy` may be called on the `reproc_t` instance to free its + associated memory. + + When `fork` is enabled. `argv` must be `NULL` when calling `reproc_start`. + */ + bool fork; + /*! + Put pipes created by reproc in nonblocking mode. This makes `reproc_read` and + `reproc_write` nonblocking operations. If needed, use `reproc_poll` to wait + until streams becomes readable/writable. + */ + bool nonblocking; +} reproc_options; + +enum { + /*! Data can be written to stdin. */ + REPROC_EVENT_IN = 1 << 0, + /*! Data can be read from stdout. */ + REPROC_EVENT_OUT = 1 << 1, + /*! Data can be read from stderr. */ + REPROC_EVENT_ERR = 1 << 2, + /*! The process finished running. */ + REPROC_EVENT_EXIT = 1 << 3, + /*! The deadline of the process expired. This event is added by default to the + list of interested events. */ + REPROC_EVENT_DEADLINE = 1 << 4, +}; + +typedef struct reproc_event_source { + /*! Process to poll for events. */ + reproc_t *process; + /*! Events of the process that we're interested in. Takes a combo of + `REPROC_EVENT` flags. */ + int interests; + /*! Combo of `REPROC_EVENT` flags that indicate the events that occurred. This + field is filled in by `reproc_poll`. */ + int events; +} reproc_event_source; + +/*! Allocate a new `reproc_t` instance on the heap. */ +REPROC_EXPORT reproc_t *reproc_new(void); + +/*! +Starts the process specified by `argv` in the given working directory and +redirects its input, output and error streams. + +If this function does not return an error the child process will have started +running and can be inspected using the operating system's tools for process +inspection (e.g. ps on Linux). + +Every successful call to this function should be followed by a successful call +to `reproc_wait` or `reproc_stop` and a call to `reproc_destroy`. If an error +occurs during `reproc_start` all allocated resources are cleaned up before +`reproc_start` returns and no further action is required. + +`argv` is an array of UTF-8 encoded, NUL-terminated strings that specifies the +program to execute along with its arguments. It has the following layout: + +- The first element indicates the executable to run as a child process. This can +be an absolute path, a path relative to the working directory of the parent +process or the name of an executable located in the PATH. It cannot be `NULL`. +- The following elements indicate the whitespace delimited arguments passed to +the executable. None of these elements can be `NULL`. +- The final element must be `NULL`. + +Example: ["cmake", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", `NULL`] +*/ +REPROC_EXPORT int reproc_start(reproc_t *process, + const char *const *argv, + reproc_options options); + +/*! +Returns the process ID of the child or `REPROC_EINVAL` on error. + +Note that if `reproc_wait` has been called successfully on this process already, +the returned pid will be that of the just ended child process. The operating +system will have cleaned up the resources allocated to the process +and the operating system is free to reuse the same pid for another process. + +Generally, only pass the result of this function to system calls that need a +valid pid if `reproc_wait` hasn't been called successfully on the process yet. +*/ +REPROC_EXPORT int reproc_pid(reproc_t *process); + +/*! +Polls each process in `sources` for its corresponding events in `interests` and +stores events that occurred for each process in `events`. If an event source +process member is `NULL`, the event source is ignored. + +Pass `REPROC_INFINITE` to `timeout` to have `reproc_poll` wait forever for an +event to occur. + +If one or more events occur, returns the number of processes with events. If the +timeout expires, returns zero. Returns `REPROC_EPIPE` if none of the sources +have valid pipes remaining that can be polled. + +Actionable errors: +- `REPROC_EPIPE` +*/ +REPROC_EXPORT int +reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout); + +/*! +Reads up to `size` bytes into `buffer` from the child process output stream +indicated by `stream`. + +Actionable errors: +- `REPROC_EPIPE` +- `REPROC_EWOULDBLOCK` +*/ +REPROC_EXPORT int reproc_read(reproc_t *process, + REPROC_STREAM stream, + uint8_t *buffer, + size_t size); + +/*! +Writes up to `size` bytes from `buffer` to the standard input (stdin) of the +child process. + +(POSIX) By default, writing to a closed stdin pipe terminates the parent process +with the `SIGPIPE` signal. `reproc_write` will only return `REPROC_EPIPE` if +this signal is ignored by the parent process. + +Returns the amount of bytes written. If `buffer` is `NULL` and `size` is zero, +this function returns 0. + +If the standard input of the child process wasn't opened with +`REPROC_REDIRECT_PIPE`, this function returns `REPROC_EPIPE` unless `buffer` is +`NULL` and `size` is zero. + +Actionable errors: +- `REPROC_EPIPE` +- `REPROC_EWOULDBLOCK` +*/ +REPROC_EXPORT int +reproc_write(reproc_t *process, const uint8_t *buffer, size_t size); + +/*! +Closes the child process standard stream indicated by `stream`. + +This function is necessary when a child process reads from stdin until it is +closed. After writing all the input to the child process using `reproc_write`, +the standard input stream can be closed using this function. +*/ +REPROC_EXPORT int reproc_close(reproc_t *process, REPROC_STREAM stream); + +/*! +Waits `timeout` milliseconds for the child process to exit. If the child process +has already exited or exits within the given timeout, its exit status is +returned. + +If `timeout` is 0, the function will only check if the child process is still +running without waiting. If `timeout` is `REPROC_INFINITE`, this function will +wait indefinitely for the child process to exit. If `timeout` is +`REPROC_DEADLINE`, this function waits until the deadline passed to +`reproc_start` expires. + +Actionable errors: +- `REPROC_ETIMEDOUT` +*/ +REPROC_EXPORT int reproc_wait(reproc_t *process, int timeout); + +/*! +Sends the `SIGTERM` signal (POSIX) or the `CTRL-BREAK` signal (Windows) to the +child process. Remember that successful calls to `reproc_wait` and +`reproc_destroy` are required to make sure the child process is completely +cleaned up. +*/ +REPROC_EXPORT int reproc_terminate(reproc_t *process); + +/*! +Sends the `SIGKILL` signal to the child process (POSIX) or calls +`TerminateProcess` (Windows) on the child process. Remember that successful +calls to `reproc_wait` and `reproc_destroy` are required to make sure the child +process is completely cleaned up. +*/ +REPROC_EXPORT int reproc_kill(reproc_t *process); + +/*! +Simplifies calling combinations of `reproc_wait`, `reproc_terminate` and +`reproc_kill`. The function executes each specified step and waits (using +`reproc_wait`) until the corresponding timeout expires before continuing with +the next step. + +Example: + +Wait 10 seconds for the child process to exit on its own before sending +`SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) and waiting five more seconds for +the child process to exit. + +```c +REPROC_ERROR error = reproc_stop(process, + REPROC_STOP_WAIT, 10000, + REPROC_STOP_TERMINATE, 5000, + REPROC_STOP_NOOP, 0); +``` + +Call `reproc_wait`, `reproc_terminate` and `reproc_kill` directly if you need +extra logic such as logging between calls. + +`stop` can contain up to three stop actions that instruct this function how the +child process should be stopped. The first element of each stop action specifies +which action should be called on the child process. The second element of each +stop actions specifies how long to wait after executing the operation indicated +by the first element. + +When `stop` is 3x `REPROC_STOP_NOOP`, `reproc_destroy` will wait until the +deadline expires (or forever if there is no deadline). If the process is still +running after the deadline expires, `reproc_stop` then calls `reproc_terminate` +and waits forever for the process to exit. + +Note that when a stop action specifies `REPROC_STOP_WAIT`, the function will +just wait for the specified timeout instead of performing an action to stop the +child process. + +If the child process has already exited or exits during the execution of this +function, its exit status is returned. + +Actionable errors: +- `REPROC_ETIMEDOUT` +*/ +REPROC_EXPORT int reproc_stop(reproc_t *process, reproc_stop_actions stop); + +/*! +Release all resources associated with `process` including the memory allocated +by `reproc_new`. Calling this function before a succesfull call to `reproc_wait` +can result in resource leaks. + +Does nothing if `process` is an invalid `reproc_t` instance and always returns +an invalid `reproc_t` instance (`NULL`). By assigning the result of +`reproc_destroy` to the instance being destroyed, it can be safely called +multiple times on the same instance. + +Example: `process = reproc_destroy(process)`. +*/ +REPROC_EXPORT reproc_t *reproc_destroy(reproc_t *process); + +/*! +Returns a string describing `error`. This string must not be modified by the +caller. +*/ +REPROC_EXPORT const char *reproc_strerror(int error); + +#ifdef __cplusplus +} +#endif diff --git a/extern/reproc/reproc/include/reproc/run.h b/extern/reproc/reproc/include/reproc/run.h new file mode 100644 index 000000000..5d8deffb5 --- /dev/null +++ b/extern/reproc/reproc/include/reproc/run.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! Sets `options.redirect.parent = true` unless `discard` is set and calls +`reproc_run_ex` with `REPROC_SINK_NULL` for the `out` and `err` sinks. */ +REPROC_EXPORT int reproc_run(const char *const *argv, reproc_options options); + +/*! +Wrapper function that starts a process with the given arguments, drain its +output and waits until it exits. Have a look at its (trivial) implementation and +the documentation of the functions it calls to see exactly what it does: +https://github.com/DaanDeMeyer/reproc/blob/master/reproc/src/run.c +*/ +REPROC_EXPORT int reproc_run_ex(const char *const *argv, + reproc_options options, + reproc_sink out, + reproc_sink err); + +#ifdef __cplusplus +} +#endif diff --git a/extern/reproc/reproc/reproc-config.cmake.in b/extern/reproc/reproc/reproc-config.cmake.in new file mode 100644 index 000000000..b3ff831bc --- /dev/null +++ b/extern/reproc/reproc/reproc-config.cmake.in @@ -0,0 +1,12 @@ +@PACKAGE_INIT@ + +set(REPROC_MULTITHREADED @REPROC_MULTITHREADED@) + +include(CMakeFindDependencyMacro) + +if(REPROC_MULTITHREADED) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_dependency(Threads) +endif() + +include(${CMAKE_CURRENT_LIST_DIR}/@TARGET@-targets.cmake) diff --git a/extern/reproc/reproc/reproc.pc.in b/extern/reproc/reproc/reproc.pc.in new file mode 100644 index 000000000..e2502aaac --- /dev/null +++ b/extern/reproc/reproc/reproc.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @TARGET@ +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@TARGET@ +Libs.private: @REPROC_THREAD_LIBRARY@ @REPROC_WINSOCK_LIBRARY@ @REPROC_RT_LIBRARY@ diff --git a/extern/reproc/reproc/resources/argv.c b/extern/reproc/reproc/resources/argv.c new file mode 100644 index 000000000..47f0861ae --- /dev/null +++ b/extern/reproc/reproc/resources/argv.c @@ -0,0 +1,10 @@ +#include + +int main(int argc, const char **argv) +{ + for (int i = 0; i < argc; i++) { + printf("%s\n", argv[i]); + } + + return 0; +} diff --git a/extern/reproc/reproc/resources/deadline.c b/extern/reproc/reproc/resources/deadline.c new file mode 100644 index 000000000..7454e569e --- /dev/null +++ b/extern/reproc/reproc/resources/deadline.c @@ -0,0 +1,7 @@ +#include "sleep.h" + +int main(void) +{ + millisleep(25000); + return 0; +} diff --git a/extern/reproc/reproc/resources/env.c b/extern/reproc/reproc/resources/env.c new file mode 100644 index 000000000..4bd9ed781 --- /dev/null +++ b/extern/reproc/reproc/resources/env.c @@ -0,0 +1,13 @@ +#include + +int main(int argc, const char **argv, const char **envp) +{ + (void) argc; + (void) argv; + + for (size_t i = 0; envp[i] != NULL; i++) { + printf("%s\n", envp[i]); + } + + return 0; +} diff --git a/extern/reproc/reproc/resources/io.c b/extern/reproc/reproc/resources/io.c new file mode 100644 index 000000000..9a4e5ee79 --- /dev/null +++ b/extern/reproc/reproc/resources/io.c @@ -0,0 +1,15 @@ +#include + +int main(void) +{ + char input[8096]; + + if (fgets(input, sizeof(input), stdin) == NULL) { + return 1; + } + + fprintf(stdout, "%s", input); + fprintf(stderr, "%s", input); + + return 0; +} diff --git a/extern/reproc/reproc/resources/overflow.c b/extern/reproc/reproc/resources/overflow.c new file mode 100644 index 000000000..3f8821dd4 --- /dev/null +++ b/extern/reproc/reproc/resources/overflow.c @@ -0,0 +1,14 @@ +#include +#include + +int main() +{ + char buffer[8192]; + + for (int i = 0; i < 200; i++) { + FILE *stream = rand() % 2 ? stdout : stderr; // NOLINT + fprintf(stream, "%s", buffer); + } + + return 0; +} diff --git a/extern/reproc/reproc/resources/path.c b/extern/reproc/reproc/resources/path.c new file mode 100644 index 000000000..3bcf25b4a --- /dev/null +++ b/extern/reproc/reproc/resources/path.c @@ -0,0 +1,10 @@ +#include + +int main(int argc, const char **argv) +{ + (void) argc; + + printf("%s", argv[0]); + + return 0; +} diff --git a/extern/reproc/reproc/resources/pid.c b/extern/reproc/reproc/resources/pid.c new file mode 100644 index 000000000..33e0bef76 --- /dev/null +++ b/extern/reproc/reproc/resources/pid.c @@ -0,0 +1,15 @@ +#include + +#ifdef _WIN32 + #include + #define getpid (int) GetCurrentProcessId +#else + #include +#endif + +int main(void) +{ + printf("%d", getpid()); + + return 0; +} diff --git a/extern/reproc/reproc/resources/sleep.h b/extern/reproc/reproc/resources/sleep.h new file mode 100644 index 000000000..a53f851b7 --- /dev/null +++ b/extern/reproc/reproc/resources/sleep.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef _WIN32 + #include +static inline void millisleep(long ms) +{ + Sleep((DWORD) ms); +} +#else + #define _POSIX_C_SOURCE 200809L + #include +static inline void millisleep(long ms) +{ + nanosleep(&(struct timespec){ .tv_sec = (ms) / 1000, + .tv_nsec = ((ms) % 1000L) * 1000000 }, + NULL); +} +#endif diff --git a/extern/reproc/reproc/resources/stop.c b/extern/reproc/reproc/resources/stop.c new file mode 100644 index 000000000..7454e569e --- /dev/null +++ b/extern/reproc/reproc/resources/stop.c @@ -0,0 +1,7 @@ +#include "sleep.h" + +int main(void) +{ + millisleep(25000); + return 0; +} diff --git a/extern/reproc/reproc/resources/working-directory.c b/extern/reproc/reproc/resources/working-directory.c new file mode 100644 index 000000000..5bc25c70e --- /dev/null +++ b/extern/reproc/reproc/resources/working-directory.c @@ -0,0 +1,21 @@ +#include + +#if defined(_WIN32) + #include + #define getcwd _getcwd +#else + #include +#endif + +int main() +{ + char working_directory[8096]; + + if (getcwd(working_directory, sizeof(working_directory)) == NULL) { + return 1; + } + + printf("%s", working_directory); + + return 0; +} diff --git a/extern/reproc/reproc/src/clock.h b/extern/reproc/reproc/src/clock.h new file mode 100644 index 000000000..460e03bb3 --- /dev/null +++ b/extern/reproc/reproc/src/clock.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +int64_t now(void); diff --git a/extern/reproc/reproc/src/clock.posix.c b/extern/reproc/reproc/src/clock.posix.c new file mode 100644 index 000000000..8d22a3b27 --- /dev/null +++ b/extern/reproc/reproc/src/clock.posix.c @@ -0,0 +1,17 @@ +#define _POSIX_C_SOURCE 200809L + +#include "clock.h" + +#include + +#include "error.h" + +int64_t now(void) +{ + struct timespec timespec = { 0 }; + + int r = clock_gettime(CLOCK_REALTIME, ×pec); + ASSERT_UNUSED(r == 0); + + return timespec.tv_sec * 1000 + timespec.tv_nsec / 1000000; +} diff --git a/extern/reproc/reproc/src/clock.windows.c b/extern/reproc/reproc/src/clock.windows.c new file mode 100644 index 000000000..3130f851f --- /dev/null +++ b/extern/reproc/reproc/src/clock.windows.c @@ -0,0 +1,10 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "clock.h" + +#include + +int64_t now(void) +{ + return (int64_t) GetTickCount64(); +} diff --git a/extern/reproc/reproc/src/drain.c b/extern/reproc/reproc/src/drain.c new file mode 100644 index 000000000..37e836afa --- /dev/null +++ b/extern/reproc/reproc/src/drain.c @@ -0,0 +1,121 @@ +#include + +#include +#include + +#include "error.h" +#include "macro.h" + +int reproc_drain(reproc_t *process, reproc_sink out, reproc_sink err) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(out.function); + ASSERT_EINVAL(err.function); + + const uint8_t initial = 0; + int r = -1; + + // 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 one by one before reading from the child + // process again. + + r = out.function(REPROC_STREAM_IN, &initial, 0, out.context); + if (r != 0) { + return r; + } + + r = err.function(REPROC_STREAM_IN, &initial, 0, err.context); + if (r != 0) { + return r; + } + + uint8_t buffer[4096]; + + for (;;) { + reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR, + 0 }; + + r = reproc_poll(&source, 1, REPROC_INFINITE); + if (r < 0) { + r = r == REPROC_EPIPE ? 0 : r; + break; + } + + if (source.events & REPROC_EVENT_DEADLINE) { + r = REPROC_ETIMEDOUT; + break; + } + + REPROC_STREAM stream = source.events & REPROC_EVENT_OUT ? REPROC_STREAM_OUT + : REPROC_STREAM_ERR; + + r = reproc_read(process, stream, buffer, ARRAY_SIZE(buffer)); + if (r < 0 && r != REPROC_EPIPE) { + break; + } + + size_t bytes_read = r == REPROC_EPIPE ? 0 : (size_t) r; + reproc_sink sink = stream == REPROC_STREAM_OUT ? out : err; + + r = sink.function(stream, buffer, bytes_read, sink.context); + if (r != 0) { + break; + } + } + + return r; +} + +static int sink_string(REPROC_STREAM stream, + const uint8_t *buffer, + size_t size, + void *context) +{ + (void) stream; + + char **string = (char **) context; + size_t string_size = *string == NULL ? 0 : strlen(*string); + + char *r = (char *) realloc(*string, string_size + size + 1); + if (r == NULL) { + return REPROC_ENOMEM; + } + + *string = r; + memcpy(*string + string_size, buffer, size); + (*string)[string_size + size] = '\0'; + + return 0; +} + +reproc_sink reproc_sink_string(char **output) +{ + return (reproc_sink){ sink_string, output }; +} + +static int sink_discard(REPROC_STREAM stream, + const uint8_t *buffer, + size_t size, + void *context) +{ + (void) stream; + (void) buffer; + (void) size; + (void) context; + + return 0; +} + +reproc_sink reproc_sink_discard(void) +{ + return (reproc_sink){ sink_discard, NULL }; +} + +const reproc_sink REPROC_SINK_NULL = { sink_discard, NULL }; + +void *reproc_free(void *ptr) +{ + free(ptr); + return NULL; +} diff --git a/extern/reproc/reproc/src/error.h b/extern/reproc/reproc/src/error.h new file mode 100644 index 000000000..bfc031117 --- /dev/null +++ b/extern/reproc/reproc/src/error.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#define ASSERT(expression) assert(expression) + +// Avoid unused assignment warnings in release mode when the result of an +// assignment is only used in an assert statement. +#define ASSERT_UNUSED(expression) \ + do { \ + (void) !(expression); \ + ASSERT((expression)); \ + } while (0) + +// Returns `r` if `expression` is false. +#define ASSERT_RETURN(expression, r) \ + do { \ + if (!(expression)) { \ + return (r); \ + } \ + } while (0) + +#define ASSERT_EINVAL(expression) ASSERT_RETURN(expression, REPROC_EINVAL) + +const char *error_string(int error); diff --git a/extern/reproc/reproc/src/error.posix.c b/extern/reproc/reproc/src/error.posix.c new file mode 100644 index 000000000..7c1a7c9bb --- /dev/null +++ b/extern/reproc/reproc/src/error.posix.c @@ -0,0 +1,31 @@ +#define _POSIX_C_SOURCE 200809L + +#include "error.h" + +#include +#include +#include + +#include + +#include "macro.h" + +const int REPROC_EINVAL = -EINVAL; +const int REPROC_EPIPE = -EPIPE; +const int REPROC_ETIMEDOUT = -ETIMEDOUT; +const int REPROC_ENOMEM = -ENOMEM; +const int REPROC_EWOULDBLOCK = -EWOULDBLOCK; + +enum { ERROR_STRING_MAX_SIZE = 512 }; + +const char *error_string(int error) +{ + static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; + + int r = strerror_r(abs(error), string, ARRAY_SIZE(string)); + if (r != 0) { + return "Failed to retrieve error string"; + } + + return string; +} diff --git a/extern/reproc/reproc/src/error.windows.c b/extern/reproc/reproc/src/error.windows.c new file mode 100644 index 000000000..b8d82343e --- /dev/null +++ b/extern/reproc/reproc/src/error.windows.c @@ -0,0 +1,58 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "error.h" + +#include +#include +#include +#include + +#include + +#include "macro.h" + +const int REPROC_EINVAL = -ERROR_INVALID_PARAMETER; +const int REPROC_EPIPE = -ERROR_BROKEN_PIPE; +const int REPROC_ETIMEDOUT = -WAIT_TIMEOUT; +const int REPROC_ENOMEM = -ERROR_NOT_ENOUGH_MEMORY; +const int REPROC_EWOULDBLOCK = -WSAEWOULDBLOCK; + +enum { ERROR_STRING_MAX_SIZE = 512 }; + +const char *error_string(int error) +{ + wchar_t *wstring = NULL; + int r = -1; + + wstring = malloc(sizeof(wchar_t) * ERROR_STRING_MAX_SIZE); + if (wstring == NULL) { + return "Failed to allocate memory for error string"; + } + + // We don't expect message sizes larger than the maximum possible int. + r = (int) FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, (DWORD) abs(error), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), wstring, + ERROR_STRING_MAX_SIZE, NULL); + if (r == 0) { + free(wstring); + return "Failed to retrieve error string"; + } + + static THREAD_LOCAL char string[ERROR_STRING_MAX_SIZE]; + + r = WideCharToMultiByte(CP_UTF8, 0, wstring, -1, string, ARRAY_SIZE(string), + NULL, NULL); + free(wstring); + if (r == 0) { + return "Failed to convert error string to UTF-8"; + } + + // Remove trailing whitespace and period. + if (r >= 4) { + string[r - 4] = '\0'; + } + + return string; +} diff --git a/extern/reproc/reproc/src/handle.h b/extern/reproc/reproc/src/handle.h new file mode 100644 index 000000000..a6ac49bde --- /dev/null +++ b/extern/reproc/reproc/src/handle.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#if defined(_WIN32) +typedef void *handle_type; // `HANDLE` +#else +typedef int handle_type; // fd +#endif + +extern const handle_type HANDLE_INVALID; + +// Sets the `FD_CLOEXEC` flag on the file descriptor. POSIX only. +int handle_cloexec(handle_type handle, bool enable); + +// Closes `handle` if it is not an invalid handle and returns an invalid handle. +// Does not overwrite the last system error if an error occurs while closing +// `handle`. +handle_type handle_destroy(handle_type handle); diff --git a/extern/reproc/reproc/src/handle.posix.c b/extern/reproc/reproc/src/handle.posix.c new file mode 100644 index 000000000..1c9ce104c --- /dev/null +++ b/extern/reproc/reproc/src/handle.posix.c @@ -0,0 +1,42 @@ +#define _POSIX_C_SOURCE 200809L + +#include "handle.h" + +#include +#include +#include + +#include "error.h" + +const int HANDLE_INVALID = -1; + +int handle_cloexec(int handle, bool enable) +{ + int r = -1; + + r = fcntl(handle, F_GETFD, 0); + if (r < 0) { + return -errno; + } + + r = enable ? r | FD_CLOEXEC : r & ~FD_CLOEXEC; + + r = fcntl(handle, F_SETFD, r); + if (r < 0) { + return -errno; + } + + return 0; +} + +int handle_destroy(int handle) +{ + if (handle == HANDLE_INVALID) { + return HANDLE_INVALID; + } + + int r = close(handle); + ASSERT_UNUSED(r == 0); + + return HANDLE_INVALID; +} diff --git a/extern/reproc/reproc/src/handle.windows.c b/extern/reproc/reproc/src/handle.windows.c new file mode 100644 index 000000000..e0cd500dc --- /dev/null +++ b/extern/reproc/reproc/src/handle.windows.c @@ -0,0 +1,23 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "handle.h" + +#include + +#include "error.h" + +const HANDLE HANDLE_INVALID = INVALID_HANDLE_VALUE; // NOLINT + +// `handle_cloexec` is POSIX-only. + +HANDLE handle_destroy(HANDLE handle) +{ + if (handle == NULL || handle == HANDLE_INVALID) { + return HANDLE_INVALID; + } + + int r = CloseHandle(handle); + ASSERT_UNUSED(r != 0); + + return HANDLE_INVALID; +} diff --git a/extern/reproc/reproc/src/init.h b/extern/reproc/reproc/src/init.h new file mode 100644 index 000000000..704e237af --- /dev/null +++ b/extern/reproc/reproc/src/init.h @@ -0,0 +1,5 @@ +#pragma once + +int init(void); + +void deinit(void); diff --git a/extern/reproc/reproc/src/init.posix.c b/extern/reproc/reproc/src/init.posix.c new file mode 100644 index 000000000..e4b44a2e3 --- /dev/null +++ b/extern/reproc/reproc/src/init.posix.c @@ -0,0 +1,10 @@ +#define _POSIX_C_SOURCE 200809L + +#include "init.h" + +int init(void) +{ + return 0; +} + +void deinit(void) {} diff --git a/extern/reproc/reproc/src/init.windows.c b/extern/reproc/reproc/src/init.windows.c new file mode 100644 index 000000000..8357b7c21 --- /dev/null +++ b/extern/reproc/reproc/src/init.windows.c @@ -0,0 +1,24 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "init.h" + +#include + +#include "error.h" + +int init(void) +{ + WSADATA data; + int r = WSAStartup(MAKEWORD(2, 2), &data); + return -r; +} + +void deinit(void) +{ + int saved = WSAGetLastError(); + + int r = WSACleanup(); + ASSERT_UNUSED(r == 0); + + WSASetLastError(saved); +} diff --git a/extern/reproc/reproc/src/macro.h b/extern/reproc/reproc/src/macro.h new file mode 100644 index 000000000..c746360f0 --- /dev/null +++ b/extern/reproc/reproc/src/macro.h @@ -0,0 +1,11 @@ +#pragma once + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#define MIN(a, b) (a) < (b) ? (a) : (b) + +#if defined(_WIN32) && !defined(__MINGW32__) + #define THREAD_LOCAL __declspec(thread) +#else + #define THREAD_LOCAL __thread +#endif diff --git a/extern/reproc/reproc/src/options.c b/extern/reproc/reproc/src/options.c new file mode 100644 index 000000000..5a94d81ab --- /dev/null +++ b/extern/reproc/reproc/src/options.c @@ -0,0 +1,137 @@ +#include "options.h" + +#include "error.h" + +static bool redirect_is_set(reproc_redirect redirect) +{ + return redirect.type || redirect.handle || redirect.file || redirect.path; +} + +static int parse_redirect(reproc_redirect *redirect, + REPROC_STREAM stream, + bool parent, + bool discard, + FILE *file, + const char *path) +{ + ASSERT(redirect); + + if (file) { + ASSERT_EINVAL(!redirect_is_set(*redirect)); + ASSERT_EINVAL(!parent && !discard && !path); + redirect->type = REPROC_REDIRECT_FILE; + redirect->file = file; + } + + if (path) { + ASSERT_EINVAL(!redirect_is_set(*redirect)); + ASSERT_EINVAL(!parent && !discard && !file); + redirect->type = REPROC_REDIRECT_PATH; + redirect->path = path; + } + + if (redirect->type == REPROC_REDIRECT_HANDLE || redirect->handle) { + ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || + redirect->type == REPROC_REDIRECT_HANDLE); + ASSERT_EINVAL(redirect->handle); + ASSERT_EINVAL(!redirect->file && !redirect->path); + redirect->type = REPROC_REDIRECT_HANDLE; + } + + if (redirect->type == REPROC_REDIRECT_FILE || redirect->file) { + ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || + redirect->type == REPROC_REDIRECT_FILE); + ASSERT_EINVAL(redirect->file); + ASSERT_EINVAL(!redirect->handle && !redirect->path); + redirect->type = REPROC_REDIRECT_FILE; + } + + if (redirect->type == REPROC_REDIRECT_PATH || redirect->path) { + ASSERT_EINVAL(redirect->type == REPROC_REDIRECT_DEFAULT || + redirect->type == REPROC_REDIRECT_PATH); + ASSERT_EINVAL(redirect->path); + ASSERT_EINVAL(!redirect->handle && !redirect->file); + redirect->type = REPROC_REDIRECT_PATH; + } + + if (redirect->type == REPROC_REDIRECT_DEFAULT) { + if (parent) { + ASSERT_EINVAL(!discard); + redirect->type = REPROC_REDIRECT_PARENT; + } else if (discard) { + ASSERT_EINVAL(!parent); + redirect->type = REPROC_REDIRECT_DISCARD; + } else { + redirect->type = stream == REPROC_STREAM_ERR ? REPROC_REDIRECT_PARENT + : REPROC_REDIRECT_PIPE; + } + } + + return 0; +} + +reproc_stop_actions parse_stop_actions(reproc_stop_actions stop) +{ + bool is_noop = stop.first.action == REPROC_STOP_NOOP && + stop.second.action == REPROC_STOP_NOOP && + stop.third.action == REPROC_STOP_NOOP; + + if (is_noop) { + stop.first.action = REPROC_STOP_WAIT; + stop.first.timeout = REPROC_DEADLINE; + stop.second.action = REPROC_STOP_TERMINATE; + stop.second.timeout = REPROC_INFINITE; + } + + return stop; +} + +int parse_options(reproc_options *options, const char *const *argv) +{ + ASSERT(options); + + int r = -1; + + r = parse_redirect(&options->redirect.in, REPROC_STREAM_IN, + options->redirect.parent, options->redirect.discard, NULL, + NULL); + if (r < 0) { + return r; + } + + r = parse_redirect(&options->redirect.out, REPROC_STREAM_OUT, + options->redirect.parent, options->redirect.discard, + options->redirect.file, options->redirect.path); + if (r < 0) { + return r; + } + + r = parse_redirect(&options->redirect.err, REPROC_STREAM_ERR, + options->redirect.parent, options->redirect.discard, + options->redirect.file, options->redirect.path); + if (r < 0) { + return r; + } + + if (options->input.data != NULL) { + ASSERT_EINVAL(options->redirect.in.type == REPROC_REDIRECT_PIPE); + } + + if (options->input.size > 0) { + ASSERT_EINVAL(options->input.data != NULL); + } + + if (options->fork) { + ASSERT_EINVAL(argv == NULL); + } else { + ASSERT_EINVAL(argv != NULL && argv[0] != NULL); + } + + if (options->deadline == 0) { + options->deadline = REPROC_INFINITE; + } + + options->stop = parse_stop_actions(options->stop); + + return 0; +} diff --git a/extern/reproc/reproc/src/options.h b/extern/reproc/reproc/src/options.h new file mode 100644 index 000000000..9e244e4e9 --- /dev/null +++ b/extern/reproc/reproc/src/options.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +reproc_stop_actions parse_stop_actions(reproc_stop_actions stop); + +int parse_options(reproc_options *options, const char *const *argv); diff --git a/extern/reproc/reproc/src/pipe.h b/extern/reproc/reproc/src/pipe.h new file mode 100644 index 000000000..6735e3a58 --- /dev/null +++ b/extern/reproc/reproc/src/pipe.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#ifdef _WIN64 +typedef uint64_t pipe_type; // `SOCKET` +#elif _WIN32 +typedef uint32_t pipe_type; // `SOCKET` +#else +typedef int pipe_type; // fd +#endif + +extern const pipe_type PIPE_INVALID; + +extern const short PIPE_EVENT_IN; +extern const short PIPE_EVENT_OUT; + +typedef struct { + pipe_type pipe; + short interests; + short events; +} pipe_event_source; + +// Creates a new anonymous pipe. `parent` and `child` are set to the parent and +// child endpoint of the pipe respectively. +int pipe_init(pipe_type *read, pipe_type *write); + +// Sets `pipe` to nonblocking mode. +int pipe_nonblocking(pipe_type pipe, bool enable); + +// Reads up to `size` bytes into `buffer` from the pipe indicated by `pipe` and +// returns the amount of bytes read. +int pipe_read(pipe_type pipe, uint8_t *buffer, size_t size); + +// Writes up to `size` bytes from `buffer` to the pipe indicated by `pipe` and +// returns the amount of bytes written. +int pipe_write(pipe_type pipe, const uint8_t *buffer, size_t size); + +// Polls the given event sources for events. +int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout); + +int pipe_shutdown(pipe_type pipe); + +pipe_type pipe_destroy(pipe_type pipe); diff --git a/extern/reproc/reproc/src/pipe.posix.c b/extern/reproc/reproc/src/pipe.posix.c new file mode 100644 index 000000000..f6d289ef4 --- /dev/null +++ b/extern/reproc/reproc/src/pipe.posix.c @@ -0,0 +1,141 @@ +#define _POSIX_C_SOURCE 200809L + +#include "pipe.h" + +#include +#include +#include +#include +#include +#include + +#include "error.h" +#include "handle.h" + +const int PIPE_INVALID = -1; + +const short PIPE_EVENT_IN = POLLIN; +const short PIPE_EVENT_OUT = POLLOUT; + +int pipe_init(int *read, int *write) +{ + ASSERT(read); + ASSERT(write); + + int pair[] = { PIPE_INVALID, PIPE_INVALID }; + int r = -1; + + r = pipe(pair); + if (r < 0) { + r = -errno; + goto finish; + } + + r = handle_cloexec(pair[0], true); + if (r < 0) { + goto finish; + } + + r = handle_cloexec(pair[1], true); + if (r < 0) { + goto finish; + } + + *read = pair[0]; + *write = pair[1]; + + pair[0] = PIPE_INVALID; + pair[1] = PIPE_INVALID; + +finish: + pipe_destroy(pair[0]); + pipe_destroy(pair[1]); + + return r; +} + +int pipe_nonblocking(int pipe, bool enable) +{ + int r = -1; + + r = fcntl(pipe, F_GETFL, 0); + if (r < 0) { + return -errno; + } + + r = enable ? r | O_NONBLOCK : r & ~O_NONBLOCK; + + r = fcntl(pipe, F_SETFL, r); + + return r < 0 ? -errno : 0; +} + +int pipe_read(int pipe, uint8_t *buffer, size_t size) +{ + ASSERT(pipe != PIPE_INVALID); + ASSERT(buffer); + + int r = (int) read(pipe, buffer, size); + + if (r == 0) { + // `read` returns 0 to indicate the other end of the pipe was closed. + return -EPIPE; + } + + return r < 0 ? -errno : r; +} + +int pipe_write(int pipe, const uint8_t *buffer, size_t size) +{ + ASSERT(pipe != PIPE_INVALID); + ASSERT(buffer); + + int r = (int) write(pipe, buffer, size); + + return r < 0 ? -errno : r; +} + +int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) +{ + ASSERT(num_sources <= INT_MAX); + + struct pollfd *pollfds = NULL; + int r = -1; + + pollfds = calloc(num_sources, sizeof(struct pollfd)); + if (pollfds == NULL) { + r = -errno; + goto finish; + } + + for (size_t i = 0; i < num_sources; i++) { + pollfds[i].fd = sources[i].pipe; + pollfds[i].events = sources[i].interests; + } + + r = poll(pollfds, (nfds_t) num_sources, timeout); + if (r < 0) { + r = -errno; + goto finish; + } + + for (size_t i = 0; i < num_sources; i++) { + sources[i].events = pollfds[i].revents; + } + +finish: + free(pollfds); + + return r; +} + +int pipe_shutdown(int pipe) +{ + (void) pipe; + return 0; +} + +int pipe_destroy(int pipe) +{ + return handle_destroy(pipe); +} diff --git a/extern/reproc/reproc/src/pipe.windows.c b/extern/reproc/reproc/src/pipe.windows.c new file mode 100644 index 000000000..bb355be31 --- /dev/null +++ b/extern/reproc/reproc/src/pipe.windows.c @@ -0,0 +1,261 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "pipe.h" + +#include +#include +#include +#include + +#include "error.h" +#include "handle.h" +#include "macro.h" + +const SOCKET PIPE_INVALID = INVALID_SOCKET; + +const short PIPE_EVENT_IN = POLLIN; +const short PIPE_EVENT_OUT = POLLOUT; + +// Inspired by https://gist.github.com/geertj/4325783. +static int socketpair(int domain, int type, int protocol, SOCKET *out) +{ + ASSERT(out); + + SOCKET server = PIPE_INVALID; + SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; + int r = -1; + + server = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); + if (server == INVALID_SOCKET) { + r = -WSAGetLastError(); + goto finish; + } + + SOCKADDR_IN localhost = { 0 }; + localhost.sin_family = AF_INET; + localhost.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK); + localhost.sin_port = 0; + + r = bind(server, (SOCKADDR *) &localhost, sizeof(localhost)); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + r = listen(server, 1); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + SOCKADDR_STORAGE name = { 0 }; + int size = sizeof(name); + r = getsockname(server, (SOCKADDR *) &name, &size); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + pair[0] = WSASocketW(domain, type, protocol, NULL, 0, 0); + if (pair[0] == INVALID_SOCKET) { + r = -WSAGetLastError(); + goto finish; + } + + struct { + WSAPROTOCOL_INFOW data; + int size; + } info = { { 0 }, sizeof(WSAPROTOCOL_INFOW) }; + + r = getsockopt(pair[0], SOL_SOCKET, SO_PROTOCOL_INFOW, (char *) &info.data, + &info.size); + if (r < 0) { + goto finish; + } + + // We require the returned sockets to be usable as Windows file handles. This + // might not be the case if extra LSP providers are installed. + + if (!(info.data.dwServiceFlags1 & XP1_IFS_HANDLES)) { + r = -ERROR_NOT_SUPPORTED; + goto finish; + } + + r = pipe_nonblocking(pair[0], true); + if (r < 0) { + goto finish; + } + + r = connect(pair[0], (SOCKADDR *) &name, size); + if (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK) { + r = -WSAGetLastError(); + goto finish; + } + + r = pipe_nonblocking(pair[0], false); + if (r < 0) { + goto finish; + } + + pair[1] = accept(server, NULL, NULL); + if (pair[1] == INVALID_SOCKET) { + r = -WSAGetLastError(); + goto finish; + } + + out[0] = pair[0]; + out[1] = pair[1]; + + pair[0] = PIPE_INVALID; + pair[1] = PIPE_INVALID; + +finish: + pipe_destroy(server); + pipe_destroy(pair[0]); + pipe_destroy(pair[1]); + + return r; +} + +int pipe_init(SOCKET *read, SOCKET *write) +{ + ASSERT(read); + ASSERT(write); + + SOCKET pair[] = { PIPE_INVALID, PIPE_INVALID }; + int r = -1; + + // Use sockets instead of pipes so we can use `WSAPoll` which only works with + // sockets. + r = socketpair(AF_INET, SOCK_STREAM, 0, pair); + if (r < 0) { + goto finish; + } + + r = SetHandleInformation((HANDLE) pair[0], HANDLE_FLAG_INHERIT, 0); + if (r == 0) { + r = -(int) GetLastError(); + goto finish; + } + + r = SetHandleInformation((HANDLE) pair[1], HANDLE_FLAG_INHERIT, 0); + if (r == 0) { + r = -(int) GetLastError(); + goto finish; + } + + // Make the connection unidirectional to better emulate a pipe. + + r = shutdown(pair[0], SD_SEND); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + r = shutdown(pair[1], SD_RECEIVE); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + *read = pair[0]; + *write = pair[1]; + + pair[0] = PIPE_INVALID; + pair[1] = PIPE_INVALID; + +finish: + pipe_destroy(pair[0]); + pipe_destroy(pair[1]); + + return r; +} + +int pipe_nonblocking(SOCKET pipe, bool enable) +{ + u_long mode = enable; + int r = ioctlsocket(pipe, (long) FIONBIO, &mode); + return r < 0 ? -WSAGetLastError() : 0; +} + +int pipe_read(SOCKET pipe, uint8_t *buffer, size_t size) +{ + ASSERT(pipe != PIPE_INVALID); + ASSERT(buffer); + ASSERT(size <= INT_MAX); + + int r = recv(pipe, (char *) buffer, (int) size, 0); + + if (r == 0) { + return -ERROR_BROKEN_PIPE; + } + + return r < 0 ? -WSAGetLastError() : r; +} + +int pipe_write(SOCKET pipe, const uint8_t *buffer, size_t size) +{ + ASSERT(pipe != PIPE_INVALID); + ASSERT(buffer); + ASSERT(size <= INT_MAX); + + int r = send(pipe, (const char *) buffer, (int) size, 0); + + return r < 0 ? -WSAGetLastError() : r; +} + +int pipe_poll(pipe_event_source *sources, size_t num_sources, int timeout) +{ + ASSERT(num_sources <= INT_MAX); + + WSAPOLLFD *pollfds = NULL; + int r = -1; + + pollfds = calloc(num_sources, sizeof(WSAPOLLFD)); + if (pollfds == NULL) { + r = -ERROR_NOT_ENOUGH_MEMORY; + goto finish; + } + + for (size_t i = 0; i < num_sources; i++) { + pollfds[i].fd = sources[i].pipe; + pollfds[i].events = sources[i].interests; + } + + r = WSAPoll(pollfds, (ULONG) num_sources, timeout); + if (r < 0) { + r = -WSAGetLastError(); + goto finish; + } + + for (size_t i = 0; i < num_sources; i++) { + sources[i].events = pollfds[i].revents; + } + +finish: + free(pollfds); + + return r; +} + +int pipe_shutdown(SOCKET pipe) +{ + if (pipe == PIPE_INVALID) { + return 0; + } + + int r = shutdown(pipe, SD_SEND); + return r < 0 ? -WSAGetLastError() : 0; +} + +SOCKET pipe_destroy(SOCKET pipe) +{ + if (pipe == PIPE_INVALID) { + return PIPE_INVALID; + } + + int r = closesocket(pipe); + ASSERT_UNUSED(r == 0); + + return PIPE_INVALID; +} diff --git a/extern/reproc/reproc/src/process.h b/extern/reproc/reproc/src/process.h new file mode 100644 index 000000000..b1455a786 --- /dev/null +++ b/extern/reproc/reproc/src/process.h @@ -0,0 +1,66 @@ +#pragma once + +#include "handle.h" + +#include + +#include + +#if defined(_WIN32) +typedef void *process_type; // `HANDLE` +#else +typedef int process_type; // `pid_t` +#endif + +extern const process_type PROCESS_INVALID; + +struct process_options { + // If `NULL`, the child process inherits the environment of the current + // process. + struct { + REPROC_ENV behavior; + const char *const *extra; + } env; + // If not `NULL`, the working directory of the child process is set to + // `working_directory`. + const char *working_directory; + // The standard streams of the child process are redirected to the `in`, `out` + // and `err` handles. If a handle is `HANDLE_INVALID`, the corresponding child + // process standard stream is closed. The `exit` handle is simply inherited by + // the child process. + struct { + handle_type in; + handle_type out; + handle_type err; + handle_type exit; + } handle; +}; + +// Spawns a child process that executes the command stored in `argv`. +// +// If `argv` is `NULL` on POSIX, `exec` is not called after fork and this +// function returns 0 in the child process and > 0 in the parent process. On +// Windows, if `argv` is `NULL`, an error is returned. +// +// The process handle of the new child process is assigned to `process`. +int process_start(process_type *process, + const char *const *argv, + struct process_options options); + +// Returns the process ID associated with the given handle. On posix systems the +// handle is the process ID and so its returned directly. On WIN32 the process +// ID is returned from GetProcessId on the pointer. +int process_pid(process_type process); + +// Returns the process's exit status if it has finished running. +int process_wait(process_type process); + +// Sends the `SIGTERM` (POSIX) or `CTRL-BREAK` (Windows) signal to the process +// indicated by `process`. +int process_terminate(process_type process); + +// Sends the `SIGKILL` signal to `process` (POSIX) or calls `TerminateProcess` +// on `process` (Windows). +int process_kill(process_type process); + +process_type process_destroy(process_type process); diff --git a/extern/reproc/reproc/src/process.posix.c b/extern/reproc/reproc/src/process.posix.c new file mode 100644 index 000000000..0f0fe0d51 --- /dev/null +++ b/extern/reproc/reproc/src/process.posix.c @@ -0,0 +1,499 @@ +#define _POSIX_C_SOURCE 200809L + +#include "process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "error.h" +#include "macro.h" +#include "pipe.h" +#include "strv.h" + +const pid_t PROCESS_INVALID = -1; + +static int signal_mask(int how, const sigset_t *newmask, sigset_t *oldmask) +{ + int r = -1; + +#if defined(REPROC_MULTITHREADED) + // `pthread_sigmask` returns positive errno values so we negate them. + r = -pthread_sigmask(how, newmask, oldmask); +#else + r = sigprocmask(how, newmask, oldmask); + r = r < 0 ? -errno : 0; +#endif + + return r; +} + +// Returns true if the NUL-terminated string indicated by `path` is a relative +// path. A path is relative if any character except the first is a forward slash +// ('/'). +static bool path_is_relative(const char *path) +{ + return strlen(path) > 0 && path[0] != '/' && strchr(path + 1, '/') != NULL; +} + +// Prepends the NUL-terminated string indicated by `path` with the current +// working directory. The caller is responsible for freeing the result of this +// function. If an error occurs, `NULL` is returned and `errno` is set to +// indicate the error. +static char *path_prepend_cwd(const char *path) +{ + ASSERT(path); + + size_t path_size = strlen(path); + size_t cwd_size = PATH_MAX; + + // We always allocate sufficient space for `path` but do not include this + // space in `cwd_size` so we can be sure that when `getcwd` succeeds there is + // sufficient space left in `cwd` to append `path`. + + // +2 reserves space to add a NUL terminator and potentially a missing '/' + // after the current working directory. + char *cwd = calloc(cwd_size + path_size + 2, sizeof(char)); + if (cwd == NULL) { + return cwd; + } + + while (getcwd(cwd, cwd_size) == NULL) { + if (errno != ERANGE) { + free(cwd); + return NULL; + } + + cwd_size += PATH_MAX; + + char *result = realloc(cwd, cwd_size + path_size + 1); + if (result == NULL) { + free(cwd); + return result; + } + + cwd = result; + } + + cwd_size = strlen(cwd); + + // Add a forward slash after `cwd` if there is none. + if (cwd[cwd_size - 1] != '/') { + cwd[cwd_size] = '/'; + cwd[cwd_size + 1] = '\0'; + cwd_size++; + } + + // We've made sure there's sufficient space left in `cwd` to add `path` and a + // NUL terminator. + memcpy(cwd + cwd_size, path, path_size); + cwd[cwd_size + path_size] = '\0'; + + return cwd; +} + +static const int MAX_FD_LIMIT = 1024 * 1024; + +static int get_max_fd(void) +{ + struct rlimit limit = { 0 }; + + int r = getrlimit(RLIMIT_NOFILE, &limit); + if (r < 0) { + return -errno; + } + + rlim_t soft = limit.rlim_cur; + + if (soft == RLIM_INFINITY || soft > INT_MAX) { + return INT_MAX; + } + + return (int) (soft - 1); +} + +static bool fd_in_set(int fd, const int *fd_set, size_t size) +{ + for (size_t i = 0; i < size; i++) { + if (fd == fd_set[i]) { + return true; + } + } + + return false; +} + +static pid_t process_fork(const int *except, size_t num_except) +{ + struct { + sigset_t old; + sigset_t new; + } mask; + + int r = -1; + + // We don't want signal handlers of the parent to run in the child process so + // we block all signals before forking. + + r = sigfillset(&mask.new); + if (r < 0) { + return -errno; + } + + r = signal_mask(SIG_SETMASK, &mask.new, &mask.old); + if (r < 0) { + return r; + } + + struct { + int read; + int write; + } pipe = { PIPE_INVALID, PIPE_INVALID }; + + r = pipe_init(&pipe.read, &pipe.write); + if (r < 0) { + return r; + } + + r = fork(); + if (r < 0) { + // `fork` error. + + r = -errno; // Save `errno`. + + int q = signal_mask(SIG_SETMASK, &mask.new, &mask.old); + ASSERT_UNUSED(q == 0); + + pipe_destroy(pipe.read); + pipe_destroy(pipe.write); + + return r; + } + + if (r > 0) { + // Parent process + + pid_t child = r; + + // From now on, the child process might have started so we don't report + // errors from `signal_mask` and `read`. This puts the responsibility + // for cleaning up the process in the hands of the caller. + + int q = signal_mask(SIG_SETMASK, &mask.old, &mask.old); + ASSERT_UNUSED(q == 0); + + // Close the error pipe write end on the parent's side so `read` will return + // when it is closed on the child side as well. + pipe_destroy(pipe.write); + + int child_errno = 0; + q = (int) read(pipe.read, &child_errno, sizeof(child_errno)); + ASSERT_UNUSED(q >= 0); + + if (child_errno > 0) { + // If the child writes to the error pipe and exits, we're certain the + // child process exited on its own and we can report errors as usual. + r = waitpid(child, NULL, 0); + ASSERT(r < 0 || r == child); + + r = r < 0 ? -errno : -child_errno; + } + + pipe_destroy(pipe.read); + + return r < 0 ? r : child; + } + + // Child process + + // Reset all signal handlers so they don't run in the child process. By + // default, a child process inherits the parent's signal handlers but we + // override this as most signal handlers won't be written in a way that they + // can deal with being run in a child process. + + struct sigaction action = { .sa_handler = SIG_DFL }; + + r = sigemptyset(&action.sa_mask); + if (r < 0) { + r = -errno; + goto finish; + } + + // NSIG is not standardized so we use a fixed limit instead. + for (int signal = 0; signal < 32; signal++) { + r = sigaction(signal, &action, NULL); + if (r < 0 && errno != EINVAL) { + r = -errno; + goto finish; + } + } + + // Reset the child's signal mask to the default signal mask. By default, a + // child process inherits the parent's signal mask (even over an `exec` call) + // but we override this as most processes won't be written in a way that they + // can deal with starting with a custom signal mask. + + r = sigemptyset(&mask.new); + if (r < 0) { + r = -errno; + goto finish; + } + + r = signal_mask(SIG_SETMASK, &mask.new, NULL); + if (r < 0) { + goto finish; + } + + // Not all file descriptors might have been created with the `FD_CLOEXEC` + // flag so we manually close all file descriptors to prevent file descriptors + // leaking into the child process. + + r = get_max_fd(); + if (r < 0) { + goto finish; + } + + int max_fd = r; + + if (max_fd > MAX_FD_LIMIT) { + // Refuse to try to close too many file descriptors. + r = -EMFILE; + goto finish; + } + + for (int i = 0; i < max_fd; i++) { + // Make sure we don't close the error pipe file descriptors twice. + if (i == pipe.read || i == pipe.write) { + continue; + } + + if (fd_in_set(i, except, num_except)) { + continue; + } + + // Check if `i` is a valid file descriptor before trying to close it. + r = fcntl(i, F_GETFD); + if (r >= 0) { + handle_destroy(i); + } + } + + r = 0; + +finish: + if (r < 0) { + (void) !write(pipe.write, &errno, sizeof(errno)); + _exit(EXIT_FAILURE); + } + + pipe_destroy(pipe.write); + pipe_destroy(pipe.read); + + return 0; +} + +int process_start(pid_t *process, + const char *const *argv, + struct process_options options) +{ + ASSERT(process); + + if (argv != NULL) { + ASSERT(argv[0] != NULL); + } + + struct { + int read; + int write; + } pipe = { PIPE_INVALID, PIPE_INVALID }; + char *program = NULL; + char **env = NULL; + int r = -1; + + // We create an error pipe to receive errors from the child process. + r = pipe_init(&pipe.read, &pipe.write); + if (r < 0) { + goto finish; + } + + if (argv != NULL) { + // We prepend the parent working directory to `program` if it is a + // relative path so that it will always be searched for relative to the + // parent working directory even after executing `chdir`. + program = options.working_directory && path_is_relative(argv[0]) + ? path_prepend_cwd(argv[0]) + : strdup(argv[0]); + if (program == NULL) { + r = -errno; + goto finish; + } + } + + extern char **environ; // NOLINT + char *const *parent = options.env.behavior == REPROC_ENV_EMPTY ? NULL + : environ; + env = strv_concat(parent, options.env.extra); + if (env == NULL) { + goto finish; + } + + int except[] = { options.handle.in, options.handle.out, options.handle.err, + pipe.read, pipe.write, options.handle.exit }; + + r = process_fork(except, ARRAY_SIZE(except)); + if (r < 0) { + goto finish; + } + + if (r == 0) { + // Redirect stdin, stdout and stderr. + + int redirect[] = { options.handle.in, options.handle.out, + options.handle.err }; + + for (int i = 0; i < (int) ARRAY_SIZE(redirect); i++) { + // `i` corresponds to the standard stream we need to redirect. + r = dup2(redirect[i], i); + if (r < 0) { + r = -errno; + goto child; + } + + // Make sure we don't accidentally cloexec the standard streams of the + // child process when we're inheriting the parent standard streams. If we + // don't call `exec`, the caller is responsible for closing the redirect + // and exit handles. + if (redirect[i] != i) { + // Make sure the pipe is closed when we call exec. + r = handle_cloexec(redirect[i], true); + if (r < 0) { + goto child; + } + } + } + + // Make sure the `exit` file descriptor is inherited. + + r = handle_cloexec(options.handle.exit, false); + if (r < 0) { + goto child; + } + + if (options.working_directory != NULL) { + r = chdir(options.working_directory); + if (r < 0) { + r = -errno; + goto child; + } + } + + // `environ` is carried over calls to `exec`. + environ = env; + + if (argv != NULL) { + ASSERT(program); + + r = execvp(program, (char *const *) argv); + if (r < 0) { + r = -errno; + goto child; + } + } + + env = NULL; + + child: + if (r < 0) { + (void) !write(pipe.write, &errno, sizeof(errno)); + _exit(EXIT_FAILURE); + } + + pipe_destroy(pipe.read); + pipe_destroy(pipe.write); + free(program); + strv_free(env); + + return 0; + } + + pid_t child = r; + + // Close the error pipe write end on the parent's side so `read` will return + // when it is closed on the child side as well. + pipe.write = pipe_destroy(pipe.write); + + int child_errno = 0; + r = (int) read(pipe.read, &child_errno, sizeof(child_errno)); + ASSERT_UNUSED(r >= 0); + + if (child_errno > 0) { + r = waitpid(child, NULL, 0); + r = r < 0 ? -errno : -child_errno; + goto finish; + } + + *process = child; + r = 0; + +finish: + pipe_destroy(pipe.read); + pipe_destroy(pipe.write); + free(program); + strv_free(env); + + return r < 0 ? r : 1; +} + +static int parse_status(int status) +{ + return WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status) + 128; +} + +int process_pid(process_type process) +{ + return process; +} + +int process_wait(pid_t process) +{ + ASSERT(process != PROCESS_INVALID); + + int status = 0; + int r = waitpid(process, &status, 0); + if (r < 0) { + return -errno; + } + + ASSERT(r == process); + + return parse_status(status); +} + +int process_terminate(pid_t process) +{ + ASSERT(process != PROCESS_INVALID); + + int r = kill(process, SIGTERM); + return r < 0 ? -errno : 0; +} + +int process_kill(pid_t process) +{ + ASSERT(process != PROCESS_INVALID); + + int r = kill(process, SIGKILL); + return r < 0 ? -errno : 0; +} + +pid_t process_destroy(pid_t process) +{ + // `waitpid` already cleans up the process for us. + (void) process; + return PROCESS_INVALID; +} diff --git a/extern/reproc/reproc/src/process.windows.c b/extern/reproc/reproc/src/process.windows.c new file mode 100644 index 000000000..666f3cb77 --- /dev/null +++ b/extern/reproc/reproc/src/process.windows.c @@ -0,0 +1,506 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "process.h" + +#include +#include +#include + +#include "error.h" +#include "macro.h" +#include "utf.h" + +const HANDLE PROCESS_INVALID = INVALID_HANDLE_VALUE; // NOLINT + +static const DWORD CREATION_FLAGS = + // Create each child process in a new process group so we don't send + // `CTRL-BREAK` signals to more than one child process in + // `process_terminate`. + CREATE_NEW_PROCESS_GROUP | + // Create each child process with a Unicode environment as we accept any + // UTF-16 encoded environment (including Unicode characters). Create each + CREATE_UNICODE_ENVIRONMENT | + // Create each child with an extended STARTUPINFOEXW structure so we can + // specify which handles should be inherited. + EXTENDED_STARTUPINFO_PRESENT; + +// Argument escaping implementation is based on the following blog post: +// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + +static bool argument_should_escape(const char *argument) +{ + ASSERT(argument); + + bool should_escape = false; + + for (size_t i = 0; i < strlen(argument); i++) { + should_escape = should_escape || argument[i] == ' ' || + argument[i] == '\t' || argument[i] == '\n' || + argument[i] == '\v' || argument[i] == '\"'; + } + + return should_escape; +} + +static size_t argument_escaped_size(const char *argument) +{ + ASSERT(argument); + + size_t argument_size = strlen(argument); + + if (!argument_should_escape(argument)) { + return argument_size; + } + + size_t size = 2; // double quotes + + for (size_t i = 0; i < argument_size; i++) { + size_t num_backslashes = 0; + + while (i < argument_size && argument[i] == '\\') { + i++; + num_backslashes++; + } + + if (i == argument_size) { + size += num_backslashes * 2; + } else if (argument[i] == '"') { + size += num_backslashes * 2 + 2; + } else { + size += num_backslashes + 1; + } + } + + return size; +} + +static size_t argument_escape(char *dest, const char *argument) +{ + ASSERT(dest); + ASSERT(argument); + + size_t argument_size = strlen(argument); + + if (!argument_should_escape(argument)) { + strcpy(dest, argument); // NOLINT + return argument_size; + } + + const char *begin = dest; + + *dest++ = '"'; + + for (size_t i = 0; i < argument_size; i++) { + size_t num_backslashes = 0; + + while (i < argument_size && argument[i] == '\\') { + i++; + num_backslashes++; + } + + if (i == argument_size) { + memset(dest, '\\', num_backslashes * 2); + dest += num_backslashes * 2; + } else if (argument[i] == '"') { + memset(dest, '\\', num_backslashes * 2 + 1); + dest += num_backslashes * 2 + 1; + *dest++ = '"'; + } else { + memset(dest, '\\', num_backslashes); + dest += num_backslashes; + *dest++ = argument[i]; + } + } + + *dest++ = '"'; + + return (size_t)(dest - begin); +} + +static char *argv_join(const char *const *argv) +{ + ASSERT(argv); + + // Determine the size of the concatenated string first. + size_t joined_size = 1; // Count the NUL terminator. + for (int i = 0; argv[i] != NULL; i++) { + joined_size += argument_escaped_size(argv[i]); + + if (argv[i + 1] != NULL) { + joined_size++; // Count whitespace. + } + } + + char *joined = calloc(joined_size, sizeof(char)); + if (joined == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + char *current = joined; + for (int i = 0; argv[i] != NULL; i++) { + current += argument_escape(current, argv[i]); + + // We add a space after each argument in the joined arguments string except + // for the final argument. + if (argv[i + 1] != NULL) { + *current++ = ' '; + } + } + + *current = '\0'; + + return joined; +} + +static size_t env_join_size(const char *const *env) +{ + ASSERT(env); + + size_t joined_size = 1; // Count the NUL terminator. + for (int i = 0; env[i] != NULL; i++) { + joined_size += strlen(env[i]) + 1; // Count the NUL terminator. + } + + return joined_size; +} + +static char *env_join(const char *const *env) +{ + ASSERT(env); + + char *joined = calloc(env_join_size(env), sizeof(char)); + if (joined == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + char *current = joined; + for (int i = 0; env[i] != NULL; i++) { + size_t to_copy = strlen(env[i]) + 1; // Include NUL terminator. + memcpy(current, env[i], to_copy); + current += to_copy; + } + + *current = '\0'; + + return joined; +} + +static const DWORD NUM_ATTRIBUTES = 1; + +static LPPROC_THREAD_ATTRIBUTE_LIST setup_attribute_list(HANDLE *handles, + size_t num_handles) +{ + ASSERT(handles); + + int r = -1; + + // Make sure all the given handles can be inherited. + for (size_t i = 0; i < num_handles; i++) { + r = SetHandleInformation(handles[i], HANDLE_FLAG_INHERIT, + HANDLE_FLAG_INHERIT); + if (r == 0) { + return NULL; + } + } + + // Get the required size for `attribute_list`. + SIZE_T attribute_list_size = 0; + r = InitializeProcThreadAttributeList(NULL, NUM_ATTRIBUTES, 0, + &attribute_list_size); + if (r == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return NULL; + } + + LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = malloc(attribute_list_size); + if (attribute_list == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + r = InitializeProcThreadAttributeList(attribute_list, NUM_ATTRIBUTES, 0, + &attribute_list_size); + if (r == 0) { + free(attribute_list); + return NULL; + } + + // Add the handles to be inherited to `attribute_list`. + r = UpdateProcThreadAttribute(attribute_list, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, + num_handles * sizeof(HANDLE), NULL, NULL); + if (r == 0) { + DeleteProcThreadAttributeList(attribute_list); + free(attribute_list); + return NULL; + } + + return attribute_list; +} + +#define NULSTR_FOREACH(i, l) \ + for ((i) = (l); (i) && *(i) != L'\0'; (i) = wcschr((i), L'\0') + 1) + +static wchar_t *env_concat(const wchar_t *a, const wchar_t *b) +{ + const wchar_t *i = NULL; + size_t size = 1; + wchar_t *c = NULL; + + NULSTR_FOREACH(i, a) { + size += wcslen(i) + 1; + } + + NULSTR_FOREACH(i, b) { + size += wcslen(i) + 1; + } + + wchar_t *r = calloc(size, sizeof(wchar_t)); + if (!r) { + return NULL; + } + + c = r; + + NULSTR_FOREACH(i, a) { + wcscpy(c, i); + c += wcslen(i) + 1; + } + + NULSTR_FOREACH(i, b) { + wcscpy(c, i); + c += wcslen(i) + 1; + } + + *c = L'\0'; + + return r; +} + +static wchar_t *env_setup(REPROC_ENV behavior, const char *const *extra) +{ + wchar_t *env_parent_wstring = NULL; + char *env_extra = NULL; + wchar_t *env_extra_wstring = NULL; + wchar_t *env_wstring = NULL; + + if (behavior == REPROC_ENV_EXTEND) { + env_parent_wstring = GetEnvironmentStringsW(); + } + + if (extra != NULL) { + env_extra = env_join(extra); + if (env_extra == NULL) { + goto finish; + } + + size_t joined_size = env_join_size(extra); + ASSERT(joined_size <= INT_MAX); + + env_extra_wstring = utf16_from_utf8(env_extra, (int) joined_size); + if (env_extra_wstring == NULL) { + goto finish; + } + } + + env_wstring = env_concat(env_parent_wstring, env_extra_wstring); + if (env_wstring == NULL) { + goto finish; + } + +finish: + FreeEnvironmentStringsW(env_parent_wstring); + free(env_extra); + free(env_extra_wstring); + + return env_wstring; +} + +int process_start(HANDLE *process, + const char *const *argv, + struct process_options options) +{ + ASSERT(process); + + if (argv == NULL) { + return -ERROR_CALL_NOT_IMPLEMENTED; + } + + ASSERT(argv[0] != NULL); + + char *command_line = NULL; + wchar_t *command_line_wstring = NULL; + wchar_t *env_wstring = NULL; + wchar_t *working_directory_wstring = NULL; + LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = NULL; + PROCESS_INFORMATION info = { PROCESS_INVALID, HANDLE_INVALID, 0, 0 }; + int r = -1; + + // Join `argv` to a whitespace delimited string as required by + // `CreateProcessW`. + command_line = argv_join(argv); + if (command_line == NULL) { + r = -(int) GetLastError(); + goto finish; + } + + // Convert UTF-8 to UTF-16 as required by `CreateProcessW`. + command_line_wstring = utf16_from_utf8(command_line, -1); + if (command_line_wstring == NULL) { + r = -(int) GetLastError(); + goto finish; + } + + // Idem for `working_directory` if it isn't `NULL`. + if (options.working_directory != NULL) { + working_directory_wstring = utf16_from_utf8(options.working_directory, -1); + if (working_directory_wstring == NULL) { + r = -(int) GetLastError(); + goto finish; + } + } + + env_wstring = env_setup(options.env.behavior, options.env.extra); + if (env_wstring == NULL) { + r = -(int) GetLastError(); + goto finish; + } + + // Windows Vista added the `STARTUPINFOEXW` structure in which we can put a + // list of handles that should be inherited. Only these handles are inherited + // by the child process. Other code in an application that calls + // `CreateProcess` without passing a `STARTUPINFOEXW` struct containing the + // handles it should inherit can still unintentionally inherit handles meant + // for a reproc child process. See https://stackoverflow.com/a/2345126 for + // more information. + HANDLE handles[] = { options.handle.exit, options.handle.in, + options.handle.out, options.handle.err }; + size_t num_handles = ARRAY_SIZE(handles); + + if (options.handle.out == options.handle.err) { + // CreateProcess doesn't like the same handle being specified twice in the + // `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute. + num_handles--; + } + + attribute_list = setup_attribute_list(handles, num_handles); + if (attribute_list == NULL) { + r = -(int) GetLastError(); + goto finish; + } + + STARTUPINFOEXW extended_startup_info = { + .StartupInfo = { .cb = sizeof(extended_startup_info), + .dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, + // `STARTF_USESTDHANDLES` + .hStdInput = options.handle.in, + .hStdOutput = options.handle.out, + .hStdError = options.handle.err, + // `STARTF_USESHOWWINDOW`. Make sure the console window of + // the child process isn't visible. See + // https://github.com/DaanDeMeyer/reproc/issues/6 and + // https://github.com/DaanDeMeyer/reproc/pull/7 for more + // information. + .wShowWindow = SW_HIDE }, + .lpAttributeList = attribute_list + }; + + LPSTARTUPINFOW startup_info_address = &extended_startup_info.StartupInfo; + + // Child processes inherit the error mode of their parents. To avoid child + // processes creating error dialogs we set our error mode to not create error + // dialogs temporarily which is inherited by the child process. + DWORD previous_error_mode = SetErrorMode(SEM_NOGPFAULTERRORBOX); + + SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), + .bInheritHandle = false, + .lpSecurityDescriptor = NULL }; + + r = CreateProcessW(NULL, command_line_wstring, &do_not_inherit, + &do_not_inherit, true, CREATION_FLAGS, env_wstring, + working_directory_wstring, startup_info_address, &info); + + SetErrorMode(previous_error_mode); + + if (r == 0) { + r = -(int) GetLastError(); + goto finish; + } + + *process = info.hProcess; + r = 0; + +finish: + free(command_line); + free(command_line_wstring); + free(env_wstring); + free(working_directory_wstring); + DeleteProcThreadAttributeList(attribute_list); + free(attribute_list); + handle_destroy(info.hThread); + + return r < 0 ? r : 1; +} + +int process_pid(process_type process) +{ + ASSERT(process); + return (int) GetProcessId(process); +} + +int process_wait(HANDLE process) +{ + ASSERT(process); + + int r = -1; + + r = (int) WaitForSingleObject(process, INFINITE); + if ((DWORD) r == WAIT_FAILED) { + return -(int) GetLastError(); + } + + DWORD status = 0; + r = GetExitCodeProcess(process, &status); + if (r == 0) { + return -(int) GetLastError(); + } + + // `GenerateConsoleCtrlEvent` causes a process to exit with this exit code. + // Because `GenerateConsoleCtrlEvent` has roughly the same semantics as + // `SIGTERM`, we map its exit code to `SIGTERM`. + if (status == 3221225786) { + status = (DWORD) REPROC_SIGTERM; + } + + return (int) status; +} + +int process_terminate(HANDLE process) +{ + ASSERT(process && process != PROCESS_INVALID); + + // `GenerateConsoleCtrlEvent` can only be called on a process group. To call + // `GenerateConsoleCtrlEvent` on a single child process it has to be put in + // its own process group (which we did when starting the child process). + BOOL r = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(process)); + + return r == 0 ? -(int) GetLastError() : 0; +} + +int process_kill(HANDLE process) +{ + ASSERT(process && process != PROCESS_INVALID); + + // We use 137 (`SIGKILL`) as the exit status because it is the same exit + // status as a process that is stopped with the `SIGKILL` signal on POSIX + // systems. + BOOL r = TerminateProcess(process, (DWORD) REPROC_SIGKILL); + + return r == 0 ? -(int) GetLastError() : 0; +} + +HANDLE process_destroy(HANDLE process) +{ + return handle_destroy(process); +} diff --git a/extern/reproc/reproc/src/redirect.c b/extern/reproc/reproc/src/redirect.c new file mode 100644 index 000000000..2c25d13e4 --- /dev/null +++ b/extern/reproc/reproc/src/redirect.c @@ -0,0 +1,164 @@ +#include "redirect.h" + +#include "error.h" + +static int redirect_pipe(pipe_type *parent, + handle_type *child, + REPROC_STREAM stream, + bool nonblocking) +{ + ASSERT(parent); + ASSERT(child); + + pipe_type pipe[] = { PIPE_INVALID, PIPE_INVALID }; + int r = -1; + + r = pipe_init(&pipe[0], &pipe[1]); + if (r < 0) { + goto finish; + } + + r = pipe_nonblocking(stream == REPROC_STREAM_IN ? pipe[1] : pipe[0], + nonblocking); + if (r < 0) { + goto finish; + } + + *parent = stream == REPROC_STREAM_IN ? pipe[1] : pipe[0]; + *child = stream == REPROC_STREAM_IN ? (handle_type) pipe[0] + : (handle_type) pipe[1]; + +finish: + if (r < 0) { + pipe_destroy(pipe[0]); + pipe_destroy(pipe[1]); + } + + return r; +} + +int redirect_init(pipe_type *parent, + handle_type *child, + REPROC_STREAM stream, + reproc_redirect redirect, + bool nonblocking, + handle_type out) +{ + ASSERT(parent); + ASSERT(child); + + int r = REPROC_EINVAL; + + switch (redirect.type) { + + case REPROC_REDIRECT_DEFAULT: + ASSERT(false); + break; + + case REPROC_REDIRECT_PIPE: + r = redirect_pipe(parent, child, stream, nonblocking); + break; + + case REPROC_REDIRECT_PARENT: + r = redirect_parent(child, stream); + if (r == REPROC_EPIPE) { + // Discard if the corresponding parent stream is closed. + r = redirect_discard(child, stream); + } + + if (r < 0) { + break; + } + + *parent = PIPE_INVALID; + + break; + + case REPROC_REDIRECT_DISCARD: + r = redirect_discard(child, stream); + if (r < 0) { + break; + } + + *parent = PIPE_INVALID; + + break; + + case REPROC_REDIRECT_HANDLE: + ASSERT(redirect.handle); + + r = 0; + + *child = redirect.handle; + *parent = PIPE_INVALID; + + break; + + case REPROC_REDIRECT_FILE: + ASSERT(redirect.file); + + r = redirect_file(child, redirect.file); + if (r < 0) { + break; + } + + *parent = PIPE_INVALID; + + break; + + case REPROC_REDIRECT_STDOUT: + ASSERT(stream == REPROC_STREAM_ERR); + ASSERT(out != HANDLE_INVALID); + + r = 0; + + *child = out; + *parent = PIPE_INVALID; + + break; + + case REPROC_REDIRECT_PATH: + ASSERT(redirect.path); + + r = redirect_path(child, stream, redirect.path); + if (r < 0) { + break; + } + + *parent = PIPE_INVALID; + + break; + } + + return r; +} + +handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type) +{ + if (child == HANDLE_INVALID) { + return HANDLE_INVALID; + } + + switch (type) { + case REPROC_REDIRECT_DEFAULT: + ASSERT(false); + break; + case REPROC_REDIRECT_PIPE: + // We know `handle` is a pipe if `REDIRECT_PIPE` is used so the cast is + // safe. This little hack prevents us from having to introduce a generic + // handle type. + pipe_destroy((pipe_type) child); + break; + case REPROC_REDIRECT_DISCARD: + case REPROC_REDIRECT_PATH: + handle_destroy(child); + break; + case REPROC_REDIRECT_PARENT: + case REPROC_REDIRECT_FILE: + case REPROC_REDIRECT_HANDLE: + case REPROC_REDIRECT_STDOUT: + break; + } + + return HANDLE_INVALID; +} diff --git a/extern/reproc/reproc/src/redirect.h b/extern/reproc/reproc/src/redirect.h new file mode 100644 index 000000000..4cee19cd7 --- /dev/null +++ b/extern/reproc/reproc/src/redirect.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "handle.h" +#include "pipe.h" + +int redirect_init(pipe_type *parent, + handle_type *child, + REPROC_STREAM stream, + reproc_redirect redirect, + bool nonblocking, + handle_type out); + +handle_type redirect_destroy(handle_type child, REPROC_REDIRECT type); + +// Internal prototypes + +int redirect_parent(handle_type *child, REPROC_STREAM stream); + +int redirect_discard(handle_type *child, REPROC_STREAM stream); + +int redirect_file(handle_type *child, FILE *file); + +int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path); diff --git a/extern/reproc/reproc/src/redirect.posix.c b/extern/reproc/reproc/src/redirect.posix.c new file mode 100644 index 000000000..943fe9eb7 --- /dev/null +++ b/extern/reproc/reproc/src/redirect.posix.c @@ -0,0 +1,79 @@ +#define _POSIX_C_SOURCE 200809L + +#include "redirect.h" + +#include +#include +#include + +#include "error.h" +#include "pipe.h" + +static FILE *stream_to_file(REPROC_STREAM stream) +{ + switch (stream) { + case REPROC_STREAM_IN: + return stdin; + case REPROC_STREAM_OUT: + return stdout; + case REPROC_STREAM_ERR: + return stderr; + } + + return NULL; +} + +int redirect_parent(int *child, REPROC_STREAM stream) +{ + ASSERT(child); + + FILE *file = stream_to_file(stream); + if (file == NULL) { + return -EINVAL; + } + + int r = fileno(file); + if (r < 0) { + return errno == EBADF ? -EPIPE : -errno; + } + + *child = r; // `r` contains the duplicated file descriptor. + + return 0; +} + +int redirect_discard(int *child, REPROC_STREAM stream) +{ + return redirect_path(child, stream, "/dev/null"); +} + +int redirect_file(int *child, FILE *file) +{ + ASSERT(child); + + int r = fileno(file); + if (r < 0) { + return -errno; + } + + *child = r; + + return 0; +} + +int redirect_path(int *child, REPROC_STREAM stream, const char *path) +{ + ASSERT(child); + ASSERT(path); + + int mode = stream == REPROC_STREAM_IN ? O_RDONLY : O_WRONLY; + + int r = open(path, mode | O_CREAT | O_CLOEXEC, 0640); + if (r < 0) { + return -errno; + } + + *child = r; + + return 0; +} diff --git a/extern/reproc/reproc/src/redirect.windows.c b/extern/reproc/reproc/src/redirect.windows.c new file mode 100644 index 000000000..c634145e4 --- /dev/null +++ b/extern/reproc/reproc/src/redirect.windows.c @@ -0,0 +1,113 @@ +#define _WIN32_WINNT _WIN32_WINNT_VISTA + +#include "redirect.h" + +#include +#include +#include + +#include "error.h" +#include "pipe.h" +#include "utf.h" + +static DWORD stream_to_id(REPROC_STREAM stream) +{ + switch (stream) { + case REPROC_STREAM_IN: + return STD_INPUT_HANDLE; + case REPROC_STREAM_OUT: + return STD_OUTPUT_HANDLE; + case REPROC_STREAM_ERR: + return STD_ERROR_HANDLE; + } + + return 0; +} + +int redirect_parent(HANDLE *child, REPROC_STREAM stream) +{ + ASSERT(child); + + DWORD id = stream_to_id(stream); + if (id == 0) { + return -ERROR_INVALID_PARAMETER; + } + + HANDLE *handle = GetStdHandle(id); + if (handle == INVALID_HANDLE_VALUE) { + return -(int) GetLastError(); + } + + if (handle == NULL) { + return -ERROR_BROKEN_PIPE; + } + + *child = handle; + + return 0; +} + +enum { FILE_NO_TEMPLATE = 0 }; + +int redirect_discard(HANDLE *child, REPROC_STREAM stream) +{ + return redirect_path(child, stream, "NUL"); +} + +int redirect_file(HANDLE *child, FILE *file) +{ + ASSERT(child); + ASSERT(file); + + int r = _fileno(file); + if (r < 0) { + return -ERROR_INVALID_HANDLE; + } + + intptr_t result = _get_osfhandle(r); + if (result == -1) { + return -ERROR_INVALID_HANDLE; + } + + *child = (HANDLE) result; + + return 0; +} + +int redirect_path(handle_type *child, REPROC_STREAM stream, const char *path) +{ + ASSERT(child); + ASSERT(path); + + DWORD mode = stream == REPROC_STREAM_IN ? GENERIC_READ : GENERIC_WRITE; + HANDLE handle = HANDLE_INVALID; + int r = -1; + + wchar_t *wpath = utf16_from_utf8(path, -1); + if (wpath == NULL) { + r = -(int) GetLastError(); + goto finish; + } + + SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES), + .bInheritHandle = false, + .lpSecurityDescriptor = NULL }; + + handle = CreateFileW(wpath, mode, FILE_SHARE_READ | FILE_SHARE_WRITE, + &do_not_inherit, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, + (HANDLE) FILE_NO_TEMPLATE); + if (handle == INVALID_HANDLE_VALUE) { + r = -(int) GetLastError(); + goto finish; + } + + *child = handle; + handle = HANDLE_INVALID; + r = 0; + +finish: + free(wpath); + handle_destroy(handle); + + return r; +} diff --git a/extern/reproc/reproc/src/reproc.c b/extern/reproc/reproc/src/reproc.c new file mode 100644 index 000000000..4104717d3 --- /dev/null +++ b/extern/reproc/reproc/src/reproc.c @@ -0,0 +1,695 @@ +#include + +#include + +#include "clock.h" +#include "error.h" +#include "handle.h" +#include "init.h" +#include "macro.h" +#include "options.h" +#include "pipe.h" +#include "process.h" +#include "redirect.h" + +struct reproc_t { + process_type handle; + + struct { + pipe_type in; + pipe_type out; + pipe_type err; + pipe_type exit; + } pipe; + + int status; + reproc_stop_actions stop; + int64_t deadline; + bool nonblocking; + + struct { + pipe_type out; + pipe_type err; + } child; +}; + +enum { + STATUS_NOT_STARTED = -1, + STATUS_IN_PROGRESS = -2, + STATUS_IN_CHILD = -3, +}; + +#define SIGOFFSET 128 + +const int REPROC_SIGKILL = SIGOFFSET + 9; +const int REPROC_SIGTERM = SIGOFFSET + 15; + +const int REPROC_INFINITE = -1; +const int REPROC_DEADLINE = -2; + +static int setup_input(pipe_type *pipe, const uint8_t *data, size_t size) +{ + if (data == NULL) { + ASSERT(size == 0); + return 0; + } + + ASSERT(pipe && *pipe != PIPE_INVALID); + + // `reproc_write` only needs the child process stdin pipe to be initialized. + size_t written = 0; + int r = -1; + + // Make sure we don't block indefinitely when `input` is bigger than the + // size of the pipe. + r = pipe_nonblocking(*pipe, true); + if (r < 0) { + return r; + } + + while (written < size) { + r = pipe_write(*pipe, data + written, size - written); + if (r < 0) { + return r; + } + + ASSERT(written + (size_t) r <= size); + written += (size_t) r; + } + + *pipe = pipe_destroy(*pipe); + + return 0; +} + +static int expiry(int timeout, int64_t deadline) +{ + if (timeout == REPROC_INFINITE && deadline == REPROC_INFINITE) { + return REPROC_INFINITE; + } + + if (deadline == REPROC_INFINITE) { + return timeout; + } + + int64_t n = now(); + + if (n >= deadline) { + return REPROC_DEADLINE; + } + + // `deadline` exceeds `now` by at most a full `int` so the cast is safe. + int remaining = (int) (deadline - n); + + if (timeout == REPROC_INFINITE) { + return remaining; + } + + return MIN(timeout, remaining); +} + +static size_t find_earliest_deadline(reproc_event_source *sources, + size_t num_sources) +{ + ASSERT(sources); + ASSERT(num_sources > 0); + + size_t earliest = 0; + int min = REPROC_INFINITE; + + for (size_t i = 0; i < num_sources; i++) { + reproc_t *process = sources[i].process; + + if (process == NULL) { + continue; + } + + int current = expiry(REPROC_INFINITE, process->deadline); + + if (current == REPROC_DEADLINE) { + return i; + } + + if (min == REPROC_INFINITE || current < min) { + earliest = i; + min = current; + } + } + + return earliest; +} + +reproc_t *reproc_new(void) +{ + reproc_t *process = malloc(sizeof(reproc_t)); + if (process == NULL) { + return NULL; + } + + *process = (reproc_t){ .handle = PROCESS_INVALID, + .pipe = { .in = PIPE_INVALID, + .out = PIPE_INVALID, + .err = PIPE_INVALID, + .exit = PIPE_INVALID }, + .child = { .out = PIPE_INVALID, .err = PIPE_INVALID }, + .status = STATUS_NOT_STARTED, + .deadline = REPROC_INFINITE }; + + return process; +} + +int reproc_start(reproc_t *process, + const char *const *argv, + reproc_options options) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status == STATUS_NOT_STARTED); + + struct { + handle_type in; + handle_type out; + handle_type err; + pipe_type exit; + } child = { HANDLE_INVALID, HANDLE_INVALID, HANDLE_INVALID, PIPE_INVALID }; + int r = -1; + + r = init(); + if (r < 0) { + return r; // Make sure we can always call `deinit` in `finish`. + } + + r = parse_options(&options, argv); + if (r < 0) { + goto finish; + } + + r = redirect_init(&process->pipe.in, &child.in, REPROC_STREAM_IN, + options.redirect.in, options.nonblocking, HANDLE_INVALID); + if (r < 0) { + goto finish; + } + + r = redirect_init(&process->pipe.out, &child.out, REPROC_STREAM_OUT, + options.redirect.out, options.nonblocking, HANDLE_INVALID); + if (r < 0) { + goto finish; + } + + r = redirect_init(&process->pipe.err, &child.err, REPROC_STREAM_ERR, + options.redirect.err, options.nonblocking, child.out); + if (r < 0) { + goto finish; + } + + r = pipe_init(&process->pipe.exit, &child.exit); + if (r < 0) { + goto finish; + } + + r = setup_input(&process->pipe.in, options.input.data, options.input.size); + if (r < 0) { + goto finish; + } + + struct process_options process_options = { + .env = { .behavior = options.env.behavior, .extra = options.env.extra }, + .working_directory = options.working_directory, + .handle = { .in = child.in, + .out = child.out, + .err = child.err, + .exit = (handle_type) child.exit } + }; + + r = process_start(&process->handle, argv, process_options); + if (r < 0) { + goto finish; + } + + if (r > 0) { + process->stop = options.stop; + + if (options.deadline != REPROC_INFINITE) { + process->deadline = now() + options.deadline; + } + + process->nonblocking = options.nonblocking; + } + +finish: + // Either an error has ocurred or the child pipe endpoints have been copied to + // the stdin/stdout/stderr streams of the child process. Either way, they can + // be safely closed. + redirect_destroy(child.in, options.redirect.in.type); + + // See `reproc_poll` for why we do this. + +#ifdef _WIN32 + if (r < 0 || options.redirect.out.type != REPROC_REDIRECT_PIPE) { + child.out = redirect_destroy(child.out, options.redirect.out.type); + } + + if (r < 0 || options.redirect.err.type != REPROC_REDIRECT_PIPE) { + child.err = redirect_destroy(child.err, options.redirect.err.type); + } +#else + child.out = redirect_destroy(child.out, options.redirect.out.type); + child.err = redirect_destroy(child.err, options.redirect.err.type); +#endif + + pipe_destroy(child.exit); + + if (r < 0) { + process->handle = process_destroy(process->handle); + process->pipe.in = pipe_destroy(process->pipe.in); + process->pipe.out = pipe_destroy(process->pipe.out); + process->pipe.err = pipe_destroy(process->pipe.err); + process->pipe.exit = pipe_destroy(process->pipe.exit); + deinit(); + } else if (r == 0) { + process->handle = PROCESS_INVALID; + // `process_start` has already taken care of closing the handles for us. + process->pipe.in = PIPE_INVALID; + process->pipe.out = PIPE_INVALID; + process->pipe.err = PIPE_INVALID; + process->pipe.exit = PIPE_INVALID; + process->status = STATUS_IN_CHILD; + } else { + process->child.out = (pipe_type) child.out; + process->child.err = (pipe_type) child.err; + process->status = STATUS_IN_PROGRESS; + } + + return r; +} + +enum { PIPES_PER_SOURCE = 4 }; + +static bool contains_valid_pipe(pipe_event_source *sources, size_t num_sources) +{ + for (size_t i = 0; i < num_sources; i++) { + if (sources[i].pipe != PIPE_INVALID) { + return true; + } + } + + return false; +} + +int reproc_poll(reproc_event_source *sources, size_t num_sources, int timeout) +{ + ASSERT_EINVAL(sources); + ASSERT_EINVAL(num_sources > 0); + + size_t earliest = find_earliest_deadline(sources, num_sources); + int64_t deadline = sources[earliest].process == NULL + ? REPROC_INFINITE + : sources[earliest].process->deadline; + + int first = expiry(timeout, deadline); + size_t num_pipes = num_sources * PIPES_PER_SOURCE; + int r = REPROC_ENOMEM; + + if (first == REPROC_DEADLINE) { + for (size_t i = 0; i < num_sources; i++) { + sources[i].events = 0; + } + + sources[earliest].events = REPROC_EVENT_DEADLINE; + return 1; + } + + pipe_event_source *pipes = calloc(num_pipes, sizeof(pipe_event_source)); + if (pipes == NULL) { + return r; + } + + for (size_t i = 0; i < num_pipes; i++) { + pipes[i].pipe = PIPE_INVALID; + } + + for (size_t i = 0; i < num_sources; i++) { + size_t j = i * PIPES_PER_SOURCE; + reproc_t *process = sources[i].process; + int interests = sources[i].interests; + + if (process == NULL) { + continue; + } + + bool in = interests & REPROC_EVENT_IN; + pipes[j + 0].pipe = in ? process->pipe.in : PIPE_INVALID; + pipes[j + 0].interests = PIPE_EVENT_OUT; + + bool out = interests & REPROC_EVENT_OUT; + pipes[j + 1].pipe = out ? process->pipe.out : PIPE_INVALID; + pipes[j + 1].interests = PIPE_EVENT_IN; + + bool err = interests & REPROC_EVENT_ERR; + pipes[j + 2].pipe = err ? process->pipe.err : PIPE_INVALID; + pipes[j + 2].interests = PIPE_EVENT_IN; + + bool exit = (interests & REPROC_EVENT_EXIT) || + (interests & REPROC_EVENT_OUT && + process->child.out != PIPE_INVALID) || + (interests & REPROC_EVENT_ERR && + process->child.err != PIPE_INVALID); + pipes[j + 3].pipe = exit ? process->pipe.exit : PIPE_INVALID; + pipes[j + 3].interests = PIPE_EVENT_IN; + } + + if (!contains_valid_pipe(pipes, num_pipes)) { + r = REPROC_EPIPE; + goto finish; + } + + r = pipe_poll(pipes, num_pipes, first); + if (r < 0) { + goto finish; + } + + for (size_t i = 0; i < num_sources; i++) { + sources[i].events = 0; + } + + if (r == 0 && first != timeout) { + // Differentiate between timeout and deadline expiry. Deadline expiry is an + // event, timeouts are not. + sources[earliest].events = REPROC_EVENT_DEADLINE; + r = 1; + } else if (r > 0) { + // Convert pipe events to process events. + for (size_t i = 0; i < num_pipes; i++) { + if (pipes[i].pipe == PIPE_INVALID) { + continue; + } + + if (pipes[i].events > 0) { + // Index in a set of pipes determines the process pipe and thus the + // process event. + // 0 = stdin pipe => REPROC_EVENT_IN + // 1 = stdout pipe => REPROC_EVENT_OUT + // ... + int event = 1 << (i % PIPES_PER_SOURCE); + sources[i / PIPES_PER_SOURCE].events |= event; + } + } + + r = 0; + + // Count the number of processes with events. + for (size_t i = 0; i < num_sources; i++) { + r += sources[i].events > 0; + } + + // On Windows, when redirecting to sockets, we keep the child handles alive + // in the parent process (see `reproc_start`). We do this because Windows + // doesn't correctly flush redirected socket handles when a child process + // exits. This can lead to data loss where the parent process doesn't + // receive all output of the child process. To get around this, we keep an + // extra handle open in the parent process which we close correctly when we + // detect the child process has exited. Detecting whether a child process + // has exited happens via another inherited socket, but here there's no + // danger of data loss because no data is received over this socket. + + bool again = false; + + for (size_t i = 0; i < num_sources; i++) { + if (!(sources[i].events & REPROC_EVENT_EXIT)) { + continue; + } + + reproc_t *process = sources[i].process; + + if (process->child.out == PIPE_INVALID && + process->child.err == PIPE_INVALID) { + continue; + } + + r = pipe_shutdown(process->child.out); + if (r < 0) { + goto finish; + } + + r = pipe_shutdown(process->child.err); + if (r < 0) { + goto finish; + } + + process->child.out = pipe_destroy(process->child.out); + process->child.err = pipe_destroy(process->child.err); + again = true; + } + + // If we've closed handles, we poll again so we can include any new close + // events that occurred because we closed handles. + + if (again) { + r = reproc_poll(sources, num_sources, timeout); + if (r < 0) { + goto finish; + } + } + } + +finish: + free(pipes); + + return r; +} + +int reproc_read(reproc_t *process, + REPROC_STREAM stream, + uint8_t *buffer, + size_t size) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(stream == REPROC_STREAM_OUT || stream == REPROC_STREAM_ERR); + ASSERT_EINVAL(buffer); + + pipe_type *pipe = stream == REPROC_STREAM_OUT ? &process->pipe.out + : &process->pipe.err; + pipe_type child = stream == REPROC_STREAM_OUT ? process->child.out + : process->child.err; + int r = -1; + + if (*pipe == PIPE_INVALID) { + return REPROC_EPIPE; + } + + // If we've kept extra handles open in the parent, make sure we use + // `reproc_poll` which closes the extra handles we keep open when the child + // process exits. If we don't, `pipe_read` will block forever because the + // extra handles we keep open in the parent would never be closed. + if (child != PIPE_INVALID) { + int event = stream == REPROC_STREAM_OUT ? REPROC_EVENT_OUT + : REPROC_EVENT_ERR; + reproc_event_source source = { process, event, 0 }; + r = reproc_poll(&source, 1, process->nonblocking ? 0 : REPROC_INFINITE); + if (r <= 0) { + return r == 0 ? REPROC_EWOULDBLOCK : r; + } + } + + r = pipe_read(*pipe, buffer, size); + + if (r == REPROC_EPIPE) { + *pipe = pipe_destroy(*pipe); + } + + return r; +} + +int reproc_write(reproc_t *process, const uint8_t *buffer, size_t size) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + + if (buffer == NULL) { + // Allow `NULL` buffers but only if `size == 0`. + ASSERT_EINVAL(size == 0); + return 0; + } + + if (process->pipe.in == PIPE_INVALID) { + return REPROC_EPIPE; + } + + int r = pipe_write(process->pipe.in, buffer, size); + + if (r == REPROC_EPIPE) { + process->pipe.in = pipe_destroy(process->pipe.in); + } + + return r; +} + +int reproc_close(reproc_t *process, REPROC_STREAM stream) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + + switch (stream) { + case REPROC_STREAM_IN: + process->pipe.in = pipe_destroy(process->pipe.in); + return 0; + case REPROC_STREAM_OUT: + process->pipe.out = pipe_destroy(process->pipe.out); + return 0; + case REPROC_STREAM_ERR: + process->pipe.err = pipe_destroy(process->pipe.err); + return 0; + } + + return REPROC_EINVAL; +} + +int reproc_wait(reproc_t *process, int timeout) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); + + int r = -1; + + if (process->status >= 0) { + return process->status; + } + + if (timeout == REPROC_DEADLINE) { + timeout = expiry(REPROC_INFINITE, process->deadline); + // If the deadline has expired, `expiry` returns `REPROC_DEADLINE` which + // means we'll only check if the process is still running. + if (timeout == REPROC_DEADLINE) { + timeout = 0; + } + } + + ASSERT(process->pipe.exit != PIPE_INVALID); + + pipe_event_source source = { .pipe = process->pipe.exit, + .interests = PIPE_EVENT_IN }; + + r = pipe_poll(&source, 1, timeout); + if (r <= 0) { + return r == 0 ? REPROC_ETIMEDOUT : r; + } + + r = process_wait(process->handle); + if (r < 0) { + return r; + } + + process->pipe.exit = pipe_destroy(process->pipe.exit); + + return process->status = r; +} + +int reproc_terminate(reproc_t *process) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); + + if (process->status >= 0) { + return 0; + } + + return process_terminate(process->handle); +} + +int reproc_kill(reproc_t *process) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); + + if (process->status >= 0) { + return 0; + } + + return process_kill(process->handle); +} + +int reproc_stop(reproc_t *process, reproc_stop_actions stop) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); + + stop = parse_stop_actions(stop); + + reproc_stop_action actions[] = { stop.first, stop.second, stop.third }; + int r = -1; + + for (size_t i = 0; i < ARRAY_SIZE(actions); i++) { + r = REPROC_EINVAL; // NOLINT + + switch (actions[i].action) { + case REPROC_STOP_NOOP: + r = 0; + continue; + case REPROC_STOP_WAIT: + r = 0; + break; + case REPROC_STOP_TERMINATE: + r = reproc_terminate(process); + break; + case REPROC_STOP_KILL: + r = reproc_kill(process); + break; + } + + // Stop if `reproc_terminate` or `reproc_kill` fail. + if (r < 0) { + break; + } + + r = reproc_wait(process, actions[i].timeout); + if (r != REPROC_ETIMEDOUT) { + break; + } + } + + return r; +} + +int reproc_pid(reproc_t *process) +{ + ASSERT_EINVAL(process); + ASSERT_EINVAL(process->status != STATUS_IN_CHILD); + ASSERT_EINVAL(process->status != STATUS_NOT_STARTED); + + return process_pid(process->handle); +} + +reproc_t *reproc_destroy(reproc_t *process) +{ + ASSERT_RETURN(process, NULL); + + if (process->status == STATUS_IN_PROGRESS) { + reproc_stop(process, process->stop); + } + + process_destroy(process->handle); + pipe_destroy(process->pipe.in); + pipe_destroy(process->pipe.out); + pipe_destroy(process->pipe.err); + pipe_destroy(process->pipe.exit); + + pipe_destroy(process->child.out); + pipe_destroy(process->child.err); + + if (process->status != STATUS_NOT_STARTED) { + deinit(); + } + + free(process); + + return NULL; +} + +const char *reproc_strerror(int error) +{ + return error_string(error); +} diff --git a/extern/reproc/reproc/src/run.c b/extern/reproc/reproc/src/run.c new file mode 100644 index 000000000..fbdc3c41d --- /dev/null +++ b/extern/reproc/reproc/src/run.c @@ -0,0 +1,54 @@ +#include + +#include + +#include "error.h" + +int reproc_run(const char *const *argv, reproc_options options) +{ + if (!options.redirect.discard && !options.redirect.file && + !options.redirect.path) { + options.redirect.parent = true; + } + + return reproc_run_ex(argv, options, REPROC_SINK_NULL, REPROC_SINK_NULL); +} + +int reproc_run_ex(const char *const *argv, + reproc_options options, // lgtm [cpp/large-parameter] + reproc_sink out, + reproc_sink err) +{ + reproc_t *process = NULL; + int r = REPROC_ENOMEM; + + // There's no way for `reproc_run_ex` to inform the caller whether we're in + // the forked process or the parent process so let's not allow forking when + // using `reproc_run_ex`. + ASSERT_EINVAL(!options.fork); + + process = reproc_new(); + if (process == NULL) { + goto finish; + } + + r = reproc_start(process, argv, options); + if (r < 0) { + goto finish; + } + + r = reproc_drain(process, out, err); + if (r < 0) { + goto finish; + } + + r = reproc_stop(process, options.stop); + if (r < 0) { + goto finish; + } + +finish: + reproc_destroy(process); + + return r; +} diff --git a/extern/reproc/reproc/src/strv.c b/extern/reproc/reproc/src/strv.c new file mode 100644 index 000000000..210b4d4c5 --- /dev/null +++ b/extern/reproc/reproc/src/strv.c @@ -0,0 +1,88 @@ +#include "strv.h" + +#include +#include +#include + +#include "error.h" + +static char *str_dup(const char *s) +{ + ASSERT_RETURN(s, NULL); + + char *r = malloc(strlen(s) + 1); + if (!r) { + return NULL; + } + + strcpy(r, s); // NOLINT + + return r; +} + +char **strv_concat(char *const *a, const char *const *b) +{ + char *const *i = NULL; + const char *const *j = NULL; + size_t size = 1; + size_t c = 0; + + STRV_FOREACH(i, a) { + size++; + } + + STRV_FOREACH(j, b) { + size++; + } + + char **r = calloc(size, sizeof(char *)); + if (!r) { + goto finish; + } + + STRV_FOREACH(i, a) { + r[c] = str_dup(*i); + if (!r[c]) { + goto finish; + } + + c++; + } + + STRV_FOREACH(j, b) { + r[c] = str_dup(*j); + if (!r[c]) { + goto finish; + } + + c++; + } + + r[c++] = NULL; + +finish: + if (c < size) { + STRV_FOREACH(i, r) { + free(*i); + } + + free(r); + + return NULL; + } + + return r; +} + +char **strv_free(char **l) +{ + char **s = NULL; + + STRV_FOREACH(s, l) { + free(*s); + } + + free(l); + + return NULL; +} diff --git a/extern/reproc/reproc/src/strv.h b/extern/reproc/reproc/src/strv.h new file mode 100644 index 000000000..d5b273821 --- /dev/null +++ b/extern/reproc/reproc/src/strv.h @@ -0,0 +1,7 @@ +#pragma once + +#define STRV_FOREACH(s, l) for ((s) = (l); (s) && *(s); (s)++) + +char **strv_concat(char *const *a, const char *const *b); + +char **strv_free(char **l); diff --git a/extern/reproc/reproc/src/utf.h b/extern/reproc/reproc/src/utf.h new file mode 100644 index 000000000..d6a96b395 --- /dev/null +++ b/extern/reproc/reproc/src/utf.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +// `size` represents the entire size of `string`, including NUL-terminators. We +// take the entire size because strings like the environment string passed to +// CreateProcessW includes multiple NUL-terminators so we can't always rely on +// `strlen` to calculate the string length for us. See the lpEnvironment +// documentation of CreateProcessW: +// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw +// Pass -1 as the size to have `utf16_from_utf8` calculate the size until (and +// including) the first NUL terminator. +wchar_t *utf16_from_utf8(const char *string, int size); diff --git a/extern/reproc/reproc/src/utf.posix.c b/extern/reproc/reproc/src/utf.posix.c new file mode 100644 index 000000000..cdb71a394 --- /dev/null +++ b/extern/reproc/reproc/src/utf.posix.c @@ -0,0 +1,3 @@ +#include "utf.h" + +// `utf16_from_utf8` is Windows-only. diff --git a/extern/reproc/reproc/src/utf.windows.c b/extern/reproc/reproc/src/utf.windows.c new file mode 100644 index 000000000..92f1e08dd --- /dev/null +++ b/extern/reproc/reproc/src/utf.windows.c @@ -0,0 +1,39 @@ +#include "utf.h" + +#include +#include +#include + +#include "error.h" + +wchar_t *utf16_from_utf8(const char *string, int size) +{ + ASSERT(string); + + // Determine wstring size (`MultiByteToWideChar` returns the required size if + // its last two arguments are `NULL` and 0). + int r = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string, size, NULL, + 0); + if (r == 0) { + return NULL; + } + + // `MultiByteToWideChar` does not return negative values so the cast to + // `size_t` is safe. + wchar_t *wstring = calloc((size_t) r, sizeof(wchar_t)); + if (wstring == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return NULL; + } + + // Now we pass our allocated string and its size as the last two arguments + // instead of `NULL` and 0 which makes `MultiByteToWideChar` actually perform + // the conversion. + r = MultiByteToWideChar(CP_UTF8, 0, string, size, wstring, r); + if (r == 0) { + free(wstring); + return NULL; + } + + return wstring; +} diff --git a/extern/reproc/reproc/test/argv.c b/extern/reproc/reproc/test/argv.c new file mode 100644 index 000000000..0c383c0ac --- /dev/null +++ b/extern/reproc/reproc/test/argv.c @@ -0,0 +1,33 @@ +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/argv", "\"argument 1\"", + "\"argument 2\"", NULL }; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink); + ASSERT_OK(r); + ASSERT(output != NULL); + + const char *current = output; + + for (size_t i = 0; i < 3; i++) { + size_t size = strlen(argv[i]); + + ASSERT_GE_SIZE(strlen(current), size); + ASSERT_EQ_MEM(current, argv[i], size); + + current += size; + current += *current == '\r'; + current += *current == '\n'; + } + + ASSERT_EQ_SIZE(strlen(current), (size_t) 0); + + reproc_free(output); +} diff --git a/extern/reproc/reproc/test/assert.h b/extern/reproc/reproc/test/assert.h new file mode 100644 index 000000000..ea0d0a9c8 --- /dev/null +++ b/extern/reproc/reproc/test/assert.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#define ASSERT(expression) ASSERT_MSG(expression, "%s", "") +#define ASSERT_OK(r) ASSERT_MSG(r >= 0, "%s", reproc_strerror(r)) + +#define ASSERT_EQ_MEM(left, right, size) \ + ASSERT_MSG(memcmp(left, right, size) == 0, "\"%.*s\" == \"%.*s\"", \ + (int) size, left, (int) size, right) + +#define ASSERT_EQ_STR(left, right) \ + ASSERT_MSG(strcmp(left, right) == 0, "%s == %s", left, right) + +#define ASSERT_GE_SIZE(left, right) \ + ASSERT_MSG(left >= right, "%zu >= %zu", left, right) + +#define ASSERT_EQ_SIZE(left, right) \ + ASSERT_MSG(left == right, "%zu == %zu", left, right) + +#define ASSERT_EQ_INT(left, right) \ + ASSERT_MSG(left == right, "%i == %i", left, right) + +#ifdef _WIN32 + #define ABORT() exit(EXIT_FAILURE) +#else + // Use `abort` so we get a coredump. + #define ABORT() abort() +#endif + +#define ASSERT_MSG(expression, format, ...) \ + do { \ + if (!(expression)) { \ + fprintf(stderr, "%s:%u: Assertion '%s' (" format ") failed", __FILE__, \ + __LINE__, #expression, __VA_ARGS__); \ + \ + fflush(stderr); \ + \ + ABORT(); \ + } \ + } while (0) diff --git a/extern/reproc/reproc/test/deadline.c b/extern/reproc/reproc/test/deadline.c new file mode 100644 index 000000000..017cced62 --- /dev/null +++ b/extern/reproc/reproc/test/deadline.c @@ -0,0 +1,10 @@ +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/deadline", NULL }; + int r = reproc_run(argv, (reproc_options){ .deadline = 100 }); + ASSERT(r == REPROC_SIGTERM); +} diff --git a/extern/reproc/reproc/test/env.c b/extern/reproc/reproc/test/env.c new file mode 100644 index 000000000..74a508dca --- /dev/null +++ b/extern/reproc/reproc/test/env.c @@ -0,0 +1,36 @@ +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/env", NULL }; + const char *envp[] = { "IP=127.0.0.1", "PORT=8080", NULL }; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + r = reproc_run_ex(argv, + (reproc_options){ .env.behavior = REPROC_ENV_EMPTY, + .env.extra = envp }, + sink, sink); + ASSERT_OK(r); + ASSERT(output != NULL); + + const char *current = output; + + for (size_t i = 0; i < 2; i++) { + size_t size = strlen(envp[i]); + + ASSERT_GE_SIZE(strlen(current), size); + ASSERT_EQ_MEM(current, envp[i], size); + + current += size; + current += *current == '\r'; + current += *current == '\n'; + } + + ASSERT_EQ_SIZE(strlen(current), (size_t) 0); + + reproc_free(output); +} diff --git a/extern/reproc/reproc/test/fork.c b/extern/reproc/reproc/test/fork.c new file mode 100644 index 000000000..33a8ba4c9 --- /dev/null +++ b/extern/reproc/reproc/test/fork.c @@ -0,0 +1,39 @@ +#include +#include +#include + +#include + +#include "assert.h" + +int main(void) +{ + reproc_t *process = reproc_new(); + const char *MESSAGE = "reproc stands for REdirected PROCess!"; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + r = reproc_start(process, NULL, (reproc_options){ .fork = true }); + + if (r == 0) { + printf("%s", MESSAGE); + fclose(stdout); // `_exit` doesn't flush stdout. + _exit(EXIT_SUCCESS); + } + + ASSERT_OK(r); + + r = reproc_drain(process, sink, sink); + ASSERT_OK(r); + + ASSERT(output != NULL); + ASSERT_EQ_STR(output, MESSAGE); + + r = reproc_wait(process, REPROC_INFINITE); + ASSERT_OK(r); + + reproc_destroy(process); + + reproc_free(output); +} diff --git a/extern/reproc/reproc/test/io.c b/extern/reproc/reproc/test/io.c new file mode 100644 index 000000000..e65af9c11 --- /dev/null +++ b/extern/reproc/reproc/test/io.c @@ -0,0 +1,74 @@ +#include +#include + +#include "assert.h" + +#define MESSAGE "reproc stands for REdirected PROCess" + +static void io() +{ + int r = -1; + + reproc_t *process = reproc_new(); + ASSERT(process); + + const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL }; + + r = reproc_start(process, argv, + (reproc_options){ + .redirect.err.type = REPROC_REDIRECT_STDOUT }); + ASSERT_OK(r); + + r = reproc_write(process, (uint8_t *) MESSAGE, strlen(MESSAGE)); + ASSERT_OK(r); + ASSERT_EQ_INT(r, (int) strlen(MESSAGE)); + + r = reproc_close(process, REPROC_STREAM_IN); + ASSERT_OK(r); + + char *out = NULL; + r = reproc_drain(process, reproc_sink_string(&out), REPROC_SINK_NULL); + ASSERT_OK(r); + + ASSERT(out != NULL); + ASSERT_EQ_STR(out, MESSAGE MESSAGE); + + r = reproc_wait(process, REPROC_INFINITE); + ASSERT_OK(r); + + reproc_destroy(process); + + reproc_free(out); +} + +static void timeout(void) +{ + int r = -1; + + reproc_t *process = reproc_new(); + ASSERT(process); + + const char *argv[] = { RESOURCE_DIRECTORY "/io", NULL }; + + r = reproc_start(process, argv, (reproc_options){ 0 }); + ASSERT_OK(r); + + reproc_event_source source = { process, REPROC_EVENT_OUT | REPROC_EVENT_ERR, + 0 }; + r = reproc_poll(&source, 1, 200); + ASSERT(r == 0); + + r = reproc_close(process, REPROC_STREAM_IN); + ASSERT_OK(r); + + r = reproc_poll(&source, 1, 200); + ASSERT_OK(r); + + reproc_destroy(process); +} + +int main(void) +{ + io(); + timeout(); +} diff --git a/extern/reproc/reproc/test/overflow.c b/extern/reproc/reproc/test/overflow.c new file mode 100644 index 000000000..a93f9432d --- /dev/null +++ b/extern/reproc/reproc/test/overflow.c @@ -0,0 +1,17 @@ +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/overflow", NULL }; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + r = reproc_run_ex(argv, (reproc_options){ 0 }, sink, sink); + ASSERT_OK(r); + ASSERT(output != NULL); + + reproc_free(output); +} diff --git a/extern/reproc/reproc/test/path.c b/extern/reproc/reproc/test/path.c new file mode 100644 index 000000000..23319cfd4 --- /dev/null +++ b/extern/reproc/reproc/test/path.c @@ -0,0 +1,41 @@ +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/path", NULL }; + int r = -1; + + r = reproc_run(argv, (reproc_options){ .redirect.path = "path.txt" }); + ASSERT_OK(r); + + FILE *file = fopen("path.txt", "rb"); + ASSERT(file != NULL); + + r = fseek(file, 0, SEEK_END); + ASSERT_OK(r); + + r = (int) ftell(file); + ASSERT_OK(r); + + size_t size = (size_t) r; + char *string = malloc(size + 1); + ASSERT(string != NULL); + + rewind(file); + r = (int) fread(string, sizeof(char), size, file); + ASSERT_EQ_INT(r, (int) size); + + string[r] = '\0'; + + r = fclose(file); + ASSERT_OK(r); + + r = remove("path.txt"); + ASSERT_OK(r); + + ASSERT_EQ_STR(string, argv[0]); + + free(string); +} diff --git a/extern/reproc/reproc/test/pid.c b/extern/reproc/reproc/test/pid.c new file mode 100644 index 000000000..cabc519a3 --- /dev/null +++ b/extern/reproc/reproc/test/pid.c @@ -0,0 +1,37 @@ +#include +#include + +#include +#include + +#include "assert.h" + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/pid", NULL }; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + reproc_t *process = reproc_new(); + ASSERT(process); + + ASSERT(reproc_pid(process) == REPROC_EINVAL); + + r = reproc_start(process, argv, (reproc_options){ 0 }); + ASSERT_OK(r); + + r = reproc_drain(process, sink, sink); + ASSERT_OK(r); + ASSERT(output != NULL); + + ASSERT(reproc_pid(process) == strtol(output, NULL, 10)); + + r = reproc_wait(process, REPROC_INFINITE); + ASSERT_OK(r); + + ASSERT(reproc_pid(process) == strtol(output, NULL, 10)); + + reproc_destroy(process); + reproc_free(output); +} diff --git a/extern/reproc/reproc/test/stop.c b/extern/reproc/reproc/test/stop.c new file mode 100644 index 000000000..cbf744408 --- /dev/null +++ b/extern/reproc/reproc/test/stop.c @@ -0,0 +1,32 @@ +#include + +#include "assert.h" + +static void stop(REPROC_STOP action, int status) +{ + int r = -1; + + reproc_t *process = reproc_new(); + ASSERT(process); + + const char *argv[] = { RESOURCE_DIRECTORY "/stop", NULL }; + + r = reproc_start(process, argv, (reproc_options){ 0 }); + ASSERT_OK(r); + + r = reproc_wait(process, 50); + ASSERT(r == REPROC_ETIMEDOUT); + + reproc_stop_actions stop = { .first = { action, 500 } }; + + r = reproc_stop(process, stop); + ASSERT_EQ_INT(r, status); + + reproc_destroy(process); +} + +int main(void) +{ + stop(REPROC_STOP_TERMINATE, REPROC_SIGTERM); + stop(REPROC_STOP_KILL, REPROC_SIGKILL); +} diff --git a/extern/reproc/reproc/test/working-directory.c b/extern/reproc/reproc/test/working-directory.c new file mode 100644 index 000000000..55515f46f --- /dev/null +++ b/extern/reproc/reproc/test/working-directory.c @@ -0,0 +1,29 @@ +#include + +#include "assert.h" + +static void replace(char *string, char old, char new) +{ + for (size_t i = 0; i < strlen(string); i++) { + string[i] = (char) (string[i] == old ? new : string[i]); + } +} + +int main(void) +{ + const char *argv[] = { RESOURCE_DIRECTORY "/working-directory", NULL }; + char *output = NULL; + reproc_sink sink = reproc_sink_string(&output); + int r = -1; + + r = reproc_run_ex(argv, + (reproc_options){ .working_directory = RESOURCE_DIRECTORY }, + sink, sink); + ASSERT_OK(r); + ASSERT(output != NULL); + + replace(output, '\\', '/'); + ASSERT_EQ_STR(output, RESOURCE_DIRECTORY); + + reproc_free(output); +} 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/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/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index 774a6dbc9..1440f0107 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -51,10 +51,6 @@ utility::JsonTree *find_node_matching_string_field( } 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; } @@ -375,7 +371,9 @@ void GlobalUIModelData::set_data( utility::JsonTree *node = find_node_matching_string_field( &(models_[model_name]->data_), attr_uuid_role_name, to_string(attribute_uuid)); - + if (!node) { + throw std::runtime_error("Failed to find expected field"); + } auto &j = node->data(); bool changed = false; @@ -470,8 +468,13 @@ void GlobalUIModelData::insert_attribute_data_into_model( utility::JsonTree *parent_node = &(models_[model_name]->data_); try { - parent_node = find_node_matching_string_field( + 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(); @@ -793,6 +796,9 @@ 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 ([[maybe_unused]] std::exception &e) { // exception is thrown if we fail to find a match diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index b54c28c39..d6dbe9ce4 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -199,6 +199,7 @@ 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()); @@ -207,7 +208,7 @@ BookmarkModel::getJSONFuture(const QModelIndex &index, const QString &path) cons bookmark_actor_, json_store::get_json_atom_v, addr, - StdFromQString(path)); + path_string); return QStringFromStd(result.dump()); diff --git a/src/ui/qml/bookmark/src/export.h b/src/ui/qml/bookmark/src/export.h new file mode 100644 index 000000000..cf2dd4ade --- /dev/null +++ b/src/ui/qml/bookmark/src/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/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/embedded_python_ui.cpp b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp index 29afd2696..54a8a54e2 100644 --- a/src/ui/qml/embedded_python/src/embedded_python_ui.cpp +++ b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp @@ -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()); 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/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/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index cfbc4940e..d5c1ac272 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -52,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); } 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/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 440c67a4c..9093a8b6c 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -53,9 +53,10 @@ 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; anon_send( central_models_data_actor_, @@ -119,7 +120,7 @@ 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) { -#ifdef _WIN32 +#ifdef false //_WIN32 emit dataChanged( idx, idx, @@ -188,7 +189,8 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r .toJson(QJsonDocument::Compact) .constData()); } else if (std::string(value.typeName()) == "QString") { - j = nlohmann::json::parse(StdFromQString(value.toString())); + std::string value_string = StdFromQString(value.toString()); + j = nlohmann::json::parse(value_string); } else { j = nlohmann::json::parse(QJsonDocument::fromVariant(value) .toJson(QJsonDocument::Compact) @@ -582,10 +584,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_); } } @@ -658,11 +661,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_) { @@ -674,9 +683,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/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/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/quickfuture/src/export.h b/src/ui/qml/quickfuture/src/export.h new file mode 100644 index 000000000..bdf00b1bc --- /dev/null +++ b/src/ui/qml/quickfuture/src/export.h @@ -0,0 +1,42 @@ + +#ifndef QUICKFUTURE_QML_EXPORT_H +#define QUICKFUTURE_QML_EXPORT_H + +#ifdef QUICKFUTURE_QML_STATIC_DEFINE +# define QUICKFUTURE_QML_EXPORT +# define QUICKFUTURE_QML_NO_EXPORT +#else +# ifndef QUICKFUTURE_QML_EXPORT +# ifdef quickfuture_qml_EXPORTS + /* We are building this library */ +# define QUICKFUTURE_QML_EXPORT __declspec(dllexport) +# else + /* We are using this library */ +# define QUICKFUTURE_QML_EXPORT __declspec(dllimport) +# endif +# endif + +# ifndef QUICKFUTURE_QML_NO_EXPORT +# define QUICKFUTURE_QML_NO_EXPORT +# endif +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED +# define QUICKFUTURE_QML_DEPRECATED __declspec(deprecated) +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED_EXPORT +# define QUICKFUTURE_QML_DEPRECATED_EXPORT QUICKFUTURE_QML_EXPORT QUICKFUTURE_QML_DEPRECATED +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED_NO_EXPORT +# define QUICKFUTURE_QML_DEPRECATED_NO_EXPORT QUICKFUTURE_QML_NO_EXPORT QUICKFUTURE_QML_DEPRECATED +#endif + +#if 0 /* DEFINE_NO_DEPRECATED */ +# ifndef QUICKFUTURE_QML_NO_DEPRECATED +# define QUICKFUTURE_QML_NO_DEPRECATED +# endif +#endif + +#endif /* QUICKFUTURE_QML_EXPORT_H */ diff --git a/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h b/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h new file mode 100644 index 000000000..bdf00b1bc --- /dev/null +++ b/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h @@ -0,0 +1,42 @@ + +#ifndef QUICKFUTURE_QML_EXPORT_H +#define QUICKFUTURE_QML_EXPORT_H + +#ifdef QUICKFUTURE_QML_STATIC_DEFINE +# define QUICKFUTURE_QML_EXPORT +# define QUICKFUTURE_QML_NO_EXPORT +#else +# ifndef QUICKFUTURE_QML_EXPORT +# ifdef quickfuture_qml_EXPORTS + /* We are building this library */ +# define QUICKFUTURE_QML_EXPORT __declspec(dllexport) +# else + /* We are using this library */ +# define QUICKFUTURE_QML_EXPORT __declspec(dllimport) +# endif +# endif + +# ifndef QUICKFUTURE_QML_NO_EXPORT +# define QUICKFUTURE_QML_NO_EXPORT +# endif +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED +# define QUICKFUTURE_QML_DEPRECATED __declspec(deprecated) +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED_EXPORT +# define QUICKFUTURE_QML_DEPRECATED_EXPORT QUICKFUTURE_QML_EXPORT QUICKFUTURE_QML_DEPRECATED +#endif + +#ifndef QUICKFUTURE_QML_DEPRECATED_NO_EXPORT +# define QUICKFUTURE_QML_DEPRECATED_NO_EXPORT QUICKFUTURE_QML_NO_EXPORT QUICKFUTURE_QML_DEPRECATED +#endif + +#if 0 /* DEFINE_NO_DEPRECATED */ +# ifndef QUICKFUTURE_QML_NO_DEPRECATED +# define QUICKFUTURE_QML_NO_DEPRECATED +# endif +#endif + +#endif /* QUICKFUTURE_QML_EXPORT_H */ 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_methods_ui.cpp b/src/ui/qml/session/src/session_model_methods_ui.cpp index 76fa2b31c..33645fe1e 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -706,7 +706,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... @@ -1122,18 +1123,19 @@ 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)); + 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()); } 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/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/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/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 41d003bfc..41f401258 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -240,14 +240,23 @@ std::string xstudio::utility::uri_to_posix_path(const caf::uri &uri) { } #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 } + // Now, replace forward slashes with backslashes std::replace(path.begin(), path.end(), '/', '\\'); + */ #endif return path; } diff --git a/src/utility/src/json_store.cpp b/src/utility/src/json_store.cpp index 8ebe62527..9b57fdd7d 100644 --- a/src/utility/src/json_store.cpp +++ b/src/utility/src/json_store.cpp @@ -11,7 +11,9 @@ JsonStore::JsonStore(nlohmann::json json) : nlohmann::json(std::move(json)) {} // JsonStore::JsonStore(const JsonStore &other) : json_(other) {} -nlohmann::json JsonStore::get(const std::string &path) const { return at(json_pointer(path)); } +nlohmann::json JsonStore::get(const std::string &path) const { + return at(json_pointer(path)); +} void JsonStore::set(const nlohmann::json &json, const std::string &path) { (*this)[json_pointer(path)] = json; diff --git a/vcpkg b/vcpkg new file mode 160000 index 000000000..a1212c93c --- /dev/null +++ b/vcpkg @@ -0,0 +1 @@ +Subproject commit a1212c93cabaa9c5c36c1ffdb4bddd59fdf31e43 From 847f5bdd49d1218ee02c3192dd70b2293f1ae0cd Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 28 May 2024 16:04:55 -0700 Subject: [PATCH 18/42] In-Progress Audio Signed-off-by: Michael Kessler --- CMakeLists.txt | 19 +++- CMakePresets.json | 62 ++++++++++++- cmake/macros.cmake | 3 +- include/xstudio/audio/audio_output_actor.hpp | 8 +- include/xstudio/audio/audio_output_device.hpp | 2 +- .../audio}/windows_audio_output_device.hpp | 3 +- .../embedded_python/embedded_python.hpp | 2 +- src/audio/src/audio_output.cpp | 67 +++----------- src/audio/src/audio_output_actor.cpp | 2 +- src/audio/src/windows_audio_output_device.cpp | 87 +++++++++---------- src/embedded_python/src/embedded_python.cpp | 2 +- src/global/src/global_actor.cpp | 5 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 37 +------- vcpkg | 1 - vcpkg.json | 10 ++- 15 files changed, 151 insertions(+), 159 deletions(-) rename {src/audio/src => include/xstudio/audio}/windows_audio_output_device.hpp (97%) delete mode 160000 vcpkg diff --git a/CMakeLists.txt b/CMakeLists.txt index bc38b1550..1ffc40f8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ option(OPTIMIZE_FOR_NATIVE "Build with -march=native" OFF) option(BUILD_RESKIN "Build xstudio reskin binary" ON) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") set(STUDIO_PLUGINS "" CACHE STRING "Enable compilation of SITE plugins") @@ -99,10 +100,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") @@ -156,6 +161,7 @@ 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 @@ -186,6 +192,11 @@ if(USE_VCPKG) list(APPEND CMAKE_PREFIX_PATH "${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows") # enable UUID System Generator add_definitions(-DUUID_SYSTEM_GENERATOR=ON) + + # 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) + # build quickpromise add_subdirectory("extern/quickpromise") add_subdirectory("extern/quickfuture") diff --git a/CMakePresets.json b/CMakePresets.json index b689bc75e..d08436f48 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -3,13 +3,67 @@ "configurePresets": [ { "name": "windows", - "generator": "Visual Studio 16 2019", + "generator": "Visual Studio 17 2022", "binaryDir": "${sourceDir}/build", "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", - "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "_____FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", + "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", + "BUILD_DOCS": "OFF", + "CMAKE_BUILD_PARALLEL_LEVEL": "64", + "CMAKE_AUTOGEN_PARALLEL": "64" + } + }, + { + "name": "windows-ninja (Release)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", + "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", + "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", + "BUILD_DOCS": "OFF", + "CMAKE_BUILD_PARALLEL_LEVEL": "64", + "CMAKE_AUTOGEN_PARALLEL": "64", + "INSTALL_PARALLEL": "ON" + } + }, + { + "name": "windows-ninja (RelWithDebInfo)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "USE_SANITIZER": "address", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", + "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", + "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", + "BUILD_DOCS": "OFF", + "CMAKE_BUILD_PARALLEL_LEVEL": "64", + "CMAKE_AUTOGEN_PARALLEL": "64", + "INSTALL_PARALLEL": "ON", + "PARALLEL_LEVEL": "64" + } + }, + { + "name": "windows-ninja (Debug)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_SANITIZER": "address", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", + "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", + "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", "BUILD_DOCS": "OFF", "CMAKE_BUILD_PARALLEL_LEVEL": "64", diff --git a/cmake/macros.cmake b/cmake/macros.cmake index ccf68ef06..4406b4fb3 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -2,7 +2,8 @@ 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 $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index 6746bb5c4..0f25e3980 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -21,8 +21,8 @@ class AudioOutputDeviceActor : public caf::event_based_actor { waiting_for_samples_(false), audio_samples_actor_(samples_actor) { - spdlog::debug("Created {} {}", "AudioOutputDeviceActor", OutputClassType::name()); - utility::print_on_exit(this, OutputClassType::name()); + //spdlog::info("Created {} {}", "AudioOutputDeviceActor", OutputClassType::name()); + //utility::print_on_exit(this, OutputClassType::name()); try { auto prefs = global_store::GlobalStoreHelper(system()); @@ -102,7 +102,7 @@ class AudioOutputDeviceActor : public caf::event_based_actor { try { output_device_ = std::make_unique(prefs); } catch (std::exception &e) { - spdlog::debug( + spdlog::error( "{} Failed to connect to an audio device: {}", __PRETTY_FUNCTION__, e.what()); } } @@ -187,7 +187,7 @@ class GlobalAudioOutputActor : public caf::event_based_actor, module::Module { template void AudioOutputActor::init() { - spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); + //spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); utility::print_on_exit(this, "AudioOutputControlActor"); audio_output_device_ = spawn>(caf::actor_cast(this)); diff --git a/include/xstudio/audio/audio_output_device.hpp b/include/xstudio/audio/audio_output_device.hpp index 42c36970d..d19f4b50f 100644 --- a/include/xstudio/audio/audio_output_device.hpp +++ b/include/xstudio/audio/audio_output_device.hpp @@ -59,7 +59,7 @@ class AudioOutputDevice { * block while the soundcard consumes samples, depending on the implementation of * the subclass. */ - virtual void push_samples(const void *sample_data, const long num_samples, int channel_count) = 0; + virtual void push_samples(const void *sample_data, const long num_samples) = 0; /** * @brief Query the audio pipeline delay from the last sample in the soundcard diff --git a/src/audio/src/windows_audio_output_device.hpp b/include/xstudio/audio/windows_audio_output_device.hpp similarity index 97% rename from src/audio/src/windows_audio_output_device.hpp rename to include/xstudio/audio/windows_audio_output_device.hpp index b9dd6513e..06393e6f7 100644 --- a/src/audio/src/windows_audio_output_device.hpp +++ b/include/xstudio/audio/windows_audio_output_device.hpp @@ -29,7 +29,7 @@ namespace audio { long desired_samples() override; - void push_samples(const void *sample_data, const long num_samples, int channel_count) override; + void push_samples(const void *sample_data, const long num_samples) override; long latency_microseconds() override; @@ -46,6 +46,7 @@ namespace audio { SampleFormat sample_format_ = {SampleFormat::INT16}; CComPtr audio_client_; CComPtr render_client_; + const utility::JsonStore config_; const utility::JsonStore prefs_; 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/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index 9fd006667..2b765439a 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -129,25 +129,15 @@ void AudioOutputControl::prepare_samples_for_soundcard( const int num_channels, const int sample_rate) { - spdlog::info("Preparing samples for soundcard."); - try { v.resize(num_samps_to_push * num_channels); - spdlog::info("[xstudio] Resized vector 'v' to size: {}", v.size()); memset(v.data(), 0, v.size() * sizeof(int16_t)); - spdlog::info( - "[xstudio] Set all values in 'v' to 0 using memset for a total size of {} bytes.", - v.size() * sizeof(int16_t)); int16_t *d = v.data(); long n = num_samps_to_push; long num_samps_pushed = 0; - spdlog::info( - "[xstudio] Set 'd' pointer to v.data(), 'n' to {}, and 'num_samps_pushed' to {}", - n, - num_samps_pushed); if (muted()) @@ -157,8 +147,6 @@ void AudioOutputControl::prepare_samples_for_soundcard( if (!current_buf_ && sample_data_.size()) { - spdlog::info("Next buffer for playback is being selected."); - // when is the next sample that we copy into the buffer going to get played? // We assume there are 'samples_in_soundcard_buffer + num_samps_pushed' audio // samples already either in our soundcard buffer or that are already in our @@ -171,7 +159,6 @@ void AudioOutputControl::prepare_samples_for_soundcard( if (current_buf_) { - spdlog::info("Selected a buffer for playback."); current_buf_pos_ = 0; // is audio playback stable ? i.e. is the next sample buffer @@ -186,14 +173,14 @@ 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."); + //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."); + //spdlog::warn("Break hit because both current_buf_ and sample_data_ are empty."); break; } @@ -208,11 +195,11 @@ 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."); + //spdlog::info("Current buffer is exhausted."); previous_buf_ = current_buf_; current_buf_.reset(); } else { - spdlog::warn("Break hit due to unspecified condition."); + //spdlog::warn("Break hit due to unspecified condition."); break; } } @@ -239,15 +226,7 @@ void AudioOutputControl::queue_samples_for_playing( const bool forwards, const float velocity) { - spdlog::info( - "Queueing samples for playing. Playing: {}, Direction: {}, Velocity: {}", - playing ? "Yes" : "No", - forwards ? "Forwards" : "Backwards", - velocity); - if (!playing) { - spdlog::info("Not playing, exiting queue_samples_for_playing."); - return; } @@ -266,13 +245,13 @@ void AudioOutputControl::queue_samples_for_playing( !audio_frame->num_samples()) { - spdlog::info("Audio frame skipped due to either being null, matching " - "previous/current buffer or having no 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()); + //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 @@ -288,8 +267,8 @@ void AudioOutputControl::queue_samples_for_playing( 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."); + //spdlog::info("Found and erasing existing audio sample from queue with the " + // "same media key."); sample_data_.erase(p); break; } @@ -299,14 +278,11 @@ void AudioOutputControl::queue_samples_for_playing( if (audio_repitch_ && velocity != 1.0f) { - spdlog::info( - "Respeeding audio buffer due to audio repitch and non-standard velocity."); audio_frame = super_simple_respeed_audio_buffer(audio_frame, fabs(velocity)); } if (!forwards) { - spdlog::info("Reversing audio buffer for backwards playback."); media_reader::AudioBufPtr reversed( new media_reader::AudioBuffer(audio_frame->params())); @@ -329,11 +305,9 @@ void AudioOutputControl::queue_samples_for_playing( reversed->set_display_timestamp_seconds(audio_frame->display_timestamp_seconds()); } else { - spdlog::info("Queueing audio frame for forwards playback."); sample_data_[when_to_sound_audio] = audio_frame; } } - spdlog::info("Finished queueing samples for playing."); } void AudioOutputControl::clear_queued_samples() { @@ -466,16 +440,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( const int num_channels, const int fade_in_out) { - spdlog::info("Starting copy operation."); - spdlog::info( - "Initial values - Current Buffer Position: {}, Samples to Copy: {}, Samples Pushed: " - "{}, Num Channels: {}, Fade Setting: {}", - current_buf_position, - num_samples_to_copy, - num_samps_pushed, - num_channels, - fade_in_out); - static std::vector fade_coeffs; if (fade_coeffs.empty()) { fade_coeffs.resize(FADE_FUNC_SAMPS); @@ -485,7 +449,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( } if (fade_in_out == AudioOutputControl::NoFade) { - spdlog::info("Copy operation with no fade."); if (((long)current_buf->num_samples() - (long)current_buf_position) > num_samples_to_copy) { @@ -500,7 +463,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( current_buf_position += num_samples_to_copy; num_samples_to_copy = 0; } else { - spdlog::info("Copy operation with fading."); // fewer samples left in current buffer than the soundcard wants memcpy( stream, @@ -537,7 +499,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( num_samples_to_copy), 0l); - spdlog::info("Copy without fading for {} samples.", bpos); memcpy(stream, tt, bpos * num_channels * sizeof(T)); num_samples_to_copy -= bpos; num_samps_pushed += bpos; @@ -562,12 +523,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( } } } - spdlog::info( - "Completed copy operation. New Buffer Position: {}, Samples Left to Copy: {}, Total " - "Samples Pushed: {}", - current_buf_position, - num_samples_to_copy, - num_samps_pushed); } diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index 60f453f37..476b74b5f 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -16,7 +16,7 @@ #include "linux_audio_output_device.hpp" #endif #ifdef _WIN32 -#include "windows_audio_output_device.hpp" +#include "xstudio/audio/windows_audio_output_device.hpp" #endif using namespace caf; diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 77a98e23a..5ae17acdf 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -8,7 +8,10 @@ #include #include -#include "windows_audio_output_device.hpp" +#include +#include + +#include "xstudio/audio/windows_audio_output_device.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/logging.hpp" @@ -71,7 +74,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( // For this example, we're just taking the first device CComPtr first_device; - hr = device_collection->Item(0, &first_device); + hr = device_collection->Item(1, &first_device); if (FAILED(hr)) { return hr; } @@ -166,7 +169,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( // Initialize the audio client with the mix format hr = audio_client_->InitializeSharedAudioStream( - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + 0, MINP, wavefmt, nullptr // session GUID @@ -177,6 +180,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( CoTaskMemFree(wavefmt); return hr; + } void WindowsAudioOutputDevice::connect_to_soundcard() { @@ -213,6 +217,8 @@ void WindowsAudioOutputDevice::connect_to_soundcard() { return; // or handle the error as appropriate } + audio_client_->Start(); + spdlog::info("Connected to soundcard"); } @@ -221,11 +227,14 @@ long WindowsAudioOutputDevice::desired_samples() { // 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"); } - return bufferSize; + + //TODO: Why 2, because channels? + return bufferSize*2; } long WindowsAudioOutputDevice::latency_microseconds() { @@ -241,11 +250,10 @@ long WindowsAudioOutputDevice::latency_microseconds() { } void WindowsAudioOutputDevice::push_samples( - const void *sample_data, const long num_samples, int channel_count) { + const void *sample_data, const long num_samples) { - // 1. Function Entry & Initial Parameters - spdlog::info( - "Entering push_samples with {} samples, {} channels.", num_samples, channel_count); + // TODO: Use actual channel layout. + int channel_count = 2; if (num_samples < 0 || num_samples % channel_count != 0) { spdlog::error( @@ -269,8 +277,6 @@ void WindowsAudioOutputDevice::push_samples( return; } - // 2. Buffer Size Info - spdlog::info("Buffer frame count from WASAPI: {}", buffer_framecount); // Get the number of frames of padding in the endpoint buffer. UINT32 pad = 0; @@ -280,9 +286,6 @@ void WindowsAudioOutputDevice::push_samples( return; } - // 3. Padding Info - spdlog::info("Current padding from WASAPI: {}", pad); - // 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; @@ -290,43 +293,33 @@ void WindowsAudioOutputDevice::push_samples( frames_to_write = available_frames; } - // 4. Write and Availability Info - spdlog::info( - "Frames available to write: {}. Frames attempting to write: {}", - available_frames, - frames_to_write); + if (frames_to_write) { - // Get a buffer from WASAPI for our audio data. - BYTE *buffer; - hr = render_client_->GetBuffer(frames_to_write * channel_count, &buffer); - if (FAILED(hr)) { - spdlog::error("GetBuffer failed with HRESULT: 0x{:08x}", hr); - throw std::runtime_error("Failed to get buffer from WASAPI"); - } - - // 5. Successful Buffer Retrieval - spdlog::info("Successfully retrieved buffer from WASAPI for writing."); + // Get a buffer from WASAPI for our audio data. + BYTE *buffer; + hr = render_client_->GetBuffer(frames_to_write, &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; + // 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; - } + 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 * channel_count, 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"); + // 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"); + } } - - // 6. Successful Buffer Release - spdlog::info("Successfully released buffer to WASAPI for playback."); - - // 7. Function Exit - spdlog::info("Exiting push_samples."); + + std::this_thread::sleep_for(std::chrono::microseconds(4100)); } diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index 84a309e4a..dd78eb7e5 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) { diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index 8aa6af71a..a81f0bb12 100644 --- a/src/global/src/global_actor.cpp +++ b/src/global/src/global_actor.cpp @@ -39,7 +39,7 @@ #elif __APPLE__ // TO DO #elif _WIN32 -// TO DO +#include "xstudio/audio/windows_audio_output_device.hpp" #endif using namespace caf; @@ -141,7 +141,8 @@ void GlobalActor::init(const utility::JsonStore &prefs) { #elif __APPLE__ // TO DO #elif _WIN32 - // TO DO + auto audio_out = spawn>(); + link_to(audio_out); #endif python_enabled_ = false; diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index 4f1f61a61..2ef8e376d 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -503,8 +503,6 @@ FFMpegStream::convert_av_frame_to_thumbnail(const size_t size_hint) { AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_sample_rate) { - spdlog::info("Entering get_ffmpeg_frame_as_xstudio_audio."); - AudioBufPtr audio_buffer(new AudioBuffer()); audio_buffer->allocate( soundcard_sample_rate, // sample rate @@ -513,12 +511,6 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ audio::SampleFormat::INT16 // format ); - spdlog::info( - "Allocated AudioBuffer with sample rate: {}, channels: {}, format: {}.", - soundcard_sample_rate, - 2, - "INT16"); - // N.B. We aren't supporting 'planar' audio data in xstudio (yet) - if a source codec_ // supplies planar audio ffmpeg will convert to interleaved which is what soundcards want switch (audio_buffer->sample_format()) { @@ -544,9 +536,6 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ throw media_corrupt_error("Audio buffer format is not set."); } - spdlog::info( - "Determined target sample format: {}.", av_get_sample_fmt_name(target_sample_format_)); - target_sample_rate_ = audio_buffer->sample_rate(); target_audio_channels_ = audio_buffer->num_channels(); @@ -554,16 +543,12 @@ 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)); + //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); - - spdlog::info("Resampled audio data size: {} samples.", audio_buffer->num_samples()); - - spdlog::info("Exiting get_ffmpeg_frame_as_xstudio_audio."); return audio_buffer; } @@ -717,13 +702,6 @@ int64_t FFMpegStream::frame_to_pts(int frame) const { size_t FFMpegStream::resample_audio( AVFrame *frame, AudioBufPtr &audio_buffer, int offset_into_output_buffer) { - spdlog::info("Resampling audio frame with the following attributes:"); - spdlog::info("Sample rate: {}", frame->sample_rate); - spdlog::info("Format: {}", av_get_sample_fmt_name((AVSampleFormat)frame->format)); - spdlog::info("Channels: {}", frame->channels); - spdlog::info("Number of samples: {}", frame->nb_samples); - spdlog::info("Channel layout: {}", frame->channel_layout); - // N.B. this method is based loosely on the audio resampling in ffplay.c in ffmpeg source const int64_t target_channel_layout = av_get_default_channel_layout(2); @@ -834,8 +812,6 @@ size_t FFMpegStream::resample_audio( int converted_n_samps = swr_convert(audio_resampler_ctx_, &out, out_count, in, frame->nb_samples); - spdlog::info("Converted {} samples.", converted_n_samps); - if (offset_into_output_buffer == -1) { audio_buffer->set_num_samples(audio_buffer->num_samples() + converted_n_samps); } @@ -844,11 +820,6 @@ size_t FFMpegStream::resample_audio( throw media_corrupt_error("swr_convert() failed"); } - spdlog::info( - "Resampled audio data size: {} bytes", - converted_n_samps * target_audio_channels_ * - av_get_bytes_per_sample(target_sample_format_)); - return converted_n_samps * target_audio_channels_ * av_get_bytes_per_sample(target_sample_format_); } diff --git a/vcpkg b/vcpkg deleted file mode 160000 index a1212c93c..000000000 --- a/vcpkg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a1212c93cabaa9c5c36c1ffdb4bddd59fdf31e43 diff --git a/vcpkg.json b/vcpkg.json index cd126c1ea..173898371 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,13 +13,19 @@ "lcms", "caf", "opencolorio", - "openimageio" + "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": "fmt", "version": "8.0.1" }, + { "name": "ffmpeg", "version": "5.1.2#6" } ] } From 20dae309c1cc726c030cb5a2d30a14444038ca9a Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 29 May 2024 14:48:52 -0700 Subject: [PATCH 19/42] Sound plays back without obvious problems Signed-off-by: Michael Kessler --- include/xstudio/audio/audio_output_actor.hpp | 2 +- src/audio/src/windows_audio_output_device.cpp | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index 0f25e3980..c33d289ec 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -84,7 +84,7 @@ class AudioOutputDeviceActor : public caf::event_based_actor { [=](const std::vector &samples_to_play) mutable { output_device_->push_samples( - (const void *)samples_to_play.data(), num_samps_soundcard_wants); + (const void *)samples_to_play.data(), samples_to_play.size()); waiting_for_samples_ = false; diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 5ae17acdf..4e9ce91e0 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -233,8 +233,13 @@ long WindowsAudioOutputDevice::desired_samples() { throw std::runtime_error("Failed to get buffer size"); } - //TODO: Why 2, because channels? - return bufferSize*2; + 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() { @@ -252,8 +257,7 @@ long WindowsAudioOutputDevice::latency_microseconds() { void WindowsAudioOutputDevice::push_samples( const void *sample_data, const long num_samples) { - // TODO: Use actual channel layout. - int channel_count = 2; + int channel_count = num_channels_; if (num_samples < 0 || num_samples % channel_count != 0) { spdlog::error( @@ -297,7 +301,7 @@ void WindowsAudioOutputDevice::push_samples( // Get a buffer from WASAPI for our audio data. BYTE *buffer; - hr = render_client_->GetBuffer(frames_to_write, &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"); @@ -320,6 +324,4 @@ void WindowsAudioOutputDevice::push_samples( throw std::runtime_error("Failed to release buffer to WASAPI"); } } - - std::this_thread::sleep_for(std::chrono::microseconds(4100)); } From 0dd8a9c0c8f7ee203e0c6835fc36e5ac2efacc63 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 29 May 2024 15:18:39 -0700 Subject: [PATCH 20/42] Make audio device default to the "default" device. Signed-off-by: Michael Kessler --- src/audio/src/windows_audio_output_device.cpp | 59 +++++-------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 4e9ce91e0..2852b47c8 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -59,49 +59,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( // If sound_card is not provided, enumerate the devices and pick the first one if (sound_card.empty() || sound_card == L"default") { - CComPtr device_collection; - UINT device_count = 0; - hr = device_enumerator->EnumAudioEndpoints( - eRender, DEVICE_STATE_ACTIVE, &device_collection); - if (FAILED(hr)) { - return hr; - } - - hr = device_collection->GetCount(&device_count); - if (FAILED(hr) || device_count == 0) { - return E_FAIL; // or some suitable error - } - - // For this example, we're just taking the first device - CComPtr first_device; - hr = device_collection->Item(1, &first_device); - if (FAILED(hr)) { - return hr; - } - - // Print the device name - CComPtr property_store; - hr = first_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"Device Name: %s\n", var_name.pwszVal); - PropVariantClear(&var_name); // always clear the PROPVARIANT to release any - // memory it might've allocated - } - } - - LPWSTR device_id = nullptr; - hr = first_device->GetId(&device_id); - if (FAILED(hr)) { - return hr; - } - - hr = device_enumerator->GetDevice(device_id, &audio_device); - ::CoTaskMemFree(device_id); // free the memory for the ID + hr = device_enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &audio_device); } else { // Get the audio-render device based on the provided sound_card hr = device_enumerator->GetDevice(sound_card.c_str(), &audio_device); @@ -111,6 +69,21 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( return hr; } + // 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 + } + } + // Get an IAudioClient3 instance hr = audio_device->Activate( __uuidof(IAudioClient3), From e0705f2424a9daf3464a9e895e7e00b692790e09 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 29 May 2024 18:13:40 -0700 Subject: [PATCH 21/42] Update build instructions and minor cmake fixes. Signed-off-by: Michael Kessler --- CMakeLists.txt | 43 ++++++------ CMakePresets.json | 35 ++-------- docs/build_guides/windows.md | 114 +++++++++++++------------------- scripts/setup/setup_dev_env.ps1 | 30 --------- 4 files changed, 74 insertions(+), 148 deletions(-) delete mode 100644 scripts/setup/setup_dev_env.ps1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ffc40f8a..db638442e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,9 +3,6 @@ cmake_minimum_required(VERSION 3.12 FATAL_ERROR) option(USE_VCPKG "Use Vcpkg for package management" OFF) if(WIN32) set(USE_VCPKG ON) - set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") - add_compile_options($<$:/MP>) - endif() if (USE_VCPKG) @@ -30,6 +27,12 @@ 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") @@ -156,6 +159,21 @@ endif() 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) + + # build quickpromise + add_subdirectory("extern/quickpromise") + add_subdirectory("extern/quickfuture") + + 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) @@ -166,10 +184,8 @@ endif() # Add the necessary libraries from Vcpkg if Vcpkg integration is enabled if(USE_VCPKG) - set(CMAKE_CXX_STANDARD 20) - add_compile_options(/permissive-) + set(VCPKG_INTEGRATION ON) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # Set Python in VCPKG set(Python_EXECUTABLE "${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3/python.exe") # Install pip and sphinx @@ -181,7 +197,7 @@ if(USE_VCPKG) 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 sphinx breathe sphinx-rtd-theme OpenTimelineIO importlib_metadata zipp + 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) @@ -190,19 +206,6 @@ if(USE_VCPKG) endif() # append vcpkg packages list(APPEND CMAKE_PREFIX_PATH "${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows") - # enable UUID System Generator - add_definitions(-DUUID_SYSTEM_GENERATOR=ON) - - # 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) - - # build quickpromise - add_subdirectory("extern/quickpromise") - add_subdirectory("extern/quickfuture") - - # 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() diff --git a/CMakePresets.json b/CMakePresets.json index d08436f48..15d512dd5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,22 +1,6 @@ { "version": 2, "configurePresets": [ - { - "name": "windows", - "generator": "Visual Studio 17 2022", - "binaryDir": "${sourceDir}/build", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "_____FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", - "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", - "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF", - "CMAKE_BUILD_PARALLEL_LEVEL": "64", - "CMAKE_AUTOGEN_PARALLEL": "64" - } - }, { "name": "windows-ninja (Release)", "generator": "Ninja", @@ -24,14 +8,10 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF", - "CMAKE_BUILD_PARALLEL_LEVEL": "64", - "CMAKE_AUTOGEN_PARALLEL": "64", - "INSTALL_PARALLEL": "ON" + "BUILD_DOCS": "OFF" } }, { @@ -42,15 +22,10 @@ "CMAKE_BUILD_TYPE": "RelWithDebInfo", "USE_SANITIZER": "address", "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", + "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF", - "CMAKE_BUILD_PARALLEL_LEVEL": "64", - "CMAKE_AUTOGEN_PARALLEL": "64", - "INSTALL_PARALLEL": "ON", - "PARALLEL_LEVEL": "64" + "BUILD_DOCS": "OFF" } }, { @@ -65,9 +40,7 @@ "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF", - "CMAKE_BUILD_PARALLEL_LEVEL": "64", - "CMAKE_AUTOGEN_PARALLEL": "64" + "BUILD_DOCS": "OFF" } } ] diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md index 7ca42da35..53cc88a65 100644 --- a/docs/build_guides/windows.md +++ b/docs/build_guides/windows.md @@ -1,67 +1,47 @@ -# Build XStudio on Windows - -## Install basic tools ---- - -To build XStudio on Windows, you need to install some basic tools. The Windows build uses VCPKG to install third-party dependencies. Follow these steps to set up the development environment: - -1. First, ensure that you are using an [administrative](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/) shell. - -2. Install MS Visual Studio 2019 (Community Edition is fine) [Visual Studio Legacy Installs](https://visualstudio.microsoft.com/vs/older-downloads/). - -3. Restart your machine after Visual Studio finishes installing - -4. Execute the script `setup_dev_env.ps1` located in the [script/setup](/scripts/setup/setup_dev_env.ps1) folder. - -## Install Qt 5.15 ---- - -To install Qt 5.15, follow these steps: - -1. [Download](https://www.qt.io/download-qt-installer-oss?hsCtaTracking=99d9dd4f-5681-48d2-b096-470725510d34%7C074ddad0-fdef-4e53-8aa8-5e8a876d6ab4) the Qt Windows installer. -2. Execute the installer. -3. During the installation, select the components as shown in the following picture: - - ![Qt Components](/docs/build_guides/media/images/Qt5_select_components.png) - -4. The installation may take some time. You can grab a cup of coffee while it completes. -5. Note that you will need a valid username and password to download Qt. The installer allows you to add or remove components in the future. - -## Install FFMPEG ---- -**Important**: The current supported version of FFMPEG is 5.1. - -There are two options to install FFMPEG: - -1. Using VCPKG. -2. Using a prebuilt binary. - -### Install FFMPEG with a prebuilt binary -You can locate prebuilt FFMPEG binaries that fit your licensing criteria [here](https://ffmpeg.org/download.html#build-windows). - -Download and extract the archive to your local machine. - -## Build XStudio using CMake GUI and Visual Studio 2019 ---- - -To build XStudio using CMake GUI, follow these steps: - -1. Configure the CMakePresets.json to your appropriate paths. - ![Qt5 CMake location](/docs/build_guides/media/images/setup_Qt5.png) - ![FFMPEG ROOT](/docs/build_guides/media/images/setup_ffmpeg.png) -2. Launch CMake-GUI. -3. Select the source root path in CMake-GUI -4. Select the "windows" preset -5. Select the build location to be in your source root path in a folder called build -6. Click on the "Configure" button. -7. Define Visual Studio 2019 and select the X64 architecture. -8. Click on the "Generate" button. -9. Click on the "Open Project" button. -10. Choose your Build Type (Debug/Release) -11. Select the INSTALL target and build. - -# Known Caveats - -* Python interpreter is not set up properly -* Audio does not work and may have lots of debugging messages still in-tact. -* Some tools or plugins may not be fully functional. \ No newline at end of file +## 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 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 + * Ensure your drive has a decent amount of space free + * TODO: Create a ballpark figure of disk space used by dependencies. + * 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 (which it bootstraps itself). + * Once configured, you can switch to the Solution Explorer's solution view to view CMake targets. + * Double-click `CMake Targets View` + * Right-click on `xStudio Project` and select `Build All` + * One 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. \ No newline at end of file diff --git a/scripts/setup/setup_dev_env.ps1 b/scripts/setup/setup_dev_env.ps1 deleted file mode 100644 index a78883521..000000000 --- a/scripts/setup/setup_dev_env.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -$ErrorActionPreference = "Stop" -Set-ExecutionPolicy Bypass -Scope Process -Force; - -## Install Chocolatey -Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) - -Write-Host "=== Creating your XStudio development environment! ===" - -Write-Host "====> Installing Choco packages..." -choco --version -choco feature enable -name=exitOnRebootDetected -choco install ChocolateyGUI -y - -# core components -Write-Host "====> Installing core components..." -choco install powershell-core -y -choco install git -y -choco install cmake -y -choco install ninja -y -choco install doxygen.install -y - -# ides -Write-Host "====> Installing IDEs..." -#choco install visualstudio2019buildtools -y -#choco install visualstudio2019community -y --package-parameters "--includeRecommended --locale en-US --passive --add Microsoft.VisualStudio.Component.CoreEditor --add Microsoft.VisualStudio.Workload.NetWeb --add Microsoft.VisualStudio.Workload.Azure --add Microsoft.VisualStudio.Workload.NetCoreTools --add Microsoft.VisualStudio.Workload.Python --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.NativeGame --add Microsoft.VisualStudio.Workload.NativeCrossPlat --add Component.GitHub.VisualStudio --add Component.Incredibuild --add Microsoft.VisualStudio.Workload.VisualStudioExtension --add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.Universal" -#choco install visualstudio2019professional -y --package-parameters "--includeRecommended --locale en-US --passive --add Microsoft.VisualStudio.Component.CoreEditor --add Microsoft.VisualStudio.Workload.NetWeb --add Microsoft.VisualStudio.Workload.Azure --add Microsoft.VisualStudio.Workload.NetCoreTools --add Microsoft.VisualStudio.Workload.Python --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.NativeGame --add Microsoft.VisualStudio.Workload.NativeCrossPlat --add Component.GitHub.VisualStudio --add Microsoft.VisualStudio.Workload.VisualStudioExtension --add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.Universal" - -refreshenv - -Write-Host "=== Your XStudio development environment is ready to use! Enjoy! ===" From e9c203c0068d81490025af9df360ebaa4f4051d0 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Thu, 30 May 2024 11:34:11 -0700 Subject: [PATCH 22/42] Add git install to build instructions. Signed-off-by: Michael Kessler --- docs/build_guides/windows.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md index 53cc88a65..4356658ae 100644 --- a/docs/build_guides/windows.md +++ b/docs/build_guides/windows.md @@ -1,7 +1,9 @@ -## Windows 10/11 +# 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) @@ -22,8 +24,7 @@ * Clone this project to your local drive. Tips to consider: * The path should not have spaces in it. * Ideally, keep the path short - * Ensure your drive has a decent amount of space free - * TODO: Create a ballpark figure of disk space used by dependencies. + * 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 @@ -44,4 +45,4 @@ # Questions? -Reach out on the ASWF Slack in the #open-review-initiative channel. \ No newline at end of file +Reach out on the ASWF Slack in the #open-review-initiative channel. From 5709e7cab6baeac6e4146a54c6327ba113db7a8a Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Thu, 30 May 2024 12:52:12 -0700 Subject: [PATCH 23/42] Update Readme Signed-off-by: Michael Kessler --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5f6563ffc..c58dd78cf 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,22 @@ 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. -This release of xSTUDIO can be built on various Linux flavours. MacOS compatibility is not available yet but this work is on the roadmap for 2023. - -We provide comprehensive build steps for 4 of the most popular Linux distributions. +We provide comprehensive build steps for 4 of the most popular 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) + +### Building xSTUDIO for Windows * [Windows](docs/build_guides/windows.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 MacOS +### Building xSTUDIO for MacOS -MacOS compatibility is not yet available but it is due in Q3 or Q4 2023. Watch this space! +MacOS compatibility is not yet available. Watch this space! From dc61e388e493e15ea11b5ecd3c67437f27d146cc Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Thu, 30 May 2024 13:32:42 -0700 Subject: [PATCH 24/42] Add build target swap to guide. Signed-off-by: Michael Kessler --- docs/build_guides/windows.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md index 4356658ae..81d740d5f 100644 --- a/docs/build_guides/windows.md +++ b/docs/build_guides/windows.md @@ -36,6 +36,7 @@ * Use Open Folder to point at the ${CLONE_ROOT} * Visual Studio should start configuring the project, including downloading dependencies via VCPKG (which it bootstraps itself). * 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` * One built, right-click on `xStudio Project` and select `Install` From 5e69608fa69a5c1baa082438e7028aaf52ea8747 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Thu, 30 May 2024 14:19:02 -0700 Subject: [PATCH 25/42] Add conditional to hide windows cmake presets from other platforms. Signed-off-by: Michael Kessler --- CMakePresets.json | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 15d512dd5..d45439290 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,12 +1,16 @@ { - "version": 2, + "version": 3, "configurePresets": [ - { - "name": "windows-ninja (Release)", + { "name": "windows-base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "hidden": true, "generator": "Ninja", "binaryDir": "${sourceDir}/build", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", "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": "D:/xstudio_install", @@ -15,32 +19,26 @@ } }, { - "name": "windows-ninja (RelWithDebInfo)", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build", + "name": "windows (Release)", + "inherits": ["windows-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows (RelWithDebInfo)", + "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", - "USE_SANITIZER": "address", - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", - "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF" + "USE_SANITIZER": "address" } }, { - "name": "windows-ninja (Debug)", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build", + "name": "windows (Debug)", + "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "USE_SANITIZER": "address", - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "FFMPEG_ROOT": "D:/ffmpeg-5.1.2-full_build-shared/", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", - "CMAKE_INSTALL_PREFIX": "D:/xstudio_install", - "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF" + "USE_SANITIZER": "address" } } ] From 2b474a67e2c89ab3d6736d01d68a549aa6108c04 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 31 May 2024 18:12:32 -0700 Subject: [PATCH 26/42] Remove failing dataChanged modification. Signed-off-by: Michael Kessler --- src/ui/qml/helper/src/model_data_ui.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ui/qml/helper/src/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 9093a8b6c..602ff5f23 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -120,17 +120,12 @@ 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) { -#ifdef false //_WIN32 + emit dataChanged( idx, idx, QVector({static_cast(Roles::LASTROLE + static_cast(i))})); -#else - emit dataChanged( - idx, - idx, - QVector({Roles::LASTROLE + static_cast(i)})); -#endif + break; } } From 0ee4e0c6993ba8b46cc2bccd77d5b8bf3bf12162 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Sun, 2 Jun 2024 18:05:06 -0700 Subject: [PATCH 27/42] CoreAudio Stub and json debugging. Signed-off-by: Michael Kessler --- CMakePresets.json | 1 + cmake/macros.cmake | 20 +- .../windows_spatial_audio_output_device.hpp | 60 +++ share/preference/core_audio.json | 60 +++ src/audio/src/windows_audio_output_device.cpp | 17 +- .../windows_spatial_audio_output_device.cpp | 374 ++++++++++++++++++ src/json_store/src/json_store_actor.cpp | 3 +- 7 files changed, 526 insertions(+), 9 deletions(-) create mode 100644 include/xstudio/audio/windows_spatial_audio_output_device.hpp create mode 100644 src/audio/src/windows_spatial_audio_output_device.cpp diff --git a/CMakePresets.json b/CMakePresets.json index d45439290..7bca51746 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -35,6 +35,7 @@ }, { "name": "windows (Debug)", + "hidden": true, "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 4406b4fb3..33ad00adb 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -235,6 +235,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}) @@ -244,13 +245,18 @@ macro(create_plugin_with_alias NAME ALIASNAME VERSION DEPS) ) if(WIN32) #TODO: Determine if we need to keep this limited to win32. - # We don't want the vcpkg install. - - #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 ) + #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. + _install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/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() @@ -430,10 +436,10 @@ endmacro() macro(set_python_to_proper_build_type) if(WIN32) + #TODO: Debug build fails to find the appropriate Python lib. #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:-DPy_DEBUG") #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:PYTHON_IS_DEBUG=0>") - #add_compile_definitions(PY_NO_LINK_LIB) - set_property(TARGET ${PROJECT_NAME} PROPERTY EXCLUDE_FROM_DEFAULT_BUILD_DEBUG TRUE) + # set_property(TARGET ${PROJECT_NAME} PROPERTY EXCLUDE_FROM_DEFAULT_BUILD_DEBUG TRUE) endif() endmacro() diff --git a/include/xstudio/audio/windows_spatial_audio_output_device.hpp b/include/xstudio/audio/windows_spatial_audio_output_device.hpp new file mode 100644 index 000000000..5e567b7e7 --- /dev/null +++ b/include/xstudio/audio/windows_spatial_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 WindowsSpatialAudioOutputDevice : public AudioOutputDevice { + public: + WindowsSpatialAudioOutputDevice(const utility::JsonStore &prefs); + + ~WindowsSpatialAudioOutputDevice() 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 spatial_audio_client_; + + const utility::JsonStore config_; + const utility::JsonStore prefs_; + + HRESULT initializeAudioClient( + const std::wstring &sound_card = L"", + long sample_rate = 48000, + int num_channels = 2); + }; +} // namespace audio +} // namespace xstudio diff --git a/share/preference/core_audio.json b/share/preference/core_audio.json index a330050e8..5fe24baa7 100644 --- a/share/preference/core_audio.json +++ b/share/preference/core_audio.json @@ -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_cards", + "default_value": "default", + "description": "Soundcard name", + "value": "default", + "datatype": "string", + "context": ["APPLICATION"] } } } diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 2852b47c8..6aaf7a70e 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -115,8 +115,23 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( WAVEFORMATEXTENSIBLE *pExtensible = reinterpret_cast(pMixFormat); spdlog::info("Valid Bits Per Sample: {}", pExtensible->Samples.wValidBitsPerSample); - spdlog::info("Channel Mask: {}", pExtensible->dwChannelMask); + 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) + + ); } // Fetch the currently active shared mode format diff --git a/src/audio/src/windows_spatial_audio_output_device.cpp b/src/audio/src/windows_spatial_audio_output_device.cpp new file mode 100644 index 000000000..feb71754e --- /dev/null +++ b/src/audio/src/windows_spatial_audio_output_device.cpp @@ -0,0 +1,374 @@ +#include "xstudio/audio/windows_spatial_audio_output_device.hpp" + +#define WIN32_LEAN_AND_MEAN +#include +#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; + + +WindowsSpatialAudioOutputDevice::WindowsSpatialAudioOutputDevice( + const utility::JsonStore &prefs) + : AudioOutputDevice(), prefs_(prefs) {} + +WindowsSpatialAudioOutputDevice::~WindowsSpatialAudioOutputDevice() { + disconnect_from_soundcard(); +} + +void WindowsSpatialAudioOutputDevice::disconnect_from_soundcard() { + if (render_client_) { + render_client_ = nullptr; + } + if (audio_client_) { + audio_client_->Stop(); + audio_client_ = nullptr; + } +} + +HRESULT WindowsSpatialAudioOutputDevice::initializeAudioClient( + const std::wstring &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 == L"default") { + hr = device_enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &audio_device); + } else { + // Get the audio-render device based on the provided sound_card + hr = device_enumerator->GetDevice(sound_card.c_str(), &audio_device); + } + + if (FAILED(hr)) { + return hr; + } + + // 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 + } + PROPVARIANT audio_channel_config; + PropVariantInit(&audio_channel_config); + hr = property_store->GetValue(KSPROPERTY_AUDIO_CHANNEL_CONFIG, &audio_channel_config); + if (SUCCEEDED(hr)) { + spdlog::info("Physical Speaker Mask: {}", audio_channel_config.uintVal); + } + } + + + hr = audio_device->Activate( + __uuidof(ISpatialAudioClient), + CLSCTX_INPROC_SERVER, + nullptr, + (void **)&spatialAudioClient_); + if (FAILED(hr)) { + spdlog::warn("Unable to activate spatial audio client."); + return hr; + } else { + UINT32 dynamicCount = 0; + hr = spatialAudioClient_->GetMaxDynamicObjectCount(&dynamicCount); + if (FAILED(hr)) { + spdlog::warn("Unable to get dynmic object count from spatial audio system"); + } else { + spdlog::info("Found {} dynamic object count in spatial audio system", dynamicCount); + } + + AudioObjectType mask; + spatialAudioClient_->GetNativeStaticObjectTypeMask(&mask); + + if (FAILED(hr)) { + spdlog::warn( + "Unable to get statc object position for front-left from spatial audio system"); + } else { + spdlog::info("Found object mask for native static objects: {}", mask); + + spdlog::info( + "None {} Dynamic {} FL {} FR {} FC {} LFE {} SR {} SL {} TBL {}", + (bool)(AudioObjectType_None & mask), + (bool)(AudioObjectType_Dynamic & mask), + (bool)(AudioObjectType_FrontLeft & mask), + (bool)(AudioObjectType_FrontRight & mask), + (bool)(AudioObjectType_FrontCenter & mask), + (bool)(AudioObjectType_LowFrequency & mask), + (bool)(AudioObjectType_SideRight & mask), + (bool)(AudioObjectType_SideLeft & mask), + (bool)(AudioObjectType_TopBackLeft & mask)); + } + + HANDLE bufferCompletionEvent = CreateEvent(nullptr, false, false, nullptr); + + SpatialAudioObjectRenderStreamActivationParams streamParams; + streamParams.StaticObjectTypeMask = + AudioObjectType_FrontLeft | AudioObjectType_FrontRight; + streamParams.MinDynamicObjectCount = 0; + streamParams.MaxDynamicObjectCount = 0; + streamParams.Category = AudioCategory_Movie; + streamParams.EventHandle = bufferCompletionEvent; + streamParams.NotifyObject = nullptr; + + PROPVARIANT pv; + PropVariantInit(&pv); + pv.vt = VT_BLOB; + pv.blob.cbSize = sizeof(streamParams); + pv.blob.pBlobData = (BYTE *)&streamParams; + + CComPtr spatialAudioStream; + hr = spatialAudioClient->ActivateSpatialAudioStream( + &pv, __uuidof(spatialAudioStream), (void **)&spatialAudioStream); + } + + + // 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; + } + + // 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); + spdlog::info("Channel Mask: {}", pExtensible->dwChannelMask); + // Add more fields if needed + } + + // 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_->InitializeSharedAudioStream( + 0, + MINP, + wavefmt, + nullptr // session GUID + ); + + // Free the mix format and wave format after usage + CoTaskMemFree(pMixFormat); + CoTaskMemFree(wavefmt); + + return hr; +} + +void WindowsSpatialAudioOutputDevice::connect_to_soundcard() { + + sample_rate_ = 48000; // default values + num_channels_ = 2; + std::wstring sound_card(L"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(); + + spdlog::info("Connected to soundcard"); +} + +long WindowsSpatialAudioOutputDevice::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 WindowsSpatialAudioOutputDevice::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 WindowsSpatialAudioOutputDevice::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"); + } + } +} diff --git a/src/json_store/src/json_store_actor.cpp b/src/json_store/src/json_store_actor.cpp index 8df0aad8b..71580ded2 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()); From bb06653f0dcf7c6656bbe56b02f4c087dab92a85 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 5 Jun 2024 10:06:58 -0700 Subject: [PATCH 28/42] Community Feedback Fixes Signed-off-by: Michael Kessler --- CMakePresets.json | 6 +-- cmake/macros.cmake | 32 ++++++------ include/xstudio/audio/audio_output_actor.hpp | 7 ++- include/xstudio/audio/audio_output_device.hpp | 7 +++ .../audio/windows_audio_output_device.hpp | 5 +- .../xstudio/api/session/playlist/playlist.py | 2 +- retired/playlist/src/playlist_ui.cpp | 2 +- retired/session_ui.cpp | 2 +- share/preference/core_audio.json | 2 +- share/preference/core_cache.json | 4 +- src/audio/src/audio_output.cpp | 35 +++++++++++-- src/audio/src/windows_audio_output_device.cpp | 50 +++++++++++++------ src/json_store/src/json_store_actor.cpp | 35 ++++++++----- src/media_hook/src/media_hook_actor.cpp | 9 ++-- src/playlist/src/playlist_actor.cpp | 8 +-- .../colour_op/grading/src/qml/CMakeLists.txt | 11 +++- src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp | 3 +- src/ui/model_data/src/model_data_actor.cpp | 3 +- .../session/src/session_model_methods_ui.cpp | 14 ++++-- vcpkg.json | 4 +- 20 files changed, 164 insertions(+), 77 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 7bca51746..7207774d3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -19,14 +19,14 @@ } }, { - "name": "windows (Release)", + "name": "Release", "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }, { - "name": "windows (RelWithDebInfo)", + "name": "RelWithDebInfo", "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", @@ -34,7 +34,7 @@ } }, { - "name": "windows (Debug)", + "name": "Debug", "hidden": true, "inherits": ["windows-base"], "cacheVariables": { diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 33ad00adb..0bb0e2a12 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -153,6 +153,22 @@ macro(default_plugin_options name) ) install(TARGETS ${name} LIBRARY DESTINATION share/xstudio/plugin) + + if(WIN32) #TODO: Determine if we need to keep this limited to 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) @@ -244,22 +260,6 @@ macro(create_plugin_with_alias NAME ALIASNAME VERSION DEPS) PUBLIC ${DEPS} ) - if(WIN32) #TODO: Determine if we need to keep this limited to 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. - _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() - - set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) endmacro() diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index c33d289ec..96e99752f 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -45,6 +45,9 @@ class AudioOutputDeviceActor : public caf::event_based_actor { }, [=](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_) { @@ -82,11 +85,11 @@ class AudioOutputDeviceActor : public caf::event_based_actor { (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()); - waiting_for_samples_ = false; + if (playing_) { anon_send(actor_cast(this), push_samples_atom_v); diff --git a/include/xstudio/audio/audio_output_device.hpp b/include/xstudio/audio/audio_output_device.hpp index d19f4b50f..ca92875c0 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/include/xstudio/audio/windows_audio_output_device.hpp b/include/xstudio/audio/windows_audio_output_device.hpp index 06393e6f7..9a1eabe3a 100644 --- a/include/xstudio/audio/windows_audio_output_device.hpp +++ b/include/xstudio/audio/windows_audio_output_device.hpp @@ -46,12 +46,15 @@ namespace audio { 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::wstring &sound_card = L"", + const std::string &sound_card = "", long sample_rate = 48000, int num_channels = 2); 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/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/share/preference/core_audio.json b/share/preference/core_audio.json index 5fe24baa7..44f121039 100644 --- a/share/preference/core_audio.json +++ b/share/preference/core_audio.json @@ -155,7 +155,7 @@ "context": ["APPLICATION"] }, "sound_card": { - "path": "/core/audio/windows_audio_prefs/sound_cards", + "path": "/core/audio/windows_audio_prefs/sound_card", "default_value": "default", "description": "Soundcard name", "value": "default", 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/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index 2b765439a..c06968578 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -204,7 +204,7 @@ void AudioOutputControl::prepare_samples_for_soundcard( } } - /* + const float vol = volume(); static float last_vol = vol; if (last_vol != vol) { @@ -213,7 +213,6 @@ void AudioOutputControl::prepare_samples_for_soundcard( static_volume_adjust(v, vol / 100.0f); } last_vol = vol; - */ } catch (std::exception &e) { spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -231,6 +230,32 @@ void AudioOutputControl::queue_samples_for_playing( } 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) { @@ -277,9 +302,9 @@ void AudioOutputControl::queue_samples_for_playing( - 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) { diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 6aaf7a70e..0205eec39 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -28,17 +29,11 @@ WindowsAudioOutputDevice::~WindowsAudioOutputDevice() { } void WindowsAudioOutputDevice::disconnect_from_soundcard() { - if (render_client_) { - render_client_ = nullptr; - } - if (audio_client_) { - audio_client_->Stop(); - audio_client_ = nullptr; - } + return; } HRESULT WindowsAudioOutputDevice::initializeAudioClient( - const std::wstring &sound_card /* = L"" */, + const std::string &sound_card /* = L"" */, long sample_rate /* = 48000 */, int num_channels /* = 2 */) { @@ -58,17 +53,22 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( } // If sound_card is not provided, enumerate the devices and pick the first one - if (sound_card.empty() || sound_card == L"default") { + 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 - hr = device_enumerator->GetDevice(sound_card.c_str(), &audio_device); + 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); @@ -83,6 +83,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( // memory it might've allocated } } + #endif // Get an IAudioClient3 instance hr = audio_device->Activate( @@ -102,6 +103,9 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( return hr; } + + sample_rate_ = pMixFormat->nSamplesPerSec; + #if false // Print the mix format details spdlog::info("Mix Format Details:"); spdlog::info("Format Tag: {}", pMixFormat->wFormatTag); @@ -133,6 +137,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( ); } + #endif // Fetch the currently active shared mode format WAVEFORMATEX *wavefmt = NULL; @@ -156,9 +161,11 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( } // Initialize the audio client with the mix format - hr = audio_client_->InitializeSharedAudioStream( + hr = audio_client_->Initialize( + AUDCLNT_SHAREMODE_SHARED, 0, MINP, + 0, wavefmt, nullptr // session GUID ); @@ -171,11 +178,11 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( } -void WindowsAudioOutputDevice::connect_to_soundcard() { +void WindowsAudioOutputDevice::initialize_sound_card() { sample_rate_ = 48000; //default values num_channels_ = 2; - std::wstring sound_card(L"default"); + std::string sound_card("default"); buffer_size_ = 2048; // Adjust to match your preferences // Replace with your method to get preference values @@ -186,7 +193,7 @@ void WindowsAudioOutputDevice::connect_to_soundcard() { 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"); + 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()); } @@ -207,7 +214,10 @@ void WindowsAudioOutputDevice::connect_to_soundcard() { audio_client_->Start(); - spdlog::info("Connected to soundcard"); +} + +void WindowsAudioOutputDevice::connect_to_soundcard() { + // We are already playing ;-D } long WindowsAudioOutputDevice::desired_samples() { @@ -257,7 +267,7 @@ void WindowsAudioOutputDevice::push_samples( // Ensure we have a valid render_client_ if (!render_client_) { - spdlog::error("Invalid Render Client"); + //spdlog::error("Invalid Render Client"); return; // Exit if no render client is set } @@ -311,5 +321,13 @@ void WindowsAudioOutputDevice::push_samples( 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/json_store/src/json_store_actor.cpp b/src/json_store/src/json_store_actor.cpp index 71580ded2..c6fa2be83 100644 --- a/src/json_store/src/json_store_actor.cpp +++ b/src/json_store/src/json_store_actor.cpp @@ -52,6 +52,7 @@ JsonStoreActor::JsonStoreActor( }, [=](erase_json_atom, const std::string &path) -> bool { + std::string p = path; auto result = json_store_.remove(path); if (result) broadcast_change(); @@ -59,13 +60,15 @@ JsonStoreActor::JsonStoreActor( }, [=](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; }, @@ -73,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; }, @@ -86,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; }, @@ -102,27 +109,30 @@ 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 ? + std::string p = path; auto rp = make_response_promise(); this->request(_actor, caf::infinite, utility::get_group_atom_v) .then( - [&, path, _actor, rp]( + [&, 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( @@ -130,7 +140,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); }, @@ -181,6 +191,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); @@ -188,6 +199,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/media_hook/src/media_hook_actor.cpp b/src/media_hook/src/media_hook_actor.cpp index fb6b50a57..545eca3cf 100644 --- a/src/media_hook/src/media_hook_actor.cpp +++ b/src/media_hook/src/media_hook_actor.cpp @@ -94,10 +94,13 @@ 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; + } - auto rp = make_response_promise(); request(media_source, infinite, json_store::get_json_atom_v, "") .then( diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index e7a1cbc7b..5274649ee 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -614,12 +614,14 @@ 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 + + std::vector media_actors = ma; auto source_count = std::make_shared(); (*source_count) = media_actors.size(); auto rp = make_response_promise(); @@ -651,7 +653,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! @@ -666,6 +667,7 @@ void PlaylistActor::init() { add_media_atom_v, i); } + send_content_changed_event(); } }, [=](error &err) mutable { @@ -674,7 +676,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); diff --git a/src/plugin/colour_op/grading/src/qml/CMakeLists.txt b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt index 497344c89..5c50e0831 100644 --- a/src/plugin/colour_op/grading/src/qml/CMakeLists.txt +++ b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt @@ -1,7 +1,14 @@ project(grading VERSION 0.1.0 LANGUAGES CXX) -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) +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) diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp index c1062cc76..7b3db4077 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp @@ -271,8 +271,7 @@ void FFMpegMediaReader::update_preferences(const utility::JsonStore &prefs) { preference_value(prefs, "/core/audio/pulse_audio_prefs/sample_rate"); #endif #ifdef _WIN32 - soundcard_sample_rate_ = 48000; - //preference_value(prefs, "/core/audio/windows_audio_prefs/sample_rate"); + soundcard_sample_rate_ = preference_value(prefs, "/core/audio/windows_audio_prefs/sample_rate"); #endif } catch (const std::exception &e) { diff --git a/src/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index 1440f0107..bb644e2fe 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -440,10 +440,11 @@ void GlobalUIModelData::set_data( void GlobalUIModelData::insert_attribute_data_into_model( const std::string &model_name, const utility::Uuid &attribute_uuid, - const utility::JsonStore &attribute_data, + 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()) { 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 33645fe1e..24c40f590 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -656,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()}; @@ -679,14 +681,18 @@ QFuture> SessionModel::handleUriListDropFuture( 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); - sub_target = actorFromString(system(), ij.at("actor")); + actor = ij.at("actor"); + sub_target = actorFromString(system(), actor); } else if (type == "Timeline") { target = actorFromIndex(index.parent(), true); - sub_target = actorFromString(system(), ij.at("actor")); + actor = ij.at("actor"); + sub_target = actorFromString(system(), actor); } else if ( type == "Video Track" or type == "Audio Track" or type == "Gap" or type == "Clip") { diff --git a/vcpkg.json b/vcpkg.json index 173898371..6da1cfb13 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -8,6 +8,7 @@ "glew", "freetype", "pybind11", + "python3", "spdlog", "fmt", "lcms", @@ -26,6 +27,7 @@ { "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": "ffmpeg", "version": "5.1.2#6" }, + { "name": "python3", "version": "3.10.7#7" } ] } From e9025917dd30238635b185bb324d5b52b53fb257 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 5 Jun 2024 11:44:17 -0700 Subject: [PATCH 29/42] Clean unused comments Signed-off-by: Michael Kessler --- cmake/macros.cmake | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 0bb0e2a12..430b863b3 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -154,7 +154,7 @@ macro(default_plugin_options name) install(TARGETS ${name} LIBRARY DESTINATION share/xstudio/plugin) - if(WIN32) #TODO: Determine if we need to keep this limited to win32. + 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 ) @@ -435,13 +435,7 @@ endmacro() macro(set_python_to_proper_build_type) - if(WIN32) - #TODO: Debug build fails to find the appropriate Python lib. - #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:-DPy_DEBUG") - #target_compile_definitions(${PROJECT_NAME} PUBLIC "$<$:PYTHON_IS_DEBUG=0>") - # set_property(TARGET ${PROJECT_NAME} PROPERTY EXCLUDE_FROM_DEFAULT_BUILD_DEBUG TRUE) - - endif() + #TODO Resolve linking error when running debug build: https://github.com/pybind/pybind11/issues/3403 endmacro() From 4fa3afabfee74a1a9f44fc2f14d5bf5e0629a5d0 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 5 Jun 2024 11:45:44 -0700 Subject: [PATCH 30/42] Remove spatial audio stub. Signed-off-by: Michael Kessler --- .../windows_spatial_audio_output_device.hpp | 60 --- .../windows_spatial_audio_output_device.cpp | 374 ------------------ 2 files changed, 434 deletions(-) delete mode 100644 include/xstudio/audio/windows_spatial_audio_output_device.hpp delete mode 100644 src/audio/src/windows_spatial_audio_output_device.cpp diff --git a/include/xstudio/audio/windows_spatial_audio_output_device.hpp b/include/xstudio/audio/windows_spatial_audio_output_device.hpp deleted file mode 100644 index 5e567b7e7..000000000 --- a/include/xstudio/audio/windows_spatial_audio_output_device.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// 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 WindowsSpatialAudioOutputDevice : public AudioOutputDevice { - public: - WindowsSpatialAudioOutputDevice(const utility::JsonStore &prefs); - - ~WindowsSpatialAudioOutputDevice() 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 spatial_audio_client_; - - const utility::JsonStore config_; - const utility::JsonStore prefs_; - - HRESULT initializeAudioClient( - const std::wstring &sound_card = L"", - long sample_rate = 48000, - int num_channels = 2); - }; -} // namespace audio -} // namespace xstudio diff --git a/src/audio/src/windows_spatial_audio_output_device.cpp b/src/audio/src/windows_spatial_audio_output_device.cpp deleted file mode 100644 index feb71754e..000000000 --- a/src/audio/src/windows_spatial_audio_output_device.cpp +++ /dev/null @@ -1,374 +0,0 @@ -#include "xstudio/audio/windows_spatial_audio_output_device.hpp" - -#define WIN32_LEAN_AND_MEAN -#include -#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; - - -WindowsSpatialAudioOutputDevice::WindowsSpatialAudioOutputDevice( - const utility::JsonStore &prefs) - : AudioOutputDevice(), prefs_(prefs) {} - -WindowsSpatialAudioOutputDevice::~WindowsSpatialAudioOutputDevice() { - disconnect_from_soundcard(); -} - -void WindowsSpatialAudioOutputDevice::disconnect_from_soundcard() { - if (render_client_) { - render_client_ = nullptr; - } - if (audio_client_) { - audio_client_->Stop(); - audio_client_ = nullptr; - } -} - -HRESULT WindowsSpatialAudioOutputDevice::initializeAudioClient( - const std::wstring &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 == L"default") { - hr = device_enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &audio_device); - } else { - // Get the audio-render device based on the provided sound_card - hr = device_enumerator->GetDevice(sound_card.c_str(), &audio_device); - } - - if (FAILED(hr)) { - return hr; - } - - // 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 - } - PROPVARIANT audio_channel_config; - PropVariantInit(&audio_channel_config); - hr = property_store->GetValue(KSPROPERTY_AUDIO_CHANNEL_CONFIG, &audio_channel_config); - if (SUCCEEDED(hr)) { - spdlog::info("Physical Speaker Mask: {}", audio_channel_config.uintVal); - } - } - - - hr = audio_device->Activate( - __uuidof(ISpatialAudioClient), - CLSCTX_INPROC_SERVER, - nullptr, - (void **)&spatialAudioClient_); - if (FAILED(hr)) { - spdlog::warn("Unable to activate spatial audio client."); - return hr; - } else { - UINT32 dynamicCount = 0; - hr = spatialAudioClient_->GetMaxDynamicObjectCount(&dynamicCount); - if (FAILED(hr)) { - spdlog::warn("Unable to get dynmic object count from spatial audio system"); - } else { - spdlog::info("Found {} dynamic object count in spatial audio system", dynamicCount); - } - - AudioObjectType mask; - spatialAudioClient_->GetNativeStaticObjectTypeMask(&mask); - - if (FAILED(hr)) { - spdlog::warn( - "Unable to get statc object position for front-left from spatial audio system"); - } else { - spdlog::info("Found object mask for native static objects: {}", mask); - - spdlog::info( - "None {} Dynamic {} FL {} FR {} FC {} LFE {} SR {} SL {} TBL {}", - (bool)(AudioObjectType_None & mask), - (bool)(AudioObjectType_Dynamic & mask), - (bool)(AudioObjectType_FrontLeft & mask), - (bool)(AudioObjectType_FrontRight & mask), - (bool)(AudioObjectType_FrontCenter & mask), - (bool)(AudioObjectType_LowFrequency & mask), - (bool)(AudioObjectType_SideRight & mask), - (bool)(AudioObjectType_SideLeft & mask), - (bool)(AudioObjectType_TopBackLeft & mask)); - } - - HANDLE bufferCompletionEvent = CreateEvent(nullptr, false, false, nullptr); - - SpatialAudioObjectRenderStreamActivationParams streamParams; - streamParams.StaticObjectTypeMask = - AudioObjectType_FrontLeft | AudioObjectType_FrontRight; - streamParams.MinDynamicObjectCount = 0; - streamParams.MaxDynamicObjectCount = 0; - streamParams.Category = AudioCategory_Movie; - streamParams.EventHandle = bufferCompletionEvent; - streamParams.NotifyObject = nullptr; - - PROPVARIANT pv; - PropVariantInit(&pv); - pv.vt = VT_BLOB; - pv.blob.cbSize = sizeof(streamParams); - pv.blob.pBlobData = (BYTE *)&streamParams; - - CComPtr spatialAudioStream; - hr = spatialAudioClient->ActivateSpatialAudioStream( - &pv, __uuidof(spatialAudioStream), (void **)&spatialAudioStream); - } - - - // 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; - } - - // 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); - spdlog::info("Channel Mask: {}", pExtensible->dwChannelMask); - // Add more fields if needed - } - - // 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_->InitializeSharedAudioStream( - 0, - MINP, - wavefmt, - nullptr // session GUID - ); - - // Free the mix format and wave format after usage - CoTaskMemFree(pMixFormat); - CoTaskMemFree(wavefmt); - - return hr; -} - -void WindowsSpatialAudioOutputDevice::connect_to_soundcard() { - - sample_rate_ = 48000; // default values - num_channels_ = 2; - std::wstring sound_card(L"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(); - - spdlog::info("Connected to soundcard"); -} - -long WindowsSpatialAudioOutputDevice::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 WindowsSpatialAudioOutputDevice::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 WindowsSpatialAudioOutputDevice::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"); - } - } -} From 0893def0bc6b9a28c744f21b92d794de7c513e42 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 5 Jun 2024 19:55:44 -0700 Subject: [PATCH 31/42] Reorder build section Signed-off-by: Michael Kessler --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c58dd78cf..124f666e4 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,19 @@ This release of xSTUDIO can be built on various Linux flavours and Windows 10 an We provide comprehensive build steps for 4 of the most popular 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) ### Building xSTUDIO for Windows -* [Windows](docs/build_guides/windows.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. +* [Windows](docs/build_guides/windows.md) ### Building xSTUDIO for MacOS MacOS compatibility is not yet available. 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. From 7527421375b5674cf0818742b8799bc13b559863 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 5 Jun 2024 19:56:21 -0700 Subject: [PATCH 32/42] Add boost fix for latest visual studio. Signed-off-by: Michael Kessler --- docs/build_guides/windows.md | 10 +++++----- vcpkg.json | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md index 81d740d5f..a578812f1 100644 --- a/docs/build_guides/windows.md +++ b/docs/build_guides/windows.md @@ -23,7 +23,7 @@ * Clone this project to your local drive. Tips to consider: * The path should not have spaces in it. - * Ideally, keep the path short + * 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} @@ -34,16 +34,16 @@ * Open VisualStudio 2022 * Use Open Folder to point at the ${CLONE_ROOT} - * Visual Studio should start configuring the project, including downloading dependencies via VCPKG (which it bootstraps itself). + * 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` - * One built, right-click on `xStudio Project` and select `Install` - - + * 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/vcpkg.json b/vcpkg.json index 6da1cfb13..f943775a7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -28,6 +28,7 @@ { "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": "python3", "version": "3.10.7#7" }, + {"name":"boost-modular-build-helper","version":"1.84.0#3"} ] } From 42a9633d71205918e8ac92f2ba9551059b35b603 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 7 Jun 2024 10:06:14 -0700 Subject: [PATCH 33/42] fix missing space in vcpkg override. Signed-off-by: Michael Kessler --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index f943775a7..8c6b34c28 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -29,6 +29,6 @@ { "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"} + { "name":"boost-modular-build-helper","version":"1.84.0#3"} ] } From ecb42d4aba99a4ecf2e7ceb1921f3f7555bea6fb Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 7 Jun 2024 10:09:35 -0700 Subject: [PATCH 34/42] yet another formatting fix for overrides Signed-off-by: Michael Kessler --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 8c6b34c28..d543c04d7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -29,6 +29,6 @@ { "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"} + { "name":"boost-modular-build-helper","version":"1.84.0#3" } ] } From ad7c8bf5e93f69505bf26026d0ef81a6af625fd3 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Fri, 7 Jun 2024 16:17:15 -0700 Subject: [PATCH 35/42] Support getting extensions from frame sequences. Resolves #106 Signed-off-by: Michael Kessler --- CMakePresets.json | 3 ++- include/xstudio/utility/helpers.hpp | 39 +++++++++++++++++++-------- src/global_store/src/global_store.cpp | 11 ++++---- src/media/src/media_actor.cpp | 10 +++---- src/playlist/src/playlist_actor.cpp | 5 ++-- src/utility/src/helpers.cpp | 7 ++--- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 7207774d3..3856a0150 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,7 +1,8 @@ { "version": 3, "configurePresets": [ - { "name": "windows-base", + { + "name": "windows-base", "condition": { "type": "equals", "lhs": "${hostSystemName}", diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 952298eae..7b6aa1736 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -25,6 +25,7 @@ #ifdef _WIN32 #include +#include #endif namespace xstudio { @@ -378,14 +379,32 @@ inline std::string snippets_path(const std::string &append_path = "") { return get_file_mtime(uri_to_posix_path(path)); } - - inline bool is_file_supported(const caf::uri &uri) { - fs::path p(uri_to_posix_path(uri)); + inline std::string get_path_extension(const fs::path p) + { + const std::string sp = p.string(); #ifdef _WIN32 - std::string ext = to_upper(p.extension().string()); // Convert path extension to string + 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 - std::string ext = to_upper(p.extension()); + return sp.extension().string(); #endif + } + + + inline bool is_file_supported(const caf::uri &uri) { + const std::string sp = uri_to_posix_path(uri); + std::string ext = to_upper(get_path_extension(fs::path(sp))); + for (const auto &i : supported_extensions) if (i == ext) return true; @@ -394,7 +413,7 @@ inline std::string snippets_path(const std::string &append_path = "") { inline bool is_session(const std::string &path) { fs::path p(path); - std::string ext = to_upper(path_to_string(p.extension())); + std::string ext = to_upper(path_to_string(get_path_extension(p))); for (const auto &i : session_extensions) if (i == ext) return true; @@ -405,11 +424,9 @@ inline std::string snippets_path(const std::string &append_path = "") { inline bool is_timeline_supported(const caf::uri &uri) { fs::path p(uri_to_posix_path(uri)); -#ifdef _WIN32 - std::string ext = to_upper_path(p.extension()); -#else - std::string ext = to_upper(p.extension()); -#endif + 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; diff --git a/src/global_store/src/global_store.cpp b/src/global_store/src/global_store.cpp index e7ebc3bb7..247ff8d1a 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -72,7 +72,7 @@ bool xstudio::global_store::preference_load_defaults( 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; } @@ -124,7 +124,8 @@ 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()); @@ -213,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"); @@ -223,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()); diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index fbe8ff682..d3fb2f313 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -330,13 +330,9 @@ void MediaActor::init() { const FrameList &frame_list, const utility::FrameRate &rate) -> result { auto rp = make_response_promise(); -#ifdef _WIN32 - std::string ext = - ltrim_char(to_upper_path(fs::path(uri_to_posix_path(uri)).extension()), '.'); -#else - std::string ext = - ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); -#endif + + std::string ext = ltrim_char( + to_upper(get_path_extension(fs::path(uri_to_posix_path(uri)))), '.'); const auto source_uuid = Uuid::generate(); auto source = diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index 5274649ee..dce679634 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -76,9 +76,10 @@ void blocking_loader( const FrameList &frame_list = i.second; const auto uuid = Uuid::generate(); + #ifdef _WIN32 - std::string ext = - ltrim_char(to_upper_path(fs::path(uri_to_posix_path(uri)).extension()), '.'); + 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()), '.'); diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 41f401258..4f2cf3809 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -252,10 +252,6 @@ std::string xstudio::utility::uri_to_posix_path(const caf::uri &uri) { if (pos != std::string::npos) { path.erase(0, pos + 1); // +1 to erase the colon } - - - // Now, replace forward slashes with backslashes - std::replace(path.begin(), path.end(), '/', '\\'); */ #endif return path; @@ -507,7 +503,8 @@ xstudio::utility::scan_posix_path(const std::string &path, const int depth) { items.insert(items.end(), more.begin(), more.end()); } else if (fs::is_regular_file(entry)) #ifdef _WIN32 - files.push_back(entry.path().string()); + files.push_back( + std::regex_replace(entry.path().string(), std::regex("[\]"), "/")); #else files.push_back(entry.path()); #endif From 69e1856835f6eafe995820ccb34d1defbf2f65ee Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 12 Jun 2024 12:46:24 -0700 Subject: [PATCH 36/42] Fix buffer deletion alignment; issue found by Ted Signed-off-by: Michael Kessler --- include/xstudio/media_reader/buffer.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xstudio/media_reader/buffer.hpp b/include/xstudio/media_reader/buffer.hpp index e93b6d794..7cde442d6 100644 --- a/include/xstudio/media_reader/buffer.hpp +++ b/include/xstudio/media_reader/buffer.hpp @@ -65,7 +65,7 @@ namespace media_reader { struct BufferData { struct BufferDeleter { - void operator()(byte *ptr) const { operator delete[](ptr); } + void operator()(byte *ptr) const { operator delete[](ptr, std::align_val_t(1024)); } }; BufferData(byte *d) : data_(d, BufferDeleter()) {} From a19a4c34e5bbb18a2e77b776f5f33762ef04614c Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Tue, 25 Jun 2024 18:39:17 +0100 Subject: [PATCH 37/42] Various tweaks for Linux build compatibility. Signed-off-by: Ted Waine --- CMakeLists.txt | 5 +- cmake/macros.cmake | 13 +- extern/include/QuickFuture | 2 +- extern/include/gsl | 1 - extern/include/qffuture.h | 77 ++++- extern/include/qfvariantwrapper.h | 311 +++++++++++++++++- extern/include/quickfuture.h | 34 +- extern/include/reproc++ | 1 - extern/include/stduuid | 1 - extern/quickfuture/CMakeLists.txt | 43 ++- extern/quickfuture/buildlib/buildlib.pro | 2 +- extern/quickfuture/src/qffuture.cpp | 12 + extern/quickfuture/src/qmldir | 2 +- extern/quickpromise/CMakeLists.txt | 18 +- .../audio/linux_audio_output_device.hpp | 3 + include/xstudio/bookmark/bookmark.hpp | 2 +- include/xstudio/ui/qml/bookmark_model_ui.hpp | 47 +-- include/xstudio/ui/qml/embedded_python_ui.hpp | 44 +-- include/xstudio/ui/qml/event_ui.hpp | 43 +-- .../xstudio/ui/qml/global_store_model_ui.hpp | 44 +-- include/xstudio/ui/qml/helper_ui.hpp | 3 +- include/xstudio/ui/qml/hotkey_ui.hpp | 43 +-- include/xstudio/ui/qml/log_ui.hpp | 44 +-- include/xstudio/ui/qml/module_menu_ui.hpp | 43 +-- include/xstudio/ui/qml/module_ui.hpp | 43 +-- include/xstudio/ui/qml/qml_viewport.hpp | 43 +-- include/xstudio/ui/qml/session_model_ui.hpp | 43 +-- include/xstudio/ui/qml/tag_ui.hpp | 43 +-- include/xstudio/utility/chrono.hpp | 2 +- include/xstudio/utility/helpers.hpp | 5 +- include/xstudio/utility/lock_file.hpp | 4 + src/audio/src/CMakeLists.txt | 6 +- src/audio/src/audio_output_actor.cpp | 2 +- src/audio/src/linux_audio_output_device.cpp | 6 +- src/audio/src/linux_audio_output_device.hpp | 50 --- src/launch/xstudio/src/CMakeLists.txt | 27 +- src/launch/xstudio/src/xstudio.cpp | 14 - src/launch/xstudio/src/xstudio.sh.in | 9 +- src/media/src/CMakeLists.txt | 1 - .../ffprobe/src/ffprobe_lib.cpp | 2 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 7 +- src/python_module/src/CMakeLists.txt | 2 +- src/thumbnail/src/thumbnail_manager_actor.cpp | 2 + src/timeline/src/timeline_actor.cpp | 2 +- src/ui/qml/CMakeLists.txt | 6 - src/ui/qml/bookmark/src/export.h | 42 --- src/ui/qml/quickfuture/src/CMakeLists.txt | 10 - src/ui/qml/quickfuture/src/QuickFuture | 1 - src/ui/qml/quickfuture/src/export.h | 42 --- .../src/include/quickfuture_qml_export.h | 42 --- src/ui/qml/quickfuture/src/qffuture.cpp | 274 --------------- src/ui/qml/quickfuture/src/qffuture.h | 76 ----- src/ui/qml/quickfuture/src/qfvariantwrapper.h | 310 ----------------- src/ui/qml/quickfuture/src/qmldir | 1 - src/ui/qml/quickfuture/src/quickfuture.h | 33 -- .../qml/quickfuture/src/quickfuture.qmltypes | 1 - src/ui/qml/session/src/session_model_ui.cpp | 2 +- src/utility/src/helpers.cpp | 11 + src/utility/src/remote_session_file.cpp | 9 +- ui/qml/xstudio/extern/QuickPromise | 1 - 60 files changed, 592 insertions(+), 1420 deletions(-) mode change 120000 => 100644 extern/include/QuickFuture delete mode 120000 extern/include/gsl mode change 120000 => 100644 extern/include/qffuture.h mode change 120000 => 100644 extern/include/qfvariantwrapper.h mode change 120000 => 100644 extern/include/quickfuture.h delete mode 120000 extern/include/reproc++ delete mode 120000 extern/include/stduuid delete mode 100644 src/audio/src/linux_audio_output_device.hpp delete mode 100644 src/ui/qml/bookmark/src/export.h delete mode 100644 src/ui/qml/quickfuture/src/CMakeLists.txt delete mode 120000 src/ui/qml/quickfuture/src/QuickFuture delete mode 100644 src/ui/qml/quickfuture/src/export.h delete mode 100644 src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h delete mode 100644 src/ui/qml/quickfuture/src/qffuture.cpp delete mode 120000 src/ui/qml/quickfuture/src/qffuture.h delete mode 120000 src/ui/qml/quickfuture/src/qfvariantwrapper.h delete mode 120000 src/ui/qml/quickfuture/src/qmldir delete mode 120000 src/ui/qml/quickfuture/src/quickfuture.h delete mode 120000 src/ui/qml/quickfuture/src/quickfuture.qmltypes delete mode 120000 ui/qml/xstudio/extern/QuickPromise diff --git a/CMakeLists.txt b/CMakeLists.txt index db638442e..63c452607 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ 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) @@ -272,7 +272,10 @@ if(INSTALL_XSTUDIO) endif () +# add extern libs that are build-time dependencies of xstudio add_subdirectory("extern/reproc") +add_subdirectory("extern/quickfuture") +add_subdirectory("extern/quickpromise") if(USE_VCPKG) # To provide reliable ordering, we need to make this install script happen in a subdirectory. diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 430b863b3..e03e3440f 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -7,15 +7,12 @@ macro(default_compile_options name) # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> - PRIVATE $<$: - $<$:-Wno-unused-function> - $<$:/wd4100> - > + PRIVATE $<$,$>:-Wno-unused-function> + PRIVATE $<$,$>:-Wextra> + PRIVATE $<$,$>:-Wpedantic> + PRIVATE $<$,$>:/wd4100> # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> - $<$:PRIVATE $<$:-Wno-unused-function>> - $<$:PRIVATE $<$:-Wextra>> - $<$:PRIVATE $<$:-Wpedantic>> # PRIVATE ${GTEST_CFLAGS} ) @@ -52,7 +49,7 @@ if (BUILD_TESTING) # PRIVATE $<$:-Werror> $<$:PRIVATE $<$:-Wextra>> $<$:PRIVATE $<$:-Wpedantic>> - PRIVATE ${GTEST_CFLAGS} + $ PRIVATE ${GTEST_CFLAGS} ) target_compile_features(${name} diff --git a/extern/include/QuickFuture b/extern/include/QuickFuture deleted file mode 120000 index f93058bea..000000000 --- a/extern/include/QuickFuture +++ /dev/null @@ -1 +0,0 @@ -#include "../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/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/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/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/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt index 28c206b75..4b50f097e 100644 --- a/extern/quickfuture/CMakeLists.txt +++ b/extern/quickfuture/CMakeLists.txt @@ -8,8 +8,47 @@ find_package(Qt5 COMPONENTS Core Quick REQUIRED) FILE(GLOB_RECURSE SOURCES src/*.cpp src/*.h) -add_library(quickfuture STATIC ${SOURCES}) +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) \ No newline at end of file +target_include_directories(quickfuture PUBLIC src) + +set(QML_FILES + src/qmldir + src/quickfuture.qmltypes +) + +set_target_properties(quickfuture + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture" +) + +if(WIN32) + install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/QuickFuture) + install(TARGETS quickfuture LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/QuickFuture) +else() + 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 9333d7d31..2ed083cd7 100644 --- a/extern/quickpromise/CMakeLists.txt +++ b/extern/quickpromise/CMakeLists.txt @@ -49,13 +49,19 @@ 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() -if(${STATIC}) -# -else() - install(FILES ${QML_FILES} DESTINATION ${QML_INSTALL_DIR}) -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/include/xstudio/audio/linux_audio_output_device.hpp b/include/xstudio/audio/linux_audio_output_device.hpp index 14f5b6658..7964b472a 100644 --- a/include/xstudio/audio/linux_audio_output_device.hpp +++ b/include/xstudio/audio/linux_audio_output_device.hpp @@ -40,6 +40,9 @@ namespace audio { 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/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index e4450a201..8dbaff21d 100644 --- a/include/xstudio/bookmark/bookmark.hpp +++ b/include/xstudio/bookmark/bookmark.hpp @@ -239,7 +239,7 @@ namespace bookmark { std::string created() const { #ifdef _WIN32 auto dt = (created_ ? *created_ : std::chrono::high_resolution_clock::now()); -#elif +#else auto dt = (created_ ? *created_ : std::chrono::system_clock::now()); #endif return utility::to_string(dt); diff --git a/include/xstudio/ui/qml/bookmark_model_ui.hpp b/include/xstudio/ui/qml/bookmark_model_ui.hpp index 5cfc20452..79216f9e5 100644 --- a/include/xstudio/ui/qml/bookmark_model_ui.hpp +++ b/include/xstudio/ui/qml/bookmark_model_ui.hpp @@ -1,50 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - -#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 */ - - #include #include @@ -57,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" diff --git a/include/xstudio/ui/qml/embedded_python_ui.hpp b/include/xstudio/ui/qml/embedded_python_ui.hpp index bc01c477c..5166a46f6 100644 --- a/include/xstudio/ui/qml/embedded_python_ui.hpp +++ b/include/xstudio/ui/qml/embedded_python_ui.hpp @@ -2,48 +2,8 @@ #pragma once -#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 */ - +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/embedded_python_qml_export.h" #include #include diff --git a/include/xstudio/ui/qml/event_ui.hpp b/include/xstudio/ui/qml/event_ui.hpp index daec9bba4..caf5c9380 100644 --- a/include/xstudio/ui/qml/event_ui.hpp +++ b/include/xstudio/ui/qml/event_ui.hpp @@ -2,47 +2,8 @@ #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/event_qml_export.h" #include #include diff --git a/include/xstudio/ui/qml/global_store_model_ui.hpp b/include/xstudio/ui/qml/global_store_model_ui.hpp index 3f1bc4c8b..c01332a6f 100644 --- a/include/xstudio/ui/qml/global_store_model_ui.hpp +++ b/include/xstudio/ui/qml/global_store_model_ui.hpp @@ -2,48 +2,8 @@ #pragma once -#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 */ - +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/global_store_qml_export.h" #include diff --git a/include/xstudio/ui/qml/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index b6cec1bdf..42c03f22a 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -13,7 +13,8 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" -#include "helper_qml_export.h" +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/helper_qml_export.h" CAF_PUSH_WARNINGS #include diff --git a/include/xstudio/ui/qml/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index 6e4b7a715..9d798825d 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -1,47 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/viewport_qml_export.h" #include #include diff --git a/include/xstudio/ui/qml/log_ui.hpp b/include/xstudio/ui/qml/log_ui.hpp index 09f116fb1..2b2b3082f 100644 --- a/include/xstudio/ui/qml/log_ui.hpp +++ b/include/xstudio/ui/qml/log_ui.hpp @@ -3,48 +3,8 @@ #include - -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/log_qml_export.h" #include "spdlog/common.h" #include "spdlog/details/log_msg.h" diff --git a/include/xstudio/ui/qml/module_menu_ui.hpp b/include/xstudio/ui/qml/module_menu_ui.hpp index 7e49b9be8..60bf56e2f 100644 --- a/include/xstudio/ui/qml/module_menu_ui.hpp +++ b/include/xstudio/ui/qml/module_menu_ui.hpp @@ -1,47 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/module_qml_export.h" #include #include diff --git a/include/xstudio/ui/qml/module_ui.hpp b/include/xstudio/ui/qml/module_ui.hpp index 5db91e310..793cea5fb 100644 --- a/include/xstudio/ui/qml/module_ui.hpp +++ b/include/xstudio/ui/qml/module_ui.hpp @@ -1,47 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/module_qml_export.h" #include #include diff --git a/include/xstudio/ui/qml/qml_viewport.hpp b/include/xstudio/ui/qml/qml_viewport.hpp index 396f4ae83..e0a8c2a90 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -1,47 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/viewport_qml_export.h" #include diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index 198444343..108a849c9 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -1,46 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/session_qml_export.h" #include diff --git a/include/xstudio/ui/qml/tag_ui.hpp b/include/xstudio/ui/qml/tag_ui.hpp index ae1f9aa6d..f8a159f54 100644 --- a/include/xstudio/ui/qml/tag_ui.hpp +++ b/include/xstudio/ui/qml/tag_ui.hpp @@ -4,47 +4,8 @@ #pragma once -#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 */ +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/tag_qml_export.h" #include #include diff --git a/include/xstudio/utility/chrono.hpp b/include/xstudio/utility/chrono.hpp index 66934cfc0..a053a614c 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -20,7 +20,7 @@ namespace utility { #ifdef _WIN32 using sysclock = std::chrono::high_resolution_clock; #else - using sysclock = std::chrono::system_clock + using sysclock = std::chrono::system_clock; #endif using sys_time_point = sysclock::time_point; using sys_time_duration = sysclock::duration; diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 7b6aa1736..3c90c3896 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -270,12 +270,11 @@ namespace utility { inline std::string xstudio_root(const std::string &append_path) { auto root = get_env("XSTUDIO_ROOT"); + std::string fallback_root; #ifdef _WIN32 char filename[MAX_PATH]; DWORD nSize = _countof(filename); DWORD result = GetModuleFileNameA(NULL, filename, nSize); - - std::string fallback_root; if (result == 0) { spdlog::debug("Unable to determine executable path from Windows API, falling back " "to standard methods"); @@ -396,7 +395,7 @@ inline std::string snippets_path(const std::string &append_path = "") { std::string ext = pth.extension().string(); // Convert path extension to string return ext; #else - return sp.extension().string(); + return p.extension().string(); #endif } diff --git a/include/xstudio/utility/lock_file.hpp b/include/xstudio/utility/lock_file.hpp index c2ceccee9..c6d2cb479 100644 --- a/include/xstudio/utility/lock_file.hpp +++ b/include/xstudio/utility/lock_file.hpp @@ -149,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/src/audio/src/CMakeLists.txt b/src/audio/src/CMakeLists.txt index ba0d6ff19..153ca43e8 100644 --- a/src/audio/src/CMakeLists.txt +++ b/src/audio/src/CMakeLists.txt @@ -36,8 +36,10 @@ target_link_libraries(${PROJECT_NAME} if(WIN32) # Link against Windows Core Audio libraries. target_link_libraries(${PROJECT_NAME} PUBLIC "avrt.lib" "mmdevapi.lib") -elseif(UNIX) - list(APPEND LINK_DEPS pulse-simple) +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_actor.cpp b/src/audio/src/audio_output_actor.cpp index 476b74b5f..1c836a252 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -13,7 +13,7 @@ // include for system (soundcard) audio output #ifdef __linux__ -#include "linux_audio_output_device.hpp" +#include "xstudio/audio/linux_audio_output_device.hpp" #endif #ifdef _WIN32 #include "xstudio/audio/windows_audio_output_device.hpp" diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index 000cfbc48..4af41a8d3 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -86,11 +86,13 @@ long LinuxAudioOutputDevice::latency_microseconds() { } -void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long num_samples, int channel_count) { +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) < + 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); diff --git a/src/audio/src/linux_audio_output_device.hpp b/src/audio/src/linux_audio_output_device.hpp deleted file mode 100644 index 014089b60..000000000 --- a/src/audio/src/linux_audio_output_device.hpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/audio/audio_output_device.hpp" -#include "xstudio/utility/json_store.hpp" - -#include - -namespace xstudio { -namespace audio { - - /** - * @brief LinuxAudioOutputDevice class, low level interface with audio output - * - * @details - * See header for AudioOutputDevice - */ - class LinuxAudioOutputDevice : public AudioOutputDevice { - public: - LinuxAudioOutputDevice(const utility::JsonStore &prefs); - - ~LinuxAudioOutputDevice() 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, int channel_count) 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_ = {44100}; - int num_channels_ = {2}; - long buffer_size_ = {2048}; - SampleFormat sample_format_ = {SampleFormat::INT16}; - pa_simple *playback_handle_ = {nullptr}; - const utility::JsonStore config_; - const utility::JsonStore prefs_; - }; -} // namespace audio -} // namespace xstudio diff --git a/src/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index 7f043fa64..097e41559 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -30,7 +30,7 @@ configure_file(.clang-tidy .clang-tidy) if(WIN32) configure_file(xstudio.bat.in xstudio.bat) else() - configure_file(xstudio.sh.in xstudio.sh) + configure_file(xstudio.sh.in xstudio.sh @ONLY) endif() default_options(${PROJECT_NAME}) @@ -49,7 +49,6 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::qml::module xstudio::ui::qml::playhead xstudio::ui::model_data - xstudio::ui::qml::quickfuture xstudio::ui::qml::session xstudio::ui::qml::studio xstudio::ui::qml::tag @@ -69,23 +68,29 @@ target_link_libraries(${PROJECT_NAME} ZLIB::ZLIB #OTIO::opentime #OTIO::opentimelineio + quickfuture ) if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE dbghelp) endif() -set_target_properties(${PROJECT_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - if(WIN32) +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> - else() - OUTPUT_NAME "${PROJECT_NAME}.bin" - endif() - LINK_DEPENDS_NO_SHARED true -) + 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) diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index 3578e542f..a04634894 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -81,15 +81,6 @@ CAF_POP_WARNINGS #include "xstudio/ui/qml/thumbnail_provider_ui.hpp" #include "xstudio/ui/qt/offscreen_viewport.hpp" //NOLINT -//TODO: Ahead Fix -#include "QuickFuture" - -Q_DECLARE_METATYPE(QUrl) -Q_DECLARE_METATYPE(QList) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture>) -Q_DECLARE_METATYPE(QFuture) - using namespace std; using namespace caf; using namespace std::chrono_literals; @@ -1021,11 +1012,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 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/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/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index 9a8600796..e87730c2e 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -106,7 +106,7 @@ AVDictionary **init_find_stream_opts(AVFormatContext *avfc, AVDictionary *codec_ if (avfc->nb_streams) { #ifdef _WIN32 result = (AVDictionary **)av_calloc(avfc->nb_streams, sizeof(*result)); -#elif +#else result = (AVDictionary **)av_malloc_array(avfc->nb_streams, sizeof(*result)); #endif diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index 2ef8e376d..c9473e837 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "ffmpeg_stream.hpp" #include "xstudio/media/media_error.hpp" @@ -148,13 +149,13 @@ void set_shader_pix_format_info( switch (color_range) { case AVCOL_RANGE_JPEG: { Imath::V3f offset(1, 128, 128); - offset *= std::powf(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::powf(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]); @@ -163,7 +164,7 @@ void set_shader_pix_format_info( yuv_to_rgb *= scale; Imath::V3f offset(16, 128, 128); - offset *= std::powf(2, bitdepth - 8); + offset *= std::pow(2.0f, float(bitdepth - 8)); jsn["yuv_offsets"] = {"ivec3", 1, offset[0], offset[1], offset[2]}; } } diff --git a/src/python_module/src/CMakeLists.txt b/src/python_module/src/CMakeLists.txt index fa384e93d..c47f3b85b 100644 --- a/src/python_module/src/CMakeLists.txt +++ b/src/python_module/src/CMakeLists.txt @@ -1,7 +1,7 @@ project(__pybind_xstudio VERSION 0.1.0 LANGUAGES CXX) find_package(pybind11 CONFIG REQUIRED) -find_package(caf CONFIG REQUIRED) +#find_package(caf CONFIG REQUIRED) find_package(Python COMPONENTS Interpreter) set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") diff --git a/src/thumbnail/src/thumbnail_manager_actor.cpp b/src/thumbnail/src/thumbnail_manager_actor.cpp index 38d5c76ed..9118a760b 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/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index 8b2960d4d..c4b29ba22 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -651,7 +651,7 @@ void TimelineActor::init() { 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); -#elif +#else anon_send(history_, history::log_atom_v, sysclock::now(), jsn); #endif } diff --git a/src/ui/qml/CMakeLists.txt b/src/ui/qml/CMakeLists.txt index d8338d891..0554a4823 100644 --- a/src/ui/qml/CMakeLists.txt +++ b/src/ui/qml/CMakeLists.txt @@ -17,11 +17,6 @@ endif() # 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) @@ -32,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/export.h b/src/ui/qml/bookmark/src/export.h deleted file mode 100644 index cf2dd4ade..000000000 --- a/src/ui/qml/bookmark/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef BOOKMARK_QML_EXPORT_H -#define BOOKMARK_QML_EXPORT_H - -#ifdef BOOKMARK_QML_STATIC_DEFINE -# define BOOKMARK_QML_EXPORT -# define BOOKMARK_QML_NO_EXPORT -#else -# ifndef BOOKMARK_QML_EXPORT -# ifdef bookmark_qml_EXPORTS - /* We are building this library */ -# define BOOKMARK_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define BOOKMARK_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef BOOKMARK_QML_NO_EXPORT -# define BOOKMARK_QML_NO_EXPORT -# endif -#endif - -#ifndef BOOKMARK_QML_DEPRECATED -# define BOOKMARK_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef BOOKMARK_QML_DEPRECATED_EXPORT -# define BOOKMARK_QML_DEPRECATED_EXPORT BOOKMARK_QML_EXPORT BOOKMARK_QML_DEPRECATED -#endif - -#ifndef BOOKMARK_QML_DEPRECATED_NO_EXPORT -# define BOOKMARK_QML_DEPRECATED_NO_EXPORT BOOKMARK_QML_NO_EXPORT BOOKMARK_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef BOOKMARK_QML_NO_DEPRECATED -# define BOOKMARK_QML_NO_DEPRECATED -# endif -#endif - -#endif /* BOOKMARK_QML_EXPORT_H */ diff --git a/src/ui/qml/quickfuture/src/CMakeLists.txt b/src/ui/qml/quickfuture/src/CMakeLists.txt deleted file mode 100644 index 244341899..000000000 --- a/src/ui/qml/quickfuture/src/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -SET(LINK_DEPS - Qt5::Core - Qt5::Quick -) - -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 216bd6d88..000000000 --- a/src/ui/qml/quickfuture/src/QuickFuture +++ /dev/null @@ -1 +0,0 @@ -#include "../../../../../extern/quickfuture/src/QuickFuture" \ No newline at end of file diff --git a/src/ui/qml/quickfuture/src/export.h b/src/ui/qml/quickfuture/src/export.h deleted file mode 100644 index bdf00b1bc..000000000 --- a/src/ui/qml/quickfuture/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef QUICKFUTURE_QML_EXPORT_H -#define QUICKFUTURE_QML_EXPORT_H - -#ifdef QUICKFUTURE_QML_STATIC_DEFINE -# define QUICKFUTURE_QML_EXPORT -# define QUICKFUTURE_QML_NO_EXPORT -#else -# ifndef QUICKFUTURE_QML_EXPORT -# ifdef quickfuture_qml_EXPORTS - /* We are building this library */ -# define QUICKFUTURE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define QUICKFUTURE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef QUICKFUTURE_QML_NO_EXPORT -# define QUICKFUTURE_QML_NO_EXPORT -# endif -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED -# define QUICKFUTURE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED_EXPORT -# define QUICKFUTURE_QML_DEPRECATED_EXPORT QUICKFUTURE_QML_EXPORT QUICKFUTURE_QML_DEPRECATED -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED_NO_EXPORT -# define QUICKFUTURE_QML_DEPRECATED_NO_EXPORT QUICKFUTURE_QML_NO_EXPORT QUICKFUTURE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef QUICKFUTURE_QML_NO_DEPRECATED -# define QUICKFUTURE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* QUICKFUTURE_QML_EXPORT_H */ diff --git a/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h b/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h deleted file mode 100644 index bdf00b1bc..000000000 --- a/src/ui/qml/quickfuture/src/include/quickfuture_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef QUICKFUTURE_QML_EXPORT_H -#define QUICKFUTURE_QML_EXPORT_H - -#ifdef QUICKFUTURE_QML_STATIC_DEFINE -# define QUICKFUTURE_QML_EXPORT -# define QUICKFUTURE_QML_NO_EXPORT -#else -# ifndef QUICKFUTURE_QML_EXPORT -# ifdef quickfuture_qml_EXPORTS - /* We are building this library */ -# define QUICKFUTURE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define QUICKFUTURE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef QUICKFUTURE_QML_NO_EXPORT -# define QUICKFUTURE_QML_NO_EXPORT -# endif -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED -# define QUICKFUTURE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED_EXPORT -# define QUICKFUTURE_QML_DEPRECATED_EXPORT QUICKFUTURE_QML_EXPORT QUICKFUTURE_QML_DEPRECATED -#endif - -#ifndef QUICKFUTURE_QML_DEPRECATED_NO_EXPORT -# define QUICKFUTURE_QML_DEPRECATED_NO_EXPORT QUICKFUTURE_QML_NO_EXPORT QUICKFUTURE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef QUICKFUTURE_QML_NO_DEPRECATED -# define QUICKFUTURE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* QUICKFUTURE_QML_EXPORT_H */ 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 7253b49ec..000000000 --- a/src/ui/qml/quickfuture/src/qffuture.h +++ /dev/null @@ -1,76 +0,0 @@ -#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/src/ui/qml/quickfuture/src/qfvariantwrapper.h b/src/ui/qml/quickfuture/src/qfvariantwrapper.h deleted file mode 120000 index 1807bb3d8..000000000 --- a/src/ui/qml/quickfuture/src/qfvariantwrapper.h +++ /dev/null @@ -1,310 +0,0 @@ -#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/src/ui/qml/quickfuture/src/qmldir b/src/ui/qml/quickfuture/src/qmldir deleted file mode 120000 index 8693fc6e0..000000000 --- a/src/ui/qml/quickfuture/src/qmldir +++ /dev/null @@ -1 +0,0 @@ -include "../../../../../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 516980d37..000000000 --- a/src/ui/qml/quickfuture/src/quickfuture.h +++ /dev/null @@ -1,33 +0,0 @@ -#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/src/ui/qml/quickfuture/src/quickfuture.qmltypes b/src/ui/qml/quickfuture/src/quickfuture.qmltypes deleted file mode 120000 index 84ef0cd06..000000000 --- a/src/ui/qml/quickfuture/src/quickfuture.qmltypes +++ /dev/null @@ -1 +0,0 @@ -#include "../../../../../extern/quickfuture/src/quickfuture.qmltypes" \ No newline at end of file diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index 02cdaf6fe..8246975af 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -461,7 +461,7 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & setData( parent_index.parent(), - QVariant::fromValue(unsigned long(children.size())), + QVariant::fromValue(int(children.size())), mediaCountRole); } else { diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 4f2cf3809..3c370a0d2 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -591,6 +591,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); diff --git a/src/utility/src/remote_session_file.cpp b/src/utility/src/remote_session_file.cpp index 7dad932ec..94758b5bb 100644 --- a/src/utility/src/remote_session_file.cpp +++ b/src/utility/src/remote_session_file.cpp @@ -111,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() { diff --git a/ui/qml/xstudio/extern/QuickPromise b/ui/qml/xstudio/extern/QuickPromise deleted file mode 120000 index 5399b0dd6..000000000 --- a/ui/qml/xstudio/extern/QuickPromise +++ /dev/null @@ -1 +0,0 @@ -#include "../../../../extern/quickpromise/qml/QuickPromise" \ No newline at end of file From d5c5932ec9d3cc1acd5e4fc0eb74def492e49285 Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Tue, 25 Jun 2024 18:57:22 +0100 Subject: [PATCH 38/42] Adding duplicated extern headers Signed-off-by: Ted Waine --- extern/include/stduuid/gsl/gsl | 29 + extern/include/stduuid/gsl/gsl_algorithm | 63 + extern/include/stduuid/gsl/gsl_assert | 145 ++ extern/include/stduuid/gsl/gsl_byte | 181 ++ extern/include/stduuid/gsl/gsl_util | 158 ++ extern/include/stduuid/gsl/multi_span | 2242 ++++++++++++++++++++++ extern/include/stduuid/gsl/pointers | 193 ++ extern/include/stduuid/gsl/span | 766 ++++++++ extern/include/stduuid/gsl/string_span | 730 +++++++ extern/include/stduuid/uuid.h | 910 +++++++++ 10 files changed, 5417 insertions(+) create mode 100644 extern/include/stduuid/gsl/gsl create mode 100644 extern/include/stduuid/gsl/gsl_algorithm create mode 100644 extern/include/stduuid/gsl/gsl_assert create mode 100644 extern/include/stduuid/gsl/gsl_byte create mode 100644 extern/include/stduuid/gsl/gsl_util create mode 100644 extern/include/stduuid/gsl/multi_span create mode 100644 extern/include/stduuid/gsl/pointers create mode 100644 extern/include/stduuid/gsl/span create mode 100644 extern/include/stduuid/gsl/string_span create mode 100644 extern/include/stduuid/uuid.h 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..34f59e5f8 --- /dev/null +++ b/extern/include/stduuid/uuid.h @@ -0,0 +1,910 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#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))); + } + }; +} From 9b3451b3bdf7cbdecfc20303358049b535235fb5 Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Tue, 25 Jun 2024 18:58:46 +0100 Subject: [PATCH 39/42] Adding moew duplicated extern headers Signed-off-by: Ted Waine --- extern/include/gsl/gsl | 29 + extern/include/gsl/gsl_algorithm | 63 + extern/include/gsl/gsl_assert | 145 ++ extern/include/gsl/gsl_byte | 181 ++ extern/include/gsl/gsl_util | 158 ++ extern/include/gsl/multi_span | 2242 +++++++++++++++++ extern/include/gsl/pointers | 193 ++ extern/include/gsl/span | 766 ++++++ extern/include/gsl/string_span | 730 ++++++ extern/include/reproc++/arguments.hpp | 59 + extern/include/reproc++/detail/array.hpp | 53 + .../include/reproc++/detail/type_traits.hpp | 18 + extern/include/reproc++/drain.hpp | 152 ++ extern/include/reproc++/env.hpp | 76 + extern/include/reproc++/export.hpp | 21 + extern/include/reproc++/input.hpp | 37 + extern/include/reproc++/reproc.hpp | 223 ++ extern/include/reproc++/run.hpp | 41 + 18 files changed, 5187 insertions(+) create mode 100644 extern/include/gsl/gsl create mode 100644 extern/include/gsl/gsl_algorithm create mode 100644 extern/include/gsl/gsl_assert create mode 100644 extern/include/gsl/gsl_byte create mode 100644 extern/include/gsl/gsl_util create mode 100644 extern/include/gsl/multi_span create mode 100644 extern/include/gsl/pointers create mode 100644 extern/include/gsl/span create mode 100644 extern/include/gsl/string_span create mode 100644 extern/include/reproc++/arguments.hpp create mode 100644 extern/include/reproc++/detail/array.hpp create mode 100644 extern/include/reproc++/detail/type_traits.hpp create mode 100644 extern/include/reproc++/drain.hpp create mode 100644 extern/include/reproc++/env.hpp create mode 100644 extern/include/reproc++/export.hpp create mode 100644 extern/include/reproc++/input.hpp create mode 100644 extern/include/reproc++/reproc.hpp create mode 100644 extern/include/reproc++/run.hpp 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/reproc++/arguments.hpp b/extern/include/reproc++/arguments.hpp new file mode 100644 index 000000000..c542fafc0 --- /dev/null +++ b/extern/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/include/reproc++/detail/array.hpp b/extern/include/reproc++/detail/array.hpp new file mode 100644 index 000000000..a4081471a --- /dev/null +++ b/extern/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/include/reproc++/detail/type_traits.hpp b/extern/include/reproc++/detail/type_traits.hpp new file mode 100644 index 000000000..553f12755 --- /dev/null +++ b/extern/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/include/reproc++/drain.hpp b/extern/include/reproc++/drain.hpp new file mode 100644 index 000000000..90ad2efa9 --- /dev/null +++ b/extern/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/include/reproc++/env.hpp b/extern/include/reproc++/env.hpp new file mode 100644 index 000000000..144f41dc9 --- /dev/null +++ b/extern/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/include/reproc++/export.hpp b/extern/include/reproc++/export.hpp new file mode 100644 index 000000000..3eb0af0e6 --- /dev/null +++ b/extern/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/include/reproc++/input.hpp b/extern/include/reproc++/input.hpp new file mode 100644 index 000000000..e69049d26 --- /dev/null +++ b/extern/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/include/reproc++/reproc.hpp b/extern/include/reproc++/reproc.hpp new file mode 100644 index 000000000..ab6f1394a --- /dev/null +++ b/extern/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/include/reproc++/run.hpp b/extern/include/reproc++/run.hpp new file mode 100644 index 000000000..196121f72 --- /dev/null +++ b/extern/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); +} + +} From cfc31c1b6d45e199f7fbc4b1c9e3be47ed685de4 Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Fri, 5 Jul 2024 16:51:43 +0100 Subject: [PATCH 40/42] Restore Win build compatibility Signed-off-by: Ted Waine --- CMakeLists.txt | 18 +++++++++--------- CMakePresets.json | 2 +- cmake/macros.cmake | 2 ++ extern/include/cpp-httplib/httplib.h | 2 +- extern/include/stduuid/uuid.h | 3 ++- extern/quickfuture/CMakeLists.txt | 13 +++++++------ extern/stduuid/include/uuid.h | 3 ++- include/xstudio/utility/helpers.hpp | 1 - include/xstudio/utility/string_helpers.hpp | 1 + src/audio/src/windows_audio_output_device.cpp | 1 - src/python_module/src/py_messages.cpp | 2 +- src/timeline/src/item.cpp | 2 +- src/utility/src/frame_list.cpp | 3 ++- src/utility/src/helpers.cpp | 3 ++- 14 files changed, 31 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 63c452607..f5da236e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,10 +166,6 @@ if(WIN32) # https://github.com/nlohmann/json/issues/3868#issuecomment-1563726354 add_definitions(-DJSON_HAS_THREE_WAY_COMPARISON=OFF) - # build quickpromise - add_subdirectory("extern/quickpromise") - add_subdirectory("extern/quickfuture") - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # When moving to Qt6 or greater, we might be able to use qt_generate_deploy_app_script @@ -229,6 +225,15 @@ 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) @@ -272,11 +277,6 @@ if(INSTALL_XSTUDIO) endif () -# add extern libs that are build-time dependencies of xstudio -add_subdirectory("extern/reproc") -add_subdirectory("extern/quickfuture") -add_subdirectory("extern/quickpromise") - 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. diff --git a/CMakePresets.json b/CMakePresets.json index 3856a0150..d3020c537 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -14,7 +14,7 @@ "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": "D:/xstudio_install", + "CMAKE_INSTALL_PREFIX": "C:/xstudio_install3", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", "BUILD_DOCS": "OFF" } diff --git a/cmake/macros.cmake b/cmake/macros.cmake index e03e3440f..53a27b288 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -4,6 +4,7 @@ macro(default_compile_options name) # PRIVATE -fvisibility=hidden PRIVATE $<$,$>:-fno-omit-frame-pointer> PRIVATE $<$,$>:/Oy> + PRIVATE $<$,$>:/showIncludes> # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> @@ -32,6 +33,7 @@ macro(default_compile_options name) PUBLIC BINARY_DIR=\"${CMAKE_BINARY_DIR}/bin\" PRIVATE TEST_RESOURCE=\"${TEST_RESOURCE}\" PRIVATE ROOT_DIR=\"${ROOT_DIR}\" + $<$:WIN32_LEAN_AND_MEAN> PRIVATE $<$:XSTUDIO_DEBUG=1> ) endmacro() 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/stduuid/uuid.h b/extern/include/stduuid/uuid.h index 34f59e5f8..8e06b97e9 100644 --- a/extern/include/stduuid/uuid.h +++ b/extern/include/stduuid/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/extern/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt index 4b50f097e..6bc949b57 100644 --- a/extern/quickfuture/CMakeLists.txt +++ b/extern/quickfuture/CMakeLists.txt @@ -19,15 +19,16 @@ set(QML_FILES src/quickfuture.qmltypes ) -set_target_properties(quickfuture - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture" -) - if(WIN32) install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/QuickFuture) - install(TARGETS quickfuture LIBRARY 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) 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/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 3c90c3896..cfdcb426f 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -22,7 +22,6 @@ #include "xstudio/utility/string_helpers.hpp" #include "xstudio/caf_error.hpp" #include "xstudio/caf_utility/caf_setup.hpp" - #ifdef _WIN32 #include #include diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index 45b93cf39..d3d5f5248 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -4,6 +4,7 @@ #ifdef _WIN32 #include #endif + #include #include #include diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 0205eec39..f0e2890b2 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -1,4 +1,3 @@ -#define WIN32_LEAN_AND_MEAN #include #include #include diff --git a/src/python_module/src/py_messages.cpp b/src/python_module/src/py_messages.cpp index 9145d5b6c..0bce6319f 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.cpp @@ -6,6 +6,7 @@ // #include // #include // #include +#include "xstudio/utility/helpers.hpp" #include "py_opaque.hpp" @@ -15,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" diff --git a/src/timeline/src/item.cpp b/src/timeline/src/item.cpp index 9de919bd5..3b8d98747 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -2,8 +2,8 @@ #include #include -#include "xstudio/timeline/item.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/timeline/item.hpp" using namespace xstudio; using namespace xstudio::timeline; diff --git a/src/utility/src/frame_list.cpp b/src/utility/src/frame_list.cpp index f032366b0..cb9643027 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" diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 3c370a0d2..c28466394 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include "xstudio/utility/helpers.hpp" + #ifdef __linux__ #define __USE_POSIX #include @@ -18,7 +20,6 @@ //#include #include "xstudio/utility/frame_list.hpp" -#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/sequence.hpp" #include "xstudio/utility/string_helpers.hpp" From 931fe13f03e60c72201266d6ef68addec5579e7e Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Mon, 22 Jul 2024 15:04:01 -0700 Subject: [PATCH 41/42] Ran clang-format on src and includes Signed-off-by: Michael Kessler --- include/xstudio/audio/audio_output.hpp | 19 +- include/xstudio/audio/audio_output_actor.hpp | 241 +++++++++--------- include/xstudio/audio/audio_output_device.hpp | 2 +- .../audio/linux_audio_output_device.hpp | 1 - .../audio/windows_audio_output_device.hpp | 13 +- include/xstudio/global_store/global_store.hpp | 5 +- include/xstudio/media/media_actor.hpp | 1 - include/xstudio/media_reader/buffer.hpp | 6 +- include/xstudio/module/module.hpp | 3 +- .../xstudio/plugin_manager/plugin_base.hpp | 2 +- .../ui/model_data/model_data_actor.hpp | 3 +- .../ui/opengl/opengl_offscreen_renderer.hpp | 2 +- include/xstudio/ui/opengl/texture.hpp | 2 +- include/xstudio/ui/qml/actor_object.hpp | 7 +- .../xstudio/ui/qml/global_store_model_ui.hpp | 3 +- include/xstudio/ui/qml/hotkey_ui.hpp | 3 +- include/xstudio/ui/qml/model_data_ui.hpp | 2 +- .../xstudio/ui/qml/qml_viewport_renderer.hpp | 2 +- include/xstudio/ui/qml/studio_ui.hpp | 1 - include/xstudio/ui/qt/offscreen_viewport.hpp | 33 +-- include/xstudio/ui/viewport/viewport.hpp | 7 +- include/xstudio/utility/caf_helpers.hpp | 9 +- include/xstudio/utility/chrono.hpp | 10 +- include/xstudio/utility/edit_list.hpp | 2 +- include/xstudio/utility/frame_rate.hpp | 2 +- .../utility/frame_rate_and_duration.hpp | 2 +- include/xstudio/utility/helpers.hpp | 91 ++++--- include/xstudio/utility/lock_file.hpp | 4 +- include/xstudio/utility/sequence.hpp | 2 +- include/xstudio/utility/string_helpers.hpp | 8 +- src/audio/src/audio_output.cpp | 61 ++--- src/audio/src/audio_output_actor.cpp | 18 +- src/audio/src/linux_audio_output_device.cpp | 3 +- src/audio/src/windows_audio_output_device.cpp | 43 ++-- src/colour_pipeline/src/colour_pipeline.cpp | 6 +- src/embedded_python/src/embedded_python.cpp | 2 +- .../src/embedded_python_actor.cpp | 2 +- src/global/src/global_actor.cpp | 4 +- src/global_store/src/global_store.cpp | 20 +- src/json_store/src/json_store_actor.cpp | 15 +- src/launch/xstudio/src/xstudio.cpp | 25 +- src/media/src/media_actor.cpp | 15 +- src/media/src/media_source_actor.cpp | 53 ++-- src/media_cache/src/media_cache_actor.cpp | 2 +- src/media_hook/src/media_hook_actor.cpp | 6 +- src/module/src/module.cpp | 64 ++--- src/playhead/src/playhead.cpp | 1 - src/playhead/src/playhead_actor.cpp | 2 +- .../src/playhead_global_events_actor.cpp | 28 +- src/playhead/src/sub_playhead.cpp | 52 ++-- src/playlist/src/playlist_actor.cpp | 13 +- .../grading/src/grading_colour_op.cpp | 4 +- src/plugin/colour_pipeline/ocio/src/ocio.cpp | 20 +- .../colour_pipeline/ocio/src/ocio_ui.cpp | 8 +- .../src/data_source_shotgun_worker.cpp | 1 - .../media_hook/dneg/dnhook/src/dneg.cpp | 6 +- .../ffprobe/src/ffprobe_lib.cpp | 4 +- src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp | 5 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 8 +- src/plugin/utility/dneg/dnrun/src/dnrun.cpp | 2 +- src/plugin_manager/src/plugin_manager.cpp | 3 +- .../src/plugin_manager_actor.cpp | 2 +- src/scanner/src/scanner_actor.cpp | 4 +- src/session/src/session_actor.cpp | 3 +- .../src/shotgun_client_actor.cpp | 7 +- src/thumbnail/src/thumbnail_manager_actor.cpp | 2 +- src/timeline/src/item.cpp | 34 +-- src/timeline/src/timeline_actor.cpp | 60 +++-- src/ui/model_data/src/model_data_actor.cpp | 14 +- .../opengl/src/opengl_viewport_renderer.cpp | 2 +- src/ui/opengl/src/texture.cpp | 49 ++-- src/ui/qml/bookmark/src/bookmark_model_ui.cpp | 6 +- .../src/global_store_model_ui.cpp | 2 +- src/ui/qml/helper/src/model_data_ui.cpp | 9 +- src/ui/qml/playhead/src/playhead_ui.cpp | 2 +- .../session/src/session_model_methods_ui.cpp | 8 +- src/ui/qml/studio/src/studio_ui.cpp | 22 +- .../viewport/src/qml_viewport_renderer.cpp | 11 +- .../src/offscreen_viewport.cpp | 179 ++++++------- src/ui/viewport/src/viewport.cpp | 99 ++++--- .../src/viewport_frame_queue_actor.cpp | 41 +-- src/utility/src/chrono.cpp | 2 +- src/utility/src/frame_list.cpp | 2 +- src/utility/src/helpers.cpp | 6 +- src/utility/src/json_store.cpp | 4 +- src/utility/src/logging.cpp | 2 +- src/utility/src/remote_session_file.cpp | 10 +- src/utility/src/sequence.cpp | 3 +- 88 files changed, 753 insertions(+), 816 deletions(-) diff --git a/include/xstudio/audio/audio_output.hpp b/include/xstudio/audio/audio_output.hpp index 133c8f5d7..70338b38b 100644 --- a/include/xstudio/audio/audio_output.hpp +++ b/include/xstudio/audio/audio_output.hpp @@ -75,16 +75,14 @@ class AudioOutputControl { 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; + 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); @@ -101,10 +99,9 @@ class AudioOutputControl { int fade_in_out_ = {NoFade}; - bool audio_repitch_ = {false}; + bool audio_repitch_ = {false}; bool audio_scrubbing_ = {false}; - float volume_ = {100.0f}; - bool muted_ = {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 96e99752f..0ff730b0b 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -12,103 +12,101 @@ template class AudioOutputDeviceActor : public caf::event_based_actor { public: + 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; }); + } + + ); + } - 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()); - } - } + 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; @@ -117,11 +115,9 @@ class AudioOutputDeviceActor : public caf::event_based_actor { const char *name() const override { return name_.c_str(); } protected: - std::unique_ptr output_device_; private: - caf::behavior behavior_; std::string name_; bool playing_; @@ -133,18 +129,13 @@ template class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { public: - AudioOutputActor( - caf::actor_config &cfg) - : caf::event_based_actor(cfg) { - init(); - } + AudioOutputActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { init(); } ~AudioOutputActor() override = default; caf::behavior make_behavior() override { return behavior_; } private: - caf::actor audio_output_device_; void init(); @@ -160,43 +151,43 @@ class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { utility::Uuid sub_playhead_uuid_; }; -/* Singleton class that receives audio sample buffers from the current +/* 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(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()); } + 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() { +template void AudioOutputActor::init() { - //spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); + // spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); utility::print_on_exit(this, "AudioOutputControlActor"); - audio_output_device_ = spawn>(caf::actor_cast(this)); + audio_output_device_ = + spawn>(caf::actor_cast(this)); link_to(audio_output_device_); - auto global_audio_actor = system().registry().template get(audio_output_registry); + auto global_audio_actor = + system().registry().template get(audio_output_registry); utility::join_event_group(this, global_audio_actor); behavior_.assign( @@ -204,7 +195,8 @@ void AudioOutputActor::init() { [=](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); + send( + audio_output_device_, utility::event_atom_v, playhead::play_atom_v, is_playing); }, [=](get_samples_for_soundcard_atom, @@ -224,15 +216,12 @@ void AudioOutputActor::init() { } 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, + 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, @@ -240,7 +229,6 @@ void AudioOutputActor::init() { const bool playing, const bool forwards, const float velocity) { - if (!playing) { clear_queued_samples(); } else { @@ -255,7 +243,6 @@ void AudioOutputActor::init() { } ); - } } // namespace xstudio::audio diff --git a/include/xstudio/audio/audio_output_device.hpp b/include/xstudio/audio/audio_output_device.hpp index ca92875c0..c0b86f8b1 100644 --- a/include/xstudio/audio/audio_output_device.hpp +++ b/include/xstudio/audio/audio_output_device.hpp @@ -32,7 +32,7 @@ class AudioOutputDevice { * @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; /** diff --git a/include/xstudio/audio/linux_audio_output_device.hpp b/include/xstudio/audio/linux_audio_output_device.hpp index 7964b472a..2ff0005d1 100644 --- a/include/xstudio/audio/linux_audio_output_device.hpp +++ b/include/xstudio/audio/linux_audio_output_device.hpp @@ -40,7 +40,6 @@ namespace audio { static std::string name() { return "LinuxAudioOutputDevice"; } private: - void initialize_sound_card() override {} long sample_rate_ = {44100}; diff --git a/include/xstudio/audio/windows_audio_output_device.hpp b/include/xstudio/audio/windows_audio_output_device.hpp index 9a1eabe3a..ec06390b7 100644 --- a/include/xstudio/audio/windows_audio_output_device.hpp +++ b/include/xstudio/audio/windows_audio_output_device.hpp @@ -40,24 +40,21 @@ namespace audio { [[nodiscard]] SampleFormat sample_format() const override { return sample_format_; } private: - long sample_rate_ = {48000}; - int num_channels_ = {2}; - long buffer_size_ = {2048}; + 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); - + const std::string &sound_card = "", long sample_rate = 48000, int num_channels = 2); }; } // namespace audio } // namespace xstudio diff --git a/include/xstudio/global_store/global_store.hpp b/include/xstudio/global_store/global_store.hpp index fa43910e4..1fea8d83b 100644 --- a/include/xstudio/global_store/global_store.hpp +++ b/include/xstudio/global_store/global_store.hpp @@ -232,7 +232,7 @@ namespace global_store { const bool broacast_change = true) { 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( @@ -240,8 +240,7 @@ namespace global_store { const utility::JsonStore &default_, const bool async = true, const bool broacast_change = true, - const std::string &context="APPLICATION" - ); + 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/media/media_actor.hpp b/include/xstudio/media/media_actor.hpp index a787d3710..7fe34ac0b 100644 --- a/include/xstudio/media/media_actor.hpp +++ b/include/xstudio/media/media_actor.hpp @@ -96,7 +96,6 @@ namespace media { const char *name() const override { return NAME.c_str(); } private: - void update_media_status(); void update_media_detail(); diff --git a/include/xstudio/media_reader/buffer.hpp b/include/xstudio/media_reader/buffer.hpp index 7cde442d6..7bd8b0c5e 100644 --- a/include/xstudio/media_reader/buffer.hpp +++ b/include/xstudio/media_reader/buffer.hpp @@ -63,9 +63,11 @@ namespace media_reader { error_state_ = HAS_ERROR; } - struct BufferData { + struct BufferData { struct BufferDeleter { - void operator()(byte *ptr) const { operator delete[](ptr, std::align_val_t(1024)); } + void operator()(byte *ptr) const { + operator delete[](ptr, std::align_val_t(1024)); + } }; BufferData(byte *d) : data_(d, BufferDeleter()) {} diff --git a/include/xstudio/module/module.hpp b/include/xstudio/module/module.hpp index b4c81aa98..d538dd9e0 100644 --- a/include/xstudio/module/module.hpp +++ b/include/xstudio/module/module.hpp @@ -123,8 +123,7 @@ namespace module { const bool both_ways, const bool initial_push_sync); - void unlink_module( - caf::actor other_module); + 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 diff --git a/include/xstudio/plugin_manager/plugin_base.hpp b/include/xstudio/plugin_manager/plugin_base.hpp index dac633740..3189e2ff2 100644 --- a/include/xstudio/plugin_manager/plugin_base.hpp +++ b/include/xstudio/plugin_manager/plugin_base.hpp @@ -55,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; } }; diff --git a/include/xstudio/ui/model_data/model_data_actor.hpp b/include/xstudio/ui/model_data/model_data_actor.hpp index c1219fe26..91f509932 100644 --- a/include/xstudio/ui/model_data/model_data_actor.hpp +++ b/include/xstudio/ui/model_data/model_data_actor.hpp @@ -97,7 +97,8 @@ namespace ui { void push_to_prefs(const std::string &model_name, const bool actually_push = false); - void remove_attribute_from_model(const std::string &model_name, const utility::Uuid &attr_uuid); + void 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); diff --git a/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp index e428cf0be..1d09c7c91 100644 --- a/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp @@ -13,7 +13,7 @@ namespace ui { class OpenGLOffscreenRenderer { public: explicit OpenGLOffscreenRenderer(GLint color_format); - OpenGLOffscreenRenderer(const OpenGLOffscreenRenderer &) = delete; + OpenGLOffscreenRenderer(const OpenGLOffscreenRenderer &) = delete; OpenGLOffscreenRenderer &operator=(const OpenGLOffscreenRenderer &) = delete; ~OpenGLOffscreenRenderer(); diff --git a/include/xstudio/ui/opengl/texture.hpp b/include/xstudio/ui/opengl/texture.hpp index d30e88065..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 { diff --git a/include/xstudio/ui/qml/actor_object.hpp b/include/xstudio/ui/qml/actor_object.hpp index 6a3e3fe94..2c5f6e943 100644 --- a/include/xstudio/ui/qml/actor_object.hpp +++ b/include/xstudio/ui/qml/actor_object.hpp @@ -54,9 +54,10 @@ class actor_object : public Base { } }; - //TODO: Ahead This is a bad hack for windows to make it compile currently, possible solution is to pass - // JsonTreeModel as a reference or a pointer. - template < + // 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)...) { diff --git a/include/xstudio/ui/qml/global_store_model_ui.hpp b/include/xstudio/ui/qml/global_store_model_ui.hpp index c01332a6f..e09ba4c8d 100644 --- a/include/xstudio/ui/qml/global_store_model_ui.hpp +++ b/include/xstudio/ui/qml/global_store_model_ui.hpp @@ -22,7 +22,8 @@ class GlobalStoreHelper; namespace xstudio::ui::qml { using namespace caf; -class GLOBAL_STORE_QML_EXPORT 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/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index 9d798825d..3c309f5c5 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -24,7 +24,8 @@ namespace utility { namespace ui { namespace qml { - class VIEWPORT_QML_EXPORT HotkeysUI : public caf::mixin::actor_object { + class VIEWPORT_QML_EXPORT HotkeysUI + : public caf::mixin::actor_object { Q_OBJECT diff --git a/include/xstudio/ui/qml/model_data_ui.hpp b/include/xstudio/ui/qml/model_data_ui.hpp index 0ad95e8da..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 diff --git a/include/xstudio/ui/qml/qml_viewport_renderer.hpp b/include/xstudio/ui/qml/qml_viewport_renderer.hpp index c917ef90f..b59d6c210 100644 --- a/include/xstudio/ui/qml/qml_viewport_renderer.hpp +++ b/include/xstudio/ui/qml/qml_viewport_renderer.hpp @@ -79,7 +79,7 @@ namespace ui { [[nodiscard]] QString name() const { return QStringFromStd(viewport_renderer_->name()); } - + void linkToViewport(QMLViewportRenderer *other_viewport); void renderImageToFile( diff --git a/include/xstudio/ui/qml/studio_ui.hpp b/include/xstudio/ui/qml/studio_ui.hpp index d42c09d26..48e71eb66 100644 --- a/include/xstudio/ui/qml/studio_ui.hpp +++ b/include/xstudio/ui/qml/studio_ui.hpp @@ -81,7 +81,6 @@ namespace ui { 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/qt/offscreen_viewport.hpp b/include/xstudio/ui/qt/offscreen_viewport.hpp index 6f111ecf1..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 @@ -36,12 +36,12 @@ namespace ui { void setPlayhead(const QString &playheadAddress); std::string name() { return viewport_renderer_->name(); } - + void stop(); - + public slots: - void autoDelete(); + void autoDelete(); private: void receive_change_notification(viewport::Viewport::ChangeCallbackId id); @@ -61,7 +61,10 @@ namespace ui { 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); + const int w, + const int h, + media_reader::ImageBufPtr &image, + const viewport::ImageFormat format); void initGL(); @@ -80,7 +83,8 @@ namespace ui { const caf::uri path, const std::string &ext); - void setupTextureAndFrameBuffer(const int width, const int height, const viewport::ImageFormat format); + void setupTextureAndFrameBuffer( + const int width, const int height, const viewport::ImageFormat format); void make_conversion_lut(); @@ -95,23 +99,22 @@ namespace ui { // 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; + 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; + 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/viewport.hpp b/include/xstudio/ui/viewport/viewport.hpp index b3051673d..384a78843 100644 --- a/include/xstudio/ui/viewport/viewport.hpp +++ b/include/xstudio/ui/viewport/viewport.hpp @@ -38,7 +38,7 @@ namespace ui { caf::actor parent_actor, const int viewport_index, ViewportRendererPtr the_renderer, - const std::string & name = std::string()); + const std::string &name = std::string()); virtual ~Viewport(); bool process_pointer_event(PointerEvent &); @@ -138,7 +138,7 @@ 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); /** @@ -289,7 +289,8 @@ namespace ui { media_reader::ImageBufPtr get_onscreen_image(); - void set_aux_shader_uniforms(const utility::JsonStore & j, const bool clear_and_overwrite = false); + void set_aux_shader_uniforms( + const utility::JsonStore &j, const bool clear_and_overwrite = false); protected: void register_hotkeys() override; diff --git a/include/xstudio/utility/caf_helpers.hpp b/include/xstudio/utility/caf_helpers.hpp index 075c65fee..447ff961c 100644 --- a/include/xstudio/utility/caf_helpers.hpp +++ b/include/xstudio/utility/caf_helpers.hpp @@ -29,17 +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;; + 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 a053a614c..68b4f3d45 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -27,11 +27,11 @@ namespace utility { 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 + 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; 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/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 cfdcb426f..ea4535d0c 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -90,7 +90,8 @@ namespace utility { namespace fs = std::filesystem; - // Centralizing the Path to String conversions in case we run into encoding problems down the line. + // 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(); @@ -270,7 +271,7 @@ namespace utility { auto root = get_env("XSTUDIO_ROOT"); std::string fallback_root; - #ifdef _WIN32 +#ifdef _WIN32 char filename[MAX_PATH]; DWORD nSize = _countof(filename); DWORD result = GetModuleFileNameA(NULL, filename, nSize); @@ -280,73 +281,72 @@ namespace utility { } 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. + // 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 +#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); + std::string path = (root ? (*root) + append_path : fallback_root + append_path); return path; } -inline std::string remote_session_path() { - const char* root; + inline std::string remote_session_path() { + const char *root; #ifdef _WIN32 - root = std::getenv("USERPROFILE"); + root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + root = std::getenv("HOME"); #endif - std::filesystem::path path; - if (root) - { - path = std::filesystem::path(root) / ".config" / "DNEG" / "xstudio" / "sessions"; - } + std::filesystem::path path; + if (root) { + path = std::filesystem::path(root) / ".config" / "DNEG" / "xstudio" / "sessions"; + } - return path.string(); -} + return path.string(); + } -inline std::string preference_path(const std::string &append_path = "") { - const char *root; + inline std::string preference_path(const std::string &append_path = "") { + const char *root; #ifdef _WIN32 - root = std::getenv("USERPROFILE"); + root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + 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; + 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(); -} + return path.string(); + } -inline std::string snippets_path(const std::string &append_path = "") { - const char *root; + inline std::string snippets_path(const std::string &append_path = "") { + const char *root; #ifdef _WIN32 - root = std::getenv("USERPROFILE"); + root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + 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; + 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(); -} + return path.string(); + } inline std::string preference_path_context(const std::string &context) { return preference_path(to_lower(context) + ".json"); @@ -377,8 +377,7 @@ inline std::string snippets_path(const std::string &append_path = "") { return get_file_mtime(uri_to_posix_path(path)); } - inline std::string get_path_extension(const fs::path p) - { + inline std::string get_path_extension(const fs::path p) { const std::string sp = p.string(); #ifdef _WIN32 std::string sanitized; diff --git a/include/xstudio/utility/lock_file.hpp b/include/xstudio/utility/lock_file.hpp index c6d2cb479..115138718 100644 --- a/include/xstudio/utility/lock_file.hpp +++ b/include/xstudio/utility/lock_file.hpp @@ -53,7 +53,7 @@ inline struct passwd *getpwuid(uid_t uid) { } #else // For Linux or non-Windows platforms -using uid_t = uid_t; +using uid_t = uid_t; using gid_t = gid_t; #include #endif @@ -116,7 +116,7 @@ namespace utility { lpath = fs::canonical(lpath); return posix_path_to_uri(lpath + ".lock"); -#endif +#endif } [[nodiscard]] bool locked() const { return locked_; } [[nodiscard]] bool owned() const { return owned_; } diff --git a/include/xstudio/utility/sequence.hpp b/include/xstudio/utility/sequence.hpp index c2e0e7a92..fcbc48a8b 100644 --- a/include/xstudio/utility/sequence.hpp +++ b/include/xstudio/utility/sequence.hpp @@ -7,7 +7,7 @@ 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 uid_t = uid_t; using gid_t = gid_t; #endif diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index d3d5f5248..ef21c8cbc 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -183,7 +183,7 @@ namespace utility { return result; } - inline std::wstring to_upper(const std::wstring& str) { + inline std::wstring to_upper(const std::wstring &str) { static std::locale loc; std::wstring result; result.reserve(str.size()); @@ -204,9 +204,9 @@ namespace utility { return result; } - - //TODO: Ahead to refactor - inline std::string to_upper_path(const std::filesystem::path &path) { + + // 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()); diff --git a/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index c06968578..1b4d2a37f 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -131,7 +131,7 @@ void AudioOutputControl::prepare_samples_for_soundcard( try { - v.resize(num_samps_to_push * num_channels); + v.resize(num_samps_to_push * num_channels); memset(v.data(), 0, v.size() * sizeof(int16_t)); @@ -173,14 +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."); + // 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."); + // spdlog::warn("Break hit because both current_buf_ and sample_data_ are + // empty."); break; } @@ -195,11 +197,11 @@ 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."); + // spdlog::info("Current buffer is exhausted."); previous_buf_ = current_buf_; current_buf_.reset(); } else { - //spdlog::warn("Break hit due to unspecified condition."); + // spdlog::warn("Break hit due to unspecified condition."); break; } } @@ -230,32 +232,30 @@ void AudioOutputControl::queue_samples_for_playing( } 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(); + // 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 + // 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; + // 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) { @@ -267,16 +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."); + // 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()); + } + + // 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 @@ -292,8 +293,9 @@ void AudioOutputControl::queue_samples_for_playing( 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."); + // spdlog::info("Found and erasing existing audio sample from queue with the + // " + // "same media key."); sample_data_.erase(p); break; } @@ -301,7 +303,6 @@ void AudioOutputControl::queue_samples_for_playing( } - if (audio_repitch_ && playback_velocity_ != 1.0f) { audio_frame = super_simple_respeed_audio_buffer( audio_frame, fabs(playback_velocity_)); diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index 1c836a252..cfbdd1da7 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -25,8 +25,7 @@ using namespace xstudio::utility; using namespace xstudio; GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) - : caf::event_based_actor(cfg), module::Module("GlobalAudioOutputActor") -{ + : caf::event_based_actor(cfg), module::Module("GlobalAudioOutputActor") { audio_repitch_ = add_boolean_attribute("Audio Repitch", "Audio Repitch", false); audio_repitch_->set_role_data( @@ -58,9 +57,7 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) behavior_.assign( - [=](utility::get_event_group_atom) -> caf::actor { - return event_group_; - }, + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -73,8 +70,8 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) const bool playing, const bool forwards, const float velocity) { - - send(event_group_, + send( + event_group_, utility::event_atom_v, playhead::sound_audio_atom_v, audio_buffers, @@ -82,15 +79,11 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) playing, forwards, velocity); - } ); connect_to_ui(); - - - } void GlobalAudioOutputActor::on_exit() { system().registry().erase(audio_output_registry); } @@ -98,7 +91,8 @@ void GlobalAudioOutputActor::on_exit() { system().registry().erase(audio_output_ void GlobalAudioOutputActor::attribute_changed(const utility::Uuid &attr_uuid, const int role) { // update and audio output clients with volume, mute etc. - send(event_group_, + send( + event_group_, utility::event_atom_v, module::change_attribute_event_atom_v, volume_->value(), diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index 4af41a8d3..9dc3e7e0c 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -92,8 +92,7 @@ void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long nu // 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, &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 index f0e2890b2..fb6771638 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -20,16 +20,11 @@ using namespace xstudio::global_store; WindowsAudioOutputDevice::WindowsAudioOutputDevice(const utility::JsonStore &prefs) - : AudioOutputDevice(), prefs_(prefs) { -} + : AudioOutputDevice(), prefs_(prefs) {} -WindowsAudioOutputDevice::~WindowsAudioOutputDevice() { - disconnect_from_soundcard(); -} +WindowsAudioOutputDevice::~WindowsAudioOutputDevice() { disconnect_from_soundcard(); } -void WindowsAudioOutputDevice::disconnect_from_soundcard() { - return; -} +void WindowsAudioOutputDevice::disconnect_from_soundcard() { return; } HRESULT WindowsAudioOutputDevice::initializeAudioClient( const std::string &sound_card /* = L"" */, @@ -67,7 +62,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( return hr; } - #if false +#if false // Print the device name CComPtr property_store; hr = audio_device->OpenPropertyStore(STGM_READ, &property_store); @@ -82,7 +77,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( // memory it might've allocated } } - #endif +#endif // Get an IAudioClient3 instance hr = audio_device->Activate( @@ -104,7 +99,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( sample_rate_ = pMixFormat->nSamplesPerSec; - #if false +#if false // Print the mix format details spdlog::info("Mix Format Details:"); spdlog::info("Format Tag: {}", pMixFormat->wFormatTag); @@ -136,7 +131,7 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( ); } - #endif +#endif // Fetch the currently active shared mode format WAVEFORMATEX *wavefmt = NULL; @@ -174,12 +169,11 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( CoTaskMemFree(wavefmt); return hr; - } void WindowsAudioOutputDevice::initialize_sound_card() { - sample_rate_ = 48000; //default values + sample_rate_ = 48000; // default values num_channels_ = 2; std::string sound_card("default"); buffer_size_ = 2048; // Adjust to match your preferences @@ -190,7 +184,8 @@ void WindowsAudioOutputDevice::initialize_sound_card() { 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"); + 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) { @@ -199,20 +194,20 @@ void WindowsAudioOutputDevice::initialize_sound_card() { 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); + 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_)); + 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() { @@ -224,7 +219,7 @@ long WindowsAudioOutputDevice::desired_samples() { // 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"); @@ -251,8 +246,7 @@ long WindowsAudioOutputDevice::latency_microseconds() { return defaultDevicePeriod / 10; // convert 100-nanosecond units to microseconds } -void WindowsAudioOutputDevice::push_samples( - const void *sample_data, const long num_samples) { +void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { int channel_count = num_channels_; @@ -266,7 +260,7 @@ void WindowsAudioOutputDevice::push_samples( // Ensure we have a valid render_client_ if (!render_client_) { - //spdlog::error("Invalid Render Client"); + // spdlog::error("Invalid Render Client"); return; // Exit if no render client is set } @@ -326,7 +320,4 @@ void WindowsAudioOutputDevice::push_samples( // Avoid tight loop thrashing when we are out of samples. std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - - - } diff --git a/src/colour_pipeline/src/colour_pipeline.cpp b/src/colour_pipeline/src/colour_pipeline.cpp index 2bd322c75..75d41569a 100644 --- a/src/colour_pipeline/src/colour_pipeline.cpp +++ b/src/colour_pipeline/src/colour_pipeline.cpp @@ -65,9 +65,9 @@ caf::message_handler ColourPipeline::message_handler_extensions() { if (worker) { link_to_module( worker, - true, // link_all_attrs - false, // both_ways - true // initial_push_sync + true, // link_all_attrs + false, // both_ways + true // initial_push_sync ); workers_.push_back(worker); } diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index dd78eb7e5..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) { diff --git a/src/embedded_python/src/embedded_python_actor.cpp b/src/embedded_python/src/embedded_python_actor.cpp index bad1d2dcb..d194fde59 100644 --- a/src/embedded_python/src/embedded_python_actor.cpp +++ b/src/embedded_python/src/embedded_python_actor.cpp @@ -26,7 +26,7 @@ using namespace nlohmann; using namespace caf; using namespace pybind11::literals; -namespace py = pybind11; +namespace py = pybind11; #ifdef BUILD_OTIO namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index a81f0bb12..b53db7dbe 100644 --- a/src/global/src/global_actor.cpp +++ b/src/global/src/global_actor.cpp @@ -133,13 +133,13 @@ void GlobalActor::init(const utility::JsonStore &prefs) { link_to(thumbnail); link_to(ui_models); - + // Make default audio output #ifdef __linux__ auto audio_out = spawn>(); link_to(audio_out); #elif __APPLE__ - // TO DO + // TO DO #elif _WIN32 auto audio_out = spawn>(); link_to(audio_out); diff --git a/src/global_store/src/global_store.cpp b/src/global_store/src/global_store.cpp index 247ff8d1a..5b6d583d4 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -124,8 +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 - get_path_extension(tmp) == ".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()); @@ -192,7 +191,8 @@ 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); - if (set_as_overridden) 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())); @@ -363,15 +363,14 @@ utility::JsonStore GlobalStoreHelper::get_existing_or_create_new_preference( const utility::JsonStore &default_, const bool async, const bool broacast_change, - const std::string &context) -{ + 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"}); + v["path"] = path; + v["context"] = std::vector({"APPLICATION"}); JsonStoreHelper::set(v, path, async, broacast_change); } return v["value"]; @@ -379,12 +378,11 @@ utility::JsonStore GlobalStoreHelper::get_existing_or_create_new_preference( } catch (...) { utility::JsonStore v; - v["value"] = default_; + v["value"] = default_; v["overridden_value"] = default_; - v["path"] = path; - v["context"] = std::vector({"APPLICATION"}); + 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/json_store/src/json_store_actor.cpp b/src/json_store/src/json_store_actor.cpp index c6fa2be83..cc3e873fa 100644 --- a/src/json_store/src/json_store_actor.cpp +++ b/src/json_store/src/json_store_actor.cpp @@ -53,7 +53,7 @@ JsonStoreActor::JsonStoreActor( [=](erase_json_atom, const std::string &path) -> bool { std::string p = path; - auto result = json_store_.remove(path); + auto result = json_store_.remove(path); if (result) broadcast_change(); return result; @@ -61,7 +61,7 @@ JsonStoreActor::JsonStoreActor( [=](patch_atom, const JsonStore &json) -> bool { const JsonStore j = json; - json_store_ = json_store_.patch(j); + json_store_ = json_store_.patch(j); broadcast_change(); return true; }, @@ -91,7 +91,7 @@ JsonStoreActor::JsonStoreActor( [=](set_json_atom, const JsonStore &json, const std::string &path, const bool async) -> bool { // is it a subset - std::string p = path; + std::string p = path; const JsonStore j = json; try { json_store_.set(j, p); @@ -124,12 +124,11 @@ JsonStoreActor::JsonStoreActor( [=](subscribe_atom, const std::string &path, caf::actor _actor) -> caf::result { // delegate to reader, return promise ? - std::string p = path; - auto rp = make_response_promise(); + std::string p = path; + auto rp = make_response_promise(); this->request(_actor, caf::infinite, utility::get_group_atom_v) .then( - [&, p, _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] = p; @@ -191,7 +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; + std::string p = path; if (broadcast_delay_.count() and async) { if (not update_pending_) { delayed_anon_send(this, broadcast_delay_, jsonstore_change_atom_v); diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index a04634894..76501dc46 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -124,14 +124,13 @@ struct ExitTimeoutKiller { void stop() { #ifdef _WIN32 - spdlog::debug("ExitTimeoutKiller stop ignored"); - } + 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; @@ -548,7 +547,7 @@ struct Launcher { // prefs files *might* be located in a 'preference' subfolder under XSTUDIO_PLUGIN_PATH // folders - char * plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); + 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")) @@ -681,10 +680,8 @@ struct Launcher { media_rate); } catch (const std::exception &e) { spdlog::error("Failed to load media '{}'", e.what()); - - } - + } else { spdlog::warn("Invalid URI {}", p); } @@ -898,14 +895,14 @@ int main(int argc, char **argv) { if (l.actions["headless"]) { system.await_actors_before_shutdown(true); - //TODO: Ahead Fix - //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.. @@ -1051,7 +1048,7 @@ 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"); + 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")); diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index d3fb2f313..11f3997f4 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -331,8 +331,8 @@ void MediaActor::init() { const utility::FrameRate &rate) -> result { auto rp = make_response_promise(); - std::string ext = ltrim_char( - to_upper(get_path_extension(fs::path(uri_to_posix_path(uri)))), '.'); + std::string ext = + ltrim_char(to_upper(get_path_extension(fs::path(uri_to_posix_path(uri)))), '.'); const auto source_uuid = Uuid::generate(); auto source = @@ -1467,7 +1467,7 @@ void MediaActor::auto_set_current_source(const media::MediaType media_type) { // 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()); @@ -1477,16 +1477,13 @@ void MediaActor::auto_set_current_source(const media::MediaType media_type) { try { auto stream_details = request_receive>( - *sys, - source_actor, - detail_atom_v, - media_type); + *sys, source_actor, detail_atom_v, media_type); if (stream_details.size()) sources_matching_media_type.insert(source_uuid); - } catch (...) {} + } 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 855abd8ac..f4dd3db09 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -439,15 +439,17 @@ void MediaSourceActor::init() { [=](current_media_stream_atom, const MediaType media_type) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v).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; + 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 { @@ -518,27 +520,26 @@ void MediaSourceActor::init() { [=](get_edit_list_atom, const MediaType media_type, const Uuid &uuid) -> result { - auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v).then( - [=](bool) mutable { - if (base_.current(media_type).is_null()) { - rp.deliver(make_error(xstudio_error::error, "No streams")); - } + 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()) - rp.deliver(utility::EditList({EditListSection( - base_.uuid(), + 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())})); - 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; - + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, [=](get_media_pointer_atom, diff --git a/src/media_cache/src/media_cache_actor.cpp b/src/media_cache/src/media_cache_actor.cpp index 0285e181f..bdd51ad06 100644 --- a/src/media_cache/src/media_cache_actor.cpp +++ b/src/media_cache/src/media_cache_actor.cpp @@ -36,7 +36,7 @@ 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 diff --git a/src/media_hook/src/media_hook_actor.cpp b/src/media_hook/src/media_hook_actor.cpp index 545eca3cf..a5c44caa8 100644 --- a/src/media_hook/src/media_hook_actor.cpp +++ b/src/media_hook/src/media_hook_actor.cpp @@ -94,10 +94,10 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) }, [=](get_media_hook_atom, caf::actor media_source) -> result { - auto rp = make_response_promise(); + auto rp = make_response_promise(); - if (hooks.empty()){ - rp.deliver(true); + if (hooks.empty()) { + rp.deliver(true); return rp; } diff --git a/src/module/src/module.cpp b/src/module/src/module.cpp index 04fb70dd6..1e6d9f576 100644 --- a/src/module/src/module.cpp +++ b/src/module/src/module.cpp @@ -146,10 +146,9 @@ void Module::link_to_module( } } -void Module::unlink_module(caf::actor other_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); + 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); @@ -162,8 +161,7 @@ void Module::unlink_module(caf::actor other_module) } if (found_link) { - anon_send( - other_module, module::link_module_atom_v, self(), false); + anon_send(other_module, module::link_module_atom_v, self(), false); } } @@ -305,13 +303,10 @@ bool Module::remove_attribute(const utility::Uuid &attribute_uuid) { } else { throw std::runtime_error( fmt::format( - "{}: No attribute with id {}", - __PRETTY_FUNCTION__, - to_string(attribute_uuid)).c_str() - ); + "{}: No attribute with id {}", __PRETTY_FUNCTION__, to_string(attribute_uuid)) + .c_str()); } return true; - } utility::JsonStore Module::serialise() const { @@ -636,17 +631,14 @@ 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; - - }, + [=](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 { @@ -693,12 +685,10 @@ 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); - } + [=](link_module_atom, caf::actor linkwith, bool unlink) { + if (unlink) { + unlink_module(linkwith); + } }, [=](connect_to_ui_atom) { connect_to_ui(); }, @@ -922,30 +912,24 @@ void Module::notify_change( } if (role == Attribute::PreferencePath) { - - // looks like the preference path is being set on the attribute. Note + + // 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() - ); + xstudio::utility::ActorSystemSingleton::actor_system_ref()); std::string pref_path; try { - pref_path = attr->get_role_data(Attribute::PreferencePath); + 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) { + pref_path, attr->role_data_as_json(Attribute::Value), true, false)); + + } catch (std::exception &e) { spdlog::warn("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); } diff --git a/src/playhead/src/playhead.cpp b/src/playhead/src/playhead.cpp index ed133a652..d34a722c2 100644 --- a/src/playhead/src/playhead.cpp +++ b/src/playhead/src/playhead.cpp @@ -160,7 +160,6 @@ void PlayheadBase::add_attributes() { 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"); - } diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index 4b7e24ee5..e37fccbd4 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -1350,7 +1350,7 @@ void PlayheadActor::switch_key_playhead(int idx) { // 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 diff --git a/src/playhead/src/playhead_global_events_actor.cpp b/src/playhead/src/playhead_global_events_actor.cpp index 80e05347f..955cbf4b2 100644 --- a/src/playhead/src/playhead_global_events_actor.cpp +++ b/src/playhead/src/playhead_global_events_actor.cpp @@ -84,19 +84,25 @@ void PlayheadGlobalEventsActor::init() { playhead); on_screen_playhead_ = playhead; if (playhead) { - // force an event broadcast for the on-screen media and + // 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 &) {}); + 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); } diff --git a/src/playhead/src/sub_playhead.cpp b/src/playhead/src/sub_playhead.cpp index 5e4404548..73d7d7f38 100644 --- a/src/playhead/src/sub_playhead.cpp +++ b/src/playhead/src/sub_playhead.cpp @@ -421,24 +421,23 @@ void SubPlayhead::init() { }, [=](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); }); + 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; }, @@ -479,23 +478,24 @@ void SubPlayhead::init() { }, [=](media_atom) -> result { - // MediaActor at current playhead position auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, media_source_atom_v).then( - [=](caf::actor media_source) mutable { - if (!media_source) - rp.deliver(caf::actor()); - else { - request(media_source, infinite, utility::parent_atom_v) - .then( - [=](caf::actor media_actor) mutable { rp.deliver(media_actor); }, - [=](const error &err) mutable { rp.deliver(err); }); - } - - }, - [=](const error &err) mutable { rp.deliver(err); }); + 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; }, diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index dce679634..6e5634b40 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -368,7 +368,7 @@ void PlaylistActor::init() { std::string ext = ltrim_char(to_upper_path(fs::path(uri_to_posix_path(uri)).extension()), '.'); #else - std::string ext = + std::string ext = ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); #endif const auto source_uuid = Uuid::generate(); @@ -431,7 +431,7 @@ void PlaylistActor::init() { std::string ext = ltrim_char(to_upper_path(fs::path(uri_to_posix_path(uri)).extension()), '.'); #else - std::string ext = + std::string ext = ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); #endif const auto source_uuid = Uuid::generate(); @@ -623,9 +623,9 @@ void PlaylistActor::init() { // duration and frame rate must be known up-front std::vector media_actors = ma; - auto source_count = std::make_shared(); - (*source_count) = media_actors.size(); - auto rp = make_response_promise(); + auto source_count = std::make_shared(); + (*source_count) = media_actors.size(); + auto rp = make_response_promise(); // add to lis first, then lazy update.. @@ -1938,7 +1938,8 @@ void PlaylistActor::add_media( open_media_reader(ua.actor()); }, [=](error &err) mutable { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, to_string(err), to_string(ua.actor())); + spdlog::warn( + "{} {} {}", __PRETTY_FUNCTION__, to_string(err), to_string(ua.actor())); send_content_changed_event(); base_.send_changed(event_group_, this); rp.deliver(ua); diff --git a/src/plugin/colour_op/grading/src/grading_colour_op.cpp b/src/plugin/colour_op/grading/src/grading_colour_op.cpp index 6224a59f6..cb45e9bdf 100644 --- a/src/plugin/colour_op/grading/src/grading_colour_op.cpp +++ b/src/plugin/colour_op/grading/src/grading_colour_op.cpp @@ -471,8 +471,8 @@ GradingColourOperator::setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader : LUTDescriptor::NEAREST; auto xs_lut = std::make_shared( height > 1 - ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) - : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), + ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) + : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), samplerName); const int channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL ? 1 : 3; diff --git a/src/plugin/colour_pipeline/ocio/src/ocio.cpp b/src/plugin/colour_pipeline/ocio/src/ocio.cpp index e65c62b3c..b3507f586 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio.cpp @@ -521,23 +521,23 @@ std::string OCIOColourPipeline::input_space_for_view( std::string new_colourspace; - auto colourspace_or = [media_param](const std::string &cs, const std::string &fallback){ + 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"]; + 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"); + 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"); + 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"); + new_colourspace = + is_untonemapped ? "disp_sRGB" : colourspace_or("DNEG_sRGB", "Film_sRGB"); } // Double check the new colourspace actually exists @@ -1041,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; diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp index 6acef9957..220c7107e 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp @@ -411,7 +411,8 @@ void OCIOColourPipeline::setup_ui() { // Source colour space mode - adjust_source_ = add_boolean_attribute(ui_text_.SOURCE_CS_MODE, ui_text_.SOURCE_CS_MODE_SHORT, true); + 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( @@ -607,12 +608,13 @@ 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) { +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; + MediaParams update_media_param = media_param; update_media_param.user_input_cs = new_cs; set_media_params(update_media_param); 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 index c354ccc20..4a8dd4bc6 100644 --- 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 @@ -83,7 +83,6 @@ void ShotgunMediaWorker::add_media_step_3( request(media, infinite, media::add_media_source_atom_v, srcs) .then( [=](const bool) mutable { - rp.deliver(true); // push metadata to media actor. anon_send( diff --git a/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp index ffa96d09f..167ce6d33 100644 --- a/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp +++ b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp @@ -333,8 +333,8 @@ class DNegMediaHook : public MediaHook { 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(); + const bool has_untonemapped_view = + std::find(views.begin(), views.end(), "Un-tone-mapped") != views.end(); // Input media category detection @@ -474,7 +474,7 @@ class DNegMediaHook : public MediaHook { r["viewing_rules"] = true; } else { - r["ocio_config"] = "__raw__"; + r["ocio_config"] = "__raw__"; r["working_space"] = "raw"; } diff --git a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index e87730c2e..b420339d9 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -25,7 +25,9 @@ using namespace xstudio::ffprobe; namespace { -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 +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); diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp index 7b3db4077..313c75f5e 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp @@ -266,12 +266,13 @@ void FFMpegMediaReader::update_preferences(const utility::JsonStore &prefs) { try { readers_per_source_ = preference_value(prefs, "/plugin/media_reader/FFMPEG/readers_per_source"); -#ifdef __linux__ +#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"); + soundcard_sample_rate_ = + preference_value(prefs, "/core/audio/windows_audio_prefs/sample_rate"); #endif } catch (const std::exception &e) { diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index c9473e837..f8ee75459 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -544,10 +544,10 @@ 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)); + // 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); diff --git a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp index 0aed07b56..c4012baea 100644 --- a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp +++ b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp @@ -402,7 +402,7 @@ template class DNRunPluginActor : public caf::event_based_actor { quickview = jsn.at("args").at("quickview") == "true"; } } - + bool ab_compare = jsn.at("args").contains("compare") && jsn.at("args").at("compare") == "ab"; diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index b6a97f6b6..c2d5c28e3 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -54,7 +54,8 @@ size_t PluginManager::load_plugins() { // read dir content.. for (const auto &entry : fs::directory_iterator(path)) { if (not fs::is_regular_file(entry.status()) or - not(entry.path().extension() == ".so" || entry.path().extension() == ".dll")) + not(entry.path().extension() == ".so" || + entry.path().extension() == ".dll")) continue; #ifdef __linux__ diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index ae8ceab7f..863112288 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -27,7 +27,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base // use env var 'XSTUDIO_PLUGIN_PATH' to extend the folders searched for // xstudio plugins - char * plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); + 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); diff --git a/src/scanner/src/scanner_actor.cpp b/src/scanner/src/scanner_actor.cpp index ccb4b7368..e27b864f4 100644 --- a/src/scanner/src/scanner_actor.cpp +++ b/src/scanner/src/scanner_actor.cpp @@ -175,7 +175,7 @@ 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 @@ -198,7 +198,7 @@ ScanHelperActor::ScanHelperActor(caf::actor_config &cfg) : caf::event_based_acto #else auto checksum = get_checksum(entry.path()); #endif - cache_[puri] = std::make_pair(checksum, size); + 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 7f5b944c6..fc27d7101 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -335,7 +335,8 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { fs::path p(uri_to_posix_path(i)); if (fs::is_directory(p)) { #ifdef _WIN32 - request(session_, infinite, add_playlist_atom_v, std::string(p.filename().string())) + 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 diff --git a/src/shotgun_client/src/shotgun_client_actor.cpp b/src/shotgun_client/src/shotgun_client_actor.cpp index 63a63c5fb..aaaa65eac 100644 --- a/src/shotgun_client/src/shotgun_client_actor.cpp +++ b/src/shotgun_client/src/shotgun_client_actor.cpp @@ -1578,7 +1578,7 @@ void ShotgunClientActor::init() { return rp; } - + //, // TODO: Ahead Fix // [=](shotgun_image_atom, @@ -1603,7 +1603,8 @@ void ShotgunClientActor::init() { // 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); + // //rp.delegate(thumbgen, media_reader::get_thumbnail_atom_v, + // bytedata); // } else { // rp.deliver(make_error( // sce::response_error, "Thumbnail manager not available")); @@ -1613,7 +1614,7 @@ void ShotgunClientActor::init() { // return rp; // } - ); + ); } void ShotgunClientActor::acquire_token( diff --git a/src/thumbnail/src/thumbnail_manager_actor.cpp b/src/thumbnail/src/thumbnail_manager_actor.cpp index 9118a760b..c849af536 100644 --- a/src/thumbnail/src/thumbnail_manager_actor.cpp +++ b/src/thumbnail/src/thumbnail_manager_actor.cpp @@ -254,7 +254,7 @@ 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/item.cpp b/src/timeline/src/item.cpp index 3b8d98747..1f9888971 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -743,8 +743,8 @@ bool Item::process_event(const utility::JsonStore &event) { case IT_ACTIVE: set_active_range_direct(event.at("value")); has_active_range_ = event.at("value2"); - break; - case IT_AVAIL: + break; + case IT_AVAIL: set_available_range_direct(event.at("value")); has_available_range_ = event.at("value2"); break; @@ -791,29 +791,29 @@ bool Item::process_event(const utility::JsonStore &event) { } } break; - case IT_ADDR: + case IT_ADDR: if (event.at("value").is_null()) - set_actor_addr_direct(caf::actor_addr()); - else + set_actor_addr_direct(caf::actor_addr()); + else set_actor_addr_direct(string_to_actor_addr(event.at("value"))); - break; - case IA_NONE: - default: - break; - } - if (item_event_callback_) - item_event_callback_(event, *this); - } else { + break; + case IA_NONE: + default: + break; + } + if (item_event_callback_) + item_event_callback_(event, *this); + } else { // child ? for (auto &i : *this) { if (i.process_event(event)) return true; - } - - return false; } - return true; + + return false; } + return true; +} void Item::bind_item_event_func(ItemEventFunc fn, const bool recursive) { recursive_bind_ = recursive; diff --git a/src/timeline/src/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index c4b29ba22..84f9a6b4f 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -647,11 +647,14 @@ void TimelineActor::init() { 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 + 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 } @@ -678,12 +681,14 @@ void TimelineActor::init() { 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); + 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 +#endif } return jsn; }, @@ -705,14 +710,16 @@ void TimelineActor::init() { 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); + 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 - +#endif } return jsn; }, @@ -1609,9 +1616,11 @@ void TimelineActor::init() { // #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()); + // 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 @@ -2013,10 +2022,17 @@ void TimelineActor::init() { [=](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()}); + 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()}); + anon_send( + requester, + utility::event_atom_v, + playhead::selection_changed_atom_v, + UuidList{base_.uuid()}); #endif }, @@ -2040,7 +2056,7 @@ void TimelineActor::init() { [=](session::import_atom, const std::string &data) -> result { auto rp = make_response_promise(); - // purge timeline.. ? + // purge timeline.. ? #ifdef BUILD_OTIO spawn( diff --git a/src/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index bb644e2fe..f92f20960 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -214,9 +214,8 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ [=](remove_rows_atom, const std::string &model_name, - const utility::Uuid &attribute_uuid) - { - remove_attribute_from_model(model_name, attribute_uuid); + const utility::Uuid &attribute_uuid) { + remove_attribute_from_model(model_name, attribute_uuid); }, [=](remove_rows_atom, @@ -351,7 +350,7 @@ void GlobalUIModelData::set_data( broadcast_whole_model_data(model_name); } - } catch ([[maybe_unused]]std::exception &e) { + } catch ([[maybe_unused]] std::exception &e) { // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -445,7 +444,7 @@ void GlobalUIModelData::insert_attribute_data_into_model( caf::actor client) { const utility::JsonStore attribute_data = attr_data; - auto p = models_.find(model_name); + auto p = models_.find(model_name); if (p != models_.end()) { // model with this name already exists. Simply add client and send the @@ -693,8 +692,8 @@ void GlobalUIModelData::remove_rows( } } -void GlobalUIModelData::remove_attribute_from_model(const std::string &model_name, const utility::Uuid &attr_uuid) -{ +void GlobalUIModelData::remove_attribute_from_model( + const std::string &model_name, const utility::Uuid &attr_uuid) { try { check_model_is_registered(model_name); @@ -707,7 +706,6 @@ void GlobalUIModelData::remove_attribute_from_model(const std::string &model_nam } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - } void GlobalUIModelData::push_to_prefs(const std::string &model_name, const bool actually_push) { diff --git a/src/ui/opengl/src/opengl_viewport_renderer.cpp b/src/ui/opengl/src/opengl_viewport_renderer.cpp index d2df464f5..26d8f072a 100644 --- a/src/ui/opengl/src/opengl_viewport_renderer.cpp +++ b/src/ui/opengl/src/opengl_viewport_renderer.cpp @@ -335,7 +335,7 @@ void OpenGLViewportRenderer::render( } // coordinate system set-up - utility::JsonStore shader_params = shader_uniforms_; + 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; diff --git a/src/ui/opengl/src/texture.cpp b/src/ui/opengl/src/texture.cpp index c1dec8933..1ffb77ffd 100644 --- a/src/ui/opengl/src/texture.cpp +++ b/src/ui/opengl/src/texture.cpp @@ -31,9 +31,9 @@ class DebugTimer { } // namespace void GLBlindTex::release() { - //if linux - //mutex_.unlock(); - //endif + // if linux + // mutex_.unlock(); + // endif when_last_used_ = utility::clock::now(); } @@ -138,8 +138,8 @@ void GLDoubleBufferedTexture::upload_next( void GLDoubleBufferedTexture::release() { current_->release(); } GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { - //if linux? TODO: Merged this in but might be problematic for Windows, do check. - // ensure no copying is in flight + // if 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(); @@ -223,23 +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(); - //std::unique_lock lck(mutex_); - //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_); + // std::unique_lock lck(mutex_); if (!new_source_frame_->size()) { - //if linux - //mutex_.unlock(); - //endif + // if linux + // mutex_.unlock(); + // endif return; } @@ -264,7 +264,7 @@ void GLBlindRGBA8bitTex::pixel_upload() { t.join(); } - //cv.notify_one(); // notify the waiting thread + // cv.notify_one(); // notify the waiting thread } @@ -273,8 +273,8 @@ void GLBlindRGBA8bitTex::map_buffer_for_upload(media_reader::ImageBufPtr &frame) if (!frame) return; // acquire a write lock, - //mutex_.lock(); - //std::lock_guard lock(mutex_); + // mutex_.lock(); + // std::lock_guard lock(mutex_); new_source_frame_ = frame; media_key_ = frame->media_key(); @@ -294,17 +294,16 @@ void GLBlindRGBA8bitTex::map_buffer_for_upload(media_reader::ImageBufPtr &frame) } // The mutex will be automatically unlocked here when lock goes out of scope. // No need to manually call mutex_.unlock(). - //mutex_.unlock(); + // mutex_.unlock(); // N.B. threads are probably still running here! - } void GLBlindRGBA8bitTex::bind(int tex_index, Imath::V2i &dims) { - + // mutex_.lock(); - //std::unique_lock lck(mutex_); + // std::unique_lock lck(mutex_); dims.x = tex_width_; dims.y = tex_height_; @@ -312,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. diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index d6dbe9ce4..ff3d3367b 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -204,11 +204,7 @@ BookmarkModel::getJSONFuture(const QModelIndex &index, const QString &path) cons scoped_actor sys{system()}; auto addr = UuidFromQUuid(index.data(uuidRole).toUuid()); auto result = request_receive( - *sys, - bookmark_actor_, - json_store::get_json_atom_v, - addr, - path_string); + *sys, bookmark_actor_, json_store::get_json_atom_v, addr, path_string); return QStringFromStd(result.dump()); 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 eaa16d8bf..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,7 +201,7 @@ bool GlobalStoreModel::updateProperty( // convert to internal representation. nlohmann::json GlobalStoreModel::storeToTree(const nlohmann::json &src) { - + auto result = R"([])"_json; for (const auto &[k, v] : src.items()) { if (v.count("datatype")) { diff --git a/src/ui/qml/helper/src/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 602ff5f23..0f024fe36 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -124,7 +124,8 @@ void UIModelData::init(caf::actor_system &system) { emit dataChanged( idx, idx, - QVector({static_cast(Roles::LASTROLE + static_cast(i))})); + QVector({static_cast( + Roles::LASTROLE + static_cast(i))})); break; } @@ -185,7 +186,7 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r .constData()); } else if (std::string(value.typeName()) == "QString") { std::string value_string = StdFromQString(value.toString()); - j = nlohmann::json::parse(value_string); + j = nlohmann::json::parse(value_string); } else { j = nlohmann::json::parse(QJsonDocument::fromVariant(value) .toJson(QJsonDocument::Compact) @@ -656,8 +657,8 @@ void MenuModelItem::insertIntoMenuModel() { global_ui_model_data_registry); utility::JsonStore menu_item_data; - std::string name_string = StdFromQString(text_); - menu_item_data["name"] = name_string; + 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; diff --git a/src/ui/qml/playhead/src/playhead_ui.cpp b/src/ui/qml/playhead/src/playhead_ui.cpp index d66097ad7..6b3233f15 100644 --- a/src/ui/qml/playhead/src/playhead_ui.cpp +++ b/src/ui/qml/playhead/src/playhead_ui.cpp @@ -97,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_ = 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 24c40f590..aeba9ff2e 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -713,7 +713,7 @@ QFuture> SessionModel::handleUriListDropFuture( if (target) { for (const auto &path : jdrop.at("text/uri-list")) { auto path_string = path.get(); - auto uri = caf::make_uri(url_clean(path_string)); + auto uri = caf::make_uri(url_clean(path_string)); if (uri) { // uri maybe timeline... // hacky... @@ -1132,11 +1132,7 @@ QFuture SessionModel::getJSONFuture(const QModelIndex &index, const QSt std::string path_string = StdFromQString(path); if (type == "Media") { auto jsn = request_receive( - *sys, - actor, - json_store::get_json_atom_v, - Uuid(), - path_string); + *sys, actor, json_store::get_json_atom_v, Uuid(), path_string); result = QStringFromStd(jsn.dump()); } else { diff --git a/src/ui/qml/studio/src/studio_ui.cpp b/src/ui/qml/studio/src/studio_ui.cpp index d1bab527e..dded1291f 100644 --- a/src/ui/qml/studio/src/studio_ui.cpp +++ b/src/ui/qml/studio/src/studio_ui.cpp @@ -121,7 +121,6 @@ void StudioUI::init(actor_system &system_) { // 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) { @@ -134,15 +133,12 @@ void StudioUI::init(actor_system &system_) { requester, ui::offscreen_viewport_atom_v, offscreen_viewports_.back()->as_actor()); - }, - [=](std::string) { - - loadVideoOutputPlugins(); - + [=](std::string) { + loadVideoOutputPlugins(); } - - }; + + }; }); // here we tell the studio that we're up and running so it can send us @@ -155,15 +151,13 @@ void StudioUI::init(actor_system &system_) { // 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() - ); + 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")); - + delayed_anon_send( + as_actor(), std::chrono::seconds(5), std::string("load video output plugins")); } void StudioUI::setSessionActorAddr(const QString &addr) { @@ -361,7 +355,7 @@ void StudioUI::loadVideoOutputPlugins() { 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); diff --git a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp index 0519df3e7..745697707 100644 --- a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp +++ b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp @@ -74,8 +74,8 @@ void QMLViewportRenderer::paint() { } } -void QMLViewportRenderer::frameSwapped() { - viewport_renderer_->framebuffer_swapped(utility::clock::now()); +void QMLViewportRenderer::frameSwapped() { + viewport_renderer_->framebuffer_swapped(utility::clock::now()); } void QMLViewportRenderer::setWindow(QQuickWindow *window) { m_window = window; } @@ -396,10 +396,7 @@ void QMLViewportRenderer::renderImageToFile( std::cerr << "A\n"; utility::request_receive( - *sys, - offscreen_viewport, - viewport::viewport_playhead_atom_v, - playhead); + *sys, offscreen_viewport, viewport::viewport_playhead_atom_v, playhead); std::cerr << "B\n"; utility::request_receive( @@ -414,7 +411,7 @@ void QMLViewportRenderer::renderImageToFile( } else { emit snapshotRequestResult(QString("Offscreen viewport renderer was not found.")); } - } catch (std::exception & e) { + } catch (std::exception &e) { emit snapshotRequestResult(QString(e.what())); } } diff --git a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp index c3c1f940a..db93fdb21 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -30,55 +30,51 @@ using namespace xstudio::ui::viewport; namespace fs = std::filesystem; namespace { - static void threaded_memcpy(void * _dst, void * _src, size_t n, int n_threads) { +static void threaded_memcpy(void *_dst, void *_src, size_t n, int n_threads) { - std::vector memcpy_threads; - size_t step = ((n / n_threads) / 4096) * 4096; + std::vector memcpy_threads; + size_t step = ((n / n_threads) / 4096) * 4096; - uint8_t *dst = (uint8_t *)_dst; - uint8_t *src = (uint8_t *)_src; - - 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; - } - - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) - t.join(); - } + uint8_t *dst = (uint8_t *)_dst; + uint8_t *src = (uint8_t *)_src; + 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; } - static std::map format_to_gl_tex_format = { - {viewport::ImageFormat::RGBA_8, GL_RGBA8}, - {viewport::ImageFormat::RGBA_10_10_10_2, GL_RGBA8}, - {viewport::ImageFormat::RGBA_16, GL_RGBA16}, - {viewport::ImageFormat::RGBA_16F, GL_RGBA16F}, - {viewport::ImageFormat::RGBA_32F, GL_RGBA32F} - }; - - static std::map format_to_gl_pixe_type = { - {viewport::ImageFormat::RGBA_8, GL_UNSIGNED_BYTE}, - {viewport::ImageFormat::RGBA_10_10_10_2, GL_UNSIGNED_BYTE}, - {viewport::ImageFormat::RGBA_16, GL_UNSIGNED_SHORT}, - {viewport::ImageFormat::RGBA_16F, GL_HALF_FLOAT}, - {viewport::ImageFormat::RGBA_32F, GL_FLOAT} - }; - - static std::map format_to_bytes_per_pixel = { - {viewport::ImageFormat::RGBA_8, 4}, - {viewport::ImageFormat::RGBA_10_10_10_2, 4}, - {viewport::ImageFormat::RGBA_16, 8}, - {viewport::ImageFormat::RGBA_16F, 8}, - {viewport::ImageFormat::RGBA_32F, 16} - }; - + // ensure any threads still running to copy data to this texture are done + for (auto &t : memcpy_threads) { + if (t.joinable()) + t.join(); + } } +static std::map format_to_gl_tex_format = { + {viewport::ImageFormat::RGBA_8, GL_RGBA8}, + {viewport::ImageFormat::RGBA_10_10_10_2, GL_RGBA8}, + {viewport::ImageFormat::RGBA_16, GL_RGBA16}, + {viewport::ImageFormat::RGBA_16F, GL_RGBA16F}, + {viewport::ImageFormat::RGBA_32F, GL_RGBA32F}}; + +static std::map format_to_gl_pixe_type = { + {viewport::ImageFormat::RGBA_8, GL_UNSIGNED_BYTE}, + {viewport::ImageFormat::RGBA_10_10_10_2, GL_UNSIGNED_BYTE}, + {viewport::ImageFormat::RGBA_16, GL_UNSIGNED_SHORT}, + {viewport::ImageFormat::RGBA_16F, GL_HALF_FLOAT}, + {viewport::ImageFormat::RGBA_32F, GL_FLOAT}}; + +static std::map format_to_bytes_per_pixel = { + {viewport::ImageFormat::RGBA_8, 4}, + {viewport::ImageFormat::RGBA_10_10_10_2, 4}, + {viewport::ImageFormat::RGBA_16, 8}, + {viewport::ImageFormat::RGBA_16F, 8}, + {viewport::ImageFormat::RGBA_32F, 16}}; + +} // namespace + OffscreenViewport::OffscreenViewport(const std::string name) : super() { // This class is a QObject with a caf::actor 'companion' that allows it @@ -108,7 +104,7 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { auto callback = [this](auto &&PH1) { receive_change_notification(std::forward(PH1)); }; - //viewport_renderer_->set_change_callback(callback); + // viewport_renderer_->set_change_callback(callback); self()->set_down_handler([=](down_msg &msg) { if (msg.source == video_output_actor_) { @@ -173,35 +169,31 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { } 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; - + vid_out_width_ = outputWidth; + vid_out_height_ = outputHeight; + vid_out_format_ = format; }, - [=](video_output_actor_atom, - caf::actor video_output_actor) { + [=](video_output_actor_atom, caf::actor video_output_actor) { video_output_actor_ = video_output_actor; }, - [=](render_viewport_to_image_atom) { + [=](render_viewport_to_image_atom) { // force a redraw receive_change_notification(Viewport::ChangeCallbackId::Redraw); } - - }); + + }); }); initGL(); - } OffscreenViewport::~OffscreenViewport() { @@ -216,15 +208,10 @@ OffscreenViewport::~OffscreenViewport() { delete surface_; video_output_actor_ = caf::actor(); - } -void OffscreenViewport::autoDelete() { - - delete this; - -} +void OffscreenViewport::autoDelete() { delete this; } void OffscreenViewport::initGL() { @@ -266,8 +253,8 @@ void OffscreenViewport::initGL() { moveToThread(thread_); thread_->start(); - // Note - the only way I seem to be able to 'cleanly' exit is - // delete ourselves when the thread quits. Not 100% sure if this + // 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. @@ -408,9 +395,10 @@ void OffscreenViewport::exportToCompressedFormat( } } -void OffscreenViewport::setupTextureAndFrameBuffer(const int width, const int height, const viewport::ImageFormat format) { +void OffscreenViewport::setupTextureAndFrameBuffer( + const int width, const int height, const viewport::ImageFormat format) { - if (tex_width_ == width && tex_height_ == height && format == vid_out_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); @@ -425,8 +413,8 @@ void OffscreenViewport::setupTextureAndFrameBuffer(const int width, const int he glDeleteTextures(1, &depth_texId_); } - tex_width_ = width; - tex_height_ = height; + tex_width_ = width; + tex_height_ = height; vid_out_format_ = format; utility::JsonStore j; @@ -448,9 +436,11 @@ void OffscreenViewport::setupTextureAndFrameBuffer(const int width, const int he nullptr); GLint iTexFormat; - glGetTexLevelParameteriv ( GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &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}", + 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_]); @@ -500,8 +490,10 @@ void OffscreenViewport::setupTextureAndFrameBuffer(const int width, const int he } void OffscreenViewport::renderToImageBuffer( - const int w, const int h, media_reader::ImageBufPtr &image, const viewport::ImageFormat format) -{ + const int w, + const int h, + media_reader::ImageBufPtr &image, + const viewport::ImageFormat format) { auto t0 = utility::clock::now(); // ensure our GLContext is current @@ -519,8 +511,8 @@ void OffscreenViewport::renderToImageBuffer( auto t1 = utility::clock::now(); // Clearup before render, probably useless for a new buffer - //glClearColor(0.0, 1.0, 0.0, 0.0); - //glClear(GL_COLOR_BUFFER_BIT); + // glClearColor(0.0, 1.0, 0.0, 0.0); + // glClear(GL_COLOR_BUFFER_BIT); glViewport(0, 0, w, h); @@ -537,21 +529,21 @@ void OffscreenViewport::renderToImageBuffer( 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_]; + size_t pix_buf_size = w * h * format_to_bytes_per_pixel[vid_out_format_]; // init RGBA float array image->allocate(pix_buf_size); image->set_image_dimensions(Imath::V2i(w, h)); - image.when_to_display_ = utility::clock::now(); + image.when_to_display_ = utility::clock::now(); image->params()["pixel_format"] = (int)format; - + if (!pixel_buffer_object_) { glGenBuffers(1, &pixel_buffer_object_); } @@ -571,47 +563,36 @@ void OffscreenViewport::renderToImageBuffer( glPixelStorei(GL_PACK_ALIGNMENT, 1); auto t3 = utility::clock::now(); - + glBindFramebuffer(GL_FRAMEBUFFER, 0); - glGetTexImage(GL_TEXTURE_2D, - 0, - GL_RGBA, - format_to_gl_pixe_type[vid_out_format_], - nullptr); - + 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); + void *mappedBuffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); auto t4 = utility::clock::now(); - - threaded_memcpy( - image->buffer(), - mappedBuffer, - pix_buf_size, - 8 - ); + + threaded_memcpy(image->buffer(), mappedBuffer, pix_buf_size, 8); - //now mapped buffer contains the pixel data + // 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::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) { +void OffscreenViewport::receive_change_notification(Viewport::ChangeCallbackId id) { if (id == Viewport::ChangeCallbackId::Redraw) { diff --git a/src/ui/viewport/src/viewport.cpp b/src/ui/viewport/src/viewport.cpp index 68151da6c..6e9cf8a18 100644 --- a/src/ui/viewport/src/viewport.cpp +++ b/src/ui/viewport/src/viewport.cpp @@ -109,8 +109,10 @@ Viewport::Viewport( ViewportRendererPtr the_renderer, const std::string &_name) : Module( - _name.empty() ? (viewport_index >= 0 ? fmt::format("viewport{0}", viewport_index) - : fmt::format("offscreen_viewport{0}", abs(viewport_index))) : _name), + _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)) { @@ -168,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 = (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; + 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; }; @@ -181,8 +187,8 @@ Viewport::Viewport( normalised_pointer_position() * interact_start_projection_matrix_; const float scale_factor = powf( 2.0, - (state_.mirror_mode_ & MirrorMode::Flip ? delta_trans.x : -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 = @@ -558,12 +564,8 @@ 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 - for (auto &o: other_viewports_) { - anon_send( - o, - viewport_pan_atom_v, - state_.translate_.x, - state_.translate_.y); + 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_); } @@ -575,7 +577,7 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { previous_fit_zoom_state_.scale_ = old_scale; state_.fit_mode_ = Free; fit_mode_->set_value("Off"); - for (auto &o: other_viewports_) { + for (auto &o : other_viewports_) { anon_send(o, fit_mode_atom_v, Free); } } @@ -620,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(), devicePixelRatio); + set_size( + (bottomright - bottomleft).length(), + (bottomleft - topleft).length(), + devicePixelRatio); return true; } return false; @@ -670,10 +675,10 @@ void Viewport::update_fit_mode_matrix( } else if (fit_mode() == One2One && state_.image_size_.x) { - // for 1:1 to work when we have high DPI display scaling (e.g. with QT_SCALE_FACTOR!=1.0) - // we need to account for the pixel ratio - int screen_pix_size_x = (int)round(float(size().x)*devicePixelRatio_); - int screen_pix_size_y = (int)round(float(size().y)*devicePixelRatio_); + // 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; @@ -706,7 +711,7 @@ void Viewport::set_scale(const float scale) { } void Viewport::set_size(const float w, const float h, const float devicePixelRatio) { - state_.size_ = Imath::V2f(w, h); + state_.size_ = Imath::V2f(w, h); devicePixelRatio_ = devicePixelRatio; update_matrix(); } @@ -815,7 +820,7 @@ void Viewport::revert_fit_zoom_to_previous(const bool synced) { event_callback_(Redraw); if (state_.fit_mode_ == FitMode::Free && !synced) { - for (auto &o: other_viewports_) { + for (auto &o : other_viewports_) { anon_send(o, fit_mode_atom_v, "revert"); } } @@ -864,8 +869,7 @@ void Viewport::update_matrix() { inv_projection_matrix_.makeIdentity(); inv_projection_matrix_.scale(Imath::V3f(1.0f, -1.0f, 1.0f)); - inv_projection_matrix_.scale( - Imath::V3f(1.0, state_.size_.x / state_.size_.y, 1.0f)); + 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)); @@ -876,8 +880,7 @@ void Viewport::update_matrix() { 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_)); - projection_matrix_.scale( - Imath::V3f(1.0, state_.size_.y / state_.size_.x, 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(); @@ -941,7 +944,12 @@ caf::message_handler Viewport::message_handler() { const float devicePixelRatio) { float zoom = pixel_zoom(); if (set_scene_coordinates( - topleft, topright, bottomright, bottomleft, scene_size, devicePixelRatio)) { + topleft, + topright, + bottomright, + bottomleft, + scene_size, + devicePixelRatio)) { if (zoom != pixel_zoom()) { event_callback_(ZoomChanged); } @@ -960,9 +968,7 @@ caf::message_handler Viewport::message_handler() { } }, - [=](other_viewport_atom, - caf::actor other_view, - bool link) { + [=](other_viewport_atom, caf::actor other_view, bool link) { if (link) { auto p = other_viewports_.begin(); @@ -970,7 +976,7 @@ caf::message_handler Viewport::message_handler() { if (*p == other_view) { return; } - p++; + p++; } other_viewports_.push_back(other_view); @@ -987,16 +993,14 @@ caf::message_handler Viewport::message_handler() { } unlink_module(other_view); } - }, [=](other_viewport_atom, caf::actor other_view, caf::actor other_colour_pipeline) { - other_viewports_.push_back(other_view); link_to_module(other_view, true, true, true); - + if (other_colour_pipeline) { // here we link up the colour pipelines of the two viewports anon_send( @@ -1005,8 +1009,9 @@ caf::message_handler Viewport::message_handler() { 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) + viewport_index_ == + 0 // push sync (if we are main viewport, sync the + // attrs on the other colour pipelin to ourselves) ); } }, @@ -1087,14 +1092,11 @@ caf::message_handler Viewport::message_handler() { 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); - }, + const utility::time_point swap_time) { framebuffer_swapped(swap_time); }, [=](aux_shader_uniforms_atom, const utility::JsonStore &shader_extras, - const bool overwrite_and_clear) - { + const bool overwrite_and_clear) { set_aux_shader_uniforms(shader_extras, overwrite_and_clear); }, @@ -1387,8 +1389,8 @@ void Viewport::framebuffer_swapped(const utility::time_point swap_time) { screen_refresh_period_, viewport_index_); - static auto tp = utility::clock::now(); - auto t0 = utility::clock::now(); + static auto tp = utility::clock::now(); + auto t0 = utility::clock::now(); if (about_to_go_on_screen_frame_buffer_ != on_screen_frame_buffer_) { @@ -1408,7 +1410,7 @@ void Viewport::framebuffer_swapped(const utility::time_point swap_time) { std::chrono::duration_cast(t0-tp).count() << "\n"; } - + ff[viewport_index_] = f;*/ anon_send( @@ -1421,11 +1423,9 @@ void Viewport::framebuffer_swapped(const utility::time_point swap_time) { /*std::cerr << name() << " frame repeated " << std::chrono::duration_cast(t0-tp).count() << "\n";*/ - } - - tp = t0; + tp = t0; } media_reader::ImageBufPtr Viewport::get_onscreen_image() { @@ -1470,7 +1470,6 @@ void Viewport::get_frames_for_display(std::vector &ne std::chrono::milliseconds(1000), colour_pipeline::colour_operation_uniforms_atom_v, image); - } if (next_images.size()) { @@ -1821,9 +1820,7 @@ void Viewport::auto_connect_to_playhead(bool auto_connect) { } void Viewport::set_aux_shader_uniforms( - const utility::JsonStore & j, - const bool clear_and_overwrite) -{ + const utility::JsonStore &j, const bool clear_and_overwrite) { if (clear_and_overwrite) { aux_shader_uniforms_ = j; } else if (j.is_object()) { @@ -1832,11 +1829,11 @@ void Viewport::set_aux_shader_uniforms( } } else { spdlog::warn( - "{} Invalid shader uniforms data:\n\"{}\".\n\nIt must be a dictionary of key/value pairs.", + "{} 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 9fab647d3..17dbe83dd 100644 --- a/src/ui/viewport/src/viewport_frame_queue_actor.cpp +++ b/src/ui/viewport/src/viewport_frame_queue_actor.cpp @@ -458,7 +458,7 @@ 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); @@ -502,7 +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() + @@ -524,7 +524,6 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi const double phase = timebase::to_seconds(phase_adjusted_tp - rounded_phase_adjusted_tp) / timebase::to_seconds(video_refresh_period); - } } return rounded_phase_adjusted_tp; @@ -551,9 +550,9 @@ xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( } 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 + // 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 @@ -566,15 +565,15 @@ xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( // 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 + // 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; + 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; + 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()) { @@ -585,9 +584,12 @@ xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( // 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); + 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; + 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++; @@ -595,8 +597,9 @@ xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( refresh_event_index += n_refreshes_between_events; } - next_refresh *= 1.0/estimate_count; - auto offset = std::chrono::duration_cast(timebase::to_flicks(next_refresh)); + next_refresh *= 1.0 / estimate_count; + auto offset = std::chrono::duration_cast( + timebase::to_flicks(next_refresh)); auto result = now + offset; return result; @@ -633,7 +636,8 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { // Here we try to match out measurement with commong video refresh rates: // Assume 24fps is the minimum refresh we'll ever encounter - const int hertz_refresh = std::max(24, int(round(1.0/average_video_refresh_period()))); + const int hertz_refresh = + 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}); @@ -671,6 +675,5 @@ double ViewportFrameQueueActor::average_video_refresh_period() const { t += *(r++); } - return timebase::to_seconds(t)/(double(deltas.size() - 16)); - + return timebase::to_seconds(t) / (double(deltas.size() - 16)); } diff --git a/src/utility/src/chrono.cpp b/src/utility/src/chrono.cpp index 26431e435..2250180c9 100644 --- a/src/utility/src/chrono.cpp +++ b/src/utility/src/chrono.cpp @@ -6,7 +6,7 @@ xstudio::utility::to_sys_time_point(const std::string &datetime) { std::istringstream in{datetime}; sys_time_point tp; #ifdef _WIN32 -//TODO: Ahead to fix +// TODO: Ahead to fix #else in >> date::parse("%Y-%m-%dT%TZ", tp); #endif diff --git a/src/utility/src/frame_list.cpp b/src/utility/src/frame_list.cpp index cb9643027..c9dcee03c 100644 --- a/src/utility/src/frame_list.cpp +++ b/src/utility/src/frame_list.cpp @@ -276,7 +276,7 @@ xstudio::utility::frame_groups_from_sequence_spec(const caf::uri &from_path) { #ifdef _WIN32 auto entryPath = entry.path().string(); // Convert to std::string #else - auto entryPath = entry.path().c_str(); + auto entryPath = entry.path().c_str(); #endif if (std::regex_match(entryPath, m, path_re)) { int frame = std::atoi(m[1].str().c_str()); diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index c28466394..471d6e333 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -16,8 +16,8 @@ #include -//#include -//#include +// #include +// #include #include "xstudio/utility/frame_list.hpp" #include "xstudio/utility/sequence.hpp" @@ -458,7 +458,7 @@ caf::uri xstudio::utility::posix_path_to_uri(const std::string &path, const bool #ifdef _WIN32 p = (fs::path(*pwd) / path).lexically_normal().string(); else - p = (std::filesystem::current_path() / path).lexically_normal().string(); + p = (std::filesystem::current_path() / path).lexically_normal().string(); #else p = fs::path(fs::path(*pwd) / path).lexically_normal(); else diff --git a/src/utility/src/json_store.cpp b/src/utility/src/json_store.cpp index 9b57fdd7d..8ebe62527 100644 --- a/src/utility/src/json_store.cpp +++ b/src/utility/src/json_store.cpp @@ -11,9 +11,7 @@ JsonStore::JsonStore(nlohmann::json json) : nlohmann::json(std::move(json)) {} // JsonStore::JsonStore(const JsonStore &other) : json_(other) {} -nlohmann::json JsonStore::get(const std::string &path) const { - return at(json_pointer(path)); -} +nlohmann::json JsonStore::get(const std::string &path) const { return at(json_pointer(path)); } void JsonStore::set(const nlohmann::json &json, const std::string &path) { (*this)[json_pointer(path)] = json; diff --git a/src/utility/src/logging.cpp b/src/utility/src/logging.cpp index 9f2a9e6a0..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 94758b5bb..20ab3d800 100644 --- a/src/utility/src/remote_session_file.cpp +++ b/src/utility/src/remote_session_file.cpp @@ -111,13 +111,13 @@ fs::path RemoteSessionFile::filepath() const { return p; } -pid_t RemoteSessionFile::get_pid() const { - +pid_t RemoteSessionFile::get_pid() const { + #ifdef _WIN32 - return _getpid(); + return _getpid(); #else - return getpid(); -#endif + return getpid(); +#endif } diff --git a/src/utility/src/sequence.cpp b/src/utility/src/sequence.cpp index 1f36ac08c..53a1f1d75 100644 --- a/src/utility/src/sequence.cpp +++ b/src/utility/src/sequence.cpp @@ -110,7 +110,8 @@ Sequence::Sequence(const Entry &entry) 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( From 214b9e3dfa8c77772b692e8f05b8c7821b19f6f9 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Tue, 23 Jul 2024 00:47:22 -0700 Subject: [PATCH 42/42] Removing extra numeral from cmake install prefix. Signed-off-by: Michael Kessler --- CMakePresets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index d3020c537..fa832be0e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -14,7 +14,7 @@ "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_install3", + "CMAKE_INSTALL_PREFIX": "C:/xstudio_install", "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", "BUILD_DOCS": "OFF" }

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