From 4ddc9dfddf66622543edc9526c3a066e151a5e63 Mon Sep 17 00:00:00 2001 From: Ted Waine Date: Mon, 13 Jan 2025 12:00:52 +0000 Subject: [PATCH] Sync to DNEG internal repo --- CMakeLists.txt | 2 +- cmake/macros.cmake | 1 + include/xstudio/atoms.hpp | 34 +- include/xstudio/audio/audio_output.hpp | 26 +- include/xstudio/audio/audio_output_actor.hpp | 32 +- include/xstudio/bookmark/bookmark.hpp | 5 + .../colour_pipeline/colour_pipeline.hpp | 16 +- .../xstudio/conform/conform_manager_actor.hpp | 1 + .../contact_sheet/contact_sheet_actor.hpp | 9 +- .../embedded_python/embedded_python.hpp | 7 + include/xstudio/enums.hpp | 3 +- include/xstudio/global/global_actor.hpp | 28 +- include/xstudio/global_store/global_store.hpp | 9 + .../global_store/global_store_actor.hpp | 1 + include/xstudio/media/media.hpp | 123 +- include/xstudio/media/media_actor.hpp | 1 + include/xstudio/media_reader/audio_buffer.hpp | 6 +- .../cacheing_media_reader_actor.hpp | 5 +- include/xstudio/media_reader/image_buffer.hpp | 50 +- .../xstudio/media_reader/image_buffer_set.hpp | 248 ++-- include/xstudio/module/attribute.hpp | 2 + include/xstudio/playhead/enums.hpp | 7 +- include/xstudio/playhead/playhead.hpp | 9 +- include/xstudio/playhead/playhead_actor.hpp | 21 +- .../playhead/playhead_global_events_actor.hpp | 1 - include/xstudio/playhead/string_out_actor.hpp | 25 +- include/xstudio/playhead/sub_playhead.hpp | 21 +- include/xstudio/playlist/playlist_actor.hpp | 5 +- .../xstudio/plugin_manager/plugin_base.hpp | 36 +- include/xstudio/session/session_actor.hpp | 17 +- include/xstudio/subset/subset_actor.hpp | 9 +- include/xstudio/sync/sync_actor.hpp | 58 - include/xstudio/timeline/clip_actor.hpp | 5 +- include/xstudio/timeline/item.hpp | 8 +- include/xstudio/timeline/timeline_actor.hpp | 15 +- include/xstudio/timeline/track_actor.hpp | 1 - include/xstudio/ui/canvas/canvas.hpp | 8 + include/xstudio/ui/keyboard.hpp | 12 + .../ui/opengl/opengl_canvas_renderer.hpp | 2 +- .../opengl/opengl_multi_buffered_texture.hpp | 53 +- .../xstudio/ui/opengl/opengl_texture_base.hpp | 16 +- .../ui/opengl/opengl_viewport_renderer.hpp | 48 +- include/xstudio/ui/qml/conform_ui.hpp | 3 +- include/xstudio/ui/qml/helper_ui.hpp | 5 + include/xstudio/ui/qml/hotkey_ui.hpp | 38 +- include/xstudio/ui/qml/qml_viewport.hpp | 7 +- .../xstudio/ui/qml/qml_viewport_renderer.hpp | 2 +- include/xstudio/ui/qml/session_model_ui.hpp | 19 +- include/xstudio/ui/qt/offscreen_viewport.hpp | 5 +- include/xstudio/ui/qt/viewport_opengl_ui.hpp | 2 +- .../ui/viewport/video_output_plugin.hpp | 2 +- include/xstudio/ui/viewport/viewport.hpp | 56 +- .../viewport/viewport_frame_queue_actor.hpp | 16 +- .../ui/viewport/viewport_layout_plugin.hpp | 304 +++-- .../ui/viewport/viewport_renderer_base.hpp | 20 +- include/xstudio/utility/enums.hpp | 3 +- include/xstudio/utility/frame_rate.hpp | 13 +- include/xstudio/utility/frame_time.hpp | 1 - include/xstudio/utility/helpers.hpp | 8 +- .../xstudio/utility/notification_handler.hpp | 7 +- .../xstudio/utility/remote_session_file.hpp | 12 +- include/xstudio/utility/string_helpers.hpp | 92 +- include/xstudio/utility/time_cache.hpp | 67 +- include/xstudio/utility/types.hpp | 22 + python/CMakeLists.txt | 1 - python/src/xstudio/api/__init__.py | 1 - python/src/xstudio/api/auxiliary/__init__.py | 3 +- .../src/xstudio/api/auxiliary/notification.py | 205 +++ python/src/xstudio/api/module.py | 30 +- python/src/xstudio/api/session/container.py | 5 +- .../xstudio/api/session/playlist/playlist.py | 4 +- .../xstudio/api/session/playlist/subset.py | 13 +- .../api/session/playlist/timeline/__init__.py | 128 +- .../api/session/playlist/timeline/item.py | 4 + .../api/session/playlist/timeline/stack.py | 73 +- .../api/session/playlist/timeline/track.py | 150 ++- python/src/xstudio/api/session/session.py | 39 +- python/src/xstudio/cli/control.py | 16 +- python/src/xstudio/cli/inject.py | 6 +- python/src/xstudio/cli/xstudiopy_startup.py | 18 +- python/src/xstudio/connection/__init__.py | 113 +- python/src/xstudio/plugin/plugin_base.py | 34 +- python/src/xstudio/sync_api/__init__.py | 15 - python/src/xstudiopy | 6 - python/test/test_api.py | 3 - share/preference/core_api.json | 26 + share/preference/core_bookmark.json | 26 +- share/preference/core_cache.json | 4 +- share/preference/core_global_store.json | 8 + share/preference/core_playhead.json | 44 + share/preference/core_plugin_manager.json | 2 +- share/preference/core_sequence.json | 2 +- share/preference/core_session.json | 4 +- share/preference/core_sync.json | 42 - share/preference/ui_qml.json | 9 - share/snippets/clip/Demo/random_colour.py | 15 + .../playlist/DNeg/name_from_media_shot.py | 11 + src/CMakeLists.txt | 1 - src/audio/src/audio_output.cpp | 266 +++- src/audio/src/audio_output_actor.cpp | 205 ++- src/audio/src/linux_audio_output_device.cpp | 3 +- src/audio/src/windows_audio_output_device.cpp | 6 +- src/bookmark/src/bookmark.cpp | 7 +- src/bookmark/src/bookmark_actor.cpp | 3 +- src/bookmark/src/bookmarks_actor.cpp | 10 +- .../src/colour_cache_actor.cpp | 43 +- src/colour_pipeline/src/colour_pipeline.cpp | 54 +- src/conform/src/conform_manager_actor.cpp | 64 +- src/contact_sheet/src/contact_sheet_actor.cpp | 142 ++- src/embedded_python/src/embedded_python.cpp | 51 +- .../src/embedded_python_actor.cpp | 238 +++- src/global/src/CMakeLists.txt | 1 - src/global/src/global_actor.cpp | 181 +-- src/global_store/src/global_store_actor.cpp | 96 +- src/launch/xstudio/src/xstudio.cpp | 53 +- src/media/src/media.cpp | 11 +- src/media/src/media_actor.cpp | 46 +- src/media/src/media_source_actor.cpp | 86 +- src/media_reader/src/media_reader.cpp | 15 +- src/media_reader/src/media_reader_actor.cpp | 51 +- src/module/src/module.cpp | 23 +- src/playhead/src/playhead.cpp | 72 +- src/playhead/src/playhead_actor.cpp | 877 ++++++++----- .../src/playhead_global_events_actor.cpp | 8 +- src/playhead/src/playhead_selection_actor.cpp | 76 +- src/playhead/src/string_out_actor.cpp | 64 +- src/playhead/src/sub_playhead.cpp | 434 +++++-- src/playlist/src/playlist_actor.cpp | 101 +- src/plugin/colour_op/grading/src/grading.cpp | 177 ++- src/plugin/colour_op/grading/src/grading.h | 7 +- .../grading/src/grading_colour_op.cpp | 15 +- .../grading/src/grading_colour_op.hpp | 2 +- .../grading/src/grading_mask_gl_renderer.cpp | 7 +- .../grading/src/grading_mask_gl_renderer.h | 3 +- .../src/qml/Grading.2/GTAttributes.qml | 14 - .../src/qml/Grading.2/GradingOverlay.qml | 2 +- .../src/qml/Grading.2/GradingTools.qml | 144 ++- .../grading/src/qml/Grading.2/grading.qrc | 4 + .../src/qml/Grading.2/icons/all_inclusive.svg | 1 + .../src/qml/Grading.2/icons/invert_colors.svg | 1 + .../src/qml/Grading.2/icons/step_into.svg | 1 + .../grading/src/qml/Grading.2/qmldir | 7 +- .../src/qml/Grading.2/sections/Sec0Menu.qml | 214 ++++ .../src/qml/Grading.2/sections/Sec1Header.qml | 17 +- .../qml/Grading.2/sections/Sec2LayerList.qml | 98 +- .../qml/Grading.2/sections/Sec3MaskTools.qml | 289 +---- .../colour_pipeline/ocio/src/ocio_engine.cpp | 15 +- .../colour_pipeline/ocio/src/ocio_plugin.cpp | 5 + .../ocio/src/ocio_shared_settings.cpp | 3 +- .../plugin_conformer_shotbrowser.json | 2 +- .../src/conform_shotbrowser.cpp | 20 +- .../preference/plugin_data_source_ivy.json | 2 +- .../dneg/ivy/src/data_source_ivy.cpp | 22 +- .../plugin_data_source_shotbrowser.json | 79 +- .../dneg/shotbrowser/src/get_actions.cpp | 21 +- .../qml/ShotBrowser.1/ShotBrowserHelpers.js | 116 +- .../ShotBrowserMediaListMenu.qml | 4 + .../notes_history/NotesHistory.qml | 19 +- .../notes_history/NotesHistoryActionDiv.qml | 2 +- .../NotesHistoryListDelegate.qml | 2 +- .../notes_history/NotesHistoryResultPopup.qml | 7 + .../shot_browser/XsShotBrowser.qml | 18 +- .../left_sections/XsSBL1Tools.qml | 2 + .../left_sections/XsSBL3Actions.qml | 10 +- .../viewItems/XsSBPresetDelegate.qml | 140 +- .../viewItems/XsSBPresetGroupDelegate.qml | 128 +- .../viewItems/XsSBPresetsView.qml | 5 +- .../right_sections/XsSBR3Actions.qml | 2 +- .../XsSBRPlaylistResultPopup.qml | 8 + .../shot_history/ShotHistory.qml | 16 +- .../shot_history/ShotHistoryActionDiv.qml | 2 +- .../shot_history/ShotHistoryListDelegate.qml | 2 +- .../shot_history/ShotHistoryResultPopup.qml | 8 + .../dneg/shotbrowser/src/result_model_ui.cpp | 9 + .../dneg/shotbrowser/src/result_model_ui.hpp | 2 + .../src/shotbrowser_engine_query_ui.cpp | 28 +- .../shotbrowser/src/shotbrowser_engine_ui.hpp | 6 +- .../shotbrowser/src/shotbrowser_plugin.cpp | 18 +- .../shotbrowser/src/shotbrowser_plugin.hpp | 4 +- .../exr_data_window/src/exr_data_window.cpp | 9 +- .../exr_data_window/src/exr_data_window.hpp | 7 +- .../image_boundary/src/image_boundary_hud.cpp | 9 +- .../image_boundary/src/image_boundary_hud.hpp | 7 +- .../hud/pixel_probe/src/pixel_probe.cpp | 44 +- .../hud/pixel_probe/src/pixel_probe.hpp | 6 +- src/plugin/media_hook/CMakeLists.txt | 7 +- .../default_hook/src/CMakeLists.txt | 10 + .../default_hook/src/default_hook.cpp | 93 ++ .../default_hook}/test/CMakeLists.txt | 1 - src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp | 105 +- .../ffmpeg/src/ffmpeg_decoder.cpp | 6 +- .../media_reader/ffmpeg/src/ffmpeg_stream.cpp | 71 +- .../media_reader/ffmpeg/src/ffmpeg_stream.hpp | 12 +- src/plugin/python_plugins/CMakeLists.txt | 3 +- .../on_screen_version_name/__init__.py | 2 + .../on_screen_version_name.py | 160 +++ .../OnScreenVersionNameOverlay.qml | 118 ++ .../qml/OnScreenVersionName.1/qmldir | 3 + .../ViewportFlagIndicatorSettingsDialog.qml | 322 ----- .../ViewportFlagIndicatorSettingsOverlay.qml | 47 - .../qml/ViewportFlagIndicator.1/check.svg | 1 - .../qml/ViewportFlagIndicator.1/qmldir | 4 - .../ViewportFlagIndicatorSettingsOverlay.qml | 4 +- .../viewport_flag_indicator_plugin.py | 10 - src/plugin/utility/dneg/dnrun/src/dnrun.cpp | 9 +- .../SessionSnapshotsMenu.qml | 10 +- .../src/viewport_layout_default.cpp | 11 +- .../src/viewport_layout_default.hpp | 62 +- .../src/viewport_layout_grid.cpp | 71 +- .../src/viewport_layout_grid.hpp | 53 +- .../src/wipe_viewport_layout.cpp | 116 +- .../src/wipe_viewport_layout.hpp | 52 +- src/plugin/viewport_overlay/CMakeLists.txt | 1 + .../annotations/src/annotation.hpp | 2 + .../src/annotation_opengl_renderer.cpp | 71 +- .../src/annotation_opengl_renderer.hpp | 28 +- .../annotations/src/annotations_tool.cpp | 267 ++-- .../annotations/src/annotations_tool.hpp | 18 +- .../dockedLR/XsToolDisplayLR.qml | 11 +- .../dockedTB/XsToolActionsTB.qml | 16 +- .../audio_waveform/src/CMakeLists.txt | 11 + .../src/audio_waveform_overlay.cpp | 341 +++++ .../src/audio_waveform_overlay.hpp | 99 ++ .../audio_waveform/test/CMakeLists.txt | 0 .../src/basic_viewport_masking.cpp | 16 +- .../src/basic_viewport_masking.hpp | 9 +- .../BasicViewportMaskOverlay.qml | 2 +- src/plugin_manager/src/hud_plugin.cpp | 2 +- src/plugin_manager/src/plugin_base.cpp | 101 +- src/plugin_manager/src/plugin_manager.cpp | 8 +- .../src/plugin_manager_actor.cpp | 12 +- src/python_module/src/py_atoms.cpp | 11 +- src/python_module/src/py_config.hpp | 30 +- src/python_module/src/py_context.cpp | 15 + src/python_module/src/py_context.hpp | 2 + src/python_module/src/py_link.cpp | 5 + src/python_module/src/py_messages.cpp | 16 + src/python_module/src/py_register.cpp | 105 +- .../src/py_remote_session_file.cpp | 4 +- src/python_module/src/py_utility.cpp | 11 +- src/session/src/session_actor.cpp | 286 +++-- src/subset/src/subset_actor.cpp | 77 +- src/sync/src/CMakeLists.txt | 9 - src/sync/src/sync_actor.cpp | 34 - src/sync/src/sync_gateway_actor.cpp | 50 - src/sync/src/sync_gateway_manager_actor.cpp | 94 -- src/sync/test/sync_actor_test.cpp | 23 - .../src/thumbnail_disk_cache_actor.cpp | 2 +- src/timeline/src/clip_actor.cpp | 117 +- src/timeline/src/gap_actor.cpp | 2 + src/timeline/src/item.cpp | 157 +-- src/timeline/src/stack_actor.cpp | 2 + src/timeline/src/timeline_actor.cpp | 418 +++--- src/timeline/src/track_actor.cpp | 9 +- src/ui/base/src/keyboard.cpp | 88 +- src/ui/canvas/src/canvas.cpp | 14 +- .../opengl/src/opengl_colour_lut_texture.cpp | 2 +- .../src/opengl_multi_buffered_texture.cpp | 62 +- .../src/opengl_rgba8bit_image_texture.cpp | 2 +- src/ui/opengl/src/opengl_shape_renderer.cpp | 4 +- .../opengl/src/opengl_ssbo_image_texture.cpp | 2 +- src/ui/opengl/src/opengl_texture_base.cpp | 16 +- .../opengl/src/opengl_viewport_renderer.cpp | 149 ++- src/ui/qml/bookmark/src/bookmark_model_ui.cpp | 9 +- src/ui/qml/conform/src/conform_ui.cpp | 28 +- .../src/global_store_model_ui.cpp | 4 +- src/ui/qml/helper/src/CMakeLists.txt | 35 +- .../qml/helper/src/QTreeModelToTableModel.cpp | 9 +- src/ui/qml/helper/src/helper_ui.cpp | 30 + src/ui/qml/helper/src/model_data_ui.cpp | 21 +- src/ui/qml/helper/src/model_helper_ui.cpp | 4 +- src/ui/qml/session/src/caf_response_ui.cpp | 8 +- .../qml/session/src/session_model_core_ui.cpp | 43 +- .../session/src/session_model_handler_ui.cpp | 12 +- .../session/src/session_model_manip_ui.cpp | 14 +- .../session/src/session_model_methods_ui.cpp | 66 +- .../session/src/session_model_timeline_ui.cpp | 1134 ++++++++++------- src/ui/qml/session/src/session_model_ui.cpp | 21 +- src/ui/qml/studio/src/studio_ui.cpp | 9 +- src/ui/qml/viewport/src/hotkey_ui.cpp | 140 +- src/ui/qml/viewport/src/qml_viewport.cpp | 10 +- .../viewport/src/qml_viewport_renderer.cpp | 31 +- .../src/offscreen_viewport.cpp | 113 +- .../viewport_widget/src/viewport_widget.cpp | 4 +- src/ui/viewport/src/keypress_monitor.cpp | 29 + src/ui/viewport/src/viewport.cpp | 348 ++--- .../src/viewport_frame_queue_actor.cpp | 315 +++-- .../viewport/src/viewport_layout_plugin.cpp | 235 ++-- src/utility/src/helpers.cpp | 16 + src/utility/src/notification_handler.cpp | 46 +- src/utility/src/remote_session_file.cpp | 34 +- src/utility/test/frame_time_test.cpp | 3 +- src/utility/test/remote_session_file_test.cpp | 8 +- ui/qml/xstudio/assets/icons/communities.svg | 1 + .../assets/icons/view_object_track.svg | 1 + ui/qml/xstudio/helpers/XsDialogHelpers.qml | 11 + .../layout_framework/XsViewContainer.qml | 29 +- ui/qml/xstudio/qml.qrc | 7 +- ui/qml/xstudio/session_data/XsPlayhead.qml | 25 +- ui/qml/xstudio/session_data/XsSessionData.qml | 28 +- ui/qml/xstudio/views/media/XsMediaPanel.qml | 1 + ui/qml/xstudio/views/media/XsMediaToolBar.qml | 2 +- .../XsMediaThumbnailHighlight.qml | 2 +- .../XsMediaThumbnailImage.qml | 2 +- .../media/functions/XsMediaListFunctions.qml | 22 +- .../views/media/grid_view/XsMediaGrid.qml | 4 +- .../delegates/XsMediaGridItemDelegate.qml | 48 +- .../views/media/list_view/XsMediaHeader.qml | 26 +- .../views/media/list_view/XsMediaList.qml | 75 +- .../media/list_view/XsMediaListLayout.qml | 92 +- .../data_indicators/XsMediaFlagIndicator.qml | 10 +- .../data_indicators/XsMediaTextItem.qml | 27 + .../delegates/XsMediaHeaderColumn.qml | 4 + .../delegates/XsMediaItemDelegate.qml | 11 +- .../widgets/XsMediaListConfigureDialog.qml | 11 +- .../widgets/XsMediaMetadataWindow.qml | 12 +- .../media/widgets/XsMediaListContextMenu.qml | 8 +- .../media/widgets/XsMediaListPlusMenu.qml | 18 +- .../views/notes/sections/XsNoteSection1.qml | 2 +- .../views/notes/sections/XsNoteSection2.qml | 35 +- .../xstudio/views/playlists/XsPlaylists.qml | 54 +- .../delegates/XsPlaylistItemBase.qml | 25 +- .../delegates/XsSubsetItemDelegate.qml | 2 +- .../delegates/XsTimelineItemDelegate.qml | 2 +- .../playlists/widgets/XsPlaylistPlusMenu.qml | 10 +- ui/qml/xstudio/views/timeline/XsTimeline.qml | 153 ++- .../xstudio/views/timeline/XsTimelineMenu.qml | 41 +- .../views/timeline/XsTimelinePanel.qml | 25 +- .../delegates/XsDelegateAudioTrack.qml | 154 +-- .../timeline/delegates/XsDelegateClip.qml | 13 - .../timeline/delegates/XsDelegateStack.qml | 2 +- .../timeline/delegates/XsDelegateTrack.qml | 220 ++++ .../delegates/XsDelegateVideoTrack.qml | 158 +-- .../views/timeline/widgets/XsTrackHeader.qml | 46 + .../viewport/XsOffscreenViewportOverlays.qml | 2 + ui/qml/xstudio/views/viewport/XsViewport.qml | 2 +- .../views/viewport/XsViewportPanel.qml | 5 +- .../viewport/widgets/XsViewportInfoBar.qml | 1 + .../toolbar/XsViewerCompareModeButton.qml | 1 + .../toolbar/XsViewerSourceSelectorButton.qml | 45 + .../XsViewerSourceSelectorPopupWindow.qml | 167 +++ .../transport_bar/XsViewerTimeline.qml | 2 +- .../transport_bar/XsViewerVolumeButton.qml | 15 +- .../widgets/buttons/XsSearchButton.qml | 4 + .../xstudio/widgets/controls/XsComboBox.qml | 4 +- .../widgets/dialogs/XsSnapshotDialog.qml | 8 +- ui/qml/xstudio/widgets/dialogs/XsWindow.qml | 2 + .../delegates/XsCompareModePref.qml | 97 ++ .../delegates/XsPreferenceCategory.qml | 14 + .../XsStringMultichoicePreference.qml | 1 - ui/qml/xstudio/widgets/menus/XsMenu.qml | 31 +- .../widgets/menus/XsMenuItemToggle.qml | 6 +- .../xstudio/widgets/outputs/XsSplitView.qml | 3 +- .../xstudio/windows/XsFloatingViewWindow.qml | 21 +- .../xstudio/windows/XsPopoutViewerWindow.qml | 5 +- ui/qml/xstudio/windows/XsSessionWindow.qml | 9 +- .../file_menu/XsFileFunctions.qml | 8 +- .../main_menu_bar/file_menu/XsFileMenu.qml | 27 +- .../windows/quickview/XsQuickViewLauncher.qml | 8 +- .../windows/quickview/XsQuickViewWindow.qml | 2 + .../windows/runtime/XsRuntimeQMLItems.qml | 15 + ui/qml/xstudio/xStudio/qmldir | 2 + 362 files changed, 10504 insertions(+), 6247 deletions(-) delete mode 100644 include/xstudio/sync/sync_actor.hpp create mode 100644 python/src/xstudio/api/auxiliary/notification.py delete mode 100644 python/src/xstudio/sync_api/__init__.py delete mode 100644 share/preference/core_sync.json create mode 100644 share/snippets/clip/Demo/random_colour.py create mode 100644 share/snippets/playlist/DNeg/name_from_media_shot.py create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg create mode 100644 src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml create mode 100644 src/plugin/media_hook/default_hook/src/CMakeLists.txt create mode 100644 src/plugin/media_hook/default_hook/src/default_hook.cpp rename src/{sync => plugin/media_hook/default_hook}/test/CMakeLists.txt (82%) create mode 100644 src/plugin/python_plugins/on_screen_version_name/__init__.py create mode 100755 src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py create mode 100644 src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml create mode 100644 src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir delete mode 100644 src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml delete mode 100644 src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml delete mode 100755 src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/check.svg delete mode 100644 src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/qmldir create mode 100644 src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt create mode 100644 src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp create mode 100644 src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp create mode 100644 src/plugin/viewport_overlay/audio_waveform/test/CMakeLists.txt delete mode 100644 src/sync/src/CMakeLists.txt delete mode 100644 src/sync/src/sync_actor.cpp delete mode 100644 src/sync/src/sync_gateway_actor.cpp delete mode 100644 src/sync/src/sync_gateway_manager_actor.cpp delete mode 100644 src/sync/test/sync_actor_test.cpp create mode 100644 ui/qml/xstudio/assets/icons/communities.svg create mode 100644 ui/qml/xstudio/assets/icons/view_object_track.svg create mode 100644 ui/qml/xstudio/views/timeline/delegates/XsDelegateTrack.qml create mode 100644 ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml create mode 100644 ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsCompareModePref.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index b497342eb..44d4946de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,7 +241,7 @@ if(INSTALL_XSTUDIO) add_subdirectory(share/fonts) if(BUILD_DOCS) - add_subdirectory(docs) + #add_subdirectory(docs) else() install(DIRECTORY share/docs/ DESTINATION share/xstudio/docs) endif () diff --git a/cmake/macros.cmake b/cmake/macros.cmake index f8b7a1967..3851449df 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -27,6 +27,7 @@ macro(default_compile_options name) PRIVATE $<$:-Wfatal-errors> # Stop after first error PRIVATE $<$,$>:-Wpedantic> PRIVATE $<$,$>:/wd4100> + PRIVATE $<$:/bigobj> # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> # PRIVATE $<$:-Wextra> diff --git a/include/xstudio/atoms.hpp b/include/xstudio/atoms.hpp index 21ff0a39d..e6fcf74ee 100644 --- a/include/xstudio/atoms.hpp +++ b/include/xstudio/atoms.hpp @@ -93,7 +93,8 @@ namespace media { class MediaKey; class AVFrameID; class StreamDetail; - typedef std::shared_ptr>> FrameTimeMapPtr; + typedef std::shared_ptr>> + FrameTimeMapPtr; typedef std::vector>> AVFrameIDsAndTimePoints; typedef std::vector> AVFrameIDs; @@ -113,7 +114,7 @@ namespace media_reader { class ImageBufDisplaySet; class ImageSetLayoutData; typedef std::shared_ptr ImageBufDisplaySetPtr; - typedef std::shared_ptr ImageSetLayoutDataPtr; + typedef std::shared_ptr ImageSetLayoutDataPtr; class MediaReaderManager; class PixelInfo; } // namespace media_reader @@ -218,7 +219,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) 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::FrameTimeMapPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::FrameTimeMapPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::media_error)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaDetail)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaKey)) @@ -228,12 +229,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_metadata::MMCertainty)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::AudioBufPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageBufPtr)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageBufDisplaySetPtr)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageSetLayoutDataPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageBufDisplaySetPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageSetLayoutDataPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::MRCertainty)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::PixelInfo)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::AssemblyMode)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::AutoAlignMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::AutoAlignMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::LoopMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::OverflowMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::plugin_manager::PluginDetail)) @@ -253,8 +254,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::FitMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::MirrorMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GPUShaderPtr)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GraphicsAPI)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::ViewportRendererPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GraphicsAPI)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::ViewportRendererPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::absolute_receive_timeout)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::ContainerDetail)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::CopyResult)) @@ -276,6 +277,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::NotificationType)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::Notification)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::SelectionMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::timeline::ItemType)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::ColourTriplet)) CAF_END_TYPE_ID_BLOCK(xstudio_simple_types) @@ -377,6 +380,11 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>>)) + + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) + + CAF_END_TYPE_ID_BLOCK(xstudio_complex_types) CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) @@ -389,7 +397,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, create_studio_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, exit_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_actor_from_registry_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_api_mode_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_application_mode_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_audio_cache_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_image_cache_atom) @@ -442,9 +449,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, release_ui_focus_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, update_attribute_in_preferences_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, authorise_connection_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, get_sync_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, request_connection_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::thumbnail, cache_path_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::thumbnail, cache_stats_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, change_atom) @@ -473,6 +477,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, notification_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, authenticate_atom) + CAF_END_TYPE_ID_BLOCK(xstudio_framework_atoms) @@ -666,6 +672,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, expanded_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, current_media_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_selection_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_type_atom) CAF_END_TYPE_ID_BLOCK(xstudio_session_atoms) @@ -675,6 +683,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, get_samples_for_soundcard_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, push_samples_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, set_override_volume_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, audio_samples_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_operation_uniforms_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_pipeline_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, connect_to_viewport_atom) @@ -792,6 +801,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, FIRST_CUSTOM_ID + (200 * 6)) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, register_hotkey_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, skipped_mouse_event_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, text_entry_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, watch_hotkey_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, insert_or_update_menu_node_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, insert_rows_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, menu_node_activated_atom) diff --git a/include/xstudio/audio/audio_output.hpp b/include/xstudio/audio/audio_output.hpp index 2839faaf8..a4c5f2bc6 100644 --- a/include/xstudio/audio/audio_output.hpp +++ b/include/xstudio/audio/audio_output.hpp @@ -34,7 +34,19 @@ class AudioOutputControl { * audio frames. * */ - void prepare_samples_for_soundcard( + void prepare_samples_for_soundcard_playback( + std::vector &samples, + const long num_samps_to_push, + const long microseconds_delay, + const int num_channels, + const int sample_rate); + + /** + * @brief Pick audio samples based on the current playhead position to sound + * audio during timeline scrubbing. + * + */ + void prepare_samples_for_soundcard_scrubbing( std::vector &samples, const long num_samps_to_push, const long microseconds_delay, @@ -54,13 +66,13 @@ class AudioOutputControl { /** * @brief Queue audio buffer for streaming to the soundcard */ - void queue_samples_for_playing( - const std::vector &audio_buffers); + void queue_samples_for_playing(const std::vector &audio_buffers); /** * @brief Fine grained update of playhead position */ - void playhead_position_changed(const timebase::flicks playhead_position, + void playhead_position_changed( + const timebase::flicks playhead_position, const bool forward, const float velocity, const bool playing, @@ -92,7 +104,7 @@ class AudioOutputControl { override_volume_ = override_volume; } - private: + protected: media_reader::AudioBufPtr pick_audio_buffer(const utility::clock::time_point &tp, const bool drop_old_buffers); @@ -104,14 +116,16 @@ class AudioOutputControl { std::map sample_data_; media_reader::AudioBufPtr current_buf_; media_reader::AudioBufPtr previous_buf_; + media_reader::AudioBufPtr next_buf_; long current_buf_pos_; float playback_velocity_ = {1.0f}; int fade_in_out_ = {NoFade}; timebase::flicks playhead_position_; - bool playing_forward_ = {true}; + bool playing_forward_ = {true}; utility::time_point playhead_position_update_tp_; + timebase::flicks last_buffer_pts_; bool audio_repitch_ = {false}; bool audio_scrubbing_ = {false}; diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index c11f0e66f..6bf531fdc 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -53,8 +53,17 @@ class AudioOutputDeviceActor : public caf::event_based_actor { // we get this message every time the AudioOutputActor has // received samples to play. // connect to the sound output device if necessary - if (output_device_) - output_device_->connect_to_soundcard(); + if (output_device_) { + try { + output_device_->connect_to_soundcard(); + } catch (std::exception &err) { + spdlog::critical("Failed to connect to audio device: {}", err.what()); + output_device_.reset(); + return; + } + } else { + return; + } if (!waiting_for_samples_) { // start playback loop @@ -77,7 +86,6 @@ class AudioOutputDeviceActor : public caf::event_based_actor { } }, [=](push_samples_atom) { - if (!output_device_) return; @@ -100,10 +108,10 @@ class AudioOutputDeviceActor : public caf::event_based_actor { // essentially we have two loops running within the single actor. if (waiting_for_samples_) return; - waiting_for_samples_ = true; + waiting_for_samples_ = true; const long num_samps_soundcard_wants = (long)output_device_->desired_samples(); - auto tt = utility::clock::now(); + auto tt = utility::clock::now(); request( audio_samples_actor_, infinite, @@ -152,8 +160,13 @@ class AudioOutputDeviceActor : public caf::event_based_actor { class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { public: - AudioOutputActor(caf::actor_config &cfg, std::shared_ptr output_device) - : caf::event_based_actor(cfg), output_device_(output_device) { + AudioOutputActor( + caf::actor_config &cfg, + std::shared_ptr output_device, + bool subscribe_to_global_audio_stream = true) + : caf::event_based_actor(cfg), + output_device_(output_device), + is_global_(subscribe_to_global_audio_stream) { init(); } @@ -170,13 +183,13 @@ class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { caf::behavior behavior_; const utility::JsonStore params_; - bool playing_ = {false}; int video_frame_ = {0}; int retry_on_error_ = {0}; utility::Uuid uuid_ = {utility::Uuid::generate()}; utility::Uuid sub_playhead_uuid_; std::shared_ptr output_device_; caf::actor playhead_; + bool is_global_; }; /* Singleton class that receives audio sample buffers from the current @@ -203,6 +216,8 @@ class GlobalAudioOutputActor : public caf::event_based_actor, module::Module { private: + caf::actor independent_output(const utility::Uuid &playhead_uuid); + caf::actor event_group_; caf::message_handler behavior_; module::BooleanAttribute *audio_repitch_; @@ -210,6 +225,7 @@ class GlobalAudioOutputActor : public caf::event_based_actor, module::Module { module::FloatAttribute *volume_; module::BooleanAttribute *muted_; utility::Uuid mute_hotkey_; + std::map independent_outputs_; }; } // namespace xstudio::audio diff --git a/include/xstudio/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index 887d5875c..baab5dd7f 100644 --- a/include/xstudio/bookmark/bookmark.hpp +++ b/include/xstudio/bookmark/bookmark.hpp @@ -31,6 +31,8 @@ namespace bookmark { } utility::Uuid bookmark_uuid_; + virtual size_t hash() const { return 0; } + private: utility::JsonStore store_; }; @@ -104,6 +106,7 @@ namespace bookmark { std::optional has_note_; std::optional has_annotation_; + std::optional annotation_hash_; std::optional media_reference_; std::optional media_flag_; @@ -124,6 +127,7 @@ namespace bookmark { f.field("utp", x.user_type_), f.field("udt", x.user_data_), f.field("hasa", x.has_annotation_), + f.field("anh", x.annotation_hash_), f.field("hasn", x.has_note_), f.field("aut", x.author_), f.field("cat", x.category_), @@ -288,6 +292,7 @@ namespace bookmark { auto has_note() const { return static_cast(note_); } auto has_annotation() const { return static_cast(annotation_); } + size_t annotation_hash() const { return annotation_ ? annotation_->hash() : 0; } void create_note(); void create_annotation(); diff --git a/include/xstudio/colour_pipeline/colour_pipeline.hpp b/include/xstudio/colour_pipeline/colour_pipeline.hpp index 5d45d47cc..780f81822 100644 --- a/include/xstudio/colour_pipeline/colour_pipeline.hpp +++ b/include/xstudio/colour_pipeline/colour_pipeline.hpp @@ -39,11 +39,10 @@ namespace colour_pipeline { std::any user_data_; void set_cache_id(const std::string &id) { cache_id_ = id; } - [[nodiscard]] const std::string & cache_id() const { return cache_id_; } + [[nodiscard]] const std::string &cache_id() const { return cache_id_; } - private: + private: std::string cache_id_; - }; typedef std::shared_ptr ColourOperationDataPtr; @@ -92,10 +91,9 @@ namespace colour_pipeline { } void set_cache_id(const std::string &id) { cache_id_ = id; } - [[nodiscard]] const std::string & cache_id() const { return cache_id_; } + [[nodiscard]] const std::string &cache_id() const { return cache_id_; } private: - std::string cache_id_; /*Apply grades and other colour manipulations after stage_zero_operation_*/ @@ -217,7 +215,9 @@ namespace colour_pipeline { virtual size_t fast_display_transform_hash(const media::AVFrameID &media_ptr) = 0; protected: - void make_pre_draw_gpu_hook(caf::typed_response_promise rp); + void make_pre_draw_gpu_hook( + const std::string &viewport_name, + caf::typed_response_promise rp); void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; @@ -268,7 +268,9 @@ namespace colour_pipeline { bool colour_ops_loaded_ = false; std::vector colour_op_plugins_; - std::vector> hook_requests_; + std::vector< + std::pair>> + hook_requests_; }; } // namespace colour_pipeline diff --git a/include/xstudio/conform/conform_manager_actor.hpp b/include/xstudio/conform/conform_manager_actor.hpp index 28676c6e7..e8ae861fb 100644 --- a/include/xstudio/conform/conform_manager_actor.hpp +++ b/include/xstudio/conform/conform_manager_actor.hpp @@ -100,6 +100,7 @@ class ConformWorkerActor : public caf::event_based_actor { inline static const std::string NAME = "ConformWorkerActor"; caf::behavior behavior_; std::vector conformers_; + bool initialised_{false}; }; class ConformManagerActor : public caf::event_based_actor, public module::Module { diff --git a/include/xstudio/contact_sheet/contact_sheet_actor.hpp b/include/xstudio/contact_sheet/contact_sheet_actor.hpp index 393b48888..0e3f84bbb 100644 --- a/include/xstudio/contact_sheet/contact_sheet_actor.hpp +++ b/include/xstudio/contact_sheet/contact_sheet_actor.hpp @@ -10,9 +10,8 @@ namespace contact_sheet { class ContactSheetActor : public subset::SubsetActor { - public: - - ContactSheetActor( + public: + ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn); ContactSheetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name); @@ -20,13 +19,11 @@ namespace contact_sheet { return override_behaviour_.or_else(subset::SubsetActor::make_behavior()); } - private: - + private: void init(); inline static const std::string NAME = "ContactSheetActor"; caf::message_handler override_behaviour_; - utility::JsonStore playhead_serialisation_; }; /*class ContactSheetActor : public caf::event_based_actor { diff --git a/include/xstudio/embedded_python/embedded_python.hpp b/include/xstudio/embedded_python/embedded_python.hpp index 04d1d9851..761c9037b 100644 --- a/include/xstudio/embedded_python/embedded_python.hpp +++ b/include/xstudio/embedded_python/embedded_python.hpp @@ -60,10 +60,16 @@ namespace embedded_python { void remove_message_callback(const py::tuple &xs); void run_callback_with_delay(const py::tuple &args); void run_callback(const utility::Uuid &id); + utility::JsonStore run_plugin_callback( + const utility::Uuid &plugin_uuid, + const std::string method_name, + const utility::JsonStore &packed_args); + void register_python_plugin_instance(const py::tuple &xs); static void s_add_message_callback(const py::tuple &xs); static void s_remove_message_callback(const py::tuple &xs); static void s_run_callback_with_delay(const py::tuple &delayed_cb_args); + static void s_register_python_plugin_instance(const py::tuple &delayed_cb_args); void finalize(); @@ -72,6 +78,7 @@ namespace embedded_python { std::map> message_handler_callbacks_; std::map message_conversion_function_; std::map delayed_callbacks_; + std::map plugin_registry_; EmbeddedPythonActor *parent_; diff --git a/include/xstudio/enums.hpp b/include/xstudio/enums.hpp index 712188f2b..995c11a69 100644 --- a/include/xstudio/enums.hpp +++ b/include/xstudio/enums.hpp @@ -9,6 +9,7 @@ #include "xstudio/plugin_manager/enums.hpp" #include "xstudio/session/enums.hpp" #include "xstudio/shotgun_client/enums.hpp" -#include "xstudio/ui/viewport/enums.hpp" #include "xstudio/thumbnail/enums.hpp" +#include "xstudio/timeline/enums.hpp" +#include "xstudio/ui/viewport/enums.hpp" #include "xstudio/utility/enums.hpp" \ No newline at end of file diff --git a/include/xstudio/global/global_actor.hpp b/include/xstudio/global/global_actor.hpp index 89d15a403..f4a72fc6b 100644 --- a/include/xstudio/global/global_actor.hpp +++ b/include/xstudio/global/global_actor.hpp @@ -14,6 +14,22 @@ namespace xstudio { namespace global { + class DLL_PUBLIC APIActor : public caf::event_based_actor { + public: + APIActor(caf::actor_config &cfg, const caf::actor &global); + ~APIActor() override = default; + const char *name() const override { return NAME.c_str(); } + caf::behavior make_behavior() override { return behavior_; } + + private: + inline static const std::string NAME = "APIActor"; + caf::behavior behavior_; + caf::actor global_; + + bool allow_unauthenticated_{false}; + utility::JsonStore authentication_passwords_; + utility::JsonStore authentication_keys_; + }; class DLL_PUBLIC GlobalActor : public caf::event_based_actor { public: @@ -34,9 +50,7 @@ namespace global { void init(const utility::JsonStore &prefs = utility::JsonStore()); void connect_api(const caf::actor &embedded_python); - void connect_sync_api(); void disconnect_api(const caf::actor &embedded_python, const bool force = false); - void disconnect_sync_api(const bool force = false); template caf::actor spawn_audio_output_actor(const utility::JsonStore &prefs) { @@ -44,7 +58,7 @@ namespace global { std::is_base_of::value, "Not derived from audio::AudioOutputDevice"); return spawn( - std::shared_ptr(new AudioOutputDev(prefs))); + std::shared_ptr(new AudioOutputDev(prefs)), true); } inline static const std::string NAME = "GlobalActor"; @@ -52,6 +66,7 @@ namespace global { caf::actor studio_; caf::actor ui_studio_; caf::actor event_group_; + caf::actor apia_; bool python_enabled_; bool api_enabled_; @@ -61,14 +76,7 @@ namespace global { std::string bind_address_; bool connected_; - bool sync_api_enabled_; - int sync_port_minimum_; - int sync_port_; - int sync_port_maximum_; - std::string sync_bind_address_; - bool sync_connected_; std::string remote_api_session_name_; - std::string remote_sync_session_name_; utility::RemoteSessionManager rsm_; bool session_autosave_{false}; diff --git a/include/xstudio/global_store/global_store.hpp b/include/xstudio/global_store/global_store.hpp index 6aa870928..6a8720bc0 100644 --- a/include/xstudio/global_store/global_store.hpp +++ b/include/xstudio/global_store/global_store.hpp @@ -258,6 +258,15 @@ namespace global_store { JsonStoreHelper::set(value, path + "/value", async, broacast_change); } + template + inline void set_overridden_path( + const value_type &value, + const std::string &path, + const bool async = true, + const bool broacast_change = true) { + JsonStoreHelper::set(value, path + "/overridden_path", async, broacast_change); + } + /*If a preference is found at path return the value. Otherwise build a preference at path and return default.*/ utility::JsonStore get_existing_or_create_new_preference( diff --git a/include/xstudio/global_store/global_store_actor.hpp b/include/xstudio/global_store/global_store_actor.hpp index 5e6f8c369..1a65e7d4d 100644 --- a/include/xstudio/global_store/global_store_actor.hpp +++ b/include/xstudio/global_store/global_store_actor.hpp @@ -43,6 +43,7 @@ namespace global_store { const std::string reg_value_; GlobalStore base_; caf::actor jsonactor_; + caf::actor ioactor_; }; } // namespace global_store } // namespace xstudio diff --git a/include/xstudio/media/media.hpp b/include/xstudio/media/media.hpp index 28515c54f..fba3b850a 100644 --- a/include/xstudio/media/media.hpp +++ b/include/xstudio/media/media.hpp @@ -152,15 +152,21 @@ namespace media { const std::string &stream_id); bool operator!=(const MediaKey &o) const { - return (hash_ == o.hash_) ? static_cast(*this) != static_cast(o) : true; + return (hash_ == o.hash_) ? static_cast(*this) != + static_cast(o) + : true; } bool operator==(const MediaKey &o) const { - return (hash_ == o.hash_) ? static_cast(*this) == static_cast(o) : false; + return (hash_ == o.hash_) ? static_cast(*this) == + static_cast(o) + : false; } bool operator<(const MediaKey &o) const { - return (hash_ != o.hash_) ? hash_ < o.hash_ : static_cast(*this) < static_cast(o); + return (hash_ != o.hash_) ? hash_ < o.hash_ + : static_cast(*this) < + static_cast(o); } [[nodiscard]] size_t hash() const { return hash_; } @@ -172,8 +178,7 @@ namespace media { return f.object(x).fields(f.field("data", static_cast(x))); } - private: - + private: size_t hash_; }; @@ -210,18 +215,17 @@ namespace media { // copy and data footprint class AVFrameID { public: - AVFrameID( const AVFrameID &shared, const caf::uri &uri, const int frame, const std::string &key_format, - const utility::Timecode time_code = utility::Timecode()) : - fixed_media_data_(shared.fixed_media_data_), - uri_(uri == shared.fixed_media_data_->fixed_uri_ ? caf::uri() : uri), - frame_(frame), - key_(key_format, uri, frame, shared.fixed_media_data_->stream_id_), - timecode_(time_code) {} + const utility::Timecode time_code = utility::Timecode()) + : fixed_media_data_(shared.fixed_media_data_), + uri_(uri == shared.fixed_media_data_->fixed_uri_ ? caf::uri() : uri), + frame_(frame), + key_(key_format, uri, frame, shared.fixed_media_data_->stream_id_), + timecode_(time_code) {} AVFrameID( const caf::uri &uri = caf::uri(), @@ -242,19 +246,19 @@ namespace media { frame_(frame), key_(key_format, uri, frame, stream_id), timecode_(time_code) { - FixedMediaData * md = new FixedMediaData; - md->first_frame_ = first_frame; - md->rate_ = rate; - md->stream_id_ = stream_id; - md->reader_ = reader; - md->actor_addr_ = addr; - md->params_ = params; - md->source_uuid_ = source_uuid; - md->media_uuid_ = media_uuid; - md->clip_uuid_ = clip_uuid; - md->media_type_ = media_type; - fixed_media_data_.reset(md); - } + FixedMediaData *md = new FixedMediaData; + md->first_frame_ = first_frame; + md->rate_ = rate; + md->stream_id_ = stream_id; + md->reader_ = reader; + md->actor_addr_ = addr; + md->params_ = params; + md->source_uuid_ = source_uuid; + md->media_uuid_ = media_uuid; + md->clip_uuid_ = clip_uuid; + md->media_type_ = media_type; + fixed_media_data_.reset(md); + } virtual ~AVFrameID() = default; @@ -269,24 +273,41 @@ namespace media { timecode_ == other.timecode_ and error_ == other.error_); } - [[nodiscard]] const caf::uri & uri() const { return uri_.empty() ? fixed_media_data_->fixed_uri_ : uri_; } + bool operator!=(const AVFrameID &other) const { return !(*this == other); } + + [[nodiscard]] const caf::uri &uri() const { + return uri_.empty() ? fixed_media_data_->fixed_uri_ : uri_; + } [[nodiscard]] int frame() const { return frame_; } [[nodiscard]] int first_frame() const { return fixed_media_data_->first_frame_; } - [[nodiscard]] const utility::FrameRate & rate() const { return fixed_media_data_->rate_; } - [[nodiscard]] const std::string & stream_id() const { return fixed_media_data_->stream_id_; } - [[nodiscard]] const MediaKey & key() const { return key_; } - [[nodiscard]] const std::string & reader() const { return fixed_media_data_->reader_; } - [[nodiscard]] const caf::actor_addr & actor_addr() const { return fixed_media_data_->actor_addr_; } - [[nodiscard]] const utility::JsonStore & params() const { return fixed_media_data_->params_; } - [[nodiscard]] const utility::Uuid & source_uuid() const { return fixed_media_data_->source_uuid_; } - [[nodiscard]] const utility::Uuid & media_uuid() const { return fixed_media_data_->media_uuid_; } - [[nodiscard]] const utility::Uuid & clip_uuid() const { return fixed_media_data_->clip_uuid_; } + [[nodiscard]] const utility::FrameRate &rate() const { + return fixed_media_data_->rate_; + } + [[nodiscard]] const std::string &stream_id() const { + return fixed_media_data_->stream_id_; + } + [[nodiscard]] const MediaKey &key() const { return key_; } + [[nodiscard]] const std::string &reader() const { return fixed_media_data_->reader_; } + [[nodiscard]] const caf::actor_addr &actor_addr() const { + return fixed_media_data_->actor_addr_; + } + [[nodiscard]] const utility::JsonStore ¶ms() const { + return fixed_media_data_->params_; + } + [[nodiscard]] const utility::Uuid &source_uuid() const { + return fixed_media_data_->source_uuid_; + } + [[nodiscard]] const utility::Uuid &media_uuid() const { + return fixed_media_data_->media_uuid_; + } + [[nodiscard]] const utility::Uuid &clip_uuid() const { + return fixed_media_data_->clip_uuid_; + } [[nodiscard]] MediaType media_type() const { return fixed_media_data_->media_type_; } - [[nodiscard]] const utility::Timecode & timecode() const { return timecode_; } - [[nodiscard]] const std::string & error() const { return error_; } - - private: + [[nodiscard]] const utility::Timecode &timecode() const { return timecode_; } + [[nodiscard]] const std::string &error() const { return error_; } + private: caf::uri uri_; int frame_; MediaKey key_; @@ -308,14 +329,14 @@ namespace media { }; std::shared_ptr fixed_media_data_; - }; typedef std::pair> MediaPointerAndTimePoint; typedef std::vector AVFrameIDsAndTimePoints; typedef std::map> FrameTimeMap; - typedef std::shared_ptr>> FrameTimeMapPtr; + typedef std::shared_ptr>> + FrameTimeMapPtr; class Media : public utility::Container { public: @@ -459,9 +480,8 @@ namespace media { StreamDetail detail_; }; - inline std::shared_ptr make_blank_frame( - const utility::FrameRate rate, - const MediaType media_type) { + inline std::shared_ptr + make_blank_frame(const utility::FrameRate rate, const MediaType media_type) { utility::JsonStore js; js["BLANK_FRAME"] = true; @@ -479,15 +499,14 @@ namespace media { utility::Uuid(), utility::Uuid(), media_type)); - } + } inline std::shared_ptr make_blank_frame( const MediaType media_type, - const utility::Uuid media_uuid = utility::Uuid(), - const utility::Uuid source_uuid= utility::Uuid(), - const utility::Uuid clip_uuid= utility::Uuid(), - const caf::actor_addr actor_addr = caf::actor_addr() - ) { + const utility::Uuid media_uuid = utility::Uuid(), + const utility::Uuid source_uuid = utility::Uuid(), + const utility::Uuid clip_uuid = utility::Uuid(), + const caf::actor_addr actor_addr = caf::actor_addr()) { utility::JsonStore js; js["BLANK_FRAME"] = true; @@ -511,8 +530,6 @@ namespace media { namespace std { template <> struct hash { - size_t operator()(const xstudio::media::MediaKey &k) const { - return k.hash(); - } + size_t operator()(const xstudio::media::MediaKey &k) const { return k.hash(); } }; } // namespace std \ No newline at end of file diff --git a/include/xstudio/media/media_actor.hpp b/include/xstudio/media/media_actor.hpp index ba8965f6d..5ca09d0ec 100644 --- a/include/xstudio/media/media_actor.hpp +++ b/include/xstudio/media/media_actor.hpp @@ -159,6 +159,7 @@ namespace media { caf::actor_addr parent_; utility::Uuid parent_uuid_; std::vector> pending_stream_detail_requests_; + bool media_metadata_up_to_date_ = {false}; }; class MediaStreamActor : public caf::event_based_actor { diff --git a/include/xstudio/media_reader/audio_buffer.hpp b/include/xstudio/media_reader/audio_buffer.hpp index 908cba363..aecb32842 100644 --- a/include/xstudio/media_reader/audio_buffer.hpp +++ b/include/xstudio/media_reader/audio_buffer.hpp @@ -104,13 +104,15 @@ namespace media_reader { AudioBufPtr() = default; AudioBufPtr(AudioBuffer *imbuf) : Base(imbuf) {} AudioBufPtr(const AudioBufPtr &o) - : Base(static_cast(o)), when_to_display_(o.when_to_display_), tts_(o.tts_) {} + : Base(static_cast(o)), + when_to_display_(o.when_to_display_), + tts_(o.tts_) {} AudioBufPtr &operator=(const AudioBufPtr &o) { Base &b = static_cast(*this); b = static_cast(o); when_to_display_ = o.when_to_display_; - tts_ = o.tts_; + tts_ = o.tts_; return *this; } diff --git a/include/xstudio/media_reader/cacheing_media_reader_actor.hpp b/include/xstudio/media_reader/cacheing_media_reader_actor.hpp index 66895f3ca..847427149 100644 --- a/include/xstudio/media_reader/cacheing_media_reader_actor.hpp +++ b/include/xstudio/media_reader/cacheing_media_reader_actor.hpp @@ -32,7 +32,10 @@ namespace media_reader { caf::actor &playhead, const utility::time_point &time, timebase::flicks playhead_position) - : mptr_(mptr), playhead_(playhead), time_point_(time), playhead_position_(playhead_position) {} + : mptr_(mptr), + playhead_(playhead), + time_point_(time), + playhead_position_(playhead_position) {} ImmediateImageReqest(const ImmediateImageReqest &) = default; ImmediateImageReqest() = default; diff --git a/include/xstudio/media_reader/image_buffer.hpp b/include/xstudio/media_reader/image_buffer.hpp index dcf40bacc..6a8948a78 100644 --- a/include/xstudio/media_reader/image_buffer.hpp +++ b/include/xstudio/media_reader/image_buffer.hpp @@ -46,7 +46,11 @@ namespace media_reader { } } - [[nodiscard]] float image_aspect() const { return image_size_in_pixels_.y ? pixel_aspect_*image_size_in_pixels_.x/image_size_in_pixels_.y : 16.0f/9.0f; } + [[nodiscard]] float image_aspect() const { + return image_size_in_pixels_.y + ? pixel_aspect_ * image_size_in_pixels_.x / image_size_in_pixels_.y + : 16.0f / 9.0f; + } [[nodiscard]] float pixel_aspect() const { return pixel_aspect_; } void set_pixel_aspect(const float aspect) { pixel_aspect_ = aspect; } @@ -54,9 +58,6 @@ namespace media_reader { [[nodiscard]] const media::MediaKey &media_key() const { return media_key_; } void set_media_key(const media::MediaKey &key) { media_key_ = key; } - [[nodiscard]] double duration_seconds() const { return frame_duration_; } - void set_duration_seconds(const double d) { frame_duration_ = d; } - [[nodiscard]] int decoder_frame_number() const { return frame_num_; } void set_decoder_frame_number(const int f) { frame_num_ = f; } @@ -81,8 +82,7 @@ namespace media_reader { Imath::Box2i pixels_bounds_; float pixel_aspect_ = {1.0f}; media::MediaKey media_key_; - double frame_duration_ = {1.0}; - int frame_num_ = -1; + int frame_num_ = -1; ui::viewport::GPUShaderPtr shader_; PixelPickerFunc pixel_picker_; bool has_alpha_ = false; @@ -112,17 +112,17 @@ namespace media_reader { playhead_logical_frame_(o.playhead_logical_frame_) {} ImageBufPtr &operator=(const ImageBufPtr &o) { - Base &b = static_cast(*this); - b = static_cast(o); - colour_pipe_data_ = o.colour_pipe_data_; - colour_pipe_uniforms_ = o.colour_pipe_uniforms_; - when_to_display_ = o.when_to_display_; - plugin_blind_data_ = o.plugin_blind_data_; - tts_ = o.tts_; - frame_id_ = o.frame_id_; - bookmarks_ = o.bookmarks_; - intrinsic_transform_ = o.intrinsic_transform_; - layout_transform_ = o.layout_transform_; + Base &b = static_cast(*this); + b = static_cast(o); + colour_pipe_data_ = o.colour_pipe_data_; + colour_pipe_uniforms_ = o.colour_pipe_uniforms_; + when_to_display_ = o.when_to_display_; + plugin_blind_data_ = o.plugin_blind_data_; + tts_ = o.tts_; + frame_id_ = o.frame_id_; + bookmarks_ = o.bookmarks_; + intrinsic_transform_ = o.intrinsic_transform_; + layout_transform_ = o.layout_transform_; playhead_logical_frame_ = o.playhead_logical_frame_; return *this; } @@ -181,15 +181,14 @@ namespace media_reader { return utility::BlindDataObjectPtr(); } - std::map< - utility::Uuid, - utility::BlindDataObjectPtr> - plugin_blind_data_; + std::map plugin_blind_data_; [[nodiscard]] const timebase::flicks &timeline_timestamp() const { return tts_; } void set_timline_timestamp(const timebase::flicks tts) { tts_ = tts; } - [[nodiscard]] const int &playhead_logical_frame() const { return playhead_logical_frame_; } + [[nodiscard]] const int &playhead_logical_frame() const { + return playhead_logical_frame_; + } void set_playhead_logical_frame(const int frame) { playhead_logical_frame_ = frame; } [[nodiscard]] const bookmark::BookmarkAndAnnotations &bookmarks() const { @@ -202,14 +201,15 @@ namespace media_reader { [[nodiscard]] const media::AVFrameID &frame_id() const { return frame_id_; } void set_frame_id(const media::AVFrameID &id) { frame_id_ = id; } - [[nodiscard]] const Imath::M44f & intrinsic_transform() const { return intrinsic_transform_; } + [[nodiscard]] const Imath::M44f &intrinsic_transform() const { + return intrinsic_transform_; + } void set_intrinsic_transform(const Imath::M44f &t) { intrinsic_transform_ = t; } - [[nodiscard]] const Imath::M44f & layout_transform() const { return layout_transform_; } + [[nodiscard]] const Imath::M44f &layout_transform() const { return layout_transform_; } void set_layout_transform(const Imath::M44f &t) { layout_transform_ = t; } private: - Imath::M44f intrinsic_transform_; Imath::M44f layout_transform_; diff --git a/include/xstudio/media_reader/image_buffer_set.hpp b/include/xstudio/media_reader/image_buffer_set.hpp index 19ae8481b..71f72ff2c 100644 --- a/include/xstudio/media_reader/image_buffer_set.hpp +++ b/include/xstudio/media_reader/image_buffer_set.hpp @@ -9,9 +9,9 @@ namespace media_reader { // A simple struct that contains matrices for transforming a set of images, // a vector of indexes providing the draw order of images in a set, the // overall display aspect of the layout and a json store for any custom - // layout data. Layout data is attached to the ImageBufDisplaySet just + // layout data. Layout data is attached to the ImageBufDisplaySet just // before the viewport is rendererd. - struct ImageSetLayoutData { + struct ImageSetLayoutData { std::vector image_transforms_; std::vector image_draw_order_hint_; float layout_aspect_; @@ -24,136 +24,148 @@ namespace media_reader { // Each sub-playhead is attached to a source, and requests image reads from // the media readers and then broadcasts the images to the parent playhead. // The parent playhead then re-broadcasts the images to the viewport. - // The ImageBufDisplaySet is used to gather the images coming from the + // The ImageBufDisplaySet is used to gather the images coming from the // multiple sub-playheads into one object which is finally used at draw- // time for rendering the images to screen. The sub-playheads are somewhat // independent of each other, so the synchronisation at display time of // which images should be on-screen is handled by the ViewportFrameQueueActor class ImageBufDisplaySet { - public: - - ImageBufDisplaySet() = default; - ImageBufDisplaySet(const ImageBufDisplaySet & o) = default; - ImageBufDisplaySet(const utility::UuidVector & sub_playhead_ids) : sub_playhead_ids_(sub_playhead_ids) {} - - // For a given sub-playhead, set the image that should be on-screen for the next viewport - // redraw - void add_on_screen_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr & image) { - image_set(sub_playhead_id)->on_screen_image = image; - } - - // Overwrite the on-screen image at sub_playhead_idx - void set_on_screen_image(const int &sub_playhead_idx, ImageBufPtr & image) { - std::swap(onscreen_image_m(sub_playhead_idx), image); - } - - // For a given sub-playhead, add and image that is expected to go on screen soon. These - // should be added in the order that they are expected to hit the screen - void append_future_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr & image) { - image_set(sub_playhead_id)->future_images.push_back(image); - } - - // Loop through all images in the set to get the *earliest* display - // timestamp in the set. This gives us the overall 'when_to_display' - // estimate for the set. It's used by ViewportFrameQueueActor to - // purge stale images from it's cache. - [[nodiscard]] utility::time_point when_to_display() const { - utility::time_point t = utility::time_point::max(); - for (const auto &p: sub_playhead_sets_) { - for (const auto & d: p.second->future_images) { - t = std::min(d.when_to_display_, t); - } + public: + ImageBufDisplaySet() = default; + ImageBufDisplaySet(const ImageBufDisplaySet &o) = default; + ImageBufDisplaySet(const utility::UuidVector &sub_playhead_ids) + : sub_playhead_ids_(sub_playhead_ids) {} + + // For a given sub-playhead, set the image that should be on-screen for the next + // viewport redraw + void + add_on_screen_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr &image) { + image_set(sub_playhead_id)->on_screen_image = image; + } + + // Overwrite the on-screen image at sub_playhead_idx + void set_on_screen_image(const int &sub_playhead_idx, ImageBufPtr &image) { + std::swap(onscreen_image_m(sub_playhead_idx), image); + } + + // For a given sub-playhead, add and image that is expected to go on screen soon. These + // should be added in the order that they are expected to hit the screen + void + append_future_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr &image) { + image_set(sub_playhead_id)->future_images.push_back(image); + } + + // Loop through all images in the set to get the *earliest* display + // timestamp in the set. This gives us the overall 'when_to_display' + // estimate for the set. It's used by ViewportFrameQueueActor to + // purge stale images from it's cache. + [[nodiscard]] utility::time_point when_to_display() const { + utility::time_point t = utility::time_point::max(); + for (const auto &p : sub_playhead_sets_) { + for (const auto &d : p.second->future_images) { + t = std::min(d.when_to_display_, t); } - return t; } - - [[nodiscard]] const std::vector & future_images(const int sub_playhead_index) const { - return image_set(sub_playhead_index)->future_images; - } - [[nodiscard]] int num_future_images(const utility::Uuid &playhead_id) const { - return int(image_set(playhead_id)->future_images.size()); - } - [[nodiscard]] int num_future_images(const int sub_playhead_index) const { - return int(image_set(sub_playhead_index)->future_images.size()); - } - [[nodiscard]] size_t images_keys_hash() const { return images_hash_; } - [[nodiscard]] int num_onscreen_images() const { return int(sub_playhead_ids_.size()); } - [[nodiscard]] bool empty() const { return sub_playhead_ids_.empty(); } - [[nodiscard]] float layout_aspect() const { return layout_data_ ? layout_data_->layout_aspect_ : 16.0f/9.0f; } - [[nodiscard]] int hero_sub_playhead_index() const { return hero_sub_playhead_index_; } - [[nodiscard]] int previous_hero_sub_playhead_index() const { return previous_hero_sub_playhead_index_; } - [[nodiscard]] const ImageSetLayoutDataPtr & layout_data() const { return layout_data_; } - [[nodiscard]] const ImageBufPtr & onscreen_image(const int sub_playhead_index) const { - return image_set(sub_playhead_index)->on_screen_image; - } - [[nodiscard]] const ImageBufPtr & hero_image() const { - return image_set(hero_sub_playhead_index_)->on_screen_image; - } - - [[nodiscard]] ImageBufPtr & onscreen_image_m(const int sub_playhead_index) { - return image_set(sub_playhead_index)->on_screen_image; + return t; + } + + [[nodiscard]] const std::vector & + future_images(const int sub_playhead_index) const { + return image_set(sub_playhead_index)->future_images; + } + [[nodiscard]] int num_future_images(const utility::Uuid &playhead_id) const { + return int(image_set(playhead_id)->future_images.size()); + } + [[nodiscard]] int num_future_images(const int sub_playhead_index) const { + return int(image_set(sub_playhead_index)->future_images.size()); + } + [[nodiscard]] size_t images_keys_hash() const { return images_hash_; } + [[nodiscard]] int num_onscreen_images() const { return int(sub_playhead_ids_.size()); } + [[nodiscard]] bool empty() const { return sub_playhead_ids_.empty(); } + [[nodiscard]] float layout_aspect() const { + return layout_data_ ? layout_data_->layout_aspect_ : 16.0f / 9.0f; + } + [[nodiscard]] int hero_sub_playhead_index() const { return hero_sub_playhead_index_; } + [[nodiscard]] int previous_hero_sub_playhead_index() const { + return previous_hero_sub_playhead_index_; + } + [[nodiscard]] const ImageSetLayoutDataPtr &layout_data() const { return layout_data_; } + [[nodiscard]] const ImageBufPtr &onscreen_image(const int sub_playhead_index) const { + return image_set(sub_playhead_index)->on_screen_image; + } + [[nodiscard]] const ImageBufPtr &hero_image() const { + return image_set(hero_sub_playhead_index_)->on_screen_image; + } + + [[nodiscard]] ImageBufPtr &onscreen_image_m(const int sub_playhead_index) { + return image_set(sub_playhead_index)->on_screen_image; + } + [[nodiscard]] const utility::JsonStore &as_json() const { return as_json_; } + [[nodiscard]] size_t images_layout_hash() const { return hash_; } + + void set_layout_data(const ImageSetLayoutDataPtr &layout_data) { + layout_data_ = layout_data; + } + void set_hero_sub_playhead_index(const int idx) { hero_sub_playhead_index_ = idx; } + void set_previous_hero_sub_playhead_index(const int idx) { + previous_hero_sub_playhead_index_ = idx; + } + + // this is called after an ImageBufDisplaySet has been built to set + // up some internal read-only data + void finalise(); + + private: + struct ImageSet { + ImageBufPtr on_screen_image; + std::vector future_images; + }; + + const std::shared_ptr &image_set(const int sub_playhead_index) const { + if (sub_playhead_index >= (int)sub_playhead_ids_.size()) + return null_set_; + return image_set(sub_playhead_ids_[sub_playhead_index]); + } + + std::shared_ptr &image_set(const int &sub_playhead_index) { + if (sub_playhead_index >= (int)sub_playhead_ids_.size()) + return null_set_; + return image_set(sub_playhead_ids_[sub_playhead_index]); + } + + const std::shared_ptr &image_set(const utility::Uuid &playhead_id) const { + auto p = sub_playhead_sets_.find(playhead_id); + if (p == sub_playhead_sets_.end()) + return null_set_; + return p->second; + } + + std::shared_ptr &image_set(const utility::Uuid &playhead_id) { + if (!sub_playhead_sets_[playhead_id]) { + sub_playhead_sets_[playhead_id].reset(new ImageSet); } - [[nodiscard]] const utility::JsonStore & as_json() const { return as_json_; } - [[nodiscard]] size_t images_layout_hash() const { return hash_; } - - void set_layout_data(const ImageSetLayoutDataPtr &layout_data) { layout_data_ = layout_data; } - void set_hero_sub_playhead_index(const int idx) { hero_sub_playhead_index_ = idx; } - void set_previous_hero_sub_playhead_index(const int idx) { previous_hero_sub_playhead_index_ = idx; } - - // this is called after an ImageBufDisplaySet has been built to set - // up some internal read-only data - void finalise(); - - private: - - struct ImageSet { - ImageBufPtr on_screen_image; - std::vector future_images; - }; - - const std::shared_ptr & image_set(const int sub_playhead_index) const { - if (sub_playhead_index >= (int)sub_playhead_ids_.size()) return null_set_; - return image_set(sub_playhead_ids_[sub_playhead_index]); - } - - std::shared_ptr & image_set(const int &sub_playhead_index) { - if (sub_playhead_index >= (int)sub_playhead_ids_.size()) return null_set_; - return image_set(sub_playhead_ids_[sub_playhead_index]); - } - - const std::shared_ptr & image_set(const utility::Uuid &playhead_id) const { - auto p = sub_playhead_sets_.find(playhead_id); - if (p == sub_playhead_sets_.end()) return null_set_; - return p->second; - } - - std::shared_ptr & image_set(const utility::Uuid &playhead_id) { - if (!sub_playhead_sets_[playhead_id]) { - sub_playhead_sets_[playhead_id].reset(new ImageSet); - } - return sub_playhead_sets_[playhead_id]; - } - - // using shared_ptr to reduce overhead of map rearranging itself - // and copying elements - std::map> sub_playhead_sets_; + return sub_playhead_sets_[playhead_id]; + } - // ordered vec of the IDs of the sub-playheads that are suppling - // the image sets. The first in the vec is the 'key' subplayhead - // corresponding to the first item in the playhead selection. - utility::UuidVector sub_playhead_ids_; + // using shared_ptr to reduce overhead of map rearranging itself + // and copying elements + std::map> sub_playhead_sets_; - int hero_sub_playhead_index_ = {0}; - int previous_hero_sub_playhead_index_ = {-1}; + // ordered vec of the IDs of the sub-playheads that are suppling + // the image sets. The first in the vec is the 'key' subplayhead + // corresponding to the first item in the playhead selection. + utility::UuidVector sub_playhead_ids_; - std::shared_ptr null_set_ = {std::shared_ptr(new ImageSet)}; + int hero_sub_playhead_index_ = {0}; + int previous_hero_sub_playhead_index_ = {-1}; - ImageSetLayoutDataPtr layout_data_; + std::shared_ptr null_set_ = {std::shared_ptr(new ImageSet)}; - utility::JsonStore as_json_; - size_t hash_ = {0}; - size_t images_hash_ = {0}; + ImageSetLayoutDataPtr layout_data_; + utility::JsonStore as_json_; + size_t hash_ = {0}; + size_t images_hash_ = {0}; }; typedef std::shared_ptr ImageBufDisplaySetPtr; diff --git a/include/xstudio/module/attribute.hpp b/include/xstudio/module/attribute.hpp index c7c8c8019..c788719b8 100644 --- a/include/xstudio/module/attribute.hpp +++ b/include/xstudio/module/attribute.hpp @@ -70,6 +70,7 @@ namespace module { OverrideValue, SerializeKey, QmlCode, + ModuleUuid, LeftRightDockWidgetQmlCode, TopBottomDockWidgetQmlCode, PreferencePath, // use this to set a pref path that means the attribute always @@ -117,6 +118,7 @@ namespace module { {OverrideValue, "override_value"}, {SerializeKey, "serialize_key"}, {QmlCode, "qml_code"}, + {ModuleUuid, "module_uuid"}, {LeftRightDockWidgetQmlCode, "left_right_dock_widget_qml_code"}, {TopBottomDockWidgetQmlCode, "top_bottom_dock_widget_qml_code"}, {PreferencePath, "preference_path"}, diff --git a/include/xstudio/playhead/enums.hpp b/include/xstudio/playhead/enums.hpp index 0bc5a4458..84a5beb6a 100644 --- a/include/xstudio/playhead/enums.hpp +++ b/include/xstudio/playhead/enums.hpp @@ -13,12 +13,7 @@ namespace playhead { CM_OFF } CompareMode; - typedef enum { - AM_STRING = 0, - AM_ONE, - AM_ALL, - AM_TEN - } AssemblyMode; + typedef enum { AM_STRING = 0, AM_ONE, AM_ALL, AM_TEN } AssemblyMode; typedef enum { AAM_ALIGN_OFF = 0, AAM_ALIGN_FRAMES, AAM_ALIGN_TRIM } AutoAlignMode; diff --git a/include/xstudio/playhead/playhead.hpp b/include/xstudio/playhead/playhead.hpp index 9f4588faf..73c4db600 100644 --- a/include/xstudio/playhead/playhead.hpp +++ b/include/xstudio/playhead/playhead.hpp @@ -79,7 +79,9 @@ namespace playhead { [[nodiscard]] timebase::flicks duration() const { return duration_; } [[nodiscard]] timebase::flicks effective_frame_period() const; [[nodiscard]] AssemblyMode assembly_mode() const { return assembly_mode_; } - [[nodiscard]] const utility::time_point & last_playhead_set_timepoint() const { return position_set_tp_; } + [[nodiscard]] const utility::time_point &last_playhead_set_timepoint() const { + return position_set_tp_; + } timebase::flicks clamp_timepoint_to_loop_range(const timebase::flicks pos) const; @@ -167,9 +169,10 @@ namespace playhead { utility::Uuid step_backward_; utility::Uuid jump_to_first_frame_; utility::Uuid jump_to_last_frame_; - utility::Uuid jump_to_next_clip_; - utility::Uuid jump_to_previous_clip_; + utility::Uuid cycle_image_layer_up_; + utility::Uuid cycle_image_layer_down_; + bool deserialised_ = {false}; float drag_start_x_; timebase::flicks drag_start_playhead_position_; utility::time_point click_timepoint_; diff --git a/include/xstudio/playhead/playhead_actor.hpp b/include/xstudio/playhead/playhead_actor.hpp index a24b3247e..885769acd 100644 --- a/include/xstudio/playhead/playhead_actor.hpp +++ b/include/xstudio/playhead/playhead_actor.hpp @@ -15,19 +15,14 @@ namespace xstudio { namespace playhead { + enum AudioPath { GLOBAL_AUDIO, INDEPENDENT_AUDIO, NO_AUDIO }; + class PlayheadActor : public caf::event_based_actor, public PlayheadBase { public: - - PlayheadActor( - caf::actor_config &cfg, - const utility::JsonStore &jsn, - caf::actor playlist_selection = caf::actor(), - const utility::Uuid uuid = utility::Uuid::generate(), - caf::actor_addr parent_playlist = caf::actor_addr()); - PlayheadActor( caf::actor_config &cfg, const std::string &name, + const AudioPath = GLOBAL_AUDIO, caf::actor playlist_selection = caf::actor(), const utility::Uuid uuid = utility::Uuid::generate(), caf::actor_addr parent_playlist = caf::actor_addr()); @@ -54,7 +49,8 @@ namespace playhead { void rebuild_from_timeline_sources(); void rebuild_from_dynamic_sources(); - void connect_to_playlist_selection_actor(caf::actor playlist_selection); + void connect_to_playlist_selection_actor( + caf::actor playlist_selection, caf::typed_response_promise rp); void new_source_list(); void switch_key_playhead(int idx); void calculate_duration(); @@ -100,7 +96,6 @@ namespace playhead { void on_exit() override; protected: - void attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) override; void hotkey_pressed( const utility::Uuid &hotkey_uuid, @@ -113,6 +108,8 @@ namespace playhead { void connected_to_ui_changed() override; void check_if_loop_range_makes_sense(); void make_source_menu_model(); + void apply_compare_prefs(); + void step_to_next_media(const bool forwards); caf::message_handler behavior_; @@ -130,6 +127,7 @@ namespace playhead { utility::UuidActor video_string_out_actor_; utility::UuidActor timeline_actor_; utility::UuidActorVector source_actors_; + utility::UuidActorVector previous_source_actors_; utility::UuidActorVector timeline_track_actors_; utility::UuidActorVector dynamic_source_actors_; @@ -148,7 +146,7 @@ namespace playhead { utility::Uuid previous_source_uuid_; utility::Uuid current_source_uuid_; std::map media_frame_per_media_uuid_; - std::map switch_key_playhead_hotkeys_; + std::map switch_key_playhead_hotkeys_; utility::Uuid move_selection_up_hotkey_; utility::Uuid move_selection_down_hotkey_; utility::Uuid jump_to_previous_note_hotkey_; @@ -171,6 +169,7 @@ namespace playhead { bool wrap_sources_ = {false}; bool contact_sheet_mode_ = {false}; int sub_playhead_precache_idx_ = {0}; + const AudioPath audio_path_; utility::UuidActorVector to_uuid_actor_vec(const std::vector &actors); }; diff --git a/include/xstudio/playhead/playhead_global_events_actor.hpp b/include/xstudio/playhead/playhead_global_events_actor.hpp index 92119942b..01eb8a23e 100644 --- a/include/xstudio/playhead/playhead_global_events_actor.hpp +++ b/include/xstudio/playhead/playhead_global_events_actor.hpp @@ -35,7 +35,6 @@ namespace playhead { caf::behavior make_behavior() override { return behavior_; } protected: - void on_exit() override; caf::behavior behavior_; diff --git a/include/xstudio/playhead/string_out_actor.hpp b/include/xstudio/playhead/string_out_actor.hpp index 07ac996cd..c8f651089 100644 --- a/include/xstudio/playhead/string_out_actor.hpp +++ b/include/xstudio/playhead/string_out_actor.hpp @@ -12,27 +12,23 @@ namespace playhead { /* This simple actor allows a number of sources to be 'strung' together so that they can be played in a sequence. This supports the 'String' - compare mode. It provides the one message handler required to be + compare mode. It provides the one message handler required to be playable by a SubPlayhead, plug it handles event messages emitted by - any of its sources. - + any of its sources. + This handles mixed frame rate sources. - + */ class StringOutActor : public caf::event_based_actor { public: - - StringOutActor( - caf::actor_config &cfg, - const utility::UuidActorVector &sources); + StringOutActor(caf::actor_config &cfg, const utility::UuidActorVector &sources); virtual ~StringOutActor() = default; const char *name() const override { return NAME.c_str(); } private: - - void build_frame_map( + void build_frame_map( const media::MediaType media_type, const utility::TimeSourceMode tsm, const utility::FrameRate &override_rate, @@ -41,17 +37,14 @@ namespace playhead { void finalise_frame_map(caf::typed_response_promise rp); - inline static const std::string NAME = "StringOutActor"; + inline static const std::string NAME = "StringOutActor"; - caf::behavior make_behavior() override { - return behavior_; - } + caf::behavior make_behavior() override { return behavior_; } utility::UuidActorVector source_actors_; caf::actor event_group_; - std::map< utility::Uuid, media::FrameTimeMapPtr > source_frames_; + std::map source_frames_; caf::behavior behavior_; - }; } // namespace playhead } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playhead/sub_playhead.hpp b/include/xstudio/playhead/sub_playhead.hpp index 598467449..b53c09b45 100644 --- a/include/xstudio/playhead/sub_playhead.hpp +++ b/include/xstudio/playhead/sub_playhead.hpp @@ -28,7 +28,7 @@ namespace playhead { const utility::TimeSourceMode time_source_mode_, const utility::FrameRate override_frame_rate_, const media::MediaType media_type, - const utility::Uuid & uuid = utility::Uuid::generate()); + const utility::Uuid &uuid = utility::Uuid::generate()); ~SubPlayhead(); const char *name() const override { return NAME.c_str(); } @@ -49,9 +49,7 @@ namespace playhead { void init(); - caf::behavior make_behavior() override { - return behavior_; - } + caf::behavior make_behavior() override { return behavior_; } void broadcast_image_frame( const utility::time_point when_to_show_frame, @@ -64,6 +62,8 @@ namespace playhead { std::shared_ptr frame, const bool is_future_frame); + void broadcast_audio_samples(); + std::vector get_lookahead_frame_pointers( media::AVFrameIDsAndTimePoints &result, const int max_num_frames); @@ -125,7 +125,6 @@ namespace playhead { void bookmark_changed(const utility::UuidActor bookmark); protected: - media::FrameTimeMap::iterator current_frame_iterator(); media::FrameTimeMap::iterator current_frame_iterator(const timebase::flicks t); @@ -146,13 +145,13 @@ namespace playhead { int logical_frame_ = {0}; timebase::flicks position_flicks_ = timebase::k_flicks_zero_seconds; - bool playing_forwards_ = {true}; - float playback_velocity_ = {1.0f}; - int read_ahead_frames_ = {0}; - int precache_start_frame_ = {std::numeric_limits::lowest()}; - int64_t frame_offset_ = {0}; + bool playing_forwards_ = {true}; + float playback_velocity_ = {1.0f}; + int read_ahead_frames_ = {0}; + int precache_start_frame_ = {std::numeric_limits::lowest()}; + int64_t frame_offset_ = {0}; timebase::flicks forced_duration_ = timebase::k_flicks_zero_seconds; - utility::FrameRate default_rate_ = utility::FrameRate(timebase::k_flicks_24fps); + utility::FrameRate default_rate_ = utility::FrameRate(timebase::k_flicks_24fps); const bool source_is_timeline_; int pre_cache_read_ahead_frames_ = {32}; diff --git a/include/xstudio/playlist/playlist_actor.hpp b/include/xstudio/playlist/playlist_actor.hpp index a82f93a81..a62167b64 100644 --- a/include/xstudio/playlist/playlist_actor.hpp +++ b/include/xstudio/playlist/playlist_actor.hpp @@ -45,7 +45,8 @@ namespace playlist { caf::behavior make_behavior() override { return message_handler() .or_else(base_.container_message_handler(this)) - .or_else(notification_.message_handler(this, base_.event_group())); + .or_else(notification_.message_handler(this, base_.event_group())) + .or_else(utility::NotificationHandler::default_event_handler()); } void add_media( @@ -98,9 +99,9 @@ namespace playlist { const utility::UuidUuidMap &media_map); private: - caf::behavior behavior_; Playlist base_; utility::NotificationHandler notification_; + utility::JsonStore playhead_serialisation_; caf::actor_addr session_; utility::UuidActor playhead_; diff --git a/include/xstudio/plugin_manager/plugin_base.hpp b/include/xstudio/plugin_manager/plugin_base.hpp index 3e8608ca5..13839476f 100644 --- a/include/xstudio/plugin_manager/plugin_base.hpp +++ b/include/xstudio/plugin_manager/plugin_base.hpp @@ -50,13 +50,21 @@ namespace plugin { the overlay can render ontop of the image, after it is drawn. If 'have_alpha_buffer' is false, the BeforeImage pass is not executed. */ - virtual void render_opengl( + virtual void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer){}; + /* An overlay can render visuals to the viewport without an associated + image via this method. */ + virtual void render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer){}; + [[nodiscard]] virtual RenderPass preferred_render_pass() const { return AfterImage; } }; @@ -77,7 +85,6 @@ namespace plugin { } protected: - void on_exit() override; virtual caf::message_handler message_handler_extensions() { @@ -88,8 +95,9 @@ namespace plugin { // TODO: deprecate prepare_render_data and use this everywhere virtual utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr & /*image*/, const std::string & /*viewport_name*/ - ) const { + const media_reader::ImageBufPtr & /*image*/, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const { return utility::BlindDataObjectPtr(); } @@ -101,13 +109,16 @@ namespace plugin { const bool /*playhead_playing*/ ) {} - virtual ViewportOverlayRendererPtr make_overlay_renderer() { + virtual ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) { return ViewportOverlayRendererPtr(); } // Override this and return your own subclass of GPUPreDrawHook to allow // arbitrary GPU rendering (e.g. when in the viewport OpenGL context) - virtual GPUPreDrawHookPtr make_pre_draw_gpu_hook() { return GPUPreDrawHookPtr(); } + virtual GPUPreDrawHookPtr make_pre_draw_gpu_hook(const std::string &viewport_name) { + return GPUPreDrawHookPtr(); + } // reimplement this function in an annotations plugin to return your // custom annotation class, based on bookmark::AnnotationBase base class. @@ -129,11 +140,17 @@ namespace plugin { virtual void on_playhead_playing_changed(const bool // is playing ) {} + /* Reimplement to receive this notification telling us when the playhead driving a given + named viewport has changed */ + virtual void + viewport_playhead_changed(const std::string &viewport_name, caf::actor playhead) {} + + /* Use this function to define the qml code that draws information over the xstudio viewport. See basic_viewport_masking and pixel_probe plugin examples. */ void qml_viewport_overlay_code(const std::string &code); - /* Use this function to create a new bookmark on the given frame (as + /* Use this function to create a new bookmark on the given frame (as per frame_details). See annotations_tool.cpp for example useage. */ utility::Uuid create_bookmark_on_frame( const media::AVFrameID &frame_details, @@ -204,10 +221,9 @@ namespace plugin { void join_studio_events(); void __images_going_on_screen( - const media_reader::ImageBufDisplaySetPtr & image_set, + const media_reader::ImageBufDisplaySetPtr &image_set, const std::string viewport_name, - const bool playhead_playing - ); + const bool playhead_playing); caf::actor_addr active_viewport_playhead_; caf::actor_addr playhead_media_events_group_; diff --git a/include/xstudio/session/session_actor.hpp b/include/xstudio/session/session_actor.hpp index a9d2df8d0..f226948ff 100644 --- a/include/xstudio/session/session_actor.hpp +++ b/include/xstudio/session/session_actor.hpp @@ -8,6 +8,7 @@ #include "xstudio/session/session.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace xstudio { namespace session { @@ -28,9 +29,10 @@ namespace session { inline static const std::string NAME = "SessionActor"; void init(); - caf::message_handler message_handler(); + caf::message_handler message_handler(); caf::behavior make_behavior() override { return behavior_; } + void create_container( caf::actor actor, caf::typed_response_promise> rp, @@ -78,17 +80,11 @@ namespace session { const utility::Uuid &uuid_before = utility::Uuid(), const bool into = false); - void save_json_to( - caf::typed_response_promise &rp, - const utility::JsonStore &js, - const caf::uri &path, - const bool update_path = true, - const size_t hash = 0); - void associate_bookmarks(caf::typed_response_promise &rp); void sync_to_json_store(caf::typed_response_promise &rp); + [[nodiscard]] std::string get_next_name(const std::string &name_template) const; void gather_media_sources( caf::typed_response_promise &rp, @@ -121,10 +117,13 @@ namespace session { void check_media_hook_plugin_version(const utility::JsonStore &jsn, const caf::uri &path); + utility::NotificationHandler notification_; + caf::behavior behavior_; Session base_; caf::actor json_store_; caf::actor bookmarks_; + caf::actor ioactor_; std::map playlists_; std::map serialise_targets_; // std::map players_; @@ -134,6 +133,8 @@ namespace session { utility::UuidActor inspectedContainer_; std::vector selectedMedia_; + + utility::UuidActorVector selection_; }; } // namespace session } // namespace xstudio diff --git a/include/xstudio/subset/subset_actor.hpp b/include/xstudio/subset/subset_actor.hpp index c577faf36..40e9639c2 100644 --- a/include/xstudio/subset/subset_actor.hpp +++ b/include/xstudio/subset/subset_actor.hpp @@ -12,7 +12,11 @@ namespace subset { class SubsetActor : public caf::event_based_actor { public: SubsetActor(caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn); - SubsetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name, const std::string &override_type = "Subset"); + SubsetActor( + caf::actor_config &cfg, + caf::actor playlist, + const std::string &name, + const std::string &override_type = "Subset"); ~SubsetActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -44,9 +48,10 @@ namespace subset { bool remove_media(caf::actor actor, const utility::Uuid &uuid); protected: - utility::JsonStore serialise() const { return base_.serialise(); } + utility::JsonStore playhead_serialisation_; + caf::behavior behavior_; caf::actor_addr playlist_; caf::actor change_event_group_; diff --git a/include/xstudio/sync/sync_actor.hpp b/include/xstudio/sync/sync_actor.hpp deleted file mode 100644 index be2cd86e0..000000000 --- a/include/xstudio/sync/sync_actor.hpp +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include -#include -#include - -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace sync { - class SyncActor : public caf::event_based_actor { - public: - SyncActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncActor"; - caf::behavior behavior_; - }; - - class SyncGatewayActor : public caf::event_based_actor { - public: - SyncGatewayActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncGatewayActor"; - caf::behavior behavior_; - }; - - class SyncGatewayManagerActor : public caf::event_based_actor { - public: - SyncGatewayManagerActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncGatewayManagerActor"; - caf::behavior behavior_; - std::map lock_key_; - std::map clients_; - }; -} // namespace sync -} // namespace xstudio diff --git a/include/xstudio/timeline/clip_actor.hpp b/include/xstudio/timeline/clip_actor.hpp index d1196de9b..74a811e84 100644 --- a/include/xstudio/timeline/clip_actor.hpp +++ b/include/xstudio/timeline/clip_actor.hpp @@ -38,7 +38,10 @@ namespace timeline { return message_handler().or_else(base_.container_message_handler(this)); } - void link_media(caf::typed_response_promise rp, const utility::UuidActor &media); + void link_media( + caf::typed_response_promise rp, + const utility::UuidActor &media, + const bool refresh = true); private: Clip base_; diff --git a/include/xstudio/timeline/item.hpp b/include/xstudio/timeline/item.hpp index ea41fb1b0..37994f3d0 100644 --- a/include/xstudio/timeline/item.hpp +++ b/include/xstudio/timeline/item.hpp @@ -213,8 +213,8 @@ namespace timeline { [[nodiscard]] std::string flag() const { return flag_; } [[nodiscard]] utility::JsonStore prop() const { return prop_; } [[nodiscard]] bool transparent() const; - [[nodiscard]] utility::UuidActorVector - find_all_uuid_actors(const ItemType item_type, const bool only_enabled_items = false) const; + [[nodiscard]] utility::UuidActorVector find_all_uuid_actors( + const ItemType item_type, const bool only_enabled_items = false) const; [[nodiscard]] std::vector> find_all_items(const ItemType item_type, const ItemType track_type = IT_NONE) const; @@ -328,8 +328,7 @@ namespace timeline { const media::MediaType media_type, const utility::TimeSourceMode tsm, const utility::FrameRate &override_rate, - const utility::UuidSet &focus_list = utility::UuidSet() - ); + const utility::UuidSet &focus_list = utility::UuidSet()); private: bool process_event(const utility::JsonStore &event); @@ -358,7 +357,6 @@ namespace timeline { [[nodiscard]] caf::actor_system &system() const { return *the_system_; } private: - friend class BuildFrameIDsHelper; ItemType item_type_{IT_NONE}; diff --git a/include/xstudio/timeline/timeline_actor.hpp b/include/xstudio/timeline/timeline_actor.hpp index f1284e4bb..c97bb67f4 100644 --- a/include/xstudio/timeline/timeline_actor.hpp +++ b/include/xstudio/timeline/timeline_actor.hpp @@ -6,6 +6,7 @@ #include "xstudio/timeline/timeline.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace xstudio { namespace timeline { @@ -35,7 +36,9 @@ namespace timeline { caf::message_handler message_handler(); caf::behavior make_behavior() override { - return message_handler().or_else(base_.container_message_handler(this)); + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())); } void add_item(const utility::UuidActor &ua); @@ -90,9 +93,12 @@ namespace timeline { void export_otio( caf::typed_response_promise rp, + const std::string &otio_str, const caf::uri &path, const std::string &type); + void export_otio_as_string(caf::typed_response_promise rp); + private: Timeline base_; caf::actor change_event_group_; @@ -105,12 +111,19 @@ namespace timeline { utility::UuidActorMap actors_; utility::UuidActorMap media_actors_; + utility::JsonStore playhead_serialisation_; + caf::actor_addr playlist_; bool content_changed_{false}; utility::UuidActor playhead_; // might need to prune.. ? std::set events_processed_; utility::UuidActorVector video_tracks_; + + // current selection + utility::UuidActorVector selection_; + + utility::NotificationHandler notification_; }; } // namespace timeline diff --git a/include/xstudio/timeline/track_actor.hpp b/include/xstudio/timeline/track_actor.hpp index 4136ea710..fa134141b 100644 --- a/include/xstudio/timeline/track_actor.hpp +++ b/include/xstudio/timeline/track_actor.hpp @@ -129,7 +129,6 @@ namespace timeline { // might need to prune.. ? std::set events_processed_; utility::NotificationHandler notification_; - }; } // namespace timeline } // namespace xstudio diff --git a/include/xstudio/ui/canvas/canvas.hpp b/include/xstudio/ui/canvas/canvas.hpp index 7242c0afc..607fe3b7f 100644 --- a/include/xstudio/ui/canvas/canvas.hpp +++ b/include/xstudio/ui/canvas/canvas.hpp @@ -82,6 +82,7 @@ namespace ui { next_shape_id_ = o.next_shape_id_; // make sure current_item_ is pushed into finished // strokes/captions on copy + changed(); end_draw_no_lock(); return *this; } @@ -208,6 +209,12 @@ namespace ui { std::array caption_cursor_position() const; Imath::V2f caption_cursor_bottom() const; + // Note: not a real hash (yet). Just a way of knowing when the canvas + // appearance has changed but the hash is not unique from one + // canvas to the next. The hash does change if a given canvas has + // a new stroke or caption etc. + size_t hash() const { return hash_; } + void update_caption_text(const std::string &text); void update_caption_position(const Imath::V2f &position); void update_caption_width(float wrap_width); @@ -304,6 +311,7 @@ namespace ui { std::string::const_iterator cursor_position_; uint32_t next_shape_id_{0}; + size_t hash_{0}; mutable std::shared_mutex mutex_; }; diff --git a/include/xstudio/ui/keyboard.hpp b/include/xstudio/ui/keyboard.hpp index 3448cf4a4..7b1562e08 100644 --- a/include/xstudio/ui/keyboard.hpp +++ b/include/xstudio/ui/keyboard.hpp @@ -54,8 +54,17 @@ namespace ui { bool update(const Hotkey &o); + void add_watcher(caf::actor_addr w); + void watcher_died(caf::actor_addr &watcher); + // setting an exclusive watcher means it is the only actor to get the + // hotkey event. Hotkey events are not broadcast to 'regular' watchers + // or to the general event_group of the global keyboard_events actor. + // The most recent actor to set itself exclusive is the one that gets + // the events. + void exclusive_watcher(caf::actor_addr w, bool watch); + void update_state( const std::set ¤t_keys, const std::string &context, @@ -67,6 +76,8 @@ namespace ui { sequence_to_key_and_modifier(const std::string &sequence, int &keycode, int &modifier); private: + + void notify(const std::string &context, const std::string &window, caf::actor keypress_monitor); void notify_watchers(const std::string &context, const std::string &window); int key_ = 0; @@ -78,6 +89,7 @@ namespace ui { std::string description_; bool auto_repeat_; std::vector watchers_; + std::vector exclusive_watchers_; public: // This is a straight clone of the Qt::Key enums but instead we provide string diff --git a/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp index 8102a3c29..afe03bf0f 100644 --- a/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp @@ -53,4 +53,4 @@ namespace ui { } // end namespace opengl } // end namespace ui -} // end namespace xstudio \ No newline at end of file +} // end namespace xstudio diff --git a/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp b/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp index 7d0c96ef7..f3620f38d 100644 --- a/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp +++ b/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp @@ -13,21 +13,19 @@ namespace ui { class GLDoubleBufferedTexture { public: - typedef std::shared_ptr GLBlindTexturePtr; - template - static GLDoubleBufferedTexture * create() { - // we are creating 8 textures. This allows us to do asynchronous uploads, - // so that some of the textures can start uploading pixel data for upcoming - // redraws while others are being used to draw the current frame. - // TODO: some rigorous testing for best number of textures and upload threads. - // Also provide user preferences to manually tweak these settings if needed. - auto result = new GLDoubleBufferedTexture(); - for (int i = 0; i < 48; ++i) { - result->textures_.emplace_back(new TexType()); - } - return result; + template static GLDoubleBufferedTexture *create() { + // we are creating 8 textures. This allows us to do asynchronous uploads, + // so that some of the textures can start uploading pixel data for upcoming + // redraws while others are being used to draw the current frame. + // TODO: some rigorous testing for best number of textures and upload threads. + // Also provide user preferences to manually tweak these settings if needed. + auto result = new GLDoubleBufferedTexture(); + for (int i = 0; i < 48; ++i) { + result->textures_.emplace_back(new TexType()); + } + return result; } virtual ~GLDoubleBufferedTexture() = default; @@ -39,36 +37,33 @@ namespace ui { void queue_image_set_for_upload(const media_reader::ImageBufDisplaySetPtr &images); private: - GLDoubleBufferedTexture(); GLBlindTexturePtr get_free_texture(); class TexSet : public std::vector { public: - TexSet::iterator find(const media_reader::ImageBufPtr &image) { - return std::find_if(begin(), end(), [=](const auto & t) { - return t->media_key() == image->media_key(); - }); - } - TexSet::iterator find_pending(const media_reader::ImageBufPtr &image) { - return std::find_if(begin(), end(), [=](const auto & t) { - return t->pending_media_key() == image->media_key(); - }); - } + TexSet::iterator find(const media_reader::ImageBufPtr &image) { + return std::find_if(begin(), end(), [=](const auto &t) { + return t->media_key() == image->media_key(); + }); + } + TexSet::iterator find_pending(const media_reader::ImageBufPtr &image) { + return std::find_if(begin(), end(), [=](const auto &t) { + return t->pending_media_key() == image->media_key(); + }); + } }; - void queue_for_upload(TexSet &available_textures, const media_reader::ImageBufPtr &image); + void queue_for_upload( + TexSet &available_textures, const media_reader::ImageBufPtr &image); TexSet textures_; std::deque image_queue_; - TextureTransferWorker * worker_; - - + TextureTransferWorker *worker_; }; } // namespace opengl } // namespace ui } // namespace xstudio - diff --git a/include/xstudio/ui/opengl/opengl_texture_base.hpp b/include/xstudio/ui/opengl/opengl_texture_base.hpp index b49c95c92..2c03d6d1f 100644 --- a/include/xstudio/ui/opengl/opengl_texture_base.hpp +++ b/include/xstudio/ui/opengl/opengl_texture_base.hpp @@ -19,7 +19,6 @@ namespace ui { class GLBlindTex { public: - GLBlindTex(); ~GLBlindTex(); @@ -31,7 +30,9 @@ namespace ui { } - [[nodiscard]] const media::MediaKey &pending_media_key() const { return pending_media_key_; } + [[nodiscard]] const media::MediaKey &pending_media_key() const { + return pending_media_key_; + } [[nodiscard]] const media::MediaKey &media_key() const { return media_key_; } [[nodiscard]] const utility::time_point &when_last_used() const { return when_last_used_; @@ -44,10 +45,9 @@ namespace ui { void cancel_upload(); protected: - - virtual uint8_t *map_buffer_for_upload(const size_t buffer_size) = 0; - virtual void __bind(int tex_index, Imath::V2i &dims) = 0; - virtual size_t tex_size_bytes() const = 0; + virtual uint8_t *map_buffer_for_upload(const size_t buffer_size) = 0; + virtual void __bind(int tex_index, Imath::V2i &dims) = 0; + virtual size_t tex_size_bytes() const = 0; void wait_on_upload_pixels(); @@ -59,13 +59,13 @@ namespace ui { media::MediaKey pending_media_key_; media_reader::ImageBufPtr pending_source_frame_; - uint8_t * gpu_mapped_mem_ = nullptr; + uint8_t *gpu_mapped_mem_ = nullptr; std::thread upload_thread_; std::mutex mutex_; std::condition_variable cv_; bool pending_upload_ = {false}; - bool in_progress_ = {false}; + bool in_progress_ = {false}; }; } // namespace opengl } // namespace ui diff --git a/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp b/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp index ab87365b3..17a7c0edb 100644 --- a/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp @@ -43,7 +43,6 @@ namespace ui { class OpenGLViewportRenderer : public viewport::ViewportRenderer { public: - OpenGLViewportRenderer(const std::string &window_id); virtual ~OpenGLViewportRenderer() override; @@ -52,7 +51,8 @@ namespace ui { const media_reader::ImageBufDisplaySetPtr &images, const Imath::M44f &window_to_viewport_matrix, const Imath::M44f &viewport_to_image_matrix, - const std::map &overlay_renderers) override; + const std::map + &overlay_renderers) override; virtual void draw_image( const media_reader::ImageBufPtr &image_to_be_drawn, @@ -67,14 +67,14 @@ namespace ui { void set_prefs(const utility::JsonStore &prefs) override; protected: - void __draw_image( const media_reader::ImageBufDisplaySetPtr &all_images, const int index, const Imath::M44f &window_to_viewport_matrix, const Imath::M44f &viewport_to_image_space, const float viewport_du_dx, - const std::map &overlay_renderers); + const std::map + &overlay_renderers); void pre_init() override; @@ -82,8 +82,7 @@ namespace ui { const viewport::GPUShaderPtr &image_buffer_unpack_shader, const std::vector &operations); - void - upload_image_and_colour_data(const media_reader::ImageBufPtr &image); + void upload_image_and_colour_data(const media_reader::ImageBufPtr &image); void bind_textures(const media_reader::ImageBufPtr &image); void release_textures(); void clear_viewport_area(const Imath::M44f &window_to_viewport_matrix); @@ -91,22 +90,29 @@ namespace ui { typedef std::shared_ptr GLTexturePtr; struct SharedResources { - std::vector textures_; - std::map programs_; - ColourPipeLutCollection colour_pipe_textures_; - unsigned int vbo_, vao_; - GLShaderProgramPtr no_image_shader_program_; - void init(); - ~SharedResources(); + std::vector textures_; + std::map programs_; + ColourPipeLutCollection colour_pipe_textures_; + unsigned int vbo_ = 0; + unsigned int vao_ = 0; + GLShaderProgramPtr no_image_shader_program_; + void init(); + ~SharedResources(); }; - + typedef std::shared_ptr SharedResourcesPtr; - std::vector & textures() { return resources_->textures_; } - std::map & shader_programs() { return resources_->programs_; } - ColourPipeLutCollection & colour_pipe_textures() { return resources_->colour_pipe_textures_; } - GLShaderProgramPtr & no_image_shader_program() { return resources_-> no_image_shader_program_; } - unsigned int & vbo() { return resources_->vbo_; } - unsigned int & vao() { return resources_->vao_; } + std::vector &textures() { return resources_->textures_; } + std::map &shader_programs() { + return resources_->programs_; + } + ColourPipeLutCollection &colour_pipe_textures() { + return resources_->colour_pipe_textures_; + } + GLShaderProgramPtr &no_image_shader_program() { + return resources_->no_image_shader_program_; + } + unsigned int &vbo() { return resources_->vbo_; } + unsigned int &vao() { return resources_->vao_; } static std::map s_resources_store_; @@ -114,7 +120,7 @@ namespace ui { GLShaderProgramPtr active_shader_program_; std::string latest_colour_pipe_data_cacheid_; const std::string window_id_; - bool use_ssbo_ = {false}; + bool use_ssbo_ = {false}; bool use_alpha_ = {false}; std::array scissor_coords_; }; diff --git a/include/xstudio/ui/qml/conform_ui.hpp b/include/xstudio/ui/qml/conform_ui.hpp index 4b4c7cbf3..0e3b51d42 100644 --- a/include/xstudio/ui/qml/conform_ui.hpp +++ b/include/xstudio/ui/qml/conform_ui.hpp @@ -22,7 +22,8 @@ namespace xstudio { namespace ui { namespace qml { - class CONFORM_QML_EXPORT ConformEngineUI : public caf::mixin::actor_object { + class CONFORM_QML_EXPORT ConformEngineUI + : public caf::mixin::actor_object { Q_OBJECT public: diff --git a/include/xstudio/ui/qml/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index 879104db8..18a0eacd5 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -770,6 +770,11 @@ namespace ui { Q_INVOKABLE void inhibitScreenSaver(const bool inhibit = true) const; + Q_INVOKABLE QVariant python_callback( + QString method_name, + QUuid python_plugin_uuid, + const QVariant args = QVariant()) const; + private: QQmlEngine *engine_; }; diff --git a/include/xstudio/ui/qml/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index 36f66ef5d..256087a17 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -203,33 +203,59 @@ namespace ui { QUuid hotkey_uuid_; }; + /*XsHotkeyReference item. This lets us 'watch' an already existing hotkey. + We use the name of the hotkey to identify it.*/ class VIEWPORT_QML_EXPORT HotkeyReferenceUI : public QMLActor { Q_OBJECT Q_PROPERTY(QString sequence READ sequence NOTIFY sequenceChanged) Q_PROPERTY( - QUuid hotkeyUuid READ hotkeyUuid WRITE setHotkeyUuid NOTIFY hotkeyUuidChanged) + QString hotkeyName READ hotkeyName WRITE setHotkeyName NOTIFY hotkeyNameChanged) + Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) + Q_PROPERTY(QString context READ context WRITE setContext NOTIFY contextChanged) + Q_PROPERTY(bool exclusive READ exclusive WRITE setExclusive NOTIFY exclusiveChanged) public: explicit HotkeyReferenceUI(QObject *parent = nullptr); - ~HotkeyReferenceUI() override = default; + ~HotkeyReferenceUI() override; void init(caf::actor_system &system) override; - void setHotkeyUuid(const QUuid &uuid); + void setHotkeyName(const QString &name); - [[nodiscard]] const QUuid &hotkeyUuid() const { return uuid_; } + [[nodiscard]] const QString &hotkeyName() const { return hotkey_name_; } [[nodiscard]] const QString &sequence() const { return sequence_; } + [[nodiscard]] QUuid uuid() const { return hotkey_uuid_; } + [[nodiscard]] const QString context() const { return QStringFromStd(context_); } + [[nodiscard]] bool exclusive() const { return exclusive_; } + + void setContext(const QString &context) { + context_ = StdFromQString(context); + emit contextChanged(); + } + + void setExclusive(const bool exclusive); signals: void sequenceChanged(); - void hotkeyUuidChanged(); + void hotkeyNameChanged(); + void activated(const QString context); + void uuidChanged(); + void contextChanged(); + void exclusiveChanged(); private: + + void notifyExclusiveChanged(); + QString sequence_; - QUuid uuid_; + QString hotkey_name_; + QUuid hotkey_uuid_; + std::string context_; + bool exclusive_ = {false}; + }; } // namespace qml diff --git a/include/xstudio/ui/qml/qml_viewport.hpp b/include/xstudio/ui/qml/qml_viewport.hpp index 248cd30c2..c777b7a5a 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -35,9 +35,10 @@ namespace ui { Q_OBJECT Q_PROPERTY(QPoint mousePosition READ mousePosition NOTIFY mousePositionChanged) - Q_PROPERTY(QVariantList imageBoundariesInViewport READ imageBoundariesInViewport NOTIFY - imageBoundariesInViewportChanged) - Q_PROPERTY(QVariantList imageResolutions READ imageResolutions NOTIFY imageResolutionsChanged) + Q_PROPERTY(QVariantList imageBoundariesInViewport READ imageBoundariesInViewport + NOTIFY imageBoundariesInViewportChanged) + Q_PROPERTY(QVariantList imageResolutions READ imageResolutions NOTIFY + imageResolutionsChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QUuid playheadUuid READ playheadUuid NOTIFY playheadUuidChanged) diff --git a/include/xstudio/ui/qml/qml_viewport_renderer.hpp b/include/xstudio/ui/qml/qml_viewport_renderer.hpp index 4430b609f..4e4a52760 100644 --- a/include/xstudio/ui/qml/qml_viewport_renderer.hpp +++ b/include/xstudio/ui/qml/qml_viewport_renderer.hpp @@ -62,7 +62,7 @@ namespace ui { } void set_playhead(caf::actor playhead); - + [[nodiscard]] QVariantList imageResolutions() const; [[nodiscard]] QVariantList imageBoundariesInViewport() const; [[nodiscard]] caf::actor playhead() { diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index 0bd04b996..4099be87d 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -156,6 +156,10 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object - exportOTIO(const QModelIndex &timeline, const QUrl &path, const QString &type = "otio"); + Q_INVOKABLE QFuture exportOTIO( + const QModelIndex &timeline, const QUrl &path, const QString &type = "otio_json"); + Q_INVOKABLE QVariantList getTimelineExportTypes() const; // end timeline operations @@ -402,7 +411,7 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object( - std::shared_ptr(audio_dev)); + std::shared_ptr(audio_dev), true); link_to(audio_output_); } } diff --git a/include/xstudio/ui/viewport/viewport.hpp b/include/xstudio/ui/viewport/viewport.hpp index 1dbaa7375..0575f3c9d 100644 --- a/include/xstudio/ui/viewport/viewport.hpp +++ b/include/xstudio/ui/viewport/viewport.hpp @@ -79,7 +79,7 @@ namespace ui { * an active OpenGL context). Thread safe, as required by QML integration where * render thread is separate to main GUI thread. * - * prepare_render_data should be called from GUI thread before calling this + * prepare_render_data should be called from GUI thread before calling this * method. */ void render() const; @@ -111,12 +111,14 @@ namespace ui { /** * @brief Any pre-render interaction with Viewport state data etc. must be done - * in this function. All necessary data for rendering the viewport must be gatherered - * and finalised within this function. The data should be made thread safe, as the - * rendering can/will be executed in a separate thread to the main GUI thread that - * the Viewport lives in. + * in this function. All necessary data for rendering the viewport must be + * gatherered and finalised within this function. The data should be made thread + * safe, as the rendering can/will be executed in a separate thread to the main GUI + * thread that the Viewport lives in. */ - void prepare_render_data(const utility::time_point &when_going_on_screen = utility::time_point()); + void prepare_render_data( + const utility::time_point &when_going_on_screen = utility::time_point(), + const bool sync_to_playhead = false); /** * @brief Initialise the viewport. @@ -124,7 +126,10 @@ namespace ui { * @details Carry out first time render one-shot initialisation of any member * data or other state/data initialisation of graphics resources */ - void init() { if (active_renderer_) active_renderer_->init(); } + void init() { + if (active_renderer_) + active_renderer_->init(); + } /** * @brief Inform the viewport of how its coordinate system maps to the @@ -210,7 +215,7 @@ namespace ui { * @brief Get the bounding box, in the viewport area, of the current image * */ - const std::vector & image_bounds_in_viewport_pixels() const { + const std::vector &image_bounds_in_viewport_pixels() const { return image_bounds_in_viewport_pixels_; } @@ -218,7 +223,7 @@ namespace ui { * @brief Get the resolution of the current images * */ - const std::vector & image_resolutions() const { + const std::vector &image_resolutions() const { return image_resolutions_; } @@ -230,7 +235,13 @@ namespace ui { caf::message_handler message_handler() override; - enum ChangeCallbackId { Redraw, TranslationChanged, ImageResolutionsChanged, ImageBoundsChanged, PlayheadChanged }; + enum ChangeCallbackId { + Redraw, + TranslationChanged, + ImageResolutionsChanged, + ImageBoundsChanged, + PlayheadChanged + }; typedef std::function ChangeCallback; @@ -238,9 +249,9 @@ namespace ui { void set_playhead(caf::actor playhead, const bool wait_for_refresh = false); - void set_compare_mode(const std::string & compare_mode); + void set_compare_mode(const std::string &compare_mode); - void grid_mode_media_select(const PointerEvent &pointer_event); + void pointer_select_media(const PointerEvent &pointer_event); caf::actor fps_monitor() { return fps_monitor_; } @@ -300,7 +311,7 @@ namespace ui { Imath::V4f pointer_position_; FitMode fit_mode_ = {Height}; MirrorMode mirror_mode_ = {Off}; - float layout_aspect_ = {16.0f / 9.0f}; + float layout_aspect_ = {16.0f / 9.0f}; } state_, interact_start_state_; struct FitModeStat { @@ -343,8 +354,8 @@ namespace ui { const int in_pt = -1, const int out_pt = -1); - media_reader::ImageBufDisplaySetPtr prepare_image_for_display( - const media_reader::ImageBufPtr &image_buf) const; + media_reader::ImageBufDisplaySetPtr + prepare_image_for_display(const media_reader::ImageBufPtr &image_buf) const; void calc_image_bounds_in_viewport_pixels(); @@ -384,19 +395,20 @@ namespace ui { ChangeCallback event_callback_; protected: - - bool done_init_ = {false}; - bool playing_ = {false}; - bool is_visible_ = {false}; + bool done_init_ = {false}; + bool playing_ = {false}; + bool is_visible_ = {false}; size_t last_images_hash_ = {0}; - bool needs_redraw_ = {true}; + bool needs_redraw_ = {true}; + bool hover_image_select_ = {false}; std::set held_keys_; std::vector image_bounds_in_viewport_pixels_; std::vector image_resolutions_; size_t image_bounds_hash_ = {0}; std::map overlay_plugin_instances_; std::map hud_plugin_instances_; - std::map viewport_overlay_renderers_; + std::map + viewport_overlay_renderers_; std::map overlay_pre_render_hooks_; module::BooleanAttribute *zoom_mode_toggle_; @@ -418,6 +430,7 @@ namespace ui { utility::Uuid reset_hotkey_; utility::Uuid fit_mode_hotkey_; utility::Uuid mirror_mode_hotkey_; + utility::Uuid hover_select_; utility::Uuid reset_menu_item_; @@ -436,7 +449,6 @@ namespace ui { caf::actor viewport_layout_controller_; ViewportRendererPtr viewport_layout_renderer_; std::vector> compare_modes_; - }; std::vector viewport_layouts_; diff --git a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp index 588236c25..d0b3e4111 100644 --- a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp +++ b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp @@ -32,7 +32,7 @@ namespace ui { caf::actor_config &cfg, caf::actor viewport, std::map overlay_actors, - const std::string &viewport_name, + const std::string &viewport_name, caf::actor colour_pipeline); ~ViewportFrameQueueActor() override; @@ -44,8 +44,7 @@ namespace ui { caf::behavior behavior_; - caf::actor playhead_broadcast_group_; - caf::actor playhead_; + utility::UuidActor playhead_; utility::Uuid current_key_sub_playhead_id_, previous_key_sub_playhead_id_; /** * @brief Receive frame buffer and colour pipeline data for drawing to screen. @@ -69,14 +68,13 @@ namespace ui { void append_overlays_data( caf::typed_response_promise rp, - media_reader::ImageBufDisplaySet * result); + media_reader::ImageBufDisplaySet *result); void append_overlays_data( caf::typed_response_promise rp, const int img_idx, - media_reader::ImageBufDisplaySet * result, - std::shared_ptr response_count - ); + media_reader::ImageBufDisplaySet *result, + std::shared_ptr response_count); typedef std::map OrderedImagesToDraw; @@ -96,7 +94,8 @@ namespace ui { double average_video_refresh_period() const; - caf::typed_response_promise set_playhead(caf::actor playhead, const bool prefetch_inital_image); + caf::typed_response_promise + set_playhead(utility::UuidActor playhead, const bool prefetch_inital_image); bool playing_ = {false}; bool playing_forwards_ = {true}; @@ -126,7 +125,6 @@ namespace ui { std::string viewport_layout_mode_name_; double playhead_velocity_ = {1.0}; - }; } // namespace viewport diff --git a/include/xstudio/ui/viewport/viewport_layout_plugin.hpp b/include/xstudio/ui/viewport/viewport_layout_plugin.hpp index 56c2994b7..4f8e03870 100644 --- a/include/xstudio/ui/viewport/viewport_layout_plugin.hpp +++ b/include/xstudio/ui/viewport/viewport_layout_plugin.hpp @@ -12,176 +12,172 @@ namespace xstudio { namespace ui { -namespace viewport { - - /** - * @brief ViewportLayoutPlugin class. - * - * @details - * Subclass from this to create custom layouts for mulitple images in the - * xSTUDIO viewport. The plugin is told about the number of images that - * are available for drawing into the viewport. The plugin is then - * resposible for providing a transform matrix for each image to position - * each image in the viewport coordinate space. - * Plugins can also provide their own custom GPU shader code for both the - * vertex and fragment shaders at draw time. - * A complete draw routine can also be overidden for full control of how - * image pixels are rendered to the viewport. - */ - - class ViewportLayoutPlugin : public plugin::StandardPlugin { - public: - - ViewportLayoutPlugin( - caf::actor_config &cfg, - const utility::JsonStore &init_settings); - - protected: + namespace viewport { /** - * @brief Calculate a transform matrix for each on-screen video item - * for displaying into the viewport coordinate space. Also provide an - * aspect ratio hint for the overall layout geometry. + * @brief ViewportLayoutPlugin class. * - * @details To use the regular viewport renderer, which does a - * straightforward draw of each image, set the member data in the - * layout_data. See image_buffer_set.hpp for more details. - * - * This method is called once only for a given image_set characteristics. - * The characteristics depends on the number of images and the size in - * pixels of each image in the set. If you want to re-compute the layout - * for a given image set characteritics call 'clear_cache()' to force - * a re-evaluation of this method. + * @details + * Subclass from this to create custom layouts for mulitple images in the + * xSTUDIO viewport. The plugin is told about the number of images that + * are available for drawing into the viewport. The plugin is then + * resposible for providing a transform matrix for each image to position + * each image in the viewport coordinate space. + * Plugins can also provide their own custom GPU shader code for both the + * vertex and fragment shaders at draw time. + * A complete draw routine can also be overidden for full control of how + * image pixels are rendered to the viewport. */ - virtual void do_layout( - const std::string &layout_mode, - const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data - ); - /** - * @brief Override this method to provide your own, custom renderer. If - * you don't override the default renderer will be provided which may - * or may not be able to enable your desired viewport graphics layout. - * - * @details See the default renderer implementations for a reference - * for creating a custom viewport renderer - */ - virtual ViewportRenderer * make_renderer(const std::string &window_id) = 0; - - /** - * @brief Call this method to add a layout mode that your plugin - * provides. The string sets the name, this will appear in the list - * of compare modes. If the name is already taken a warning is - * generated. - * The AssemblyMode tells the playhead how many images are needed to - * draw the viewport layout. - * - */ - void add_layout_mode( - const std::string &name, - const playhead::AssemblyMode mode, - const playhead::AutoAlignMode default_auto_align = playhead::AutoAlignMode::AAM_ALIGN_OFF - ); - - /** - * @brief Expose an attribute in the Settings panel for your layout - * plugin. The button for the settings panel will show under the - * 'Compare' viewport toolbar button. - * - */ - void add_layout_settings_attribute(module::Attribute *attr, const std::string &layout_name); + class ViewportLayoutPlugin : public plugin::StandardPlugin { + public: + ViewportLayoutPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + protected: + /** + * @brief Calculate a transform matrix for each on-screen video item + * for displaying into the viewport coordinate space. Also provide an + * aspect ratio hint for the overall layout geometry. + * + * @details To use the regular viewport renderer, which does a + * straightforward draw of each image, set the member data in the + * layout_data. See image_buffer_set.hpp for more details. + * + * This method is called once only for a given image_set characteristics. + * The characteristics depends on the number of images and the size in + * pixels of each image in the set. If you want to re-compute the layout + * for a given image set characteritics call 'clear_cache()' to force + * a re-evaluation of this method. + */ + virtual void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data); + + /** + * @brief Override this method to provide your own, custom renderer. If + * you don't override the default renderer will be provided which may + * or may not be able to enable your desired viewport graphics layout. + * + * @details See the default renderer implementations for a reference + * for creating a custom viewport renderer + */ + virtual ViewportRenderer *make_renderer(const std::string &window_id) = 0; + + /** + * @brief Call this method to add a layout mode that your plugin + * provides. The string sets the name, this will appear in the list + * of compare modes. If the name is already taken a warning is + * generated. + * The AssemblyMode tells the playhead how many images are needed to + * draw the viewport layout. + * + */ + void add_layout_mode( + const std::string &name, + const playhead::AssemblyMode mode, + const playhead::AutoAlignMode default_auto_align = + playhead::AutoAlignMode::AAM_ALIGN_OFF); + + /** + * @brief Expose an attribute in the Settings panel for your layout + * plugin. The button for the settings panel will show under the + * 'Compare' viewport toolbar button. + * + */ + void add_layout_settings_attribute( + module::Attribute *attr, const std::string &layout_name); + + /** + * @brief Call this method with necessary QML snippet to instance + * custom overlay graphics for the viewport. + * See plugins/viewport_layout/wipe_viewport_layout for example useage + * + */ + void add_viewport_layout_qml_overlay( + const std::string &layout_name, const std::string &qml_code); + + void on_exit() override; + + private: + void init(); + + using LayoutDataRP = + caf::typed_response_promise; + + void attribute_changed( + const utility::Uuid &attribute_uuid, const int /*role*/ + ) override; + + void __do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + LayoutDataRP rp); + + media_reader::ImageSetLayoutDataPtr + python_layout_data_to_ours(const utility::JsonStore &python_data) const; + + caf::message_handler message_handler_extensions() override { return handler_; } + + caf::message_handler handler_; + + module::BooleanAttribute *settings_toggle_; + + std::map layout_names_; + + std::map> + layouts_cache_; + + std::map> pending_responses_; + + const bool is_python_plugin_; + + + // Python ViewportLayout plugins work by setting this 'remote_' + // member. This C++ base class talks to the remote_ (which is the + // Python class instance) to effectively provide the virtual methods. + caf::actor event_group_; + caf::actor layouts_manager_; + caf::actor gobal_playhead_events_; + }; /** - * @brief Call this method with necessary QML snippet to instance - * custom overlay graphics for the viewport. - * See plugins/viewport_layout/wipe_viewport_layout for example useage + * @brief ViewportLayoutManager class. * + * @details + * A singleton instance of this class is created by the GlobalActor. The + * ViewportLayoutManager instances ViewportLayoutPlugins, provides model + * data about them to the UI layer so that the Compare button/menu can + * be populated, and activates the layout plugin when the user or api + * switches the active layout. */ - void add_viewport_layout_qml_overlay( - const std::string &layout_name, - const std::string &qml_code - ); - - void on_exit() override; - - private: - - void init(); - - using LayoutDataRP = caf::typed_response_promise; - - void attribute_changed( - const utility::Uuid &attribute_uuid, const int /*role*/ - ) override; - - void __do_layout( - const std::string &layout_mode, - const media_reader::ImageBufDisplaySetPtr &image_set, - LayoutDataRP rp); - - media_reader::ImageSetLayoutDataPtr python_layout_data_to_ours( - const utility::JsonStore &python_data) const; - - caf::message_handler message_handler_extensions() override { return handler_; } - - caf::message_handler handler_; - - module::BooleanAttribute *settings_toggle_; - - std::map layout_names_; - - std::map> layouts_cache_; - - std::map> pending_responses_; - - const bool is_python_plugin_; - - - // Python ViewportLayout plugins work by setting this 'remote_' - // member. This C++ base class talks to the remote_ (which is the - // Python class instance) to effectively provide the virtual methods. - caf::actor event_group_; - caf::actor layouts_manager_; - caf::actor gobal_playhead_events_; - - }; - - /** - * @brief ViewportLayoutManager class. - * - * @details - * A singleton instance of this class is created by the GlobalActor. The - * ViewportLayoutManager instances ViewportLayoutPlugins, provides model - * data about them to the UI layer so that the Compare button/menu can - * be populated, and activates the layout plugin when the user or api - * switches the active layout. - */ - - class ViewportLayoutManager : public caf::event_based_actor { - public: - - ViewportLayoutManager( - caf::actor_config &cfg); - - ~ViewportLayoutManager() override; - void on_exit() override; + class ViewportLayoutManager : public caf::event_based_actor { + public: + ViewportLayoutManager(caf::actor_config &cfg); - caf::behavior make_behavior() override { return behavior_; } + ~ViewportLayoutManager() override; - private: + void on_exit() override; - void spawn_plugins(); + caf::behavior make_behavior() override { return behavior_; } - caf::behavior behavior_; + private: + void spawn_plugins(); - std::map viewport_layout_plugins_; - std::map >> viewport_layouts_; - caf::actor event_group_; + caf::behavior behavior_; - }; + std::map viewport_layout_plugins_; + std::map< + std::string, + std::pair< + caf::actor, + std::pair>> + viewport_layouts_; + caf::actor event_group_; + }; -} // namespace viewport + } // namespace viewport } // namespace ui } // namespace xstudio diff --git a/include/xstudio/ui/viewport/viewport_renderer_base.hpp b/include/xstudio/ui/viewport/viewport_renderer_base.hpp index 0ed07f130..bdfe5d41d 100644 --- a/include/xstudio/ui/viewport/viewport_renderer_base.hpp +++ b/include/xstudio/ui/viewport/viewport_renderer_base.hpp @@ -41,7 +41,8 @@ namespace ui { const media_reader::ImageBufDisplaySetPtr &images, const Imath::M44f &window_to_viewport_matrix, const Imath::M44f &viewport_to_image_matrix, - const std::map &overlay_renderers) = 0; + const std::map + &overlay_renderers) = 0; /** * @brief Provide default preference dictionary for the viewport renderer @@ -61,8 +62,8 @@ namespace ui { */ virtual void set_prefs(const utility::JsonStore &prefs) = 0; - void - set_pre_renderer_hooks(const std::map &hooks) { + void set_pre_renderer_hooks( + const std::map &hooks) { pre_render_gpu_hooks_ = hooks; } @@ -86,15 +87,14 @@ namespace ui { } protected: - /** - * @brief Create a json dict with essential shader parameters for drawing + * @brief Create a json dict with essential shader parameters for drawing * the image to the viewport. * * @details The json dictionary returned from this function is as follows: * - * mat4 to_coord_system (matrix to project from viewport to viewport coordinate system - * mat4 to_canvas (matrix to transform from window area to viewport area) + * mat4 to_coord_system (matrix to project from viewport to viewport coordinate + * system mat4 to_canvas (matrix to transform from window area to viewport area) * bool use_bilinear_filtering (tells us whether to use bilinear pixel filtering) * mat4 image_transform_matrix (transform image into viewport coordinate system) * @@ -104,11 +104,11 @@ namespace ui { const Imath::M44f &window_to_viewport_matrix, const Imath::M44f &viewport_to_image_space, const float viewport_du_dx, - const utility::JsonStore & layout_data, + const utility::JsonStore &layout_data, const int image_index) const; - Imath::M44f mat; - + Imath::M44f mat; + /** * @brief Initialise the viewport. * diff --git a/include/xstudio/utility/enums.hpp b/include/xstudio/utility/enums.hpp index 4b2ff19d6..d58321cfa 100644 --- a/include/xstudio/utility/enums.hpp +++ b/include/xstudio/utility/enums.hpp @@ -5,7 +5,8 @@ namespace xstudio { namespace utility { enum class TimeSourceMode { FIXED = 1, REMAPPED = 2, DYNAMIC = 3 }; enum NotificationType { - NT_INFO = 0, + NT_UNKNOWN = 0, + NT_INFO, NT_WARN, NT_PROCESSING, NT_PROGRESS_RANGE, diff --git a/include/xstudio/utility/frame_rate.hpp b/include/xstudio/utility/frame_rate.hpp index 3c1bedd3a..7f85cab68 100644 --- a/include/xstudio/utility/frame_rate.hpp +++ b/include/xstudio/utility/frame_rate.hpp @@ -58,9 +58,8 @@ namespace utility { class FrameRate : public timebase::flicks { - - public: + public: inline static const std::map rate_string_to_flicks = { {"23.976", timebase::flicks(29429400)}, {"24.0", timebase::flicks(29400000)}, @@ -101,8 +100,8 @@ namespace utility { return std::chrono::duration_cast(*this); } - operator timebase::flicks() const { return this->to_flicks(); } - operator const timebase::flicks & () const { return *this; } + // operator timebase::flicks() const { return this->to_flicks(); } + // operator const timebase::flicks & () const { return *this; } // [[nodiscard]] std::chrono::nanoseconds::rep count() const { return count(); } [[nodiscard]] double to_seconds() const { return timebase::to_seconds(*this); } @@ -112,12 +111,14 @@ namespace utility { fps = 1.0 / timebase::to_seconds(*this); return fps; } - [[nodiscard]] timebase::flicks to_flicks() const { return timebase::flicks(this->count()); } + [[nodiscard]] timebase::flicks to_flicks() const { + return timebase::flicks(this->count()); + } FrameRate &operator=(const FrameRate &) = default; FrameRate &operator=(FrameRate &&) = default; - //operator bool() const { return count() != 0; } + // operator bool() const { return count() != 0; } template friend bool inspect(Inspector &f, FrameRate &x) { return f.object(x).fields(f.field("flick", static_cast(x))); diff --git a/include/xstudio/utility/frame_time.hpp b/include/xstudio/utility/frame_time.hpp index 4fb6fd021..505f80fe4 100644 --- a/include/xstudio/utility/frame_time.hpp +++ b/include/xstudio/utility/frame_time.hpp @@ -1,3 +1,2 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index 748bb421a..3cc12ea3a 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -280,8 +280,9 @@ namespace utility { DWORD nSize = _countof(filename); DWORD result = GetModuleFileNameA(NULL, filename, nSize); if (result == 0) { - spdlog::critical("Unable to determine executable path from Windows API, falling back " - "to standard methods"); + spdlog::critical( + "Unable to determine executable path from Windows API, falling back " + "to standard methods"); } else { auto exePath = fs::path(filename); @@ -296,9 +297,8 @@ namespace utility { #endif std::string path = (root ? (*root) + append_path : fallback_root + append_path); - const auto p = fs::path(path).string(); + const auto p = fs::path(path).string(); return p; - } inline std::string remote_session_path() { diff --git a/include/xstudio/utility/notification_handler.hpp b/include/xstudio/utility/notification_handler.hpp index 8e7a65be7..493fd72e3 100644 --- a/include/xstudio/utility/notification_handler.hpp +++ b/include/xstudio/utility/notification_handler.hpp @@ -15,7 +15,8 @@ class NotificationHandler; class Notification { public: - Notification() = default; + Notification() = default; + Notification(const JsonStore &jsn); virtual ~Notification() = default; Notification( @@ -231,9 +232,7 @@ class Notification { class NotificationHandler { public: - NotificationHandler() = default; - ; - + NotificationHandler() = default; virtual ~NotificationHandler() = default; virtual caf::message_handler diff --git a/include/xstudio/utility/remote_session_file.hpp b/include/xstudio/utility/remote_session_file.hpp index 632b7ce85..787a3e2f2 100644 --- a/include/xstudio/utility/remote_session_file.hpp +++ b/include/xstudio/utility/remote_session_file.hpp @@ -24,11 +24,10 @@ namespace utility { class RemoteSessionFile { public: RemoteSessionFile(const std::string &file_path); - RemoteSessionFile(const std::string path, const int port, const bool sync = false); + RemoteSessionFile(const std::string path, const int port); RemoteSessionFile( const std::string path, const int port, - const bool sync, const std::string session_name, const std::string host = "localhost", const bool force_cleanup = false); @@ -37,12 +36,10 @@ namespace utility { [[nodiscard]] std::string path() const { return path_; } [[nodiscard]] std::string session_name() const { return session_name_; } [[nodiscard]] std::string filename() const { - return session_name_ + "_" + (sync_ ? "sync" : "api") + "_" + host_ + "_" + - std::to_string(port_); + return session_name_ + "_api_" + host_ + "_" + std::to_string(port_); } [[nodiscard]] fs::path filepath() const; [[nodiscard]] std::string host() const { return host_; } - [[nodiscard]] bool sync() const { return sync_; } [[nodiscard]] int port() const { return port_; } [[nodiscard]] bool remove_on_delete() const { return remove_on_delete_; } void set_remove_on_delete(const bool remove = true) { remove_on_delete_ = remove; } @@ -62,7 +59,6 @@ namespace utility { int port_ = {0}; bool remove_on_delete_ = {false}; pid_t pid_ = {0}; - bool sync_ = {false}; fs::file_time_type last_write_; }; @@ -73,10 +69,9 @@ namespace utility { void scan(); - std::string create_session_file(const int port, const bool sync = false); + std::string create_session_file(const int port); void create_session_file( const int port, - const bool sync, const std::string session_name, const std::string host = "localhost", const bool force_cleanup = false); @@ -89,7 +84,6 @@ namespace utility { [[nodiscard]] std::optional first_api() const; - [[nodiscard]] std::optional first_sync() const; [[nodiscard]] std::optional find(const std::string &session_name) const; diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index 01df5cfb7..fed1569aa 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -181,67 +181,67 @@ namespace utility { result = items[0]; if (items.size() > 1) { for (size_t i = 1; i < items.size(); i++) - result += separator + items[i]; - } + result += separator + items[i]; } - - return result; } - inline std::string to_lower(const std::string &str) { - static std::locale loc; - std::string result; - result.reserve(str.size()); + return result; + } - for (auto elem : str) - result += std::tolower(elem, loc); + inline std::string to_lower(const std::string &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); - return result; - } + for (auto elem : str) + result += std::tolower(elem, loc); - inline std::wstring to_upper(const std::wstring &str) { - static std::locale loc; - std::wstring result; - result.reserve(str.size()); + return result; + } - for (auto elem : str) - result += std::toupper(elem, loc); + inline std::wstring to_upper(const std::wstring &str) { + static std::locale loc; + std::wstring result; + result.reserve(str.size()); - return result; - } + for (auto elem : str) + result += std::toupper(elem, loc); - inline std::string to_upper(const std::string &str) { - static std::locale loc; - std::string result; - result.reserve(str.size()); + return result; + } - for (auto elem : str) - result += std::toupper(elem, loc); + inline std::string to_upper(const std::string &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); - return result; - } + for (auto elem : str) + result += std::toupper(elem, loc); - // This is used on WIN build only. There was a note to refactor - // here. TODO: check what it's for and why it might need refactor. - inline std::string to_upper_path(const std::filesystem::path &path) { - static std::locale loc; - std::string result; - result.reserve(path.string().size()); - for (auto elem : path.string()) - result += std::toupper(elem, loc); + return result; + } - return result; - } + // This is used on WIN build only. There was a note to refactor + // here. TODO: check what it's for and why it might need refactor. + inline std::string to_upper_path(const std::filesystem::path &path) { + static std::locale loc; + std::string result; + result.reserve(path.string().size()); + for (auto elem : path.string()) + result += std::toupper(elem, loc); - inline std::optional get_env(const std::string &key) { - const char *val = std::getenv(key.c_str()); - if (val) - return std::string(val); - return {}; - } + return result; + } + + inline std::optional get_env(const std::string &key) { + const char *val = std::getenv(key.c_str()); + if (val) + return std::string(val); + return {}; + } - inline std::string get_env(const std::string &key, const std::string &default_value) { - const char *val = std::getenv(key.c_str()); + inline std::string get_env(const std::string &key, const std::string &default_value) { + const char *val = std::getenv(key.c_str()); if (val) return std::string(val); return default_value; diff --git a/include/xstudio/utility/time_cache.hpp b/include/xstudio/utility/time_cache.hpp index 2c2c781bc..f641b5413 100644 --- a/include/xstudio/utility/time_cache.hpp +++ b/include/xstudio/utility/time_cache.hpp @@ -7,14 +7,14 @@ // When the cache is full (the size of the buffers exceeds the size set on // the cache), buffers (values) are discarded to make space for new buffers. // We do this by checking timestamps that store when a buffer will be needed -// in the near future. If the timestamps are in the past the buffer can be +// in the near future. If the timestamps are in the past the buffer can be // discarded. // The playheads will continually notify the cache which buffers that are going // to need in the coming few seconds for playback, and the cache updates the // timepoints accordingly. -// +// // Note that it's possible for a buffer to be needed 0.1s and again 2.0s in the -// future, say. Once we have advanced > 0.1s, then the other timepoint at 2.0s +// future, say. Once we have advanced > 0.1s, then the other timepoint at 2.0s // becomes relevant in terms of retaining the buffer when we need to make more // space. // @@ -23,12 +23,12 @@ // stored for 10s in favour of the buffer needed more imminently. This is the // reason for storing a set of timepoints telling us when buffers are needed // rather than a single timepoitn. -// -// We also store Uuids against the cache entries. These uuids tell us which +// +// We also store Uuids against the cache entries. These uuids tell us which // object is 'insterested' in that cache entry. For example, a SubPlayhead X154 // might be requesting reads for a particular media item and then retrieveing the // buffers from the cache during display. When the user switches to view another -// piece of media it means the SubPlayhead X154 will be destroyed. It tells the +// piece of media it means the SubPlayhead X154 will be destroyed. It tells the // MediaReader, which then checks the cache - if the only Uuid stored with the // buffers is X154 then the buffers are considered safe for removal as they will // no longer be needed for display. @@ -39,7 +39,7 @@ // our cache is usually in the order of a few hundred or at most a few thousand // entries and binary lookup of std::map is superiour at these small sizes. -// The goal of the cache is to keep values that we need and +// The goal of the cache is to keep values that we need and #pragma once #include @@ -61,7 +61,6 @@ namespace xstudio { namespace utility { template class TimeCache { public: - struct CacheEntry { CacheEntry() = default; CacheEntry(const V _v) : value(_v) {} @@ -71,7 +70,7 @@ namespace utility { }; typedef std::shared_ptr CacheEntryPtr; - using cache_type = std::map; + using cache_type = std::map; cache_type cache_; TimeCache( @@ -453,8 +452,8 @@ namespace utility { // The goal here is to find the cache entry that has the oldest timepoints // in the whole set, and remove it. Remember, timepoint is the estimated // time when the given data entry will be needed. If the timepoints tell us - // it was needed in the past, then we can safely remove. If it's in the - // future, and inside 'newtp' then we can't get rid of it as we will need + // it was needed in the past, then we can safely remove. If it's in the + // future, and inside 'newtp' then we can't get rid of it as we will need // it soon so we have a release failure. // return empty buffer on fail / empty cache. template @@ -469,7 +468,7 @@ namespace utility { long int max_offset(0); typename cache_type::iterator it = cache_.end(); - typename cache_type::iterator i = cache_.begin(); + typename cache_type::iterator i = cache_.begin(); // set to our proposed time, if we're bigger than every chache entry we fail. if (not force_eviction) @@ -479,29 +478,29 @@ namespace utility { // loop over every cache entry while (i != cache_.end()) { - const std::set & timepoints = i->second->timepoints; + const std::set &timepoints = i->second->timepoints; // timepoints is an ordered set, so the last entry is the timepoint // furthest forward in time. How does this timepoint (which represents // the furthest point in the future that we would need this Value) // compare to 'max_offset' ? if (timepoints.size()) { - long int offset = std::abs( - std::chrono::duration_cast(ntp - *(timepoints.rbegin())).count()); + long int offset = + std::abs(std::chrono::duration_cast( + ntp - *(timepoints.rbegin())) + .count()); if (offset >= max_offset) { max_offset = offset; - it = i; + it = i; } } i++; - } if (it != cache_.end()) { ptr = it->second->value; erase(it); } - return ptr; - + return ptr; } // release the oldest item that is older than out_of_date_time @@ -513,29 +512,29 @@ namespace utility { return ptr; typename cache_type::iterator it = cache_.end(); - typename cache_type::iterator i = cache_.begin(); + typename cache_type::iterator i = cache_.begin(); long int maxx = 0; // loop over every cache entry while (i != cache_.end()) { - const std::set & timepoints = i->second->timepoints; + const std::set &timepoints = i->second->timepoints; // timepoints is an ordered set, so the last entry is the timepoint // furthest forward in time. How does this timepoint (which represents // the furthest point in the future that we would need this Value) // compare to 'max_offset' ? if (timepoints.size()) { - long int offset = std::abs( - std::chrono::duration_cast(out_of_date_time - *(timepoints.rbegin())).count()); + long int offset = std::chrono::duration_cast( + out_of_date_time - *(timepoints.rbegin())) + .count(); if (offset >= maxx) { maxx = offset; - it = i; + it = i; } } ++i; - } // valid key ? if (it != cache_.end()) { @@ -594,8 +593,10 @@ namespace utility { } avg_sum += sum; avg_ct += 1024; - std::cerr << this << " Cache Size: " << cache_.size() << " Min retrieve: " << _min << ", Max retrieve: " << _max << ", Average: " << sum/1024 << " (mean), " << retrieve_times[512] << " (median), " << " worst 100: " << last_ten/100 << " Running average " << avg_sum/avg_ct << "\n"; - retrieve_times.clear(); + std::cerr << this << " Cache Size: " << cache_.size() << " Min retrieve: " << + _min << ", Max retrieve: " << _max << ", Average: " << sum/1024 << " (mean), " << + retrieve_times[512] << " (median), " << " worst 100: " << last_ten/100 << " Running + average " << avg_sum/avg_ct << "\n"; retrieve_times.clear(); } }*/ @@ -664,11 +665,13 @@ namespace utility { template void TimeCache::clean_timepoints(const K &key) { auto it = cache_.find(key); - if (it == cache_.end()) return; + if (it == cache_.end()) + return; - std::set & timepoints = it->second->timepoints; + std::set &timepoints = it->second->timepoints; - if (timepoints.empty()) return; + if (timepoints.empty()) + return; const time_point now = utility::clock::now(); @@ -678,12 +681,12 @@ namespace utility { // erasing all entries older than 'now' while ((*timepoints.begin()) < now) { timepoints.erase(timepoints.begin()); - if (timepoints.empty()) break; + if (timepoints.empty()) + break; } if (timepoints.empty()) timepoints.emplace(newest); - } } // namespace utility } // namespace xstudio diff --git a/include/xstudio/utility/types.hpp b/include/xstudio/utility/types.hpp index 3a3ede48f..11672bfdf 100644 --- a/include/xstudio/utility/types.hpp +++ b/include/xstudio/utility/types.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include +#include + namespace xstudio { namespace utility { @@ -22,7 +25,26 @@ namespace utility { float r = {0.0f}; // NOLINT float g = {0.0f}; // NOLINT float b = {0.0f}; // NOLINT + + float red() const { return r; } + float green() const { return g; } + float blue() const { return b; } + + void setRed(const float _r) { r = _r; } + void setGreen(const float _g) { g = _g; } + void setBlue(const float _b) { b = _b; } + + friend std::string to_string(const ColourTriplet &value); + + template friend bool inspect(Inspector &f, ColourTriplet &x) { + return f.object(x).fields(f.field("r", x.r), f.field("g", x.g), f.field("b", x.b)); + } }; + inline std::string to_string(const ColourTriplet &c) { + return fmt::format("ColourTriplet({}, {}, {})", c.r, c.g, c.b); + } + + } // namespace utility } // namespace xstudio \ No newline at end of file diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 8227751e7..36b9eff19 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -23,7 +23,6 @@ file(GLOB DEPS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/connection/*.py ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/gui/*.py ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/plugin/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/sync_api/*.py ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/core/*.py ) diff --git a/python/src/xstudio/api/__init__.py b/python/src/xstudio/api/__init__.py index 346438288..b11aa78e3 100644 --- a/python/src/xstudio/api/__init__.py +++ b/python/src/xstudio/api/__init__.py @@ -222,7 +222,6 @@ def status(self): else: # connection stats stat["connection"]["connected"] = True - stat["connection"]["type"] = self.connection.api_type stat["connection"]["version"] = self.connection.app_version stat["connection"]["application"] = self.connection.app_type stat["connection"]["host"] = self.connection.host diff --git a/python/src/xstudio/api/auxiliary/__init__.py b/python/src/xstudio/api/auxiliary/__init__.py index e47f878ab..940e052b5 100644 --- a/python/src/xstudio/api/auxiliary/__init__.py +++ b/python/src/xstudio/api/auxiliary/__init__.py @@ -1,2 +1,3 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.api.auxiliary.helpers import ActorConnection \ No newline at end of file +from xstudio.api.auxiliary.helpers import ActorConnection +from xstudio.api.auxiliary.notification import Notification, NotificationHandler \ No newline at end of file diff --git a/python/src/xstudio/api/auxiliary/notification.py b/python/src/xstudio/api/auxiliary/notification.py new file mode 100644 index 000000000..8ac64bb6d --- /dev/null +++ b/python/src/xstudio/api/auxiliary/notification.py @@ -0,0 +1,205 @@ +# SPDX-License-Identifier: Apache-2.0 +from xstudio.core import notification_atom, Notification +import json + +class NotificationPy(): + """Wrapper for Notifaction""" + def __init__(self, actorconnection, notification): + """Create NotificationHandler object. + + Args: + actorconnection(ActorConnection): Connection object + + """ + + self._actor_connection = actorconnection + self._notification = notification + + @property + def type(self): + return self._notification.type() + + @type.setter + def type(self, value): + self._notification.set_type(ntype) + + @property + def text(self): + return self._notification.text() + + @text.setter + def text(self, value): + self._notification.set_text(value) + + @property + def uuid(self): + return self._notification.uuid() + + @uuid.setter + def uuid(self, value): + self._notification.set_uuid(value) + + @property + def progress(self): + return self._notification.progress() + + @progress.setter + def progress(self, value): + self._notification.set_progress(value) + + @property + def progress_maximum(self): + return self._notification.progress_maximum() + + @progress_maximum.setter + def progress_maximum(self, value): + self._notification.set_progress_maximum(value) + + @property + def progress_minimum(self): + return self._notification.progress_minimum() + + @progress_minimum.setter + def progress_minimum(self, value): + self._notification.set_progress_minimum(value) + + @property + def progress_percentage(self): + return self._notification.progress_percentage() + + @property + def progress_text_percentage(self): + return self._notification.progress_text_percentage() + + @property + def progress_text_range(self): + return self._notification.progress_text_range() + + def remove(self): + """Remove Notification""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self.uuid)[0] + + def push_update(self): + """Push changes""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self._notification)[0] + + def push_progress(self, progress): + """Push changes""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self.uuid, progress)[0] + + +class NotificationHandler(): + """Access and create notification events""" + def __init__(self, actorconnection): + """Create NotificationHandler object. + + Args: + actorconnection(ActorConnection): Connection object + + """ + + self._actor_connection = actorconnection + + @property + def notification_digest(self): + return json.loads(self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom())[0].dump()) + + @property + def notifications(self): + result = [] + # turn into array of NotificationPy + + tmp = self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), notification_atom())[0] + for i in tmp: + result.append(NotificationPy(self._actor_connection, i)) + + return result + + def createInfoNotification(self, text, expiresSeconds=10): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.InfoNotification(text, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createWarnNotification(self, text, expiresSeconds=10): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.WarnNotification(text, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProcessingNotification(self, text): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProcessingNotification(text) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProgressPercentageNotification(self, text, progress=0.0, expiresSeconds=600): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProgressPercentageNotification(text, progress, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProgressRangeNotification(self, text, progress=0.0, progress_min=0.0, progress_max=0.0, expiresSeconds=600): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProgressRangeNotification(text, progress, progress_min, progress_max, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + + + + + + + + + + + + + diff --git a/python/src/xstudio/api/module.py b/python/src/xstudio/api/module.py index 057ce92c6..8faa1022d 100644 --- a/python/src/xstudio/api/module.py +++ b/python/src/xstudio/api/module.py @@ -10,6 +10,7 @@ from xstudio.core import event_atom, show_atom, module_add_menu_item_atom from xstudio.core import module_remove_menu_item_atom, uuid_atom from xstudio.core import menu_node_activated_atom, set_node_data_atom +from xstudio.core import ColourTriplet from xstudio.api.auxiliary import ActorConnection from xstudio.core import JsonStore, Uuid from xstudio.api.auxiliary.helpers import get_event_group @@ -50,6 +51,12 @@ def __init__( if uuid: self.uuid = uuid else: + # as yet, I haven't worked out how to allow type conversion from + # custom python type (like ColourTriplet) and JsonStore !! + if isinstance(attribute_value, ColourTriplet): + # backend knows to interpret this back into a ColourTriplet + attribute_value = ["colour", 1, attribute_value.red, attribute_value.green, attribute_value.blue] + self.uuid = connection.request_receive( parent_remote, add_attribute_atom(), @@ -181,6 +188,7 @@ def __init__( self.menu_trigger_callbacks = {} self.menu_item_ids = [] self.hotkey_callbacks = {} + self._uuid = None for attr_uuid in attr_uuids: attr_wrapper = ModuleAttribute( @@ -203,12 +211,7 @@ def __init__( ) self.__attribute_changed = None - self.__playhead_event_callback = None - - def get_uuid(self): - return self.connection.request_receive( - self.remote, - uuid_atom())[0] + self.__playhead_event_callback = None def connect_to_ui(self): """Call this method to 'activate' the plugin and forward live data about @@ -656,4 +659,17 @@ def incoming_msg(self, *args): self.connection.caf_message_to_tuple(args[0][0]) ) except Exception as err: - pass \ No newline at end of file + pass + + @property + def uuid(self): + """Get Module uuid + + Returns: + uuid(Uuid): Uuid of actor container. + """ + if not self._uuid: + self._uuid = self.connection.request_receive( + self.remote, + uuid_atom())[0] + return self._uuid diff --git a/python/src/xstudio/api/session/container.py b/python/src/xstudio/api/session/container.py index 236adf501..3ea904cbf 100644 --- a/python/src/xstudio/api/session/container.py +++ b/python/src/xstudio/api/session/container.py @@ -122,7 +122,6 @@ def children(self): return self._children - class Container(ActorConnection): def __init__(self, connection, remote, uuid=None): """Create Container object. @@ -157,7 +156,7 @@ def add_event_callback_function(self, callback_function): (self.group, callback_function, self.remote) ) else: - self.connection.link.run_callback_with_delay( + self.connection.link.add_message_callback( (self.group, callback_function, self.remote) ) @@ -176,7 +175,7 @@ def remove_event_callback_function(self, callback_function): (self.group, callback_function) ) else: - self.connection.remove_message_callback( + self.connection.link.remove_message_callback( (self.group, callback_function) ) diff --git a/python/src/xstudio/api/session/playlist/playlist.py b/python/src/xstudio/api/session/playlist/playlist.py index cbbfc031b..49b9afc14 100644 --- a/python/src/xstudio/api/session/playlist/playlist.py +++ b/python/src/xstudio/api/session/playlist/playlist.py @@ -17,10 +17,11 @@ from xstudio.api.session.playlist.subset import Subset from xstudio.api.session.playlist.contact_sheet import ContactSheet from xstudio.api.session.playlist.timeline import Timeline +from xstudio.api.auxiliary import NotificationHandler import json -class Playlist(Container): +class Playlist(Container, NotificationHandler): """Playlist object.""" def __init__(self, connection, remote, uuid=None): @@ -36,6 +37,7 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) @property def playhead(self): diff --git a/python/src/xstudio/api/session/playlist/subset.py b/python/src/xstudio/api/session/playlist/subset.py index a305eff19..d438e6bde 100644 --- a/python/src/xstudio/api/session/playlist/subset.py +++ b/python/src/xstudio/api/session/playlist/subset.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import Uuid, add_media_atom, actor, selection_actor_atom +from xstudio.core import Uuid, add_media_atom, actor, selection_actor_atom, get_media_atom from xstudio.api.session.container import Container from xstudio.api.session.media.media import Media from xstudio.api.session.playhead import Playhead, PlayheadSelection @@ -44,6 +44,17 @@ def add_media(self, media, before_uuid=Uuid()): return Media(self.connection, result.actor, result.uuid) + @property + def media(self): + """Get media in subset + + Returns: + media(list[media]): Media + """ + result = self.connection.request_receive(self.remote, get_media_atom())[0] + return [Media(self.connection, i.actor, i.uuid) for i in result] + + @property def playhead_selection(self): """The actor that filters a selection of media from a playhead diff --git a/python/src/xstudio/api/session/playlist/timeline/__init__.py b/python/src/xstudio/api/session/playlist/timeline/__init__.py index 9a41ff9cc..a71517160 100644 --- a/python/src/xstudio/api/session/playlist/timeline/__init__.py +++ b/python/src/xstudio/api/session/playlist/timeline/__init__.py @@ -1,13 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import UuidActor, Uuid, actor, item_atom, MediaType, ItemType, enable_atom, item_flag_atom from xstudio.core import active_range_atom, available_range_atom, undo_atom, redo_atom, history_atom, add_media_atom, item_name_atom -from xstudio.core import URI, selection_actor_atom -from xstudio.core import import_atom, erase_item_atom, get_playhead_atom +from xstudio.core import URI, selection_actor_atom, item_selection_atom, item_type_atom, get_media_atom, save_atom, export_atom +from xstudio.core import import_atom, erase_item_atom, get_playhead_atom, FrameRate, FrameRateDuration from xstudio.api.session.container import Container from xstudio.api.intrinsic import History from xstudio.api.session.media.media import Media from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.playhead import Playhead, PlayheadSelection +from xstudio.api.auxiliary import NotificationHandler def create_gap(connection, name="Gap"): """Create Gap object. @@ -22,7 +23,7 @@ def create_gap(connection, name="Gap"): gap(Gap): Gap object. """ - return Gap(connection, connection.remote_spawn("Gap",name)) + return Gap(connection, connection.remote_spawn("Gap", name, FrameRateDuration())) def create_clip(connection, media, name="Clip"): """Create Clip object. @@ -68,7 +69,7 @@ def create_video_track(connection, name="Video Track"): track(Track): Track object. """ - return Track(ItemType.IT_VIDEO_TRACK, connection, connection.remote_spawn("Track", name, MediaType.MT_IMAGE)) + return Track(ItemType.IT_VIDEO_TRACK, connection, connection.remote_spawn("Track", name, FrameRate(), MediaType.MT_IMAGE)) def create_audio_track(connection, name="Audio Track"): """Create Track object. @@ -83,29 +84,32 @@ def create_audio_track(connection, name="Audio Track"): track(Track): Track object. """ - return Track(ItemType.IT_AUDIO_TRACK, connection, connection.remote_spawn("Track", name, MediaType.MT_AUDIO)) + return Track(ItemType.IT_AUDIO_TRACK, connection, connection.remote_spawn("Track", name, FrameRate(), MediaType.MT_AUDIO)) def create_item_container(connection, item): + return create_item_container_from_type(connection, item.item_type(), item.uuid(), item.actor()) + +def create_item_container_from_type(connection, item_type, uuid, actor): result = None - if item.item_type() == ItemType.IT_GAP: - result = Gap(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_CLIP: - result = Clip(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_STACK: - result = Stack(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_VIDEO_TRACK: - result = Track(ItemType.IT_VIDEO_TRACK, connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_AUDIO_TRACK: - result = Track(ItemType.IT_AUDIO_TRACK, connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_TIMELINE: - result = Timeline(connection, item.actor(), item.uuid()) + if item_type == ItemType.IT_GAP: + result = Gap(connection, actor, uuid) + elif item_type == ItemType.IT_CLIP: + result = Clip(connection, actor, uuid) + elif item_type == ItemType.IT_STACK: + result = Stack(connection, actor, uuid) + elif item_type == ItemType.IT_VIDEO_TRACK: + result = Track(ItemType.IT_VIDEO_TRACK, connection, actor, uuid) + elif item_type == ItemType.IT_AUDIO_TRACK: + result = Track(ItemType.IT_AUDIO_TRACK, connection, actor, uuid) + elif item_type == ItemType.IT_TIMELINE: + result = Timeline(connection, actor, uuid) else: - raise RuntimeError("Invalid type " + i.item_type()) + raise RuntimeError("Invalid type " + item_type) return result -class Timeline(Item): +class Timeline(Item, NotificationHandler): """Timeline object.""" def __init__(self, connection, remote, uuid=None): @@ -121,6 +125,7 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Item.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) def __len__(self): """Get size. @@ -141,6 +146,25 @@ def stack(self): item = self.connection.request_receive(self.remote, item_atom(), 0)[0] return create_item_container(self.connection, item) + @property + def selection(self): + """Get currently selected items + + Returns: + list([item]): Selected Items + """ + + items = self.connection.request_receive(self.remote, item_selection_atom())[0] + result = [] + + for i in items: + item_type = self.connection.request_receive(i.actor, item_type_atom())[0] + result.append( + create_item_container_from_type(self.connection, item_type, i.uuid, i.actor) + ) + + return result + @property def children(self): """Get children. @@ -271,6 +295,37 @@ def create_video_track(self, name="Video Track"): """ return create_video_track(self.connection, name) + def insert_video_track(self, name="Video Track", index=0): + """Insert video track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_VIDEO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_IMAGE)) + self.stack.insert_child(result, index) + return result + + def insert_audio_track(self, name="Audio Track", index=-1): + """Insert audio track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_AUDIO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_AUDIO)) + self.stack.insert_child(result, index) + return result + + def export_timeline_to_file(self, path, adapter_name=None): """Create otio from timeline. @@ -326,6 +381,41 @@ def load_otio(self, otio_body, path="", clear=False): otio_body, clear)[0] + def export_otio(self, path): + """Export timeline via OpenTimelineIO. File path extension infers the + format of the exported file. + + Args: + path(str/uri): Path to expor to. + + Returns: + bool: True on success, False on failure + """ + otio_string = self.connection.request_receive(self.remote, export_atom())[0] + + from opentimelineio.adapters import read_from_string, write_to_file + otio_object = read_from_string(otio_string) + return write_to_file(otio_object, path) + + @property + def audio_tracks(self): + return self.stack.audio_tracks + + @property + def video_tracks(self): + return self.stack.video_tracks + + @property + def media(self): + """Get media in timeline + + Returns: + media(list[media]): Media + """ + result = self.connection.request_receive(self.remote, get_media_atom())[0] + return [Media(self.connection, i.actor, i.uuid) for i in result] + + @property def playhead(self): """Get playhead. diff --git a/python/src/xstudio/api/session/playlist/timeline/item.py b/python/src/xstudio/api/session/playlist/timeline/item.py index 718b9ffa3..2868f4211 100644 --- a/python/src/xstudio/api/session/playlist/timeline/item.py +++ b/python/src/xstudio/api/session/playlist/timeline/item.py @@ -163,6 +163,10 @@ def children(self): def trimmed_range(self): return self.item.trimmed_range() + @property + def rate(self): + return self.item.trimmed_range().rate() + @property def available_range(self): return self.item.available_range() diff --git a/python/src/xstudio/api/session/playlist/timeline/stack.py b/python/src/xstudio/api/session/playlist/timeline/stack.py index 572000154..faaeb7069 100644 --- a/python/src/xstudio/api/session/playlist/timeline/stack.py +++ b/python/src/xstudio/api/session/playlist/timeline/stack.py @@ -1,9 +1,10 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import insert_item_atom, remove_item_atom, erase_item_atom, move_item_atom -from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec +from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec, MediaType from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.playlist.timeline import create_item_container +from xstudio.api.session.playlist.timeline.track import Track class Stack(Item): """Timeline object.""" @@ -30,6 +31,48 @@ def __len__(self): """ return len(self.item) + @property + def tracks(self): + return self.children + + @property + def audio_tracks(self): + return self.children_of_type([ItemType.IT_AUDIO_TRACK]) + + @property + def video_tracks(self): + return self.children_of_type([ItemType.IT_VIDEO_TRACK]) + + def insert_video_track(self, name="Video Track", index=0): + """Insert video track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_VIDEO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_IMAGE)) + self.insert_child(result, index) + return result + + def insert_audio_track(self, name="Audio Track", index=-1): + """Insert audio track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_AUDIO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_AUDIO)) + self.insert_child(result, index) + return result + def insert_child(self, obj, index=-1): """Insert child obj @@ -49,8 +92,8 @@ def insert_child(self, obj, index=-1): return self.connection.request_receive(self.remote, insert_item_atom(), index, uav)[0] - def remove_child(self, index=-1): - """Remove child obj + def remove_child_at_index(self, index=-1): + """Remove child obj, removed item must be released or reparented. Args: index(int): Index to remove @@ -60,7 +103,7 @@ def remove_child(self, index=-1): """ return self.connection.request_receive(self.remote, remove_item_atom(), index)[0] - def erase_child(self, index=-1): + def erase_child_at_index(self, index=-1): """Remove and destroy child obj Args: @@ -71,6 +114,28 @@ def erase_child(self, index=-1): """ return self.connection.request_receive(self.remote, erase_item_atom(), index, True)[0] + def remove_child(self, child): + """Remove child obj, removed item must be released or reparented. + + Args: + child(Item): Child to remove + + Returns: + item(Item): Item removed + """ + return self.connection.request_receive(self.remote, remove_item_atom(), child.uuid)[0] + + def erase_child(self, child): + """Remove and destroy child obj + + Args: + child(Item): Child to erase + + Returns: + success(bool): Item erased + """ + return self.connection.request_receive(self.remote, erase_item_atom(), child.uuid, True)[0] + def move_children(self, start, count, dest): """Move child items diff --git a/python/src/xstudio/api/session/playlist/timeline/track.py b/python/src/xstudio/api/session/playlist/timeline/track.py index c6f98db40..edce87648 100644 --- a/python/src/xstudio/api/session/playlist/timeline/track.py +++ b/python/src/xstudio/api/session/playlist/timeline/track.py @@ -1,10 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import insert_item_atom, remove_item_atom, erase_item_atom, move_item_atom -from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec +from xstudio.core import split_item_at_frame_atom, split_item_atom, erase_item_at_frame_atom +from xstudio.core import move_item_at_frame_atom +from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec, FrameRateDuration from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.playlist.timeline import create_item_container +from xstudio.api.session.playlist.timeline.gap import Gap +from xstudio.api.session.playlist.timeline.clip import Clip +from xstudio.api.auxiliary import NotificationHandler -class Track(Item): +class Track(Item, NotificationHandler): """Timeline object.""" def __init__(self, item_type, connection, remote, uuid=None): @@ -20,6 +25,7 @@ def __init__(self, item_type, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Item.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) self.item_type = item_type @property @@ -40,6 +46,14 @@ def is_video(self): """ return self.item_type == ItemType.IT_VIDEO_TRACK + @property + def clips(self): + return self.children_of_type([ItemType.IT_CLIP]) + + @property + def gaps(self): + return self.children_of_type([ItemType.IT_GAP]) + def __len__(self): """Get size. @@ -48,6 +62,38 @@ def __len__(self): """ return len(self.item) + def insert_gap(self, frames, index=-1): + """Insert gap + Args: + frames(int): Duration in frames. + index(int): Position to insert + + returns success(Item): New item + """ + result = Gap(self.connection, + self.connection.remote_spawn("Gap", "Gap", FrameRateDuration(frames, self.rate)) + ) + + self.insert_child(result, index) + + return result + + def insert_clip(self, media, index=-1): + """Insert gap + Args: + media(Media): Media to insert, MUST already exist in timeline. + index(int): Position to insert + + returns success(Item): New item + """ + result = Clip(self.connection, + self.connection.remote_spawn("Clip", media.uuid_actor(), "") + ) + + self.insert_child(result, index) + + return result + def insert_child(self, obj, index=-1): """Insert child obj @@ -67,8 +113,8 @@ def insert_child(self, obj, index=-1): return self.connection.request_receive(self.remote, insert_item_atom(), index, uav)[0] - def remove_child(self, index=-1): - """Remove child obj + def remove_child_at_index(self, index=-1, count=1, add_gap=True): + """Remove child obj, removed item must be released or reparented. Args: index(int): Index to remove @@ -76,9 +122,22 @@ def remove_child(self, index=-1): Returns: item(Item): Item removed """ - return self.connection.request_receive(self.remote, remove_item_atom(), index)[0] + return self.connection.request_receive(self.remote, remove_item_atom(), index, count, add_gap)[0] + + + def erase_frames(self, frame=0, duration=1, add_gap=True): + """Remove and destroy frames + Args: + frame(int): start frame + duration(int): Duration frames + add_gap(bool): Replace with gap + + Returns: + Transaction(JsonStore): Transaction. + """ + return self.connection.request_receive(self.remote, erase_item_at_frame_atom(), frame, duration, add_gap)[0] - def erase_child(self, index=-1): + def erase_child_at_index(self, index=-1, count=1, add_gap=True): """Remove and destroy child obj Args: @@ -87,9 +146,31 @@ def erase_child(self, index=-1): Returns: success(bool): Item erased """ - return self.connection.request_receive(self.remote, erase_item_atom(), index)[0] + return self.connection.request_receive(self.remote, erase_item_atom(), index, count, add_gap)[0] + + def remove_child(self, child, add_gap=True): + """Remove child obj, removed item must be released or reparented. + + Args: + child(Item): Child to remove + + Returns: + item(Item): Child removed + """ + return self.connection.request_receive(self.remote, remove_item_atom(), child.uuid, add_gap)[0] + + def erase_child(self, child, add_gap=True): + """Remove and destroy child obj + + Args: + child(Item): Child to erase + + Returns: + success(bool): Item erased + """ + return self.connection.request_receive(self.remote, erase_item_atom(), child.uuid, add_gap)[0] - def move_children(self, start, count, dest): + def move_children(self, start, count, dest, add_gap=False): """Move child items Args: @@ -100,7 +181,58 @@ def move_children(self, start, count, dest): Returns: success(bool): Items moved """ - return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest)[0] + return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest, add_gap)[0] + + def move_frames(self, frame, duration, destination, insert=True, add_gap=False): + """Move frames + + Args: + frame(int): First frame + duration(int): Duration in frames + destination(int): Destination frame + insert(bool): Insert at destination frame, overwrite if False + add_gap(bool): Add gap at source + + Returns: + success(bool): Items moved + """ + return self.connection.request_receive(self.remote, move_item_at_frame_atom(), frame, duration, destination, insert, add_gap)[0] + + + def split_child_at_index(self, index, frame): + """Split child obj + + Args: + index(int): Index to split + frame(int): Frame of child to split + + Returns: + success(bool): Item split + """ + return self.connection.request_receive(self.remote, split_item_atom(), index, frame)[0] + + def split_child(self, child, frame): + """Split child obj + + Args: + child(Item): Child to split + frame(int): Frame of child to split + + Returns: + success(bool): Item split + """ + return self.connection.request_receive(self.remote, split_item_atom(), child.uuid, frame)[0] + + def split(self, frame): + """Split track item at frame + + Args: + frame(int): Frame of track to split + + Returns: + success(bool): Item split + """ + return self.connection.request_receive(self.remote, split_item_at_frame_atom(), frame)[0] def children_of_type(self, types): """Get children matching types. diff --git a/python/src/xstudio/api/session/session.py b/python/src/xstudio/api/session/session.py index a070b4e3b..b999179fa 100644 --- a/python/src/xstudio/api/session/session.py +++ b/python/src/xstudio/api/session/session.py @@ -5,7 +5,7 @@ from xstudio.core import reflag_container_atom, merge_playlist_atom, copy_container_to_atom from xstudio.core import get_bookmark_atom, save_atom, active_media_container_atom, current_media_atom, name_atom from xstudio.core import viewport_active_media_container_atom -from xstudio.core import URI, Uuid, UuidVec +from xstudio.core import URI, Uuid, UuidVec, item_selection_atom, type_atom from xstudio.api.session.container import Container, PlaylistTree, PlaylistItem from xstudio.api.session.playlist import Playlist @@ -14,9 +14,10 @@ from xstudio.api.session.playlist.subset import Subset from xstudio.api.session.playlist.contact_sheet import ContactSheet from xstudio.api.session.playlist.timeline import Timeline +from xstudio.api.auxiliary import NotificationHandler -class Session(Container): +class Session(Container, NotificationHandler): """Session object.""" def __init__(self, connection, remote, uuid=None): @@ -32,6 +33,7 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) @property def selected_media(self): @@ -48,6 +50,39 @@ def selected_media(self): return media + @property + def selected_containers(self): + """Get currently selected containers. + + Returns: + container(Playlist,Subset,Timelime,ContactSheet): Container. + """ + + items = self.connection.request_receive(self.remote, item_selection_atom())[0] + result = [] + + for i in items: + item_type = self.connection.request_receive(i.actor, type_atom())[0] + if item_type == "Timeline": + result.append( + Timeline(self.connection, i.actor, i.uuid) + ) + elif item_type == "Subset": + result.append( + Subset(self.connection, i.actor, i.uuid) + ) + elif item_type == "ContactSheet": + result.append( + ContactSheet(self.connection, i.actor, i.uuid) + ) + elif item_type == "Playlist": + result.append( + Playlist(self.connection, i.actor, i.uuid) + ) + + + return result + @property def viewed_playlist(self): """Get currently viewed (Playlist,Subset,Timelime,ContactSheet). diff --git a/python/src/xstudio/cli/control.py b/python/src/xstudio/cli/control.py index c93767e51..f3ee81fbb 100644 --- a/python/src/xstudio/cli/control.py +++ b/python/src/xstudio/cli/control.py @@ -38,12 +38,6 @@ def control_main(): default=45500 ) - parser.add_argument( - "-r", "--require-sync", action="store_true", - help="Downgrade FULL to SYNC.", - default=False - ) - parser.add_argument( "-i", "--info", action="store_true", help="Print connection info.", @@ -67,19 +61,19 @@ def control_main(): m = RemoteSessionManager(remote_session_path()) s = m.find(args.session) conn = Connection(debug=args.debug) - conn.connect_remote(s.host(), s.port.port(), args.require_sync) + conn.connect_remote(s.host(), s.port.port()) elif args.host: conn = Connection(debug=args.debug) - conn.connect_remote(args.host, args.port, args.require_sync) + conn.connect_remote(args.host, args.port) else: conn = Connection(debug=args.debug) - conn.connect_remote("localhost", args.port, args.require_sync) + conn.connect_remote("localhost", args.port) if args.info: print( - " App: {}\n API: {}\nVersion: {}\nSession: {}\n".format( - conn.app_type, conn.api_type, + " App: {}\n Version: {}\nSession: {}\n".format( + conn.app_type, conn.app_version, conn.api.app.session.name ) ) diff --git a/python/src/xstudio/cli/inject.py b/python/src/xstudio/cli/inject.py index 149f3fa35..c2d4870f5 100644 --- a/python/src/xstudio/cli/inject.py +++ b/python/src/xstudio/cli/inject.py @@ -50,14 +50,14 @@ def inject_main(): m = RemoteSessionManager(remote_session_path()) s = m.find(args.session) conn = Connection() - conn.connect_remote(s.host(), s.port.port(), False) + conn.connect_remote(s.host(), s.port.port()) elif args.host: conn = Connection() - conn.connect_remote(args.host, args.port, False) + conn.connect_remote(args.host, args.port) else: conn = Connection() - conn.connect_remote("localhost", args.port, False) + conn.connect_remote("localhost", args.port) pl = conn.api.app.session.create_playlist("Injected Media")[1] diff --git a/python/src/xstudio/cli/xstudiopy_startup.py b/python/src/xstudio/cli/xstudiopy_startup.py index 14abef9d0..311288301 100644 --- a/python/src/xstudio/cli/xstudiopy_startup.py +++ b/python/src/xstudio/cli/xstudiopy_startup.py @@ -8,10 +8,6 @@ __DEBUG = False -__SYNC = os.environ.get("XSTUDIOPY_SYNC","0").lower() in [ - 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh' - ] - if os.environ.get("XSTUDIOPY_DEBUG", "False") == "True": __DEBUG = True @@ -21,8 +17,7 @@ ) XSTUDIO.connect_remote( os.environ.get("XSTUDIOPY_HOST", "localhost"), - int(os.environ.get("XSTUDIOPY_PORT", "45500")), - __SYNC + int(os.environ.get("XSTUDIOPY_PORT", "45500")) ) elif os.environ.get("XSTUDIOPY_SESSION", None): m = RemoteSessionManager(remote_session_path()) @@ -34,17 +29,13 @@ if s is not None: XSTUDIO.connect_remote( s.host(), - s.port(), - __SYNC + s.port() ) else: print("Remote session not found") else: m = RemoteSessionManager(remote_session_path()) - if __SYNC: - s = m.first_sync() - else: - s = m.first_api() + s = m.first_api() XSTUDIO = Connection( debug=__DEBUG @@ -52,8 +43,7 @@ if s is not None: XSTUDIO.connect_remote( s.host(), - s.port(), - __SYNC + s.port() ) else: print("No remote session found") diff --git a/python/src/xstudio/connection/__init__.py b/python/src/xstudio/connection/__init__.py index 2b9ceff70..e0cb44acb 100644 --- a/python/src/xstudio/connection/__init__.py +++ b/python/src/xstudio/connection/__init__.py @@ -4,14 +4,13 @@ Controls xStudio connections. """ -from xstudio.core import get_api_mode_atom, absolute_receive_timeout -from xstudio.core import request_connection_atom, get_sync_atom, version_atom -from xstudio.core import get_application_mode_atom, authorise_connection_atom, broadcast_down_atom +from xstudio.core import absolute_receive_timeout +from xstudio.core import version_atom, authenticate_atom +from xstudio.core import get_application_mode_atom, broadcast_down_atom from xstudio.core import join_broadcast_atom, exit_atom, api_exit_atom, leave_broadcast_atom, get_event_group_atom from xstudio.core import error, Link from xstudio.core import XSTUDIO_LOCAL_PLUGIN_PATH from xstudio.api import API -from xstudio.sync_api import SyncAPI from xstudio.core import RemoteSessionManager, remote_session_path import uuid import time @@ -21,9 +20,6 @@ class Connection(object): """Handle connection to xstudio.""" - API_TYPE_FULL = "FULL" - API_TYPE_SYNC = "SYNC" - API_TYPE_GATEWAY = "GATEWAY" APP_TYPE_XSTUDIO = "XSTUDIO" APP_TYPE_XSTUDIO_GUI = "XSTUDIO_GUI" @@ -37,7 +33,6 @@ def __init__(self, auto_connect=False, background_processing=False, debug=False, """ self.link = Link() self.connected = False - self.api_type = None self.app_type = None self.app_version = None self._api = None @@ -244,7 +239,7 @@ def handle_broadcast(self, event): pass # print("ignored broadcast", sender, req_id, event[:len(event)-2]) - def get_key_from_stdin(self, lock): + def get_authentication_from_stdin(self): """Request lock key from user. Args: @@ -253,7 +248,7 @@ def get_key_from_stdin(self, lock): Returns: user_input(str): String entered by user. """ - return input("Enter key for lock {}: ".format(lock)) + return input("Enter script key or userpassword for remote session {}:{}: ".format(self.link.host, self.link.port)) def connect_local(self, actor): """Connect to in-process actor. @@ -268,25 +263,21 @@ def connect_local(self, actor): else: raise RuntimeError("Failed to connect") - def connect_remote_auto(self, session=None, sync_mode=False, sync_key_callback=None): + def connect_remote_auto(self, session=None, authentication_callback=None): """Connect to xStudio using session file. Kwargs: session (str): Session name. - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ r = RemoteSessionManager(remote_session_path()) if session is None: - if sync_mode: - s = r.first_sync() - else: - s = r.first_api() + s = r.first_api() else: s = m.find(session) - self.connect_remote(s.host(), s.port(), sync_mode, sync_key_callback) + self.connect_remote(s.host(), s.port(), authentication_callback) - def connect_remote(self, host, port, sync_mode=False, sync_key_callback=None): + def connect_remote(self, host, port, authentication_callback=None): """Connect to xStudio using host/port. Args: @@ -294,52 +285,43 @@ def connect_remote(self, host, port, sync_mode=False, sync_key_callback=None): port (int): Host port number. Kwargs: - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ - if sync_key_callback is None: - sync_key_callback = self.get_key_from_stdin + if authentication_callback is None: + authentication_callback = self.get_authentication_from_stdin self.disconnect() connected = self.link.connect_remote(host, port) if connected: - self.negotiate(sync_mode, sync_key_callback) + self.negotiate(authentication_callback) else: raise RuntimeError("Failed to connect") - def negotiate(self, sync_mode=False, sync_key_callback=None): - """Negotiate connection, also handles sync lock/key. + def negotiate(self, authentication_callback=None): + """Negotiate connection, also handles authentication. Kwargs: - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ + self.app_version = self.request_receive(self.link.remote(), version_atom())[0] + self.app_type = self.request_receive(self.link.remote(), get_application_mode_atom())[0] - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] - - if self.api_type == self.API_TYPE_GATEWAY: - if not sync_mode: - raise RuntimeError("Connection failed, this is the wrong port for FULL API") - - # need to authorise.. - lock = self.request_receive(self.link.remote(), request_connection_atom())[0] - remote = self.request_receive(self.link.remote(), - authorise_connection_atom(), - lock, - sync_key_callback(lock) - )[0] - - self.link.set_remote(remote) - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] - elif self.api_type == self.API_TYPE_FULL and sync_mode: - self.link.set_remote(self.request_receive(self.link.remote(), get_sync_atom())[0]) - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] + try: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom())[0]) + except: + if authentication_callback: + auth = authentication_callback().split() + if len(auth) == 2: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom(), auth[0], auth[1])[0]) + elif len(auth) == 1: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom(), auth[0])[0]) + else: + raise + else: + raise - self.app_version = self.request_receive(self.link.remote(), version_atom())[0] - self.app_type = self.request_receive(self.link.remote(), get_application_mode_atom())[0] - if self.api_type: - self.connected = True + self.connected = True # clear old handlers.. self.broadcast_channel = {} @@ -451,24 +433,6 @@ def send(self, *args): if self.connected: self.link.send(*args) - # def join(self, group): - # """Join message group - - # Args: - # group (group): Group to join. - # """ - # if self.api_type: - # self.link.join(group) - - # def leave(self, group): - # """Leave message group - - # Args: - # group (group): Group to leave. - # """ - # if self.api_type: - # self.link.leave(group) - def dequeue_messages(self, timeout=10): """Pop waiting messages""" while True: @@ -531,10 +495,6 @@ def _dequeue_messages(self, timeout_milli, watch_for = None): def disconnect(self): """Disconnect from xstudio""" - if self.api_type == "SYNC": - self.link.send_exit(self.link.remote()) - - self.api_type = None self.app_type = None self.app_version = None self._api = None @@ -547,13 +507,10 @@ def api(self): """Access API object. Returns: - API(API or SyncAPI): API object or None + API(API): API object or None """ if self._api is None: - if self.api_type == "SYNC": - self._api = SyncAPI(self) - elif self.api_type == "FULL": - self._api = API(self) + self._api = API(self) return self._api diff --git a/python/src/xstudio/plugin/plugin_base.py b/python/src/xstudio/plugin/plugin_base.py index 45af508c3..1b4612cc4 100644 --- a/python/src/xstudio/plugin/plugin_base.py +++ b/python/src/xstudio/plugin/plugin_base.py @@ -41,7 +41,7 @@ def __init__( name, JsonStore(), base_class_name) - self.uuid = a[1] + remote = a[0] ModuleBase.__init__(self, connection, remote) @@ -58,6 +58,12 @@ def __init__( super().set_attribute_changed_event_handler(self._PluginBase__attribute_changed) self.user_attr_handler_ = None + #if XStudioExtensions: + # XStudioExtensions.register_python_plugin_instance((self, self.uuid)) + #else: + # self.connection.link.register_python_plugin_instance((self, self.uuid)) + + def set_attribute_changed_event_handler(self, handler): # here we override the method on Module class to ensure that # when someone using this base class doesn't @@ -146,7 +152,7 @@ def popup_message_box( def create_qml_item( self, qml_item, - callback_fn + callback_fn=None ): """Create and show a qml item (typically a pop-up window, dialog etc.). Args: @@ -174,7 +180,8 @@ def create_qml_item( "enabled": False }) - self.qml_item_attrs[attr.uuid] = (attr, callback_fn) + if callback_fn: + self.qml_item_attrs[attr.uuid] = (attr, callback_fn) # 'attr_enabled' controls the visibility of the widget attr.set_role_data("attr_enabled", True) @@ -186,8 +193,25 @@ def _PluginBase__attribute_changed( # check if 'CallbackData' has been set - if the attribute is one of our # qml_item_attrs then we execute the associated callback - if role == AttributeRole.CallbackData and attribute.uuid in self.qml_item_attrs: + if role == AttributeRole.CallbackData and \ + attribute.uuid in self.qml_item_attrs and \ + attribute.role_data("callback_data") != {}: self.qml_item_attrs[attribute.uuid][1](attribute.role_data("callback_data")) + # now reset the callback data so if the callback is called again with + # the same data as before we still get an 'attribute_changed' signal + attribute.set_role_data("callback_data", {}) if self.user_attr_handler_: - self.user_attr_handler_(attribute) \ No newline at end of file + self.user_attr_handler_(attribute) + + def run_callback_func(self, cb_name, args): + + import json + the_callback = getattr(self, cb_name) + translated_args = json.loads(args.dump()) + if isinstance(translated_args, dict): + return JsonStore(the_callback(**translated_args)) + elif isinstance(translated_args, list): + return JsonStore(the_callback(*translated_args)) + else: + return JsonStore(the_callback(translated_args)) \ No newline at end of file diff --git a/python/src/xstudio/sync_api/__init__.py b/python/src/xstudio/sync_api/__init__.py deleted file mode 100644 index 9d2dc7ff5..000000000 --- a/python/src/xstudio/sync_api/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -from xstudio.common_api import CommonAPI - -class SyncAPI(CommonAPI): - """We use this as a base for the SYNC API connection.""" - - def __init__(self, connection): - """Hold connection handle. - - Args: - connection (object): Hold connection object. - - """ - super(SyncAPI, self).__init__(connection) - diff --git a/python/src/xstudiopy b/python/src/xstudiopy index 7099cd8a6..a9804c798 100755 --- a/python/src/xstudiopy +++ b/python/src/xstudiopy @@ -8,7 +8,6 @@ function help echo " -s SESSION, --session SESSION Session name (DEFAULT: '')" echo " -h HOST, --host HOST Host xstudio is running on (DEFAULT: '')" echo " -p, --port Port to connect to (DEFAULT: 45500)" - echo " --sync Force SYNC mode (DEFAULT: False)" echo " -e, --execute Execute command and exit" echo " --help show this help message and exit" echo " -d, --debug Print debug output" @@ -20,7 +19,6 @@ DEBUG=False SESSION= HOST= PORT=45500 -SYNC=False EXECUTE= SOURCE="${BASH_SOURCE[0]}" @@ -61,9 +59,6 @@ case $key in PORT="$2" shift ;; - --sync) - SYNC=True - ;; --help) help ;; @@ -89,7 +84,6 @@ export XSTUDIOPY_DEBUG=$DEBUG export XSTUDIOPY_SESSION=$SESSION export XSTUDIOPY_HOST=$HOST export XSTUDIOPY_PORT=$PORT -export XSTUDIOPY_SYNC=$SYNC export XSTUDIOPY_EXECUTE=$EXECUTE if [[ -z "$EXECUTE" ]]; then diff --git a/python/test/test_api.py b/python/test/test_api.py index 73d811f8c..f5bed076b 100644 --- a/python/test/test_api.py +++ b/python/test/test_api.py @@ -4,9 +4,6 @@ def test_type(spawn): assert spawn.app_type == spawn.APP_TYPE_XSTUDIO -def test_api_type(spawn): - assert spawn.api_type == spawn.API_TYPE_FULL - def test_api_session(spawn): assert isinstance(spawn.api.session, xstudio.api.session.Session) diff --git a/share/preference/core_api.json b/share/preference/core_api.json index 3336f1305..0dbd99a3a 100644 --- a/share/preference/core_api.json +++ b/share/preference/core_api.json @@ -36,6 +36,32 @@ "value": "127.0.0.1", "datatype": "string", "context": ["APPLICATION"] + }, + "authentication": { + "allow_unauthenticated": { + "path": "/core/api/authentication/allow_unauthenticated", + "default_value": false, + "description": "Allow unauthenticated access.", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"] + }, + "keys": { + "path": "/core/api/authentication/keys", + "default_value": [], + "description": "Authentication client/keys.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + }, + "passwords": { + "path": "/core/api/authentication/passwords", + "default_value": [], + "description": "Authentication user/passwords.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + } } } } diff --git a/share/preference/core_bookmark.json b/share/preference/core_bookmark.json index e2660d6be..7582fe90a 100644 --- a/share/preference/core_bookmark.json +++ b/share/preference/core_bookmark.json @@ -3,8 +3,8 @@ "bookmark":{ "note_category": { "path": "/core/bookmark/note_category", - "default_value": "default", - "value": "default", + "default_value": "", + "value": "", "description": "Default note category.", "datatype": "string", "context": ["APPLICATION"] @@ -31,7 +31,27 @@ { "value": "default", "colour": "", - "text": "Default" + "text": "Miscellaneous" + }, + { + "value": "director", + "colour": "#FFFF0000", + "text": "Director Note" + }, + { + "value": "studio", + "colour": "#FF00FF00", + "text": "Studio Note" + }, + { + "value": "vfx", + "colour": "#FF0000FF", + "text": "VFX Supervisor Note" + }, + { + "value": "editor", + "colour": "#FF00FFFF", + "text": "Editor Note" } ], "description": "Category defaults.", diff --git a/share/preference/core_cache.json b/share/preference/core_cache.json index c4deea4a5..4b63bf93d 100644 --- a/share/preference/core_cache.json +++ b/share/preference/core_cache.json @@ -35,9 +35,9 @@ "audio_cache":{ "max_count": { "path": "/core/audio_cache/max_count", - "default_value": 1000000, + "default_value": 4096, "description": "Maximum number of entries to store in cache.", - "value": 1000000, + "value": 4096, "minimum": 1, "maximum": 1000000, "datatype": "int", diff --git a/share/preference/core_global_store.json b/share/preference/core_global_store.json index 06e11bf72..478023659 100644 --- a/share/preference/core_global_store.json +++ b/share/preference/core_global_store.json @@ -10,6 +10,14 @@ "maximum": 600, "datatype": "int", "context": ["APPLICATION"] + }, + "autosave_enable": { + "path": "/core/global_store/autosave_enable", + "default_value": true, + "description": "Enable autosave of preferences.", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"] } } } diff --git a/share/preference/core_playhead.json b/share/preference/core_playhead.json index fd31f5031..6a34b631f 100644 --- a/share/preference/core_playhead.json +++ b/share/preference/core_playhead.json @@ -30,6 +30,50 @@ "maximum": 64, "datatype": "int", "context": ["APPLICATION"] + }, + "playlist_default_compare": { + "path": "/core/playhead/playlist_default_compare", + "default_value": ["A/B", "On"], + "description": "Set the default compare mode and auto align behaviour for new Playlists.", + "value": ["A/B", "On"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Playlists" + }, + "subset_default_compare": { + "path": "/core/playhead/subset_default_compare", + "default_value": ["A/B", "On"], + "description": "Set the default compare mode and auto align behaviour for new Subsets.", + "value": ["A/B", "On"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Subsets" + }, + "contact_sheet_default_compare": { + "path": "/core/playhead/contact_sheet_default_compare", + "default_value": ["Grid", "Off"], + "description": "Set the default compare mode and auto align behaviour for new Contact Sheets.", + "value": ["Grid", "Off"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Contact Sheets" + }, + "timeline_default_compare": { + "path": "/core/playhead/timeline_default_compare", + "default_value": ["Off", "Off"], + "description": "Set the default compare mode and auto align behaviour for new Timelines.", + "value": ["Off", "Off"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Timelines" } } } diff --git a/share/preference/core_plugin_manager.json b/share/preference/core_plugin_manager.json index 31f349ec4..e290219b4 100644 --- a/share/preference/core_plugin_manager.json +++ b/share/preference/core_plugin_manager.json @@ -6,7 +6,7 @@ "default_value": "{}", "description": "Enabled plugins.", "value": { - "e4e1d569-2338-4e6e-b127-5a9688df161a": false, + "e4e1d569-2338-4e6e-b127-5a9688df161a": true, "33201f8d-db32-4278-9c40-8c068372a304": false, "46f386a0-cb9a-4820-8e99-fb53f6c019eb": true, "5598e01e-c6bc-4cf9-80ff-74bb560df12a": true, diff --git a/share/preference/core_sequence.json b/share/preference/core_sequence.json index 9c25af461..6b4ba8952 100644 --- a/share/preference/core_sequence.json +++ b/share/preference/core_sequence.json @@ -8,7 +8,7 @@ ], "description": "Create default tracks quickly", "value": [ - {"name": "ABC", "video tracks": ["A","B","C"], "audio tracks": ["A", "B","C"]} + {"name": "ABC", "video tracks": [{"name":"A", "colour":"#ffff0000"},"B","C"], "audio tracks": ["A", "B","C"]} ], "datatype": "json", "context": ["APPLICATION"] diff --git a/share/preference/core_session.json b/share/preference/core_session.json index 8c48198f7..e45093c51 100644 --- a/share/preference/core_session.json +++ b/share/preference/core_session.json @@ -29,7 +29,7 @@ "options": ["23.976", "24.0", "25.0", "29.97", "30.0", "48.0", "50.0", "59.94", "60.0", "90.0", "100.0", "119.88", "120.0"], "category": "General", "display_name": "Default media rate for new sessions." - }, + }, "compression": { "path": "/core/session/compression", "default_value": false, @@ -81,6 +81,8 @@ "description": "Enable autosave of session.", "value": true, "datatype": "bool", + "display_name": "Enable Autosave", + "category": "General", "context": ["APPLICATION"] }, "interval": { diff --git a/share/preference/core_sync.json b/share/preference/core_sync.json deleted file mode 100644 index ec3d8c21d..000000000 --- a/share/preference/core_sync.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "core": { - "sync":{ - "enabled": { - "path": "/core/sync/enabled", - "default_value": false, - "description": "Enable external SYNC API port.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] - }, - "port_minimum": { - "path": "/core/sync/port_minimum", - "default_value": 12346, - "description": "SYNC minimum port number.", - "value": 45130, - "minimum": 0, - "maximum": 65535, - "datatype": "int", - "context": ["APPLICATION"] - }, - "port_maximum": { - "path": "/core/sync/port_maximum", - "default_value": 12346, - "description": "SYNC maximum port number.", - "value": 45130, - "minimum": 0, - "maximum": 65535, - "datatype": "int", - "context": ["APPLICATION"] - }, - "bind_address": { - "path": "/core/sync/bind_address", - "default_value": "127.0.0.1", - "description": "IP to bind against.", - "value": "127.0.0.1", - "datatype": "string", - "context": ["APPLICATION"] - } - } - } -} \ No newline at end of file diff --git a/share/preference/ui_qml.json b/share/preference/ui_qml.json index 0ad67be1b..a534998e4 100644 --- a/share/preference/ui_qml.json +++ b/share/preference/ui_qml.json @@ -65,15 +65,6 @@ "datatype": "string", "context": ["QML_UI"] }, - "autosave": { - "path": "/ui/qml/autosave", - "default_value": true, - "description": "Enable autosave.", - "value": true, - "datatype": "bool", - "context": ["QML_UI"], - "category": "General" - }, "start_play_on_load": { "path": "/ui/qml/start_play_on_load", "default_value": true, diff --git a/share/snippets/clip/Demo/random_colour.py b/share/snippets/clip/Demo/random_colour.py new file mode 100644 index 000000000..9dfcac4e5 --- /dev/null +++ b/share/snippets/clip/Demo/random_colour.py @@ -0,0 +1,15 @@ +from xstudio.api.session.playlist.timeline import Timeline, Clip +import random +import colorsys + +def random_clip_colour(item=XSTUDIO.api.session.viewed_container): + if isinstance(item, Timeline): + for i in item.selection: + if isinstance(i, Clip): + c = random.randrange(0, 255) + + rgb = colorsys.hsv_to_rgb(c * (1.0/255.0), 1, 1) + i.item_flag = "#FF" + f"{int(rgb[0]*255.0):0{2}x}"+ f"{int(rgb[1]*255.0):0{2}x}"+ f"{int(rgb[2]*255.0):0{2}x}" + +random_clip_colour() + diff --git a/share/snippets/playlist/DNeg/name_from_media_shot.py b/share/snippets/playlist/DNeg/name_from_media_shot.py new file mode 100644 index 000000000..d9b389b01 --- /dev/null +++ b/share/snippets/playlist/DNeg/name_from_media_shot.py @@ -0,0 +1,11 @@ +def name_from_media_shot(items=XSTUDIO.api.session.selected_containers): + for item in items: + try: + media = item.media[0] + project = media.metadata["metadata"]["shotgun"]["version"]["relationships"]["project"]["data"]["name"] + shot = media.metadata["metadata"]["shotgun"]["version"]["relationships"]["entity"]["data"]["name"] + item.name = project+" - "+shot + except: + pass + +name_from_media_shot() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2152a9f7d..521c982d8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,7 +43,6 @@ if(INSTALL_XSTUDIO) add_src_and_test(shotgun_client) add_src_and_test(studio) add_src_and_test(subset) - add_src_and_test(sync) add_src_and_test(thumbnail) add_src_and_test(ui) diff --git a/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index fdd0cab91..552708d1a 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -122,7 +122,7 @@ template media_reader::AudioBufPtr super_simple_respeed_audio_buffer(const media_reader::AudioBufPtr in, const float velocity); -void AudioOutputControl::prepare_samples_for_soundcard( +void AudioOutputControl::prepare_samples_for_soundcard_playback( std::vector &v, const long num_samps_to_push, const long microseconds_delay, @@ -131,16 +131,10 @@ void AudioOutputControl::prepare_samples_for_soundcard( try { - v.resize(num_samps_to_push * num_channels); - memset(v.data(), 0, v.size() * sizeof(int16_t)); - int16_t *d = v.data(); long n = num_samps_to_push; long num_samps_pushed = 0; - if (muted() || (!playing_ && !audio_scrubbing_)) - return; - while (n > 0) { if (!current_buf_ && sample_data_.size()) { @@ -153,17 +147,6 @@ void AudioOutputControl::prepare_samples_for_soundcard( utility::clock::now() + std::chrono::microseconds(microseconds_delay) + std::chrono::microseconds((num_samps_pushed * 1000000) / sample_rate); - if (!playing_) { - - // when the user is scrubbing the timeline, the playhead pushes a couple - // of frames worth of samples to be played immediately. Because - // playback accuracy is out of the window when scrubbing, we can - // ignore the samples in the buffer and just pick the buffer - // in the queue closest to 'now' to push into the soundcard - // buffer - next_sample_play_time = utility::clock::now(); - } - current_buf_ = pick_audio_buffer(next_sample_play_time, true); if (current_buf_) { @@ -172,14 +155,24 @@ void AudioOutputControl::prepare_samples_for_soundcard( // is audio playback stable ? i.e. is the next sample buffer // continuous with the one we are about to play? - auto next_buf = pick_audio_buffer( + next_buf_ = pick_audio_buffer( next_sample_play_time + std::chrono::microseconds( int(round(current_buf_->duration_seconds() * 1000000.0))), false); fade_in_out_ = check_if_buffer_is_contiguous_with_previous_and_next( - current_buf_, next_buf, previous_buf_); + current_buf_, next_buf_, previous_buf_); + + /*if (fade_in_out_ != NoFade) { + if (previous_buf_) + std::cerr << "P " << to_string(previous_buf_->media_key()) << "\n"; + if (current_buf_) + std::cerr << "C " << to_string(current_buf_->media_key()) << "\n"; + if (next_buf_) + std::cerr << "N " << to_string(next_buf_->media_key()) << "\n\n"; + + }*/ } else { fade_in_out_ = DoFadeHeadAndTail; @@ -190,13 +183,6 @@ void AudioOutputControl::prepare_samples_for_soundcard( break; } - if (!playing_) { - // when scrubbing, audio sample buffers are unlikely to be - // continguous, so we fade out head and tail of the buffer to - // reduce distortion - fade_in_out_ = DoFadeHeadAndTail; - } - copy_from_xstudio_audio_buffer_to_soundcard_buffer( d, current_buf_, @@ -207,7 +193,8 @@ void AudioOutputControl::prepare_samples_for_soundcard( fade_in_out_); if (current_buf_pos_ == (long)current_buf_->num_samples()) { - // current buf is exhausted + // current buf is exhausted, clear current_buf_ so we pick + // a new one on next pass through this loop previous_buf_ = current_buf_; current_buf_.reset(); } else { @@ -228,6 +215,109 @@ void AudioOutputControl::prepare_samples_for_soundcard( } } + +void AudioOutputControl::prepare_samples_for_soundcard_scrubbing( + std::vector &v, + const long num_samps_to_push, + const long microseconds_delay, + const int num_channels, + const int sample_rate) { + + try { + + // when scrubbing we're hard-coded here to play the audio from + + v.resize(num_samps_to_push * num_channels); + memset(v.data(), 0, v.size() * sizeof(int16_t)); + + int16_t *d = v.data(); + long n = num_samps_to_push; + long num_samps_pushed = 0; + + + while (n > 0) { + + if (!current_buf_ && sample_data_.size()) { + + // get the audio frame closest to current playhead position + auto r = sample_data_.lower_bound(playhead_position_); + if (r == sample_data_.end()) { + break; + return; + } + + // get et the audio buf with a 'show' time that is CLOSEST + // to now, need to look at the previous element to see if + // it's nearer + if (r != sample_data_.begin()) { + auto r2 = r; + r2--; + const auto d2 = playhead_position_ - r2->first; + const auto d1 = r->first - playhead_position_; + + if (d1 > d2) { + r = r2; + } + } + + current_buf_ = r->second; + current_buf_pos_ = 0; + + if (current_buf_ == previous_buf_) { + // never play the same audio frame that we just played + current_buf_.reset(); + break; + } + r++; + if (r != sample_data_.end()) + next_buf_ = r->second; + else + next_buf_.reset(); + + + // blend the first few samples up from zero amplitude to full + // to avoid transients + fade_in_out_ = DoFadeHead; + + } else if (sample_data_.empty()) { + break; + } + + copy_from_xstudio_audio_buffer_to_soundcard_buffer( + d, + current_buf_, + current_buf_pos_, + n, + num_samps_pushed, + num_channels, + fade_in_out_); + + if (current_buf_pos_ == (long)current_buf_->num_samples()) { + // current buf is exhausted, move on to next_buf_ + if (next_buf_) { + previous_buf_ = current_buf_; + current_buf_ = next_buf_; + current_buf_pos_ = 0; + // blend the last few samples down to zero amplitude + // to avoid transients + fade_in_out_ = DoFadeTail; + next_buf_.reset(); + } else { + current_buf_.reset(); + } + } else { + break; + } + } + + static_volume_adjust(v, volume() / 100.0f); + last_volume_ = volume(); + + } catch (std::exception &e) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + void AudioOutputControl::queue_samples_for_playing( const std::vector &audio_frames) { @@ -235,8 +325,8 @@ void AudioOutputControl::queue_samples_for_playing( if (audio_frames.size()) { t0 = audio_frames[0].timeline_timestamp(); } - - for (const auto & frame: audio_frames) { + timebase::flicks o; + for (const auto &_frame : audio_frames) { // xstudio stores a frame of audio samples for every video frame for any // given source (if the source has no video it is assigned a 'virtual' video @@ -245,17 +335,26 @@ void AudioOutputControl::queue_samples_for_playing( // However, audio frames generally // do not have the same duration as video frames, so there is always some // offset between when the video frame is shown and when the audio samples - // associated with that frame should sound. - const auto adjusted_timeline_timestamp = - std::chrono::duration_cast(frame.timeline_timestamp() + (frame ? frame->time_delta_to_video_frame() : std::chrono::microseconds(0))); - - if (frame) - t0 += timebase::to_flicks(frame->duration_seconds()); + // associated with that frame should sound. When building sample_data_ + // map we take account of this difference, which was calculate in + // the fffmpeg reader when the audio samples are packaged up. + const auto adjusted_timeline_timestamp = std::chrono::duration_cast( + _frame.timeline_timestamp() + + (_frame ? _frame->time_delta_to_video_frame() : std::chrono::microseconds(0))); + + if (_frame) + t0 += timebase::to_flicks(_frame->duration_seconds()); + else + continue; + + media_reader::AudioBufPtr frame = + audio_repitch_ && playback_velocity_ != 1.0f + ? super_simple_respeed_audio_buffer(_frame, playback_velocity_) + : _frame; if (!playing_forward_) { - media_reader::AudioBufPtr reversed( - new media_reader::AudioBuffer(frame->params())); + media_reader::AudioBufPtr reversed(new media_reader::AudioBuffer(frame->params())); reversed->allocate( frame->sample_rate(), @@ -276,9 +375,7 @@ void AudioOutputControl::queue_samples_for_playing( } else { sample_data_[adjusted_timeline_timestamp] = frame; } - } - } void AudioOutputControl::playhead_position_changed( @@ -286,12 +383,14 @@ void AudioOutputControl::playhead_position_changed( const bool forward, const float velocity, const bool playing, - utility::time_point when_position_changed) -{ - playhead_position_ = playhead_position; - playback_velocity_ = audio_repitch_ ? std::max(0.1f, velocity) : 1.0f; - playing_ = playing; - playing_forward_ = forward; + utility::time_point when_position_changed) { + if (!playing_ && playhead_position == playhead_position_) { + // playhead hasn't moved + } + playhead_position_ = playhead_position; + playback_velocity_ = std::max(0.1f, velocity); + playing_ = playing; + playing_forward_ = forward; playhead_position_update_tp_ = when_position_changed; } @@ -303,30 +402,58 @@ void AudioOutputControl::clear_queued_samples() { media_reader::AudioBufPtr AudioOutputControl::pick_audio_buffer( const utility::clock::time_point &tp, bool drop_old_buffers) { - // based on 'tp' - we estimate the playhead position when the - // audio buffer should actually be sounding - auto future_playhead_position = playhead_position_; - if (playing_) { - - // predict where we will be at the timepoint 'next_video_refresh' ... - const timebase::flicks delta = std::chrono::duration_cast( - tp - playhead_position_update_tp_); - const double v = (playing_forward_ ? 1.0f : -1.0f) * playback_velocity_; + // The idea here is we pick an audio buffer from sample_data_ to draw + // samples off and stream to the soundcard. - future_playhead_position = - timebase::to_flicks(v * timebase::to_seconds(delta)) + playhead_position_; + // sample_data_ is a map of audio buffers stored against their play timestamp + // in the playhead timeline. So for example frame zero in the timeline + // has timestamp = 0. Frame 2 has timestamp = 41ms (for a 24fps source). + // 'tp' here is a system clock timepoint that tells us when the last + // audio sample currently in the soundcard sample buffer will actually + // get played. + + // based on 'tp' - we estimate the playhead position when the + // first sample of the next audio buffer that we pick to put into the + // soundcard buffer will sound. + + // predict where we will be at the timepoint 'next_video_refresh' ... + const timebase::flicks delta = + std::chrono::duration_cast(tp - playhead_position_update_tp_); + const double v = (playing_forward_ ? 1.0f : -1.0f) * playback_velocity_; + + auto future_playhead_position = + timebase::to_flicks(v * timebase::to_seconds(delta)) + playhead_position_; + + + // during playback, we just pick the audio buffer immediately after + // the one that we last used. However, we check if this new buffer's + // position in the playback timeline matches well with our estimate of + // where the playhead will be when the audio samples hit the speakers. + // + // If it doesn't we continue and use a best match search below + + // let's step from the last audio buffer we used to the next... + auto p = sample_data_.find(last_buffer_pts_); + if (p != sample_data_.end()) { + if (playing_forward_) { + p++; + } else if (!playing_forward_ && p != sample_data_.begin()) { + p--; + } + + auto drift = timebase::to_seconds(future_playhead_position - p->first); + if (fabs(drift) < 0.05) { + if (drop_old_buffers) + last_buffer_pts_ = p->first; + return p->second; + } } auto r = sample_data_.lower_bound(future_playhead_position); if (r == sample_data_.end()) { auto r = media_reader::AudioBufPtr(); - if (!playing_ && sample_data_.size()) { - // user is scrubbing. This means we - r = sample_data_.rbegin()->second; - sample_data_.clear(); - } return r; } @@ -344,17 +471,20 @@ media_reader::AudioBufPtr AudioOutputControl::pick_audio_buffer( } } - media_reader::AudioBufPtr v = r->second; + media_reader::AudioBufPtr buf = r->second; + if (drop_old_buffers) + last_buffer_pts_ = r->first; // what if our 'best' buffer, i.e. the one nearest to 'future_playhead_position' // is still not close. Some innaccuracy is happening, e.g. buffers that we need // haven't been delivered. We must play silence instead, - const auto delta = - double(std::chrono::duration_cast(r->first - future_playhead_position).count()) / - 1000000.0; + const auto t_mismatch = double(std::chrono::duration_cast( + r->first - future_playhead_position) + .count()) / + 1000000.0; // so if we are more than half the duration of the buffer out, return empty ptr - if (!v || fabs(delta) > v->duration_seconds() / 2) { + if (!buf || fabs(t_mismatch) > buf->duration_seconds() / 2) { return media_reader::AudioBufPtr(); } @@ -363,7 +493,7 @@ media_reader::AudioBufPtr AudioOutputControl::pick_audio_buffer( } else if (drop_old_buffers) { sample_data_.erase(r, sample_data_.end()); } - return v; + return buf; } AudioOutputControl::Fade diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index 472756f6c..dbc29619e 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -23,6 +23,33 @@ using namespace xstudio::audio; using namespace xstudio::utility; using namespace xstudio; +/* Notes: + +In order to support multiple audio outputs (e.g. PC soundcard, SDI output card +etc.) receiving samples from multiple playing playheads, the audio output model +is a somewhat complex. The model for audio playback is described as follows: + +During playback a PlayheadActor delivers audio samples to the singleton +GlobalAudioOutputActor instance. + +The GlobalAudioOutputActor then forwards the samples data to AudioOutputActors +that then deliver the samples to sound devices. + +For the active playhead in the xSTUDIO's main UI, the audio samples from that +playhead are forwarded via the event_group of the GlobalAudioOutputActor. +The default PC audio AudioOutputActor, plus AudioOutputActor from video output +plugins (e.g. SDI card AV output plugin) will receive and play audio from this +event_group. + +For playheads belonging to the xSTUDIO 'quicview' light viewers, a separate +instance of the AudioOutputActor delivering to the PC audio is created and +samples from those playheads are delivered to these separate AudioOutputActor +instances. This allows independent audio playback in the quickviewer windows +from the main xSTUDIO UI. + +*/ + + void AudioOutputActor::init() { // spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); @@ -34,7 +61,13 @@ void AudioOutputActor::init() { auto global_audio_actor = system().registry().template get(audio_output_registry); - utility::join_event_group(this, global_audio_actor); + + if (is_global_) { + // we only want to receive audio via the global_audio_actor event group + // if we are marked as a global AudioOutputActor - this is how we play + // audio from whatever is playing in the xstudio main UI. + utility::join_event_group(this, global_audio_actor); + } behavior_.assign( @@ -52,14 +85,37 @@ void AudioOutputActor::init() { const long microseconds_delay, const int num_channels, const int sample_rate) -> result> { + // this message is a request from the AudioOutputDeviceActor to + // get samples for the soundcard + std::vector samples; + samples.resize(num_samps_to_push * num_channels); + memset(samples.data(), 0, samples.size() * sizeof(int16_t)); + + if (muted() || (!playing_ && !audio_scrubbing_)) + return samples; + try { - prepare_samples_for_soundcard( - samples, num_samps_to_push, microseconds_delay, num_channels, sample_rate); + if (!playing_) { - } catch (std::exception &e) { + prepare_samples_for_soundcard_scrubbing( + samples, + num_samps_to_push, + microseconds_delay, + num_channels, + sample_rate); + } else { + prepare_samples_for_soundcard_playback( + samples, + num_samps_to_push, + microseconds_delay, + num_channels, + sample_rate); + } + + } catch (std::exception &e) { return caf::make_error(xstudio_error::error, e.what()); } return samples; @@ -82,7 +138,7 @@ void AudioOutputActor::init() { sub_playhead_uuid_ = sub_playhead; } queue_samples_for_playing(audio_buffers); - send(audio_output_device_, utility::event_atom_v, playhead::play_atom_v); + send(audio_output_device_, utility::event_atom_v, playhead::play_atom_v); }, [=](utility::event_atom, playhead::position_atom, @@ -91,10 +147,17 @@ void AudioOutputActor::init() { const float velocity, const bool playing, utility::time_point when_position_changed) { - // these event messages are very fine-grained, so we know very accurately the playhead - // position during playback + // these event messages are very fine-grained, so we know very accurately the + // playhead position during playback playhead_position_changed( playhead_position, forward, velocity, playing, when_position_changed); + }, + [=](utility::event_atom, + audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + }); // kicks the global samples actor to update us with current volume etc. @@ -117,8 +180,6 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) volume_ = add_float_attribute("volume", "volume", 100.0f, 0.0f, 100.0f, 0.05f); volume_->set_role_data(module::Attribute::PreferencePath, "/core/audio/volume"); - - // by setting static UUIDs on these module we only create them once in the UI volume_->set_role_data(module::Attribute::UIDataModels, nlohmann::json{"audio_output"}); muted_ = add_boolean_attribute("muted", "muted", false); @@ -143,35 +204,52 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](playhead::play_atom, const bool is_playing) { - send(event_group_, utility::event_atom_v, playhead::play_atom_v, is_playing); + [=](playhead::play_atom, + const bool is_playing, + bool global, + const utility::Uuid &playhead_uuid) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + if (dest) { + send(dest, utility::event_atom_v, playhead::play_atom_v, is_playing); + } }, [=](playhead::position_atom, const timebase::flicks playhead_position, const bool forward, const float velocity, const bool playing, - utility::time_point when_position_changed) { - send( - event_group_, - utility::event_atom_v, - playhead::position_atom_v, - playhead_position, - forward, - velocity, - playing, - when_position_changed - ); + utility::time_point when_position_changed, + bool global, + const utility::Uuid &playhead_uuid) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + + if (dest) { + send( + dest, + utility::event_atom_v, + playhead::position_atom_v, + playhead_position, + forward, + velocity, + playing, + when_position_changed); + } }, [=](playhead::sound_audio_atom, const std::vector &audio_buffers, - const Uuid &sub_playhead_id) { - send( - event_group_, - utility::event_atom_v, - playhead::sound_audio_atom_v, - audio_buffers, - sub_playhead_id); + const Uuid &sub_playhead_id, + bool global, + const utility::Uuid &playhead_uuid) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + + if (dest) { + send( + dest, + utility::event_atom_v, + playhead::sound_audio_atom_v, + audio_buffers, + sub_playhead_id); + } }, [=](module::change_attribute_event_atom, caf::actor requester) { send( @@ -182,6 +260,27 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) muted_->value(), audio_repitch_->value(), audio_scrubbing_->value()); + }, + [=](audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + send( + event_group_, + utility::event_atom_v, + audio::audio_samples_atom_v, + audio_buffers, + playhead_position, + playhead_uuid); + }, + [=](playhead::sound_audio_atom, const utility::Uuid &playhead_uuid, bool) { + // playhead is exiting, if the playhead has its own audid output + // actor, kill it now + auto p = independent_outputs_.find(playhead_uuid); + if (p != independent_outputs_.end()) { + send_exit(p->second, caf::exit_reason::user_shutdown); + independent_outputs_.erase(p); + } } ); @@ -203,6 +302,17 @@ void GlobalAudioOutputActor::attribute_changed(const utility::Uuid &attr_uuid, c audio_repitch_->value(), audio_scrubbing_->value()); + for (auto &p : independent_outputs_) { + send( + p.second, + 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); } @@ -213,3 +323,40 @@ void GlobalAudioOutputActor::hotkey_pressed( muted_->set_value(!muted_->value()); } } + +caf::actor GlobalAudioOutputActor::independent_output(const utility::Uuid &playhead_uuid) { + + auto p = independent_outputs_.find(playhead_uuid); + if (p != independent_outputs_.end()) { + return p->second; + } + + try { + JsonStore prefs_jsn; + auto prefs = global_store::GlobalStoreHelper(system()); + join_broadcast(this, prefs.get_group(prefs_jsn)); + +#ifdef __linux__ + auto output_actor = spawn( + std::shared_ptr( + new audio::LinuxAudioOutputDevice(prefs_jsn)), + false); +#elif __APPLE__ + // TO DO +#elif _WIN32 + auto output_actor = spawn( + std::shared_ptr( + new audio::WindowsAudioOutputDevice(prefs_jsn)), + false); +#endif + + link_to(output_actor); + independent_outputs_[playhead_uuid] = output_actor; + return output_actor; + } catch (std::exception &e) { + independent_outputs_[playhead_uuid] = caf::actor(); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return caf::actor(); +} \ No newline at end of file diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index 593db4bcd..6190e838f 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -64,8 +64,9 @@ void LinuxAudioOutputDevice::connect_to_soundcard() { nullptr, nullptr, &error))) { - spdlog::warn( + std::string err = fmt::format( "{} pa_simple_new() failed: {} ", __PRETTY_FUNCTION__, pa_strerror(error)); + throw std::runtime_error(err.c_str()); } spdlog::debug("{} Connected to soundcard : {} ", __PRETTY_FUNCTION__, sound_card.c_str()); diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index 05fedbaf1..a33354972 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -211,12 +211,12 @@ void WindowsAudioOutputDevice::initialize_sound_card() { } audio_client_->Start(); - } void WindowsAudioOutputDevice::connect_to_soundcard() { // We are already playing ;-D - if (!audio_client_) initialize_sound_card(); + if (!audio_client_) + initialize_sound_card(); } long WindowsAudioOutputDevice::desired_samples() { @@ -259,7 +259,7 @@ long WindowsAudioOutputDevice::latency_microseconds() { << " " << (long(pad) * long(1000000)) / long(sample_rate_) << " ";*/ - return (long(pad)*long(1000000))/long(sample_rate_); + return (long(pad) * long(1000000)) / long(sample_rate_); } bool WindowsAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { diff --git a/src/bookmark/src/bookmark.cpp b/src/bookmark/src/bookmark.cpp index 835fdef45..d38a7106d 100644 --- a/src/bookmark/src/bookmark.cpp +++ b/src/bookmark/src/bookmark.cpp @@ -234,9 +234,10 @@ BookmarkDetail &BookmarkDetail::operator=(const Bookmark &other) { user_data_ = other.user_data_; created_ = other.created_; - has_note_ = other.has_note(); - has_annotation_ = other.has_annotation(); - owner_ = UuidActor(other.owner_, caf::actor()); + has_note_ = other.has_note(); + has_annotation_ = other.has_annotation(); + annotation_hash_ = other.annotation_hash(); + owner_ = UuidActor(other.owner_, caf::actor()); if (*(has_note_)) { author_ = other.note_->author(); diff --git a/src/bookmark/src/bookmark_actor.cpp b/src/bookmark/src/bookmark_actor.cpp index 5c2666abf..88fbf3226 100644 --- a/src/bookmark/src/bookmark_actor.cpp +++ b/src/bookmark/src/bookmark_actor.cpp @@ -459,7 +459,6 @@ void BookmarkActor::set_owner(caf::actor owner, const bool dead) { [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + send(base_.event_group(), utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); } - - send(base_.event_group(), utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); } diff --git a/src/bookmark/src/bookmarks_actor.cpp b/src/bookmark/src/bookmarks_actor.cpp index bdd22b9a1..1c9c3f12f 100644 --- a/src/bookmark/src/bookmarks_actor.cpp +++ b/src/bookmark/src/bookmarks_actor.cpp @@ -399,7 +399,7 @@ caf::message_handler BookmarksActor::message_handler() { return std::vector(); auto rp = make_response_promise>(); - + auto w = map_value_to_vec(bookmarks_); fan_out_request( map_value_to_vec(bookmarks_), infinite, bookmark_detail_atom_v) .then( @@ -597,7 +597,8 @@ void BookmarksActor::init() { set_down_handler([=](down_msg &msg) { // find in playhead list.. - for (auto it = std::begin(bookmarks_); it != std::end(bookmarks_); ++it) { + auto it = bookmarks_.begin(); + while (it != bookmarks_.end()) { if (msg.source == it->second) { demonitor(it->second); // spdlog::warn("bookmark exited {}", to_string(it->first)); @@ -607,8 +608,9 @@ void BookmarksActor::init() { utility::event_atom_v, remove_bookmark_atom_v, it->first); - bookmarks_.erase(it); - break; + it = bookmarks_.erase(it); + } else { + it++; } } }); diff --git a/src/colour_pipeline/src/colour_cache_actor.cpp b/src/colour_pipeline/src/colour_cache_actor.cpp index e94d341b1..b78a0425e 100644 --- a/src/colour_pipeline/src/colour_cache_actor.cpp +++ b/src/colour_pipeline/src/colour_cache_actor.cpp @@ -84,56 +84,57 @@ GlobalColourCacheActor::GlobalColourCacheActor(caf::actor_config &cfg) [=](preserve_atom, const std::string &key) -> bool { return cache_.preserve(key); }, [=](preserve_atom, const std::string &key, const time_point &time) -> bool { - //std::cerr << "K " << key << "\n"; + // std::cerr << "K " << key << "\n"; return cache_.preserve(key, time); }, [=](preserve_atom, const std::string &key, const time_point &time, const Uuid &uuid) - -> bool { - //std::cerr << "K1 " << key << "\n"; - return cache_.preserve(key, time, uuid); - }, + -> bool { + // std::cerr << "K1 " << key << "\n"; + return cache_.preserve(key, time, uuid); + }, [=](retrieve_atom, const std::string &key) -> ColourOperationDataPtr { - //std::cerr << "K2 " << key << "\n"; + // std::cerr << "K2 " << key << "\n"; return cache_.retrieve(key); }, - [=](retrieve_atom, const std::string &key, const time_point &time) - -> ColourOperationDataPtr { - //std::cerr << "K3 " << key << "\n"; - return cache_.retrieve(key, time); - }, + [=](retrieve_atom, + const std::string &key, + const time_point &time) -> ColourOperationDataPtr { + // std::cerr << "K3 " << key << "\n"; + return cache_.retrieve(key, time); + }, [=](retrieve_atom, const std::string &key, const time_point &time, const Uuid &uuid) - -> ColourOperationDataPtr { - //std::cerr << "K4 " << key << "\n"; + -> ColourOperationDataPtr { + // std::cerr << "K4 " << key << "\n"; - return cache_.retrieve(key, time, uuid); - }, + return cache_.retrieve(key, time, uuid); + }, [=](size_atom) -> size_t { return cache_.size(); }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf) -> bool { - //std::cerr << "K5 " << key << "\n"; + // std::cerr << "K5 " << key << "\n"; return cache_.store(key, buf); }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf, - const time_point &when) -> bool { - //std::cerr << "K6 " << key << "\n"; - return cache_.store(key, buf, when); - }, + const time_point &when) -> bool { + // std::cerr << "K6 " << key << "\n"; + return cache_.store(key, buf, when); + }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf, const time_point &when, const utility::Uuid &uuid) -> bool { - //std::cerr << "K7 " << key << "\n"; + // std::cerr << "K7 " << key << "\n"; return cache_.store(key, buf, when, false, uuid); }); diff --git a/src/colour_pipeline/src/colour_pipeline.cpp b/src/colour_pipeline/src/colour_pipeline.cpp index d20d094db..105697e0b 100644 --- a/src/colour_pipeline/src/colour_pipeline.cpp +++ b/src/colour_pipeline/src/colour_pipeline.cpp @@ -62,7 +62,7 @@ caf::message_handler ColourPipeline::message_handler_extensions() { // This call takes an image ptr, and returns a copy of the image ptr // but with all colour management data filled in. // Note that we do this in two steps. The ColourPipelineDataPtr carries - // static colour pipe data, like LUTs and shader code. This data is + // static colour pipe data, like LUTs and shader code. This data is // generally expensive to generate and is pre-prepared and cached by // this actor before we need it a draw-time. // The colour_pipe_unirorms is dynamic colour management data - stuff like @@ -70,26 +70,23 @@ caf::message_handler ColourPipeline::message_handler_extensions() { // is not cached but provided on-demand. [=](get_colour_pipe_data_atom, const media_reader::ImageBufPtr image) -> caf::result { - auto rp = make_response_promise(); - request(self(), infinite, get_colour_pipe_data_atom_v, image.frame_id()).then( - [=](const ColourPipelineDataPtr ptr) mutable { - media_reader::ImageBufPtr result = image; - result.colour_pipe_data_ = ptr; + request(self(), infinite, get_colour_pipe_data_atom_v, image.frame_id()) + .then( + [=](const ColourPipelineDataPtr ptr) mutable { + media_reader::ImageBufPtr result = image; + result.colour_pipe_data_ = ptr; - request(self(), infinite, colour_operation_uniforms_atom_v, result).then( - [=](const utility::JsonStore & colour_pipe_uniforms) mutable { - result.colour_pipe_uniforms_ = colour_pipe_uniforms; - rp.deliver(result); - }, - [=](caf::error &err) mutable { - rp.deliver(err); - }); - }, - [=](caf::error &err) mutable { - rp.deliver(err); - }); + request(self(), infinite, colour_operation_uniforms_atom_v, result) + .then( + [=](const utility::JsonStore &colour_pipe_uniforms) mutable { + result.colour_pipe_uniforms_ = colour_pipe_uniforms; + rp.deliver(result); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); return rp; }, @@ -401,7 +398,8 @@ caf::message_handler ColourPipeline::message_handler_extensions() { }, [=](json_store::update_atom, const utility::JsonStore &) mutable {}, [=](utility::serialise_atom) -> utility::JsonStore { return serialise(); }, - [=](ui::viewport::pre_render_gpu_hook_atom) -> result { + [=](ui::viewport::pre_render_gpu_hook_atom, + const std::string &viewport_name) -> result { // This message handler overrides the one in PluginBase class. // op plugins themselves might have a GPUPreDrawHook that needs // to be passed back up to the Viewport object that is making this @@ -409,10 +407,10 @@ caf::message_handler ColourPipeline::message_handler_extensions() { // (see load_colour_op_plugins) we therefore need our own logic here. auto rp = make_response_promise(); if (colour_ops_loaded_) { - make_pre_draw_gpu_hook(rp); + make_pre_draw_gpu_hook(viewport_name, rp); } else { // add to a queue of these requests pending a response - hook_requests_.push_back(rp); + hook_requests_.push_back(std::make_pair(viewport_name, rp)); // load_colour_op_plugins() will respond to these requests // when all the plugins are loaded. } @@ -428,7 +426,7 @@ void ColourPipeline::attribute_changed(const utility::Uuid &attr_uuid, const int bool ColourPipeline::add_in_flight_request( const std::string &transform_id, caf::typed_response_promise rp) { in_flight_requests_[transform_id].push_back(rp); - return in_flight_requests_[transform_id].size() > 1; + return in_flight_requests_[transform_id].size() > 1; } bool ColourPipeline::make_colour_pipe_data_from_cached_data( @@ -565,7 +563,7 @@ void ColourPipeline::load_colour_op_plugins() { if (colour_op_plugin_details.empty()) { colour_ops_loaded_ = true; for (auto &hr : hook_requests_) { - make_pre_draw_gpu_hook(hr); + make_pre_draw_gpu_hook(hr.first, hr.second); } hook_requests_.clear(); } @@ -588,14 +586,14 @@ void ColourPipeline::load_colour_op_plugins() { if (!(*count)) { colour_ops_loaded_ = true; for (auto &hr : hook_requests_) { - make_pre_draw_gpu_hook(hr); + make_pre_draw_gpu_hook(hr.first, hr.second); } hook_requests_.clear(); } }, [=](const caf::error &err) mutable { for (auto &hr : hook_requests_) { - hr.deliver(err); + hr.second.deliver(err); } hook_requests_.clear(); }); @@ -603,7 +601,7 @@ void ColourPipeline::load_colour_op_plugins() { }, [=](const caf::error &err) mutable { for (auto &hr : hook_requests_) { - hr.deliver(err); + hr.second.deliver(err); } hook_requests_.clear(); }); @@ -634,6 +632,7 @@ class HookCollection : public plugin::GPUPreDrawHook { void ColourPipeline::make_pre_draw_gpu_hook( + const std::string &viewport_name, caf::typed_response_promise rp) { auto collection = std::make_shared(); @@ -650,7 +649,8 @@ void ColourPipeline::make_pre_draw_gpu_hook( auto count = std::make_shared(colour_op_plugins_.size()); for (auto &colour_op_plugin : colour_op_plugins_) { - request(colour_op_plugin, infinite, ui::viewport::pre_render_gpu_hook_atom_v) + request( + colour_op_plugin, infinite, ui::viewport::pre_render_gpu_hook_atom_v, viewport_name) .then( [=](plugin::GPUPreDrawHookPtr &hook) mutable { if (hook) { diff --git a/src/conform/src/conform_manager_actor.cpp b/src/conform/src/conform_manager_actor.cpp index 1e6ffbc3d..da454945f 100644 --- a/src/conform/src/conform_manager_actor.cpp +++ b/src/conform/src/conform_manager_actor.cpp @@ -26,31 +26,59 @@ using namespace caf; ConformWorkerActor::ConformWorkerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { // get hooks - { - auto pm = system().registry().template get(plugin_manager_registry); - scoped_actor sys{system()}; - auto details = request_receive>( - *sys, - pm, - utility::detail_atom_v, - plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); - - for (const auto &i : details) { - if (i.enabled_) { - auto actor = request_receive( - *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); - link_to(actor); - conformers_.push_back(actor); - } - } - } + // { + // auto pm = system().registry().template get(plugin_manager_registry); + // scoped_actor sys{system()}; + // auto details = request_receive>( + // *sys, + // pm, + // utility::detail_atom_v, + // plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); + + // for (const auto &i : details) { + // if (i.enabled_) { + // auto actor = request_receive( + // *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); + // link_to(actor); + // conformers_.push_back(actor); + // } + // } + // } // distribute to all conformers. + delayed_anon_send( + caf::actor_cast(this), std::chrono::seconds(4), conform_tasks_atom_v); behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](conform_tasks_atom) -> result> { + if (not initialised_) { + // should be the first function called by the manager + auto pm = system().registry().template get(plugin_manager_registry); + scoped_actor sys{system()}; + try { + auto details = request_receive>( + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); + + for (const auto &i : details) { + if (i.enabled_) { + auto actor = request_receive( + *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); + link_to(actor); + conformers_.push_back(actor); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + initialised_ = true; + } + if (not conformers_.empty()) { auto rp = make_response_promise>(); fan_out_request(conformers_, infinite, conform_tasks_atom_v) diff --git a/src/contact_sheet/src/contact_sheet_actor.cpp b/src/contact_sheet/src/contact_sheet_actor.cpp index 31a411b6d..fa5ed9cfc 100644 --- a/src/contact_sheet/src/contact_sheet_actor.cpp +++ b/src/contact_sheet/src/contact_sheet_actor.cpp @@ -22,16 +22,12 @@ ContactSheetActor::ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn) : SubsetActor(cfg, playlist, jsn) { - if (jsn.contains("playhead")) { - playhead_serialisation_ = jsn["playhead"]; - } init(); } ContactSheetActor::ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const std::string &name) - : SubsetActor(cfg, playlist, name, "ContactSheet") -{ + : SubsetActor(cfg, playlist, name, "ContactSheet") { init(); } @@ -40,92 +36,100 @@ void ContactSheetActor::init() { // here we join our own events channel. The 'change_event_group' just emits // change events when the underlying container (i.e. the SubSet base class) // changes, i.e. when media is added, removed or re-ordered - request(caf::actor_cast(this), infinite, get_change_event_group_atom_v).then( - [=](caf::actor grp) { - join_broadcast(this, grp); - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + request(caf::actor_cast(this), infinite, get_change_event_group_atom_v) + .then( + [=](caf::actor grp) { join_broadcast(this, grp); }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); - override_behaviour_ = caf::message_handler { + override_behaviour_ = caf::message_handler{ - [=](playlist::create_playhead_atom) -> UuidActor { + [=](duplicate_atom) -> result { + // clone ourself.. + auto actor = + spawn(caf::actor_cast(playlist_), base_.name()); + anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + // get uuid from actor.. + try { + caf::scoped_actor sys(system()); + // get uuid.. + Uuid uuid = request_receive(*sys, actor, utility::uuid_atom_v); + + // maybe not be safe.. as ordering isn't implicit.. + std::vector media_actors; + for (const auto &i : base_.media()) + media_actors.emplace_back(UuidActor(i, actors_[i])); + + request_receive( + *sys, actor, playlist::add_media_atom_v, media_actors, Uuid()); + + return UuidActor(uuid, actor); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return make_error(xstudio_error::error, "Invalid uuid"); + }, + [=](playlist::create_playhead_atom) -> UuidActor { if (playhead_) return playhead_; auto uuid = utility::Uuid::generate(); auto actor = spawn( std::string("Contact Sheet Playhead"), + playhead::GLOBAL_AUDIO, selection_actor_, uuid, caf::actor_cast(this)); link_to(actor); anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); - + playhead_ = UuidActor(uuid, actor); - // now get the playhead to load our media - request(caf::actor_cast(this), infinite, playlist::get_media_atom_v).then( - [=](const UuidActorVector &media) mutable { - request(playhead_.actor(), infinite, playhead::source_atom_v, media).then( - [=](bool) mutable { - - // restore the playhead state, if we have serialisation data - if (!playhead_serialisation_.is_null()) { - anon_send(playhead_.actor(), module::deserialise_atom_v, playhead_serialisation_); - } else { - anon_send(playhead_.actor(), playhead::compare_mode_atom_v, "Grid"); - } - - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + // now get the playhead to load our media + request(caf::actor_cast(this), infinite, playlist::get_media_atom_v) + .then( + [=](const UuidActorVector &media) mutable { + request(playhead_.actor(), infinite, playhead::source_atom_v, media) + .then( + [=](bool) mutable { + // restore the playhead state, if we have serialisation data + if (!playhead_serialisation_.is_null()) { + anon_send( + playhead_.actor(), + module::deserialise_atom_v, + playhead_serialisation_); + } else { + anon_send( + playhead_.actor(), + playhead::compare_mode_atom_v, + "Grid"); + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); return playhead_; - }, [=](utility::event_atom, utility::change_atom) { - if (!playhead_) return; - request(caf::actor_cast(this), infinite, playlist::get_media_atom_v).then( - [=](const UuidActorVector &media) mutable { - anon_send(playhead_.actor(), playhead::source_atom_v, media); - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - - }, - [=](utility::serialise_atom) -> result { - auto rp = make_response_promise(); - JsonStore j; - j["base"] = SubsetActor::serialise(); - if (playhead_) { - request(playhead_.actor(), infinite, utility::serialise_atom_v).then( - [=](const utility::JsonStore &playhead_state) mutable { - playhead_serialisation_ = playhead_state; - j["playhead"] = playhead_state; - rp.deliver(j); + if (!playhead_) + return; + request(caf::actor_cast(this), infinite, playlist::get_media_atom_v) + .then( + [=](const UuidActorVector &media) mutable { + anon_send(playhead_.actor(), playhead::source_atom_v, media); }, - [=](caf::error &err) mutable { + [=](caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(j); }); - - } else { - if (!playhead_serialisation_.is_null()) { - j["playhead"] = playhead_serialisation_; - } - rp.deliver(j); - } - return rp; - } - }; + }}; } \ 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 9173577bc..c7806c269 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -121,8 +121,8 @@ bool EmbeddedPython::connect(caf::actor actor) { // spdlog::warn("connect 1.5"); auto pyactor = py::cast(actor); // spdlog::warn("connect 2.5"); - auto local = py::dict("actor"_a = pyactor); - + py::dict local; + local["actor"] = pyactor; // spdlog::warn("connect 2"); exec(R"( XSTUDIO = Connection( @@ -211,13 +211,17 @@ bool EmbeddedPython::remove_session(const utility::Uuid &session_uuid) { bool EmbeddedPython::input_session( const utility::Uuid &session_uuid, const std::string &input) { if (sessions_.count(session_uuid)) { - std::string clean_input = replace_all(input, "'", R"(\')"); + std::string clean_input = rtrim(input); py::object scope = py::module::import("__main__").attr("__dict__"); py::exec( - "xstudio_sessions['" + to_string(session_uuid) + "'].interact_more('" + - rtrim(clean_input) + "')", + "xstudio_sessions['" + to_string(session_uuid) + "'].interact_more(\"\"\"" + + clean_input + "\"\"\")", scope); return true; + } else if (session_uuid.is_null()) { + std::string clean_input = rtrim(input); + py::object scope = py::module::import("__main__").attr("__dict__"); + py::exec(clean_input, scope); } return false; @@ -299,6 +303,22 @@ void EmbeddedPython::add_message_callback(const py::tuple &cb_particulars) { } } +void EmbeddedPython::register_python_plugin_instance(const py::tuple &cb_particulars) { + + if (cb_particulars.size() == 2) { + + auto i = cb_particulars.begin(); + auto plugin_object_instace = (*i).cast(); + i++; + auto plugin_instace_uuid = (*i).cast(); + plugin_registry_[plugin_instace_uuid] = plugin_object_instace; + + } else { + throw std::runtime_error("register_python_plugin_instance expecting tuple of size 2 " + "(plugin_object, plugin_uuid)."); + } +} + void EmbeddedPython::remove_message_callback(const py::tuple &cb_particulars) { try { @@ -385,6 +405,20 @@ void EmbeddedPython::run_callback(const utility::Uuid &id) { } } +utility::JsonStore EmbeddedPython::run_plugin_callback( + const utility::Uuid &plugin_uuid, + const std::string method_name, + const utility::JsonStore &packed_args) { + if (plugin_registry_.find(plugin_uuid) == plugin_registry_.end()) { + + throw std::runtime_error("Callback request supplied with python plugin UUID that is " + "not in plugin registry."); + } + py::object result = + plugin_registry_[plugin_uuid].attr("run_callback_func")(method_name, packed_args); + return result.cast(); +} + void EmbeddedPython::s_add_message_callback(const py::tuple &cb_particulars) { if (s_instance_) { s_instance_->add_message_callback(cb_particulars); @@ -403,12 +437,19 @@ void EmbeddedPython::s_run_callback_with_delay(const py::tuple &delayed_cb_args) } } +void EmbeddedPython::s_register_python_plugin_instance(const py::tuple &args) { + if (s_instance_) { + s_instance_->register_python_plugin_instance(args); + } +} PYBIND11_EMBEDDED_MODULE(XStudioExtensions, m) { // `m` is a `py::module_` which is used to bind functions and classes m.def("run_callback_with_delay", &EmbeddedPython::s_run_callback_with_delay); m.def("add_message_callback", &EmbeddedPython::s_add_message_callback); m.def("remove_message_callback", &EmbeddedPython::s_remove_message_callback); + m.def( + "register_python_plugin_instance", &EmbeddedPython::s_register_python_plugin_instance); py::class_(m, "CafMessage", py::module_local()); } diff --git a/src/embedded_python/src/embedded_python_actor.cpp b/src/embedded_python/src/embedded_python_actor.cpp index b1cba930c..2be6279a0 100644 --- a/src/embedded_python/src/embedded_python_actor.cpp +++ b/src/embedded_python/src/embedded_python_actor.cpp @@ -90,6 +90,19 @@ void send_output( } } +void py_print(const std::string &e) { + // Note . on Windows, using py::arg or _a literal is causing seg-fault + // but using a dict to set kwargs on py::print works fine. + auto py_stderr = py::module::import("sys").attr("stderr").cast(); +#ifdef _WIN32 + py::dict d; + d["file"] = py_stderr; + py::print(e, d); +#else + py::print(e, "file"_a = py_stderr); +#endif +} + class PyObjectRef { public: PyObjectRef(PyObject *obj) : obj_(obj) { @@ -176,9 +189,7 @@ void EmbeddedPythonActor::act() { } } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -186,8 +197,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -209,9 +219,7 @@ void EmbeddedPythonActor::act() { } } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -219,8 +227,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -231,6 +238,131 @@ void EmbeddedPythonActor::act() { return result; }, + [=](session::export_atom) -> result> { + // get otio supported export formats. + auto result = std::vector({"otio"}); + + if (not base_.enabled()) + return make_error(xstudio_error::error, "EmbeddedPython disabled"); + + std::string error; + + try { + PyStdErrOutStreamRedirect out{}; + try { + // Import the OTIO Python module. + auto p_module = + PyObjectRef(PyImport_ImportModule("opentimelineio.adapters")); + auto p_suffixes_with_defined_adapters = PyObjectRef( + PyObject_GetAttrString(p_module, "suffixes_with_defined_adapters")); + auto p_suffixes_with_defined_adapters_args = PyObjectRef(PyTuple_New(2)); + + PyTuple_SetItem(p_suffixes_with_defined_adapters_args, 0, Py_False); + PyTuple_SetItem(p_suffixes_with_defined_adapters_args, 1, Py_True); + + auto p_suffixes = PyObjectRef(PyObject_CallObject( + p_suffixes_with_defined_adapters, + p_suffixes_with_defined_adapters_args)); + + PyObject *repr = PyObject_Repr(p_suffixes); + if (repr) { + PyObject *str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + if (str) { + const char *bytes = PyBytes_AS_STRING(str); + + for (const auto &i : + resplit(std::string(bytes), std::regex{"\\{'|'\\}|',\\s'"})) { + if (not i.empty()) + result.push_back(i); + } + // something like .. {'otiod', 'edl', 'mb', 'ma', 'kdenlive', + // 'otio', 'otioz', 'ale', 'm3u8', 'xml', 'aaf', 'fcpxml', 'svg', + // 'xges', 'rv'} + + Py_XDECREF(str); + } + Py_XDECREF(repr); + } + + std::sort(result.begin(), result.end()); + return result; + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + // send_output(this, event_group_, out, uuid); + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + + return make_error(xstudio_error::error, error); + }, + + [=](session::export_atom, + const std::string &otio_str, + const caf::uri &upath, + const std::string &type) -> result { + // get otio supported export formats. + if (not base_.enabled()) + return make_error(xstudio_error::error, "EmbeddedPython disabled"); + + std::string error; + + try { + PyStdErrOutStreamRedirect out{}; + otio::ErrorStatus error_status; + // Import the OTIO Python module. + auto p_module = PyObjectRef(PyImport_ImportModule("opentimelineio.adapters")); + auto p_read_from_string = + PyObjectRef(PyObject_GetAttrString(p_module, "read_from_string")); + auto p_read_from_string_args = PyObjectRef(PyTuple_New(1)); + auto p_read_from_string_arg = + PyUnicode_FromStringAndSize(otio_str.c_str(), otio_str.size()); + if (not p_read_from_string_arg) + throw std::runtime_error("cannot create arg"); + PyTuple_SetItem(p_read_from_string_args, 0, p_read_from_string_arg); + auto p_timeline = PyObjectRef( + PyObject_CallObject(p_read_from_string, p_read_from_string_args)); + + auto path = uri_to_posix_path(upath); + + // Write the Python timeline. + auto p_write_to_file = + PyObjectRef(PyObject_GetAttrString(p_module, "write_to_file")); + auto p_write_to_file_args = PyObjectRef(PyTuple_New(2)); + auto p_write_to_file_arg = + PyUnicode_FromStringAndSize(path.c_str(), path.size()); + if (not p_write_to_file_arg) + throw std::runtime_error("cannot create arg"); + PyTuple_SetItem(p_write_to_file_args, 0, p_timeline); + PyTuple_SetItem(p_write_to_file_args, 1, p_write_to_file_arg); + PyObjectRef(PyObject_CallObject(p_write_to_file, p_write_to_file_args)); + + return true; + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + + return make_error(xstudio_error::error, error); + }, + // import otio file return as otio xml string. // if already native format should be quick.. [=](session::import_atom, const caf::uri &path) -> result { @@ -273,9 +405,7 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -285,9 +415,8 @@ void EmbeddedPythonActor::act() { // send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -310,9 +439,8 @@ void EmbeddedPythonActor::act() { return session_uuid; } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -338,9 +466,7 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -350,9 +476,8 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -371,8 +496,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval(pystring, locals)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -391,9 +515,7 @@ void EmbeddedPythonActor::act() { base_.eval_file(uri_to_posix_path(pyfile)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -402,8 +524,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -418,8 +539,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval_locals(pystring)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -438,8 +558,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval_locals(pystring, locals)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -458,9 +577,7 @@ void EmbeddedPythonActor::act() { base_.exec(pystring); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -469,8 +586,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -485,8 +601,7 @@ void EmbeddedPythonActor::act() { return base_.remove_session(uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -513,20 +628,21 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); - return make_error(xstudio_error::error, err.what()); + py_print(error); + // get console back to input mode.. + auto result = base_.input_session(uuid, ""); + send_output(this, event_group_, out, uuid); + return result; + // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + // return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -551,9 +667,7 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -562,9 +676,8 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -631,6 +744,17 @@ void EmbeddedPythonActor::act() { [=](const utility::Uuid &cb_id) { base_.run_callback(cb_id); }, + [=](embedded_python::python_exec_atom, + const utility::Uuid &plugin_uuid, + const std::string method_name, + const utility::JsonStore &packed_args) -> result { + try { + return base_.run_plugin_callback(plugin_uuid, method_name, packed_args); + } catch (std::exception &e) { + return make_error(xstudio_error::error, e.what()); + } + }, + [&](exit_msg &em) { if (em.reason) { if (base_.enabled()) diff --git a/src/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index c46b817c9..f865fb553 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -37,7 +37,6 @@ target_link_libraries(${PROJECT_NAME} xstudio::scanner xstudio::session xstudio::studio - xstudio::sync xstudio::thumbnail xstudio::ui::model_data xstudio::ui::viewport diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index fc138990d..17e96dc08 100644 --- a/src/global/src/global_actor.cpp +++ b/src/global/src/global_actor.cpp @@ -25,7 +25,6 @@ #include "xstudio/plugin_manager/plugin_manager_actor.hpp" #include "xstudio/scanner/scanner_actor.hpp" #include "xstudio/studio/studio_actor.hpp" -#include "xstudio/sync/sync_actor.hpp" #include "xstudio/thumbnail/thumbnail_manager_actor.hpp" #include "xstudio/ui/model_data/model_data_actor.hpp" #include "xstudio/ui/viewport/keypress_monitor.hpp" @@ -48,6 +47,84 @@ using namespace xstudio::global; using namespace xstudio::utility; using namespace xstudio::global_store; +APIActor::APIActor(caf::actor_config &cfg, const caf::actor &global) + : caf::event_based_actor(cfg), global_(global) { + + spdlog::debug("Created APIActor"); + print_on_exit(this, "APIActor"); + + behavior_.assign( + make_get_version_handler(), + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](get_application_mode_atom) -> result { + // don't expose global.. + auto rp = make_response_promise(); + + request(global_, infinite, get_application_mode_atom_v) + .then( + [=](const std::string &result) mutable { rp.deliver(result); }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; + }, + + [=](authenticate_atom) -> result { + if (allow_unauthenticated_ or + (current_sender() and current_sender()->node() == node())) + return global_; + + return make_error(xstudio_error::error, "Authentication required."); + }, + + [=](authenticate_atom, const std::string &key) -> result { + if (allow_unauthenticated_) + return global_; + else + for (const auto &i : authentication_keys_) + if (key == i) + return global_; + + return make_error(xstudio_error::error, "Authentication failed."); + }, + + [=](authenticate_atom, + const std::string &user, + const std::string &pass) -> result { + if (allow_unauthenticated_) + return global_; + else + for (const auto &i : authentication_passwords_) + if (user == i.at("user") and pass == i.at("password")) + return global_; + + return make_error(xstudio_error::error, "Authentication failed."); + }, + + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) { + delegate(actor_cast(this), json_store::update_atom_v, full); + }, + + [=](json_store::update_atom, const JsonStore &j) mutable { + try { + allow_unauthenticated_ = + preference_value(j, "/core/api/authentication/allow_unauthenticated"); + + authentication_passwords_ = + preference_value(j, "/core/api/authentication/passwords"); + + authentication_keys_ = + preference_value(j, "/core/api/authentication/keys"); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }); +} + GlobalActor::GlobalActor(caf::actor_config &cfg, const utility::JsonStore &prefs) : caf::event_based_actor(cfg), rsm_(remote_session_path()) { init(prefs); @@ -69,7 +146,6 @@ int GlobalActor::publish_port( return port; } - void GlobalActor::init(const utility::JsonStore &prefs) { // launch global actors.. // preferences first.. @@ -92,11 +168,11 @@ void GlobalActor::init(const utility::JsonStore &prefs) { gsa = spawn("GlobalStore", prefs); } - auto sga = spawn(); - auto sgma = spawn(); + auto phev = spawn(); auto keyboard_events = spawn(); auto ui_models = spawn(); auto metadata_mgr = spawn(); + auto audio = spawn(); auto pm = spawn(); auto colour = spawn(); auto gmma = spawn(); @@ -106,8 +182,6 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto gcca = spawn(); auto gmha = spawn(); auto thumbnail = spawn(); - auto audio = spawn(); - auto phev = spawn(); auto pa = spawn("Python"); auto scanner = spawn(); auto conform = spawn(); @@ -128,8 +202,6 @@ void GlobalActor::init(const utility::JsonStore &prefs) { link_to(phev); link_to(pm); link_to(scanner); - link_to(sga); - link_to(sgma); link_to(metadata_mgr); link_to(thumbnail); link_to(ui_models); @@ -158,27 +230,24 @@ void GlobalActor::init(const utility::JsonStore &prefs) { port_maximum_ = 12345; bind_address_ = "127.0.0.1"; - sync_connected_ = false; - sync_api_enabled_ = false; - sync_port_ = -1; - sync_port_minimum_ = 12346; - sync_port_maximum_ = 12346; - sync_bind_address_ = "127.0.0.1"; - event_group_ = spawn(this); link_to(event_group_); + apia_ = spawn(this); + try { auto prefs = GlobalStoreHelper(system()); JsonStore j; join_broadcast(this, prefs.get_group(j)); // spawn resident plugins (application level) anon_send(pm, plugin_manager::spawn_plugin_atom_v, j); + anon_send(apia_, json_store::update_atom_v, j); anon_send(this, json_store::update_atom_v, j); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + behavior_.assign( make_get_version_handler(), [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -319,7 +388,8 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto prefs = global_store:: GlobalStoreHelper(system()); prefs.set_value( - fspath.string(), + to_string(posix_path_to_uri( + fspath.string())), "/core/session/autosave/" "last_auto_save"); prefs.save("APPLICATION"); @@ -366,8 +436,6 @@ void GlobalActor::init(const utility::JsonStore &prefs) { return studio_; }, - [=](get_api_mode_atom) -> std::string { return "FULL"; }, - [=](get_application_mode_atom) -> std::string { return (ui_studio_ ? "XSTUDIO_GUI" : "XSTUDIO"); }, @@ -402,8 +470,9 @@ void GlobalActor::init(const utility::JsonStore &prefs) { delegate(actor_cast(this), json_store::update_atom_v, full); }, - [=](json_store::update_atom, const JsonStore &j) mutable { + anon_send(apia_, json_store::update_atom_v, j); + try { python_enabled_ = preference_value(j, "/core/python/enabled"); api_enabled_ = preference_value(j, "/core/api/enabled"); @@ -411,14 +480,9 @@ void GlobalActor::init(const utility::JsonStore &prefs) { port_maximum_ = preference_value(j, "/core/api/port_maximum"); bind_address_ = preference_value(j, "/core/api/bind_address"); - sync_api_enabled_ = preference_value(j, "/core/sync/enabled"); - sync_port_minimum_ = preference_value(j, "/core/sync/port_minimum"); - sync_port_maximum_ = preference_value(j, "/core/sync/port_maximum"); - sync_bind_address_ = - preference_value(j, "/core/sync/bind_address"); - session_autosave_interval_ = preference_value(j, "/core/session/autosave/interval"); + try { session_autosave_path_ = posix_path_to_uri(expand_envvars( preference_value(j, "/core/session/autosave/path"))); @@ -437,9 +501,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { } disconnect_api(pa); - disconnect_sync_api(); connect_api(pa); - connect_sync_api(); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -454,7 +516,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { rsm_.remove_session(remote_api_session_name_); remote_api_session_name_ = session_name; rsm_.create_session_file( - port_, false, remote_api_session_name_, "localhost", true); + port_, remote_api_session_name_, "localhost", true); spdlog::info( "API enabled on {}:{}, session name {}", @@ -477,9 +539,9 @@ void GlobalActor::init(const utility::JsonStore &prefs) { delegate(studio_, _atom, path, js); }, - [=](bookmark::get_bookmark_atom atom) { delegate(studio_, atom); }, + [=](bookmark::get_bookmark_atom atom) { delegate(studio_, atom); } - [=](sync::get_sync_atom _atm) { delegate(sgma, _atm); }); + ); } void GlobalActor::on_exit() { @@ -487,26 +549,28 @@ void GlobalActor::on_exit() { // clear autosave. send(event_group_, exit_atom_v); auto prefs = global_store::GlobalStoreHelper(system()); - prefs.set_value("", "/core/session/autosave/last_auto_save", false); + // prefs.set_value("", "/core/session/autosave/last_auto_save", false); prefs.save("APPLICATION"); + if (system().has_middleman()) { - system().middleman().unpublish(actor_cast(this), port_); - system().middleman().unpublish(actor_cast(this), sync_port_); + system().middleman().unpublish(apia_, port_); } + send_exit(apia_, caf::exit_reason::user_shutdown); + apia_ = caf::actor(); + system().registry().erase(global_registry); system().registry().erase(pc_audio_output_registry); } void GlobalActor::connect_api(const caf::actor &embedded_python) { if (not connected_ and api_enabled_) { - port_ = publish_port(port_minimum_, port_maximum_, bind_address_, this); + port_ = publish_port(port_minimum_, port_maximum_, bind_address_, apia_); if (port_ != -1) { rsm_.remove_session(remote_api_session_name_); if (remote_api_session_name_.empty()) - remote_api_session_name_ = rsm_.create_session_file(port_, false); + remote_api_session_name_ = rsm_.create_session_file(port_); else - rsm_.create_session_file( - port_, false, remote_api_session_name_, "localhost", true); + rsm_.create_session_file(port_, remote_api_session_name_, "localhost", true); spdlog::info( "API enabled on {}:{}, session name {}", bind_address_, @@ -538,33 +602,6 @@ void GlobalActor::connect_api(const caf::actor &embedded_python) { } } -void GlobalActor::connect_sync_api() { - if (not sync_connected_ and sync_api_enabled_) { - sync_port_ = - publish_port(sync_port_minimum_, sync_port_maximum_, sync_bind_address_, this); - if (sync_port_ != -1) { - rsm_.remove_session(remote_sync_session_name_); - if (remote_sync_session_name_.empty()) - remote_sync_session_name_ = rsm_.create_session_file(sync_port_, true); - else - rsm_.create_session_file( - sync_port_, true, remote_sync_session_name_, "localhost", true); - spdlog::info( - "SYNC API enabled on {}:{}, session name {}", - sync_bind_address_, - sync_port_, - remote_sync_session_name_); - sync_connected_ = true; - } else { - spdlog::warn( - "SYNC API failed to open {}:{}-{}", - sync_bind_address_, - sync_port_minimum_, - sync_port_maximum_); - } - } -} - void GlobalActor::disconnect_api(const caf::actor &embedded_python, const bool force) { if (connected_ and (force or not api_enabled_ or port_ > port_maximum_ or port_ < port_minimum_)) { @@ -584,24 +621,10 @@ void GlobalActor::disconnect_api(const caf::actor &embedded_python, const bool f send(event_group_, api_exit_atom_v); // wait..? - system().middleman().unpublish(actor_cast(this), port_); + system().middleman().unpublish(apia_, port_); rsm_.remove_session(remote_api_session_name_); spdlog::info("API disabled on port {}", port_); } port_ = -1; } } - -void GlobalActor::disconnect_sync_api(const bool force) { - if (sync_connected_ and - (force or not sync_api_enabled_ or sync_port_ > sync_port_maximum_ or - sync_port_ < sync_port_minimum_)) { - sync_connected_ = false; - if (system().has_middleman()) { - system().middleman().unpublish(actor_cast(this), sync_port_); - rsm_.remove_session(remote_sync_session_name_); - spdlog::info("SYNC API disabled on port {}", sync_port_); - } - sync_port_ = -1; - } -} diff --git a/src/global_store/src/global_store_actor.cpp b/src/global_store/src/global_store_actor.cpp index 14e364a54..f0716e9a8 100644 --- a/src/global_store/src/global_store_actor.cpp +++ b/src/global_store/src/global_store_actor.cpp @@ -13,6 +13,53 @@ using namespace caf; namespace xstudio::global_store { +class GlobalStoreIOActor : public caf::event_based_actor { + public: + GlobalStoreIOActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) {} + const char *name() const override { return NAME.c_str(); } + + caf::message_handler message_handler() { + return caf::message_handler{ + [=](save_atom, + const JsonStore &data, + const std::string &path) -> caf::result { + try { + // check dir exists.. + std::ofstream o(path + ".tmp", std::ofstream::out | std::ofstream::trunc); + try { + o.exceptions(std::ios_base::failbit | std::ifstream::badbit); + + o << std::setw(4) << data.cref() << std::endl; + o.close(); + + fs::rename(path + ".tmp", path); + + spdlog::debug("Saved {}", path); + } catch (const std::exception &err) { + if (o.is_open()) { + o.close(); + fs::remove(path + ".tmp"); + } + return caf::result(make_error( + xstudio_error::error, + fmt::format("Failed to save {} {}", path, err.what()))); + } + } catch (const std::exception &err) { + return caf::result(make_error( + xstudio_error::error, + fmt::format("Failed to save {} {}", path, err.what()))); + } + return true; + }}; + } + + caf::behavior make_behavior() override { return message_handler(); } + + private: + inline static const std::string NAME = "GlobalStoreIOActor"; +}; + + GlobalStoreActor::GlobalStoreActor( caf::actor_config &cfg, const JsonStore &jsn, std::string reg_value) : caf::event_based_actor(cfg), @@ -102,12 +149,20 @@ caf::message_handler GlobalStoreActor::message_handler() { [=](json_store::update_atom, const JsonStore &json) { base_.preferences_.set(json); try { - auto tmp = preference_value( - base_.preferences_, "/core/global_store/autosave_interval"); - if (tmp != base_.autosave_interval_) { + if (auto tmp = preference_value( + base_.preferences_, "/core/global_store/autosave_interval"); + tmp != base_.autosave_interval_) { base_.autosave_interval_ = tmp; spdlog::debug("autosave updated {}", base_.autosave_interval_); } + + if (auto tmp = preference_value( + base_.preferences_, "/core/global_store/autosave_enable"); + tmp != base_.autosave_) { + base_.autosave_ = tmp; + spdlog::debug("autosave enable updated {}", base_.autosave_); + } + } catch (...) { } }, @@ -131,32 +186,9 @@ caf::message_handler GlobalStoreActor::message_handler() { JsonStore prefs = get_preference_values( base_.preferences_, std::set{context}, true, path); - try { - // check dir exists.. - std::ofstream o(path + ".tmp", std::ofstream::out | std::ofstream::trunc); - try { - o.exceptions(std::ios_base::failbit | std::ifstream::badbit); - - o << std::setw(4) << prefs.cref() << std::endl; - o.close(); - - fs::rename(path + ".tmp", path); - - spdlog::debug("Saved {} {}", context, path); - } catch (const std::exception &err) { - if (o.is_open()) { - o.close(); - fs::remove(path + ".tmp"); - } - return caf::result(make_error( - xstudio_error::error, - fmt::format("Failed to save {} {}", context, err.what()))); - } - } catch (const std::exception &err) { - return caf::result(make_error( - xstudio_error::error, - fmt::format("Failed to save {} {}", context, err.what()))); - } + auto rp = make_response_promise(); + rp.delegate(ioactor_, save_atom_v, prefs, path); + return rp; } else { return caf::result(make_error( @@ -222,10 +254,16 @@ void GlobalStoreActor::init() { base_.preferences_.set(std::get<2>(result)); base_.autosave_interval_ = preference_value(base_.preferences_, "/core/global_store/autosave_interval"); + base_.autosave_ = + preference_value(base_.preferences_, "/core/global_store/autosave_enable"); + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + ioactor_ = spawn(); + link_to(ioactor_); + system().registry().put(reg_value_, this); } diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index 2cafb2775..de15c8641 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -255,7 +255,7 @@ 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 user_prefs_off = {parser, "user-prefs-off", "Skip loading and saving of user preferences.", {"user-prefs-off"}}; args::Flag quick_view = { parser, "quick-view", @@ -349,7 +349,7 @@ struct Launcher { start_logger( cli_args.debug.Matched() ? spdlog::level::debug : spdlog::level::info, args::get(cli_args.logfile)); - prefs = load_preferences(); + prefs = load_preferences(cli_args.user_prefs_off.Matched()); scoped_actor self{system}; @@ -357,7 +357,7 @@ struct Launcher { actions["new_instance"] = cli_args.new_session.Matched(); actions["headless"] = cli_args.headless.Matched(); actions["debug"] = cli_args.debug.Matched(); - actions["player"] = cli_args.player.Matched(); + actions["user_prefs_off"] = cli_args.user_prefs_off.Matched(); actions["quick_view"] = cli_args.quick_view.Matched(); actions["disable_vsync"] = cli_args.disable_vsync.Matched(); actions["share_opengl_contexts"] = cli_args.share_opengl_contexts.Matched(); @@ -393,9 +393,9 @@ struct Launcher { // check for unassigned media. auto media = uri_params.equal_range("media"); if (media.first != uri_params.end()) { - actions["playlists"]["Untitled Playlist"] = nlohmann::json::array(); + actions["playlists"]["Added Media"] = nlohmann::json::array(); for (auto m = media.first; m != media.second; ++m) { - actions["playlists"]["Untitled Playlist"].push_back(m->second); + actions["playlists"]["Added Media"].push_back(m->second); } } @@ -434,7 +434,7 @@ struct Launcher { // check for media. auto playlist_name = args::get(cli_args.playlist_name).empty() - ? (actions["quick_view"] ? "QuickView Media" : "Untitled Playlist") + ? (actions["quick_view"] ? "QuickView Media" : "Added Media") : args::get(cli_args.playlist_name); actions["playlists"][playlist_name] = nlohmann::json::array(); if (not args::get(cli_args.media_paths).empty()) @@ -548,7 +548,7 @@ struct Launcher { // 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"]) { + if (p.key() == "Added Media" and not actions["new_instance"]) { try { playlist = request_receive( *self, session, session::active_media_container_atom_v); @@ -596,14 +596,16 @@ struct Launcher { } } - JsonStore load_preferences() { + JsonStore load_preferences(const bool skip_user_prefs) { std::vector pref_paths; for (const auto &p : args::get(cli_args.cli_pref_paths)) { pref_paths.push_back(p); } - for (const auto &i : global_store::PreferenceContexts) - pref_paths.push_back(preference_path_context(i)); + if (!skip_user_prefs) { + for (const auto &i : global_store::PreferenceContexts) + pref_paths.push_back(preference_path_context(i)); + } for (const auto &p : args::get(cli_args.cli_override_pref_paths)) { pref_paths.push_back(p); @@ -919,15 +921,19 @@ struct Launcher { // try open ? auto connecting = system.spawn(connect_to_remote); try { - auto a = request_receive_wait( + auto remote = request_receive_wait( *self, connecting, std::chrono::seconds(2), caf::connect_atom_v, host, port); + + auto auth = request_receive_wait( + *self, remote, std::chrono::seconds(2), authenticate_atom_v); + spdlog::info("Connected to session '{}' at {}:{}", name, host, port); - return a; + return auth; } catch (const std::exception &err) { spdlog::debug("Failed to connect '{}'", err.what()); } @@ -972,7 +978,7 @@ int main(int argc, char **argv) { // but buffers might be cleaned up (destroyed) after the media_reader component // is cleaned up on exit. So we make a copy here to ensure the ImageBufferRecyclerCache // instance outlives any Buffer objects. - + // auto buffer_cache_handle = media_reader::Buffer::s_buf_cache; // As far as I can tell caf only allows config to be modified @@ -987,16 +993,22 @@ int main(int argc, char **argv) { argv[0], "--caf.scheduler.max-threads=128", "--caf.scheduler.policy=sharing", - "--caf.logger.console.verbosity=trace"}; + "--caf.logger.console.verbosity=debug"}; caf_config config{4, const_cast(args)}; - config.add_actor_type("Gap"); + config.add_actor_type< + timeline::GapActor, + const std::string &, + const utility::FrameRateDuration &>("Gap"); config.add_actor_type("Stack"); config.add_actor_type( "Clip"); - config.add_actor_type( - "Track"); + config.add_actor_type< + timeline::TrackActor, + const std::string &, + const utility::FrameRate &, + const media::MediaType &>("Track"); { @@ -1200,8 +1212,6 @@ int main(int argc, char **argv) { engine.addImportPath("qrc:///"); engine.addImportPath("qrc:///extern"); - qDebug() << "FART " << QStringFromStd(xstudio_root("/plugin/qml")); - // gui plugins.. engine.addImportPath(QStringFromStd(xstudio_root("/plugin/qml"))); engine.addPluginPath(QStringFromStd(xstudio_root("/plugin"))); @@ -1235,9 +1245,8 @@ int main(int argc, char **argv) { // fingers crossed... // need to stop monitoring or we'll be sending events to a dead QtObject spdlog::get("xstudio")->sinks().pop_back(); - // save state.. BUT NOT WHEN USING PLAYER MODE.. (THIS needs work) - // if we store prefs then we affect the normal mode.. (DUAL mode for prefs ?) - if (not l.actions.value("player", false)) { + // save state.. BUT NOT WHEN user_prefs_off + if (not l.actions.value("user_prefs_off", false)) { for (const auto &context : global_store::PreferenceContexts) { try { request_receive( diff --git a/src/media/src/media.cpp b/src/media/src/media.cpp index e89e69e86..c7cb20b67 100644 --- a/src/media/src/media.cpp +++ b/src/media/src/media.cpp @@ -17,12 +17,11 @@ MediaKey::MediaKey( const int frame, const std::string &stream_id) : std::string(fmt::format( - fmt::runtime(key_format), - to_string(uri), - (frame == std::numeric_limits::min() ? 0 : frame), - stream_id)) -{ - hash_ = std::hash{}(static_cast(*this)); + fmt::runtime(key_format), + to_string(uri), + (frame == std::numeric_limits::min() ? 0 : frame), + stream_id)) { + hash_ = std::hash{}(static_cast(*this)); } Media::Media(const JsonStore &jsn) diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index ee4235903..645a946d5 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -122,7 +122,6 @@ caf::message_handler MediaActor::default_event_handler() { const std::tuple &) {}, [=](utility::event_atom, add_media_source_atom, const utility::UuidActorVector &) {}, [=](utility::event_atom, media_status_atom, const MediaStatus ms) {}, - [=](utility::event_atom, media_status_atom, const MediaStatus ms) {}, [=](json_store::update_atom, const JsonStore &, const std::string &, @@ -669,7 +668,6 @@ caf::message_handler MediaActor::message_handler() { [=](get_media_pointer_atom atom, const MediaType media_type, const int logical_frame) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current(media_type))) return make_error(xstudio_error::error, "No MediaSources"); auto rp = make_response_promise(); @@ -683,13 +681,16 @@ caf::message_handler MediaActor::message_handler() { const MediaType media_type, const utility::TimeSourceMode tsm, const utility::FrameRate &override_rate) -> caf::result { - auto rp = make_response_promise(); - if (base_.empty() or not media_sources_.count(base_.current(media_type))) return make_error(xstudio_error::error, "No MediaSources"); + auto rp = make_response_promise(); rp.delegate( - media_sources_.at(base_.current(media_type)), atom, media_type, tsm, override_rate); - + media_sources_.at(base_.current(media_type)), + atom, + media_type, + tsm, + override_rate); + return rp; }, @@ -740,16 +741,14 @@ caf::message_handler MediaActor::message_handler() { utility::Uuid()); }, - [=](utility::rate_atom, - const media::MediaType media_type) -> caf::result { - + [=](utility::rate_atom, const media::MediaType media_type) -> caf::result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, media_reference_atom_v, media_type).then( - [=](const MediaReference &ref) mutable { - rp.deliver(ref.rate()); - }, - [=](error &err) mutable { rp.deliver(err); }); - + request( + caf::actor_cast(this), infinite, media_reference_atom_v, media_type) + .then( + [=](const MediaReference &ref) mutable { rp.deliver(ref.rate()); }, + [=](error &err) mutable { rp.deliver(err); }); + return rp; }, @@ -759,9 +758,7 @@ caf::message_handler MediaActor::message_handler() { return make_error(xstudio_error::error, "No MediaSources"); auto rp = make_response_promise(); - rp.delegate( - media_sources_.at(base_.current(media_type)), - atom); + rp.delegate(media_sources_.at(base_.current(media_type)), atom); return rp; }, @@ -1174,9 +1171,10 @@ caf::message_handler MediaActor::message_handler() { [=](bool) mutable { // ensures media sources have had their details filled in and // we've set the media sources (image and audio) where possible - if (base_.empty() or not media_sources_.count(base_.current())) - return rp.deliver( - make_error(xstudio_error::error, "No MediaSources")); + if (base_.empty() or not media_sources_.count(base_.current())) { + rp.deliver(make_error(xstudio_error::error, "No MediaSources")); + return; + } rp.delegate(media_sources_.at(base_.current()), atom, default_rate); }, @@ -1997,7 +1995,11 @@ void MediaActor::display_info_item( }; const std::string data_type = item_query_info.value("data_type", ""); - if (data_type == "metadata") { + if (data_type == "flag") { + + rp.deliver(JsonStore(base_.flag())); + + } else if (data_type == "metadata") { const std::string metadata_path = item_query_info.value("metadata_path", ""); diff --git a/src/media/src/media_source_actor.cpp b/src/media/src/media_source_actor.cpp index 3cb3f2f94..7555c6fd2 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -54,6 +54,8 @@ MediaSourceActor::MediaSourceActor(caf::actor_config &cfg, const JsonStore &jsn) utility::Uuid::generate(), static_cast(jsn["store"]), std::chrono::milliseconds(50)); + // assuming metadata had been loaded and stored before serilisation + media_metadata_up_to_date_ = true; } link_to(json_store_); join_event_group(this, json_store_); @@ -520,7 +522,6 @@ caf::message_handler MediaSourceActor::message_handler() { [=](get_media_pointer_atom atom, const MediaType media_type, const int logical_frame) -> caf::result { - auto rp = make_response_promise(); request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) .then( @@ -531,51 +532,63 @@ caf::message_handler MediaSourceActor::message_handler() { auto dur = base_.media_reference(base_.current(media_type)).duration(); LogicalFrameRanges ranges; ranges.emplace_back(logical_frame, logical_frame); - request(caf::actor_cast(this), infinite, get_media_pointers_atom_v, media_type, ranges).then( - [=](const media::AVFrameIDs &ids) mutable{ - if (ids.size() && ids[0]) { - rp.deliver(*(ids[0])); - } else { - rp.deliver(make_error(xstudio_error::error, "No frame")); - } - }, - [=](const error &err) mutable { rp.deliver(err); }); + request( + caf::actor_cast(this), + infinite, + get_media_pointers_atom_v, + media_type, + ranges) + .then( + [=](const media::AVFrameIDs &ids) mutable { + if (ids.size() && ids[0]) { + rp.deliver(*(ids[0])); + } else { + rp.deliver( + make_error(xstudio_error::error, "No frame")); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; - }, [=](get_media_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, acquire_media_detail_atom_v) .then( [=](bool) mutable { if (base_.current(media_type).is_null()) { rp.deliver(make_error(xstudio_error::error, "No streams")); + return; } - const auto dur = base_.media_reference(base_.current(media_type)).duration(); + const auto dur = + base_.media_reference(base_.current(media_type)).duration(); LogicalFrameRanges ranges; - ranges.emplace_back(0, dur.frames()-1); - request(caf::actor_cast(this), infinite, atom, media_type, ranges).then( - [=](const media::AVFrameIDs ids) mutable { - media::FrameTimeMap * result = new media::FrameTimeMap; - timebase::flicks t(0); - for (const auto &fid: ids) { - (*result)[t] = fid; - t += dur.rate().to_flicks(); - } - rp.deliver(media::FrameTimeMapPtr(result)); - }, - [=](const error &err) mutable { rp.deliver(err); }); + ranges.emplace_back(0, dur.frames() - 1); + request( + caf::actor_cast(this), + infinite, + atom, + media_type, + ranges) + .then( + [=](const media::AVFrameIDs ids) mutable { + media::FrameTimeMap *result = new media::FrameTimeMap; + timebase::flicks t(0); + for (const auto &fid : ids) { + (*result)[t] = fid; + t += dur.rate().to_flicks(); + } + rp.deliver(media::FrameTimeMapPtr(result)); + }, + [=](const error &err) mutable { rp.deliver(err); }); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; - }, [=](get_media_pointers_atom, @@ -921,6 +934,9 @@ caf::message_handler MediaSourceActor::message_handler() { }, [=](media_metadata::get_metadata_atom) -> caf::result { + if (media_metadata_up_to_date_) + return true; + auto m_actor = system().registry().template get(media_metadata_registry); if (not m_actor) @@ -952,6 +968,7 @@ caf::message_handler MediaSourceActor::message_handler() { true) .then( [=](const bool &done) mutable { + media_metadata_up_to_date_ = true; rp.deliver(done); // notify any watchers that metadata is updated send( @@ -1014,6 +1031,7 @@ caf::message_handler MediaSourceActor::message_handler() { "/metadata/media/@") .then( [=](const bool &done) mutable { + media_metadata_up_to_date_ = true; rp.deliver(done); // notify any watchers that metadata is updated send( @@ -1295,7 +1313,6 @@ void MediaSourceActor::get_media_pointers_for_frames( get_stream_detail_atom_v) .then( [=](const StreamDetail &detail) mutable { - media::AVFrameIDs result; media::AVFrameID base_frame_id; auto timecode = @@ -1349,14 +1366,13 @@ void MediaSourceActor::get_media_pointers_for_frames( media_type, timecode); } - - result.emplace_back( - new media::AVFrameID( - base_frame_id, - *_uri, - frame, - detail.key_format_, - timecode)); + + result.emplace_back(new media::AVFrameID( + base_frame_id, + *_uri, + frame, + detail.key_format_, + timecode)); timecode = timecode + 1; diff --git a/src/media_reader/src/media_reader.cpp b/src/media_reader/src/media_reader.cpp index 9ef23c631..dc5cb82a8 100644 --- a/src/media_reader/src/media_reader.cpp +++ b/src/media_reader/src/media_reader.cpp @@ -156,15 +156,15 @@ xstudio::media_reader::byte *ImageBuffer::allocate(const size_t _size) { } void ImageBufDisplaySet::finalise() { - + utility::JsonStore image_info = nlohmann::json::parse("[]"); - images_hash_ = 0; + images_hash_ = 0; for (int i = 0; i < num_onscreen_images(); ++i) { - const auto & im = onscreen_image(i); + const auto &im = onscreen_image(i); nlohmann::json r; if (im) { r["image_size_in_pixels"] = im->image_size_in_pixels(); - //r["image_pixels_bounding_box"] = im->image_pixels_bounding_box(); + // r["image_pixels_bounding_box"] = im->image_pixels_bounding_box(); r["pixel_aspect"] = im->pixel_aspect(); images_hash_ += im.frame_id().key().hash(); } @@ -172,11 +172,10 @@ void ImageBufDisplaySet::finalise() { } as_json_.clear(); - as_json_["image_info"] = image_info; - as_json_["hero_image_index"] = hero_sub_playhead_index_; + as_json_["image_info"] = image_info; + as_json_["hero_image_index"] = hero_sub_playhead_index_; as_json_["prev_hero_image_index"] = previous_hero_sub_playhead_index_; - hash_ = int64_t(std::hash{}(as_json_.dump())); - + hash_ = int64_t(std::hash{}(as_json_.dump())); } MediaReader::MediaReader(std::string name, const utility::JsonStore &) diff --git a/src/media_reader/src/media_reader_actor.cpp b/src/media_reader/src/media_reader_actor.cpp index 6d296dbac..d8c4c8b39 100644 --- a/src/media_reader/src/media_reader_actor.cpp +++ b/src/media_reader/src/media_reader_actor.cpp @@ -254,7 +254,8 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( [=](get_image_atom, const media::AVFrameID &mptr, - const bool pin, // stamp the frame 10 minutes in the future so it sticks in the cache + const bool + pin, // stamp the frame 10 minutes in the future so it sticks in the cache const utility::Uuid &playhead_uuid, const timebase::flicks plahead_position) -> result { auto rp = make_response_promise(); @@ -292,7 +293,11 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( } else { // request new reader instance. request( - pool_, infinite, get_reader_atom_v, mptr.uri(), mptr.reader()) + pool_, + infinite, + get_reader_atom_v, + mptr.uri(), + mptr.reader()) .then( [=](caf::actor &new_reader) mutable { new_reader = add_reader( @@ -366,8 +371,7 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( caf::actor playhead, const utility::Uuid playhead_uuid, const utility::time_point &tp, - const timebase::flicks playhead_position - ) { + const timebase::flicks playhead_position) { request(image_cache_, infinite, media_cache::retrieve_atom_v, mptr.key()) .then( [=](const media_reader::ImageBufPtr &buf) mutable { @@ -388,7 +392,11 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( } else { // get reader.. request( - pool_, infinite, get_reader_atom_v, mptr.uri(), mptr.reader()) + pool_, + infinite, + get_reader_atom_v, + mptr.uri(), + mptr.reader()) .then( [=](caf::actor &new_reader) mutable { new_reader = add_reader( @@ -408,7 +416,13 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( media_reader::ImageBufPtr buf( new media_reader::ImageBuffer(to_string(err))); - send(playhead, push_image_atom_v, buf, mptr, tp, playhead_position); + send( + playhead, + push_image_atom_v, + buf, + mptr, + tp, + playhead_position); }); } } @@ -457,7 +471,6 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( const media::AVFrameIDsAndTimePoints media_ptrs, const Uuid &playhead_uuid, const media::MediaType mt) -> result { - // we've received fresh lookahead read requests from the playhead // during playback. We want to ask the cache actors if they already // have those frames, and if not we need to queue read requests to @@ -478,8 +491,8 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // clear all pending requests playback_precache_request_queue_.clear_pending_requests( playhead_uuid); - background_precache_request_queue_ - .clear_pending_requests(playhead_uuid); + background_precache_request_queue_.clear_pending_requests( + playhead_uuid); playback_precache_request_queue_.add_frame_requests( media_ptrs_not_in_image_cache, playhead_uuid); @@ -507,8 +520,8 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // clear all pending requests playback_precache_request_queue_.clear_pending_requests( playhead_uuid); - background_precache_request_queue_ - .clear_pending_requests(playhead_uuid); + background_precache_request_queue_.clear_pending_requests( + playhead_uuid); playback_precache_request_queue_.add_frame_requests( media_ptrs_not_in_audio_cache, playhead_uuid); @@ -531,10 +544,12 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( auto rp = make_response_promise(); auto tt = utility::clock::now(); - // we've been told to start background cacheing, so assume playback - // read-ahead can be cancelled. Keep a note of the timepoint of the - // first frame in the request queue - anything with an older time - // in the cache can be discarded when the cache is full + // we've been told to start background cacheing - this should only happen + // when playback has halted and the user isn't actively scrubbing the + // playhead, so we assume playback read-ahead can be cancelled. + // We keep a note of the timepoint of the first frame in the request + // queue - anything with an older time in the cache can be discarded + // when the cache is full if (mptrs.size()) { request(image_cache_, infinite, media_cache::unpreserve_atom_v, playhead_uuid) .then( @@ -770,7 +785,8 @@ void GlobalMediaReaderActor::do_precache() { continue_precacheing(); } else { try { - auto reader = get_reader(mptr->uri(), mptr->actor_addr(), mptr->reader()); + auto reader = + get_reader(mptr->uri(), mptr->actor_addr(), mptr->reader()); if (not reader) { mark_playhead_received_precache_result(playhead_uuid); continue_precacheing(); @@ -936,7 +952,8 @@ void GlobalMediaReaderActor::read_and_cache_audio( } else { continue_precacheing(); if (is_background_cache) { - // keep_cache_hot(mptr.key(), predicted_time, playhead_uuid); + // keep_cache_hot(mptr.key(), predicted_time, + // playhead_uuid); } } }, diff --git a/src/module/src/module.cpp b/src/module/src/module.cpp index f5e50fbb5..a60b11155 100644 --- a/src/module/src/module.cpp +++ b/src/module/src/module.cpp @@ -117,19 +117,6 @@ void Module::set_parent_actor_addr(caf::actor_addr addr) { } }); } - /*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) { @@ -878,7 +865,7 @@ caf::message_handler Module::message_handler() { event["context"] = context; // random num ensures the data changes every time // so notification mechanism is triggered - event["id"] = (double)rand() / RAND_MAX; + event["id"] = (double)rand() / RAND_MAX; attr->set_role_data(Attribute::UserData, event); } } @@ -2146,6 +2133,13 @@ Attribute *Module::add_attribute( attr = static_cast( add_string_attribute(title, title, value.get())); + } else if ( + value.is_array() && value.size() == 5 && value[0].is_string() && + value[0].get() == "colour") { + + auto c = value.get(); + attr = static_cast(add_colour_attribute(title, title, c)); + } else if (value.is_object() || value.is_null()) { attr = static_cast(add_json_attribute(title, nlohmann::json("{}"))); @@ -2228,6 +2222,7 @@ void Module::connect_to_viewport( void Module::add_attribute(Attribute *attr) { attributes_.emplace_back(attr); + attr->set_role_data(module::Attribute::ModuleUuid, uuid(), false); attr->set_owner(this); } diff --git a/src/playhead/src/playhead.cpp b/src/playhead/src/playhead.cpp index 070437fee..14d31209c 100644 --- a/src/playhead/src/playhead.cpp +++ b/src/playhead/src/playhead.cpp @@ -133,15 +133,11 @@ void PlayheadBase::add_attributes() { source_offset_frames_ = add_integer_attribute("Source Offset Frames", "Source Offset Frames", 0); - timeline_mode_ = - add_boolean_attribute("Timeline Mode", "Timeline Mode", false); + timeline_mode_ = add_boolean_attribute("Timeline Mode", "Timeline Mode", false); // Compare mode needs custom QML code for instatiation into the toolbar as // the choices are determined through viewport layout plugins - compare_mode_ = add_string_attribute( - "Compare", - "Compare", - "Off"); + compare_mode_ = add_string_attribute("Compare", "Compare", "Off"); compare_mode_->set_tool_tip("Access compare mode controls"); compare_mode_->set_role_data(module::Attribute::Type, "QmlCode"); compare_mode_->set_role_data( @@ -149,30 +145,43 @@ void PlayheadBase::add_attributes() { R"(import xStudio 1.0 XsViewerCompareModeButton {})"); compare_mode_->set_role_data(module::Attribute::ToolbarPosition, 9.0f); - } JsonStore PlayheadBase::serialise() const { JsonStore jsn; - jsn["name"] = Module::name(); - jsn["velocity"] = velocity_->value(); - jsn["position"] = position_.count(); - jsn["compare_mode"] = compare_mode_->value(); - jsn["auto_align_mode"] = auto_align_mode_->value(); - jsn["source_alignment_values"] = source_alignment_values_->value(); + jsn["name"] = Module::name(); + jsn["velocity"] = velocity_->value(); + jsn["position"] = position_.count(); + jsn["compare_mode"] = compare_mode_->value(); + jsn["auto_align_mode"] = auto_align_mode_->value(); + jsn["source_alignment_values"] = source_alignment_values_->value(); + jsn["loop_range_enabled"] = loop_range_enabled_->value(); + jsn["loop_mode"] = loop_mode_->value(); + jsn["loop_start"] = loop_start_.count(); + jsn["loop_end"] = loop_end_.count(); + return jsn; } void PlayheadBase::deserialise(const JsonStore &jsn) { - if (jsn.is_null()) return; + if (jsn.is_null()) + return; velocity_->set_value(jsn.value("velocity", velocity_->value())); - compare_mode_->set_value(jsn.value("compare_mode", compare_mode_->value())); - auto_align_mode_->set_value(jsn.value("auto_align_mode", auto_align_mode_->value())); - source_alignment_values_->set_value(jsn.value("source_alignment_values", source_alignment_values_->value())); - position_ = timebase::flicks(jsn.value("position", position_.count())); + compare_mode_->set_value(jsn.value("compare_mode", compare_mode_->value()), false); + auto_align_mode_->set_value(jsn.value("auto_align_mode", auto_align_mode_->value()), false); + source_alignment_values_->set_value( + jsn.value("source_alignment_values", source_alignment_values_->value())); + position_ = timebase::flicks(jsn.value("position", position_.count())); + loop_start_ = timebase::flicks(jsn.value("loop_start", loop_start_.count())); + loop_end_ = timebase::flicks(jsn.value("loop_end", loop_end_.count())); + loop_mode_->set_value(jsn.value("loop_mode", loop_mode_->value())); + loop_range_enabled_->set_value( + jsn.value("loop_range_enabled", loop_range_enabled_->value())); + + deserialised_ = true; } PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { @@ -326,18 +335,17 @@ void PlayheadBase::register_hotkeys() { false, "Playback"); - jump_to_next_clip_ = register_hotkey( - "Shift+Right", - "Jump to Next Clip", - "Jump the playhead to the start of the next clip in a timeline.", + cycle_image_layer_up_ = register_hotkey( + "Ctrl+Up", + "Cycle Image Layer / EXR Part (Up)", + "Cycle backwards through image layers (EXR parts) for the current on-screen source", false, "Playback"); - jump_to_previous_clip_ = register_hotkey( - "Shift+Left", - "Jump to Previous Clip", - "Jump the playhead to the start of the current clip, or the start of the previous " - "clip.", + cycle_image_layer_down_ = register_hotkey( + "Ctrl+Down", + "Cycle Image Layer / EXR Part (Down)", + "Cycle forwards through image layers (EXR parts) for the current on-screen source", false, "Playback"); } @@ -566,8 +574,8 @@ timebase::flicks PlayheadBase::clamp_timepoint_to_loop_range(const timebase::fli return rt; } -void PlayheadBase::set_position(const timebase::flicks p) { - position_ = p; +void PlayheadBase::set_position(const timebase::flicks p) { + position_ = p; position_set_tp_ = utility::clock::now(); } @@ -659,12 +667,10 @@ AutoAlignMode PlayheadBase::auto_align_mode() const { } -void PlayheadBase::set_assembly_mode(const AssemblyMode mode) { - assembly_mode_ = mode; -} +void PlayheadBase::set_assembly_mode(const AssemblyMode mode) { assembly_mode_ = mode; } void PlayheadBase::set_auto_align_mode(const AutoAlignMode mode) { - for (auto &a: auto_align_mode_names) { + for (auto &a : auto_align_mode_names) { if (std::get<0>(a) == mode) { auto_align_mode_->set_value(std::get<1>(a)); break; diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index 51ccd14bf..f0f203240 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -76,7 +76,7 @@ class PlayLoopActor : public caf::event_based_actor { std::vector to_actor_vector(const utility::UuidActorVector &v) { std::vector result; - for (const auto &a: v) { + for (const auto &a : v) { result.push_back(a.actor()); } return result; @@ -84,7 +84,7 @@ std::vector to_actor_vector(const utility::UuidActorVector &v) { utility::UuidVector to_uuid_vector(const utility::UuidActorVector &v) { utility::UuidVector result; - for (const auto &a: v) { + for (const auto &a : v) { result.push_back(a.uuid()); } return result; @@ -101,30 +101,35 @@ bool check_actor_down(caf::actor actor_down, utility::UuidActor &v) { bool check_actor_down(caf::actor actor_down, utility::UuidActorVector &v) { const size_t sz = v.size(); - auto p = v.begin(); + auto p = v.begin(); while (p != v.end()) { - if (p->actor() == actor_down) p = v.erase(p); - else p++; + if (p->actor() == actor_down) + p = v.erase(p); + else + p++; } return sz != v.size(); - } -} +} // namespace PlayheadActor::PlayheadActor( caf::actor_config &cfg, const std::string &name, + const AudioPath audio_path, caf::actor playlist_selection, const utility::Uuid uuid, caf::actor_addr parent_playlist) : caf::event_based_actor(cfg), PlayheadBase(name, std::move(uuid)), + audio_path_(audio_path), parent_playlist_(parent_playlist) { init(); set_parent_actor_addr(actor_cast(this)); - connect_to_playlist_selection_actor(playlist_selection); + playlist_selection_addr_ = caf::actor_cast(playlist_selection); + anon_send( + actor_cast(this), playlist::selection_actor_atom_v, 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 @@ -143,14 +148,9 @@ PlayheadActor::PlayheadActor( make_source_menu_model(); } -PlayheadActor::~PlayheadActor() { -} +PlayheadActor::~PlayheadActor() {} -void PlayheadActor::on_exit() { - - parent_actor_exiting(); - -} +void PlayheadActor::on_exit() { parent_actor_exiting(); } void PlayheadActor::init() { // get global reader and steal mrm.. @@ -160,13 +160,15 @@ void PlayheadActor::init() { link_to(event_group_); attach_functor([=](const caf::error &reason) { - spdlog::debug( - "PLAYHEAD exited: {}", - to_string(reason)); + spdlog::debug("PLAYHEAD exited: {}", to_string(reason)); }); set_exit_handler([=](scheduled_actor *a, caf::exit_msg &m) { disconnect_from_ui(); + if (audio_path_ == playhead::INDEPENDENT_AUDIO && audio_output_actor_) { + // this tells global audio output that we are exiting + send(audio_output_actor_, sound_audio_atom_v, uuid(), false); + } audio_output_actor_ = caf::actor(); empty_clip_ = utility::UuidActor(); image_cache_ = caf::actor(); @@ -182,32 +184,12 @@ void PlayheadActor::init() { sub_playheads_.clear(); source_actors_.clear(); + previous_source_actors_.clear(); string_audio_sources_.clear(); timeline_track_actors_.clear(); dynamic_source_actors_.clear(); default_exit_handler(a, m); audio_playhead_ = caf::actor(); - - }); - - - set_down_handler([=](down_msg &msg) { - - // have any of our sources gone down? e.g. timeline, timelinetracks, - // or regular media actors? If so, we need to do an immediate rebuild - auto actor_down = caf::actor_cast(msg.source); - bool need_rebuild = check_actor_down(actor_down, timeline_actor_); - need_rebuild |= check_actor_down(actor_down, source_actors_); - need_rebuild |= check_actor_down(actor_down, timeline_track_actors_); - need_rebuild |= check_actor_down(actor_down, dynamic_source_actors_); - need_rebuild |= check_actor_down(actor_down, audio_src_); - - if (need_rebuild && empty_clip_) { - // test on empty_clip_ tells us if we're actually exiting in which - // case we do not want to rebuild - new_source_list(); - } - }); try { @@ -243,13 +225,16 @@ void PlayheadActor::init() { // ensure we have a source and a child playhead, due to many messages // delagated to the child playhead - empty_clip_ = - utility::UuidActor(utility::Uuid::generate(), spawn()); - //link_to(empty_clip_.actor()); + empty_clip_ = utility::UuidActor(utility::Uuid::generate(), spawn()); + // link_to(empty_clip_.actor()); make_child_playhead(empty_clip_); switch_key_playhead(0); - audio_output_actor_ = system().registry().template get(audio_output_registry); + if (audio_path_ != playhead::NO_AUDIO) { + audio_output_actor_ = + system().registry().template get(audio_output_registry); + } + playhead_events_actor_ = system().registry().template get(global_playhead_events_actor); @@ -259,20 +244,19 @@ void PlayheadActor::init() { [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)); + "Got unwanted messate from {} {}", to_string(current_sender()), to_string(msg)); - request(caf::actor_cast(current_sender()), infinite, utility::name_atom_v).then( - [=](const std::string &nm) { - std::cerr << "NAME " << nm << "\n"; - }, - [=](caf::error &err) { - std::cerr << "NAME " << to_string(err) << "\n"; - }); + request( + caf::actor_cast(current_sender()), infinite, utility::name_atom_v) + .then( + [=](const std::string &nm) { std::cerr << "NAME " << nm << "\n"; }, + [=](caf::error &err) { std::cerr << "NAME " << to_string(err) << "\n"; }); return message{}; }); + apply_compare_prefs(); + behavior_.assign( [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, @@ -295,11 +279,9 @@ void PlayheadActor::init() { // unsubscribe(); }, - [=](compare_mode_atom) -> std::string { - return compare_mode_->value(); - }, + [=](compare_mode_atom) -> std::string { return compare_mode_->value(); }, - [=](compare_mode_atom, const std::string &compare_mode) { + [=](compare_mode_atom, const std::string &compare_mode) { compare_mode_->set_value(compare_mode); }, @@ -309,13 +291,12 @@ void PlayheadActor::init() { [=](viewport_events_group_atom) -> caf::actor { return viewport_events_group_; }, - [=](playlist::selection_actor_atom, caf::actor selection_actor) { - // this gives us a handle on a selection actor, so we know what media is - // selected in a timeline, for example, without being driven by the event - // messages that a selection actor emits. i.e. we do not run the stuff in - // connect_to_playlist_selection_actor which would normally set up the - // playhead to be driven by changes in the selection - playlist_selection_addr_ = caf::actor_cast(selection_actor); + [=](playlist::selection_actor_atom, caf::actor selection_actor) -> result { + // allows us to connect to a selection actor, fetch the current selection, and + // return a result. We use this to set-up our source list before deserialisation + auto rp = make_response_promise(); + connect_to_playlist_selection_actor(selection_actor, rp); + return rp; }, /* move all child playheads to current position */ @@ -429,7 +410,7 @@ void PlayheadActor::init() { // us the key playhead address utility::UuidVector r = to_uuid_vector(sub_playheads_); r.push_back(hero_sub_playhead_.uuid()); - return r; + return r; }, [=](key_playhead_index_atom) -> int { @@ -457,7 +438,9 @@ void PlayheadActor::init() { return unit; }, - [=](media::source_offset_frames_atom atom) { delegate(hero_sub_playhead_.actor(), atom); }, + [=](media::source_offset_frames_atom atom) { + delegate(hero_sub_playhead_.actor(), atom); + }, [=](media::source_offset_frames_atom atom, caf::actor sub_playhead, const int offset) { // pass up to the main playhead that the offset has changed @@ -589,13 +572,6 @@ void PlayheadActor::init() { delegate(hero_sub_playhead_.actor(), duration_frames_atom_v); }, - [=](json_store::update_atom, - const JsonStore &, - const std::string &, - const JsonStore &) {}, - - [=](json_store::update_atom, const JsonStore &) mutable {}, - [=](playhead_rate_atom) -> FrameRate { return playhead_rate(); }, [=](playhead_rate_atom, const FrameRate &rate) -> result { @@ -619,25 +595,30 @@ void PlayheadActor::init() { delegate(playlist_selection_, playlist::select_media_atom_v, selection); }, - [=](playlist::select_media_atom, const Uuid &media_id, const bool add_select) { - + [=](playlist::select_media_atom, + const Uuid &media_id, + const int playhead_idx, + const bool add_select) { // this message comes from the Viewport. It sends it when the user clicks on // the viewport to select an image in a Grid layout. If we are not doing - // a contact sheet we don't want to modify the selection this way as it + // a contact sheet we don't want to modify the selection this way as it // will immediately change what's in the Grid, for example if (parent_playlist_ && contact_sheet_mode_) { auto playlist = caf::actor_cast(parent_playlist_); - request(playlist, infinite, playlist::selection_actor_atom_v).then( - [=](caf::actor selection_actor) { - if (selection_actor) { - UuidVector l; - l.push_back(media_id); - anon_send(selection_actor, playlist::select_media_atom_v, l); - } - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + request(playlist, infinite, playlist::selection_actor_atom_v) + .then( + [=](caf::actor selection_actor) { + if (selection_actor) + anon_send( + selection_actor, + playlist::select_media_atom_v, + UuidVector({media_id})); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else { + switch_key_playhead(playhead_idx); } }, @@ -732,12 +713,32 @@ void PlayheadActor::init() { [=](sound_audio_atom, const Uuid &child_playhead_uuid, const std::vector &audio_buffers) { - if (caf::actor_cast(current_sender()) == audio_playhead_) { + if (audio_output_actor_ && + caf::actor_cast(current_sender()) == audio_playhead_) { anon_send( audio_output_actor_, sound_audio_atom_v, audio_buffers, - child_playhead_uuid); + child_playhead_uuid, + audio_path_ == playhead::GLOBAL_AUDIO, + uuid()); + } + }, + + [=](audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position) { + // the audio playhead broadcasts a smaller set of audio samples for + // the current playhead position - this is used for visualising + // the audio waveform, for example + if (audio_output_actor_ && + caf::actor_cast(current_sender()) == audio_playhead_) { + anon_send( + audio_output_actor_, + audio::audio_samples_atom_v, + audio_buffers, + playhead_position, + uuid()); } }, @@ -794,7 +795,7 @@ void PlayheadActor::init() { frame_ids_for_colour_mgmnt_lookeahead); }, - [=](buffer_atom, const utility::Uuid &id) -> result { + [=](buffer_atom, const utility::Uuid &id) -> result { auto rp = make_response_promise(); // fetch an image buffer for the given sub-playhead id for (const auto &i : sub_playheads_) { @@ -807,7 +808,7 @@ void PlayheadActor::init() { return rp; }, - [=](buffer_atom) { + [=](buffer_atom) { // fetch an image buffer from the hero sub-playhead delegate(hero_sub_playhead_.actor(), buffer_atom_v); }, @@ -818,9 +819,9 @@ void PlayheadActor::init() { [=](show_atom, const Uuid &child_playhead_uuid, const std::vector &future_frames) { - // see note in the other 'show_atom' handler - if (assembly_mode() == AM_ALL || assembly_mode() == AM_TEN || child_playhead_uuid == hero_sub_playhead_.uuid()) { + if (assembly_mode() == AM_ALL || assembly_mode() == AM_TEN || + child_playhead_uuid == hero_sub_playhead_.uuid()) { send(broadcast_, show_atom_v, child_playhead_uuid, future_frames); } @@ -901,7 +902,7 @@ void PlayheadActor::init() { // the end of the duration PlayheadBase::OptionalTimePoint next_step_timepoint = play_step(); // make a note of the time that the playhead position was updated - + update_child_playhead_positions(false); if (_playing != playing()) { @@ -950,7 +951,12 @@ void PlayheadActor::init() { // skip the playhead position to the start of the next clip, the // beginning of the current clip, or if we are already at the beginning // of the current clip then the beginning of the preceeding clip - request(hero_sub_playhead_.actor(), infinite, skip_to_clip_atom_v, position(), next_clip) + request( + hero_sub_playhead_.actor(), + infinite, + skip_to_clip_atom_v, + position(), + next_clip) .then( [=](const timebase::flicks new_position) mutable { set_position(new_position); @@ -981,16 +987,27 @@ void PlayheadActor::init() { // to load frames that will be on-screen soon. We stagger the message send so // that the reader is not swamped by the requests coming from multiple sub-playheads // at one time. Each sub-playhead is sent this message about once a second. - sub_playhead_precache_idx_ = (sub_playhead_precache_idx_+1)%sub_playheads_.size(); - anon_send(sub_playheads_[sub_playhead_precache_idx_].actor(), precache_atom_v); - if (audio_playhead_ && sub_playheads_[sub_playhead_precache_idx_] == hero_sub_playhead_) { - anon_send(audio_playhead_, precache_atom_v); - } - if (playing()) { - delayed_anon_send( - this, - std::chrono::milliseconds(1000/sub_playheads_.size()), - precache_atom_v); + if (assembly_mode() == AM_ONE) { + // if we aren't comparing multiple sources, only make the + // 'hero' playhead trigger a precache + anon_send(hero_sub_playhead_.actor(), precache_atom_v); + if (audio_playhead_) + anon_send(audio_playhead_, precache_atom_v); + delayed_anon_send(this, std::chrono::milliseconds(1000), precache_atom_v); + } else { + sub_playhead_precache_idx_ = + (sub_playhead_precache_idx_ + 1) % sub_playheads_.size(); + anon_send(sub_playheads_[sub_playhead_precache_idx_].actor(), precache_atom_v); + if (audio_playhead_ && + sub_playheads_[sub_playhead_precache_idx_] == hero_sub_playhead_) { + anon_send(audio_playhead_, precache_atom_v); + } + if (playing()) { + delayed_anon_send( + this, + std::chrono::milliseconds(1000 / sub_playheads_.size()), + precache_atom_v); + } } }, @@ -1107,7 +1124,7 @@ void PlayheadActor::init() { // This comes from contact sheet objects - the sources are all the // media in the contact sheet if (sources != dynamic_source_actors_) { - contact_sheet_mode_ = true; + contact_sheet_mode_ = true; dynamic_source_actors_ = sources; new_source_list(); } @@ -1133,28 +1150,28 @@ void PlayheadActor::init() { } }, - [=](source_atom, const utility::UuidActor &timeline, const utility::UuidActorVector &timeline_tracks) { - // this is broadcast from the timeline on change events, if the parent of the playhead is - // a timeline. We only rebuild if the track actors have changed. + [=](source_atom, + const utility::UuidActor &timeline, + const utility::UuidActorVector &timeline_tracks) { + // this is broadcast from the timeline on change events, if the parent of the + // playhead is a timeline. We only rebuild if the track actors have changed. if (timeline_actor_ != timeline || timeline_track_actors_ != timeline_tracks) { - timeline_actor_ = timeline; + timeline_actor_ = timeline; timeline_track_actors_ = timeline_tracks; // for timelines, there is one global frame rate set by the timline // itself - we need to get that now. if (timeline_actor_) { - request(timeline_actor_.actor(), infinite, utility::rate_atom_v).then( - [=](const utility::FrameRate &r) { - set_playhead_rate(r); - new_source_list(); - }, - [=](caf::error &err) { - new_source_list(); - }); + request(timeline_actor_.actor(), infinite, utility::rate_atom_v) + .then( + [=](const utility::FrameRate &r) { + set_playhead_rate(r); + new_source_list(); + }, + [=](caf::error &err) { new_source_list(); }); } else { new_source_list(); } - } }, @@ -1171,7 +1188,8 @@ void PlayheadActor::init() { // ensures that the SubPlayhead is 'up-to-date' in other wordfs // it has got all the data it needs from its source to start playback // like duration, timecode and AVFrameID list - fan_out_request(to_actor_vector(sub_playheads_), infinite, source_atom_v) + fan_out_request( + to_actor_vector(sub_playheads_), infinite, source_atom_v) .then( [=](const std::vector) mutable { // now sending duration_flicks to ourselves means that duration, in/out @@ -1256,7 +1274,7 @@ void PlayheadActor::init() { [=](utility::get_group_atom) -> caf::actor { return broadcast_; }, - [=](broadcast::join_broadcast_atom atom, caf::actor joiner) { + [=](broadcast::join_broadcast_atom atom, caf::actor joiner) { delegate(broadcast_, atom, joiner); }, @@ -1275,15 +1293,105 @@ void PlayheadActor::init() { }, [=](utility::serialise_atom) -> result { + auto rp = make_response_promise(); + + // We need to update the offsets per sub-playhead before completing + // serialisation ... + auto ct = std::make_shared(sub_playheads_.size()); + source_alignment_values_->set_value(std::vector(sub_playheads_.size(), 0)); + if (sub_playheads_.empty()) { + rp.deliver(serialise()); + return rp; + } + for (size_t idx = 0; idx < sub_playheads_.size(); ++idx) { - // source_offset_frames_-> - - return serialise(); + request( + sub_playheads_[idx].actor(), infinite, media::source_offset_frames_atom_v) + .then( + [=](const int64_t offset) mutable { + auto r = source_alignment_values_->value(); + if (idx < r.size()) + r[idx] = offset; + source_alignment_values_->set_value(r); + (*ct)--; + if (!(*ct)) { + rp.deliver(serialise()); + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + (*ct)--; + if (!(*ct)) { + rp.deliver(serialise()); + } + }); + } + return rp; }, - [=](module::deserialise_atom, const utility::JsonStore &j) { - return deserialise(j); + [=](module::deserialise_atom, const utility::JsonStore &j) { + // before we de-serialise, we have to be sure that we have been set-up with the + // playback sources provided by the playhead selection actor by waiting for this + // self-message to return + request( + caf::actor_cast(this), + infinite, + playlist::selection_actor_atom_v, + caf::actor_cast(playlist_selection_addr_)) + .then( + [=](bool) { + deserialise(j); + auto layouts_manager = + self()->home_system().registry().template get( + viewport_layouts_manager); + + // following desrialisation, we set-up the compare mode and set the + // frame offsets per sub-playhead + request( + layouts_manager, + infinite, + playhead::compare_mode_atom_v, + compare_mode_->value()) + .then( + [=](std::pair< + xstudio::playhead::AutoAlignMode, + xstudio::playhead::AssemblyMode> mode) { + set_assembly_mode(mode.second); + source_actors_.clear(); + new_source_list(); + align_clip_frame_numbers(); + auto offset_frames = source_alignment_values_->value(); + int idx = 0; + for (auto &sub_playhead : sub_playheads_) { + if (idx < offset_frames.size()) + anon_send( + sub_playhead.actor(), + media::source_offset_frames_atom_v, + (int64_t)offset_frames[idx++]); + } + notify_loop_end_changed(); + notify_loop_start_changed(); + send( + event_group_, + utility::event_atom_v, + loop_atom_v, + loop()); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + + // broadcast the compare mode to viewport(s) that are attached to this + // playhead + send( + viewport_events_group_, + compare_mode_atom_v, + compare_mode_->value()); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); }, [=](velocity_atom) -> float { return velocity(); }, @@ -1309,7 +1417,6 @@ void PlayheadActor::init() { const bool all_playheads, const bool force, const std::vector sub_playheads) { - if (sub_playheads == sub_playheads_) { if (all_playheads) { for (auto &ph : sub_playheads) { @@ -1343,28 +1450,58 @@ void PlayheadActor::init() { [=](bool) {}); } -void PlayheadActor::connect_to_playlist_selection_actor(caf::actor playlist_selection) { +void PlayheadActor::connect_to_playlist_selection_actor( + caf::actor playlist_selection, caf::typed_response_promise rp) { if (playlist_selection) { // Here we subscribe to event messages from a PlaylistSelectionActor - // this will push lists of media to the playhead, when media is selected // from a playlist for playback. See 'new_source_list'. + + // Note that we make sure we've joined the event group, before then + // requesting the current selected sources from the PlaylistSelectionActor + // This avoids a race condition where the PlaylistSelectionActor selection + // changes before we have joined the event group but after we make our + // direct request to get the current selection. playlist_selection_addr_ = caf::actor_cast(playlist_selection); - utility::join_event_group(this, playlist_selection); - request( - playlist_selection, - infinite, - playhead::get_selected_sources_atom_v) + request(playlist_selection, caf::infinite, utility::get_event_group_atom_v) .then( - [=](const utility::UuidActorVector &selection) { - dynamic_source_actors_ = selection; - new_source_list(); + [=](caf::actor grp) mutable { + request(grp, caf::infinite, broadcast::join_broadcast_atom_v) + .then( + [=](const bool) mutable { + if (!contact_sheet_mode_) { + request( + playlist_selection, + infinite, + playhead::get_selected_sources_atom_v) + .then( + [=](const utility::UuidActorVector + &selection) mutable { + dynamic_source_actors_ = selection; + new_source_list(); + rp.deliver(true); + }, + [=](const caf::error &e) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(e)); + rp.deliver(false); + }); + } else { + rp.deliver(true); + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(false); + }); }, - [=](const caf::error &e) { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(e)); + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + } else { + rp.deliver(false); } } @@ -1397,8 +1534,7 @@ void PlayheadActor::clear_child_playheads() { std::chrono::seconds(2), clear_precache_queue_atom_v, sub_playheads_, - video_string_out_actor_ - ); + video_string_out_actor_); sub_playheads_.clear(); hero_sub_playhead_ = utility::UuidActor(); @@ -1409,18 +1545,20 @@ caf::actor PlayheadActor::make_child_playhead(utility::UuidActor source) { std::stringstream nmstr; nmstr << "ChildPlayhead" << sub_playheads_.size(); - const auto uuid = utility::Uuid::generate(); - auto sub_playhead = utility::UuidActor(uuid, spawn( - nmstr.str(), - source, - actor_cast(this), - timeline_mode(), - loop_start(), - loop_end(), - play_rate_mode(), - playhead_rate(), - media::MediaType::MT_IMAGE, - uuid)); + const auto uuid = utility::Uuid::generate(); + auto sub_playhead = utility::UuidActor( + uuid, + spawn( + nmstr.str(), + source, + actor_cast(this), + timeline_mode(), + loop_start(), + loop_end(), + play_rate_mode(), + playhead_rate(), + media::MediaType::MT_IMAGE, + uuid)); link_to(sub_playhead.actor()); sub_playheads_.push_back(sub_playhead); @@ -1431,6 +1569,9 @@ caf::actor PlayheadActor::make_child_playhead(utility::UuidActor source) { void PlayheadActor::make_audio_child_playhead(const int source_index) { + if (!audio_output_actor_) + return; + if (source_index >= (int)source_actors_.size()) return; @@ -1441,41 +1582,43 @@ void PlayheadActor::make_audio_child_playhead(const int source_index) { audio_playhead_retimer_ = caf::actor(); } }; - + if (timeline_mode()) { // Are we already hooked up to the timeline as the audio source? - if (audio_src_ == timeline_actor_) return; + if (audio_src_ == timeline_actor_) + return; audio_src_ = timeline_actor_; - remove_retimer(); + remove_retimer(); } else { - + if (assembly_mode() == AM_STRING) { // source_actors_ is unchanged since we were last here? - if (string_audio_sources_ == source_actors_) return; + if (string_audio_sources_ == source_actors_) + return; remove_retimer(); - string_audio_sources_ = source_actors_; + string_audio_sources_ = source_actors_; audio_playhead_retimer_ = spawn(string_audio_sources_); link_to(audio_playhead_retimer_); audio_src_ = utility::UuidActor(utility::Uuid::generate(), audio_playhead_retimer_); } else { - if (audio_src_ == source_actors_[source_index]) return; + if (audio_src_ == source_actors_[source_index]) + return; audio_src_ = source_actors_[source_index]; remove_retimer(); } - - } + } if (audio_playhead_) { // delete the old audio sub-playhead unlink_from(audio_playhead_); - send_exit(audio_playhead_, caf::exit_reason::user_shutdown); + send_exit(audio_playhead_, caf::exit_reason::user_shutdown); } audio_playhead_ = spawn( @@ -1505,13 +1648,13 @@ void PlayheadActor::new_source_list() { // address, so we can restore the offsets /*source_offsets_.clear(); for (auto &ph: sub_playheads_) { - + auto sub_playhead = ph.actor(); request(sub_playhead, infinite, source_atom_v, true).then( [=](utility::Uuid source_uuid) { request(sub_playhead, infinite, media::source_offset_frames_atom_v).then( - [=](const int64_t offset) { - source_offsets_[source_uuid] = offset; + [=](const int64_t offset) { + source_offsets_[source_uuid] = offset; }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1522,18 +1665,33 @@ void PlayheadActor::new_source_list() { }); }*/ - source_actors_ = sl; - // reset the loop range as we have new sources - set_loop_start(timebase::k_flicks_low); - set_loop_end(timebase::k_flicks_max); - if (timeline_mode()) { rebuild_from_timeline_sources(); } else { rebuild_from_dynamic_sources(); } + + + if (previous_source_actors_ != source_actors_) { + // One last check ... have our sources changed since last time we did + // a rebuild? If so, reset the loop range as we have new sources + set_loop_start(timebase::k_flicks_low); + set_loop_end(timebase::k_flicks_max); + previous_source_actors_ = source_actors_; + set_use_loop_range(false); + notify_loop_start_changed(); + notify_loop_end_changed(); + + // finally,. apply auto-alignment + if (!(assembly_mode() == AM_ONE || assembly_mode() == AM_STRING)) { + + // for A/B mode, grid mode etc we need the child playheads to match + // their durations so that they map to a common (shared) timeline + align_clip_frame_numbers(); + } + } } void PlayheadActor::rebuild_from_timeline_sources() { @@ -1574,11 +1732,10 @@ void PlayheadActor::rebuild_from_timeline_sources() { "Trying to compare too many things, limiting to first ", max_compare_sources_->value()); break; - } else if (assembly_mode() == AM_TEN && count == 10 ) { + } else if (assembly_mode() == AM_TEN && count == 10) { break; } count++; - } // passing a -1 as the index forces a search for a child playhead that @@ -1593,7 +1750,6 @@ void PlayheadActor::rebuild_from_timeline_sources() { send(broadcast_, key_child_playhead_atom_v, to_uuid_vector(sub_playheads_)); anon_send(this, duration_flicks_atom_v); - } void PlayheadActor::rebuild_from_dynamic_sources() { @@ -1622,7 +1778,8 @@ void PlayheadActor::rebuild_from_dynamic_sources() { } else if (assembly_mode() == AM_STRING) { - video_string_out_actor_ = utility::UuidActor(utility::Uuid::generate(), spawn(source_actors_)); + video_string_out_actor_ = utility::UuidActor( + utility::Uuid::generate(), spawn(source_actors_)); link_to(video_string_out_actor_.actor()); make_child_playhead(video_string_out_actor_); switch_key_playhead(0); @@ -1644,7 +1801,7 @@ void PlayheadActor::rebuild_from_dynamic_sources() { "Trying to compare too many things, limiting to first ", max_compare_sources_->value()); break; - } else if (assembly_mode() == AM_TEN && count == 10 ) { + } else if (assembly_mode() == AM_TEN && count == 10) { break; } count++; @@ -1657,11 +1814,10 @@ void PlayheadActor::rebuild_from_dynamic_sources() { num_sub_playheads_->set_value(sub_playheads_.size()); send(broadcast_, key_child_playhead_atom_v, to_uuid_vector(sub_playheads_)); - } void PlayheadActor::switch_key_playhead(int idx) { - + if (idx < sub_playheads_.size() && idx >= 0 && hero_sub_playhead_ == sub_playheads_[idx]) return; @@ -1678,9 +1834,9 @@ void PlayheadActor::switch_key_playhead(int idx) { // got all the data it needs from its source request_receive(*sys, ph.actor(), source_atom_v); - if (to_string( - request_receive(*sys, ph.actor(), media_source_atom_v, true) - .uuid()) == current_media_source_uuid_->value()) { + if (to_string(request_receive( + *sys, ph.actor(), media_source_atom_v, true) + .uuid()) == current_media_source_uuid_->value()) { idx = i; break; } @@ -1705,19 +1861,20 @@ void PlayheadActor::switch_key_playhead(int idx) { try { - auto source_actor = request_receive(*sys, hero_sub_playhead_.actor(), source_atom_v); + auto source_actor = + request_receive(*sys, hero_sub_playhead_.actor(), source_atom_v); make_audio_child_playhead(idx); // this should update the 'image_source_' attribute so it shows the correct // list of available sources in the UI - auto media_actor = request_receive(*sys, hero_sub_playhead_.actor(), media_atom_v); + auto media_actor = + request_receive(*sys, hero_sub_playhead_.actor(), media_atom_v); if (media_actor) current_media_changed(media_actor); // send the change notification send(event_group_, utility::event_atom_v, utility::change_atom_v); - send( - event_group_, utility::event_atom_v, playhead::key_playhead_index_atom_v, idx); + send(event_group_, utility::event_atom_v, playhead::key_playhead_index_atom_v, idx); check_if_loop_range_makes_sense(); @@ -1735,14 +1892,6 @@ void PlayheadActor::switch_key_playhead(int idx) { update_child_playhead_positions(true); } - if (!(assembly_mode() == AM_ONE || assembly_mode() == AM_STRING)) { - - // for A/B mode, grid mode etc we need the child playheads to match - // their durations so that they map to a common (shared) timeline - align_clip_frame_numbers(); - - } - const auto switchpoint = utility::clock::now(); // this will trigger an update to the duration, at which point we can @@ -1803,11 +1952,13 @@ void PlayheadActor::update_child_playhead_positions(const bool force_broadcast) anon_send( audio_output_actor_, position_atom_v, - position(), + adjusted_position(), forward(), velocity(), playing(), - last_playhead_set_timepoint()); + last_playhead_set_timepoint(), + audio_path_ == playhead::GLOBAL_AUDIO, + uuid()); } if (audio_playhead_) { @@ -1855,7 +2006,9 @@ void PlayheadActor::update_child_playhead_positions(const bool force_broadcast) if (user_is_frame_scrubbing_->value()) { // if the user is scrubbing make sure that we aren't trying // to do lookahead reads for playback - anon_send(hero_sub_playhead_.actor(), full_precache_atom_v, false, false); + for (const auto &i : sub_playheads_) { + anon_send(i.actor(), full_precache_atom_v, false, false); + } } } @@ -1898,8 +2051,7 @@ void PlayheadActor::notify_loop_start_changed() { [=](const int loop_start) { loop_start_frame_->set_value(loop_start, false); - send( - event_group_, utility::event_atom_v, simple_loop_start_atom_v, loop_start); + send(event_group_, utility::event_atom_v, simple_loop_start_atom_v, loop_start); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1921,7 +2073,6 @@ void PlayheadActor::update_duration(caf::typed_response_promise::lowest(); - auto timeout = std::chrono::milliseconds(500); + auto timeout = std::chrono::milliseconds(500); for (auto &sub_playhead : sub_playheads_) { @@ -2111,10 +2262,10 @@ void PlayheadActor::match_video_track_durations() { sub_playhead.actor(), timeout, playhead::duration_flicks_atom_v, - true // bool here means we get the duration *before* retiming/extension is done - ) - ); - } catch (std::exception & e) { + true // bool here means we get the duration *before* retiming/extension is + // done + )); + } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -2123,10 +2274,10 @@ void PlayheadActor::match_video_track_durations() { for (auto &sub_playhead : sub_playheads_) { try { - request_receive_wait( + request_receive_wait( *sys, sub_playhead.actor(), timeout, timeline::duration_atom_v, max_duration); - } catch (...) {} - + } catch (...) { + } } } @@ -2134,15 +2285,18 @@ void PlayheadActor::align_clip_frame_numbers() { try { - if (sub_playheads_.empty()) + if (sub_playheads_.size() < 2) return; scoped_actor sys{system()}; auto timeout = std::chrono::milliseconds(500); - const bool align = auto_align_mode() != AAM_ALIGN_OFF; - const bool trim = auto_align_mode() == AAM_ALIGN_TRIM; + // in timeline_mode we always comparing video tracks from the same timeline. + // We therefore do not need to align or trim - we just extend the duration + // of video tracks to match the longest. + const bool align = timeline_mode() ? false : auto_align_mode() != AAM_ALIGN_OFF; + const bool trim = timeline_mode() ? false : auto_align_mode() == AAM_ALIGN_TRIM; // Use timecode to align the sources - if we are trimming, we use trim to the latest // start frame and the earliest end frame across all sources. If not we do the reverse, @@ -2167,10 +2321,18 @@ void PlayheadActor::align_clip_frame_numbers() { // reset the offset to zero so we start with the 'true' media first & last frame request_receive_wait( - *sys, sub_playhead.actor(), timeout, media::source_offset_frames_atom_v, int64_t(0)); + *sys, + sub_playhead.actor(), + timeout, + media::source_offset_frames_atom_v, + int64_t(0)); request_receive_wait( - *sys, sub_playhead.actor(), timeout, timeline::duration_atom_v, timebase::flicks(0)); - } + *sys, + sub_playhead.actor(), + timeout, + timeline::duration_atom_v, + timebase::flicks(0)); + } if (align) { @@ -2181,7 +2343,11 @@ void PlayheadActor::align_clip_frame_numbers() { // reset the offset to zero so we get the 'true' media first & last frame request_receive_wait( - *sys, sub_playhead.actor(), timeout, media::source_offset_frames_atom_v, int64_t(0)); + *sys, + sub_playhead.actor(), + timeout, + media::source_offset_frames_atom_v, + int64_t(0)); // reset the duration to cancel any retiming already done on the source request_receive_wait( @@ -2195,11 +2361,12 @@ void PlayheadActor::align_clip_frame_numbers() { const int source_first_frame = request_receive_wait( *sys, sub_playhead.actor(), timeout, first_frame_media_pointer_atom_v) - .timecode().total_frames(); + .timecode() + .total_frames(); // evaluate our overall first frame, depending on trim setting - first_frame = trim ? std::max(first_frame, source_first_frame) - : std::min(first_frame, source_first_frame); + first_frame = trim ? std::max(first_frame, source_first_frame) + : std::min(first_frame, source_first_frame); } for (auto sub_playhead : sub_playheads_) { @@ -2207,7 +2374,8 @@ void PlayheadActor::align_clip_frame_numbers() { const auto source_first_frame = request_receive_wait( *sys, sub_playhead.actor(), timeout, first_frame_media_pointer_atom_v); - int64_t frames_shift = first_frame - (int)source_first_frame.timecode().total_frames(); + int64_t frames_shift = + first_frame - (int)source_first_frame.timecode().total_frames(); // apply the time offset request_receive_wait( @@ -2224,7 +2392,6 @@ void PlayheadActor::align_clip_frame_numbers() { timeout, media::source_offset_frames_atom_v, frames_shift); - } } } @@ -2234,36 +2401,23 @@ void PlayheadActor::align_clip_frame_numbers() { for (auto sub_playhead : sub_playheads_) { auto d = request_receive_wait( - *sys, - sub_playhead.actor(), - timeout, - duration_flicks_atom_v); + *sys, sub_playhead.actor(), timeout, duration_flicks_atom_v); dur = trim ? std::min(d, dur) : std::max(d, dur); } for (auto sub_playhead : sub_playheads_) { request_receive_wait( - *sys, - sub_playhead.actor(), - timeout, - timeline::duration_atom_v, - dur); + *sys, sub_playhead.actor(), timeout, timeline::duration_atom_v, dur); } request_receive_wait( - *sys, - audio_playhead_, - timeout, - timeline::duration_atom_v, - dur); + *sys, audio_playhead_, timeout, timeline::duration_atom_v, dur); - source_offset_frames_->set_value(request_receive_wait( - *sys, - hero_sub_playhead_.actor(), - timeout, - media::source_offset_frames_atom_v), - false); + source_offset_frames_->set_value( + request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, media::source_offset_frames_atom_v), + false); // the cached frames display might need updating rebuild_cached_frames_status(); @@ -2288,9 +2442,9 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_current_source() { scoped_actor sys{system()}; - const auto uuid = - request_receive(*sys, hero_sub_playhead_.actor(), media_source_atom_v, true) - .uuid(); + const auto uuid = request_receive( + *sys, hero_sub_playhead_.actor(), media_source_atom_v, true) + .uuid(); if (uuid) { const auto p = media_frame_per_media_uuid_.find(uuid); @@ -2300,7 +2454,11 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_current_source() { } const auto position = request_receive( - *sys, hero_sub_playhead_.actor(), media_frame_to_flicks_atom_v, uuid, last_viewed_frame); + *sys, + hero_sub_playhead_.actor(), + media_frame_to_flicks_atom_v, + uuid, + last_viewed_frame); set_position(position); } @@ -2330,7 +2488,11 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_given_source( scoped_actor sys{system()}; const auto position = request_receive( - *sys, hero_sub_playhead_.actor(), media_frame_to_flicks_atom_v, source_uuid, last_viewed_frame); + *sys, + hero_sub_playhead_.actor(), + media_frame_to_flicks_atom_v, + source_uuid, + last_viewed_frame); set_position(position); anon_send(this, jump_atom_v); @@ -2401,7 +2563,14 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int send(broadcast_, play_atom_v, playing()); send(playhead_media_events_group_, utility::event_atom_v, play_atom_v, playing()); - anon_send(audio_output_actor_, play_atom_v, playing()); + if (audio_output_actor_) { + anon_send( + audio_output_actor_, + play_atom_v, + playing(), + audio_path_ == playhead::GLOBAL_AUDIO, + uuid()); + } anon_send( system().registry().template get(global_registry), global::busy_atom_v, @@ -2421,7 +2590,10 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int scoped_actor sys{system()}; const timebase::flicks loop_end_flicks = request_receive( - *sys, hero_sub_playhead_.actor(), logical_frame_to_flicks_atom_v, loop_end_frame_->value()); + *sys, + hero_sub_playhead_.actor(), + logical_frame_to_flicks_atom_v, + loop_end_frame_->value()); if (set_loop_end(loop_end_flicks - PlayheadBase::playback_step_increment)) { // position or loop end were also changed notify_loop_start_changed(); @@ -2465,11 +2637,18 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int notify_loop_end_changed(); } else if (attr_uuid == source_offset_frames_->uuid()) { request( - hero_sub_playhead_.actor(), infinite, media::source_offset_frames_atom_v, source_offset_frames_->value()).then( + hero_sub_playhead_.actor(), + infinite, + media::source_offset_frames_atom_v, + source_offset_frames_->value()) + .then( [=](const bool changed) { if (changed) { // update the audio playhead offset - anon_send(audio_playhead_, media::source_offset_frames_atom_v, source_offset_frames_->value()); + anon_send( + audio_playhead_, + media::source_offset_frames_atom_v, + source_offset_frames_->value()); // force broacast image buffers so we see the source // offset change in viewport etc. anon_send(this, jump_atom_v); @@ -2481,27 +2660,34 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int } else if (attr_uuid == compare_mode_->uuid()) { auto layouts_manager = - self()->home_system().registry().template get(viewport_layouts_manager); + self()->home_system().registry().template get(viewport_layouts_manager); // get the assembly mode for the new compare mode - request(layouts_manager, infinite, playhead::compare_mode_atom_v, compare_mode_->value()).then( - [=](std::pair< xstudio::playhead::AutoAlignMode, xstudio::playhead::AssemblyMode> mode) { - if (mode.second != assembly_mode()) { - set_assembly_mode(mode.second); - source_actors_.clear(); - new_source_list(); - // get the default align mode - } - set_auto_align_mode(mode.first); - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + request( + layouts_manager, infinite, playhead::compare_mode_atom_v, compare_mode_->value()) + .then( + [=](std::pair + mode) { + if (mode.second != assembly_mode()) { + set_assembly_mode(mode.second); + source_actors_.clear(); + new_source_list(); + // get the default align mode + } + // Note: not using the default align mode provided by the + // layout plugin, because we are driving this with a preference + // per container type (Playlsit, Subset, Contact Sheet etc) + + // set_auto_align_mode(mode.first); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); // broadcast the compare mode to viewport(s) that are attached to this // playhead send(viewport_events_group_, compare_mode_atom_v, compare_mode_->value()); - + } else { PlayheadBase::attribute_changed(attr_uuid, role); } @@ -2520,47 +2706,11 @@ void PlayheadActor::hotkey_pressed( if (hotkey_uuid == move_selection_down_hotkey_) { - if (sub_playheads_.size() <= 1) { - - auto playlist_selection = caf::actor_cast(playlist_selection_addr_); - if (playlist_selection) { - anon_send(playlist_selection, playhead::select_next_media_atom_v, 1); - } - - } else { - for (size_t i = 0; i < sub_playheads_.size(); ++i) { - if (sub_playheads_[i] == hero_sub_playhead_) { - if (i == (sub_playheads_.size() - 1)) { - switch_key_playhead(0); - } else { - switch_key_playhead(i + 1); - } - break; - } - } - } + step_to_next_media(true); } else if (hotkey_uuid == move_selection_up_hotkey_) { - if (sub_playheads_.size() <= 1) { - - auto playlist_selection = caf::actor_cast(playlist_selection_addr_); - if (playlist_selection) { - anon_send(playlist_selection, playhead::select_next_media_atom_v, -1); - } - - } else { - for (size_t i = 0; i < sub_playheads_.size(); ++i) { - if (sub_playheads_[i] == hero_sub_playhead_) { - if (!i) { - switch_key_playhead(sub_playheads_.size() - 1); - } else { - switch_key_playhead(i - 1); - } - break; - } - } - } + step_to_next_media(false); } else if (hotkey_uuid == jump_to_previous_note_hotkey_) { @@ -2638,13 +2788,31 @@ void PlayheadActor::hotkey_pressed( std::numeric_limits::max()); } - } else if (hotkey_uuid == jump_to_next_clip_) { + } else if (hotkey_uuid == cycle_image_layer_up_) { - anon_send(caf::actor_cast(this), skip_to_clip_atom_v, true); + auto stream_names = image_stream_->get_role_data>( + module::Attribute::StringChoices); + auto curr_stream = image_stream_->value(); + auto p = std::find(stream_names.begin(), stream_names.end(), curr_stream); + if (p != stream_names.end()) { + size_t idx = std::distance(stream_names.begin(), p); + if (idx > 0) { + image_stream_->set_value(stream_names[idx - 1]); + } + } - } else if (hotkey_uuid == jump_to_previous_clip_) { + } else if (hotkey_uuid == cycle_image_layer_down_) { - anon_send(caf::actor_cast(this), skip_to_clip_atom_v, false); + auto stream_names = image_stream_->get_role_data>( + module::Attribute::StringChoices); + auto curr_stream = image_stream_->value(); + auto p = std::find(stream_names.begin(), stream_names.end(), curr_stream); + if (p != stream_names.end()) { + size_t idx = std::distance(stream_names.begin(), p); + if (idx < (stream_names.size() - 1)) { + image_stream_->set_value(stream_names[idx + 1]); + } + } } else { PlayheadBase::hotkey_pressed(hotkey_uuid, context, window); @@ -2664,7 +2832,7 @@ void PlayheadActor::connected_to_ui_changed() { if (connected_to_ui()) { - restart_readahead_cacheing(true); + restart_readahead_cacheing(assembly_mode() != AM_ONE); update_playback_rate(); send(fps_moniotor_group_, utility::event_atom_v, velocity_atom_v, velocity()); send( @@ -2810,7 +2978,7 @@ void PlayheadActor::update_stream_multichoice( }; auto receive_error = [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); clear_attr(); }; @@ -3056,7 +3224,7 @@ void PlayheadActor::make_source_menu_model() { move_selection_up_hotkey_ = register_hotkey( "Up", - "Move backwards through selection", + "Move backwards through media/clips", "When comparing multiple selected items hit the hotkey to cycle back through the " "selection. If only one item is selected then select the previous item in the " "playlist.", @@ -3065,7 +3233,7 @@ void PlayheadActor::make_source_menu_model() { move_selection_down_hotkey_ = register_hotkey( "Down", - "Move forwards through selection", + "Move Forwards through media/clips", "When comparing multiple selected items hit the hotkey to cycle forward through the " "selection. If only one item is selected then select the next item in the playlist.", true, @@ -3113,20 +3281,103 @@ void PlayheadActor::make_source_menu_model() { insert_menu_item(source_menu_model_name, "", "", 4.0f, audio_source_); } -utility::UuidActorVector PlayheadActor::to_uuid_actor_vec(const std::vector &actors) -{ +utility::UuidActorVector +PlayheadActor::to_uuid_actor_vec(const std::vector &actors) { caf::scoped_actor sys(system()); utility::UuidActorVector r; r.resize(actors.size()); int idx = 0; - for (auto &a: actors) { + for (auto &a : actors) { try { auto uuid = request_receive(*sys, a, utility::uuid_atom_v); - r[idx] = utility::UuidActor(uuid, a); - }catch (...) {} + r[idx] = utility::UuidActor(uuid, a); + } catch (...) { + } idx++; } return r; } + +void PlayheadActor::apply_compare_prefs() { + + auto owner = caf::actor_cast(parent_playlist_); + if (owner) { + request(owner, infinite, utility::detail_atom_v) + .then( + [=](utility::ContainerDetail &detail) { + // this playhead has been set-up from serialisation data, so + // we don't apply the default compare prefs + if (deserialised_) + return; + + utility::JsonStore j; + try { + auto prefs = GlobalStoreHelper(system()); + if (detail.type_ == "Playlist") { + j = prefs.value( + "/core/playhead/playlist_default_compare"); + } else if (detail.type_ == "Subset") { + j = prefs.value( + "/core/playhead/subset_default_compare"); + } else if (detail.type_ == "Contact Sheet") { + j = prefs.value( + "/core/playhead/contact_sheet_default_compare"); + } else if (detail.type_ == "Timeline") { + j = prefs.value( + "/core/playhead/timeline_default_compare"); + } + + if (j.is_array() && j.size() == 2 && j[0].is_string() && + j[1].is_string()) { + std::string compare_mode = j[0].get(); + std::string align = j[1].get(); + compare_mode_->set_value(compare_mode); + auto_align_mode_->set_value(align); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](caf::error &err) {}); + } +} + +void PlayheadActor::step_to_next_media(const bool forwards) { + + if (timeline_mode() || assembly_mode() == AM_STRING) { + + anon_send(caf::actor_cast(this), skip_to_clip_atom_v, forwards); + + } else { + + if (sub_playheads_.size() <= 1) { + + auto playlist_selection = caf::actor_cast(playlist_selection_addr_); + if (playlist_selection) { + anon_send(playlist_selection, playhead::select_next_media_atom_v, 1); + } + + } else { + for (size_t i = 0; i < sub_playheads_.size(); ++i) { + if (sub_playheads_[i] == hero_sub_playhead_) { + if (forwards) { + if (i == (sub_playheads_.size() - 1)) { + switch_key_playhead(0); + } else { + switch_key_playhead(i + 1); + } + } else if (!i) { + switch_key_playhead(sub_playheads_.size() - 1); + } else { + switch_key_playhead(i - 1); + } + break; + } + + } + } + } +} diff --git a/src/playhead/src/playhead_global_events_actor.cpp b/src/playhead/src/playhead_global_events_actor.cpp index f6407b266..c3333980d 100644 --- a/src/playhead/src/playhead_global_events_actor.cpp +++ b/src/playhead/src/playhead_global_events_actor.cpp @@ -25,7 +25,6 @@ void PlayheadGlobalEventsActor::on_exit() { global_active_playhead_ = caf::actor(); viewports_.clear(); system().registry().erase(global_playhead_events_actor); - } void PlayheadGlobalEventsActor::init() { @@ -247,12 +246,11 @@ void PlayheadGlobalEventsActor::init() { viewport_name, viewport); }, - [=](playhead::redraw_viewport_atom) { - + [=](playhead::redraw_viewport_atom) { // force all viewport to do a redraw - for (const auto &p: viewports_) { + for (const auto &p : viewports_) { anon_send(p.second.viewport, playhead::redraw_viewport_atom_v); - } + } }, [=](ui::viewport::viewport_atom, const std::string viewport_name) -> result { diff --git a/src/playhead/src/playhead_selection_actor.cpp b/src/playhead/src/playhead_selection_actor.cpp index 2d3e141d4..18cd8dfb4 100644 --- a/src/playhead/src/playhead_selection_actor.cpp +++ b/src/playhead/src/playhead_selection_actor.cpp @@ -21,35 +21,13 @@ PlayheadSelectionActor::PlayheadSelectionActor( base_(static_cast(jsn["base"])), playlist_(std::move(monitored_playlist)) { - - // how do we restore.. ? - - // // parse and generate tracks/stacks. - // for (const auto& [key, value] : jsn["actors"].items()) { - // if(value["base"]["container"]["type"] == "TrackActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // else if(value["base"]["container"]["type"] == "ClipActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // else if(value["base"]["container"]["type"] == "StackActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // } - init(); + + // a bit clunky, but to finish deserialisation we need to re-select the + // media, thus: + UuidVector serialised_selection = base_.items_vec(); + base_.clear(); + select_media(serialised_selection); } PlayheadSelectionActor::PlayheadSelectionActor( @@ -148,9 +126,16 @@ caf::message_handler PlayheadSelectionActor::message_handler() { [=](playlist::select_all_media_atom) { select_all(); }, + [=](playlist::select_media_atom, const UuidList &media_uuids) { + UuidVector v; + for (auto &i : media_uuids) + v.push_back(i); + delegate(caf::actor_cast(this), playlist::select_media_atom_v, v); + }, + [=](playlist::select_media_atom, const UuidVector &media_uuids, bool retry) -> bool { if (media_uuids.empty()) { - select_one(); + anon_send(this, playlist::select_media_atom_v, utility::UuidVector()); } else { select_media(media_uuids, false); } @@ -159,7 +144,8 @@ caf::message_handler PlayheadSelectionActor::message_handler() { [=](playlist::select_media_atom, const UuidVector &media_uuids) -> bool { if (media_uuids.empty()) { - select_one(); + delayed_anon_send( + this, std::chrono::milliseconds(500), playlist::select_media_atom_v, true); } else { select_media(media_uuids); } @@ -171,7 +157,8 @@ caf::message_handler PlayheadSelectionActor::message_handler() { const bool retry, const playhead::SelectionMode mode) -> bool { if (media_uuids.empty()) { - select_one(); + delayed_anon_send( + this, std::chrono::milliseconds(500), playlist::select_media_atom_v, true); } else { select_media(media_uuids, retry, mode); } @@ -182,19 +169,19 @@ caf::message_handler PlayheadSelectionActor::message_handler() { const UuidVector &media_uuids, const playhead::SelectionMode mode) -> bool { if (media_uuids.empty()) - select_one(); + delayed_anon_send( + this, std::chrono::milliseconds(500), playlist::select_media_atom_v, true); else select_media(media_uuids, true, mode); return true; }, - [=](playlist::media_filter_string, const std::string &filter_string) { - filter_string_ = filter_string; + [=](playlist::select_media_atom, const bool one) { + if (base_.empty()) + select_one(); }, - [=](playlist::media_filter_string) -> std::string { return filter_string_; }, - [=](playlist::select_media_atom) -> bool { // clears the selection select_media(UuidVector(), true, SM_CLEAR); @@ -205,6 +192,13 @@ caf::message_handler PlayheadSelectionActor::message_handler() { select_media(UuidVector({media_uuid}), true, SM_SELECT); }, + [=](playlist::media_filter_string, const std::string &filter_string) { + filter_string_ = filter_string; + }, + + [=](playlist::media_filter_string) -> std::string { return filter_string_; }, + + [=](utility::event_atom, utility::change_atom) { if (current_sender() == playlist_) { // the playlist has changed - if it was previously empty but now @@ -234,9 +228,9 @@ caf::message_handler PlayheadSelectionActor::message_handler() { }, [=](utility::event_atom, playlist::remove_media_atom, const UuidVector &) {}, [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) { - if (base_.items().empty()) { - select_one(); - } + if (base_.empty()) + delayed_anon_send( + this, std::chrono::milliseconds(500), playlist::select_media_atom_v, true); }}; } @@ -342,7 +336,7 @@ void PlayheadSelectionActor::select_media( for (const auto &i : selected) { // make sure they not already active.. if (not media_uas.count(i)) { - spdlog::warn("Invalid media uuid in selection {}", to_string(i)); + spdlog::debug("Invalid media uuid in selection {}", to_string(i)); } else if (media_uas.at(i)) { if (not source_actors_.count(i)) { // add to selection.. @@ -391,7 +385,7 @@ void PlayheadSelectionActor::select_one() { request(playlist_, infinite, playlist::get_media_atom_v) .then( [=](std::vector media_actors) mutable { - if (!media_actors.empty()) { + if (not media_actors.empty()) { // select only the first item in the playlist select_media(UuidVector({media_actors[0].uuid()})); diff --git a/src/playhead/src/string_out_actor.cpp b/src/playhead/src/string_out_actor.cpp index cc8fa121f..eeade6f18 100644 --- a/src/playhead/src/string_out_actor.cpp +++ b/src/playhead/src/string_out_actor.cpp @@ -16,28 +16,25 @@ using namespace xstudio; using namespace xstudio::playhead; using namespace xstudio::media_reader; -StringOutActor::StringOutActor( - caf::actor_config &cfg, - const utility::UuidActorVector &sources) : caf::event_based_actor(cfg), source_actors_(sources) -{ - +StringOutActor::StringOutActor(caf::actor_config &cfg, const utility::UuidActorVector &sources) + : caf::event_based_actor(cfg), source_actors_(sources) { + event_group_ = spawn(this); link_to(event_group_); - - for (auto &source: source_actors_) { + + for (auto &source : source_actors_) { monitor(source.actor()); utility::join_event_group(this, source.actor()); - } set_down_handler([=](down_msg &msg) { // is source down? - auto p = source_actors_.begin(); + auto p = source_actors_.begin(); bool change = false; while (p != source_actors_.end()) { if ((*p).actor() == msg.source) { - p = source_actors_.erase(p); + p = source_actors_.erase(p); change = true; } else { p++; @@ -63,11 +60,9 @@ StringOutActor::StringOutActor( const media::MediaType media_type, const utility::TimeSourceMode tsm, const utility::FrameRate &override_rate) -> caf::result { - auto rp = make_response_promise(); build_frame_map(media_type, tsm, override_rate, rp); return rp; - }, [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) {}, @@ -87,8 +82,7 @@ StringOutActor::StringOutActor( [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { }, - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus) { - }, + [=](utility::event_atom, media::media_status_atom, const media::MediaStatus) {}, [=](utility::event_atom, media::media_display_info_atom, @@ -98,17 +92,19 @@ StringOutActor::StringOutActor( [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &) {}, [=](utility::event_atom, utility::change_atom) { - // one of the sources has changed - this might affect its actual - // frames for display. So we clear the entry in our cache, and + // frames for display. So we clear the entry in our cache, and // re-emit the change event to SubPlayhead auto sender = caf::actor_cast(current_sender()); - for (auto &s: source_actors_) { + for (auto &s : source_actors_) { if (s.actor() == sender) { source_frames_[s.uuid()].reset(); } } - send(event_group_, utility::event_atom_v, utility::change_atom_v); // triggers refresh of frames_time_list_ + send( + event_group_, + utility::event_atom_v, + utility::change_atom_v); // triggers refresh of frames_time_list_ }, [=](utility::event_atom, utility::last_changed_atom, const utility::time_point &) { @@ -126,8 +122,7 @@ StringOutActor::StringOutActor( [=](utility::event_atom, bookmark::bookmark_change_atom, - const utility::Uuid &bookmark_uuid) { - }, + const utility::Uuid &bookmark_uuid) {}, [=](utility::event_atom, bookmark::bookmark_change_atom, @@ -137,12 +132,7 @@ StringOutActor::StringOutActor( [=](utility::event_atom, playlist::reflag_container_atom, const utility::Uuid &, - const std::tuple &) {}, - - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) { - }); - - + const std::tuple &) {}); } void StringOutActor::build_frame_map( @@ -151,11 +141,13 @@ void StringOutActor::build_frame_map( const utility::FrameRate &override_rate, caf::typed_response_promise rp) { + auto count = std::make_shared(0); - for (auto &c: source_actors_) { + for (auto &c : source_actors_) { // we already have frames for this source - if (source_frames_[c.uuid()]) continue; + if (source_frames_[c.uuid()]) + continue; (*count)++; @@ -169,7 +161,6 @@ void StringOutActor::build_frame_map( override_rate) .then( [=](const media::FrameTimeMapPtr &mpts) mutable { - source_frames_[source_uuid] = mpts; (*count)--; if (!(*count)) { @@ -189,21 +180,22 @@ void StringOutActor::build_frame_map( } } -void StringOutActor::finalise_frame_map(caf::typed_response_promise rp) { +void StringOutActor::finalise_frame_map( + caf::typed_response_promise rp) { - media::FrameTimeMap * result = new media::FrameTimeMap; + media::FrameTimeMap *result = new media::FrameTimeMap; timebase::flicks d(0); - for (auto &c: source_actors_) { + for (auto &c : source_actors_) { - if (!source_frames_[c.uuid()]) continue; + if (!source_frames_[c.uuid()]) + continue; const media::FrameTimeMap &map = *(source_frames_[c.uuid()]); timebase::flicks prev(0); - for (const auto &p: map) { + for (const auto &p : map) { d += p.first - prev; (*result)[d] = p.second; - prev = p.first; + prev = p.first; } - } rp.deliver(media::FrameTimeMapPtr(result)); } diff --git a/src/playhead/src/sub_playhead.cpp b/src/playhead/src/sub_playhead.cpp index abf5bd9ea..064a77949 100644 --- a/src/playhead/src/sub_playhead.cpp +++ b/src/playhead/src/sub_playhead.cpp @@ -36,7 +36,7 @@ SubPlayhead::SubPlayhead( const utility::TimeSourceMode time_source_mode, const utility::FrameRate override_frame_rate, const media::MediaType media_type, - const utility::Uuid & uuid) + const utility::Uuid &uuid) : caf::event_based_actor(cfg), name_(name), source_(std::move(source)), @@ -93,11 +93,17 @@ void SubPlayhead::init() { link_to(event_group_); // subscribe to source.. - if (!source_) { - throw std::runtime_error("Creating child playhead actor without a source."); + if (source_) { + monitor(source_.actor()); + utility::join_event_group(this, source_); + // we need a snwsible default rate to pad out frames if we have to extend + // our duration + request(source_.actor(), infinite, utility::rate_atom_v, media_type_) + .then( + [=](const utility::FrameRate &r) mutable { default_rate_ = r; }, + [=](const caf::error &err) mutable {}); } - monitor(source_.actor()); set_down_handler([=](down_msg &msg) { // is source down? @@ -107,7 +113,6 @@ void SubPlayhead::init() { } }); - utility::join_event_group(this, source_); // this triggers us to fetch the frames information from the source anon_send(this, source_atom_v); @@ -129,13 +134,6 @@ void SubPlayhead::init() { return message{}; }); - // we need a snwsible default rate to pad out frames if we have to extend - // our duration - request(source_.actor(), infinite, utility::rate_atom_v, media_type_).then( - [=](const utility::FrameRate &r) mutable { - default_rate_ = r; - }, - [=](const caf::error &err) mutable { }); behavior_.assign( @@ -165,10 +163,6 @@ void SubPlayhead::init() { // unsubscribe(); }, - [=](source_atom, bool uuid) -> utility::Uuid { - return source_.uuid(); - }, - [=](source_atom, utility::time_point update_tp) -> result { // the message will be sent with a delay - the time it was sent // is update_tp, which, if it matches last_change_timepoint_, then @@ -190,7 +184,13 @@ void SubPlayhead::init() { get_full_timeline_frame_list(rp); return rp; }, - + + [=](source_atom, bool retry) -> result { + auto rp = make_response_promise(); + get_full_timeline_frame_list(rp, true); + return rp; + }, + [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) {}, [=](utility::event_atom, playlist::remove_media_atom, const utility::UuidVector &) {}, @@ -214,7 +214,8 @@ void SubPlayhead::init() { return true; }, - [=](duration_flicks_atom atom, bool /*before retiming, extension*/) -> result { + [=](duration_flicks_atom atom, + bool /*before retiming, extension*/) -> result { if (up_to_date_) { if (full_timeline_frames_.size() < 2) { return timebase::flicks(0); @@ -276,9 +277,7 @@ void SubPlayhead::init() { request(caf::actor_cast(this), infinite, source_atom_v) .then( [=](caf::actor) mutable { - rp.deliver( - retimed_frames_.size() ? retimed_frames_.size() - 1 - : 0); + rp.deliver(retimed_frames_.size() ? retimed_frames_.size() - 1 : 0); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; @@ -428,9 +427,7 @@ void SubPlayhead::init() { return result; }, - [=](media::source_offset_frames_atom atom) -> result { - return frame_offset_; - }, + [=](media::source_offset_frames_atom atom) -> result { return frame_offset_; }, [=](media::source_offset_frames_atom atom, const int64_t offset) -> result { if (offset != frame_offset_) { @@ -507,8 +504,7 @@ void SubPlayhead::init() { .then( [=](caf::actor) mutable { auto frame = retimed_frames_.lower_bound(position_flicks_); - if (retimed_frames_.size() && - frame != retimed_frames_.end()) { + if (retimed_frames_.size() && frame != retimed_frames_.end()) { rp.deliver(*(frame->second)); } else { rp.deliver(make_error(xstudio_error::error, "No Frame")); @@ -657,15 +653,19 @@ void SubPlayhead::init() { }, [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { - // when a timeline autoconform is happening it triggers a flood of + // when a timeline autoconform is happening it triggers a flood of // these item_atom events as the timeline is rebuilt. We don't want // to keep requesting frames from the timeline while its still busy - // doing the autoconform. By delaying the update request, and + // doing the autoconform. By delaying the update request, and // checking if another update has come in since this request was // made, we can skip excessive updates up_to_date_ = false; last_change_timepoint_ = utility::clock::now(); - delayed_anon_send(this, std::chrono::milliseconds(50), source_atom_v, last_change_timepoint_); // triggers refresh of frames_time_list_ + delayed_anon_send( + this, + std::chrono::milliseconds(50), + source_atom_v, + last_change_timepoint_); // triggers refresh of frames_time_list_ }, [=](utility::event_atom, media::media_status_atom, const media::MediaStatus) { @@ -740,7 +740,8 @@ 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())); - image_buffer.set_playhead_logical_frame(logical_frame_from_pts(timeline_pts)); + image_buffer.set_playhead_logical_frame( + logical_frame_from_pts(timeline_pts)); add_annotations_data_to_frame(image_buffer); rp.deliver(image_buffer); }, @@ -752,7 +753,9 @@ void SubPlayhead::init() { ImageBufPtr image_buffer, const media::AVFrameID &mptr, const time_point &tp, - const timebase::flicks timeline_pts) { receive_image_from_cache(image_buffer, mptr, tp, timeline_pts); }, + const timebase::flicks timeline_pts) { + receive_image_from_cache(image_buffer, mptr, tp, timeline_pts); + }, [=](playlist::get_media_uuid_atom) -> result { auto rp = make_response_promise(); @@ -771,7 +774,9 @@ void SubPlayhead::init() { request( caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) .then( - [=](const media::AVFrameID &frameid) mutable { rp.deliver(frameid.rate()); }, + [=](const media::AVFrameID &frameid) mutable { + rp.deliver(frameid.rate()); + }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, @@ -819,7 +824,11 @@ void SubPlayhead::init() { [=](utility::event_atom, utility::change_atom) { up_to_date_ = false; last_change_timepoint_ = utility::clock::now(); - delayed_anon_send(this, std::chrono::milliseconds(50), source_atom_v, last_change_timepoint_); // triggers refresh of frames_time_list_ + delayed_anon_send( + this, + std::chrono::milliseconds(50), + source_atom_v, + last_change_timepoint_); // triggers refresh of frames_time_list_ }, [=](utility::event_atom, utility::last_changed_atom, const time_point &) { @@ -849,10 +858,6 @@ void SubPlayhead::init() { 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 - }, - [=](precache_atom) -> result { auto rp = make_response_promise(); update_playback_precache_requests(rp); @@ -931,13 +936,24 @@ void SubPlayhead::set_position( timebase::flicks frame_period, timeline_pts; std::shared_ptr frame = get_frame(time, frame_period, timeline_pts); - int logical_frame = logical_frame_from_pts(timeline_pts); - + int logical_frame = logical_frame_from_pts(timeline_pts); + + if (playing && precache_start_frame_ != std::numeric_limits::lowest()) { + // a big stream of static pre-cache frame requests is probably sitting + // with the reader. But we are playing back now so we need to cancel + // the pre-cache requests + precache_start_frame_ = std::numeric_limits::lowest(); + anon_send( + pre_reader_, + media_reader::static_precache_atom_v, + media::AVFrameIDsAndTimePoints(), + uuid_); + } if (logical_frame_ != logical_frame || force_updates || scrubbing) { const bool logical_changed = logical_frame_ != logical_frame; - logical_frame_ = logical_frame; + logical_frame_ = logical_frame; auto now = utility::clock::now(); @@ -978,6 +994,12 @@ void SubPlayhead::set_position( } } + if (media_type_ == media::MediaType::MT_AUDIO) { + // we always broadcast the current audio frames - this is for + // visualisation only not sounding. + broadcast_audio_samples(); + } + // update the parent playhead with our position if (frame && (previous_frame_ != frame || force_updates || logical_changed)) { anon_send( @@ -1079,7 +1101,6 @@ void SubPlayhead::broadcast_image_frame( .then( [=](ImageBufPtr image_buffer) mutable { - image_buffer.when_to_display_ = when_to_show_frame; image_buffer.set_timline_timestamp(timeline_pts); image_buffer.set_frame_id(*(frame_media_pointer.get())); @@ -1089,7 +1110,7 @@ void SubPlayhead::broadcast_image_frame( send( parent_, show_atom_v, - uuid_, // the uuid of this playhead + uuid_, // the uuid of this playhead image_buffer, // the image true // is this the frame that should be on-screen now? ); @@ -1167,6 +1188,7 @@ void SubPlayhead::broadcast_audio_frame( auto delta = std::chrono::duration_cast( next_frame->first - frame->first); future_frames.emplace_back(tt + delta, next_frame->second); + tps.emplace_back(next_frame->first); next_frame++; if (next_frame != retimed_frames_.end() && next_frame->second) { auto delta = std::chrono::duration_cast( @@ -1210,6 +1232,70 @@ void SubPlayhead::broadcast_audio_frame( }); } +void SubPlayhead::broadcast_audio_samples() { + + // this is a lot like broadcast_audio_frame but we broadcast audio frames + // either side of and including the current audio frame - this is used + // for audio waveform visualisation on the viewport, for example + media::AVFrameIDsAndTimePoints future_frames; + std::vector tps; + + // pick the next 1 or two frames in the timeline to send audio + // samples for sounding + auto frame = current_frame_iterator(); + if (frame == retimed_frames_.end()) { + return; + } + + // step backwards two frames + if (frame != retimed_frames_.begin()) + frame--; + if (frame != retimed_frames_.begin()) + frame--; + + int i = 0; + auto tt = utility::clock::now(); + while (frame != retimed_frames_.end() && i < 5) { + if (frame->second) { + future_frames.emplace_back(tt, frame->second); + tps.emplace_back(frame->first); + } + i++; + frame++; + } + + const auto playhead_position = position_flicks_; + + // now fetch audio samples for playback + request( + pre_reader_, + std::chrono::milliseconds(5000), + media_reader::get_audio_atom_v, + future_frames, + uuid_) + .then( + + [=](std::vector &audio_buffers) mutable { + auto ab = audio_buffers.begin(); + auto fp = future_frames.begin(); + auto tt = tps.begin(); + while (ab != audio_buffers.end() && fp != future_frames.end()) { + ab->when_to_display_ = (*fp).first; + ab->set_timline_timestamp(*(tt++)); + ab++; + fp++; + } + send( + parent_, + audio::audio_samples_atom_v, + audio_buffers, // the uuid of this playhead + playhead_position); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + std::vector SubPlayhead::get_lookahead_frame_pointers( media::AVFrameIDsAndTimePoints &result, const int max_num_frames) { @@ -1312,11 +1398,14 @@ void SubPlayhead::update_playback_precache_requests(caf::typed_response_promise< make_prefetch_requests_for_colour_pipeline(requests); request( - pre_reader_, infinite, media_reader::playback_precache_atom_v, requests, uuid_, media_type_) + pre_reader_, + infinite, + media_reader::playback_precache_atom_v, + requests, + uuid_, + media_type_) .then( - [=](const bool requests_processed) mutable { - rp.deliver(requests_processed); - }, + [=](const bool requests_processed) mutable { rp.deliver(requests_processed); }, [=](const error &err) mutable { rp.deliver(err); }); } @@ -1338,8 +1427,7 @@ void SubPlayhead::make_static_precache_request( make_prefetch_requests_for_colour_pipeline(requests); - request( - pre_reader_, infinite, media_reader::static_precache_atom_v, requests, uuid_) + request(pre_reader_, infinite, media_reader::static_precache_atom_v, requests, uuid_) .await( [=](bool requests_processed) mutable { rp.deliver(requests_processed); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -1399,7 +1487,7 @@ void SubPlayhead::receive_image_from_cache( send( parent_, show_atom_v, - uuid_, // the uuid of this playhead + uuid_, // the uuid of this playhead image_buffer, // the image true // this image supposed to be shown on-screen NOW ); @@ -1425,8 +1513,18 @@ void SubPlayhead::get_full_timeline_frame_list( return; } - if (!retry) { - inflight_update_requests_.push_back(rp); + + inflight_update_requests_.push_back(rp); + + if (!source_.actor()) { + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + return; } // check if we've already requested an update (in the request immediately @@ -1448,7 +1546,6 @@ void SubPlayhead::get_full_timeline_frame_list( override_frame_rate_) .then( [=](const media::FrameTimeMapPtr &mpts) mutable { - if (mpts) { full_timeline_frames_ = *mpts; } else { @@ -1456,10 +1553,12 @@ void SubPlayhead::get_full_timeline_frame_list( } update_retiming(); - + /*auto tp = utility::clock::now(); if (media_type_ == media::MT_IMAGE) { - std::cerr << "VID FRAMES GEN DELAY " << to_string(uuid_) << " " << std::chrono::duration_cast(tp-request_update_timepoint).count() << "\n"; + std::cerr << "VID FRAMES GEN DELAY " << to_string(uuid_) << " " << + std::chrono::duration_cast(tp-request_update_timepoint).count() + << "\n"; }*/ // last step is to get all info on bookmarks for the media that @@ -1484,6 +1583,7 @@ void SubPlayhead::get_full_timeline_frame_list( for (auto &rprm : inflight_update_requests_) { rprm.deliver(source_.actor()); } + inflight_update_requests_.clear(); send( parent_, @@ -1491,7 +1591,6 @@ void SubPlayhead::get_full_timeline_frame_list( playhead::media_frame_ranges_atom_v, media_ranges_); - inflight_update_requests_.clear(); if (request_update_timepoint < last_change_timepoint_) { // One last check: @@ -1507,7 +1606,6 @@ void SubPlayhead::get_full_timeline_frame_list( } }, [=](const error &err) mutable { - // rp.deliver(err); for (auto &rprm : inflight_update_requests_) { rprm.deliver(err); } @@ -1515,18 +1613,81 @@ void SubPlayhead::get_full_timeline_frame_list( }); }, [=](const error &err) mutable { - if (to_string(err) == "error(\"No streams\")" && - media_type_ == media::MT_IMAGE) { - // We're here because the source has no IMAGE streams ... we therefore - // switch ourselves to playing audio instead. The reason is that the - // PlayheadActor always creates a master IMAGE type SubPlayhead to drive the - // duration & frame rate of the timeline. If we have an audio only source, - // though, this fails as the duration would be nil, so we can legitimately - // switch ourselves here to try and play audio instead of video so that the - // parent PlayheadActor is returned a finite duration and frame rate etc - // when it asks the SubPlayhead for that info. - media_type_ = media::MT_AUDIO; - get_full_timeline_frame_list(rp, true); + if (to_string(err) == "error(\"No streams\")" || + to_string(err) == "error(\"No MediaSources\")") { + + // We're here because the source has no streams, or it has no media - it + // could be still building itself so we try one more time (if retry flag is + // false). Otherwise we set ourselves up with empty frames but no error + // state passed back + if (retry) { + + if (media_type_ == media::MT_IMAGE) { + // we have no image streams/sources. However, there might be a + // valid AUDIO source. In this case, we want to build a blank + // image frames to align with audio frames, as the main + // PlayheadActor requires a valid IMAGE source to drive frame-rate, + // duration etc. + request( + source_.actor(), + infinite, + media::get_media_pointers_atom_v, + media::MT_AUDIO, + time_source_mode_, + override_frame_rate_) + .then( + [=](const media::FrameTimeMapPtr &mpts) mutable { + if (mpts) { + // replace the Audio frame ptrs with + // blank video frames + full_timeline_frames_ = *mpts; + auto p = full_timeline_frames_.begin(); + while (p != full_timeline_frames_.end()) { + p->second = media::make_blank_frame( + p->second->rate(), media_type_); + p++; + } + + } else { + full_timeline_frames_.clear(); + } + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + }, + [=](const error &err) mutable { + // audio frames fetch aslo failing - fallback to empty + // frame map. + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + }); + } else { + + // still no media stream/source + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + } + + } else { + delayed_anon_send( + caf::actor_cast(this), + std::chrono::milliseconds(250), + source_atom_v, + true); + } } else { // rp.deliver(err); @@ -1630,85 +1791,72 @@ timebase::flicks SubPlayhead::get_next_or_previous_clip_start_position( return result; } - auto frame = current_frame_iterator(ref_position); + timebase::flicks frame_period, timeline_pts; + std::shared_ptr frame = + get_frame(ref_position, frame_period, timeline_pts); + int logical = logical_frame_from_pts(timeline_pts); - utility::Uuid curr_frame_media_uuid = - frame->second ? frame->second->media_uuid() : utility::Uuid(); if (next_clip) { - while (frame != retimed_frames_.end()) { - - if (frame->second && frame->second->media_uuid() != curr_frame_media_uuid) { - result = frame->first; + for (int i = 0; i < (int)media_ranges_.size(); ++i) { + if (media_ranges_[i] > logical) { + logical = media_ranges_[i]; break; } - frame++; } } else { - - if (frame != retimed_frames_.begin()) { - // step back one frame. Has the media changed? - frame--; - auto prev_frame_media_uuid = - frame->second ? frame->second->media_uuid() : utility::Uuid(); - if (prev_frame_media_uuid == curr_frame_media_uuid) { - // nope, we're in the same media .. so we need to keep going - // back only until we hit some new media - } else { - // yes, so we need to go all the way back to the start of - // this previous clip - curr_frame_media_uuid = prev_frame_media_uuid; - } - } - - while (frame != retimed_frames_.begin()) { - - if (frame->second && frame->second->media_uuid() != curr_frame_media_uuid) { - // we've got to the last frame of the previous source. - // Step forward one frame to get to the first frame of - // this source - frame++; - result = frame->first; + for (int i = (int(media_ranges_.size()) - 1); i >= 0; --i) { + if (media_ranges_[i] < logical) { + logical = media_ranges_[i]; break; } - frame--; - } - - if (frame == retimed_frames_.begin()) { - result = frame->first; } } + // woopsie - this loop is not an efficient way to work out if + // we will hit the end frame!! + auto f = retimed_frames_.begin(); + while (logical && f != retimed_frames_.end()) { + f++; + logical--; + } + if (f == retimed_frames_.end()) + f--; + result = f->first; return result; } void SubPlayhead::update_retiming() { - // to do retiming we insert held frames at the start or end of + // to do retiming we insert held frames at the start or end of // full_timeline_frames_, or conversely we trim frames off the start or // end. - + auto retimed_frames = full_timeline_frames_; if (!retimed_frames.size()) { // provide a single blank frame, which can then be extended in the loop // below to fill the forced duration. - retimed_frames[timebase::flicks(0)] = media::make_blank_frame(default_rate_, media_type_); + retimed_frames[timebase::flicks(0)] = + media::make_blank_frame(default_rate_, media_type_); } - + if (frame_offset_ > 0) { - + // if offset is forward, we remove frames at the front auto p = retimed_frames.begin(); - std::advance(p, std::min(int64_t(retimed_frames.size())-1, frame_offset_)); + std::advance(p, std::min(int64_t(retimed_frames.size()) - 1, frame_offset_)); retimed_frames.erase(retimed_frames.begin(), p); - + } else if (frame_offset_ < 0) { - // negative offset means we extend the beginning with held frames, + // negative offset means we extend the beginning with held frames, // i.e. duplicate the first frame - unless we are timeline (where we // want a blank frame, not held frame, or audio where we want silence) - auto first_frame = (source_is_timeline_ || media_type_ == media::MT_AUDIO) ? media::make_blank_frame(retimed_frames.begin()->second->rate(), media_type_) : retimed_frames.begin()->second; + auto first_frame = + (source_is_timeline_ || media_type_ == media::MT_AUDIO) + ? media::make_blank_frame(retimed_frames.begin()->second->rate(), media_type_) + : retimed_frames.begin()->second; timebase::flicks frame_duration = override_frame_rate_; if (time_source_mode_ != TimeSourceMode::FIXED && first_frame) { @@ -1716,42 +1864,44 @@ void SubPlayhead::update_retiming() { } int64_t off = frame_offset_; while (off < 0) { - retimed_frames[retimed_frames.begin()->first-frame_duration] = first_frame; + retimed_frames[retimed_frames.begin()->first - frame_duration] = first_frame; off++; } - } // now we need to rebase retimed_frames so that first frame is at t=0 retimed_frames_.clear(); if (retimed_frames.size()) { auto t0 = retimed_frames.begin()->first; - for (auto & p: retimed_frames) { - retimed_frames_[p.first-t0] = p.second; + for (auto &p : retimed_frames) { + retimed_frames_[p.first - t0] = p.second; } } if (forced_duration_ != timebase::k_flicks_zero_seconds) { // trim end frame off until our duration is leq than forced_duration_ - while (retimed_frames_.size() > 1 && retimed_frames_.rbegin()->first >= forced_duration_) { + while (retimed_frames_.size() > 1 && + retimed_frames_.rbegin()->first >= forced_duration_) { retimed_frames_.erase(std::prev(retimed_frames_.end())); } // if forced_duration_ extends beyond the last entry in retimed_frames_, - // we duplicate the last frame until this is no longer the case, + // we duplicate the last frame until this is no longer the case, // i.e. held frame behaviour. UNLESS the source is a timeline, in whcih // case we want blank frames, or if we're audio playhead in which case // we want silence - auto last_frame = (source_is_timeline_ || media_type_ == media::MT_AUDIO) ? media::make_blank_frame(retimed_frames_.rbegin()->second->rate(), media_type_) : retimed_frames_.rbegin()->second; + auto last_frame = + (source_is_timeline_ || media_type_ == media::MT_AUDIO) + ? media::make_blank_frame(retimed_frames_.rbegin()->second->rate(), media_type_) + : retimed_frames_.rbegin()->second; timebase::flicks frame_duration = override_frame_rate_; if (time_source_mode_ != TimeSourceMode::FIXED && last_frame) { frame_duration = last_frame->rate(); } - while ((retimed_frames_.rbegin()->first+frame_duration) < forced_duration_) { - retimed_frames_[retimed_frames_.rbegin()->first+frame_duration] = last_frame; + while ((retimed_frames_.rbegin()->first + frame_duration) < forced_duration_) { + retimed_frames_[retimed_frames_.rbegin()->first + frame_duration] = last_frame; } - } @@ -1771,15 +1921,11 @@ void SubPlayhead::update_retiming() { ? override_frame_rate_ : retimed_frames_.rbegin()->second->rate(); retimed_frames_[last_frame_timepoint].reset(); - - - } store_media_frame_ranges(); set_in_and_out_frames(); - } void SubPlayhead::store_media_frame_ranges() { @@ -1809,11 +1955,9 @@ void SubPlayhead::store_media_frame_ranges() { } else if (!f.second) clip_uuid = utility::Uuid(); logical_frames_[f.first] = logical_frame++; - } media_ranges_.push_back(logical_frame); - } void SubPlayhead::set_in_and_out_frames() { @@ -1952,12 +2096,12 @@ void SubPlayhead::full_bookmarks_update(caf::typed_response_promise done) }, [=](const error &err) mutable { done.deliver(false); - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn("A {} {}", __PRETTY_FUNCTION__, to_string(err)); }); }, [=](const error &err) mutable { done.deliver(false); - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); }); } @@ -1984,7 +2128,6 @@ void SubPlayhead::extend_bookmark_frame( void SubPlayhead::fetch_bookmark_annotations( BookmarkRanges bookmark_ranges, caf::typed_response_promise rp) { - // TODO: this bookmark evaluation code is horrible! refactor. if (!bookmark_ranges.size()) { @@ -2088,6 +2231,12 @@ void SubPlayhead::fetch_bookmark_annotations( utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_); + + anon_send( + parent_, + jump_atom_v, + caf::actor_cast(this)); + rp.deliver(true); } }, @@ -2162,6 +2311,10 @@ void SubPlayhead::bookmark_changed(const utility::UuidActor bookmark) { } } + // null bookmark actor means a bookmark has been deleted + if (!bookmark.actor()) + return; + // even though this doesn't look like our bookmark, the change that has // happened to it might have been associating it with media that IS in // our timeline, in which case we need to rebuild our bookmarks data @@ -2179,27 +2332,26 @@ void SubPlayhead::bookmark_changed(const utility::UuidActor bookmark) { } }, [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + to_string(bookmark.uuid())); }); } -media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator(const timebase::flicks t) -{ +media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator(const timebase::flicks t) { auto frame = retimed_frames_.upper_bound(t); if (frame != retimed_frames_.begin()) { frame--; } return frame; - } -media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator() -{ +media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator() { auto frame = retimed_frames_.upper_bound(position_flicks_); if (frame != retimed_frames_.begin()) { frame--; } return frame; - } - diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index 30458563e..03b493eaf 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -197,6 +197,10 @@ PlaylistActor::PlaylistActor( std::chrono::milliseconds(50)); } + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + link_to(json_store_); join_event_group(this, json_store_); // media needs to exist before we can deserialise containers. @@ -251,13 +255,19 @@ PlaylistActor::PlaylistActor( } catch (const std::exception &e) { spdlog::error("{}", e.what()); } - } - } + } else if (value.at("base").at("container").at("type") == "PlayheadSelection") { - selection_actor_ = spawn( - "PlaylistPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); + try { + selection_actor_ = system().spawn( + static_cast(value), caf::actor_cast(this)); + link_to(selection_actor_); + + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } + } init(); } @@ -420,8 +430,8 @@ caf::message_handler PlaylistActor::message_handler() { // uuid_before); const auto uuid = Uuid::generate(); - std::string ext = - ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension().string()), '.'); + std::string ext = ltrim_char( + to_upper(fs::path(uri_to_posix_path(uri)).extension().string()), '.'); const auto source_uuid = Uuid::generate(); auto source = @@ -1184,6 +1194,15 @@ caf::message_handler PlaylistActor::message_handler() { for (const auto &i : media_) actors.push_back(i.second); + if (actors.empty()) { + if (return_null) { + rp.deliver(caf::actor()); + } else { + rp.deliver(make_error(xstudio_error::error, "No matching media source")); + } + return rp; + } + fan_out_request( actors, infinite, media::get_media_source_atom_v, uuid, true) .then( @@ -1451,7 +1470,6 @@ caf::message_handler PlaylistActor::message_handler() { // create a new timeline, attach it to new playhead. [=](playlist::create_playhead_atom) -> result { - if (playhead_) return playhead_; @@ -1459,7 +1477,12 @@ caf::message_handler PlaylistActor::message_handler() { ss << base_.name() << " Playhead"; auto uuid = utility::Uuid::generate(); auto actor = spawn( - ss.str(), selection_actor_, uuid, caf::actor_cast(this)); + ss.str(), + playhead::GLOBAL_AUDIO, + selection_actor_, + uuid, + caf::actor_cast(this)); + anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); playhead_ = UuidActor(uuid, actor); link_to(playhead_.actor()); @@ -1480,6 +1503,12 @@ caf::message_handler PlaylistActor::message_handler() { playlist::select_media_atom_v, utility::UuidVector()); } + if (!playhead_serialisation_.is_null()) { + anon_send( + playhead_.actor(), + module::deserialise_atom_v, + playhead_serialisation_); + } }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1956,10 +1985,7 @@ caf::message_handler PlaylistActor::message_handler() { clients.push_back(i.second); for (const auto &i : container_) clients.push_back(i.second); - - // Playhead serialisation disabled for now - /*if (playhead_) - clients.push_back(playhead_);*/ + clients.push_back(selection_actor_); if (not clients.empty()) { @@ -1968,22 +1994,40 @@ caf::message_handler PlaylistActor::message_handler() { .then( [=](std::vector json) mutable { JsonStore jsn; - jsn["store"] = meta; - jsn["base"] = base_.serialise(); - jsn["playheads"] = {}; - jsn["actors"] = {}; + jsn["store"] = meta; + jsn["base"] = base_.serialise(); + jsn["actors"] = {}; for (const auto &j : json) { - if (j["base"]["container"]["type"] - .get() == "Playhead") { - jsn["playheads"][static_cast( - j["base"]["container"]["uuid"])] = j; - - } else { - jsn["actors"][static_cast( - j["base"]["container"]["uuid"])] = j; + jsn["actors"][static_cast( + j["base"]["container"]["uuid"])] = j; + } + if (playhead_) { + request( + playhead_.actor(), + infinite, + utility::serialise_atom_v) + .then( + [=](const utility::JsonStore + &playhead_state) mutable { + playhead_serialisation_ = + playhead_state; + jsn["playhead"] = playhead_state; + rp.deliver(jsn); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(jsn); + }); + + } else { + if (!playhead_serialisation_.is_null()) { + jsn["playhead"] = playhead_serialisation_; } + rp.deliver(jsn); } - rp.deliver(jsn); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -2015,7 +2059,7 @@ void PlaylistActor::init() { if (!selection_actor_) { selection_actor_ = spawn( - "PlaylistPlayheadSelectionActor", caf::actor_cast(this)); + base_.name() + " SelectionActor", caf::actor_cast(this)); link_to(selection_actor_); } @@ -2241,7 +2285,6 @@ void PlaylistActor::duplicate_tree(utility::UuidTree &tre *sys, container_[tree.value().uuid()], duplicate_atom_v); // need a list of source/dest media uuids. - if (type == "Timeline") anon_send( result.actor(), @@ -2542,6 +2585,8 @@ void PlaylistActor::sort_by_media_display_info( // put it after the last element that did have a sort key std::string sort_key = fmt::format("ZZZZZZ{}", idx); + std::cerr << "media_display_info " << media_display_info.dump() << "\n"; + if (media_display_info.is_array() && sort_column_index < media_display_info.size()) { sort_key = media_display_info[sort_column_index].dump(); diff --git a/src/plugin/colour_op/grading/src/grading.cpp b/src/plugin/colour_op/grading/src/grading.cpp index 543c9ae43..ad8436e5b 100644 --- a/src/plugin/colour_op/grading/src/grading.cpp +++ b/src/plugin/colour_op/grading/src/grading.cpp @@ -65,12 +65,6 @@ GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_ working_space_ = add_string_attribute("working_space", "working_space", ""); working_space_->expose_in_ui_attrs_group("grading_settings"); - grade_in_ = add_integer_attribute("grade_in", "grade_in", -1); - grade_in_->expose_in_ui_attrs_group("grading_settings"); - - grade_out_ = add_integer_attribute("grade_out", "grade_out", -1); - grade_out_->expose_in_ui_attrs_group("grading_settings"); - // Slope slope_ = add_float_vector_attribute( "Slope", @@ -251,7 +245,9 @@ GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_ } utility::BlindDataObjectPtr GradingTool::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string & /*viewport_name*/) const { + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid & /*playhead_uuid*/) const { // 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 @@ -305,15 +301,35 @@ void GradingTool::images_going_on_screen( // 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 && images) { - + if (images && current_frame_id_ != images->hero_image().frame_id()) { + + current_viewport_ = viewport_name; viewport_current_images_ = images; - playhead_media_frame_ = images->hero_image().frame_id().frame() - images->hero_image().frame_id().first_frame(); + playhead_media_frame_ = images->hero_image().frame_id().frame() - + images->hero_image().frame_id().first_frame(); + current_frame_id_ = images->hero_image().frame_id(); + + if (!grading_data_.bookmark_uuid_.is_null()) { + // here we check if the current edited bookmark is still on-screen, + // i.e. has the user scrubbed away from the frame with a grade on + // that they were editing? + bool current_grade_is_onscreen = false; + for (const auto &bm : images->hero_image().bookmarks()) { + if (bm->detail_.uuid_ == grading_data_.bookmark_uuid_) { + current_grade_is_onscreen = true; + break; + } + } + if (!current_grade_is_onscreen) { + select_bookmark(utility::Uuid()); + } + } - } else { + } else if (!images) { viewport_current_images_.reset(); + select_bookmark(utility::Uuid()); + current_frame_id_ = media::AVFrameID(); } - } void GradingTool::on_screen_media_changed( @@ -332,7 +348,6 @@ void GradingTool::on_screen_media_changed( working_space_->set_value(working_space); media_colour_managed_->set_value(!is_unmanaged); - } void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { @@ -384,6 +399,38 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i } else if (grading_action_->value() == "Remove CC") { remove_bookmark(); + + } else if (utility::starts_with(grading_action_->value(), "Set Bookmark Full Range")) { + + auto d = utility::split(grading_action_->value(), '|'); + if (d.size() != 2) + return; + utility::Uuid bm_uuid(d[1]); + auto bmd = get_bookmark_detail(bm_uuid); + if (bmd.media_reference_) { + + bmd.start_ = timebase::k_flicks_low; + bmd.duration_ = timebase::k_flicks_max; + update_bookmark_detail(bm_uuid, bmd); + } + select_bookmark(bm_uuid); + + } else if (utility::starts_with(grading_action_->value(), "Set Bookmark One Frame")) { + + auto d = utility::split(grading_action_->value(), '|'); + if (d.size() != 3) + return; + utility::Uuid bm_uuid(d[1]); + auto bmd = get_bookmark_detail(bm_uuid); + int media_frame = std::atoi(d[2].c_str()); + if (bmd.media_reference_) { + auto &media = bmd.media_reference_.value(); + bmd.start_ = media_frame * media.rate().to_flicks(); + bmd.duration_ = timebase::k_flicks_zero_seconds; + update_bookmark_detail(bm_uuid, bmd); + } + + select_bookmark(bm_uuid); } grading_action_->set_value(""); @@ -446,64 +493,6 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i refresh_current_grade_from_ui(); - } else if (grade_in_ && attribute_uuid == grade_in_->uuid()) { - - auto bmd = get_bookmark_detail(current_bookmark()); - if (bmd.media_reference_) { - - auto &media = bmd.media_reference_.value(); - - if (grade_in_->value() == -1) { - grade_out_->set_value(-1, false); - } else if (grade_out_->value() == -1) { - grade_out_->set_value(media.frame_count(), false); - } - - if (grade_in_->value() > grade_out_->value()) { - grade_out_->set_value(grade_in_->value()); - } - - if (grade_in_->value() != -1 && grade_out_->value() != -1) { - bmd.start_ = grade_in_->value() * media.rate().to_flicks(); - bmd.duration_ = std::min( - (grade_out_->value() - grade_in_->value()) * media.rate().to_flicks(), - media.frame_count() * media.rate().to_flicks()); - } else { - bmd.start_ = timebase::k_flicks_low; - bmd.duration_ = timebase::k_flicks_max; - } - - update_bookmark_detail(current_bookmark(), bmd); - } - - } else if (grade_out_ && attribute_uuid == grade_out_->uuid()) { - - auto bmd = get_bookmark_detail(current_bookmark()); - if (bmd.media_reference_) { - - auto &media = bmd.media_reference_.value(); - - if (grade_out_->value() == -1) { - grade_in_->set_value(-1, false); - } else if (grade_in_->value() == -1) { - grade_in_->set_value(0, false); - } - - if (grade_out_->value() < grade_in_->value()) { - grade_in_->set_value(grade_out_->value()); - } - - if (grade_in_->value() != -1 && grade_out_->value() != -1) { - bmd.start_ = grade_in_->value() * media.rate().to_flicks(); - bmd.duration_ = - (grade_out_->value() - grade_in_->value()) * media.rate().to_flicks(); - } else { - bmd.start_ = timebase::k_flicks_low; - bmd.duration_ = timebase::k_flicks_max; - } - update_bookmark_detail(current_bookmark(), bmd); - } - } else if (colour_space_ && attribute_uuid == colour_space_->uuid()) { refresh_current_grade_from_ui(); @@ -1038,19 +1027,6 @@ void GradingTool::refresh_ui_from_current_grade() { mask_selected_shape_->set_value(mask_shapes_.size()); else mask_selected_shape_->set_value(-1); - - auto bmd = get_bookmark_detail(current_bookmark()); - if (bmd.media_reference_ && bmd.start_ && bmd.start_.value() != timebase::k_flicks_low && - bmd.duration_ && bmd.duration_.value() != timebase::k_flicks_max) { - - auto &media = bmd.media_reference_.value(); - grade_in_->set_value(bmd.start_.value() / media.rate().to_flicks(), false); - grade_out_->set_value( - (bmd.start_.value() + bmd.duration_.value()) / media.rate().to_flicks(), false); - } else { - grade_in_->set_value(-1, false); - grade_out_->set_value(-1, false); - } } utility::Uuid GradingTool::current_bookmark() const { @@ -1060,7 +1036,9 @@ utility::Uuid GradingTool::current_bookmark() const { utility::UuidList GradingTool::current_clip_bookmarks() { - //const utility::UuidList bookmarks_list = get_bookmarks_on_current_media(current_viewport_); + // const utility::UuidList bookmarks_list = + utility::UuidList d = get_bookmarks_on_current_media(current_viewport_); + utility::UuidList filtered_list; if (viewport_current_images_ && viewport_current_images_->hero_image()) { @@ -1119,8 +1097,6 @@ void GradingTool::create_bookmark() { void GradingTool::select_bookmark(const utility::Uuid &uuid) { - // spdlog::warn("Select bookmark {}", utility::to_string(uuid)); - if (grading_data_.bookmark_uuid_ == uuid) return; @@ -1145,18 +1121,28 @@ void GradingTool::select_bookmark(const utility::Uuid &uuid) { void GradingTool::update_boomark_shape(const utility::Uuid &uuid) { - auto bmd = get_bookmark_detail(uuid); + auto bmd = get_bookmark_detail(uuid); + bool update = false; if (bmd.user_data_) { (*bmd.user_data_)["mask_active"] = !mask_shapes_.empty(); - update_bookmark_detail(uuid, bmd); + update = true; } // First shape added on the layer, switch to Single frame mode if (mask_shapes_.size() == 1) { - grade_in_->set_value(playhead_media_frame_); - grade_out_->set_value(playhead_media_frame_); + if (bmd.media_reference_) { + + auto &media = bmd.media_reference_.value(); + bmd.start_ = playhead_media_frame_ * media.rate().to_flicks(); + bmd.duration_ = timebase::k_flicks_zero_seconds; + update = true; + } + } + + if (update) { + update_bookmark_detail(uuid, bmd); } } @@ -1175,14 +1161,9 @@ void GradingTool::remove_bookmark() { if (current_bookmark()) { // spdlog::warn("Removing bookmark {}", utility::to_string(current_bookmark())); - StandardPlugin::remove_bookmark(current_bookmark()); - } - - auto clip_layers = current_clip_bookmarks(); - if (!clip_layers.empty()) { - select_bookmark(clip_layers.back()); - } else { + auto bm = current_bookmark(); select_bookmark(utility::Uuid()); + StandardPlugin::remove_bookmark(bm); } } diff --git a/src/plugin/colour_op/grading/src/grading.h b/src/plugin/colour_op/grading/src/grading.h index c6d9c21d2..b3d47b742 100644 --- a/src/plugin/colour_op/grading/src/grading.h +++ b/src/plugin/colour_op/grading/src/grading.h @@ -25,7 +25,8 @@ class GradingTool : public plugin::StandardPlugin { ~GradingTool() override = default; utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/) const override; + const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/, + const utility::Uuid &/*playhead_uuid*/) const override; // Annotations (grading) @@ -104,8 +105,6 @@ class GradingTool : public plugin::StandardPlugin { module::StringAttribute *grading_bookmark_ {nullptr}; module::StringAttribute *working_space_ {nullptr}; module::StringChoiceAttribute *colour_space_ {nullptr}; - module::IntegerAttribute *grade_in_ {nullptr}; - module::IntegerAttribute *grade_out_ {nullptr}; module::FloatVectorAttribute *slope_ {nullptr}; module::FloatVectorAttribute *offset_ {nullptr}; @@ -153,6 +152,8 @@ class GradingTool : public plugin::StandardPlugin { int playhead_media_frame_ = {0}; media_reader::ImageBufDisplaySetPtr viewport_current_images_; + std::string current_viewport_; + media::AVFrameID current_frame_id_; // Grading ui::viewport::GradingData grading_data_; 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 6e8556f5f..fbe7b296e 100644 --- a/src/plugin/colour_op/grading/src/grading_colour_op.cpp +++ b/src/plugin/colour_op/grading/src/grading_colour_op.cpp @@ -334,17 +334,20 @@ std::shared_ptr GradingColourOperator::setup_shader_data( fs_luts.insert(fs_luts.end(), luts.begin(), luts.end()); } - gradingop_shader_ = std::make_shared(PLUGIN_UUID, fs_str); - colour_op_data->shader_ = gradingop_shader_; - colour_op_data->set_cache_id(fmt::format("{}", std::hash{}(fragment_shader))); + gradingop_shader_ = std::make_shared(PLUGIN_UUID, fs_str); + colour_op_data->shader_ = gradingop_shader_; + colour_op_data->set_cache_id( + fmt::format("{}", std::hash{}(fragment_shader))); for (const auto &lut : fs_luts) { - colour_op_data->set_cache_id(colour_op_data->cache_id() + fmt::format("{}",lut->cache_id())); + colour_op_data->set_cache_id( + colour_op_data->cache_id() + fmt::format("{}", lut->cache_id())); } return colour_op_data; } -plugin::GPUPreDrawHookPtr GradingColourOperator::make_pre_draw_gpu_hook() { - return std::make_shared(); +plugin::GPUPreDrawHookPtr +GradingColourOperator::make_pre_draw_gpu_hook(const std::string &viewport_name) { + return std::make_shared(viewport_name); } OCIO::ConstGpuShaderDescRcPtr GradingColourOperator::setup_ocio_shader( diff --git a/src/plugin/colour_op/grading/src/grading_colour_op.hpp b/src/plugin/colour_op/grading/src/grading_colour_op.hpp index 031642553..e6c4ae07e 100644 --- a/src/plugin/colour_op/grading/src/grading_colour_op.hpp +++ b/src/plugin/colour_op/grading/src/grading_colour_op.hpp @@ -32,7 +32,7 @@ class GradingColourOperator : public ColourOpPlugin { utility::JsonStore update_shader_uniforms(const media_reader::ImageBufPtr &image) override; - plugin::GPUPreDrawHookPtr make_pre_draw_gpu_hook() override; + plugin::GPUPreDrawHookPtr make_pre_draw_gpu_hook(const std::string &viewport_name) override; protected: caf::message_handler message_handler_extensions() override; diff --git a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp index 25c6dcb1e..6e70fce54 100644 --- a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp @@ -17,7 +17,8 @@ using namespace xstudio::ui::opengl; using namespace xstudio::ui::viewport; -GradingMaskRenderer::GradingMaskRenderer() { +GradingMaskRenderer::GradingMaskRenderer(const std::string &viewport_name) + : viewport_name_(viewport_name) { canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); } @@ -134,7 +135,8 @@ void GradingMaskRenderer::render_grading_data_masks( // 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_->set_cache_id(image.colour_pipe_data_->cache_id() + cache_id_modifier); + image.colour_pipe_data_->set_cache_id( + image.colour_pipe_data_->cache_id() + cache_id_modifier); } void GradingMaskRenderer::render_layer( @@ -175,6 +177,7 @@ void GradingMaskRenderer::render_layer( 2.0f / mask_resolution.x, // See A1 have_alpha_buffer, image_aspect_ratio); + } else { glClearColor(1.0, 1.0, 1.0, 1.0); diff --git a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h index 283ed1d89..4437ed4d3 100644 --- a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h @@ -31,7 +31,7 @@ namespace viewport { public: - GradingMaskRenderer(); + GradingMaskRenderer(const std::string &viewport_name); void pre_viewport_draw_gpu_hook( const Imath::M44f &transform_window_to_viewport_space, @@ -59,6 +59,7 @@ namespace viewport { std::vector render_layers_; std::unique_ptr canvas_renderer_; + const std::string viewport_name_; }; using GradingMaskRendererSPtr = std::shared_ptr; diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml index ab7680b4b..4db97e00f 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml @@ -71,20 +71,6 @@ Item { } property alias colour_space: __colour_space.value - XsAttributeValue { - id: __grade_in - attributeTitle: "grade_in" - model: grading_tool_attrs_data - } - property alias grade_in: __grade_in.value - - XsAttributeValue { - id: __grade_out - attributeTitle: "grade_out" - model: grading_tool_attrs_data - } - property alias grade_out: __grade_out.value - XsAttributeValue { id: __mask_tool_active attributeTitle: "mask_tool_active" diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml index 082870251..b2b58ab18 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml @@ -37,7 +37,7 @@ Item { property var mask_selected_shape: attrs.mask_selected_shape property var drawing_action: attrs.drawing_action - visible: mask_shapes_visible && tool_opened_count > 0 + visible: mask_shapes_visible && tool_opened_count > 0 && !isQuickview property var polygon_init: attrs.polygon_init property var polygon_points: [] diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml index 0c72eda7a..10753d360 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml @@ -69,85 +69,103 @@ Item { id: dialog } + property alias moreMenu: menus.moreMenu + + Sec0Menu{ + id: menus + } + + property alias bookmarkList: listDiv.bookmarkList - ColumnLayout { id: leftView + Item { anchors.fill: parent anchors.margins: panelPadding - spacing: panelPadding - Sec1Header{ - Layout.fillWidth: true - Layout.preferredHeight: btnHeight - } - - GridLayout { id: itemsGrid - property bool isVertical: false - - Layout.fillWidth: true - Layout.fillHeight: true - - flow: GridLayout.TopToBottom - rowSpacing: 1 - columnSpacing: 1 - rows: itemsGrid.isVertical? 3: 1 - columns: itemsGrid.isVertical? 6:18 - - Behavior on width {NumberAnimation{ duration: 250 }} - onWidthChanged: { - if(width < 850) { - itemsGrid.isVertical = true - } else { - itemsGrid.isVertical = false + XsSplitView { + id: rightPanel + width: parent.width + height: parent.height + + thumbWidth: XsStyleSheet.panelPadding + colorHandleBg: XsStyleSheet.panelBgGradTopColor + + ColumnLayout{ id: leftBar + SplitView.minimumWidth: 270 + Layout.preferredWidth: 290 + SplitView.fillHeight: true + spacing: panelPadding + + Sec1Header{ id: header + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + y: 0 //header.height + panelPaddingeight + Layout.maximumHeight: btnHeight + } + Sec2LayerList{ id: listDiv + Layout.fillWidth: true + Layout.fillHeight: true } - } - - Sec2LayerList{ id: listDiv - Layout.fillWidth: true - Layout.fillHeight: true - Layout.columnSpan: 3 - } - - Sec3MaskTools{ - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: itemsGrid.isVertical? itemsGrid.width/2 : 100 - Layout.maximumWidth: itemsGrid.isVertical? itemsGrid.width/2 : 100 - Layout.columnSpan: 3 - } - - GTSliderItem { - Layout.fillWidth: true - Layout.preferredWidth: itemsGrid.isVertical? itemsGrid.width/2 : itemsGrid.width/6 - Layout.maximumWidth: itemsGrid.isVertical? itemsGrid.width/2 : itemsGrid.width/6 - Layout.fillHeight: true - Layout.columnSpan: 3 } - Repeater{ - model: attrs.grading_wheels_model + ColumnLayout{ id: rightBar + SplitView.minimumWidth: 200 + SplitView.fillWidth: true + SplitView.fillHeight: true + spacing: panelPadding - GTWheelItem { + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + SplitView.fillHeight: true + + Sec3MaskTools{ + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + } + XsPrimaryButton{ id: moreBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + Layout.alignment: Qt.AlignRight + imgSrc: "qrc:/icons/more_vert.svg" + onClicked:{ + if(moreMenu.visible) moreMenu.visible = false + else{ + moreMenu.x = x + width + leftBar.width + moreMenu.y = y + height + moreMenu.visible = true + } + } + } + } + RowLayout{ Layout.fillWidth: true Layout.fillHeight: true - Layout.preferredWidth: itemsGrid.isVertical? itemsGrid.width/2 : itemsGrid.width/6 - Layout.maximumWidth: itemsGrid.isVertical? itemsGrid.width/2 : itemsGrid.width/6 - Layout.columnSpan: 3 + spacing: 1 + + GTSliderItem { + Layout.fillWidth: true + Layout.preferredWidth: 150 + Layout.maximumWidth: rightPanel.width/4 + Layout.fillHeight: true + } + Repeater{ + model: attrs.grading_wheels_model + + GTWheelItem { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 150 + Layout.maximumWidth: rightPanel.width/4 + } + } } } - + + } } - - - - - - - - - } diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc b/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc index c62e29f4b..5684415b0 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc @@ -4,5 +4,9 @@ icons/brightness_high.svg icons/hexagon.svg icons/mask_domino.svg + icons/invert_colors.svg + + icons/all_inclusive.svg + icons/step_into.svg \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg new file mode 100644 index 000000000..10463bf64 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg new file mode 100644 index 000000000..0d6a9c438 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg new file mode 100644 index 000000000..bb7a1d450 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir b/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir index e421741be..34443013d 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir @@ -5,9 +5,10 @@ GradingOverlay 2.0 GradingOverlay.qml GTAttributes 2.0 GTAttributes.qml MAttributes 2.0 MAttributes.qml -Sec1Header 2.0 sections/Sec1Header.qml -Sec2LayerList 2.0 sections/Sec2LayerList.qml -Sec3MaskTools 2.0 sections/Sec3MaskTools.qml +Sec0Menu 2.0 sections/Sec0Menu.qml +Sec1Header 2.0 sections/Sec1Header.qml +Sec2LayerList 2.0 sections/Sec2LayerList.qml +Sec3MaskTools 2.0 sections/Sec3MaskTools.qml GTSliderDiv 2.0 delegates/GTSliderDiv.qml GTSliderItem 2.0 delegates/GTSliderItem.qml diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml new file mode 100644 index 000000000..20873d1c7 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.clipboard 1.0 + +Item{ id: menuDiv + + + property var copy_buffer: [] + property alias moreMenu: moreMenu + + + Clipboard{ + id: clipboard + } + + + XsPopupMenu { + id: moreMenu + visible: false + menu_model_name: "moreMenu"+menuDiv + } + + XsMenuModelItem { + menuItemType: "radiogroup" + choices: attrs.media_colour_managed ? ["scene_linear", "compositing_log"] : ["raw"] + + property string currentColorSpace: attrs.media_colour_managed ? attrs.colour_space : "raw" + + currentChoice: currentColorSpace + onCurrentChoiceChanged: { + if (currentChoice != "raw") + attrs.colour_space = currentChoice + } + + enabled: hasActiveGrade() && attrs.media_colour_managed + text: "" + menuPath: "Color Space" + menuItemPosition: 1 + menuModelName: moreMenu.menu_model_name + onActivated: { + cdl_save_dialog.open() + } + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 2 + menuModelName: moreMenu.menu_model_name + } + + XsMenuModelItem { + text: "Rename..." + enabled: false + menuPath: "" + menuItemPosition: 3 + menuModelName: moreMenu.menu_model_name + onActivated: {} + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 4 + menuModelName: moreMenu.menu_model_name + } + XsMenuModelItem { + text: "Copy" + enabled: hasActiveGrade() + menuPath: "" + menuItemPosition: 5 + menuModelName: moreMenu.menu_model_name + onActivated: { + copyFunction() + } + } + XsMenuModelItem { + text: "Paste" + enabled: copy_buffer.length == (grading_sliders_model.length + grading_wheels_model.length) + menuPath: "" + menuItemPosition: 6 + menuModelName: moreMenu.menu_model_name + onActivated: { + pasteFunction(); + } + } + XsMenuModelItem { + text: "Reset" + enabled: hasActiveGrade() + menuPath: "" + menuItemPosition: 6.5 + menuModelName: moreMenu.menu_model_name + onActivated: { + attrs.grading_action = "Clear" + } + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 7 + menuModelName: moreMenu.menu_model_name + } + XsMenuModelItem { + text: "Copy Nuke Node" + menuPath: "" + menuItemPosition: 8 + menuModelName: moreMenu.menu_model_name + onActivated: { + copyNukeNode(); + } + } + XsMenuModelItem { + text: "Save CDL..." + menuPath: "" + menuItemPosition: 9 + menuModelName: moreMenu.menu_model_name + onActivated: { + cdl_save_dialog.open() + } + } + + function copyFunction() { + var attr_values = [] + for (var i = 0; i < grading_sliders_model.length; ++i) { + attr_values.push(grading_sliders_model.get(grading_sliders_model.index(i,0),"value")) + } + for (var i = 0; i < grading_wheels_model.length; ++i) { + attr_values.push(grading_wheels_model.get(grading_wheels_model.index(i,0),"value")) + } + copy_buffer = attr_values + } + + function pasteFunction() { + for (var i = 0; i < grading_sliders_model.length; ++i) { + grading_sliders_model.set( + grading_sliders_model.index(i,0), + copy_buffer[i], + "value" + ) + } + for (var i = 0; i < grading_wheels_model.length; ++i) { + grading_wheels_model.set( + grading_wheels_model.index(i,0), + copy_buffer[grading_sliders_model.length + i], + "value" + ) + } + } + + function copyNukeNode() { + + // TODO: ColSci + // Use Grade node instead of OCIOCDLTransform to handle contrast? + + var offset = attrs.getAttrValue("Offset") + var power = attrs.getAttrValue("Power") + var slope = attrs.getAttrValue("Slope") + var sat = attrs.getAttrValue("Saturation") + var exp = attrs.getAttrValue("Exposure") + var cont = attrs.getAttrValue("Contrast") + + var cdl_node = "OCIOCDLTransform {\n" + if (attrs.colour_space != "scene_linear") { + cdl_node += " working_space " + attrs.colour_space + "\n" + } + cdl_node += " slope { " + cdl_node += (slope[0] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += (slope[1] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += (slope[2] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += "}\n" + cdl_node += " offset { " + cdl_node += (offset[0] + offset[3]) + " " + cdl_node += (offset[1] + offset[3]) + " " + cdl_node += (offset[2] + offset[3]) + " " + cdl_node += "}\n" + cdl_node += " power { " + cdl_node += (power[0] * power[3]) + " " + cdl_node += (power[1] * power[3]) + " " + cdl_node += (power[2] * power[3]) + " " + cdl_node += "}\n" + cdl_node += " saturation " + sat + "\n" + cdl_node += "}" + + clipboard.text = cdl_node + } + + 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 + + // TODO: ColSci + // Add warning if contrast is used? + + 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" + } + + attrs.grading_action = "Save CDL " + path + } + } + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml index 76bfd62e7..f3c9ec9e8 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml @@ -29,7 +29,7 @@ Item{ id: toolDiv XsPrimaryButton{ id: deleteBtn Layout.preferredWidth: btnWidth Layout.preferredHeight: btnHeight - imgSrc: "qrc:/icons/delete.svg" + imgSrc: "qrc:/icons/delete.svg" //tooltip: "Remove the currently selected color correction" enabled: bookmarkList.count > 0 onClicked: { @@ -69,20 +69,7 @@ Item{ id: toolDiv hotkeyNameForTooltip: "Bypass all grades" } - XsPrimaryButton{ id: moreBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: btnHeight - Layout.alignment: Qt.AlignRight - imgSrc: "qrc:/icons/more_vert.svg" - onClicked:{ - if(moreMenu.visible) moreMenu.visible = false - else{ - moreMenu.x = x + width - moreMenu.y = y + height - moreMenu.visible = true - } - } - } } + } \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml index cb54c09d5..6f42bee2a 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml @@ -24,55 +24,12 @@ Item{ id: listDiv spacing: itemSpacing property bool userSelect: false - property var lastSelected: (new Map()) ScrollBar.vertical: XsScrollBar { visible: bookmarkList.height < bookmarkList.contentHeight } - onCurrentIndexChanged: { - if(userSelect) { - userSelect = false - } else if (currentIndex < 0) { - attrs.grading_bookmark = helpers.QUuidToQString("00000000-0000-0000-0000-000000000000") - } else if (currentIndex == 0 && bookmarkFilterModel.length) { - if(currentIndex != bookmarkFilterModel.length) { - if(lastSelected.get(bookmarkFilterModel.currentMedia) != undefined) { - var index = bookmarkFilterModel.sourceModel.search( - helpers.QVariantFromUuidString(lastSelected.get(bookmarkFilterModel.currentMedia)), "uuidRole") - if (index.valid) { - var row = bookmarkFilterModel.mapFromSource(index).row - if (row >= 0 && row < bookmarkList.count && row != currentIndex) { - userSelect = true - currentIndex = row - } - } - } - } - } - } - - onCurrentItemChanged: { - if (currentItem) { - var backendUuid = helpers.QVariantFromUuidString(attrs.grading_bookmark) - var selectedUuid = currentItem.uuid - lastSelected.set(bookmarkFilterModel.currentMedia, selectedUuid) - - if (backendUuid != selectedUuid && selectedUuid) { - attrs.grading_bookmark = helpers.QUuidToQString(selectedUuid) - } - } - } - onCountChanged: { - var index = bookmarkFilterModel.sourceModel.search( - helpers.QVariantFromUuidString(attrs.grading_bookmark), "uuidRole") - if (index.valid) { - var row = bookmarkFilterModel.mapFromSource(index).row - if (row >= 0 && row < bookmarkList.count && row != currentIndex) { - currentIndex = row - } - } - } + property var curr_bookmark_id: helpers.QUuidFromUuidString(attrs.grading_bookmark) delegate: XsPrimaryButton { id: bookmark width: bookmarkList.width @@ -83,8 +40,8 @@ Item{ id: listDiv activeIndicator.width: (1*3) * 3 property var uuid: uuidRole - - readonly property bool isSelected: index == ListView.view.currentIndex + + readonly property bool isSelected: uuid == bookmarkList.curr_bookmark_id property bool isHovered: hovered MouseArea { @@ -92,9 +49,12 @@ Item{ id: listDiv acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (mouse) => { - if (mouse.button == Qt.LeftButton){ - bookmarkList.userSelect = true - bookmarkList.currentIndex = index + if (mouse.button == Qt.LeftButton && attrs.grading_bookmark != uuidRole){ + if (currentPlayhead.mediaFrame < startFrameRole || currentPlayhead.mediaFrame > endFrameRole) { + // jump to frame of grade + currentPlayhead.logicalFrame = currentPlayhead.logicalFrame + (startFrameRole-currentPlayhead.mediaFrame) + } + attrs.grading_bookmark = uuidRole } else if(mouse.button == Qt.RightButton){ if(moreMenu.visible) moreMenu.visible = false @@ -138,20 +98,54 @@ Item{ id: listDiv } } Item{ id: maskDiv - Layout.preferredWidth: height * 1.2 + Layout.preferredWidth: !maskBtn.visible? 0 : height * 1.2 Layout.fillHeight: true - XsPrimaryButton{ id: maskBtn + XsSecondaryButton{ id: maskBtn width: parent.width - 1 height: parent.height - 4 anchors.verticalCenter: parent.verticalCenter - isActiveViaIndicator: false isActive: false visible: userDataRole.mask_active imgSrc: "qrc:/grading_icons/mask_domino.svg" text: "Mask Active" scale: 0.95 + imageSrcSize: 20 + hoverEnabled: false + enabled: false + onlyVisualyEnabled: true + } + } + Item{ id: rangeDiv + Layout.preferredWidth: height * 1.2 + Layout.fillHeight: true + + XsPrimaryButton{ id: rangeBtn + width: parent.width - 1 + height: parent.height - 4 + anchors.verticalCenter: parent.verticalCenter + + property bool isFullClip: startFrameRole != -1 && startFrameRole != endFrameRole + + isActiveViaIndicator: false + isActive: false + // enabled: hasActiveGrade() + imgSrc: isFullClip? "qrc:/grading_icons/all_inclusive.svg" : "qrc:/grading_icons/step_into.svg" + text: isFullClip? "Full Clip" : "Single Frame" + scale: 0.95 + + onClicked: { + if(!isFullClip){ + + attrs.grading_action = "Set Bookmark Full Range|" + uuidRole + + } else { + + attrs.grading_action = "Set Bookmark One Frame|" + uuidRole + "|" + currentPlayhead.mediaFrame + } + } + } } Item{ id: visibilityDiv diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml index e9c4c4286..d79537309 100644 --- a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml @@ -28,37 +28,38 @@ Item{ toolImg: "qrc:/icons/radio_button_unchecked.svg" } } - // ListModel{ id: mask2ToolsModel - // ListElement{ - // toolName: "Dodge" - // toolImg: "qrc:/grading_icons/brightness_low.svg" - // } - // ListElement{ - // toolName: "Burn" - // toolImg: "qrc:/grading_icons/brightness_high.svg" - // } - // } + ListModel{ id: mask2ToolsModel + + ListElement{ + toolName: "Dodge" + toolImg: "qrc:/grading_icons/brightness_low.svg" + } + ListElement{ + toolName: "Burn" + toolImg: "qrc:/grading_icons/brightness_high.svg" + } + } + + RowLayout { + width: parent.width + height: btnHeight + anchors.centerIn: parent + spacing: buttonSpacing + + RowLayout { + spacing: buttonSpacing + Layout.preferredWidth: maskBtnWidth*2 + spacing + Layout.maximumWidth: maskBtnWidth*2 + spacing + Layout.preferredHeight: btnHeight - ColumnLayout { id: coln - spacing: 0 //buttonSpacing - anchors.fill: parent - - GridLayout{ id: row1 - Layout.fillWidth: true - Layout.preferredHeight: itemsGrid.isVertical? btnHeight : btnHeight*2 - rows: itemsGrid.isVertical? 1:2 - columns: itemsGrid.isVertical? 2:1 - rowSpacing: buttonSpacing - columnSpacing: buttonSpacing - Repeater{ model: mask1ToolsModel GTToolButtonHorz{ - Layout.fillWidth: true - // Layout.minimumWidth: row1.width/2 //maskBtnWidth/1.5 - // Layout.preferredWidth: maskBtnWidth - Layout.maximumWidth: itemsGrid.isVertical? coln.width/2 : coln.width//-buttonSpacing //maskBtnWidth + Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/1.5 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth Layout.preferredHeight: btnHeight txt: toolName src: toolImg @@ -84,11 +85,10 @@ Item{ } } - Item{ - Layout.fillWidth: true - Layout.minimumHeight: buttonSpacing - Layout.maximumHeight: buttonSpacing + // Layout.fillWidth: true + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 Layout.fillHeight: true } @@ -108,86 +108,72 @@ Item{ // activeButton = text // } // } - // } + // } - GridLayout{ id: propertiesGrid + RowLayout{ id: propertiesGrid enabled: mask_attrs.mask_selected_shape >= 0 - // spacing: 1 - rows: 3 //4 - columns: 1 - rowSpacing: buttonSpacing - columnSpacing: buttonSpacing - Layout.fillWidth: true - Layout.preferredHeight: btnHeight*2 - Layout.fillHeight: true + spacing: buttonSpacing - XsIntegerAttrControl { - // Layout.minimumWidth: maskBtnWidth/2 - // Layout.preferredWidth: maskBtnWidth - // Layout.maximumWidth: parent.width/2 - propertiesGrid.rowSpacing - Layout.maximumHeight: btnHeight - Layout.fillWidth: true + // Layout.fillWidth: true + Layout.preferredWidth: opacityBtn.width*3 + removeBtn.width + Layout.preferredHeight: btnHeight + + XsIntegerAttrControl { id: opacityBtn + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth Layout.fillHeight: true text: "Opacity" attr_group_model: mask_attrs.model attr_title: "Pen Opacity" } XsIntegerAttrControl { - // Layout.maximumWidth: parent.width/2 - propertiesGrid.rowSpacing - Layout.maximumHeight: btnHeight - Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth Layout.fillHeight: true text: "Softness" attr_group_model: mask_attrs.model attr_title: "Pen Softness" } - // XsIntegerAttrControl { - // // Layout.maximumWidth: parent.width/2 - propertiesGrid.rowSpacing - // Layout.maximumHeight: btnHeight - // Layout.fillWidth: true - // Layout.fillHeight: true - // visible: activeButton == "Dodge" || activeButton == "Burn" - // text: "Size" - // attr_group_model: mask_attrs.model - // attr_title: "Shapes Pen Size" - // } - } - - Item{ - Layout.fillWidth: true - Layout.minimumHeight: buttonSpacing - Layout.maximumHeight: buttonSpacing - Layout.fillHeight: true - } - - - GridLayout{ id: row3 - width: parent.width - height: itemsGrid.isVertical? btnHeight : btnHeight*2 - rows: itemsGrid.isVertical? 1:2 - columns: itemsGrid.isVertical? 2:1 - rowSpacing: buttonSpacing - columnSpacing: buttonSpacing - + XsIntegerAttrControl { + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.fillHeight: true + visible: activeButton == "Dodge" || activeButton == "Burn" + text: "Size" + attr_group_model: mask_attrs.model + attr_title: "Shapes Pen Size" + } XsPrimaryButton{ id: invertBtn - Layout.maximumWidth: itemsGrid.isVertical? parent.width/2 : parent.width - Layout.maximumHeight: btnHeight Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth Layout.fillHeight: true font.pixelSize: XsStyleSheet.fontSize text: "Invert" + // imgSrc: "qrc:/grading_icons/invert_colors.svg" isActive: mask_attrs.shape_invert onClicked:{ mask_attrs.shape_invert = !mask_attrs.shape_invert } } - XsPrimaryButton{ id: removeBtn - Layout.maximumWidth: itemsGrid.isVertical? parent.width/2 : parent.width - Layout.maximumHeight: btnHeight + Item{ + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 + Layout.fillHeight: true + } + XsPrimaryButton{ id: removeBtn Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/4 + Layout.preferredWidth: maskBtnWidth/2 + Layout.maximumWidth: maskBtnWidth/2 Layout.fillHeight: true font.pixelSize: XsStyleSheet.fontSize text: "Remove" + imgSrc: "qrc:/icons/delete.svg" onClicked:{ mask_attrs.drawing_action = "Remove shape" } @@ -196,145 +182,10 @@ Item{ Item{ Layout.fillWidth: true - // Layout.minimumWidth: 2 + Layout.minimumWidth: 2 + // Layout.maximumWidth: 2 Layout.fillHeight: true } - - GridLayout{ id: rangeButtonsGrid - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: itemsGrid.isVertical? btnHeight : btnHeight*2 - rows: itemsGrid.isVertical? 1:3 - columns: itemsGrid.isVertical? 3:1 - rowSpacing: buttonSpacing - columnSpacing: buttonSpacing - - // RowLayout{ id: rangeButtonsGrid - // Layout.fillWidth: true - // Layout.fillHeight: true - // Layout.maximumHeight: btnHeight//*3 - // spacing: 1 - - XsText { - Layout.fillWidth: true - Layout.minimumWidth: 0 - // Layout.maximumWidth: rangeButtonsGrid.width/3 - rangeButtonsGrid.spacing - Layout.maximumHeight: btnHeight - Layout.fillHeight: true - text: "Range:" - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - // XsComboBox { - // Layout.fillWidth: true - // Layout.fillHeight: true - // Layout.minimumWidth: 60 - - // model: ["Single Frame", "Full Clip"] - // // enabled: hasActiveGrade() - // isActive: attrs.grade_in == attrs.grade_out && attrs.grade_in != -1 - // currentIndex: 0 - // // onActivated: - // // onCountChanged: - // } - XsPrimaryButton{ - Layout.fillWidth: true - Layout.maximumWidth: rangeButtonsGrid.width/3 - rangeButtonsGrid.spacing - Layout.maximumHeight: btnHeight - Layout.fillHeight: true - font.pixelSize: XsStyleSheet.fontSize - - text: "Frame" //isActive ? "Frame (" + attrs.grade_in + ")" : "Frame" - // tooltip: "Grade applies on the current frame only" - enabled: hasActiveGrade() - isActive: attrs.grade_in == attrs.grade_out && attrs.grade_in != -1 - onClicked: { - attrs.grade_in = currentPlayhead.mediaFrame - attrs.grade_out = currentPlayhead.mediaFrame - } - } - XsPrimaryButton{ - Layout.fillWidth: true - Layout.maximumWidth: rangeButtonsGrid.width/3 - rangeButtonsGrid.spacing - Layout.maximumHeight: btnHeight - Layout.fillHeight: true - font.pixelSize: XsStyleSheet.fontSize - text: "Clip" - // tooltip: "Grade applies on the full duration of the media" - enabled: hasActiveGrade() - isActive: { - return ( - (attrs.grade_in == -1 && attrs.grade_out == -1) || - (attrs.grade_in == 0 && attrs.grade_out == currentPlayhead.durationFrames) - ) - } - onClicked: { - attrs.grade_in = -1 - attrs.grade_out = -1 - } - } - - - // XsPrimaryButton{ - // Layout.preferredWidth: maskBtnWidth/2 - // Layout.preferredHeight: (parent.height - parent.columnSpacing)/propertiesGrid.rows - // text: isActive ? "Set In (" + attrs.grade_in + ")" : "Set In" - // imgSrc: "qrc:/grading_icons/input_circle.svg" - // // tooltip: "Grade starts at this frame" - // enabled: hasActiveGrade() - // isActive: { - // return ( - // (attrs.grade_in != -1 && attrs.grade_in != 0) && - // (attrs.grade_in != attrs.grade_out) - // ) - // } - // onClicked: { - // attrs.grade_in = currentPlayhead.mediaFrame - // } - // } - // XsPrimaryButton{ - // Layout.preferredWidth: maskBtnWidth/2 - // Layout.preferredHeight: (parent.height - parent.columnSpacing)/propertiesGrid.rows - // text: isActive ? "Set Out (" + attrs.grade_out + ")" : "Set Out" - // imgSrc: "qrc:/grading_icons/output_circle.svg" - // // tooltip: "Grade ends at this frame" - // enabled: hasActiveGrade() - // isActive: { - // return ( - // (attrs.grade_out != -1) && - // (attrs.grade_in != attrs.grade_out) - // ) - // } - // onClicked: { - // attrs.grade_out = currentPlayhead.mediaFrame - // } - // } - - } - - Item{ - Layout.fillWidth: true - Layout.minimumWidth: buttonSpacing - Layout.fillHeight: true - } - - GTToolButtonHorz{ - Layout.fillWidth: true - Layout.minimumWidth: maskBtnWidth - // Layout.preferredWidth: maskBtnWidth - // Layout.maximumWidth: maskBtnWidth - Layout.preferredHeight: btnHeight - Layout.maximumHeight: btnHeight - txt: "Reset Layer" - src: "qrc:/icons/rotate-ccw.svg" - enabled: hasActiveGrade() - - onClicked: { - attrs.grading_action = "Clear" - } - } } } \ No newline at end of file diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp index 79a47a096..baf869aa1 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp @@ -292,7 +292,8 @@ void OCIOEngine::extend_pixel_info( pixel_info.add_linear_channel_info(raw_info[2].channel_name, RGB[2]); pixel_info.set_linear_colourspace_name( - std::string("Scene Linear (") + working_space(frame_id.params()) + std::string(")")); + std::string("Scene Linear (") + working_space(frame_id.params()) + + std::string(")")); } // Display output @@ -346,7 +347,19 @@ OCIOEngine::get_ocio_config(const utility::JsonStore &src_colour_mgmt_metadata) } else if (config_name == "__current__") { config = OCIO::GetCurrentConfig(); } else if (!config_name.empty()) { + +#if OCIO_VERSION_HEX >= 0x02020100 + // CreateFromBuiltinConfig introduced in OCIO v2.2 + if (fs::exists(config_name)) { + config = OCIO::Config::CreateFromFile(config_name.c_str()); + } else { + config = OCIO::Config::CreateFromBuiltinConfig(config_name.c_str()); + } +#else config = OCIO::Config::CreateFromFile(config_name.c_str()); +#endif + + /**/ } else { config = OCIO::GetCurrentConfig(); } diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp index 774f3a275..72fbdeed5 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp @@ -109,6 +109,10 @@ caf::message_handler OCIOColourPipeline::message_handler_extensions() { window_id.find("xstudio_quickview_window") != std::string::npos) return; + // snapshot viewport settings don't affect other viewports + if (window_id == "snapshot_viewport") + return; + if (ocio_config == current_config_name_) { // we don't sync OCIO Display if it's coming from a different window @@ -118,6 +122,7 @@ caf::message_handler OCIOColourPipeline::message_handler_extensions() { auto attr = get_attribute(attr_title); if (attr) { attr->update_role_data_from_json(attr_role, attr_value, false); + redraw_viewport(); } } }}) diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp index b68eeeae4..53d79e670 100644 --- a/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp +++ b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp @@ -160,7 +160,8 @@ caf::message_handler OCIOGlobalControls::message_handler_extensions() { } if (ds.contains(window_id) && ds[window_id].contains("Display")) { res["Display"] = ds[window_id]["Display"]; - } else if (window_id.find("xstudio_quickview_window") != std::string::npos) { + } else if ( + window_id.find("xstudio_quickview_window") != std::string::npos) { // special case - a quickview window wants to set its // display but no quickview has been set-up before for diff --git a/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json b/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json index ad652522d..29bd6017d 100644 --- a/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json +++ b/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json @@ -4,7 +4,7 @@ "shotbrowser": { "purge_sequence_on_import": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "bool", "default_value": true, diff --git a/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp b/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp index 49c9dd0d7..aabcc3205 100644 --- a/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp +++ b/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp @@ -260,14 +260,26 @@ template class ShotbrowserConformActor : public caf::event_based_ac // clip metadata has precedence auto media_uuid = metadata.value("media_uuid", Uuid()); if (not media_uuid.is_null() and crequest.metadata_.count(media_uuid)) { - auto tmp = crequest.metadata_.at(media_uuid); + auto tmp = crequest.metadata_.at(media_uuid); + + // if source clip media is edit ref, don't use it + // as it'll be pointing to a sequence movie,, + // but this fails if we're conforming a edit ref clip.. + // check the medi asn't also linked to a sequence instead of a shot + // ? + + auto sg_entity_type = nlohmann::json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type"); auto sg_twig_type_code = nlohmann::json::json_pointer( "/metadata/shotgun/version/attributes/sg_twig_type_code"); - if (tmp.value(sg_twig_type_code, "") != "cut" and - tmp.value(sg_twig_type_code, "") != "edl") { + + if (tmp.value(sg_entity_type, "") == "Sequence" and + (tmp.value(sg_twig_type_code, "") == "cut" or + tmp.value(sg_twig_type_code, "") == "edl")) { + // ignore media metadata + } else { tmp.update(metadata, true); metadata = tmp; - // spdlog::warn("{}", metadata.dump(2)); } // metadata.update(crequest.metadata_.at(media_uuid)); } diff --git a/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json b/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json index e87b533f5..ecc6beefa 100644 --- a/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json +++ b/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json @@ -4,7 +4,7 @@ "ivy": { "enable_audio_autoload": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "bool", "default_value": true, 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 48b0c3066..308e96b29 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 @@ -609,7 +609,7 @@ void IvyMediaWorker::get_show_stalk_uuid( static std::regex res_re(R"(^\d+x\d+$)"); std::cmatch m; auto dnuuid = utility::Uuid(); - auto show = std::string(""); + auto show = std::string(); // turn paths into possible dir locations... std::set paths; @@ -617,11 +617,25 @@ void IvyMediaWorker::get_show_stalk_uuid( auto tmp = fs::path(uri_to_posix_path(i.uri())).parent_path(); const auto filename = tmp.filename().string(); - if (std::regex_match(filename.c_str(), m, res_re)) - paths.insert(tmp.parent_path()); - else { + if (std::regex_match(filename.c_str(), m, res_re)) { + // depth is sort of random.. + // we recurse up till we reach the TWIGTYPE level e.g. SHOT/CG + tmp = tmp.parent_path(); + paths.insert(tmp); + + while (true) { + tmp = tmp.parent_path(); + auto str = tmp.string(); + if (std::count_if(str.begin(), str.end(), [](char c) { + return c == '/'; + }) < 5) + break; + paths.insert(tmp); + } + } else { // movie path... // extract + // best try... auto stalk = filename; tmp = tmp.parent_path(); auto type = to_upper(filename); diff --git a/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json b/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json index fbd90eb89..3404bb916 100644 --- a/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json +++ b/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json @@ -5,7 +5,7 @@ "authentication": { "client_id": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -15,7 +15,7 @@ }, "client_secret": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -25,7 +25,7 @@ }, "grant_type": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "password", @@ -35,7 +35,7 @@ }, "password": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -45,7 +45,7 @@ }, "refresh_token": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -55,7 +55,7 @@ }, "session_token": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -65,7 +65,7 @@ }, "username": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "${USER}", @@ -77,7 +77,7 @@ "download": { "path": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "${HOME}/xStudio/shotbrowser_cache", @@ -87,7 +87,7 @@ }, "size": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "int", "default_value": 5, @@ -100,7 +100,7 @@ "note_history": { "scope": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -110,7 +110,7 @@ }, "type": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "", @@ -120,7 +120,7 @@ }, "popout": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": {}, @@ -132,7 +132,7 @@ "browser": { "location": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "${DNSITEDATA_SHORT_NAME}", @@ -142,7 +142,7 @@ }, "project": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "NSFL", @@ -152,7 +152,7 @@ }, "popout": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": {}, @@ -162,7 +162,7 @@ }, "pipestep": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -268,7 +268,7 @@ "note_publishing": { "note_publish_settings": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": { @@ -304,7 +304,7 @@ "server": { "host": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "shotgun.dneg.com", @@ -314,7 +314,7 @@ }, "port": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "int", "default_value": 0, @@ -324,7 +324,7 @@ }, "protocol": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "string", "default_value": "https", @@ -334,7 +334,7 @@ }, "timeout": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "int", "default_value": 120, @@ -348,7 +348,7 @@ { "version": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -358,7 +358,7 @@ }, "note": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -368,7 +368,7 @@ }, "shot": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -378,7 +378,7 @@ }, "playlist": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -388,9 +388,21 @@ } }, + "add_after_selection": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": true, + "description": "Add new media after selection, if false will add to end.", + "path": "/plugin/data_source/shotbrowser/add_after_selection", + "value": true, + "category": "Pipeline" + }, + "disable_integration": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "bool", "default_value": false, @@ -401,7 +413,7 @@ }, "project_presets": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], @@ -411,7 +423,7 @@ }, "site_presets": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [ @@ -8719,6 +8731,15 @@ "type": "term", "value": "render/element" }, + { + "enabled": true, + "id": "eb409080-4eba-4f31-801b-1b66e176d149", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, { "enabled": false, "id": "802adb53-f909-4066-bc5d-86358836cb5e", @@ -19039,7 +19060,7 @@ }, "user_presets": { "context": [ - "APPLICATION" + "PLUGIN" ], "datatype": "json", "default_value": [], diff --git a/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp b/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp index eb8082975..419db7eca 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp @@ -850,7 +850,7 @@ void ShotBrowser::get_data( if (type == "department") get_data_department(rp, type); else if (type == "project") - get_data_project(rp, type); + get_data_project(rp, type, expand_envvars(username_->value())); else if (type == "location") get_data_location(rp, type); else if (type == "review_location") @@ -940,14 +940,23 @@ void ShotBrowser::get_data_department( } void ShotBrowser::get_data_project( - caf::typed_response_promise rp, const std::string &type) { + caf::typed_response_promise rp, + const std::string &type, + const std::string &user) { + + auto filter = FilterBy().And( + StatusList("sg_status").is_not("Archive"), Text("sg_type").is_not("Template")); request( shotgun_, infinite, - shotgun_projects_atom_v, + shotgun_entity_search_atom_v, + "Projects", + JsonStore(filter), ProjectFields, - std::vector({"name"})) + std::vector({"name"}), + 1, + 4999) .then( [=](const JsonStore &data) mutable { try { @@ -1616,9 +1625,9 @@ void ShotBrowser::get_data_sequence( // ["sg_status_list", "in", ["na","del"]] - auto getShotData = GetData; + auto getShotData = GetData; getShotData["project_id"] = project_id; - getShotData["type"] = "sequence_shot"; + getShotData["type"] = "sequence_shot"; request( caf::actor_cast(this), diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js index 39cc58aaf..cfd76f576 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js @@ -143,6 +143,19 @@ function getJSON(indexes=[]) { return jsn } +function getDNUuid(indexes=[]) { + let jsn = [] + if(indexes.length) { + indexes = mapIndexesToResultModel(indexes) + + let m = indexes[0].model + for(let i = 0; i< indexes.length; i++) { + jsn.push(helpers.QUuidToQString(m.get(indexes[i], "stalkUuidRole") )) + } + } + return jsn +} + function getNote(indexes=[]) { let txt = "" if(indexes.length) { @@ -299,6 +312,20 @@ function compareSelectedResults(indexes=[]) { } } + +function getNextMediaUuid() { + let media_uuid = null + let indexes = mediaSelectionModel.selectedIndexes + + if(indexes.length) { + let next = nextItem(indexes[indexes.length-1]) + if(next.valid) { + media_uuid = mediaSelectionModel.model.get(next, "actorUuidRole") + } + } + return media_uuid +} + function replaceMediaCallback(playlist_uuid, uuids) { // find selected media. if(uuids.length && mediaSelectionModel.selectedIndexes.length) { @@ -442,37 +469,48 @@ function selectTimelineCallback(playlist_index, uuids, wait=4) { function addToSequence(indexes=[], viewed=true) { let current_pl = viewed ? viewedMediaSetProperties.values.actorUuidRole : inspectedMediaSetProperties.values.actorUuidRole if(viewed) - addToPlaylist(indexes, current_pl, null, "Untitled Playlist", conformMediaCallback) + addToPlaylist(indexes, current_pl, null, theSessionData.getNextName("Playlist {}"), conformMediaCallback) else - addToPlaylist(indexes, current_pl, null, "Untitled Playlist", conformMediaToConformTrackCallback) + addToPlaylist(indexes, current_pl, null, theSessionData.getNextName("Playlist {}"), conformMediaToConformTrackCallback) } function replaceToSequence(indexes=[], callback=replaceConformMediaCallback) { let current_pl = viewedMediaSetProperties.values.actorUuidRole - addToPlaylist(indexes, current_pl, null, "Untitled Playlist", callback) + addToPlaylist(indexes, current_pl, null, theSessionData.getNextName("Playlist {}"), callback) } function addToCurrentMediaContainer(indexes=[], media_uuid=null, viewed=true, callback=selectFirstMediaCallback) { let current_pl = viewed ? viewedMediaSetProperties.values.actorUuidRole : inspectedMediaSetProperties.values.actorUuidRole - addToPlaylist(indexes, current_pl, media_uuid, "Untitled Playlist", callback) + addToPlaylist(indexes, current_pl, media_uuid, theSessionData.getNextName("Playlist {}"), callback) } -function addToNewPlaylist(indexes=[], media_uuid=null, callback=selectFirstMediaCallback) { - addToPlaylist(indexes, null, media_uuid, "Untitled Playlist", callback) +function addToNewPlaylist(indexes=[], media_uuid=null, callback=null) { + // we can probably be smarter.. + // as we can at least determine the preset used ? + let playlistname = "Playlist {}" + if(indexes.length) { + let ind = mapIndexesToResultModel(indexes) + let cc = ind[0].model.customContext + if(cc != undefined && cc.project_name != undefined && cc.preset_name != undefined) { + playlistname = cc.project_name + " - " + cc.preset_name + } + } + + addToPlaylist(indexes, null, media_uuid, theSessionData.getNextName(playlistname), callback) } function loadShotGridPlaylist(shotgrid_playlist_id, name, context={}) { // console.log("createPlaylist", name) - let plindex = theSessionData.createPlaylist(name) + let plindex = theSessionData.createPlaylist(name, true, false) let notify_uuid = theSessionData.processingNotification(plindex, "Loading ShotGrid Playlist") // mark playlist as busy. - plindex.model.set(plindex, true, "busyRole") + // plindex.model.set(plindex, true, "busyRole") // get playlist actor uuid let pl_actor_uuid = plindex.model.get(plindex, "actorUuidRole") @@ -505,20 +543,20 @@ function loadShotGridPlaylist(shotgrid_playlist_id, name, context={}) { // add versions to playlist. Future.promise(ShotBrowserEngine.addVersionToPlaylistFuture(JSON.stringify(data), pl_actor_uuid)).then( function(json_string) { - var uuids = JSON.parse(json_string) - if(uuids.length) { - let tmp = []//uuids.length - for(let i=0;i<1;i++) - tmp.push(helpers.QVariantFromUuidString(uuids[i])) + // var uuids = JSON.parse(json_string) + // if(uuids.length) { + // let tmp = []//uuids.length + // for(let i=0;i<1;i++) + // tmp.push(helpers.QVariantFromUuidString(uuids[i])) - mediaSelectionModel.selectNewMedia(plindex, tmp) - } + // mediaSelectionModel.selectNewMedia(plindex, tmp) + // } - plindex.model.set(plindex, false, "busyRole") + // plindex.model.set(plindex, false, "busyRole") // selects the playlist so it is what's showing in // the viewport - sessionSelectionModel.setCurrentIndex(plindex, ItemSelectionModel.ClearAndSelect) + // sessionSelectionModel.setCurrentIndex(plindex, ItemSelectionModel.ClearAndSelect) theSessionData.infoNotification(plindex, "Loaded ShotGrid Playlist", 5, notify_uuid) // ShotgunHelpers.handle_response(json_string) }, @@ -557,11 +595,16 @@ function loadShotgridPlaylists(indexes=[]) { } } -function addToCurrent(indexes=[], viewed=true) { +function addToCurrent(indexes=[], viewed=true, addAfterSelection=false) { if(viewedMediaSetProperties.values.typeRole == "Timeline") { addToSequence(indexes, viewed) } else { - addToCurrentMediaContainer(indexes, null, viewed) + let afterMediaUuid = null + if(addAfterSelection) { + afterMediaUuid = getNextMediaUuid() + } + + addToCurrentMediaContainer(indexes, afterMediaUuid, viewed) } } @@ -616,7 +659,7 @@ function addSequencesToPlaylist(indexes, playlist_index=null, playlist_name ="Sh } } -function addToPlaylist(indexes=[], playlist_uuid=null, before_uuid=null, playlist_name = "Untitled Playlist", callback=null) { +function addToPlaylist(indexes=[], playlist_uuid=null, before_uuid=null, playlist_name = theSessionData.getNextName("Playlist {}"), callback=null) { indexes = mapIndexesToResultModel(indexes) let shotgrid_playlists = [] @@ -663,7 +706,8 @@ function addToPlaylist(indexes=[], playlist_uuid=null, before_uuid=null, playlis function(json_string) { try { var data = JSON.parse(json_string) - callback(playlist_uuid, data) + if(callback) + callback(playlist_uuid, data) // app_window.sessionFunction.setActivePlaylist(index) // app_window.requestActivate() @@ -696,22 +740,32 @@ function selectItem(selectionModel, index) { } function ctrlSelectItem(selectionModel, index) { - selectionModel.select(index, ItemSelectionModel.Toggle) + // make sure we don't have mixed selections / cross group selections. + + if(selectionModel.selectedIndexes.length && selectionModel.selectedIndexes[0].parent != index.parent) + selectItem(selectionModel, index) + else + selectionModel.select(index, ItemSelectionModel.Toggle) } function shiftSelectItem(selectionModel, index) { let sel = selectionModel.selectedIndexes if(sel.length) { - // find last selected entry ? - let m = sel[sel.length-1] - let s = Math.min(index.row, m.row) - let e = Math.max(index.row, m.row) - let items = [] - - for(let i=s; i<=e; i++) { - items.push(selectionModel.model.index(i, 0, index.parent)) + if(selectionModel.selectedIndexes[0].parent != index.parent) + selectItem(selectionModel, index) + else { + + // find last selected entry ? + let m = sel[sel.length-1] + let s = Math.min(index.row, m.row) + let e = Math.max(index.row, m.row) + let items = [] + + for(let i=s; i<=e; i++) { + items.push(selectionModel.model.index(i, 0, index.parent)) + } + selectionModel.select(helpers.createItemSelection(items), ItemSelectionModel.ClearAndSelect) } - selectionModel.select(helpers.createItemSelection(items), ItemSelectionModel.ClearAndSelect) } else { selectItem(selectionModel, index) } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml index 8db2206c5..9c38523b2 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml @@ -352,6 +352,10 @@ Item { publish_to_dialog.show() publish_to_dialog.playlistProperties = viewedMediaSetProperties } + Component.onCompleted: { + helpers.setMenuPathPosition("Pipeline", "main menu bar", 3.0) + } + } XsMenuModelItem { diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml index 5f22ef0fd..4b56f6ad9 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml @@ -46,6 +46,11 @@ Item{ property real panelPadding: XsStyleSheet.panelPadding + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + onOnScreenLogicalFrameChanged: { if(updateTimer.running) { updateTimer.restart() @@ -155,7 +160,7 @@ Item{ setIndexesFromPreferences() } } - + } onActiveScopeIndexChanged: { @@ -205,6 +210,15 @@ Item{ queryRunning += 1 let i = queryCounter + let customContext = {} + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get( + activeScopeIndex, + "nameRole" + ) + " - " + ShotBrowserEngine.presetsModel.get( + activeTypeIndex, + "nameRole" + ) + Future.promise( ShotBrowserEngine.executeQuery( [ @@ -216,7 +230,8 @@ Item{ activeTypeIndex, "jsonPathRole" ) - ] + ], + {},[],customContext ) ).then(function(json_string) { if(queryCounter == i) { diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml index 2504e9849..da66d5612 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml @@ -26,7 +26,7 @@ RowLayout{ clip:true Layout.fillHeight: true Layout.preferredWidth: cellWidth - onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes) + onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, true, addAfterSelection.value) } XsPrimaryButton{ diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml index 88c38f433..2116df612 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml @@ -96,7 +96,7 @@ Item{ id: thisItem onDoubleClicked: (mouse)=> { // need to know context, Which panel am I in. - ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser") + ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser", addAfterSelection.value) } Rectangle{ diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml index 92d80843c..a1daaaa82 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml @@ -95,4 +95,11 @@ XsPopupMenu { menuModelName: rightClickMenu.menu_model_name onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) } + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 10 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml index 429d0ab29..ad6d2c51b 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml @@ -212,6 +212,11 @@ Item{ onValueChanged: setProjectIndex(true) } + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + Item { // Hold properties that we want to persist between sessions. id: prefs @@ -484,10 +489,9 @@ Item{ function executeQuery() { if(currentPresetIndex && currentPresetIndex.valid) { - - // nameFilter = "" - // pipeStep = "" - // onDisk = "" + let customContext = {} + customContext["project_name"] = projectPref.value + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get(currentPresetIndex, "nameRole") resultsSelectionModel.clear() @@ -499,7 +503,7 @@ Item{ Future.promise( ShotBrowserEngine.executeQuery( - [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], {}, []) + [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], {}, [], customContext) ).then(function(json_string) { // console.log(json_string) if(queryCounter == i) { @@ -520,7 +524,7 @@ Item{ Future.promise( ShotBrowserEngine.executeProjectQuery( - [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], projectId, {}, []) + [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], projectId, {}, [], customContext) ).then(function(json_string) { // console.log(json_string) if(queryCounter == i) { @@ -570,7 +574,7 @@ Item{ let i = queryCounter Future.promise( ShotBrowserEngine.executeProjectQuery( - [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], projectId, {}, custom) + [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], projectId, {}, custom, customContext) ).then(function(json_string) { // console.log(json_string) if(queryCounter == i) { diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL1Tools.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL1Tools.qml index 1fc3ee9c5..1efca3afe 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL1Tools.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL1Tools.qml @@ -103,6 +103,8 @@ RowLayout{ } } + defaultText: "Set Project..." + textRole: "nameRole" Layout.fillWidth: true Layout.minimumWidth: btnWidth * 1.3 diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL3Actions.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL3Actions.qml index 9e296bf09..fe0f0758b 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL3Actions.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL3Actions.qml @@ -109,13 +109,17 @@ Rectangle{ // only run, if selection in tree. if(custom.length) { + let customContext = {} + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get(queryIndex, "nameRole") + customContext["project_name"] = projectPref.value + let result_json = [] let result_count = custom.length for(let i = 0; i< result_count;i++) { Future.promise( ShotBrowserEngine.executeProjectQuery( - [ShotBrowserEngine.presetsModel.get(queryIndex, "jsonPathRole")], pi, {}, [custom[i]]) + [ShotBrowserEngine.presetsModel.get(queryIndex, "jsonPathRole")], pi, {}, [custom[i]], customContext) ).then(function(json_string) { result_json[i] = json_string result_count -= 1 @@ -126,7 +130,7 @@ Rectangle{ indexes.push(quickResults.index(j,0)) } if(action == "playlist") { - ShotBrowserHelpers.addToCurrent(indexes, false) + ShotBrowserHelpers.addToCurrent(indexes, false, addAfterSelection.value) } else if(action == "sequence") { let seq_map = {} @@ -153,7 +157,7 @@ Rectangle{ indexes.push(quickResults.index(j,0)) if(action == "playlist") { - ShotBrowserHelpers.addToCurrent(indexes, false) + ShotBrowserHelpers.addToCurrent(indexes, false, addAfterSelection.value) } else if(action == "sequence") { } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetDelegate.qml index 967ecad13..e0b6f5c7a 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetDelegate.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetDelegate.qml @@ -12,7 +12,6 @@ import xstudio.qml.helpers 1.0 import xstudio.qml.models 1.0 XsPrimaryButton{ id: thisItem - isActive: isSelected //#TODO isActiveIndicatorAtLeft: true property var delegateModel: null @@ -21,9 +20,49 @@ XsPrimaryButton{ id: thisItem opacity: hiddenRole ? 0.5 : 1.0 + property bool isActive: presetModelIndex() == currentPresetIndex property bool isSelected: selectionModel.isSelected(presetModelIndex()) property bool isModified: updateRole != undefined ? updateRole : false - property bool isRunning: queryRunning && presetModelIndex() == currentPresetIndex + property bool isRunning: queryRunning && isActive + property bool itemDragging: isDragging && isSelected + property int itemDraggingOffset: itemDragging ? draggingOffset : 0 + + property int oldY: 0 + property var oldParent: null + + x: height + + onItemDraggingOffsetChanged: { + + if(itemDragging) { + let offset = itemDraggingOffset + + // calculate offset based on position in selection. + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i=0; i < ordered.length; i++) { + if(ordered[i] == presetModelIndex()) + break + offset += 1 + } + + thisItem.y = draggingY + (offset * (btnHeight-1)) - parent.contentY + (btnHeight/2) + } + } + + onItemDraggingChanged: { + if(itemDragging) { + thisItem.x = height * 2 + oldY = mapToItem(thisItem.parent, 0, 0).y + oldParent = parent + thisItem.parent = thisItem.parent.parent + } else if(oldParent) { + parent = oldParent + thisItem.y = oldY + thisItem.x = height + oldParent = null + } + } Connections { target: selectionModel @@ -32,6 +71,100 @@ XsPrimaryButton{ id: thisItem } } + // TapHandler { + // acceptedModifiers: Qt.NoModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.NoModifier) + // } + // } + + // TapHandler { + // acceptedModifiers: Qt.ShiftModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.ShiftModifier) + // } + // } + + // TapHandler { + // acceptedModifiers: Qt.ControlModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.ControlModifier) + // } + // } + + Item { + id: dummy + } + + DragHandler { + cursorShape: Qt.PointingHandCursor + xAxis.enabled: false + target: dummy + + dragThreshold: 5 + + onTranslationChanged: { + let offset = Math.floor(translation.y / (btnHeight-1)) + let row_count = filterModelIndex().model.rowCount(filterModelIndex().parent) + + if(filterModelIndex().row + offset < 0) { + draggingOffset = -(filterModelIndex().row+1) + } + else if(filterModelIndex().row + offset > row_count-1){ + draggingOffset = (row_count - 1 - filterModelIndex().row) + } else { + draggingOffset = offset + } + } + onActiveChanged: { + if(active) { + // primary drag + draggingOffset = 0 + draggingY = mapToItem(thisItem.parent, 0, 0).y + isDragging = true + } else { + isDragging = false + if(draggingOffset) { + // need to move stuff.. + let pis = [] + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i = 0; i< ordered.length; i++) + pis.push(helpers.makePersistent(ordered[i])) + + // have list of persistent indexes. + let destRow = (presetModelIndex().row+draggingOffset) +1 + + + if(draggingOffset < 0) { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + i + ) + } + } else { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + ) + } + } + } + } + } + } + MouseArea { id: ma anchors.fill: parent @@ -85,7 +218,7 @@ XsPrimaryButton{ id: thisItem anchors.bottom: parent.bottom width: borderWidth*9 height: parent.height - color: isActive? bgColorPressed : "transparent" + color: isActive ? bgColorPressed : "transparent" } RowLayout{ anchors.fill: parent @@ -153,7 +286,6 @@ XsPrimaryButton{ id: thisItem showHoverOnActive: favouriteRole && !thisItem.hovered isColoured: favouriteRole// && thisItem.hovered imgSrc: "qrc:///shotbrowser_icons/favorite.svg" - // isActive: favouriteRole scale: 0.95 opacity: groupFavouriteRole ? 1.0 : 0.5 onClicked: favouriteRole = !favouriteRole diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml index 97e75fcd7..c33b29e7e 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml @@ -12,7 +12,7 @@ import ShotBrowser 1.0 MouseArea { - id: dragArea + id: thisItem property var delegateModel: null property var selectionModel: null @@ -32,6 +32,15 @@ MouseArea { hoverEnabled: true + property bool itemDragging: isDragging && isSelected + property int itemDraggingOffset: itemDragging ? draggingOffset : 0 + + property int oldY: 0 + property var oldParent: null + + x: 0 + + function presetModelIndex() { try { let fi = filterModelIndex() @@ -54,15 +63,122 @@ MouseArea { if(mouse.modifiers == Qt.NoModifier) { selectionModel.select(presetModelIndex(), ItemSelectionModel.ClearAndSelect) } else if(mouse.modifiers == Qt.ShiftModifier){ - // ShotBrowserHelpers.shiftSelectItem(selectionModel, presetModelIndex()) + ShotBrowserHelpers.shiftSelectItem(selectionModel, presetModelIndex()) } else if(mouse.modifiers == Qt.ControlModifier) { - // ShotBrowserHelpers.ctrlSelectItem(selectionModel, presetModelIndex()) + ShotBrowserHelpers.ctrlSelectItem(selectionModel, presetModelIndex()) } } - // onDoubleClicked:{ - // openEditPopup() - // } + onItemDraggingOffsetChanged: { + + if(itemDragging) { + let offset = itemDraggingOffset + + // calculate offset based on position in selection. + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i=0; i < ordered.length; i++) { + if(ordered[i] == presetModelIndex()) + break + offset += 1 + } + + thisItem.y = draggingY + (offset * (btnHeight-1)) - parent.contentY + (btnHeight/2) + } + } + + onItemDraggingChanged: { + if(itemDragging) { + thisItem.x = height + oldY = mapToItem(thisItem.parent, 0, 0).y + oldParent = parent + thisItem.parent = thisItem.parent.parent + } else if(oldParent) { + parent = oldParent + thisItem.y = oldY + thisItem.x = 0 + oldParent = null + } + } + + Item { + id: dummy + } + + DragHandler { + cursorShape: Qt.PointingHandCursor + xAxis.enabled: false + target: dummy + + dragThreshold: 5 + + onTranslationChanged: { + let offset = Math.floor(translation.y / (btnHeight-1)) + let row_count = filterModelIndex().model.rowCount(filterModelIndex().parent) + + if(filterModelIndex().row + offset < 0) { + draggingOffset = -(filterModelIndex().row+1) + } + else if(filterModelIndex().row + offset > row_count-1){ + draggingOffset = (row_count - 1 - filterModelIndex().row) + } else { + draggingOffset = offset + } + } + onActiveChanged: { + if(active) { + // collapse all + expandedModel.clear() + // primary drag + draggingOffset = 0 + draggingY = mapToItem(thisItem.parent, 0, 0).y + isDragging = true + } else { + isDragging = false + if(draggingOffset) { + // need to move stuff.. + let pis = [] + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i = 0; i< ordered.length; i++) + pis.push(helpers.makePersistent(ordered[i])) + + // have list of persistent indexes. + let destRow = presetModelIndex().model.rowCount(presetModelIndex().parent) + let filteredDestRow = index + draggingOffset + 1 + let modelDestIndex = filterModelIndex().model.index(filteredDestRow, 0, filterModelIndex().parent) + if(modelDestIndex.valid) { + destRow = modelDestIndex.model.mapToSource(modelDestIndex).row + } + + // destRow will not be what we think it is due to filtering! + // we need to real row based off the filtered row. + + if(draggingOffset < 0) { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + i + ) + } + } else { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + ) + } + } + } + } + } + } Connections { target: expandedModel diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml index e0d202456..df16f5a71 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml @@ -18,6 +18,10 @@ XsListView { property int rightSpacing: control.height < control.contentHeight ? 12 : 0 Behavior on rightSpacing {NumberAnimation {duration: 150}} + property bool isDragging: false + property int draggingOffset: 0 + property int draggingY: 0 + model: presetsTreeModel delegate: DelegateChooser { @@ -37,7 +41,6 @@ XsListView { DelegateChoice { roleValue: "preset"; XsSBPresetDelegate{ - x: height width: control.width - height - control.rightSpacing height: btnHeight-2 delegateModel: control.model diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml index fad44260e..b1936b47d 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml @@ -25,7 +25,7 @@ RowLayout { text: "Add" onClicked: { if( ! special_sauce.includes(resultsBaseModel.groupId)) - ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, false) + ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, false, addAfterSelection.value) else ShotBrowserHelpers.addSequencesToNewPlaylist(resultsSelectionModel.selectedIndexes) } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml index 067a04244..6a38be6b4 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml @@ -76,4 +76,12 @@ XsPopupMenu { menuModelName: rightClickMenu.menu_model_name onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) } + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 6.6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } + } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml index 4034c8946..48e32e7fe 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml @@ -53,6 +53,11 @@ Item{ runQuery() } + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + onOnScreenLogicalFrameChanged: { if(updateTimer.running) { updateTimer.restart() @@ -176,11 +181,20 @@ Item{ queryCounter += 1 queryRunning += 1 let i = queryCounter + + + let customContext = {} + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get( + activeScopeIndex, + "nameRole" + ) + Future.promise( ShotBrowserEngine.executeQuery( [ShotBrowserEngine.presetsModel.get(activeScopeIndex, "jsonPathRole")], {}, - custom + custom, + customContext ) ).then( function(json_string) { diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml index 45649487a..fb9fe232d 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml @@ -22,7 +22,7 @@ RowLayout{ text: "Add" Layout.fillHeight: true Layout.preferredWidth: cellWidth - onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes) + onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, true, addAfterSelection.value) } XsPrimaryButton{ text: "Replace" diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml index 919e66c8d..f818f1094 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml @@ -85,7 +85,7 @@ Rectangle{ id: frame onDoubleClicked: (mouse)=> { let m = ShotBrowserHelpers.mapIndexesToResultModel([delegateModel.modelIndex(index)])[0].model if(! special_sauce.includes(m.groupId) ) - ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser") + ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser", addAfterSelection.value) else ShotBrowserHelpers.addSequencesToNewPlaylist([delegateModel.modelIndex(index)]) } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml index fafe3b9da..7080a1367 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml @@ -99,6 +99,14 @@ XsPopupMenu { onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) } + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 6.6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } + XsMenuModelItem { menuItemType: "divider" menuItemPosition: 7 diff --git a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp index fbac90c18..e455b9f0a 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp @@ -149,6 +149,15 @@ QDateTime ShotBrowserResultModel::requestedAt() const { return QDateTime::fromSecsSinceEpoch(getResultValue("/context/epoc", 0)); } +QVariant ShotBrowserResultModel::customContext() const { + QVariant result; + try { + result = mapFromValue(result_data_.at(json::json_pointer("/context/custom"))); + } catch (...) { + } + return result; +} + QVariantMap ShotBrowserResultModel::env() const { return QVariantMapFromJson(getResultValue("/env", R"({})"_json)); } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp index afcc3de93..bebe6397a 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp @@ -42,6 +42,7 @@ class ShotBrowserResultModel : public JSONTreeModel { Q_PROPERTY(QDateTime requestedAt READ requestedAt NOTIFY stateChanged) Q_PROPERTY(QVariantMap env READ env NOTIFY stateChanged) Q_PROPERTY(QVariantMap context READ context NOTIFY stateChanged) + Q_PROPERTY(QVariant customContext READ customContext NOTIFY stateChanged) Q_PROPERTY(bool isGrouped READ isGrouped WRITE setIsGrouped NOTIFY isGroupedChanged) Q_PROPERTY(bool canBeGrouped READ canBeGrouped NOTIFY canBeGroupedChanged) @@ -139,6 +140,7 @@ class ShotBrowserResultModel : public JSONTreeModel { [[nodiscard]] QDateTime requestedAt() const; [[nodiscard]] QVariantMap env() const; [[nodiscard]] QVariantMap context() const; + [[nodiscard]] QVariant customContext() const; [[nodiscard]] bool isGrouped() const { return is_grouped_; } [[nodiscard]] bool canBeGrouped() const { return can_be_grouped_; } diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp index 099b2cd71..9b7e722bb 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp @@ -69,7 +69,10 @@ QVariant ShotBrowserEngine::mergeQueries( } QFuture ShotBrowserEngine::executeQuery( - const QStringList &preset_paths, const QVariantMap &env, const QVariantList &custom_terms) { + const QStringList &preset_paths, + const QVariantMap &env, + const QVariantList &custom_terms, + const QVariantMap &customContext) { // spdlog::warn("ShotBrowserEngine::executeQuery{}", live_link_metadata_.dump(2)); @@ -81,7 +84,7 @@ QFuture ShotBrowserEngine::executeQuery( // if (not project_id) // throw std::runtime_error("Project metadata not found."); - return executeProjectQuery(preset_paths, project_id, env, custom_terms); + return executeProjectQuery(preset_paths, project_id, env, custom_terms, customContext); } catch (const std::exception &err) { spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), live_link_metadata_.dump(2)); return QtConcurrent::run([=]() { return QString(); }); @@ -92,7 +95,8 @@ QFuture ShotBrowserEngine::executeProjectQuery( const QStringList &preset_paths, const int project_id, const QVariantMap &env, - const QVariantList &custom_terms) { + const QVariantList &custom_terms, + const QVariantMap &customContext) { // spdlog::warn("{} project_id - {}", __PRETTY_FUNCTION__, project_id); cacheProject(project_id); @@ -117,11 +121,23 @@ QFuture ShotBrowserEngine::executeProjectQuery( "visual_source": [], "flag_text": "", "flag_colour": "", - "truncated": false + "truncated": false, + "custom": null })"_json; - request["env"] = qvariant_to_json(env); - request["custom_terms"] = qvariant_to_json(custom_terms); + try { + request["env"] = qvariant_to_json(env); + request["custom_terms"] = qvariant_to_json(custom_terms); + request["context"]["custom"] = qvariant_to_json(customContext); + + if (not request["context"]["custom"].contains("project_name")) { + request["context"]["custom"]["project_name"] = + query_engine_.get_project_name(live_link_metadata_); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } request["context"]["epoc"] = utility::to_epoc_milliseconds(utility::clock::now()); request["context"]["type"] = "result"; diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp index 2d9f33d78..33f08ffca 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp @@ -346,12 +346,14 @@ namespace ui { const QStringList &preset_paths, const int project_id, const QVariantMap &env = QVariantMap(), - const QVariantList &custom_terms = QVariantList()); + const QVariantList &custom_terms = QVariantList(), + const QVariantMap &customContext = QVariantMap()); QFuture executeQuery( const QStringList &preset_paths, const QVariantMap &env = QVariantMap(), - const QVariantList &custom_terms = QVariantList()); + const QVariantList &custom_terms = QVariantList(), + const QVariantMap &customContext = QVariantMap()); QVariant mergeQueries( const QVariant &dst, diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp index 2540dce76..f83b41554 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp @@ -383,7 +383,7 @@ caf::message_handler ShotBrowser::message_handler_extensions() { pending_preference_update_ = true; delayed_anon_send( actor_cast(this), - std::chrono::seconds(5), + std::chrono::seconds(1), json_store::sync_atom_v, true); } @@ -409,7 +409,7 @@ caf::message_handler ShotBrowser::message_handler_extensions() { engine().user_presets().as_json().at("children"), "/plugin/data_source/shotbrowser/user_presets", false); - prefs.save("APPLICATION"); + prefs.save("PLUGIN"); }, [=](json_store::sync_atom) -> UuidVector { @@ -622,7 +622,7 @@ caf::message_handler ShotBrowser::message_handler_extensions() { tokens.second, "/plugin/data_source/shotbrowser/authentication/refresh_token", false); - prefs.save("APPLICATION"); + prefs.save("PLUGIN"); set_authenticated(true); }, @@ -1034,6 +1034,18 @@ void ShotBrowser::update_preferences(const JsonStore &js) { auto user_presets = preference_value(js, "/plugin/data_source/shotbrowser/user_presets"); + auto uorp = + preference_overridden_path(js, "/plugin/data_source/shotbrowser/user_presets"); + if (ends_with(uorp, "application-v2.json")) { + + auto prefs = GlobalStoreHelper(system()); + prefs.set_overridden_path( + replace_once(uorp, "/application-v2.json", "/plugin-v2.json"), + "/plugin/data_source/shotbrowser/user_presets", + false); + prefs.save("PLUGIN"); + } + engine().merge_presets(site_presets, project_presets); engine().set_presets(user_presets, site_presets); diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp index ac578f1fb..772991755 100644 --- a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp @@ -171,7 +171,9 @@ namespace shotbrowser { caf::typed_response_promise rp, const std::string &type); void get_data_project( - caf::typed_response_promise rp, const std::string &type); + caf::typed_response_promise rp, + const std::string &type, + const std::string &user = ""); void get_data_location( caf::typed_response_promise rp, const std::string &type); 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 7e4dbb238..e86e927f3 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 @@ -24,7 +24,7 @@ class HudData : public utility::BlindDataObject { class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float /*viewport_du_dpixel*/, @@ -107,14 +107,17 @@ EXRDataWindowHUD::EXRDataWindowHUD( add_hud_settings_attribute(width_); } -plugin::ViewportOverlayRendererPtr EXRDataWindowHUD::make_overlay_renderer() { +plugin::ViewportOverlayRendererPtr +EXRDataWindowHUD::make_overlay_renderer(const std::string &viewport_name) { return plugin::ViewportOverlayRendererPtr(new EXRDataWindowRenderer()); } EXRDataWindowHUD::~EXRDataWindowHUD() = default; utility::BlindDataObjectPtr EXRDataWindowHUD::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string & /*viewport_name*/) const { + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const { auto r = utility::BlindDataObjectPtr(); 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 a713a5508..77d8e44df 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 @@ -21,9 +21,12 @@ namespace ui { protected: utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/) const override; + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const override; - plugin::ViewportOverlayRendererPtr make_overlay_renderer() override; + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override; private: module::ColourAttribute *colour_ = nullptr; diff --git a/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp b/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp index 00de79416..e45d40483 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp @@ -24,7 +24,7 @@ class HudData : public utility::BlindDataObject { class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float /*viewport_du_dpixel*/, @@ -103,14 +103,17 @@ ImageBoundaryHUD::ImageBoundaryHUD( add_hud_settings_attribute(width_); } -plugin::ViewportOverlayRendererPtr ImageBoundaryHUD::make_overlay_renderer() { +plugin::ViewportOverlayRendererPtr +ImageBoundaryHUD::make_overlay_renderer(const std::string &viewport_name) { return plugin::ViewportOverlayRendererPtr(new ImageBoundaryRenderer()); } ImageBoundaryHUD::~ImageBoundaryHUD() = default; utility::BlindDataObjectPtr ImageBoundaryHUD::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string & /*viewport_name*/) const { + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const { auto r = utility::BlindDataObjectPtr(); 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 53d4a8c9f..a66d5062b 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp @@ -21,9 +21,12 @@ namespace ui { protected: utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/) const override; + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const override; - plugin::ViewportOverlayRendererPtr make_overlay_renderer() override; + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override; private: module::ColourAttribute *colour_ = nullptr; diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp index 1cf23e584..81be8fdda 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp @@ -13,7 +13,7 @@ using namespace xstudio; using namespace xstudio::ui::viewport; -/*void PixelProbeHUDRenderer::render_opengl( +/*void PixelProbeHUDRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -92,7 +92,8 @@ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &i pixel_info_title_ = add_string_attribute("Pixel Info Title", "Pixel Info Title", ""); pixel_info_title_->expose_in_ui_attrs_group("pixel_info_attributes"); - pixel_info_current_viewport_ = add_string_attribute("Current Viewport", "Current Viewport", ""); + pixel_info_current_viewport_ = + add_string_attribute("Current Viewport", "Current Viewport", ""); pixel_info_current_viewport_->expose_in_ui_attrs_group("pixel_info_attributes"); auto font_size = add_float_attribute("Font Size", "Font Size", 10.0f, 5.0f, 50.0f, 1.0f); @@ -163,12 +164,9 @@ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &i } )", plugin::TopLeft); - } -PixelProbeHUD::~PixelProbeHUD() { - colour_pipelines_.clear(); -} +PixelProbeHUD::~PixelProbeHUD() { colour_pipelines_.clear(); } bool PixelProbeHUD::pointer_event(const ui::PointerEvent &e) { @@ -178,8 +176,7 @@ bool PixelProbeHUD::pointer_event(const ui::PointerEvent &e) { } void PixelProbeHUD::update_onscreen_info( - const std::string &viewport_name, - const Imath::V2f &pointer_position) { + const std::string &viewport_name, const Imath::V2f &pointer_position) { if (is_enabled_ != visible()) { is_enabled_ = visible(); @@ -190,7 +187,7 @@ void PixelProbeHUD::update_onscreen_info( } } - media_reader::ImageBufDisplaySetPtr onscreen_image_set; + media_reader::ImageBufDisplaySetPtr onscreen_image_set; caf::actor colour_pipeline; if (not viewport_name.empty()) { if (current_onscreen_images_.find(viewport_name) != current_onscreen_images_.end()) { @@ -210,19 +207,19 @@ void PixelProbeHUD::update_onscreen_info( // Convert pointer_position to normalised image coordinates // (image width always spans -1.0, 1.0 in x) - const float a = 1.0f/im->image_aspect(); - Imath::V4f p = pointer*im.layout_transform().inverse(); - Imath::V2f p0(p.x/p.w, p.y/p.w); + const float a = 1.0f / im->image_aspect(); + Imath::V4f p = pointer * im.layout_transform().inverse(); + Imath::V2f p0(p.x / p.w, p.y / p.w); if (p0.x >= -1.0f && p0.x <= 1.0f && p0.y >= -a && p0.y <= a) { // pointer is inside image boundary Imath::V2i image_coord( int(round((p0.x + 1.0f) * 0.5f * im->image_size_in_pixels().x)), - int(round((p0.y/a + 1.0f) * 0.5f * im->image_size_in_pixels().y))); + int(round((p0.y / a + 1.0f) * 0.5f * im->image_size_in_pixels().y))); // here we get the RGB, YUV value at the image coordinate const auto pixel_info = im->pixel_info(image_coord); - ptr_in_image = true; + ptr_in_image = true; if (!viewport_name.empty()) { // update our attribute that tracks which viewport the @@ -241,12 +238,11 @@ void PixelProbeHUD::update_onscreen_info( im.frame_id()) .then( [=](const media_reader::PixelInfo &extended_info) mutable { - make_pixel_info_onscreen_text(extended_info); }, [=](caf::error &err) { - }); + }); break; } } @@ -375,14 +371,14 @@ caf::actor PixelProbeHUD::get_colour_pipeline_actor(const std::string &viewport_ } void PixelProbeHUD::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { - - if (attribute_uuid == show_code_values_->uuid() - || attribute_uuid == show_raw_pixel_values_->uuid() - || attribute_uuid == show_linear_pixel_values_->uuid() - || attribute_uuid == show_final_screen_rgb_pixel_values_->uuid() - || attribute_uuid == value_precision_->uuid()) { - update_onscreen_info(); - } + + if (attribute_uuid == show_code_values_->uuid() || + attribute_uuid == show_raw_pixel_values_->uuid() || + attribute_uuid == show_linear_pixel_values_->uuid() || + attribute_uuid == show_final_screen_rgb_pixel_values_->uuid() || + attribute_uuid == value_precision_->uuid()) { + update_onscreen_info(); + } plugin::HUDPluginBase::attribute_changed(attribute_uuid, role); } diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp index 4a12385e1..e9a9f46cc 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp @@ -12,7 +12,7 @@ namespace ui { /*class PixelProbeHUDRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -49,7 +49,9 @@ namespace ui { bool pointer_event(const ui::PointerEvent &e) override; private: - void update_onscreen_info(const std::string &viewport_name = std::string(), const Imath::V2f &pointer = Imath::V2f(-1,-1)); + void update_onscreen_info( + const std::string &viewport_name = std::string(), + const Imath::V2f &pointer = Imath::V2f(-1, -1)); caf::actor get_colour_pipeline_actor(const std::string &viewport_name); void make_pixel_info_onscreen_text(const media_reader::PixelInfo &pixel_info); diff --git a/src/plugin/media_hook/CMakeLists.txt b/src/plugin/media_hook/CMakeLists.txt index 12b698435..29d7f5b75 100644 --- a/src/plugin/media_hook/CMakeLists.txt +++ b/src/plugin/media_hook/CMakeLists.txt @@ -1,2 +1,7 @@ +# only build default hook if STUDIO_PLUGINS is +# an empty string +if("${STUDIO_PLUGINS}" STREQUAL "") + add_src_and_test(default_hook) +endif() -build_studio_plugins("${STUDIO_PLUGINS}") +build_studio_plugins("${STUDIO_PLUGINS}") \ No newline at end of file diff --git a/src/plugin/media_hook/default_hook/src/CMakeLists.txt b/src/plugin/media_hook/default_hook/src/CMakeLists.txt new file mode 100644 index 000000000..c2f6a0c2d --- /dev/null +++ b/src/plugin/media_hook/default_hook/src/CMakeLists.txt @@ -0,0 +1,10 @@ +find_package(OpenColorIO CONFIG) + +SET(LINK_DEPS + xstudio::media_hook + xstudio::utility + xstudio::module + OpenColorIO::OpenColorIO +) + +create_plugin_with_alias(default_hook xstudio::media_hook::default_hook 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin/media_hook/default_hook/src/default_hook.cpp b/src/plugin/media_hook/default_hook/src/default_hook.cpp new file mode 100644 index 000000000..0f06af9ce --- /dev/null +++ b/src/plugin/media_hook/default_hook/src/default_hook.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include + +#include "xstudio/media_hook/media_hook.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/json_store.hpp" + +namespace fs = std::filesystem; + +using namespace xstudio; +using namespace xstudio::media_hook; +using namespace xstudio::utility; + +/* This default media hook plugin does very little except to +ensure that the fallback ACES OpenColorIO config is enabled +and media are set to the 'Raw' colourspace as a starting point. + +To see a more complex Hook that is applying various rules to set +colour management metadata and modifying media frame ranges see +the dneg/dnhook code for reference. +*/ +class DefaultMediaHook : public MediaHook { + public: + DefaultMediaHook() : MediaHook("Default") {} + ~DefaultMediaHook() override = default; + + std::optional modify_media_reference( + const utility::MediaReference &mr, const utility::JsonStore &jsn) override { + utility::MediaReference result = mr; + auto changed = false; + + // here we can add our own logic to modify the MediaReference by returning + // a modified copy. An example use case would be to automatically trim a + // slate frame, for example, by changing the frames data in the MediaReference + if (not changed) + return {}; + + return result; + } + + /* + Inject any metadata we desire into the metadata json structure + */ + + utility::JsonStore modify_metadata( + const utility::MediaReference &mr, const utility::JsonStore &metadata) override { + utility::JsonStore result = metadata; + // Example code to get the filepath from the MediaReference + const caf::uri &uri = + mr.container() or mr.uris().empty() ? mr.uri() : mr.uris()[0].first; + + const std::string path = to_string(uri); + auto ppath = uri_to_posix_path(uri); + result["colour_pipeline"] = colour_params(path, metadata); + result["colour_pipeline"]["path"] = path; + return result; + } + + /* + Colour management is enabled at this entry point. We can return a json dict + containing key/value pairs that drive the built-in OCIO plugin in xstudio. + Refer to the src/plugin/colour_pipeline/ocio/README.md file for more details. + */ + + utility::JsonStore + colour_params(const std::string &path, const utility::JsonStore &metadata) { + + utility::JsonStore r(R"({})"_json); + // using the builtin ACES config grom OCIO, assigning the colourspace + // to be Raw + r["ocio_config"] = "cg-config-v1.0.0_aces-v1.3_ocio-v2.1"; + r["input_colorspace"] = "Raw"; + return r; + } +}; + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>>( + Uuid("e4e1d569-2338-4e6e-b127-5a9688df161a"), + "Default Media Hook", + "Ted Waine", + "Minimal Hook to set up ACES colour management.", + semver::version("1.0.0"))})); +} +} \ No newline at end of file diff --git a/src/sync/test/CMakeLists.txt b/src/plugin/media_hook/default_hook/test/CMakeLists.txt similarity index 82% rename from src/sync/test/CMakeLists.txt rename to src/plugin/media_hook/default_hook/test/CMakeLists.txt index 35637d110..a73825679 100644 --- a/src/sync/test/CMakeLists.txt +++ b/src/plugin/media_hook/default_hook/test/CMakeLists.txt @@ -1,7 +1,6 @@ include(CTest) SET(LINK_DEPS - xstudio::sync caf::core ) diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp index 02533cbf7..91a3708ff 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.cpp @@ -30,6 +30,69 @@ using namespace xstudio::utility; namespace { +static Uuid blankshader_uuid{"d6c8722b-dc2a-42f9-981d-a2485c6ceea1"}; + +static std::string blankshader{R"( +#version 330 core +uniform int blank_width; +uniform int dummy; + +// forward declaration +uvec4 get_image_data_4bytes(int byte_address); + +vec4 fetch_rgba_pixel(ivec2 image_coord) +{ + int bytes_per_pixel = 4; + int pixel_bytes_offset_in_texture_memory = (image_coord.x + image_coord.y*blank_width)*bytes_per_pixel; + uvec4 c = get_image_data_4bytes(pixel_bytes_offset_in_texture_memory); + return vec4(float(c.x)/255.0f,float(c.y)/255.0f,float(c.z)/255.0f,1.0f); +} +)"}; + +static ui::viewport::GPUShaderPtr + blank_shader(new ui::opengl::OpenGLShader(blankshader_uuid, blankshader)); + +ImageBufPtr make_blank_image() { + + ImageBufPtr buf; + int width = 192; + int height = 108; + size_t size = width * height; + int bytes_per_channel = 1; + + // we are totally free to choose the pixel layout, but we need to unpack + // in the shader. RGBA 4 bytes matches underlying texture format, so most + // simple option. + int bytes_per_pixel = 4 * bytes_per_channel; + + JsonStore jsn; + jsn["blank_width"] = width; + + buf.reset(new ImageBuffer(blankshader_uuid, jsn)); + buf->allocate(size * bytes_per_pixel); + buf->set_shader(blank_shader); + buf->set_image_dimensions(Imath::V2i(width, height)); + + std::array c = {48, 48, 48}; + int i = 0; + uint8_t *b = (uint8_t *)buf->buffer(); + while (i < size) { + if (((i / 16) & 1) == (i / (192 * 16) & 1)) { + b[0] = c[0]; + b[1] = c[1]; + b[2] = c[2]; + } else { + b[0] = 0; + b[1] = 0; + b[2] = 0; + } + b += 4; // buf is implicitly rgba + ++i; + } + return buf; +} + + static Uuid s_plugin_uuid("87557f93-55f8-4650-8905-4834f1f4b78d"); static Uuid ffmpeg_shader_uuid_yuv{"9854e7c0-2e32-4600-aedd-463b2a6de95a"}; static Uuid ffmpeg_shader_uuid_rgb{"20015805-0b83-426a-bf7e-f6549226bfef"}; @@ -324,6 +387,16 @@ static std::mutex m; static int ct = 0; ImageBufPtr FFMpegMediaReader::image(const media::AVFrameID &mptr) { + + if (mptr.stream_id() == "stream -1") { + // dummy stream, return empty image + ImageBufPtr blank = make_blank_image(); + if (mptr.error() != "") { + blank->set_error(mptr.error()); + } + return blank; + } + std::string path = uri_convert(mptr.uri()); if (last_decoded_image_ && last_decoded_image_->media_key() == mptr.key()) { @@ -395,12 +468,18 @@ xstudio::media::MediaDetail FFMpegMediaReader::detail(const caf::uri &uri) const // N.B. MediaDetail needs frame duration, so invert frame rate std::vector streams; + bool have_video_stream = false; + bool have_audio_stream = false; + for (auto &p : t_decoder.streams()) { if (p.second->codec_type() == AVMEDIA_TYPE_VIDEO || p.second->codec_type() == AVMEDIA_TYPE_AUDIO) { auto frameRate = t_decoder.frame_rate(p.first); + have_audio_stream |= p.second->codec_type() == AVMEDIA_TYPE_AUDIO; + have_video_stream |= p.second->codec_type() == AVMEDIA_TYPE_VIDEO; + // If the stream has a duration of 1 then it is probably frame based. // FFMPEG assigns a default frame rate of 25fps to JPEGs, for example - // If this has happened, we want to ignore this and let xstudio apply @@ -424,6 +503,25 @@ xstudio::media::MediaDetail FFMpegMediaReader::detail(const caf::uri &uri) const } } + + // Leaving this here - dummy video streams was a bad idea because audio only + // sources were showing up in the list of valid video sources. + /*if (have_audio_stream && !have_video_stream) { + // audio only source. We need a dummy video stream, because xstudio playheads + // currently require an media::MT_IMAGE type media stream to be available for + // a given source. The duration of the source is always inferred from the + // active video track. This is easily done by adding a phoney video stream with + // the same duration as the first audio stream: + streams.emplace_back(media::StreamDetail( + streams[0].duration_, + "stream -1", + media::MT_IMAGE, + "{0}@{1}/{2}", + Imath::V2f(1920, 1080), + 1.0f, + -1)); + }*/ + return xstudio::media::MediaDetail(name(), streams, t_decoder.first_frame_timecode()); } @@ -504,10 +602,9 @@ PixelInfo FFMpegMediaReader::ffmpeg_buffer_pixel_picker( const int half_scale_uvy = buf.shader_params().value("half_scale_uvy", 0); const int half_scale_uvx = buf.shader_params().value("half_scale_uvx", 0); const int bits_per_channel = buf.shader_params().value("bits_per_channel", 0); - const Imath::M33f yuv_conv = - buf.shader_params().value("yuv_conv", Imath::M33f()); - const Imath::V3i yuv_offsets = buf.shader_params().value("yuv_offsets", Imath::V3i()); - const float norm_coeff = buf.shader_params().value("norm_coeff", 1.0f); + const Imath::M33f yuv_conv = buf.shader_params().value("yuv_conv", Imath::M33f()); + const Imath::V3i yuv_offsets = buf.shader_params().value("yuv_offsets", Imath::V3i()); + const float norm_coeff = buf.shader_params().value("norm_coeff", 1.0f); auto get_image_data_4bytes = [&](const int address) -> std::array { if (address < 0 || address >= (int)buf.size()) diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp index 39a931954..c1bc3750c 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp @@ -438,6 +438,11 @@ void FFMpegDecoder::decode_video_frame( try { + if (decode_stream_ && decode_stream_->is_attached_pic()) { + image_buffer = decode_stream_->attached_pic(); + return; + } + requested_decode_frame_ = frame_num; decoding_backwards_ = (last_requested_frame_ - frame_num) == 1 || @@ -618,7 +623,6 @@ void FFMpegDecoder::pull_video_buffer_from_stream(StreamPtr &video_stream) { // an appropriate timestamp ... we can then attach audio frames to this // video buffer to send back to playback engine. buf.reset(new ImageBuffer("No Video")); - buf->set_duration_seconds(frame_rate().to_seconds()); buf->set_display_timestamp_seconds(requested_decode_frame_ * frame_rate().to_seconds()); buf->set_decoder_frame_number(requested_decode_frame_); } diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index 0c9a072af..86993fb87 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -174,7 +174,6 @@ void set_shader_pix_format_info( } jsn["yuv_conv"] = yuv_to_rgb.transposed(); - } @@ -405,13 +404,6 @@ ImageBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_image() { image_buffer->set_pixel_aspect(1.0f); } - if (fpsNum_) { - if (fpsDen_) - image_buffer->set_duration_seconds(double(fpsDen_) / double(fpsNum_)); - else - image_buffer->set_duration_seconds(1.0 / double(fpsNum_)); - } - // determine if image has alpha - if planar, look for 'a_linesize' != 0. // Otherwise check for interleaved RGB pix formats that have an alpha int rgb_format_code = jsn.value("rgb", 0); @@ -631,21 +623,32 @@ FFMpegStream::FFMpegStream( throw std::runtime_error("No decoder found."); } - // Set the fps if it has been set correctly in the stream - if (avc_stream_->avg_frame_rate.num != 0 && avc_stream_->avg_frame_rate.den != 0) { - fpsNum_ = avc_stream_->avg_frame_rate.num; - fpsDen_ = avc_stream_->avg_frame_rate.den; - frame_rate_ = xstudio::utility::FrameRate( - static_cast(fpsDen_) / static_cast(fpsNum_)); - } else if (avc_stream_->r_frame_rate.num != 0 && avc_stream_->r_frame_rate.den != 0) { - fpsNum_ = avc_stream_->r_frame_rate.num; - fpsDen_ = avc_stream_->r_frame_rate.den; - frame_rate_ = xstudio::utility::FrameRate( - static_cast(fpsDen_) / static_cast(fpsNum_)); + if ((avc_stream_->disposition & AV_DISPOSITION_ATTACHED_PIC) == + AV_DISPOSITION_ATTACHED_PIC) { + + // for attached pic stream, we override frame rate to 24pfs + frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + is_attached_pic_ = true; + decode_attached_pic(); + } else { - fpsNum_ = 0; - fpsDen_ = 0; - frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + + // Set the fps if it has been set correctly in the stream + if (avc_stream_->avg_frame_rate.num != 0 && avc_stream_->avg_frame_rate.den != 0) { + fpsNum_ = avc_stream_->avg_frame_rate.num; + fpsDen_ = avc_stream_->avg_frame_rate.den; + frame_rate_ = xstudio::utility::FrameRate( + static_cast(fpsDen_) / static_cast(fpsNum_)); + } else if (avc_stream_->r_frame_rate.num != 0 && avc_stream_->r_frame_rate.den != 0) { + fpsNum_ = avc_stream_->r_frame_rate.num; + fpsDen_ = avc_stream_->r_frame_rate.den; + frame_rate_ = xstudio::utility::FrameRate( + static_cast(fpsDen_) / static_cast(fpsNum_)); + } else { + fpsNum_ = 0; + fpsDen_ = 0; + frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + } } } @@ -898,12 +901,18 @@ int FFMpegStream::duration_frames() const { } -/* Note 1: this experiment is disabled for now. The idea is that we circumvent -// ffmpeg's buffer allocation and insert our own pixel buffer (owned by video_frame) -// into which ffmpeg decodes. This will avoid the data copy happening in the -// call to FFMPegStream::copy_avframe_to_xstudio_buffer and does indeed speed up -// decoding. 2-3ms for typical HD res frame, more for UHD and so-on. However, the -// approach doesn't work when ffmpeg is doing multithreading on frames as the buffer -// allocation happens out of sync with the decode of a given video frame. Some more -// work could be done to fix that problem and gain a few ms per frame which may -// be needed for high res playback */ \ No newline at end of file +void FFMpegStream::decode_attached_pic() { + + int rx = send_packet(&(avc_stream_->attached_pic)); + av_frame_unref(frame); + int rt = avcodec_receive_frame(codec_context_, frame); + if (rt == 0) { + attached_pic_ = get_ffmpeg_frame_as_xstudio_image(); + } else { + try { + AVC_CHECK_THROW(rt, "avcodec_receive_frame"); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } +} diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp index bb37222f7..136f06593 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp @@ -83,6 +83,8 @@ namespace media_reader { AudioBufPtr get_ffmpeg_frame_as_xstudio_audio(const int soundcard_sample_rate); + const ImageBufPtr &attached_pic() const { return attached_pic_; } + std::shared_ptr convert_av_frame_to_thumbnail(const size_t size_hint); @@ -107,7 +109,11 @@ namespace media_reader { [[nodiscard]] int duration_frames() const; - [[nodiscard]] bool is_single_frame() const { return duration_frames() < 2; } + [[nodiscard]] bool is_single_frame() const { + return is_attached_pic_ || duration_frames() < 2; + } + + [[nodiscard]] bool is_attached_pic() const { return is_attached_pic_; } [[nodiscard]] int stream_index() const { return stream_index_; } @@ -135,6 +141,8 @@ namespace media_reader { return avc_stream_->start_time != AV_NOPTS_VALUE ? avc_stream_->start_time : 0; } + void decode_attached_pic(); + // void setup_frame(ImageStorePtr & video_frame); int stream_index_; AVCodecContext *codec_context_; @@ -154,6 +162,7 @@ namespace media_reader { int current_frame_ = {CURRENT_FRAME_UNKNOWN}; Imath::V2i resolution_ = {Imath::V2i(0, 0)}; float pixel_aspect_ = 1.0f; + bool is_attached_pic_ = {false}; // for video rescaling SwsContext *sws_context_ = {nullptr}; @@ -168,6 +177,7 @@ namespace media_reader { SwrContext *audio_resampler_ctx_ = {0}; utility::FrameRate frame_rate_; + ImageBufPtr attached_pic_; }; } // namespace ffmpeg } // namespace media_reader diff --git a/src/plugin/python_plugins/CMakeLists.txt b/src/plugin/python_plugins/CMakeLists.txt index aa44e927d..605b33dba 100644 --- a/src/plugin/python_plugins/CMakeLists.txt +++ b/src/plugin/python_plugins/CMakeLists.txt @@ -1,2 +1,3 @@ add_python_plugin(viewport_flag_indicator) -add_python_plugin(picture_in_picture) \ No newline at end of file +add_python_plugin(picture_in_picture) +add_python_plugin(on_screen_version_name) \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/__init__.py b/src/plugin/python_plugins/on_screen_version_name/__init__.py new file mode 100644 index 000000000..559d87fe7 --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +from .on_screen_version_name import create_plugin_instance \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py b/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py new file mode 100755 index 000000000..69323dc0e --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py @@ -0,0 +1,160 @@ +#!/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +from xstudio.connection import Connection +from xstudio.plugin import HUDPlugin +from xstudio.core import JsonStore +from xstudio.core import event_atom, show_atom +from xstudio.core import HUDElementPosition, ColourTriplet +from xstudio.api.session.media import Media, MediaSource + +import json + +# path to QML resources relative to this .py file +qml_folder_name = "qml/OnScreenVersionName.1" + +# QML code necessary to create the overlay item that is drawn over the xSTUDIO +# viewport. +overlay_qml = """ +OnScreenVersionNameOverlay { +} +""" + +# Declare our plugin class - we're using the HUDPlugin base class meaning we +# get a toggle under the 'HUD' button in the viewport toolbar to turn our +# hud on and off +class OnScreenVersionName(HUDPlugin): + + def __init__(self, connection): + + HUDPlugin.__init__( + self, + connection, + "On Screen Version Name", # the name of the HUD item + qml_folder=qml_folder_name, + position_in_hud_list=-9.0) + + # add an attribute to control the size of the text + self.font_size = self.add_attribute( + "Font Size", + 30.0, # default size + # additional attribute role data is provided as a dictionary + # as follows. The keys must be valid role names. See attribute.hpp + # for a list of the attribute role data names + { + "float_scrub_min": 5.0, + "float_scrub_max": 100.0, + "float_scrub_step": 2.0, + "float_display_decimals": 2 + }, + register_as_preference=True + ) + # adding to a ui attrs group means we can access the attribute in our + # QML code by referencing the group name and the attribute title + self.font_size.expose_in_ui_attrs_group("on_screen_version_name") + self.font_size.set_tool_tip("Set the size of the font for displaying the version name") + self.font_size.set_redraw_viewport_on_change(True) + + # add an attribute to control the size of the text + self.font_colour = self.add_attribute( + "Font Colour", + ColourTriplet(1.0, 1.0, 1.0) + ) + self.font_colour.expose_in_ui_attrs_group("on_screen_version_name") + + self.auto_hide = self.add_attribute( + "Auto Hide", + True, + register_as_preference=True + ) + self.auto_hide.expose_in_ui_attrs_group("on_screen_version_name") + + self.hide_timeout = self.add_attribute( + "Auto Hide Timeout (seconds)", + 10.0, # default number of seconds to hide the version + { + "float_scrub_min": 0.0, + "float_scrub_max": 20.0, + "float_scrub_step": 0.5, + "float_display_decimals": 1 + }, + register_as_preference=True + ) + self.hide_timeout.expose_in_ui_attrs_group("on_screen_version_name") + + # this attr holds the actual text to be displayed on screen + self.display_text = self.add_attribute( + "Display Text", + "", + {} + ) # default size + self.display_text.expose_in_ui_attrs_group("on_screen_version_name") + + # the following calls mean that these attributes get controls in + # the settings dialog for thie HUD, accessed via the cog icon next to + # the item in the HUD pop-up menu + self.add_hud_settings_attribute(self.font_size) + self.add_hud_settings_attribute(self.hide_timeout) + self.add_hud_settings_attribute(self.auto_hide) + self.add_hud_settings_attribute(self.font_colour) + + # here we provide the QML code to instance the item that will draw + # the overlay graphics + self.hud_element_qml( + overlay_qml, + HUDElementPosition.TopCenter) + + # expose our attributes in the UI layer + self.connect_to_ui() + + # listen for crucial events about the on-screen media changing etc. + self.subscribe_to_playhead_events(self.playhead_event_handler) + + def on_screen_source_changed(self, media_source): + + # This needs some more work, as we can have multiple things on screen + # in different viewports + + path = str(media_source.media_reference.uri()) + # if it's a dneg.mov it will have a burn-in and we don't need the + # version name overlay + if path.find(".dneg.mov") != -1: + self.display_text.set_value("") + return + + # strip the path string of its folder and extension + path = path[(path.rfind("/")+1):] + path = path[0:path.find(".")] + + self.display_text.set_value(path) + + def playhead_event_handler(self, event_args): + + # Skipping this bit, using playhead data instead in the QML overlay + return + + # various events come in here from the playhead objects. You can simply + # print 'event_args' to see the content and try and work out how to + # use them. + # + # In our case we are interested when the on-screen media changes - this + # event has a particular signature and data types .... + # We get a tuple like this + # (event_atom, show_atom, actor(media), actor(media_source), str(viewport name)) + + if len(event_args) == 5 and type(event_args[0]) == event_atom and type(event_args[1]) == show_atom: + if event_args[3]: + viewport_name = event_args[4] + media_source = MediaSource(self.connection, event_args[3]) + self.on_screen_source_changed(media_source) + +# This method is required by xSTUDIO +def create_plugin_instance(connection): + return OnScreenVersionName(connection) + + +if __name__=="__main__": + + XSTUDIO = Connection(auto_connect=True) + mask_plugin_instance = create_plugin_instance(XSTUDIO) + XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml new file mode 100644 index 000000000..d90e0a9f4 --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import QtQuick 2.14 +import QuickFuture 1.0 +import QuickPromise 1.0 + +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 + +// This overlay is as simple as it gets. We're just putting a circle in a corner +// of the screen that shows the 'flag' colour that is set on the media item that +// is on screen. If no flag is set, we don't show anything +Item { + + id: root + width: visible ? text_metrics.width : 0 + height: visible ? text_metrics.height : 0 + + // Turning this off for QuickView windows, which won't work due to use + // of currentOnScreenMediaData + visible: isQuickview ? false : version_name != undefined && opacity != 0.0 + + Rectangle { + color: "black" + opacity: 0.5 + anchors.fill: parent + } + + property var media_info: currentOnScreenMediaData.values.mediaDisplayInfoRole + property var version_name: media_info ? media_info[3] : "" + + XsText { + + id: thetext + anchors.fill: parent + font.pixelSize: font_size*view.width/1920 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: version_name + color: font_colour + onTextChanged: { + root.opacity = 1.0 + if (auto_hide && text != "") { + auto_hide_timer.running = false + auto_hide_timer.running = true + } + } + + } + + XsTimer { + id: auto_hide_timer + interval: hide_timeout*1000 + running: false + repeat: false + onTriggered: root.opacity = 0 + } + + TextMetrics { + id: text_metrics + font: thetext.font + text: thetext.text + } + + + // access the 'dnmask_settings' attribute group + XsModuleData { + id: onscreen_attrs + modelDataName: "on_screen_version_name" + } + + + XsAttributeValue { + id: __font_colour + attributeTitle: "Font Colour" + model: onscreen_attrs + } + property alias font_colour: __font_colour.value + + XsAttributeValue { + id: __hide_timeout + attributeTitle: "Auto Hide Timeout (seconds)" + model: onscreen_attrs + } + property alias hide_timeout: __hide_timeout.value + + XsAttributeValue { + id: __auto_hide + attributeTitle: "Auto Hide" + model: onscreen_attrs + } + property alias auto_hide: __auto_hide.value + + XsAttributeValue { + id: __the_text + attributeTitle: "Display Text" + model: onscreen_attrs + } + property alias display_text: __the_text.value + + XsAttributeValue { + id: __font_size + attributeTitle: "Font Size" + model: onscreen_attrs + } + property alias font_size: __font_size.value + + /*XsAttributeValue { + id: __flag_colours + attributeTitle: "Flag Colours" + model: vp_flag_indicator_settings + } + property alias flagColours: __flag_colours.value*/ + +} \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir new file mode 100644 index 000000000..5c6822862 --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir @@ -0,0 +1,3 @@ +module OnScreenVersionName + +OnScreenVersionNameOverlay 1.0 OnScreenVersionNameOverlay.qml \ No newline at end of file diff --git a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml b/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml deleted file mode 100644 index d785998fb..000000000 --- a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 -import xstudio.qml.models 1.0 - -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 -import Qt.labs.qmlmodels 1.0 - -import xstudio.qml.models 1.0 -import xStudio 1.0 - -// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic -// list of masks that the user can enable/disable - -XsWindow { - - id: dialog - width: 500 - minimumHeight: maskOptions.length*25 + 300 - title: "DNEG Pipeline Mask Settings" - property real cwidth: 50 - - XsModuleData { - id: attribute_set - modelDataName: "dnmask_settings" - } - - XsModuleData { - id: mask_options_data - modelDataName: "dnmask_options" - } - - XsModuleData { - id: mask_other_data - modelDataName: "dnmask_other" - } - - function setCWidth(ww) { - if (ww > cwidth) cwidth = ww - } - - XsAttributeValue { - id: __current_show - attributeTitle: "dnMask Current Show" - model: mask_other_data - } - property alias currentShow: __current_show.value - - - // make an alias so the mask options are accessible as an array - property alias maskOptions: mask_options_data - - ColumnLayout { - - anchors.fill: parent - anchors.margins: 10 - spacing: 10 - - RowLayout { - spacing: 10 - Layout.fillWidth: true - Rectangle { - Layout.fillWidth: true - height: 1 - color: XsStyleSheet.menuBorderColor - } - XsText { - text: "Active Masks for " + currentShow - Layout.alignment: Qt.AlignHCenter - } - Rectangle { - Layout.fillWidth: true - height: 1 - color: XsStyleSheet.menuBorderColor - } - } - - RowLayout { - - Layout.fillWidth: true - Layout.margins: 10 - - ColumnLayout { - - Layout.margins: 5 - - Repeater { - - // N.B the 'combo_box_options' attribute is propagated via the - // XsToolBox which instantiates the BasicViewportMaskButton - model: maskOptions - - Rectangle { - - height: 20 - color: "transparent" - Layout.preferredWidth: cwidth - Layout.alignment: Qt.AlignRight - - XsText { - id: thetext - anchors.fill: parent - text: title ? title : "" - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - font.strikeout: user_data == undefined - } - - TextMetrics { - id: label_metrics - font: thetext.font - text: thetext.text - } - } - - } - } - - ColumnLayout { - - Layout.fillWidth: true - Layout.margins: 5 - - Repeater { - - model: maskOptions - y: 5 - delegate: Rectangle { - color: "transparent" - height: 20 - Layout.fillWidth: true - XsBoolAttributeCheckBox { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 2 - anchors.bottomMargin: 2 - } - } - - } - } - } - - XsText { - text: "Masks that are struck out have not been set-up for the current on-screen image format." - Layout.alignment: Qt.AlignHCenter - font.italic: true - } - - Item { - Layout.fillHeight: true - } - - RowLayout { - spacing: 10 - Layout.fillWidth: true - Rectangle { - Layout.fillWidth: true - height: 1 - color: XsStyleSheet.menuBorderColor - } - XsText { - text: "Mask Settings" - Layout.alignment: Qt.AlignHCenter - } - Rectangle { - Layout.fillWidth: true - height: 1 - color: XsStyleSheet.menuBorderColor - } - } - - RowLayout { - - id: settings_items - Layout.fillWidth: true - Layout.margins: 10 - - ColumnLayout { - - Layout.margins: 5 - - Repeater { - - // N.B the 'combo_box_options' attribute is propagated via the - // XsToolBox which instantiates the BasicViewportMaskButton - model: attribute_set - id: r1 - - Rectangle { - - height: 20 - color: "transparent" - width: label_metrics.width - onWidthChanged: setCWidth(width) - Layout.alignment: Qt.AlignRight - - XsText { - id: thetext - anchors.fill: parent - text: title ? title : "" - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - y: index*20 - } - - TextMetrics { - id: label_metrics - font: thetext.font - text: thetext.text - } - } - - } - } - - ColumnLayout { - - Layout.fillWidth: true - Layout.margins: 5 - - Repeater { - - model: attribute_set - y: 5 - delegate: chooser - - DelegateChooser { - id: chooser - role: "type" - - DelegateChoice { - roleValue: "FloatScrubber"; - XsFloatAttributeSlider { - height: 20 - Layout.fillWidth: true - } - } - - DelegateChoice { - roleValue: "IntegerValue"; - XsIntAttributeSlider { - height: 20 - Layout.fillWidth: true - } - } - - DelegateChoice { - roleValue: "ColourAttribute"; - XsColourChooser { - height: 20 - Layout.fillWidth: true - } - } - - - DelegateChoice { - roleValue: "OnOffToggle"; - Rectangle { - color: "transparent" - height: 20 - Layout.fillWidth: true - XsBoolAttributeCheckBox { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 2 - anchors.bottomMargin: 2 - } - } - } - - DelegateChoice { - roleValue: "ComboBox"; - Rectangle { - height: 20 - Layout.fillWidth: true - color: "transparent" - XsComboBox { - model: combo_box_options - anchors.fill: parent - property var value_: value ? value : null - onValue_Changed: { - currentIndex = indexOfValue(value_) - } - Component.onCompleted: currentIndex = indexOfValue(value_) - onCurrentValueChanged: { - if (value != currentValue) { - value = currentValue; - } - } - } - } - } - - } - - } - } - } - - Item { - Layout.fillHeight: true - } - - XsSimpleButton { - text: qsTr("Close") - width: XsStyleSheet.primaryButtonStdWidth*2 - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - onClicked: { - dialog.close() - } - } - } - -} diff --git a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml b/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml deleted file mode 100644 index 3c9528c52..000000000 --- a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 -import QuickFuture 1.0 -import QuickPromise 1.0 - -// These imports are necessary to have access to custom QML types that are -// part of the xSTUDIO UI implementation. -import xStudio 1.0 -import xstudio.qml.models 1.0 - -// This overlay is as simple as it gets. We're just putting a circle in a corner -// of the screen that shows the 'flag' colour that is set on the media item that -// is on screen. If no flag is set, we don't show anything -Rectangle { - - width: indicatorSize - height: width - radius: indicatorSize/2 - color: flag ? flag : "transparent" - - property var flag: currentOnScreenMediaData.values.flagColourRole - - - - // access the 'dnmask_settings' attribute group - XsModuleData { - id: vp_flag_indicator_settings - modelDataName: "vp_flag_indicator" - } - - XsAttributeValue { - id: __indicator_size - attributeTitle: "Indicator Size" - model: vp_flag_indicator_settings - } - property alias indicatorSize: __indicator_size.value - - /*XsAttributeValue { - id: __flag_colours - attributeTitle: "Flag Colours" - model: vp_flag_indicator_settings - } - property alias flagColours: __flag_colours.value*/ - -} \ No newline at end of file diff --git a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/check.svg b/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/check.svg deleted file mode 100755 index 1c209899d..000000000 --- a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/qmldir b/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/qmldir deleted file mode 100644 index 991e282e3..000000000 --- a/src/plugin/python_plugins/picture_in_picture/qml/ViewportFlagIndicator.1/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module DnMask - -DnMaskSettingsDialog 1.0 DnMaskSettingsDialog.qml -DnMaskOverlay 1.0 DnMaskOverlay.qml \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml index 3c9528c52..9b65edd4a 100644 --- a/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml +++ b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml @@ -15,9 +15,9 @@ import xstudio.qml.models 1.0 // is on screen. If no flag is set, we don't show anything Rectangle { - width: indicatorSize + width: indicatorSize*(view.width/1920) height: width - radius: indicatorSize/2 + radius: width/2 color: flag ? flag : "transparent" property var flag: currentOnScreenMediaData.values.flagColourRole diff --git a/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py b/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py index 00f7e21cb..6ec514cc8 100755 --- a/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py +++ b/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py @@ -80,16 +80,6 @@ def __init__(self, connection): # attribute to the settings panel for the plugin self.add_hud_settings_attribute(self.indicator_size) - # Adding an attribute whose value is a Json dictionary. The entries in - # the dictionary will be the on-screen media flag colour for each - # viewport. There will on be one instance of this plugin, but xSTUDIO - # can have multiple viewports showing different media so we need to - # track multiple 'flag' colours - self.flag_colours_attr = self.add_attribute( - "Flag Colours", - JsonStore({})) - self.flag_colours_attr.expose_in_ui_attrs_group("vp_flag_indicator") - # here we provide the QML code to instance the item that will draw # the overlay graphics self.hud_element_qml( diff --git a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp index 1ab286a9e..8200d935d 100644 --- a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp +++ b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp @@ -375,7 +375,7 @@ template class DNRunPluginActor : public caf::event_based_actor { *sys, session, session::get_playlist_atom_v, - "QuickView Playlist"); + "QuickView Media"); } catch (...) { } } @@ -392,10 +392,7 @@ template class DNRunPluginActor : public caf::event_based_actor { if (!playlist) { playlist = request_receive( - *sys, - session, - session::get_playlist_atom_v, - "DNRun Playlist"); + *sys, session, session::get_playlist_atom_v, "Added Media"); } // third, make a new 'Ivy Media' playlist @@ -405,7 +402,7 @@ template class DNRunPluginActor : public caf::event_based_actor { *sys, session, session::add_playlist_atom_v, - "DNRun Playlist") + "Added Media") .second.actor(); } diff --git a/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml index 07dca952b..73a83cc64 100644 --- a/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml +++ b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml @@ -133,11 +133,11 @@ XsMenuItemNew { Future.promise(theSessionData.importFuture(path, null)).then( function(result) { - if (result) { - dialogHelpers.errorDialogFunc("Snapshot Loaded", "Snapshot " + name + " was added to your session.") - } else { - dialogHelpers.errorDialogFunc("Snapshot Error", "Snapshot " + name + " was not added to your session, an error occurred. Check your terminal for more info.") - } + // if (result) { + // dialogHelpers.errorDialogFunc("Snapshot Loaded", "Snapshot " + name + " was added to your session.") + // } else { + // dialogHelpers.errorDialogFunc("Snapshot Error", "Snapshot " + name + " was not added to your session, an error occurred. Check your terminal for more info.") + // } }) } diff --git a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp index 2a9252d9f..28734e277 100644 --- a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp +++ b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp @@ -12,11 +12,10 @@ using namespace xstudio::ui::viewport; using namespace xstudio; DefaultViewportLayout::DefaultViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings) + caf::actor_config &cfg, const utility::JsonStore &init_settings) : ViewportLayoutPlugin(cfg, init_settings) { - // Note: the base class takes care of making the actual layout data. The + // Note: the base class takes care of making the actual layout data. The // default layout is to just display the 'hero' image in the ImageDisplaySet // with no transform applied. @@ -31,12 +30,12 @@ DefaultViewportLayout::DefaultViewportLayout( if (!init_settings.value("is_python", false)) { add_layout_mode("Off", playhead::AssemblyMode::AM_ONE); add_layout_mode("String", playhead::AssemblyMode::AM_STRING); - add_layout_mode("A/B", playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + add_layout_mode( + "A/B", playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); } - } -ViewportRenderer * DefaultViewportLayout::make_renderer(const std::string &window_id) { +ViewportRenderer *DefaultViewportLayout::make_renderer(const std::string &window_id) { return new opengl::OpenGLViewportRenderer(window_id); } diff --git a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp index 832e3a786..747cc02d3 100644 --- a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp +++ b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp @@ -9,37 +9,35 @@ namespace xstudio { namespace ui { -namespace viewport { - - /** - * @brief DefaultViewportLayout class. - * - * @details - * This plugin provides the default viewport layout behviour. It simply - * plots a single image to the viewport fitted to the -1.0 < x < 1.0 and - * centered on (0.0, 0.0). - * - * When there are multiple playhead sources selected it simply plots the - * 'hero' image and ignores other image streams. - * No compositing or multi-image layouts are available. - * - * It provides the 'String' and 'Off' - */ - - class DefaultViewportLayout : public ViewportLayoutPlugin { - public: - - DefaultViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings); - - ~DefaultViewportLayout() override = default; - - inline static const utility::Uuid PLUGIN_UUID = {"f7d63ed9-80ed-4ce9-8b39-1d5e5079ce4b"}; - - ViewportRenderer * make_renderer(const std::string &window_id) override; - - }; -} // namespace viewport + namespace viewport { + + /** + * @brief DefaultViewportLayout class. + * + * @details + * This plugin provides the default viewport layout behviour. It simply + * plots a single image to the viewport fitted to the -1.0 < x < 1.0 and + * centered on (0.0, 0.0). + * + * When there are multiple playhead sources selected it simply plots the + * 'hero' image and ignores other image streams. + * No compositing or multi-image layouts are available. + * + * It provides the 'String' and 'Off' + */ + + class DefaultViewportLayout : public ViewportLayoutPlugin { + public: + DefaultViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~DefaultViewportLayout() override = default; + + inline static const utility::Uuid PLUGIN_UUID = { + "f7d63ed9-80ed-4ce9-8b39-1d5e5079ce4b"}; + + ViewportRenderer *make_renderer(const std::string &window_id) override; + }; + } // namespace viewport } // namespace ui } // namespace xstudio diff --git a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp index 59578ff1a..dbdf2770d 100644 --- a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp +++ b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp @@ -12,13 +12,14 @@ using namespace xstudio::ui::viewport; using namespace xstudio; GridViewportLayout::GridViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings) + caf::actor_config &cfg, const utility::JsonStore &init_settings) : ViewportLayoutPlugin(cfg, init_settings) { spacing_ = add_float_attribute("Spacing", "Spacing", 0.0f, 0.0f, 50.0f, 0.5f); spacing_->set_redraw_viewport_on_change(true); - spacing_->set_role_data(module::Attribute::ToolTip, "Spacing between images in grid layout as a % of image size."); + spacing_->set_role_data( + module::Attribute::ToolTip, + "Spacing between images in grid layout as a % of image size."); add_layout_settings_attribute(spacing_, "Grid"); aspect_mode_ = add_string_choice_attribute( @@ -29,7 +30,7 @@ GridViewportLayout::GridViewportLayout( aspect_mode_->set_redraw_viewport_on_change(true); aspect_mode_->set_role_data( module::Attribute::ToolTip, -R"(Set how the cell aspect is set. Auto means the most common image aspect in the + R"(Set how the cell aspect is set. Auto means the most common image aspect in the set of images being displayed is used. Hero means that the current 'hero' image sets the aspect of all cells in the layout. 16:9 or 1.89 forces equivalent aspect.)"); add_layout_settings_attribute(aspect_mode_, "Grid"); @@ -38,29 +39,27 @@ sets the aspect of all cells in the layout. 16:9 or 1.89 forces equivalent aspec add_layout_mode("Grid", playhead::AssemblyMode::AM_ALL); add_layout_mode("Horizontal", playhead::AssemblyMode::AM_ALL); add_layout_mode("Vertical", playhead::AssemblyMode::AM_ALL); - } void GridViewportLayout::do_layout( const std::string &layout_mode, const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data - ) -{ + media_reader::ImageSetLayoutData &layout_data) { const int num_images = image_set->num_onscreen_images(); if (!num_images) { return; } - if (!image_set) return; + if (!image_set) + return; const auto hero_image = image_set->hero_image(); - - float hero_aspect = hero_image ? hero_image->image_aspect() : 16.0f/9.0f; + + float hero_aspect = hero_image ? hero_image->image_aspect() : 16.0f / 9.0f; if (aspect_mode_->value() == "Auto") { std::map aspects; for (int i = 0; i < num_images; ++i) { - const auto & im = image_set->onscreen_image(i); + const auto &im = image_set->onscreen_image(i); if (im) { float a = im->image_aspect(); if (aspects.count(a)) { @@ -71,49 +70,52 @@ void GridViewportLayout::do_layout( } } int c = 0; - for (const auto &p: aspects) { + for (const auto &p : aspects) { if (p.second > c) { - c = p.second; + c = p.second; hero_aspect = p.first; } } } else if (hero_aspect && aspect_mode_->value() == "Hero") { } else if (aspect_mode_->value() == "16:9") { - hero_aspect = 16.0f/9.0f; + hero_aspect = 16.0f / 9.0f; } else if (aspect_mode_->value() == "1.89") { - hero_aspect = 2048.0f/1080.0f; + hero_aspect = 2048.0f / 1080.0f; } layout_data.image_transforms_.resize(image_set->num_onscreen_images()); - int num_rows = layout_mode == "Grid" ? (int)round(sqrt(float(num_images))) : layout_mode == "Vertical" ? num_images : 1; - int num_cols = layout_mode == "Grid" ? (int)ceil(float(num_images) / float(num_rows)) : layout_mode == "Vertical" ? 1 : num_images; + int num_rows = layout_mode == "Grid" ? (int)round(sqrt(float(num_images))) + : layout_mode == "Vertical" ? num_images + : 1; + int num_cols = layout_mode == "Grid" ? (int)ceil(float(num_images) / float(num_rows)) + : layout_mode == "Vertical" ? 1 + : num_images; - layout_data.layout_aspect_ = (hero_aspect)*float(num_cols)/float(num_rows); + layout_data.layout_aspect_ = (hero_aspect) * float(num_cols) / float(num_rows); - float scale = (1.0f-spacing_->value()/100.0f)/float(num_cols); + float scale = (1.0f - spacing_->value() / 100.0f) / float(num_cols); - float x_off = -1.0f + 1.0f/num_cols; - float y_off = (-1.0f + 1.0f/num_rows)/layout_data.layout_aspect_; - float x_step = num_cols > 1 ? 2.0f/(num_cols) : 0.0f; - float y_step = num_rows > 1 ? 2.0f/(layout_data.layout_aspect_*num_rows) : 0.0f; + float x_off = -1.0f + 1.0f / num_cols; + float y_off = (-1.0f + 1.0f / num_rows) / layout_data.layout_aspect_; + float x_step = num_cols > 1 ? 2.0f / (num_cols) : 0.0f; + float y_step = num_rows > 1 ? 2.0f / (layout_data.layout_aspect_ * num_rows) : 0.0f; for (int i = 0; i < num_images; ++i) { - const auto & im = image_set->onscreen_image(i); - float xs = 1.0f; + const auto &im = image_set->onscreen_image(i); + float xs = 1.0f; if (im) { if (im->image_aspect() < hero_aspect) { - xs = im->image_aspect()/hero_aspect; + xs = im->image_aspect() / hero_aspect; } } - int row = i / num_cols; - int col = i % num_cols; - Imath::M44f & m = layout_data.image_transforms_[i]; + int row = i / num_cols; + int col = i % num_cols; + Imath::M44f &m = layout_data.image_transforms_[i]; m.makeIdentity(); - m.translate(Imath::V3f(x_off+col*x_step, y_off+row*y_step, 0.0f)); - m.scale(Imath::V3f(scale*xs, scale*xs, 1.0f)); - + m.translate(Imath::V3f(x_off + col * x_step, y_off + row * y_step, 0.0f)); + m.scale(Imath::V3f(scale * xs, scale * xs, 1.0f)); } // we draw all images. It doesn't matter the order that the are drawn @@ -122,10 +124,9 @@ void GridViewportLayout::do_layout( for (int i = 0; i < num_images; ++i) { layout_data.image_draw_order_hint_[i] = i; } - } -ViewportRenderer * GridViewportLayout::make_renderer(const std::string &window_id) { +ViewportRenderer *GridViewportLayout::make_renderer(const std::string &window_id) { return new opengl::OpenGLViewportRenderer(window_id); } diff --git a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp index 74aa78d50..c77fcc886 100644 --- a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp +++ b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp @@ -5,41 +5,36 @@ namespace xstudio { namespace ui { -namespace viewport { + namespace viewport { - /** - * @brief GridViewportLayout class. - * - * @details - * This plugin provides a layout plugin for arranging multiple images - * in a grid - */ + /** + * @brief GridViewportLayout class. + * + * @details + * This plugin provides a layout plugin for arranging multiple images + * in a grid + */ - class GridViewportLayout : public ViewportLayoutPlugin { - public: + class GridViewportLayout : public ViewportLayoutPlugin { + public: + GridViewportLayout(caf::actor_config &cfg, const utility::JsonStore &init_settings); - GridViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings); + ~GridViewportLayout() override = default; - ~GridViewportLayout() override = default; + inline static const utility::Uuid PLUGIN_UUID = { + "b2f442c8-a185-4267-af98-66af43fdce68"}; - inline static const utility::Uuid PLUGIN_UUID = {"b2f442c8-a185-4267-af98-66af43fdce68"}; + protected: + ViewportRenderer *make_renderer(const std::string &window_id) override; - protected: + void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) override; - ViewportRenderer * make_renderer(const std::string &window_id) override; - - void do_layout( - const std::string &layout_mode, - const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data) override; - - module::FloatAttribute * spacing_; - module::StringChoiceAttribute * aspect_mode_; - - - }; -} // namespace viewport + module::FloatAttribute *spacing_; + module::StringChoiceAttribute *aspect_mode_; + }; + } // namespace viewport } // namespace ui } // namespace xstudio diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp index 0f47a84bd..f5a41c76d 100644 --- a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp @@ -16,15 +16,17 @@ using namespace xstudio; using namespace xstudio::ui::opengl; -class OpenGLViewportWipeRenderer: public OpenGLViewportRenderer { +class OpenGLViewportWipeRenderer : public OpenGLViewportRenderer { - public: - - OpenGLViewportWipeRenderer(const std::string &window_id) : OpenGLViewportRenderer(window_id) {} + public: + OpenGLViewportWipeRenderer(const std::string &window_id) + : OpenGLViewportRenderer(window_id) {} virtual ~OpenGLViewportWipeRenderer() { - if (wipe_vbo_) glDeleteBuffers(1, &wipe_vbo_); - if (wipe_vao_) glDeleteVertexArrays(1, &wipe_vao_); + if (wipe_vbo_) + glDeleteBuffers(1, &wipe_vbo_); + if (wipe_vao_) + glDeleteVertexArrays(1, &wipe_vao_); } void pre_init() override { @@ -34,16 +36,15 @@ class OpenGLViewportWipeRenderer: public OpenGLViewportRenderer { } void draw_image( - const media_reader::ImageBufPtr &image, - const media_reader::ImageSetLayoutDataPtr &layout_data, - const int index, - const Imath::M44f &window_to_viewport_matrix, - const Imath::M44f &viewport_to_image_space, - const float viewport_du_dx) override; + const media_reader::ImageBufPtr &image, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) override; unsigned int wipe_vbo_ = 0; unsigned int wipe_vao_ = 0; - }; void OpenGLViewportWipeRenderer::draw_image( @@ -54,14 +55,17 @@ void OpenGLViewportWipeRenderer::draw_image( const Imath::M44f &viewport_to_image_space, const float viewport_du_dx) { - // wipe value is the position of the wipe handle normalised to the + // wipe value is the position of the wipe handle normalised to the // viewport width. float wipe_screen_space = 0.5f; if (layout_data && layout_data->custom_layout_data_.contains("wipe_pos")) { wipe_screen_space = layout_data->custom_layout_data_.value("wipe_pos", 0.5f); } - const bool first_image = layout_data->image_draw_order_hint_.size() > 1 && index == layout_data->image_draw_order_hint_[1] ? false : true; + const bool first_image = layout_data->image_draw_order_hint_.size() > 1 && + index == layout_data->image_draw_order_hint_[1] + ? false + : true; if (wipe_screen_space <= 0.011f && first_image) { // wipe at the far left of the screen. Don't draw the wipe @@ -73,9 +77,9 @@ void OpenGLViewportWipeRenderer::draw_image( // re-normalise to -1.0,1.0 and multiply by projection matrix to get wipe // position in image space - Imath::V4f w(-1.0f+wipe_screen_space*2.0f, 0.0f, 0.0f, 1.0f); + Imath::V4f w(-1.0f + wipe_screen_space * 2.0f, 0.0f, 0.0f, 1.0f); w *= viewport_to_image_space; - float wipe_pos_image_space = w.x/w.w; + float wipe_pos_image_space = w.x / w.w; if (wipe_pos_image_space <= -1.0f && first_image) { // wipe is all the way left. Only draw second image! @@ -85,8 +89,9 @@ void OpenGLViewportWipeRenderer::draw_image( return; } - const bool no_wipe = layout_data->image_draw_order_hint_.size() < 2 || (wipe_screen_space <= 0.011f || wipe_screen_space > 0.989f) - || (wipe_pos_image_space <= -1.0f || wipe_pos_image_space > 0.9999f); + const bool no_wipe = layout_data->image_draw_order_hint_.size() < 2 || + (wipe_screen_space <= 0.011f || wipe_screen_space > 0.989f) || + (wipe_pos_image_space <= -1.0f || wipe_pos_image_space > 0.9999f); active_shader_program_->use(); @@ -98,24 +103,41 @@ void OpenGLViewportWipeRenderer::draw_image( viewport_to_image_space, viewport_du_dx, layout_data->custom_layout_data_, - index - ); + index); active_shader_program_->set_shader_parameters(shader_params); { - float left = no_wipe ? -1.0f : first_image ? -1.0 : wipe_pos_image_space; + float left = no_wipe ? -1.0f : first_image ? -1.0 : wipe_pos_image_space; float right = no_wipe ? 1.0f : first_image ? wipe_pos_image_space : 1.0; std::array vertices = { - // 1st triangle - left, 1.0f, 0.0f, 1.0f, // top left - right, 1.0f, 0.0f, 1.0f, // top right - right, -1.0f, 0.0f, 1.0f, // bottom right - // 2nd triangle - right, -1.0f, 0.0f, 1.0f, // bottom right - left, 1.0f, 0.0f, 1.0f, // top left - left, -1.0f, 0.0f, 1.0f // bottom left + // 1st triangle + left, + 1.0f, + 0.0f, + 1.0f, // top left + right, + 1.0f, + 0.0f, + 1.0f, // top right + right, + -1.0f, + 0.0f, + 1.0f, // bottom right + // 2nd triangle + right, + -1.0f, + 0.0f, + 1.0f, // bottom right + left, + 1.0f, + 0.0f, + 1.0f, // top left + left, + -1.0f, + 0.0f, + 1.0f // bottom left }; glBindVertexArray(wipe_vao_); @@ -132,26 +154,27 @@ void OpenGLViewportWipeRenderer::draw_image( glDrawArrays(GL_TRIANGLES, 0, 6); glDisableVertexAttribArray(0); glBindVertexArray(0); - } glUseProgram(0); - } WipeViewportLayout::WipeViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings) + caf::actor_config &cfg, const utility::JsonStore &init_settings) : ViewportLayoutPlugin(cfg, init_settings) { - wipe_position_ = add_vec4f_attribute("Wipe Position", "Wipe Position", Imath::V4f(0.5f,0.5f,0.0f,1.0f)); + wipe_position_ = add_vec4f_attribute( + "Wipe Position", "Wipe Position", Imath::V4f(0.5f, 0.5f, 0.0f, 1.0f)); wipe_position_->set_redraw_viewport_on_change(true); - wipe_position_->set_role_data(module::Attribute::ToolTip, "Spacing between images in grid layout as a % of image size."); + wipe_position_->set_role_data( + module::Attribute::ToolTip, + "Spacing between images in grid layout as a % of image size."); add_layout_settings_attribute(wipe_position_, "Wipe"); wipe_position_->expose_in_ui_attrs_group("wipe_layout_attrs"); - add_layout_mode("Wipe", playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + add_layout_mode( + "Wipe", playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); add_viewport_layout_qml_overlay( "Wipe", @@ -159,16 +182,13 @@ WipeViewportLayout::WipeViewportLayout( import WipeLayoutOverlay 1.0 WipeLayoutOverlay { } - )" - ); + )"); } void WipeViewportLayout::do_layout( const std::string &layout_mode, const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data - ) -{ + media_reader::ImageSetLayoutData &layout_data) { const int num_images = image_set->num_onscreen_images(); if (!num_images) { return; @@ -180,7 +200,10 @@ void WipeViewportLayout::do_layout( layout_data.image_draw_order_hint_.push_back(wipeA); if (num_images > 1) { - int wipeB = image_set->previous_hero_sub_playhead_index() != -1 ? image_set->previous_hero_sub_playhead_index() : wipeA ? 0 : 1; + int wipeB = image_set->previous_hero_sub_playhead_index() != -1 + ? image_set->previous_hero_sub_playhead_index() + : wipeA ? 0 + : 1; layout_data.image_draw_order_hint_.push_back(wipeB); } @@ -188,11 +211,12 @@ void WipeViewportLayout::do_layout( layout_data.image_transforms_.resize(image_set->num_onscreen_images()); layout_data.custom_layout_data_["wipe_pos"] = wipe_position_->value().x; - layout_data.layout_aspect_ = image_set->onscreen_image(wipeA) ? image_set->onscreen_image(wipeA)->image_aspect() : 16.0f/9.0f; - + layout_data.layout_aspect_ = image_set->onscreen_image(wipeA) + ? image_set->onscreen_image(wipeA)->image_aspect() + : 16.0f / 9.0f; } -ViewportRenderer * WipeViewportLayout::make_renderer(const std::string &window_id) { +ViewportRenderer *WipeViewportLayout::make_renderer(const std::string &window_id) { return new OpenGLViewportWipeRenderer(window_id); } diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp index 6cc0ed746..e236dbaf7 100644 --- a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp @@ -5,41 +5,35 @@ namespace xstudio { namespace ui { -namespace viewport { + namespace viewport { - /** - * @brief GridViewportLayout class. - * - * @details - * This plugin provides a layout plugin for arranging multiple images - * in a grid - */ + /** + * @brief GridViewportLayout class. + * + * @details + * This plugin provides a layout plugin for arranging multiple images + * in a grid + */ - class WipeViewportLayout : public ViewportLayoutPlugin { - public: + class WipeViewportLayout : public ViewportLayoutPlugin { + public: + WipeViewportLayout(caf::actor_config &cfg, const utility::JsonStore &init_settings); - WipeViewportLayout( - caf::actor_config &cfg, - const utility::JsonStore &init_settings); + ~WipeViewportLayout() override = default; - ~WipeViewportLayout() override = default; + inline static const utility::Uuid PLUGIN_UUID = { + "fa41b47a-6fde-4627-bd38-6e7834e75007"}; - inline static const utility::Uuid PLUGIN_UUID = {"fa41b47a-6fde-4627-bd38-6e7834e75007"}; + protected: + virtual ViewportRenderer *make_renderer(const std::string &window_id); - protected: + void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) override; - virtual ViewportRenderer * make_renderer(const std::string &window_id); - - void do_layout( - const std::string &layout_mode, - const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data - ) override; - - module::Vec4fAttribute * wipe_position_; - - - }; -} // namespace viewport + module::Vec4fAttribute *wipe_position_; + }; + } // namespace viewport } // namespace ui } // namespace xstudio diff --git a/src/plugin/viewport_overlay/CMakeLists.txt b/src/plugin/viewport_overlay/CMakeLists.txt index 4056b725f..0b5197dda 100644 --- a/src/plugin/viewport_overlay/CMakeLists.txt +++ b/src/plugin/viewport_overlay/CMakeLists.txt @@ -1,4 +1,5 @@ add_src_and_test(basic_viewport_mask) add_src_and_test(annotations) +add_src_and_test(audio_waveform) build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/viewport_overlay/annotations/src/annotation.hpp b/src/plugin/viewport_overlay/annotations/src/annotation.hpp index f00c25e3a..4f7b74133 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation.hpp @@ -22,6 +22,8 @@ namespace ui { [[nodiscard]] utility::JsonStore serialise(utility::Uuid &plugin_uuid) const override; + size_t hash() const override { return canvas_.hash(); } + xstudio::ui::canvas::Canvas &canvas() { return canvas_; } const xstudio::ui::canvas::Canvas &canvas() const { return canvas_; } diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp index f85378cce..e4f631ad1 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp @@ -13,29 +13,27 @@ using namespace xstudio::ui::canvas; using namespace xstudio::ui::viewport; -AnnotationsRenderer::AnnotationsRenderer() { +AnnotationsRenderer::AnnotationsRenderer(const xstudio::ui::canvas::Canvas &interaction_canvas) + : interaction_canvas_(interaction_canvas) { canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); } -void AnnotationsRenderer::render_opengl( +void AnnotationsRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) { - utility::BlindDataObjectPtr render_data = - frame.plugin_blind_data(AnnotationsTool::PLUGIN_UUID); - const auto *data = dynamic_cast(render_data.get()); - - // 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(); - + std::unique_lock l(mutex_); + if (!annotations_visible_) + return; // the xstudio playhead takes care of attaching bookmark data to the // ImageBufPtr that we receive here. The bookmark data may have annotations // data attached which we can draw to screen.... + bool draw_interaction_canvas = false; + for (const auto &anno : frame.bookmarks()) { // .. we don't draw the annotation attached to the frame if its bookmark @@ -43,8 +41,7 @@ void AnnotationsRenderer::render_opengl( // being edited. The reason is that the strokes and captions of this // annotation are already cloned into 'interaction_canvas_' which we // draw below. - if (data && anno->detail_.uuid_ == data->current_edited_bookmark_uuid_ && - data->interaction_frame_key_ == to_string(frame.frame_id().key())) { + if (anno->detail_.uuid_ == current_edited_bookmark_uuid_) { draw_interaction_canvas = true; continue; } @@ -63,23 +60,47 @@ void AnnotationsRenderer::render_opengl( } } - // 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 (data && draw_interaction_canvas) { + if (draw_interaction_canvas || (current_edited_bookmark_uuid_.is_null() && + (frame_being_annotated_ == frame.frame_id().key()))) { canvas_renderer_->render_canvas( - data->interaction_canvas_, - data->handle_, + interaction_canvas_, + handle_, transform_window_to_viewport_space, transform_viewport_to_image_space, viewport_du_dpixel, have_alpha_buffer, 1.f); } -} \ No newline at end of file +} + +void AnnotationsRenderer::render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer) { + std::unique_lock l(mutex_); + if (laser_drawing_mode_) { + canvas_renderer_->render_canvas( + interaction_canvas_, + handle_, + transform_window_to_viewport_space, + transform_viewport_to_normalised_coords, + viewport_du_dpixel, + have_alpha_buffer, + 1.f); + } +} + +void AnnotationsRenderer::update( + const bool show_annotations, + const xstudio::ui::canvas::HandleState &h, + const utility::Uuid ¤t_edited_bookmark_uuid, + const media::MediaKey &frame_being_annotated, + bool laser_mode) { + std::unique_lock l(mutex_); + handle_ = h; + current_edited_bookmark_uuid_ = current_edited_bookmark_uuid; + laser_drawing_mode_ = laser_mode; + frame_being_annotated_ = frame_being_annotated; + annotations_visible_ = show_annotations; +} diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp index 309667a9a..2f3895db4 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp @@ -13,19 +13,43 @@ namespace ui { class AnnotationsRenderer : public plugin::ViewportOverlayRenderer { public: - AnnotationsRenderer(); + AnnotationsRenderer(const xstudio::ui::canvas::Canvas &interaction_canvas); - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) override; + void render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer) override; + RenderPass preferred_render_pass() const override { return BeforeImage; } + void update( + const bool show_annotations, + const xstudio::ui::canvas::HandleState &h, + const utility::Uuid ¤t_edited_bookmark_uuid, + const media::MediaKey &frame_being_annotated, + bool laser_mode); + private: std::unique_ptr canvas_renderer_; + + // Canvas is thread safe + const xstudio::ui::canvas::Canvas &interaction_canvas_; + + xstudio::ui::canvas::HandleState handle_; + bool annotations_visible_; + bool laser_drawing_mode_; + utility::Uuid current_edited_bookmark_uuid_; + media_reader::ImageBufPtr image_being_annotated_; + media::MediaKey frame_being_annotated_; + std::mutex mutex_; }; } // end namespace viewport diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp index 31fe00710..386ad74aa 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp @@ -113,10 +113,7 @@ AnnotationsTool::AnnotationsTool( action_attribute_->expose_in_ui_attrs_group("annotations_tool_settings"); display_mode_attribute_ = add_string_choice_attribute( - "Display Mode", - "Disp. Mode", - "With Drawing Tools", - {"Only When Paused", "Always", "With Drawing Tools"}); + "Display Mode", "Disp. Mode", "With Drawing Tools", {"Only When Paused", "Always"}); display_mode_attribute_->expose_in_ui_attrs_group("annotations_tool_draw_mode"); display_mode_attribute_->set_preference_path("/plugin/annotations/display_mode"); @@ -204,11 +201,11 @@ caf::message_handler AnnotationsTool::message_handler_extensions() { // 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(100), utility::event_atom_v); + delayed_anon_send(this, std::chrono::milliseconds(25), utility::event_atom_v); } else { fade_looping_ = false; } - redraw_viewport(); + do_redraw(); }); } @@ -219,6 +216,7 @@ void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, con if (attribute_uuid == active_tool_->uuid()) { if (active_tool == "None") { + release_mouse_focus(); release_keyboard_focus(); end_drawing(); @@ -226,7 +224,7 @@ void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, con set_viewport_cursor(""); } else { - last_tool_ = active_tool; + grab_mouse_focus(); if (active_tool != "Text") { set_viewport_cursor("Qt.CrossCursor"); @@ -236,6 +234,20 @@ void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, con } else { set_viewport_cursor("Qt.IBeamCursor"); } + + if (active_tool == "Laser") { + + // switching INTO laser draw mode ... save any annotation to the + // bookmark if required + update_bookmark_annotation_data(); + interaction_canvas_.clear(true); + clear_caption_handle(); + current_bookmark_uuid_ = utility::Uuid(); + } else if (last_tool_ == "Laser") { + interaction_canvas_.clear(true); + } + + last_tool_ = active_tool; } } else if ( @@ -256,8 +268,6 @@ void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, con 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()) { @@ -317,20 +327,8 @@ void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, con } } - if (attribute_uuid == active_tool_->uuid()) { - - if (active_tool_->value() == "Laser") { - - // switching INTO laser draw mode ... save any annotation to the - // bookmark if required - update_bookmark_annotation_data(); - interaction_canvas_.clear(true); - clear_caption_handle(); - current_bookmark_uuid_ = utility::Uuid(); - } - } - redraw_viewport(); + do_redraw(); } void AnnotationsTool::update_attrs_from_preferences(const utility::JsonStore &j) { @@ -390,12 +388,12 @@ void AnnotationsTool::hotkey_pressed( } else if (hotkey_uuid == undo_hotkey_ && active_tool_->value() != "None") { undo(); - redraw_viewport(); + do_redraw(); } else if (hotkey_uuid == redo_hotkey_ && active_tool_->value() != "None") { redo(); - redraw_viewport(); + do_redraw(); } else if (hotkey_uuid == clear_hotkey_ && active_tool_->value() != "None") { clear_onscreen_annotations(); @@ -445,7 +443,8 @@ bool AnnotationsTool::pointer_event(const ui::PointerEvent &e) { if (e.type() == ui::Signature::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { start_editing(e.context(), pointer_pos); - start_or_edit_caption(image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); + start_or_edit_caption( + image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); grab_mouse_focus(); } else if ( e.type() == ui::Signature::EventType::Drag && @@ -454,23 +453,24 @@ bool AnnotationsTool::pointer_event(const ui::PointerEvent &e) { update_caption_action(image_transformed_ptr_pos(pointer_pos)); update_caption_handle(); } else if (e.buttons() == ui::Signature::Button::None) { - redraw = update_caption_hovered(image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); + redraw = update_caption_hovered( + image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); } } else { redraw = false; } if (redraw) - redraw_viewport(); + do_redraw(); return false; } Imath::V2f AnnotationsTool::image_transformed_ptr_pos(const Imath::V2f &p) const { - if (image_being_annotated_) { + if (image_being_annotated_ && !is_laser_mode()) { Imath::V4f pt(p.x, p.y, 0.0f, 1.0f); pt *= image_being_annotated_.layout_transform().inverse(); - return Imath::V2f(pt.x/pt.w, pt.y/pt.w); + return Imath::V2f(pt.x / pt.w, pt.y / pt.w); } return p; } @@ -480,7 +480,7 @@ void AnnotationsTool::text_entered(const std::string &text, const std::string &c if (active_tool_->value() == "Text") { interaction_canvas_.update_caption_text(text); update_caption_handle(); - redraw_viewport(); + do_redraw(); } } @@ -494,50 +494,10 @@ void AnnotationsTool::key_pressed( } interaction_canvas_.move_caption_cursor(key); update_caption_handle(); - redraw_viewport(); + do_redraw(); } } -utility::BlindDataObjectPtr AnnotationsTool::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string &viewport_name) const { - - // Rendering the viewport (including viewport overlays) occurs in - // a separate thread to the one that instances of this class live in. - // - // xSTUDIO calls this function (in our thread) so we can attach any and all - // data we want to an image using a 'BlindDataObjectPtr'. We subclass - // BlindDataObject with AnnotationRenderDataSet allowing us to bung whatever - // draw time data we want and need. This is then later available in the - // rendering thread in a thread safe manner (as long as we do it right here - // and don't pass in pointers to member data of AnnotationsTool - with the - // exception of the Canvas class which has been made thread safe) - - if (viewport_name != "snapshot_viewport") { - // snapshot viewport must always draw annotations - if (!((display_mode_ == Always) || - (display_mode_ == WithDrawTool && active_tool_->value() != "None") || - (display_mode_ == OnlyWhenPaused && !playhead_is_playing_))) { - // don't draw annotations, return empty data - return utility::BlindDataObjectPtr(); - } - } - - if (image_being_annotated_ == image) { - // 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_, - image_being_annotated_ ? to_string(image_being_annotated_->media_key()) : ""); - - return utility::BlindDataObjectPtr( - static_cast(immediate_render_data)); - } - return utility::BlindDataObjectPtr(); - -} - void AnnotationsTool::images_going_on_screen( const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, @@ -552,14 +512,14 @@ void AnnotationsTool::images_going_on_screen( playhead_is_playing_ = playhead_playing; + bool show_annotations = + (display_mode_ == Always) || (display_mode_ == OnlyWhenPaused && !playhead_is_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].reset(); + viewport_current_images_[viewport_name] = images; if (!interaction_canvas_.empty() && !current_bookmark_uuid_.is_null() && current_interaction_viewport_name_ == viewport_name) { @@ -568,11 +528,11 @@ void AnnotationsTool::images_going_on_screen( // looks like we are editing an annotation. Is the annotation still on // screen (i.e. has the user scrubbed away from it) if (images && images->layout_data()) { - const auto & im_idx = images->layout_data()->image_draw_order_hint_; - for (auto &idx: im_idx) { + const auto &im_idx = images->layout_data()->image_draw_order_hint_; + for (auto &idx : im_idx) { // loop over onscreen images. Check if the current bookmark is // visible on any of them. - const auto & cim = images->onscreen_image(idx); + const auto &cim = images->onscreen_image(idx); for (auto &bookmark : cim.bookmarks()) { auto anno = dynamic_cast(bookmark->annotation_.get()); @@ -589,18 +549,41 @@ void AnnotationsTool::images_going_on_screen( // user must have scrubbed away from it in the timeline. Thus we // push it to the bookmark and clear update_bookmark_annotation_data(); + current_bookmark_uuid_ = utility::Uuid(); interaction_canvas_.clear(true); clear_caption_handle(); - current_bookmark_uuid_ = utility::Uuid(); // calling these updates the renderes with the now cleared // interaction canvas data } } + + if (renderers_.count(viewport_name)) { + static_cast(renderers_[viewport_name].get()) + ->update( + show_annotations, + handle_state_, + current_bookmark_uuid_, + image_being_annotated_.frame_id().key(), + is_laser_mode()); + } } -plugin::ViewportOverlayRendererPtr AnnotationsTool::make_overlay_renderer() { - return plugin::ViewportOverlayRendererPtr(new AnnotationsRenderer()); +plugin::ViewportOverlayRendererPtr +AnnotationsTool::make_overlay_renderer(const std::string &viewport_name) { + + + if (viewport_name.find("snapshot") == std::string::npos) { + // we need to keep a list of regular viewport or quickview viewport renderers so we can + // do real-time updates on their data during interaction + renderers_[viewport_name].reset(new AnnotationsRenderer(interaction_canvas_)); + return renderers_[viewport_name]; + } + // the snapsot viewport is different - we want it to always draw the annotations + // when rendering snaphsots + auto rt = new AnnotationsRenderer(interaction_canvas_); + rt->update(true, handle_state_, utility::Uuid(), media::MediaKey(), false); + return plugin::ViewportOverlayRendererPtr(rt); } AnnotationBasePtr AnnotationsTool::build_annotation(const utility::JsonStore &anno_data) { @@ -626,15 +609,12 @@ void AnnotationsTool::viewport_dockable_widget_deactivated(std::string &widget_n void AnnotationsTool::turn_off_overlay_interaction() { active_tool_->set_value("None"); } -void AnnotationsTool::start_editing(const std::string &viewport_name, const Imath::V2f &pointer_position) { +void AnnotationsTool::start_editing( + const std::string &viewport_name, const Imath::V2f &pointer_position) { // ensure playback is stopped start_stop_playback(viewport_name, false); - if (is_laser_mode()) - return; - - // if the viewport is in grid mode, with multiple images laid out, which one // was clicked in ? auto before = image_being_annotated_; @@ -644,13 +624,13 @@ void AnnotationsTool::start_editing(const std::string &viewport_name, const Imat // first, check if pointer_position lands on one of the images in // the viewport - bool curr_im_is_onscreen = false; + bool curr_im_is_onscreen = false; const media_reader::ImageBufDisplaySetPtr &onscreen_image_set = p->second; - const auto & im_idx = onscreen_image_set->layout_data()->image_draw_order_hint_; - for (auto &idx: im_idx) { + const auto &im_idx = onscreen_image_set->layout_data()->image_draw_order_hint_; + for (auto &idx : im_idx) { // loop over onscreen images. translate pointer position to image // space coords - const auto & cim = onscreen_image_set->onscreen_image(idx); + const auto &cim = onscreen_image_set->onscreen_image(idx); if (cim) { @@ -658,16 +638,16 @@ void AnnotationsTool::start_editing(const std::string &viewport_name, const Imat pt *= cim.layout_transform().inverse(); // does the pointer land on the image? - float a = 1.0f/cim->image_aspect(); - if (pt.x/pt.w >= -1.0f && pt.x/pt.w <= 1.0f && - pt.y/pt.w >= -a && pt.y/pt.w <= a) { + float a = 1.0f / cim->image_aspect(); + if (pt.x / pt.w >= -1.0f && pt.x / pt.w <= 1.0f && pt.y / pt.w >= -a && + pt.y / pt.w <= a) { new_image_to_annotate = cim; } // check if image_being_annotated_ (from last time we entered this // method) is in the onscreen set - if (image_being_annotated_ == cim) curr_im_is_onscreen = true; + if (image_being_annotated_ == cim) + curr_im_is_onscreen = true; } - } if (new_image_to_annotate) { @@ -690,42 +670,54 @@ void AnnotationsTool::start_editing(const std::string &viewport_name, const Imat if (!current_bookmark_uuid_.is_null() && current_interaction_viewport_name_ == viewport_name) { + // bookmark id is has not changed, neither has the viewport that + // the interaction is happening in. return; } current_interaction_viewport_name_ = viewport_name; - // Is there an annotation on screen that we should start appending to? - Annotation *to_edit = nullptr; - current_bookmark_uuid_ = utility::Uuid(); - utility::Uuid first_bookmark_uuid; - if (image_being_annotated_) { - - for (auto &bookmark : image_being_annotated_.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 (is_laser_mode()) { + + current_bookmark_uuid_ = utility::Uuid(); + clear_caption_handle(); + image_being_annotated_ = media_reader::ImageBufPtr(); + + } else { + + // 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; + if (image_being_annotated_) { + + for (auto &bookmark : image_being_annotated_.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_; + } } } - } - clear_caption_handle(); + clear_caption_handle(); - // clone the whole annotation into our 'interaction_canvas_' - if (to_edit) - interaction_canvas_ = to_edit->canvas(); - else { - // there is a bookmark which doesn't have annotations (yet). We will - // add annotations to this bookmark - interaction_canvas_.clear(true); - current_bookmark_uuid_ = first_bookmark_uuid; + // clone the whole annotation into our 'interaction_canvas_' + if (to_edit) { + interaction_canvas_ = to_edit->canvas(); + } else { + // there is a bookmark which doesn't have annotations (yet). We will + // add annotations to this bookmark + interaction_canvas_.clear(true); + current_bookmark_uuid_ = first_bookmark_uuid; + } } } @@ -897,15 +889,13 @@ bool AnnotationsTool::update_caption_hovered( const HandleState old_state = handle_state_; - auto &canvas = interaction_canvas_; - - if (canvas.has_selected_caption()) { - handle_state_.current_caption_bdb = canvas.caption_bounding_box(); - handle_state_.hover_state = canvas.hover_selected_caption_handle( + if (interaction_canvas_.has_selected_caption()) { + handle_state_.current_caption_bdb = interaction_canvas_.caption_bounding_box(); + handle_state_.hover_state = interaction_canvas_.hover_selected_caption_handle( pointer_pos, handle_state_.handle_size, viewport_pixel_scale); } handle_state_.under_mouse_caption_bdb = - canvas.hover_caption_bounding_box(pointer_pos, viewport_pixel_scale); + interaction_canvas_.hover_caption_bounding_box(pointer_pos, viewport_pixel_scale); if (handle_state_ != old_state) { moving_scaling_text_attr_->set_value(int(handle_state_.hover_state)); } @@ -926,7 +916,7 @@ void AnnotationsTool::update_caption_handle() { } if (handle_state_ != old_state) { - redraw_viewport(); + do_redraw(); } } @@ -989,13 +979,14 @@ void AnnotationsTool::update_bookmark_annotation_data() { note_name = std::string(note_name, 0, note_name.find(".")); } current_bookmark_uuid_ = StandardPlugin::create_bookmark_on_frame( - image_being_annotated_.frame_id(), note_name, bookmark::BookmarkDetail(), false); - + image_being_annotated_.frame_id(), + note_name, + bookmark::BookmarkDetail(), + false); } if (!current_bookmark_uuid_.is_null()) update_bookmark_annotation_data(); - } } @@ -1005,7 +996,7 @@ void AnnotationsTool::undo() { start_editing(current_interaction_viewport_name_); interaction_canvas_.undo(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } void AnnotationsTool::redo() { @@ -1013,7 +1004,7 @@ void AnnotationsTool::redo() { start_editing(current_interaction_viewport_name_); interaction_canvas_.redo(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } @@ -1021,7 +1012,7 @@ void AnnotationsTool::clear_onscreen_annotations() { clear_edited_annotation(); void AnnotationsTool::restore_onscreen_annotations() { // TODO: reinstate this behaviour - redraw_viewport(); + do_redraw(); } void AnnotationsTool::clear_edited_annotation() { @@ -1030,9 +1021,11 @@ void AnnotationsTool::clear_edited_annotation() { start_editing(current_interaction_viewport_name_); interaction_canvas_.clear(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } +void AnnotationsTool::do_redraw() { redraw_viewport(); } + extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { return new plugin_manager::PluginFactoryCollection( diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp index 292aac5da..f49373726 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp @@ -38,11 +38,8 @@ namespace ui { void key_pressed( const int key, const std::string &context, const bool auto_repeat) override; - utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, - const std::string &viewport_name) const override; - - plugin::ViewportOverlayRendererPtr make_overlay_renderer() override; + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override; bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &anno_data) override; @@ -61,7 +58,9 @@ namespace ui { private: bool is_laser_mode() const; - void start_editing(const std::string &viewport_name, const Imath::V2f &pointer_position = Imath::V2f(-1e6f, -1e6f)); + void start_editing( + const std::string &viewport_name, + const Imath::V2f &pointer_position = Imath::V2f(-1e6f, -1e6f)); void start_stroke(const Imath::V2f &point); void update_stroke(const Imath::V2f &point); @@ -87,10 +86,11 @@ namespace ui { void clear_edited_annotation(); void update_bookmark_annotation_data(); Imath::V2f image_transformed_ptr_pos(const Imath::V2f &p) const; + void do_redraw(); private: enum Tool { Draw, Laser, Square, Circle, Arrow, Line, Text, Erase, None }; - enum DisplayMode { OnlyWhenPaused, Always, WithDrawTool }; + enum DisplayMode { OnlyWhenPaused, Always }; const std::map tool_names_ = { {Draw, "Draw"}, @@ -155,10 +155,10 @@ namespace ui { std::string last_tool_ = {"Draw"}; bool fade_looping_{false}; - std::map - viewport_current_images_; + std::map viewport_current_images_; media_reader::ImageBufPtr image_being_annotated_; + std::map renderers_; }; } // namespace viewport diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml index cffb22585..5d43bde5f 100644 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml @@ -50,12 +50,12 @@ Item{ id: toolActionsDiv property alias display_mode: __display_mode.value XsButtonWithImageAndText{ id: displayBtn - iconText: "Always" + iconText: display_mode.substring(display_mode.length-6, display_mode.length) x: framePadding width: parent.width- x*2 height: XsStyleSheet.primaryButtonStdHeight anchors.top: dispText.bottom - iconSrc: "qrc:///anno_icons/check_circle.svg" + iconSrc: "qrc:///anno_icons/pause_circle.svg" textDiv.visible: true textDiv.font.bold: false textDiv.font.pixelSize: XsStyleSheet.fontSize @@ -70,9 +70,6 @@ Item{ id: toolActionsDiv } } - Component.onCompleted: { - display_mode = "Always" - } } XsPopupMenu { @@ -87,8 +84,7 @@ Item{ id: toolActionsDiv menuItemPosition: 1 menuModelName: displayMenu.menu_model_name onActivated: { - display_mode = text - displayBtn.iconText = text + display_mode = "Always" displayBtn.iconSrc = menuCustomIcon } } @@ -100,7 +96,6 @@ Item{ id: toolActionsDiv menuModelName: displayMenu.menu_model_name onActivated: { display_mode = "Only When Paused" - displayBtn.iconText = "Paused" displayBtn.iconSrc = menuCustomIcon } } diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml index 549da5150..5f3757b96 100644 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml @@ -79,14 +79,14 @@ Item{ id: toolActionsDiv property alias display_mode: __display_mode.value XsButtonWithImageAndText{ id: displayBtn - iconText: "Always" + iconText: display_mode.substring(display_mode.length-6, display_mode.length) width: XsStyleSheet.primaryButtonStdWidth*2.2 height: XsStyleSheet.primaryButtonStdHeight anchors.verticalCenter: parent.verticalCenter anchors.left: toolActionUndoRedo.right anchors.leftMargin: itemSpacing //framePadding - iconSrc: "qrc:///anno_icons/check_circle.svg" + iconSrc: display_mode == "Always" ? "qrc:///anno_icons/check_circle.svg" : "qrc:///anno_icons/pause_circle.svg" textDiv.visible: true textDiv.font.bold: false textDiv.font.pixelSize: XsStyleSheet.fontSize @@ -100,10 +100,6 @@ Item{ id: toolActionsDiv displayMenu.visible = true } } - - Component.onCompleted: { - display_mode = "Always" - } } XsPopupMenu { @@ -114,25 +110,19 @@ Item{ id: toolActionsDiv XsMenuModelItem { text: "Always" menuPath: "" - menuCustomIcon: "qrc:///anno_icons/check_circle.svg" menuItemPosition: 1 menuModelName: displayMenu.menu_model_name onActivated: { - display_mode = text - displayBtn.iconText = text - displayBtn.iconSrc = menuCustomIcon + display_mode = "Always" } } XsMenuModelItem { text: "When Paused" menuPath: "" - menuCustomIcon: "qrc:///anno_icons/pause_circle.svg" menuItemPosition: 2 menuModelName: displayMenu.menu_model_name onActivated: { display_mode = "Only When Paused" - displayBtn.iconText = "Paused" - displayBtn.iconSrc = menuCustomIcon } } diff --git a/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt b/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt new file mode 100644 index 000000000..74a6b7f8f --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt @@ -0,0 +1,11 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(audio_waveform_overlay xstudio::viewport::audio_waveform_overlay 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp new file mode 100644 index 000000000..04eab30a6 --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "audio_waveform_overlay.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/utility/blind_data.hpp" +#include "xstudio/ui/viewport/viewport_helpers.hpp" +#include "xstudio/utility/helpers.hpp" + +#include +#include + +using namespace xstudio; +using namespace xstudio::ui::viewport; + +namespace { +const char *vertex_shader = R"( + #version 330 core + layout (location = 0) in float ypos; + uniform mat4 to_coord_system; + uniform mat4 to_canvas; + uniform float hscale; + uniform float vscale; + uniform float v_pos; + uniform int offset; + + void main() + { + vec4 rpos = vec4(-1.0 + float(gl_VertexID-offset)*hscale, v_pos+ypos*vscale*10.0, vec2(0.0, 1.0)); + gl_Position = rpos*to_canvas; + } + )"; + +const char *frag_shader = R"( + #version 330 core + out vec4 FragColor; + uniform vec3 line_colour; + + void main(void) + { + FragColor = vec4(line_colour, 1.0); + } + + )"; +} // namespace + +AudioWaveformOverlayRenderer::~AudioWaveformOverlayRenderer() { + if (vbo_) + glDeleteBuffers(1, &vbo_); + if (vao_) + glDeleteBuffers(1, &vao_); +} + +void AudioWaveformOverlayRenderer::render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) { + + if (!shader_) + init_overlay_opengl(); + + auto render_data = + frame.plugin_blind_data(utility::Uuid("873c508b-276b-44e3-82d0-15db2f039aa7")); + if (!render_data) + return; + + const auto *data = dynamic_cast(render_data.get()); + if (!data) + return; + + glBindVertexArray(vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData( + GL_ARRAY_BUFFER, + data->verts_.size() * sizeof(float), + data->verts_.data(), + GL_STREAM_DRAW); + // 3. then set our vertex module pointers + glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + const int n_samps = data->verts_.size() / data->num_chans; + + utility::JsonStore shader_params; + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["hscale"] = 2.0f / float(n_samps); + shader_params["vscale"] = data->vscale; + shader_params["line_colour"] = data->line_colour; + shader_->set_shader_parameters(shader_params); + shader_->use(); + glEnableVertexAttribArray(0); + + for (int c = 0; c < data->num_chans; ++c) { + utility::JsonStore es; + es["v_pos"] = data->v_pos + data->chan_spacing * c; + es["offset"] = c * n_samps; + shader_->set_shader_parameters(es); + // the actual draw! + glDrawArrays(GL_LINE_STRIP, c * n_samps, n_samps); + } + shader_->stop_using(); + glDisableVertexAttribArray(0); + glBindVertexArray(0); +} + +void AudioWaveformOverlayRenderer::init_overlay_opengl() { + + glGenBuffers(1, &vbo_); + glGenVertexArrays(1, &vao_); + + shader_ = std::make_unique(vertex_shader, frag_shader); +} + + +AudioWaveformOverlay::AudioWaveformOverlay( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::HUDPluginBase(cfg, "Audio Waveform", init_settings, 0.0f) { + + vertical_scale_ = + add_float_attribute("Vertical Scale", "Vertical Scale", 0.1f, 0.01f, 1.0f, 0.01f); + add_hud_settings_attribute(vertical_scale_); + vertical_scale_->set_tool_tip("Sets the vertical scaling of the waveform"); + + horizontal_scale_ = + add_float_attribute("Horizontal Scale", "Horizontal Scale", 50.0f, 10.0f, 100.0f, 1.0f); + add_hud_settings_attribute(horizontal_scale_); + horizontal_scale_->set_tool_tip("Sets the horizontal scaling of the waveform - the units " + "are milliseconds of audio shown on the screen"); + + chan_position_spacing_ = add_float_attribute( + "Chan Position Spacing", "Chan Position Spacing", 0.05f, 0.0f, 1.0f, 0.01f); + add_hud_settings_attribute(chan_position_spacing_); + chan_position_spacing_->set_tool_tip("Vertical spacing between channels"); + + vertical_position_ = add_float_attribute( + "Vertical Position", "Vertical Position", -0.8f, -1.0f, 1.0f, 0.01f); + add_hud_settings_attribute(vertical_position_); + vertical_position_->set_tool_tip("Vertical position for drawing the waveform"); + + separate_channels_ = + add_boolean_attribute("Show Channels Separately", "Show Channels Separately", false); + add_hud_settings_attribute(separate_channels_); + separate_channels_->set_tool_tip( + "Shows the waveforms of each channel, or combine channels if not selected."); + + line_colour_ = add_colour_attribute( + "Line Colour", "Line Colour", utility::ColourTriplet(1.0f, 1.0f, 0.0f)); + add_hud_settings_attribute(line_colour_); + line_colour_->set_tool_tip("The colour of the waveform line"); + + // Registering preference path allows these values to persist between sessions + vertical_scale_->set_preference_path("/plugin/audio_waveform/vertical_scale"); + horizontal_scale_->set_preference_path("/plugin/audio_waveform/horizontal_scale"); + chan_position_spacing_->set_preference_path("/plugin/audio_waveform/chan_position_spacing"); + vertical_position_->set_preference_path("/plugin/audio_waveform/vertical_position"); + line_colour_->set_preference_path("/plugin/audio_waveform/line_colour"); + + // get the global audio output actor and join its event group. This means we + // receive the broadcasted Audiobuffers + auto global_audio_actor = + system().registry().template get(audio_output_registry); + utility::join_event_group(this, global_audio_actor); + + message_handler_ext_ = { + [=](utility::event_atom, + module::change_attribute_event_atom, + const float volume, + const bool muted, + const bool repitch, + const bool scrubbing) {}, + [=](utility::event_atom, + playhead::sound_audio_atom, + const std::vector &audio_buffers, + const utility::Uuid &sub_playhead) {}, + [=](utility::event_atom, + playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) {}, + [=](utility::event_atom, + playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) {}, + [=](utility::event_atom, + audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + latest_audio_buffers_[playhead_uuid] = audio_buffers; + }}; + + make_behavior(); + // we need to keep track of which playhead is driving which viewport + listen_to_playhead_events(); +} + +AudioWaveformOverlay::~AudioWaveformOverlay() = default; + +void AudioWaveformOverlay::attribute_changed( + const utility::Uuid &attribute_uuid, const int /*role*/ +) { + + redraw_viewport(); +} + +utility::BlindDataObjectPtr AudioWaveformOverlay::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const { + + auto r = utility::BlindDataObjectPtr(); + if (!visible()) + return r; + + auto p = latest_audio_buffers_.find(playhead_uuid); + if (p == latest_audio_buffers_.end()) + return r; + const auto &latest_audio_buffers = p->second; + + // check our sample buffers to get sample rate & num channels + int nc = 0; + uint64_t sample_rate = 0; + for (const auto &aud_buf : latest_audio_buffers) { + + if (aud_buf && !sample_rate && !nc) { + nc = aud_buf->num_channels(); + sample_rate = aud_buf->sample_rate(); + } + } + + if (!sample_rate) + return r; + + float millisecs = horizontal_scale_->value(); + int samps_needed = int(round(millisecs * float(sample_rate) / 1000.0f)); + + std::vector verts(samps_needed * (separate_channels_->value() ? nc : 1)); + + // this gives us the ref timestamp for the start of the window of samples that + // we will draw to the screen + timebase::flicks tt = + image.timeline_timestamp() - + timebase::to_flicks((millisecs / 1000.0 - image.frame_id().rate().to_seconds()) * 0.5); + + for (const auto &aud_buf : latest_audio_buffers) { + + if (aud_buf) { + // reference timeline timestamp for first sample + timebase::flicks when_samples_play = + aud_buf.timeline_timestamp() + std::chrono::duration_cast( + aud_buf->time_delta_to_video_frame()); + const int nsamp = aud_buf->num_samples(); + + if (separate_channels_->value()) { + + // offset *into* the samples that we're generating + for (int c = 0; c < nc; ++c) { + int offset = timebase::to_seconds(when_samples_play - tt) * sample_rate; + int n = 0; + if (offset < 0) { + n = -offset; + offset = 0; + } + int16_t *samp_data = (int16_t *)aud_buf->buffer(); + samp_data += n * nc + c; + while (offset < samps_needed && n < nsamp) { + + verts[offset + samps_needed * c] = float(*samp_data) * 0.000030518f; + n++; + offset++; + samp_data += nc; + } + } + + } else { + + int offset = timebase::to_seconds(when_samples_play - tt) * sample_rate; + int n = 0; + if (offset < 0) { + n = -offset; + offset = 0; + } + int16_t *samp_data = (int16_t *)aud_buf->buffer(); + samp_data += n * nc; + while (offset < samps_needed && n < nsamp) { + + float f = 0.0f; + int c = nc; + while (c--) { + f += *(samp_data++); + } + verts[offset] = f * 0.000030518f; + n++; + offset++; + } + } + } + } + + r.reset(new WaveFormData( + verts, + separate_channels_->value() ? nc : 1, + vertical_scale_->value(), + chan_position_spacing_->value(), + vertical_position_->value(), + line_colour_->value())); + + return r; +} + +/*void AudioWaveformOverlay::viewport_playhead_changed(const std::string &viewport_name, +caf::actor playhead) { if (playhead) { request(playhead, infinite, utility::uuid_atom_v).then( + [=](utility::Uuid &playhead_uuid) { + }, + [=](caf::error &err) {}); + } +}*/ + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + utility::Uuid("873c508b-276b-44e3-82d0-15db2f039aa7"), + "AudioWaveformOverlay", + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + true, + "Ted Waine", + "Audio Waveform Overlay")})); +} +} diff --git a/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp new file mode 100644 index 000000000..39b733866 --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/opengl/opengl_text_rendering.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + class WaveFormData : public utility::BlindDataObject { + public: + WaveFormData( + const std::vector &v, + const int _num_chans, + const float _vscale, + const float _chan_spacing, + const float _v_pos, + const utility::ColourTriplet _line_colour) + : verts_(std::move(v)), + num_chans(_num_chans), + vscale(_vscale), + chan_spacing(_chan_spacing), + v_pos(_v_pos), + line_colour(_line_colour) {} + ~WaveFormData() = default; + + const std::vector verts_; + const int num_chans; + const float vscale; + const float chan_spacing; + const float v_pos; + const utility::ColourTriplet line_colour; + }; + + class AudioWaveformOverlayRenderer : public plugin::ViewportOverlayRenderer { + + public: + void render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) override; + + ~AudioWaveformOverlayRenderer(); + + void init_overlay_opengl(); + + std::unique_ptr shader_; + GLuint vbo_ = {0}; + GLuint vao_ = {0}; + }; + + class AudioWaveformOverlay : public plugin::HUDPluginBase { + + public: + AudioWaveformOverlay( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~AudioWaveformOverlay(); + + protected: + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const override; + + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override { + return plugin::ViewportOverlayRendererPtr(new AudioWaveformOverlayRenderer()); + } + + caf::message_handler message_handler_extensions() override { + return message_handler_ext_.or_else( + plugin::HUDPluginBase::message_handler_extensions()); + } + + void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; + + private: + module::FloatAttribute *vertical_scale_; + module::FloatAttribute *horizontal_scale_; + module::FloatAttribute *chan_position_spacing_; + module::FloatAttribute *vertical_position_; + module::BooleanAttribute *separate_channels_; + module::ColourAttribute *line_colour_; + + utility::Uuid mask_hotkey_; + caf::message_handler message_handler_ext_; + + std::unordered_map> + latest_audio_buffers_; + }; + + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_overlay/audio_waveform/test/CMakeLists.txt b/src/plugin/viewport_overlay/audio_waveform/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp index a3ba95b32..29d2b4ad0 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 @@ -88,7 +88,7 @@ const char *frag_shader = R"( )"; } // namespace -void BasicMaskRenderer::render_opengl( +void BasicMaskRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -108,11 +108,11 @@ void BasicMaskRenderer::render_opengl( } 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["viewport_du_dx"] = viewport_du_dpixel; - shader_params["image_transform_matrix"] = frame.layout_transform(); - shader_params["image_aspect"] = frame ? frame->image_aspect() : 16.0f/9.0f; + shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["viewport_du_dx"] = viewport_du_dpixel; + shader_params["image_transform_matrix"] = frame.layout_transform(); + shader_params["image_aspect"] = frame ? frame->image_aspect() : 16.0f / 9.0f; shader_->set_shader_parameters(shader_params); shader_->use(); @@ -296,7 +296,9 @@ void BasicViewportMasking::register_hotkeys() { } utility::BlindDataObjectPtr BasicViewportMasking::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string & /*viewport_name*/) const { + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const { auto r = utility::BlindDataObjectPtr(); if (visible() && mask_render_method_->value() == "OpenGL" && image) { 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 2a9a58cfe..00739f272 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 @@ -24,7 +24,7 @@ namespace ui { class BasicMaskRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -63,9 +63,12 @@ namespace ui { void register_hotkeys() override; utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, const std::string &/*viewport_name*/) const override; + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid) const override; - plugin::ViewportOverlayRendererPtr make_overlay_renderer() override { + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override { return plugin::ViewportOverlayRendererPtr(new BasicMaskRenderer()); } 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 269591c0f..32e3371c3 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 @@ -107,7 +107,7 @@ Item { Repeater { - model: view.imageBoundariesInViewport + model: imageBoxes Item { // Viewport class provides imageBoxes - the coordinates of each diff --git a/src/plugin_manager/src/hud_plugin.cpp b/src/plugin_manager/src/hud_plugin.cpp index 81a294af4..05814c073 100644 --- a/src/plugin_manager/src/hud_plugin.cpp +++ b/src/plugin_manager/src/hud_plugin.cpp @@ -18,7 +18,7 @@ HUDPluginBase::HUDPluginBase( const float toolbar_position) : plugin::StandardPlugin(cfg, name, init_settings) { - hud_data_ = add_boolean_attribute(name, name, true); + hud_data_ = add_boolean_attribute(name, name, false); hud_data_->expose_in_ui_attrs_group("hud_element_toggles"); // add a preference path using the plugin name so that the status of the diff --git a/src/plugin_manager/src/plugin_base.cpp b/src/plugin_manager/src/plugin_base.cpp index 0ff993035..7379b5afb 100644 --- a/src/plugin_manager/src/plugin_base.cpp +++ b/src/plugin_manager/src/plugin_base.cpp @@ -34,25 +34,24 @@ StandardPlugin::StandardPlugin( [=](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); + const std::string &viewport_name, + const utility::Uuid &playhead_id) -> utility::BlindDataObjectPtr { + return onscreen_render_data(image, viewport_name, playhead_id); }, [=](playhead::show_atom, const media_reader::ImageBufDisplaySetPtr &image_set, const std::string &viewport_name, - const bool playing) { - __images_going_on_screen(image_set, viewport_name, playing); - images_going_on_screen(image_set, viewport_name, playing); - }, - - [=](ui::viewport::overlay_render_function_atom) -> ViewportOverlayRendererPtr { - return make_overlay_renderer(); + const bool playing) { + __images_going_on_screen(image_set, viewport_name, playing); + images_going_on_screen(image_set, viewport_name, playing); }, - [=](ui::viewport::pre_render_gpu_hook_atom) -> GPUPreDrawHookPtr { - return make_pre_draw_gpu_hook(); - }, + [=](ui::viewport::overlay_render_function_atom, const std::string &viewport_name) + -> ViewportOverlayRendererPtr { return make_overlay_renderer(viewport_name); }, + + [=](ui::viewport::pre_render_gpu_hook_atom, const std::string &viewport_name) + -> GPUPreDrawHookPtr { return make_pre_draw_gpu_hook(viewport_name); }, [=](bookmark::build_annotation_atom, const utility::JsonStore &data) -> result { @@ -85,8 +84,7 @@ StandardPlugin::StandardPlugin( const int playhead_logical_frame, const int media_frame, const int media_logical_frame, - const utility::Timecode &timecode) { - }, + const utility::Timecode &timecode) {}, [=](utility::event_atom, bookmark::get_bookmarks_atom, @@ -97,12 +95,7 @@ StandardPlugin::StandardPlugin( const std::string &viewport_name, caf::actor playhead) { // the playhead of the given viewport has changed - }, - [=](utility::event_atom, - ui::viewport::viewport_playhead_atom, - const std::string &viewport_name, - caf::actor playhead) { - // the playhead of the given viewport has changed + viewport_playhead_changed(viewport_name, playhead); }, [=](utility::event_atom, @@ -128,9 +121,7 @@ StandardPlugin::StandardPlugin( }}; } -void StandardPlugin::on_exit() { - playhead_events_actor_ = caf::actor(); -} +void StandardPlugin::on_exit() { playhead_events_actor_ = caf::actor(); } void StandardPlugin::on_screen_media_changed(caf::actor media) { @@ -217,24 +208,25 @@ void StandardPlugin::join_studio_events() { } void StandardPlugin::__images_going_on_screen( - const media_reader::ImageBufDisplaySetPtr & image_set, + const media_reader::ImageBufDisplaySetPtr &image_set, const std::string viewport_name, - const bool playhead_playing -) { + const bool playhead_playing) { // skip viewports whose name doesn't start with 'viewport' - this lets us // ignore offscreen and quickview viewports - if (viewport_name.find("viewport") != 0) return; + if (viewport_name.find("viewport") != 0) + return; - if (image_set && image_set->hero_image().frame_id().source_uuid() != last_source_uuid_[viewport_name]) { + if (image_set && + image_set->hero_image().frame_id().source_uuid() != last_source_uuid_[viewport_name]) { last_source_uuid_[viewport_name] = image_set->hero_image().frame_id().source_uuid(); - auto media_source = caf::actor_cast(image_set->hero_image().frame_id().actor_addr()); - request(media_source, infinite, utility::parent_atom_v).then( - [=](caf::actor media_actor) { - on_screen_media_changed(media_actor); - }, - [=](caf::error &err) {}); + auto media_source = + caf::actor_cast(image_set->hero_image().frame_id().actor_addr()); + request(media_source, infinite, utility::parent_atom_v) + .then( + [=](caf::actor media_actor) { on_screen_media_changed(media_actor); }, + [=](caf::error &err) {}); } } @@ -249,6 +241,11 @@ void StandardPlugin::listen_to_playhead_events(const bool listen) { joined_playhead_events_ = listen; if (listen) { + if (!playhead_events_actor_) { + playhead_events_actor_ = + system().registry().template get(global_playhead_events_actor); + } + anon_send( playhead_events_actor_, broadcast::join_broadcast_atom_v, @@ -262,6 +259,35 @@ void StandardPlugin::listen_to_playhead_events(const bool listen) { [=](caf::error &err) { }); + + // get all the existing viewports.. + request(playhead_events_actor_, infinite, ui::viewport::viewport_atom_v) + .then( + [=](std::vector viewports) { + for (auto &vp : viewports) { + // get the viewport name + request(vp, infinite, utility::name_atom_v) + .then( + [=](const std::string &viewport_name) { + // get the playhead attached to the viewport + request( + vp, + infinite, + ui::viewport::viewport_playhead_atom_v, + viewport_name) + .then( + [=](caf::actor playhead) { + // call our notification method + viewport_playhead_changed( + viewport_name, playhead); + }, + [=](caf::error &err) {}); + }, + [=](caf::error &err) {}); + } + }, + [=](caf::error &err) {}); + } else { anon_send( @@ -338,7 +364,6 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor viewed_playhead) [=](error &err) mutable { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - } } @@ -365,7 +390,8 @@ utility::Uuid StandardPlugin::create_bookmark_on_frame( try { auto media_source = caf::actor_cast(frame_details.actor_addr()); - auto media = utility::request_receive(*sys, media_source, utility::parent_atom_v); + auto media = + utility::request_receive(*sys, media_source, utility::parent_atom_v); if (media) { @@ -392,7 +418,8 @@ utility::Uuid StandardPlugin::create_bookmark_on_frame( } else { // this will make a bookmark of single frame duration on the current frame - bmd.start_ = (frame_details.frame()-frame_details.first_frame())*media_ref.rate().to_flicks(); + bmd.start_ = (frame_details.frame() - frame_details.first_frame()) * + media_ref.rate().to_flicks(); bmd.duration_ = timebase::flicks(0); } diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index 468f821d8..af8eefbd0 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -55,7 +55,8 @@ size_t PluginManager::load_plugins() { #ifdef __linux__ - if (entry.path().extension() != ".so") continue; + if (entry.path().extension() != ".so") + continue; // only want .so // clear any errors.. dlerror(); @@ -76,12 +77,13 @@ size_t PluginManager::load_plugins() { } #elif defined(_WIN32) - if (entry.path().extension() != ".dll") continue; + if (entry.path().extension() != ".dll") + continue; // open .dll std::string dllPath = entry.path().string(); - HMODULE hndl = LoadLibraryA(dllPath.c_str()); + HMODULE hndl = LoadLibraryA(dllPath.c_str()); if (hndl == nullptr) { DWORD errorCode = GetLastError(); LPSTR buffer = nullptr; diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index 7f48092af..256ea5e6f 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -335,20 +335,22 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base // component link-dependent on the ui::viewport component we // spawn via the viewport_layouts_manager std::vector details = manager_.plugin_detail(); - for (auto &detail: details) { + for (auto &detail : details) { if (detail.name_ == "DefaultViewportLayout") { try { - auto j = json; - j["name"] = name; + auto j = json; + j["name"] = name; j["is_python"] = true; - rp.deliver(manager_.spawn(*scoped_actor(system()), detail.uuid_, j)); + rp.deliver( + manager_.spawn(*scoped_actor(system()), detail.uuid_, j)); } catch (std::exception &e) { rp.deliver(make_error(xstudio_error::error, e.what())); } } } if (rp.pending()) { - rp.deliver(make_error(xstudio_error::error, "Failed to spawn base ViewportLayoutPlugin")); + rp.deliver(make_error( + xstudio_error::error, "Failed to spawn base ViewportLayoutPlugin")); } return rp; } diff --git a/src/python_module/src/py_atoms.cpp b/src/python_module/src/py_atoms.cpp index d343e68e3..02404c269 100644 --- a/src/python_module/src/py_atoms.cpp +++ b/src/python_module/src/py_atoms.cpp @@ -32,7 +32,6 @@ using namespace xstudio::playhead; using namespace xstudio::playlist; using namespace xstudio::plugin_manager; using namespace xstudio::session; -using namespace xstudio::sync; using namespace xstudio::thumbnail; using namespace xstudio::timeline; using namespace xstudio::ui; @@ -56,9 +55,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::broadcast, join_broadcast_atom); ADD_ATOM(xstudio::broadcast, leave_broadcast_atom); ADD_ATOM(xstudio::broadcast, broadcast_down_atom); - ADD_ATOM(xstudio::sync, authorise_connection_atom); - ADD_ATOM(xstudio::sync, get_sync_atom); - ADD_ATOM(xstudio::sync, request_connection_atom); ADD_ATOM(xstudio::media_hook, get_media_hook_atom); ADD_ATOM(xstudio::media_hook, gather_media_sources_atom); ADD_ATOM(xstudio::media_metadata, get_metadata_atom); @@ -86,6 +82,8 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::timeline, split_item_atom); ADD_ATOM(xstudio::timeline, split_item_at_frame_atom); ADD_ATOM(xstudio::timeline, trimmed_range_atom); + ADD_ATOM(xstudio::timeline, item_selection_atom); + ADD_ATOM(xstudio::timeline, item_type_atom); ADD_ATOM(xstudio::thumbnail, cache_path_atom); ADD_ATOM(xstudio::thumbnail, cache_stats_atom); @@ -202,7 +200,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::global, api_exit_atom); ADD_ATOM(xstudio::global, busy_atom); ADD_ATOM(xstudio::global, create_studio_atom); - ADD_ATOM(xstudio::global, get_api_mode_atom); ADD_ATOM(xstudio::global, get_application_mode_atom); ADD_ATOM(xstudio::global, get_global_audio_cache_atom); ADD_ATOM(xstudio::global, get_global_image_cache_atom); @@ -216,6 +213,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::global, remote_session_name_atom); ADD_ATOM(xstudio::global, status_atom); ADD_ATOM(xstudio::global, get_actor_from_registry_atom); + ADD_ATOM(xstudio::global, authenticate_atom); ADD_ATOM(xstudio::media, acquire_media_detail_atom); ADD_ATOM(xstudio::media, add_media_source_atom); @@ -257,6 +255,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::utility, type_atom); ADD_ATOM(xstudio::utility, uuid_atom); ADD_ATOM(xstudio::utility, version_atom); + ADD_ATOM(xstudio::utility, notification_atom); ADD_ATOM(xstudio::json_store, get_json_atom); ADD_ATOM(xstudio::json_store, jsonstore_change_atom); ADD_ATOM(xstudio::json_store, patch_atom); @@ -371,7 +370,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::ui::viewport, quickview_media_atom); ADD_ATOM(xstudio::ui::viewport, viewport_atom); ADD_ATOM(xstudio::ui::viewport, hud_settings_atom); - ADD_ATOM(xstudio::ui::viewport, viewport_layout_atom); + ADD_ATOM(xstudio::ui::viewport, viewport_layout_atom); ADD_ATOM(xstudio::ui, show_message_box_atom); diff --git a/src/python_module/src/py_config.hpp b/src/python_module/src/py_config.hpp index 4e369072a..3c986eee9 100644 --- a/src/python_module/src/py_config.hpp +++ b/src/python_module/src/py_config.hpp @@ -220,23 +220,23 @@ class py_config : public caf::actor_system_config { class int_py_binding : public py_binding { public: - using py_binding::py_binding; - void append(message_builder &xs, py::handle x) const override { - // Awkward! PyBind chucks an error if you try to cast a python int - // (which is actually a long or long long) to a C int if the python - // int value > INT_MAX. - long foo = 12412; - int64_t a = PyLong_AsLong(x.ptr()); - int b = int(a); - if (a != int64_t(b)) { - xs.append(a); - } else { - xs.append(b); - } - } + using py_binding::py_binding; + void append(message_builder &xs, py::handle x) const override { + // Awkward! PyBind chucks an error if you try to cast a python int + // (which is actually a long or long long) to a C int if the python + // int value > INT_MAX. + long foo = 12412; + int64_t a = PyLong_AsLong(x.ptr()); + int b = int(a); + if (a != int64_t(b)) { + xs.append(a); + } else { + xs.append(b); + } + } }; - void add_int_py() { + void add_int_py() { auto ptr = new int_py_binding("int"); py_bindings_.emplace("int", py_binding_ptr{ptr}); bindings_.emplace(std::move("int"), ptr); diff --git a/src/python_module/src/py_context.cpp b/src/python_module/src/py_context.cpp index 5b0bada9d..60cc2dbd6 100644 --- a/src/python_module/src/py_context.cpp +++ b/src/python_module/src/py_context.cpp @@ -420,6 +420,21 @@ void py_context::py_remove_message_callback(const py::args &xs) { } } +void py_context::py_register_python_plugin_instance(const py::args &xs) { + + if (xs.size() == 2) { + + auto i = xs.begin(); + auto plugin_object_instace = (*i).cast(); + i++; + auto plugin_instace_uuid = (*i).cast(); + plugin_registry_[plugin_instace_uuid] = plugin_object_instace; + + } else { + throw std::runtime_error("register_python_plugin_instance expecting tuple of size 2 " + "(plugin_object, plugin_uuid)."); + } +} void py_context::disconnect() { host_ = ""; diff --git a/src/python_module/src/py_context.hpp b/src/python_module/src/py_context.hpp index 6e44927f6..5138c6311 100644 --- a/src/python_module/src/py_context.hpp +++ b/src/python_module/src/py_context.hpp @@ -48,6 +48,7 @@ class py_context : public py_config { void py_run_xstudio_message_loop(); void py_add_message_callback(const py::args &xs); void py_remove_message_callback(const py::args &xs); + void py_register_python_plugin_instance(const py::args &xs); actor py_self() { return self_; } actor py_remote() { return remote_; } @@ -94,5 +95,6 @@ class py_context : public py_config { std::map> message_handler_callbacks_; std::map message_conversion_function_; std::map delayed_callbacks_; + std::map plugin_registry_; }; } // namespace caf::python diff --git a/src/python_module/src/py_link.cpp b/src/python_module/src/py_link.cpp index 742bd84c0..208316785 100644 --- a/src/python_module/src/py_link.cpp +++ b/src/python_module/src/py_link.cpp @@ -59,6 +59,11 @@ void py_link(py::module_ &m) { &caf::python::py_context::py_add_message_callback, "Add a python callback function, called every time the given Actor's event group " "generates a message. ") + .def( + "register_python_plugin_instance", + &caf::python::py_context::py_register_python_plugin_instance, + "Registers a plugin instance so that callbacks can be made to methods on the " + "instance.") .def( "remove_message_callback", &caf::python::py_context::py_remove_message_callback, diff --git a/src/python_module/src/py_messages.cpp b/src/python_module/src/py_messages.cpp index ab28fe320..8b0b83257 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.cpp @@ -22,6 +22,7 @@ #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/notification_handler.hpp" #include "py_config.hpp" @@ -43,12 +44,14 @@ extern void register_FrameRate_class(py::module &m, const std::string &name); extern void register_group_down_msg(py::module &m, const std::string &name); extern void register_URI_class(py::module &m, const std::string &name); extern void register_FrameRateDuration_class(py::module &m, const std::string &name); +extern void register_colour_triplet_class(py::module &m, const std::string &name); extern void register_uuid_actor_class(py::module &m, const std::string &name); extern void register_uuid_actor_vector_class(py::module &m, const std::string &name); extern void register_uuidvec_class(py::module &m, const std::string &name); extern void register_item_class(py::module &m, const std::string &name); extern void register_frame_range_class(py::module &m, const std::string &name); extern void register_frame_list_class(py::module &m, const std::string &name); +extern void register_notification_class(py::module &m, const std::string &name); using namespace xstudio; @@ -141,6 +144,8 @@ void py_config::add_messages() { "FrameRateDuration", "xstudio::utility::FrameRateDuration", ®ister_FrameRateDuration_class); + add_message_type( + "ColourTriplet", "xstudio::utility::ColourTriplet", ®ister_colour_triplet_class); add_message_type( "JsonStore", "xstudio::utility::JsonStore", ®ister_jsonstore_class); add_message_type( @@ -175,6 +180,9 @@ void py_config::add_messages() { add_message_type( "Item", "xstudio::timeline::Item", ®ister_item_class); + add_message_type( + "ItemType", "xstudio::timeline::ItemType", nullptr); + add_message_type( "FrameRange", "xstudio::utility::FrameRange", ®ister_frame_range_class); @@ -182,5 +190,13 @@ void py_config::add_messages() { "std::pair", "std::pair", nullptr); + + add_message_type( + "Notification", "xstudio::utility::Notification", ®ister_notification_class); + + add_message_type>( + "std::vector", + "std::vector", + nullptr); } } // namespace caf::python \ 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 3ce9c84b0..f81c304ee 100644 --- a/src/python_module/src/py_register.cpp +++ b/src/python_module/src/py_register.cpp @@ -31,6 +31,7 @@ CAF_POP_WARNINGS #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace py = pybind11; @@ -80,6 +81,93 @@ void register_uuid_class(py::module &m, const std::string &name) { .def("__str__", str_impl); } +void register_notification_class(py::module &m, const std::string &name) { + py::class_(m, name.c_str()) + .def(py::init<>()) + .def("type", [](const utility::Notification &x) { return x.type(); }) + .def("uuid", [](const utility::Notification &x) { return x.uuid(); }) + .def("text", [](const utility::Notification &x) { return x.text(); }) + .def("progress", [](const utility::Notification &x) { return x.progress(); }) + .def( + "progress_maximum", + [](const utility::Notification &x) { return x.progress_maximum(); }) + .def( + "progress_minimum", + [](const utility::Notification &x) { return x.progress_minimum(); }) + .def( + "progress_percentage", + [](const utility::Notification &x) { return x.progress_percentage(); }) + .def( + "progress_text_percentage", + [](const utility::Notification &x) { return x.progress_text_percentage(); }) + .def( + "progress_text_range", + [](const utility::Notification &x) { return x.progress_text_range(); }) + + .def( + "set_type", + [](utility::Notification &x, const utility::NotificationType value) { + x.type(value); + }) + .def( + "set_uuid", + [](utility::Notification &x, const utility::Uuid &value) { x.uuid(value); }) + .def( + "set_text", + [](utility::Notification &x, const std::string &value) { x.text(value); }) + .def( + "set_progress", + [](utility::Notification &x, const float value) { x.progress(value); }) + .def( + "set_progress_minimum", + [](utility::Notification &x, const float value) { x.progress_maximum(value); }) + .def( + "set_progress_maximum", + [](utility::Notification &x, const float value) { x.progress_maximum(value); }) + .def( + "set_expires_in", + [](utility::Notification &x, const int seconds) { + x.expires_in(std::chrono::seconds(seconds)); + }) + + + .def( + "InfoNotification", + [](const std::string &text, const int seconds) { + return utility::Notification::InfoNotification( + text, std::chrono::seconds(seconds)); + }) + .def( + "WarnNotification", + [](const std::string &text, const int seconds) { + return utility::Notification::WarnNotification( + text, std::chrono::seconds(seconds)); + }) + .def( + "ProcessingNotification", + [](const std::string &text) { + return utility::Notification::ProcessingNotification(text); + }) + .def( + "ProgressPercentageNotification", + [](const std::string &text, const float progress = 0.0f, const int seconds = 600) { + return utility::Notification::ProgressPercentageNotification( + text, progress, std::chrono::seconds(seconds)); + }) + .def( + "ProgressRangeNotification", + [](const std::string &text, + const float progress = 0.0f, + const float progress_min = 0.0f, + const float progress_max = 100.0f, + const int seconds = 600) { + return utility::Notification::ProgressRangeNotification( + text, progress, progress_min, progress_max, std::chrono::seconds(seconds)); + }) + + ; +} + void register_plugindetail_class(py::module &m, const std::string &name) { py::class_(m, name.c_str()) .def(py::init<>()) @@ -248,9 +336,7 @@ void register_mediakey_class(py::module &m, const std::string &name) { } void register_jsonstore_class(py::module &m, const std::string &name) { - auto str_impl = [](const utility::JsonStore &x) -> std::string { - return x.dump(); - }; + auto str_impl = [](const utility::JsonStore &x) -> std::string { return x.dump(); }; auto get_preferences_impl = [](const utility::JsonStore &x, const std::set &context = std::set()) { @@ -332,6 +418,19 @@ void register_FrameRateDuration_class(py::module &m, const std::string &name) { .def("__str__", str_impl); } +void register_colour_triplet_class(py::module &m, const std::string &name) { + auto str_impl = [](const utility::ColourTriplet &x) { return to_string(x); }; + py::class_(m, name.c_str()) + .def(py::init<>()) + .def(py::init()) + .def_property("red", &utility::ColourTriplet::red, &utility::ColourTriplet::setRed) + .def_property( + "green", &utility::ColourTriplet::green, &utility::ColourTriplet::setGreen) + .def_property("blue", &utility::ColourTriplet::blue, &utility::ColourTriplet::setBlue) + .def("__str__", str_impl) + .def("__repr__", str_impl); +} + void register_actor_class(py::module &m, const std::string &name) { auto str_impl = [](const caf::actor &x) { return to_string(x); }; py::class_(m, name.c_str()) diff --git a/src/python_module/src/py_remote_session_file.cpp b/src/python_module/src/py_remote_session_file.cpp index 3b8c05b22..21bf608b0 100644 --- a/src/python_module/src/py_remote_session_file.cpp +++ b/src/python_module/src/py_remote_session_file.cpp @@ -31,13 +31,11 @@ void py_remote_session_file(py::module_ &m) { .def("filepath", &utility::RemoteSessionFile::filepath) .def("host", &utility::RemoteSessionFile::host) .def("port", &utility::RemoteSessionFile::port) - .def("pid", &utility::RemoteSessionFile::pid) - .def("sync", &utility::RemoteSessionFile::sync); + .def("pid", &utility::RemoteSessionFile::pid); py::class_(m, "RemoteSessionManager") .def(py::init()) .def("first_api", &utility::RemoteSessionManager::first_api) - .def("first_sync", &utility::RemoteSessionManager::first_sync) .def("find", &utility::RemoteSessionManager::find) .def("__len__", &utility::RemoteSessionManager::size) .def("empty", &utility::RemoteSessionManager::empty); diff --git a/src/python_module/src/py_utility.cpp b/src/python_module/src/py_utility.cpp index d5f80870a..094d0a6cc 100644 --- a/src/python_module/src/py_utility.cpp +++ b/src/python_module/src/py_utility.cpp @@ -27,4 +27,13 @@ void py_utility(py::module_ &m) { .value("TSM_REMAPPED", utility::TimeSourceMode::REMAPPED) .value("TSM_DYNAMIC", utility::TimeSourceMode::DYNAMIC) .export_values(); -} \ No newline at end of file + + py::enum_(m, "NotificationType") + .value("NT_UNKNOWN", utility::NotificationType::NT_UNKNOWN) + .value("NT_INFO", utility::NotificationType::NT_INFO) + .value("NT_WARN", utility::NotificationType::NT_WARN) + .value("NT_PROCESSING", utility::NotificationType::NT_PROCESSING) + .value("NT_PROGRESS_RANGE", utility::NotificationType::NT_PROGRESS_RANGE) + .value("NT_PROGRESS_PERCENTAGE", utility::NotificationType::NT_PROGRESS_PERCENTAGE) + .export_values(); +} diff --git a/src/session/src/session_actor.cpp b/src/session/src/session_actor.cpp index 98ee11207..6ea621a9e 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -30,6 +30,107 @@ namespace fs = std::filesystem; namespace { +class SessionIOActor : public caf::event_based_actor { + public: + SessionIOActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) {} + const char *name() const override { return NAME.c_str(); } + + caf::message_handler message_handler() { + return caf::message_handler{ + [=](save_atom, + const JsonStore &js, + const caf::uri &path, + const bool update_path, + const size_t hash) -> caf::result { + size_t new_hash = 0; + + try { + auto data = js.dump(2); + + auto resolve_link = false; + new_hash = std::hash{}(data); + + // no change in hash, so skip save (autosave) + if (new_hash == hash) { + return new_hash; + } + + // fix something ? + auto ppath = utility::posix_path_to_uri(utility::uri_to_posix_path(path)); + + // try and save, we are already looking at this file + if (update_path) { + // same path as session, are we allowed ? + resolve_link = true; + } + + auto save_path = uri_to_posix_path(ppath); + if (resolve_link && fs::exists(save_path) && fs::is_symlink(save_path)) +#ifdef _WIN32 + save_path = fs::canonical(save_path).string(); +#else + save_path = fs::canonical(save_path); +#endif + + + // compress data. + if (to_lower(path_to_string(fs::path(save_path).extension())) == ".xsz") { + zstr::ofstream o(save_path + ".tmp"); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + // if(not o.is_open()) + // throw std::runtime_error(); + o << std::setw(4) << data << std::endl; + o.close(); + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(save_path + ".tmp"); + } + throw std::runtime_error("Failed to open file"); + } + } else { + // this maybe a symlink in which case we should resolve it. + std::ofstream o(save_path + ".tmp"); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + // if(not o.is_open()) + // throw std::runtime_error(); + o << std::setw(4) << data << std::endl; + o.close(); + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(save_path + ".tmp"); + } + throw std::runtime_error("Failed to open file"); + } + } + + // rename tmp to final name + fs::rename(save_path + ".tmp", save_path); + + const std::string t = utility::to_string(utility::sysclock::now()); + spdlog::info("Session saved as {} at {}", save_path, t); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + return make_error(xstudio_error::error, err.what()); + } + + return new_hash; + }}; + } + + caf::behavior make_behavior() override { return message_handler(); } + + private: + inline static const std::string NAME = "SessionIOActor"; +}; + + // offload media actor copy as it blocks the session actor.. class MediaCopyActor : public caf::event_based_actor { @@ -508,6 +609,9 @@ void SessionActor::init() { print_on_create(this, base_); print_on_exit(this, base_); + ioactor_ = spawn(); + link_to(ioactor_); + // monitor serilise targets. set_down_handler([=](down_msg &msg) { // find in playhead list.. @@ -541,6 +645,7 @@ void SessionActor::init() { behavior_.assign(message_handler() .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())) .or_else(bookmark::BookmarksActor::default_event_handler()) .or_else(playlist::PlaylistActor::default_event_handler())); @@ -571,9 +676,17 @@ caf::message_handler SessionActor::message_handler() { return rp; }, + [=](name_atom, const std::string &name_template, const bool) -> std::string { + return get_next_name(name_template); + }, + [=](add_playlist_atom atom, const Uuid &uuid_before) { delegate( - actor_cast(this), atom, "Untitled Playlist", uuid_before, false); + actor_cast(this), + atom, + get_next_name("Playlist {}"), + uuid_before, + false); }, [=](add_playlist_atom atom, const std::string name) { @@ -613,6 +726,12 @@ caf::message_handler SessionActor::message_handler() { delegate(caf::actor_cast(this), atom, media, base_.media_rate()); }, + [=](timeline::item_selection_atom) -> UuidActorVector { return selection_; }, + + [=](timeline::item_selection_atom, const UuidActorVector &selection) { + selection_ = selection; + }, + [=](get_playlist_atom) -> result { // gets the first playlist if (!playlists_.size()) { @@ -779,7 +898,23 @@ caf::message_handler SessionActor::message_handler() { utility::serialise_atom_v) .then( [=](const utility::JsonStore &js) mutable { - save_json_to(rp, js, path, update_path, hash); + request( + ioactor_, infinite, save_atom_v, js, path, update_path, hash) + .then( + [=](size_t r) mutable { + rp.deliver(r); + if (update_path) { + base_.set_filepath(path); + send( + base_.event_group(), + utility::event_atom_v, + path_atom_v, + std::make_pair( + base_.filepath(), + base_.session_file_mtime())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { @@ -790,7 +925,8 @@ caf::message_handler SessionActor::message_handler() { containers) .then( [=](const utility::JsonStore &js) mutable { - save_json_to(rp, js, path, false, hash); + rp.delegate(ioactor_, save_atom_v, js, path, false, hash); + // save_json_to(rp, js, path, false, hash); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } @@ -1417,7 +1553,6 @@ caf::message_handler SessionActor::message_handler() { session::session_atom_v) .then( [=](caf::actor session) { - // If we are not THE active session. Don't try and // switch // the global playhead ... @@ -1903,34 +2038,8 @@ void SessionActor::create_playlist( const utility::Uuid &uuid_before, const bool into) { - if (name.empty()) { - - // No name supplied ... we want to create a new playlist called - // 'Playlist 1' or, if 'Playlist 1' already exists 'Playlist 2' etc. - std::function recursive_name_search; - - recursive_name_search = [&recursive_name_search]( - const PlaylistTree &tree, const std::string &name) -> Uuid { - if (tree.name() == name) { - return tree.value_uuid(); - } - for (auto i : tree.children_ref()) { - const Uuid uuid = recursive_name_search(i, name); - if (uuid != Uuid()) - return uuid; - } - return Uuid(); - }; - - int n = 1; - while (1) { - name = fmt::format("Playlist {}", n); - Uuid match = recursive_name_search(base_.containers(), name); - if (!playlists_.count(match)) - break; - n++; - } - } + if (name.empty()) + name = get_next_name("Playlist {}"); auto actor = spawn( name, utility::Uuid(), caf::actor_cast(this)); @@ -2228,101 +2337,52 @@ void SessionActor::move_containers_to( } } -void SessionActor::save_json_to( - caf::typed_response_promise &rp, - const utility::JsonStore &js, - const caf::uri &path, - const bool update_path, - const size_t hash) { - size_t new_hash = 0; +std::string SessionActor::get_next_name(const std::string &name_template) const { + auto result = name_template; - try { - auto data = js.dump(2); + auto merged_tree = base_.containers(); - auto resolve_link = false; - new_hash = std::hash{}(data); + caf::scoped_actor sys(system()); - // no change in hash, so skip save (autosave) - if (new_hash == hash) { - return rp.deliver(new_hash); - } + // pity we don't sync children.. like timelines.. + for (const auto &p : playlists()) { + auto pt = request_receive(*sys, p, playlist::get_container_atom_v); + merged_tree.insert(pt); + } - // fix something ? - auto ppath = utility::posix_path_to_uri(utility::uri_to_posix_path(path)); + // No name supplied ... we want to create a new playlist called + // 'Playlist 1' or, if 'Playlist 1' already exists 'Playlist 2' etc. + std::function recursive_name_search; - // try and save, we are already looking at this file - if (update_path) { - // same path as session, are we allowed ? - resolve_link = true; + recursive_name_search = + [&recursive_name_search](const PlaylistTree &tree, const std::string &name) -> Uuid { + if (tree.name() == name) { + return tree.value_uuid(); } - - auto save_path = uri_to_posix_path(ppath); - if (resolve_link && fs::exists(save_path) && fs::is_symlink(save_path)) -#ifdef _WIN32 - save_path = fs::canonical(save_path).string(); -#else - save_path = fs::canonical(save_path); -#endif - - - // compress data. - if (to_lower(path_to_string(fs::path(save_path).extension())) == ".xsz") { - zstr::ofstream o(save_path + ".tmp"); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - // if(not o.is_open()) - // throw std::runtime_error(); - o << std::setw(4) << data << std::endl; - o.close(); - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(save_path + ".tmp"); - } - throw std::runtime_error("Failed to open file"); - } - } else { - // this maybe a symlink in which case we should resolve it. - std::ofstream o(save_path + ".tmp"); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - // if(not o.is_open()) - // throw std::runtime_error(); - o << std::setw(4) << data << std::endl; - o.close(); - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(save_path + ".tmp"); - } - throw std::runtime_error("Failed to open file"); - } + // also search for other children of session.. + for (auto i : tree.children_ref()) { + auto uuid = recursive_name_search(i, name); + if (uuid) + return uuid; } + return Uuid(); + }; - // rename tmp to final name - fs::rename(save_path + ".tmp", save_path); + int n = 1; - const std::string t = utility::to_string(utility::sysclock::now()); - spdlog::info("Session saved as {} at {}", save_path, t); + while (true) { + result = fmt::format(fmt::runtime(name_template), n); + if (result == name_template) + break; - if (update_path) { - base_.set_filepath(path); - send( - base_.event_group(), - utility::event_atom_v, - path_atom_v, - std::make_pair(base_.filepath(), base_.session_file_mtime())); - } + if (not recursive_name_search(merged_tree, result)) + break; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return rp.deliver(make_error(xstudio_error::error, err.what())); + n++; } - rp.deliver(new_hash); + return result; } void SessionActor::associate_bookmarks(caf::typed_response_promise &rp) { diff --git a/src/subset/src/subset_actor.cpp b/src/subset/src/subset_actor.cpp index daf24ebfb..a037ff96f 100644 --- a/src/subset/src/subset_actor.cpp +++ b/src/subset/src/subset_actor.cpp @@ -25,11 +25,32 @@ SubsetActor::SubsetActor( anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + + if (jsn.contains("selection_actor")) { + try { + + selection_actor_ = system().spawn( + static_cast(jsn["selection_actor"]), + caf::actor_cast(this)); + link_to(selection_actor_); + + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } + // need to scan playlist to relink our media.. init(); } -SubsetActor::SubsetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name, const std::string &override_type) +SubsetActor::SubsetActor( + caf::actor_config &cfg, + caf::actor playlist, + const std::string &name, + const std::string &override_type) : caf::event_based_actor(cfg), playlist_(caf::actor_cast(playlist)), base_(name, override_type) { @@ -433,6 +454,7 @@ caf::message_handler SubsetActor::message_handler() { auto uuid = utility::Uuid::generate(); auto actor = spawn( std::string("Subset Playhead"), + playhead::GLOBAL_AUDIO, selection_actor_, uuid, caf::actor_cast(this)); @@ -440,7 +462,14 @@ caf::message_handler SubsetActor::message_handler() { anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + playhead_ = UuidActor(uuid, actor); + + if (!playhead_serialisation_.is_null()) { + anon_send( + playhead_.actor(), module::deserialise_atom_v, playhead_serialisation_); + } + return playhead_; }, [=](playlist::get_playhead_atom) { @@ -701,10 +730,40 @@ caf::message_handler SubsetActor::message_handler() { delegate(caf::actor_cast(playlist_), session::session_atom_v); }, - [=](utility::serialise_atom) -> JsonStore { - JsonStore jsn; - jsn["base"] = base_.serialise(); - return jsn; + [=](utility::serialise_atom) -> result { + auto rp = make_response_promise(); + JsonStore j; + j["base"] = SubsetActor::serialise(); + request(selection_actor_, infinite, utility::serialise_atom_v) + .then( + [=](const utility::JsonStore &selection_state) mutable { + j["selection_actor"] = selection_state; + if (playhead_) { + request(playhead_.actor(), infinite, utility::serialise_atom_v) + .then( + [=](const utility::JsonStore &playhead_state) mutable { + playhead_serialisation_ = playhead_state; + j["playhead"] = playhead_state; + rp.deliver(j); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(j); + }); + + } else { + if (!playhead_serialisation_.is_null()) { + j["playhead"] = playhead_serialisation_; + } + rp.deliver(j); + } + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(j); + }); + return rp; }}; } @@ -715,9 +774,11 @@ void SubsetActor::init() { change_event_group_ = spawn(this); link_to(change_event_group_); - selection_actor_ = spawn( - "SubsetPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); + if (!selection_actor_) { + selection_actor_ = spawn( + "SubsetPlayheadSelectionActor", caf::actor_cast(this)); + link_to(selection_actor_); + } set_down_handler([=](down_msg &msg) { // find in playhead list.. diff --git a/src/sync/src/CMakeLists.txt b/src/sync/src/CMakeLists.txt deleted file mode 100644 index 26dea4251..000000000 --- a/src/sync/src/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -SET(LINK_DEPS - xstudio::utility - xstudio::global_store - xstudio::media - caf::core - caf::io -) - -create_component(sync 0.1.0 "${LINK_DEPS}") diff --git a/src/sync/src/sync_actor.cpp b/src/sync/src/sync_actor.cpp deleted file mode 100644 index dd244a4cd..000000000 --- a/src/sync/src/sync_actor.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; - -SyncActor::SyncActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { init(); } - -void SyncActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncActor"); - print_on_exit(this, "SyncActor"); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](global::get_api_mode_atom) -> std::string { return "SYNC"; }, - - [=](global::get_application_mode_atom _req) { - delegate(system().registry().template get(global_registry), _req); - }); -} - -void SyncActor::on_exit() {} diff --git a/src/sync/src/sync_gateway_actor.cpp b/src/sync/src/sync_gateway_actor.cpp deleted file mode 100644 index da43d5ba4..000000000 --- a/src/sync/src/sync_gateway_actor.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; -using namespace xstudio::global_store; - -SyncGatewayActor::SyncGatewayActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - init(); -} - -void SyncGatewayActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncGatewayActor"); - print_on_exit(this, "SyncGatewayActor"); - - system().registry().put(sync_gateway_registry, this); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](authorise_connection_atom _req, const Uuid &lock, const std::string &key) { - delegate( - system().registry().template get(sync_gateway_manager_registry), - _req, - lock, - key); - }, - - [=](global::get_api_mode_atom) -> std::string { return "GATEWAY"; }, - - [=](request_connection_atom _req) { - delegate( - system().registry().template get(sync_gateway_manager_registry), - _req); - }); -} - -void SyncGatewayActor::on_exit() { system().registry().erase(sync_gateway_registry); } diff --git a/src/sync/src/sync_gateway_manager_actor.cpp b/src/sync/src/sync_gateway_manager_actor.cpp deleted file mode 100644 index 37ddbee11..000000000 --- a/src/sync/src/sync_gateway_manager_actor.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include - -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; -using namespace xstudio::global_store; -using namespace std::chrono_literals; - -SyncGatewayManagerActor::SyncGatewayManagerActor(caf::actor_config &cfg) - : caf::event_based_actor(cfg) { - init(); -} - -void SyncGatewayManagerActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncGatewayManagerActor"); - print_on_exit(this, "SyncGatewayManagerActor"); - - system().registry().put(sync_gateway_manager_registry, this); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(clients_); it != std::end(clients_); ++it) { - if (msg.source == it->second) { - spdlog::debug("Remove sync actor {}", to_string(it->first)); - demonitor(it->second); - clients_.erase(it); - break; - } - } - }); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](authorise_connection_atom, - const Uuid &lock, - const std::string &key) -> caf::result { - if (not lock_key_.count(lock)) - return make_error( - xstudio_error::error, fmt::format("Invalid lock {}", to_string(lock))); - if (clients_.count(lock)) - return make_error( - xstudio_error::error, fmt::format("Already connected {}", to_string(lock))); - - if (lock_key_[lock] != key) { - lock_key_.erase(lock); - return make_error( - xstudio_error::error, - fmt::format("Invalid key {} {}, lock invalidated.", to_string(lock), key)); - } - lock_key_.erase(lock); - auto act = spawn(); - monitor(act); - clients_.insert({lock, act}); - return act; - }, - - [=](get_sync_atom) -> caf::actor { - Uuid lock = Uuid::generate(); - auto act = spawn(); - monitor(act); - clients_.insert({lock, act}); - return act; - }, - - [=](request_connection_atom) -> Uuid { - Uuid lock = Uuid::generate(); - lock_key_.insert({lock, std::string("key")}); - // stop client hammering.. - std::this_thread::sleep_for(1s); - return lock; - }); -} - -void SyncGatewayManagerActor::on_exit() { - system().registry().erase(sync_gateway_manager_registry); -} diff --git a/src/sync/test/sync_actor_test.cpp b/src/sync/test/sync_actor_test.cpp deleted file mode 100644 index 357a5619a..000000000 --- a/src/sync/test/sync_actor_test.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; - -using namespace caf; - -#include "xstudio/utility/serialise_headers.hpp" - - -ACTOR_TEST_SETUP() - - -TEST(SyncActorTest, Test) { fixture f; } diff --git a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp index 4a0c5ef36..ed9b7a052 100644 --- a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp +++ b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp @@ -370,7 +370,7 @@ TDCHelperActor::TDCHelperActor(caf::actor_config &cfg) : caf::event_based_actor( } } return encode_save_thumb(thumbnail_path(path, thumb).string(), buffer); - + } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } diff --git a/src/timeline/src/clip_actor.cpp b/src/timeline/src/clip_actor.cpp index 333cbdee9..b5ef30980 100644 --- a/src/timeline/src/clip_actor.cpp +++ b/src/timeline/src/clip_actor.cpp @@ -18,32 +18,6 @@ using namespace xstudio::utility; using namespace xstudio::timeline; using namespace caf; -namespace { - class DebugTimer { - public: - DebugTimer(std::string p, int frame) : path_(std::move(p)), frame_(frame) { - t1_ = utility::clock::now(); - } - - ~DebugTimer() { - std::cerr << "Reading" << path_ << " @ " << frame_ << " done in " - << double(std::chrono::duration_cast( - utility::clock::now() - t1_) - .count()) / - 1000000.0 - << " seconds\n"; - } - void gronk() { - std::cerr << "BAT\n"; - } - private: - utility::time_point t1_; - const std::string path_; - const int frame_; - }; - - -} ClipActor::ClipActor(caf::actor_config &cfg, const JsonStore &jsn) : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { @@ -123,7 +97,8 @@ ClipActor::ClipActor( init(); } -void ClipActor::link_media(caf::typed_response_promise rp, const UuidActor &media) { +void ClipActor::link_media( + caf::typed_response_promise rp, const UuidActor &media, const bool refresh) { // spdlog::warn("link_media_atom {}", to_string(media)); auto old_media_actor = caf::actor_cast(media_); @@ -132,9 +107,12 @@ void ClipActor::link_media(caf::typed_response_promise rp, const UuidActor leave_event_group(this, old_media_actor); } - monitor(media.actor()); - join_event_group(this, media.actor()); - media_ = caf::actor_cast(media.actor()); + if(media.actor()) { + monitor(media.actor()); + join_event_group(this, media.actor()); + media_ = caf::actor_cast(media.actor()); + } else + media_ = caf::actor_addr(); auto jsn = base_.set_media_uuid(media.uuid()); if (not jsn.is_null()) @@ -144,20 +122,28 @@ void ClipActor::link_media(caf::typed_response_promise rp, const UuidActor image_ptr_cache_.clear(); audio_ptr_cache_.clear(); - // force update ? - // cache available range ? - delayed_send( - caf::actor_cast(this), - std::chrono::milliseconds(100), - media::acquire_media_detail_atom_v); - - // replace clip name ? - request(media.actor(), infinite, name_atom_v) - .then( - [=](const std::string &value) mutable { anon_send(this, item_name_atom_v, value); }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + if (refresh) { + // force update ? + // cache available range ? + if(media.actor()) { + delayed_send( + caf::actor_cast(this), + std::chrono::milliseconds(100), + media::acquire_media_detail_atom_v); + + // replace clip name ? + request(media.actor(), infinite, name_atom_v) + .then( + [=](const std::string &value) mutable { + anon_send(this, item_name_atom_v, value); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else { + anon_send(this, item_name_atom_v, "NO CLIP"); + } + } rp.deliver(true); } @@ -207,7 +193,8 @@ caf::message_handler ClipActor::message_handler() { link_media( rp, UuidActor( - swap.at(base_.media_uuid()), media.at(swap.at(base_.media_uuid())))); + swap.at(base_.media_uuid()), media.at(swap.at(base_.media_uuid()))), + false); } else rp.deliver(false); @@ -232,6 +219,8 @@ caf::message_handler ClipActor::message_handler() { return jsn; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + [=](item_lock_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_locked(value); if (not jsn.is_null()) @@ -573,6 +562,13 @@ caf::message_handler ClipActor::message_handler() { }, [=](utility::event_atom, utility::change_atom) { + // something has changed. It means we might need to re-generate our + // frame pointers for playback. For example, if media source metadata + // has changed that affects colour management, we need to re-gernerate + // the frame pointers that carry the colour management data to the + // playhead and up to the viewport. + image_ptr_cache_.clear(); + audio_ptr_cache_.clear(); send(base_.event_group(), utility::event_atom_v, utility::change_atom_v); }, @@ -635,7 +631,6 @@ caf::message_handler ClipActor::message_handler() { const media::MediaType media_type, const std::vector &timepoints, const FrameRate &override_rate) -> result { - if (media_) { @@ -645,7 +640,8 @@ caf::message_handler ClipActor::message_handler() { // the 'result' here is a vector of std::shared_ptr - // one element per frame timepoint. We initialise it with blank frames and // the logic below will fill in actual frames where possible - media::AVFrameID blank = *(media::make_blank_frame(media_type, base_.media_uuid(), utility::Uuid(), base_.uuid())); + media::AVFrameID blank = *(media::make_blank_frame( + media_type, base_.media_uuid(), utility::Uuid(), base_.uuid())); auto blank_ptr = std::make_shared(blank); // the result data @@ -744,8 +740,10 @@ caf::message_handler ClipActor::message_handler() { auto mp = mps.begin(); int ct = 0; for (size_t i = 0; i < indexs.size(); i++) { + auto ind = indexs[i]; auto rng = ranges[i]; + for (auto ii = rng.first; ii <= rng.second; ii++, ind++) { // if we got our logic above correct this shouldn't @@ -754,21 +752,22 @@ caf::message_handler ClipActor::message_handler() { if (mp == mps.end()) break; + std::shared_ptr foo = *mp; + if (media_type == media::MediaType::MT_IMAGE) { - image_ptr_cache_[ii] = *mp; - (*result)[ind] = *mp;//image_ptr_cache_[ii]; + image_ptr_cache_[ii] = foo; + (*result)[ind] = foo; // image_ptr_cache_[ii]; // spdlog::warn("{}", (*mp)->key_); } else if (media_type == media::MediaType::MT_AUDIO) { - audio_ptr_cache_[ii] = *mp; - (*result)[ind] = audio_ptr_cache_[ii]; + audio_ptr_cache_[ii] = foo; + (*result)[ind] = foo; } mp++; } } // spdlog::warn("deliver {}", result->size()); rp.deliver(*result); - }, [=](error &err) mutable { // can get an error for missing media - instead @@ -875,14 +874,9 @@ caf::message_handler ClipActor::message_handler() { 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 track // playable. - return base_.item().get_all_frame_IDs( - media_type, - tsm, - override_rate); - + return base_.item().get_all_frame_IDs(media_type, tsm, override_rate); }}; } @@ -893,8 +887,11 @@ void ClipActor::init() { // we die if our media dies. set_down_handler([=](down_msg &msg) { if (msg.source == media_) { - demonitor(caf::actor_cast(media_)); - send_exit(this, caf::exit_reason::user_shutdown); + // just unset media. + // anon_send(this, link_media_atom_v, UuidActor()); + + // demonitor(caf::actor_cast(media_)); + // send_exit(this, caf::exit_reason::user_shutdown); } }); } diff --git a/src/timeline/src/gap_actor.cpp b/src/timeline/src/gap_actor.cpp index 306e2adb8..5ea237217 100644 --- a/src/timeline/src/gap_actor.cpp +++ b/src/timeline/src/gap_actor.cpp @@ -76,6 +76,8 @@ caf::message_handler GapActor::message_handler() { return jsn; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) diff --git a/src/timeline/src/item.cpp b/src/timeline/src/item.cpp index bac083dc4..c308dbe61 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -394,7 +394,8 @@ bool Item::valid_child(const Item &child) const { return valid; } -utility::UuidActorVector Item::find_all_uuid_actors(const ItemType item_type, const bool only_enabled_items) const { +utility::UuidActorVector +Item::find_all_uuid_actors(const ItemType item_type, const bool only_enabled_items) const { utility::UuidActorVector items; if (item_type_ == item_type && (!only_enabled_items || enabled_)) @@ -451,7 +452,7 @@ Item::find_all_items(const ItemType item_type, const ItemType track_type) { const timebase::flicks frame_inteval, const media::MediaType mt, const utility::UuidSet &focus, - const bool must_have_focus) const + const bool must_have_focus) const { if (transparent()) return; @@ -470,13 +471,13 @@ Item::find_all_items(const ItemType item_type, const ItemType track_type) { const auto td = it.trimmed_duration(); while (time < td && time <= print_range_out) { // print clip/gap frame here - result[idx++] = std::pair(it, time + it.trimmed_start()); - time += frame_inteval; + result[idx++] = std::pair(it, time + +it.trimmed_start()); time += frame_inteval; } - + } - + }*/ std::optional Item::resolve_time( @@ -1590,52 +1591,54 @@ void Item::reset_media_uuid() { } namespace xstudio::timeline { -/* Doing sync requests to the clip actors to build our FrameTimeMap can -get a bit ugly. To help with this we have this helper that processes the +/* Doing sync requests to the clip actors to build our FrameTimeMap can +get a bit ugly. To help with this we have this helper that processes the responses from the clip actors and self destroys once all the expected responses come in*/ class BuildFrameIDsHelper { - public: - + public: BuildFrameIDsHelper( caf::typed_response_promise rp, const int rcount, - Item * parent, - const media::MediaType media_type - ) : rp_(rp), media_type_(media_type) { + Item *parent, + const media::MediaType media_type) + : rp_(rp), media_type_(media_type) { - base_rate_ = parent->rate(); + base_rate_ = parent->rate(); parent_actor_ = caf::actor_cast(parent->actor()); - count_ = size_t(rcount); - blank_frame = media::make_blank_frame(media_type); - result = new media::FrameTimeMap; + count_ = size_t(rcount); + blank_frame = media::make_blank_frame(media_type); + result = new media::FrameTimeMap; } void add_blank_frame(timebase::flicks timeline_tp); void add_frame(caf::actor, timebase::flicks, const FrameRate &clip_tp); - void incref() { - refcount_++; - } + void incref() { refcount_++; } void decref() { refcount_--; - if (!refcount_) delete this; + if (!refcount_) { + if (rp_.pending()) { + rp_.deliver(make_error( + xstudio_error::error, "Timeline Item failed to complete frame IDs build.")); + } + delete this; + } } void request_clip_frames(); void decrement_count(const size_t n = 1); - private: - + private: // store for the result - media::FrameTimeMap * result; + media::FrameTimeMap *result; size_t count_; int refcount_ = {0}; FrameRate base_rate_; - caf::event_based_actor * parent_actor_ = nullptr; + caf::event_based_actor *parent_actor_ = nullptr; caf::actor current_clip_actor_; std::vector timeline_timepoints_; std::vector clip_timepoints_; @@ -1643,19 +1646,16 @@ class BuildFrameIDsHelper { caf::typed_response_promise rp_; // blank frame std::shared_ptr blank_frame; - }; -} +} // namespace xstudio::timeline - caf::typed_response_promise Item::get_all_frame_IDs( - const media::MediaType media_type, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate, - const utility::UuidSet &focus_list - ) -{ +caf::typed_response_promise Item::get_all_frame_IDs( + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate, + const utility::UuidSet &focus_list) { auto foo = caf::actor_cast(actor()); - auto rp = foo->make_response_promise(); + auto rp = foo->make_response_promise(); if (!available_range()) { rp.deliver(media::FrameTimeMapPtr()); @@ -1663,18 +1663,16 @@ class BuildFrameIDsHelper { } // First, get our frame range - const int start_frame = - available_range()->frame_start().frames(override_rate); + const int start_frame = available_range()->frame_start().frames(override_rate); const int end_frame = - start_frame + - available_range()->frame_duration().frames(override_rate); - const int num_frames = end_frame-start_frame; + start_frame + available_range()->frame_duration().frames(override_rate); + const int num_frames = end_frame - start_frame; - BuildFrameIDsHelper * helper = new BuildFrameIDsHelper(rp, num_frames, this, media_type); + BuildFrameIDsHelper *helper = new BuildFrameIDsHelper(rp, num_frames, this, media_type); helper->incref(); const timebase::flicks delta = override_rate.to_flicks(); - timebase::flicks timepoint = start_frame * delta; + timebase::flicks timepoint = start_frame * delta; // TODO: this needs optimisation - for multi-track, long timlines we see // this taking 100s of milliseconds. The problem is the recursive call into @@ -1685,39 +1683,39 @@ class BuildFrameIDsHelper { // just do a simple loop to print itself into the result. for (auto i = start_frame; i < end_frame; i++) { - std::optional clip_item = resolve_time( - FrameRate(timepoint), - media_type, - focus_list); + std::optional clip_item = + resolve_time(FrameRate(timepoint), media_type, focus_list); - if (clip_item) helper->add_frame(clip_item->first.actor(), timepoint, clip_item->second); - else helper->add_blank_frame(timepoint); + if (clip_item) + helper->add_frame(clip_item->first.actor(), timepoint, clip_item->second); + else + helper->add_blank_frame(timepoint); timepoint += delta; - } helper->request_clip_frames(); // in case start_frame = end_frame, i.e. nothing to process this // call will ensure we deliver ont the RP - helper->decrement_count(0); + helper->decrement_count(0); helper->decref(); - + return rp; } -void BuildFrameIDsHelper::add_blank_frame(timebase::flicks timeline_tp) -{ +void BuildFrameIDsHelper::add_blank_frame(timebase::flicks timeline_tp) { if (current_clip_actor_) { request_clip_frames(); + current_clip_actor_ = caf::actor(); + timeline_timepoints_.clear(); + clip_timepoints_.clear(); } (*result)[timeline_tp] = blank_frame; - current_clip_actor_ = caf::actor(); decrement_count(); } -void BuildFrameIDsHelper::add_frame(caf::actor clip_actor, timebase::flicks timeline_tp, const FrameRate &clip_tp) -{ +void BuildFrameIDsHelper::add_frame( + caf::actor clip_actor, timebase::flicks timeline_tp, const FrameRate &clip_tp) { if (clip_actor != current_clip_actor_) { @@ -1726,35 +1724,40 @@ void BuildFrameIDsHelper::add_frame(caf::actor clip_actor, timebase::flicks time current_clip_actor_ = clip_actor; timeline_timepoints_.clear(); clip_timepoints_.clear(); - } timeline_timepoints_.emplace_back(timeline_tp); clip_timepoints_.emplace_back(clip_tp); - } -void BuildFrameIDsHelper::request_clip_frames() -{ +void BuildFrameIDsHelper::request_clip_frames() { + + if (!current_clip_actor_) { + incref(); + for (const auto &tp : timeline_timepoints_) { + (*result)[tp] = blank_frame; + } + decrement_count(timeline_timepoints_.size()); + decref(); + return; + } - if (!current_clip_actor_) return; - const auto timeline_timepoints_cpy = timeline_timepoints_; incref(); - parent_actor_->request( - current_clip_actor_, - infinite, - media::get_media_pointer_atom_v, - media_type_, - clip_timepoints_, - base_rate_) + parent_actor_ + ->request( + current_clip_actor_, + infinite, + media::get_media_pointer_atom_v, + media_type_, + clip_timepoints_, + base_rate_) .then( [=](const media::AVFrameIDs &mps) mutable { - int idx = 0; - for (const auto &mp: mps) { + for (const auto &mp : mps) { (*result)[timeline_timepoints_cpy[idx++]] = mp; } decrement_count(timeline_timepoints_cpy.size()); @@ -1762,23 +1765,21 @@ void BuildFrameIDsHelper::request_clip_frames() }, [=](error &err) mutable { - for (const auto &tp: timeline_timepoints_cpy) { + for (const auto &tp : timeline_timepoints_cpy) { (*result)[tp] = blank_frame; } decrement_count(timeline_timepoints_cpy.size()); decref(); }); - } void BuildFrameIDsHelper::decrement_count(const size_t n) { - if (n >= count_) { - - rp_.deliver(media::FrameTimeMapPtr(result)); + if (n >= count_ && rp_.pending()) { + media::FrameTimeMapPtr r(result); + rp_.deliver(r); - } - else { + } else { count_ -= n; } } \ No newline at end of file diff --git a/src/timeline/src/stack_actor.cpp b/src/timeline/src/stack_actor.cpp index 0d13a9ff9..8728f96a5 100644 --- a/src/timeline/src/stack_actor.cpp +++ b/src/timeline/src/stack_actor.cpp @@ -273,6 +273,8 @@ caf::message_handler StackActor::message_handler() { return jsn; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + [=](utility::rate_atom) -> FrameRate { return base_.item().rate(); }, [=](utility::rate_atom atom, const media::MediaType media_type) { diff --git a/src/timeline/src/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index 9a9cd225a..54086de72 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -55,10 +55,11 @@ const static auto MEDIA_COLOUR_JPOINTER = nlohmann::json::json_pointer("/xstudio caf::actor TimelineActor::deserialise(const utility::JsonStore &value, const bool replace_item) { - auto key = utility::Uuid(value.at("base").at("item").at("uuid")); + auto actor = caf::actor(); if (value.at("base").at("container").at("type") == "Stack") { + auto key = utility::Uuid(value.at("base").at("item").at("uuid")); auto item = Item(); actor = spawn(static_cast(value), item); add_item(UuidActor(key, actor)); @@ -75,6 +76,17 @@ TimelineActor::deserialise(const utility::JsonStore &value, const bool replace_i value.dump(2)); } } + } else if (value.at("base").at("container").at("type") == "PlayheadSelection") { + + try { + + selection_actor_ = system().spawn( + static_cast(value), caf::actor_cast(this)); + link_to(selection_actor_); + + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } } return actor; @@ -329,6 +341,7 @@ void process_item( const std::vector> &items, blocking_actor *self, caf::actor &parent, + const caf::uri &path, // otio path const std::map &media_lookup, const FrameRate &timeline_rate) { @@ -427,34 +440,7 @@ void process_item( UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - process_item(ii->children(), self, actor, media_lookup, timeline_rate); - // } else if (ii->kind() == otio::Track::Kind::audio) { - // // spdlog::warn("Audio Track"); - // auto uuid = Uuid::generate(); - // auto actor = - // self->spawn(ii->name(), media::MediaType::MT_AUDIO, uuid); - // auto source_range = ii->source_range(); - - // if (source_range) - // self->request( - // actor, - // infinite, - // active_range_atom_v, - // FrameRange(FrameRateDuration( - // static_cast(source_range->duration().value()), - // source_range->duration().rate()))) - // .receive([=](const JsonStore &) {}, [=](const error &err) {}); - - // 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); - // } + process_item(ii->children(), self, actor, path, media_lookup, timeline_rate); } else if (auto ii = dynamic_cast(&(*i))) { auto uuid = Uuid::generate(); @@ -545,6 +531,15 @@ void process_item( active->name_suffix(); } + // active_path maybe relative.. + if (not active_path.empty() and not caf::make_uri(active_path)) { + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + active_path = "file://" + tmp.substr(0, pos + 1) + active_path; + } + if (active_path.empty() or not media_lookup.count(active_path)) { // missing media.. actor = self->spawn(UuidActor(), ii->name(), uuid); @@ -718,7 +713,7 @@ void process_item( UuidActorVector({UuidActor(uuid, actor)})) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - process_item(ii->children(), self, parent, media_lookup, timeline_rate); + process_item(ii->children(), self, parent, path, media_lookup, timeline_rate); } } } @@ -734,10 +729,22 @@ void timeline_importer( otio::ErrorStatus error_status; otio::SerializableObject::Retainer timeline; + // notify processing.. + auto notify_uuid = Uuid(); + { + auto notify = Notification::ProgressRangeNotification("Loading timeline"); + anon_send(dst.actor(), notification_atom_v, notify); + notify_uuid = notify.uuid(); + } + timeline = otio::SerializableObject::Retainer( dynamic_cast(otio::Timeline::from_json_string(data, &error_status))); if (otio::is_error(error_status)) { + auto failnotify = Notification::WarnNotification("Failed Loading timeline"); + failnotify.uuid(notify_uuid); + anon_send(dst.actor(), notification_atom_v, failnotify); + return rp.deliver(false); } @@ -764,6 +771,14 @@ void timeline_importer( } } + { + auto notify = + Notification::ProgressRangeNotification("Loading timeline", 0, 0, clips.size()); + notify.uuid(notify_uuid); + anon_send(dst.actor(), notification_atom_v, notify); + } + + self->request( dst.actor(), infinite, @@ -815,7 +830,11 @@ void timeline_importer( }, [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + float count = 0.0; for (const auto &cl : clips) { + count++; + anon_send(dst.actor(), notification_atom_v, notify_uuid, count); + const auto &name = cl->name(); // spdlog::warn("{} {}", name, cl->active_media_reference_key()); @@ -834,6 +853,15 @@ void timeline_importer( active->name_suffix(); } + // active_path maybe relative.. + if (not caf::make_uri(active_path)) { + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + active_path = "file://" + tmp.substr(0, pos + 1) + active_path; + } + // WARNING this may inadvertantly skip auxiliary sources we want.. if (active_path.empty() or target_url_map.count(active_path)) { // spdlog::warn("SKIP {}", active_path); @@ -866,8 +894,13 @@ void timeline_importer( auto uri = caf::make_uri(ext->target_url()); if (!uri) { - uri = posix_path_to_uri(ext->target_url()); + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + uri = posix_path_to_uri(tmp.substr(0, pos + 1) + ext->target_url()); } + if (uri) { auto extname = ext->name(); auto source_uuid = utility::Uuid::generate(); @@ -1054,7 +1087,7 @@ void timeline_importer( if (not markers.empty()) anon_send(stack_actor, item_marker_atom_v, insert_item_atom_v, markers); - process_item(tracks, self, stack_actor, target_url_map, timeline_rate); + process_item(tracks, self, stack_actor, path, target_url_map, timeline_rate); } // enable history, we've finished. @@ -1064,6 +1097,10 @@ void timeline_importer( // spdlog::warn("imported"); + auto successnotify = Notification::InfoNotification("Timeline Loaded"); + successnotify.uuid(notify_uuid); + anon_send(dst.actor(), notification_atom_v, successnotify); + rp.deliver(true); } @@ -1078,6 +1115,10 @@ TimelineActor::TimelineActor( if (playlist) anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + for (const auto &[key, value] : jsn["actors"].items()) { try { deserialise(value, true); @@ -1143,12 +1184,16 @@ TimelineActor::TimelineActor( } caf::message_handler TimelineActor::default_event_handler() { - return { - [=](utility::event_atom, item_atom, const Item &) {}, - [=](utility::event_atom, item_atom, const JsonStore &, const bool) {}, - }; + return caf::message_handler( + { + [=](utility::event_atom, item_atom, const Item &) {}, + [=](utility::event_atom, item_atom, const JsonStore &, const bool) {}, + }) + .or_else(NotificationHandler::default_event_handler()) + .or_else(Container::default_event_handler()); } + caf::message_handler TimelineActor::message_handler() { return caf::message_handler{ [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -1311,22 +1356,20 @@ caf::message_handler TimelineActor::message_handler() { send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - // Ted - TODO - this WIP stuff allows comparing of video tracks within // a timeline. auto video_tracks = base_.item().find_all_uuid_actors(IT_VIDEO_TRACK, true); if (video_tracks != video_tracks_) { video_tracks_ = video_tracks; if (playhead_) { - // now set this (and its video tracks) as the source for - // the timeple playhead + // now set this (and its video tracks) as the source for + // the timeple playhead anon_send( playhead_.actor(), playhead::source_atom_v, utility::UuidActor(base_.uuid(), caf::actor_cast(this)), - video_tracks_ - ); + video_tracks_); } } }, @@ -1343,15 +1386,6 @@ caf::message_handler TimelineActor::message_handler() { } }, - // handle child change events. - // [=](event_atom, item_atom, const Item &item) { - // // it's possibly one of ours.. so try and substitue the record - // if(base_.item().replace_child(item)) { - // base_.item().refresh(); - // send(this, utility::event_atom_v, change_atom_v); - // } - // }, - // check events processes [=](item_atom, event_atom, const std::set &events) -> bool { auto result = true; @@ -1579,6 +1613,19 @@ caf::message_handler TimelineActor::message_handler() { return rp; }, + [=](bake_atom, + const FrameRate &time, + const FrameRate &duration) -> result>> { + auto result = std::vector>(); + + for (auto i = time; i <= time + duration; i += base_.item().rate()) { + result.emplace_back(base_.item().resolve_time( + i, media::MediaType::MT_IMAGE, base_.focus_list())); + } + + return result; + }, + [=](bake_atom, const FrameRate &time) -> result { auto ri = base_.item().resolve_time(time, media::MediaType::MT_IMAGE, base_.focus_list()); @@ -1588,6 +1635,12 @@ caf::message_handler TimelineActor::message_handler() { return make_error(xstudio_error::error, "No clip resolved"); }, + [=](item_selection_atom) -> UuidActorVector { return selection_; }, + + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + + [=](item_selection_atom, const UuidActorVector &selection) { selection_ = selection; }, + [=](media_frame_to_timeline_frames_atom, const utility::Uuid &media_uuid, const int mediaFrame, @@ -1754,14 +1807,6 @@ caf::message_handler TimelineActor::message_handler() { return result(make_error(xstudio_error::error, "No media")); }, - // [=](json_store::get_json_atom atom, const std::string &path) { - // delegate(json_store_, atom, path); - // }, - - // [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { - // delegate(json_store_, atom, json, path); - // }, - [=](playlist::get_media_uuid_atom) -> UuidVector { return base_.media_vector(); }, [=](playlist::add_media_atom atom, @@ -1879,16 +1924,6 @@ caf::message_handler TimelineActor::message_handler() { uuid, actor, before_uuid); - // add_media(actor, uuid, before_uuid); - // send(base_.event_group(), utility::event_atom_v, change_atom_v); - // send(change_base_.event_group(), utility::event_atom_v, - // utility::change_atom_v); send( - // base_.event_group(), - // utility::event_atom_v, - // playlist::add_media_atom_v, - // UuidActorVector({UuidActor(uuid, actor)})); - // base_.send_changed(event_group_, this); - // rp.deliver(true); }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -2120,7 +2155,19 @@ caf::message_handler TimelineActor::message_handler() { const caf::uri &path, const std::string &type) -> result { auto rp = make_response_promise(); - export_otio(rp, path, type); + + request(caf::actor_cast(this), infinite, session::export_atom_v) + .then( + [=](const std::string &result) { export_otio(rp, result, path, type); }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; + }, + + [=](session::export_atom) -> result { + // convert timeline to otio string + auto rp = make_response_promise(); + export_otio_as_string(rp); return rp; }, @@ -2130,9 +2177,6 @@ caf::message_handler TimelineActor::message_handler() { auto uuid = utility::Uuid::generate(); - /*auto actor = spawn( - std::string("Timeline Playhead"), selection_actor_, uuid);*/ - // N.B. for now we're not using the 'selection_actor_' as this // drives the playhead a list of selected media which the playhead // will play. It will ignore this timeline completely if we do that. @@ -2140,6 +2184,7 @@ caf::message_handler TimelineActor::message_handler() { // that is selected. auto playhead_actor = spawn( std::string("Timeline Playhead"), + playhead::GLOBAL_AUDIO, selection_actor_, uuid, caf::actor_cast(this)); @@ -2148,20 +2193,19 @@ caf::message_handler TimelineActor::message_handler() { anon_send(playhead_actor, playhead::playhead_rate_atom_v, base_.rate()); - // this lets the playhead talk to the selection actor whilst not being - // driven by the selection actor - anon_send(playhead_actor, playlist::selection_actor_atom_v, selection_actor_); - // now make this timeline and its vide tracks the source for the playhead video_tracks_ = base_.item().find_all_uuid_actors(IT_VIDEO_TRACK, true); anon_send( playhead_actor, playhead::source_atom_v, utility::UuidActor(base_.uuid(), caf::actor_cast(this)), - video_tracks_ - ); + video_tracks_); playhead_ = UuidActor(uuid, playhead_actor); + if (!playhead_serialisation_.is_null()) { + anon_send( + playhead_.actor(), module::deserialise_atom_v, playhead_serialisation_); + } return playhead_; }, @@ -2332,86 +2376,12 @@ caf::message_handler TimelineActor::message_handler() { return true; }, - // [=](media::get_media_pointer_atom, - // const int logical_frame) -> result { - // if (base_.empty()) - // return result(make_error(xstudio_error::error, "No - // media")); - - // auto rp = make_response_promise(); - // if (update_edit_list_) { - // request(actor_cast(this), infinite, media::get_edit_list_atom_v) - // .then( - // [=](const utility::EditList &) mutable { - // deliver_media_pointer(logical_frame, rp); - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - // } else { - // deliver_media_pointer(logical_frame, rp); - // } - - // return rp; - // }, - - // [=](start_time_atom) -> utility::FrameRateDuration { return base_.start_time(); }, - - // [=](utility::clear_atom) -> bool { - // base_.clear(); - // for (const auto &i : actors_) { - // // this->leave(i.second); - // unlink_from(i.second); - // send_exit(i.second, caf::exit_reason::user_shutdown); - // } - // actors_.clear(); - // return true; - // }, - - // [=](utility::event_atom, utility::change_atom) { - // update_edit_list_ = true; - // send(event_group_, utility::event_atom_v, utility::change_atom_v); - // }, - - // [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, - [=](utility::rate_atom) -> FrameRate { return base_.item().rate(); }, [=](utility::rate_atom atom, const media::MediaType media_type) { delegate(caf::actor_cast(this), atom); }, - // [=](utility::rate_atom, const FrameRate &rate) { base_.set_rate(rate); }, - - // this operation isn't relevant ? - - - // [=](playlist::create_playhead_atom) -> UuidActor { - // if (playhead_) - // return playhead_; - // auto uuid = utility::Uuid::generate(); - // auto actor = spawn( - // std::string("Timeline Playhead"), caf::actor_cast(this), uuid); - // link_to(actor); - // playhead_ = UuidActor(uuid, actor); - - // anon_send(actor, playhead::playhead_rate_atom_v, base_.rate()); - - // // this pushes this actor to the playhead as the 'source' that the - // // playhead will play - // send( - // actor, - // utility::event_atom_v, - // playhead::source_atom_v, - // std::vector{caf::actor_cast(this)}); - - // return playhead_; - // }, - - // emulate subset. - - // [=](playlist::selection_actor_atom) -> caf::actor { - // return caf::actor_cast(this); - // }, - [=](duplicate_atom) -> result { auto rp = make_response_promise(); @@ -2574,86 +2544,54 @@ caf::message_handler TimelineActor::message_handler() { [=](duration_atom, const int) {}, - /* - // FOR TED - // iterate over direct children of stack item, and only return indexs of - audio tracks that are enabled. auto audio_indexes = - find_indexes(base_.item().front().children(), ItemType::IT_AUDIO_TRACK, true); - // resolve frames, using indexes from above. - if(not audio_indexes.empty()) { - // get first audio index - auto audio_index = audio_indexes[0]; - - // get access to the audio track item. - auto track_it = base_.item().front().item_at_index(audio_index); - - // if it's valid (which it should be) - // resolve frame - // may return {} if no clip. - // focus list won't work correctly doing it this way. - // that would require the resolve_time function to return a vector, and - not be used on individal tracks. - - if(track_it) { - auto ii = (*track_it)->resolve_time( - FrameRate(0), - media::MediaType::MT_AUDIO, - base_.focus_list()); - } - } - */ - [=](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. return base_.item().get_all_frame_IDs( - media_type, - tsm, - override_rate, - base_.focus_list()); - + media_type, tsm, override_rate, base_.focus_list()); }, - // [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - // auto el = utility::EditList(utility::ClipList({utility::EditListSection( - // base_.uuid(), - // base_.item().trimmed_frame_duration(), - // utility::Timecode( - // base_.item().trimmed_frame_duration().frames(), - // base_.rate().to_fps()))})); - // return el; - // }, - [=](utility::serialise_atom) -> result { - if (not actors_.empty()) { - auto rp = make_response_promise(); + std::vector actors = map_value_to_vec(actors_); + actors.push_back(selection_actor_); + auto rp = make_response_promise(); - fan_out_request( - map_value_to_vec(actors_), infinite, serialise_atom_v) - .then( - [=](std::vector json) mutable { - JsonStore jsn; - jsn["base"] = base_.serialise(); - jsn["actors"] = {}; - for (const auto &j : json) - jsn["actors"] - [static_cast(j["base"]["container"]["uuid"])] = - j; - rp.deliver(jsn); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + fan_out_request(actors, infinite, serialise_atom_v) + .then( + [=](std::vector json) mutable { + JsonStore jsn; + jsn["base"] = base_.serialise(); + jsn["actors"] = {}; + for (const auto &j : json) + jsn["actors"] + [static_cast(j["base"]["container"]["uuid"])] = j; + if (playhead_) { + request(playhead_.actor(), infinite, utility::serialise_atom_v) + .then( + [=](const utility::JsonStore &playhead_state) mutable { + playhead_serialisation_ = playhead_state; + jsn["playhead"] = playhead_state; + rp.deliver(jsn); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(jsn); + }); - return rp; - } - JsonStore jsn; - jsn["base"] = base_.serialise(); - jsn["actors"] = {}; + } else { + if (!playhead_serialisation_.is_null()) { + jsn["playhead"] = playhead_serialisation_; + } + rp.deliver(jsn); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); - return result(jsn); + return rp; }, @@ -2729,7 +2667,7 @@ caf::message_handler TimelineActor::message_handler() { const caf::uri &path, const std::string &data) -> result { auto rp = make_response_promise(); - // purge timeline.. ? + // purge timeline.. ? spawn( timeline_importer, rp, @@ -2753,9 +2691,11 @@ void TimelineActor::init() { history_ = spawn>(history_uuid_); link_to(history_); - selection_actor_ = spawn( - "SubsetPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); + if (!selection_actor_) { + selection_actor_ = spawn( + "TimelinePlayheadSelectionActor", caf::actor_cast(this)); + link_to(selection_actor_); + } set_down_handler([=](down_msg &msg) { // find in playhead list.. @@ -3243,10 +3183,8 @@ void TimelineActor::bake( rp.deliver(UuidActor(track_uuid, track_actor)); } -void TimelineActor::export_otio( - caf::typed_response_promise rp, const caf::uri &path, const std::string &type) { - // build timeline from model.. - // we need clips to return information on current media source.. +void TimelineActor::export_otio_as_string(caf::typed_response_promise rp) { + otio::ErrorStatus err; auto jany = std::any(); auto meta = base_.item().prop(); @@ -3515,13 +3453,12 @@ void TimelineActor::export_otio( ostack->append_child(to_composition(track)); } - otimeline->to_json_file(uri_to_posix_path(path), &err); + const auto result = otimeline->to_json_string(&err); - if (not otio::is_error(err)) { - rp.deliver(true); - } else { - rp.deliver(false); - } + if (not otio::is_error(err)) + rp.deliver(result); + else + rp.deliver(make_error(xstudio_error::error, "Export failed")); // and crash.. otimeline->possibly_delete(); @@ -3531,3 +3468,18 @@ void TimelineActor::export_otio( rp.deliver(make_error(xstudio_error::error, err.what())); } } + + +void TimelineActor::export_otio( + caf::typed_response_promise rp, + const std::string &otio_str, + const caf::uri &path, + const std::string &type) { + // build timeline from model.. + // we need clips to return information on current media source.. + + caf::scoped_actor sys(system()); + auto global = system().registry().template get(global_registry); + auto epa = request_receive(*sys, global, global::get_python_atom_v); + rp.delegate(epa, session::export_atom_v, otio_str, path, type); +} diff --git a/src/timeline/src/track_actor.cpp b/src/timeline/src/track_actor.cpp index 2e6d533d2..0469c8ceb 100644 --- a/src/timeline/src/track_actor.cpp +++ b/src/timeline/src/track_actor.cpp @@ -483,6 +483,8 @@ caf::message_handler TrackActor::message_handler() { return rp; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + [=](remove_item_at_frame_atom, const int frame, const int duration, @@ -772,14 +774,9 @@ caf::message_handler TrackActor::message_handler() { 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 track // playable. - return base_.item().get_all_frame_IDs( - media_type, - tsm, - override_rate); - + return base_.item().get_all_frame_IDs(media_type, tsm, override_rate); } }; diff --git a/src/ui/base/src/keyboard.cpp b/src/ui/base/src/keyboard.cpp index 1934eeb6f..e3e331a6d 100644 --- a/src/ui/base/src/keyboard.cpp +++ b/src/ui/base/src/keyboard.cpp @@ -79,36 +79,69 @@ void Hotkey::update_state( if (!pressed_) { pressed_ = true; - notify_watchers(context, window); - anon_send( - keypress_monitor, - keypress_monitor::hotkey_event_atom_v, - uuid_, - pressed_, - context, - window); + notify(context, window, keypress_monitor); } else if (auto_repeat && auto_repeat_) { - notify_watchers(context, window); + notify(context, window, keypress_monitor); + } + } else if (pressed_) { + pressed_ = false; + notify(context, window, keypress_monitor); + } +} + +void Hotkey::notify(const std::string &context, + const std::string &window, + caf::actor keypress_monitor) +{ + + + auto p = exclusive_watchers_.begin(); + while (p != exclusive_watchers_.end()) { + auto exclusive = caf::actor_cast(*p); + if (exclusive) { + // we've found a valid 'exclusive' hotkey watcher anon_send( - keypress_monitor, + exclusive, keypress_monitor::hotkey_event_atom_v, uuid_, pressed_, context, - window); + window + ); + return; + } else { + // the exclusive watcher must have exited without telling us! + p = exclusive_watchers_.erase(p); } - } else if (pressed_) { - pressed_ = false; - notify_watchers(context, window); - anon_send( - keypress_monitor, - keypress_monitor::hotkey_event_atom_v, - uuid_, - pressed_, - context, - window); } + + // no exclusive watchers, broadcast hotkey event to everyone! + notify_watchers(context, window); + anon_send( + keypress_monitor, + keypress_monitor::hotkey_event_atom_v, + uuid_, + pressed_, + context, + window); +} + +void Hotkey::exclusive_watcher(caf::actor_addr w, bool watch) { + + auto p = exclusive_watchers_.begin(); + while (p != exclusive_watchers_.end()) { + if (*p = w) { + p = exclusive_watchers_.erase(p); + } else { + p++; + } + } + + if (watch) { + exclusive_watchers_.insert(exclusive_watchers_.begin(), w); + } + } void Hotkey::watcher_died(caf::actor_addr &watcher) { @@ -122,6 +155,19 @@ void Hotkey::watcher_died(caf::actor_addr &watcher) { } } +void Hotkey::add_watcher(caf::actor_addr w) { + + auto p = watchers_.begin(); + while (p != watchers_.end()) { + if (*p == w) { + return; + } + p++; + } + watchers_.push_back(w); +} + + void Hotkey::notify_watchers(const std::string &context, const std::string &window) { auto p = watchers_.begin(); while (p != watchers_.end()) { diff --git a/src/ui/canvas/src/canvas.cpp b/src/ui/canvas/src/canvas.cpp index 7e4de6044..608518491 100644 --- a/src/ui/canvas/src/canvas.cpp +++ b/src/ui/canvas/src/canvas.cpp @@ -817,7 +817,19 @@ void Canvas::end_draw_no_lock() { } } -void Canvas::changed() { last_change_time_ = utility::clock::now(); } +void Canvas::changed() { + last_change_time_ = utility::clock::now(); + // the canvas hash is guaranteed to change if the appearance of the canvas + // has *changed*. But two different canvases could easily have the same + // hash. So not a real hash at all. + hash_ = items_.size(); + if (items_.size()) { + if (std::holds_alternative(items_.back())) { + auto &caption = std::get(items_.back()); + hash_ += std::hash{}(caption.hash()); + } + } +} void xstudio::ui::canvas::from_json(const nlohmann::json &j, Canvas &c) { diff --git a/src/ui/opengl/src/opengl_colour_lut_texture.cpp b/src/ui/opengl/src/opengl_colour_lut_texture.cpp index 611c244c0..a0c0c8954 100644 --- a/src/ui/opengl/src/opengl_colour_lut_texture.cpp +++ b/src/ui/opengl/src/opengl_colour_lut_texture.cpp @@ -8,7 +8,7 @@ #include "xstudio/utility/chrono.hpp" using namespace xstudio::ui::opengl; - + GLint GLColourLutTexture::interpolation() { switch (descriptor_.interpolation_) { case colour_pipeline::LUTDescriptor::NEAREST: diff --git a/src/ui/opengl/src/opengl_multi_buffered_texture.cpp b/src/ui/opengl/src/opengl_multi_buffered_texture.cpp index f895c4067..b5fe52fb5 100644 --- a/src/ui/opengl/src/opengl_multi_buffered_texture.cpp +++ b/src/ui/opengl/src/opengl_multi_buffered_texture.cpp @@ -29,13 +29,12 @@ class DebugTimer { }; } // namespace - + namespace xstudio::ui::opengl { class TextureTransferWorker { - public: - - TextureTransferWorker(GLDoubleBufferedTexture * owner) { + public: + TextureTransferWorker(GLDoubleBufferedTexture *owner) { for (int i = 0; i < 8; ++i) { threads_.emplace_back(std::thread(&TextureTransferWorker::run, this)); } @@ -44,12 +43,12 @@ class TextureTransferWorker { ~TextureTransferWorker() { - for (auto &t: threads_) { + for (auto &t : threads_) { // when any thread picks up an empty job it exits its loop add_job(GLDoubleBufferedTexture::GLBlindTexturePtr()); } - for (auto &t: threads_) { + for (auto &t : threads_) { t.join(); } } @@ -59,7 +58,7 @@ class TextureTransferWorker { GLDoubleBufferedTexture::GLBlindTexturePtr get_job() { std::unique_lock lk(m); if (queue.empty()) { - cv.wait(lk, [=]{ return !queue.empty(); }); + cv.wait(lk, [=] { return !queue.empty(); }); } auto rt = queue.front(); queue.pop_front(); @@ -80,37 +79,35 @@ class TextureTransferWorker { { std::lock_guard lk(m); queue.push_back(ptr); - } cv.notify_one(); } - void run() - { - while(1) { - + void run() { + while (1) { + // this blocks until there is something in queue for us GLDoubleBufferedTexture::GLBlindTexturePtr tex = get_job(); - if (!tex) break; // exit + if (!tex) + break; // exit tex->do_pixel_upload(); - } } std::mutex m; std::condition_variable cv; std::deque queue; - GLDoubleBufferedTexture * owner_; + GLDoubleBufferedTexture *owner_; }; -} +} // namespace xstudio::ui::opengl GLDoubleBufferedTexture::GLDoubleBufferedTexture() { worker_ = new TextureTransferWorker(this); - } -void GLDoubleBufferedTexture::bind(const media_reader::ImageBufPtr &image, int &tex_index, Imath::V2i &dims) { +void GLDoubleBufferedTexture::bind( + const media_reader::ImageBufPtr &image, int &tex_index, Imath::V2i &dims) { auto p = textures_.find_pending(image); if (p != textures_.end()) { @@ -125,7 +122,7 @@ void GLDoubleBufferedTexture::queue_image_set_for_upload( worker_->clear_jobs(); - // image_set includes images that are due to go on-screen NOW in the + // image_set includes images that are due to go on-screen NOW in the // current draw, plus images that are not going on screen now but will // be soon. // We can do a-sync uploads of images to texture memory for performance @@ -136,17 +133,17 @@ void GLDoubleBufferedTexture::queue_image_set_for_upload( // queue of images that need to be in texture memory image_queue_.clear(); - const std::vector & draw_order = image_set->layout_data()->image_draw_order_hint_; + const std::vector &draw_order = image_set->layout_data()->image_draw_order_hint_; auto available_textures = textures_; - for (const auto &i: draw_order) { + for (const auto &i : draw_order) { auto im = image_set->onscreen_image(i); queue_for_upload(available_textures, im); } // now queue images the we'll need in the *next* redraw - for (const auto &i: draw_order) { + for (const auto &i : draw_order) { if (image_set->future_images(i).size()) { auto im = image_set->future_images(i)[0]; queue_for_upload(available_textures, im); @@ -154,26 +151,24 @@ void GLDoubleBufferedTexture::queue_image_set_for_upload( } // now queue images the we'll need in the *next* *next* redraw - for (const auto &i: draw_order) { + for (const auto &i : draw_order) { if (image_set->future_images(i).size() > 1) { auto im = image_set->future_images(i)[1]; queue_for_upload(available_textures, im); } } - } void GLDoubleBufferedTexture::queue_for_upload( GLDoubleBufferedTexture::TexSet &available_textures, - const media_reader::ImageBufPtr &image - ) -{ - if (!image) return; + const media_reader::ImageBufPtr &image) { + if (!image) + return; auto q = available_textures.find_pending(image); if (q != available_textures.end()) { available_textures.erase(q); } else if (available_textures.size()) { - q = available_textures.begin(); + q = available_textures.begin(); (*q)->prepare_for_upload(image); worker_->add_job(*q); available_textures.erase(q); @@ -182,21 +177,20 @@ void GLDoubleBufferedTexture::queue_for_upload( } } -void GLDoubleBufferedTexture::release(const media_reader::ImageBufPtr &image) { - +void GLDoubleBufferedTexture::release(const media_reader::ImageBufPtr &image) { + // move the texture containing 'image' into the used_textures_ map so // it can be used for another image if (image_queue_.size()) { auto im = image_queue_.front(); - auto q = textures_.find(image); + auto q = textures_.find(image); if (q != textures_.end()) { (*q)->prepare_for_upload(im); worker_->add_job(*q); image_queue_.pop_front(); } else { - //std::cerr << "Didn't release\n"; + // std::cerr << "Didn't release\n"; } } - } diff --git a/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp b/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp index 88c6ce9c9..bd5ade6c7 100644 --- a/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp +++ b/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp @@ -8,7 +8,7 @@ #include "xstudio/utility/chrono.hpp" using namespace xstudio::ui::opengl; - + GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { // ensure no copying is in flight if (upload_thread_.joinable()) diff --git a/src/ui/opengl/src/opengl_shape_renderer.cpp b/src/ui/opengl/src/opengl_shape_renderer.cpp index b29bd7d52..8b7959f27 100644 --- a/src/ui/opengl/src/opengl_shape_renderer.cpp +++ b/src/ui/opengl/src/opengl_shape_renderer.cpp @@ -517,6 +517,7 @@ void OpenGLShapeRenderer::render_shapes( glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } + shader_->use(); shader_->set_shader_parameters(shader_params); @@ -527,7 +528,8 @@ void OpenGLShapeRenderer::render_shapes( glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glEnable(GL_DEPTH_TEST); + // glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); shader_->stop_using(); } \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_ssbo_image_texture.cpp b/src/ui/opengl/src/opengl_ssbo_image_texture.cpp index a38ea7595..7408d721c 100644 --- a/src/ui/opengl/src/opengl_ssbo_image_texture.cpp +++ b/src/ui/opengl/src/opengl_ssbo_image_texture.cpp @@ -8,7 +8,7 @@ #include "xstudio/utility/chrono.hpp" using namespace xstudio::ui::opengl; - + GLSsboTex::~GLSsboTex() { // ensure no copying is in flight diff --git a/src/ui/opengl/src/opengl_texture_base.cpp b/src/ui/opengl/src/opengl_texture_base.cpp index 882a2118a..d7237bc41 100644 --- a/src/ui/opengl/src/opengl_texture_base.cpp +++ b/src/ui/opengl/src/opengl_texture_base.cpp @@ -15,8 +15,7 @@ void GLBlindTex::release() { when_last_used_ = utility::clock::now(); } GLBlindTex::GLBlindTex() { static int f = 0; - media_key_ = media::MediaKey("EmptyTex{}{}{}",caf::uri(),f++,"_0"); - + media_key_ = media::MediaKey("EmptyTex{}{}{}", caf::uri(), f++, "_0"); } GLBlindTex::~GLBlindTex() {} @@ -30,7 +29,7 @@ void GLBlindTex::do_pixel_upload() { lk.unlock(); if (pending_source_frame_) { - uint8_t * xstudio_pixel_buffer = (uint8_t *)pending_source_frame_->buffer(); + uint8_t *xstudio_pixel_buffer = (uint8_t *)pending_source_frame_->buffer(); size_t copy_size = std::min(tex_size_bytes(), pending_source_frame_->size()); // just a memcpy! @@ -40,9 +39,9 @@ void GLBlindTex::do_pixel_upload() { } lk.lock(); pending_upload_ = false; - in_progress_ = false; - media_key_ = pending_media_key_; - source_frame_ = pending_source_frame_; + in_progress_ = false; + media_key_ = pending_media_key_; + source_frame_ = pending_source_frame_; lk.unlock(); cv_.notify_one(); @@ -64,7 +63,7 @@ void GLBlindTex::cancel_upload() { while (in_progress_) { cv_.wait(lk); } - pending_upload_ = false; + pending_upload_ = false; pending_media_key_ = media::MediaKey(); pending_source_frame_.reset(); } @@ -78,7 +77,7 @@ void GLBlindTex::prepare_for_upload(const media_reader::ImageBufPtr &frame) { // make sure we're not still uploading pixels from a previous request wait_on_upload_pixels(); - pending_media_key_ = frame->media_key(); + pending_media_key_ = frame->media_key(); pending_source_frame_ = frame; if (pending_source_frame_ && pending_source_frame_->size()) { @@ -95,6 +94,5 @@ void GLBlindTex::prepare_for_upload(const media_reader::ImageBufPtr &frame) { std::lock_guard lk(mutex_); pending_upload_ = true; } - } } \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_viewport_renderer.cpp b/src/ui/opengl/src/opengl_viewport_renderer.cpp index 2528e6619..39b703805 100644 --- a/src/ui/opengl/src/opengl_viewport_renderer.cpp +++ b/src/ui/opengl/src/opengl_viewport_renderer.cpp @@ -81,13 +81,13 @@ void ColourPipeLutCollection::bind_luts(GLShaderProgramPtr shader, int &tex_idx) } } -std::map OpenGLViewportRenderer::s_resources_store_; +std::map + OpenGLViewportRenderer::s_resources_store_; OpenGLViewportRenderer::OpenGLViewportRenderer(const std::string &window_id) - : viewport::ViewportRenderer(), window_id_(window_id) -{ - // Instances of OpenGLViewportRenderer that are in the same xstudio - // window need to share texture resources as they display exactly the + : viewport::ViewportRenderer(), window_id_(window_id) { + // Instances of OpenGLViewportRenderer that are in the same xstudio + // window need to share texture resources as they display exactly the // same image. if (s_resources_store_.contains(window_id)) { resources_ = s_resources_store_[window_id]; @@ -102,7 +102,7 @@ OpenGLViewportRenderer::~OpenGLViewportRenderer() { resources_.reset(); // check if we were the last instance of OpenGLViewportRenderer to be - // using the entry in s_resources_store_ .. if so, remove it from + // using the entry in s_resources_store_ .. if so, remove it from // s_resources_store_ so it gets destroyed and texture resources etc are // released auto p = s_resources_store_.find(window_id_); @@ -213,10 +213,10 @@ void OpenGLViewportRenderer::clear_viewport_area(const Imath::M44f &window_to_vi botomleft *= 1.0f / botomleft.w; topright *= 1.0f / topright.w; - float left = 0.5f * (botomleft.x + 1.0f) * float(vp[2]); + float left = 0.5f * (botomleft.x + 1.0f) * float(vp[2]); float bottom = 0.5f * (botomleft.y + 1.0f) * float(vp[3]); float top = 0.5f * (topright.y + 1.0f) * float(vp[3]); - float right = 0.5f * (topright.x + 1.0f) * float(vp[2]); + float right = 0.5f * (topright.x + 1.0f) * float(vp[2]); scissor_coords_[0] = (int)round(left); scissor_coords_[1] = (int)round(bottom); @@ -224,15 +224,10 @@ void OpenGLViewportRenderer::clear_viewport_area(const Imath::M44f &window_to_vi scissor_coords_[3] = (int)round(top - bottom); glEnable(GL_SCISSOR_TEST); - glScissor( - scissor_coords_[0], - scissor_coords_[1], - scissor_coords_[2], - scissor_coords_[3]); - glClearColor(0.0f, 0.0f, 0.0f, use_alpha_ ? 1.0f : 0.0f); + glScissor(scissor_coords_[0], scissor_coords_[1], scissor_coords_[2], scissor_coords_[3]); + glClearColor(0.0f, 0.0f, 0.0f, use_alpha_ ? 0.0f : 1.0f); glClear(GL_COLOR_BUFFER_BIT); glDisable(GL_SCISSOR_TEST); - } utility::JsonStore OpenGLViewportRenderer::default_prefs() { @@ -276,7 +271,8 @@ void OpenGLViewportRenderer::render( // this value gives us how much of the parent window is covered by the viewport. // So if the xstudio window is 1000px in width, and the viewport is 500px wide // (with the rest of the UI taking up the remainder) then this value will be 0.5 - const float viewport_x_size_in_window = window_to_viewport_matrix[0][0] / window_to_viewport_matrix[3][3]; + const float viewport_x_size_in_window = + window_to_viewport_matrix[0][0] / window_to_viewport_matrix[3][3]; // the gl viewport corresponds to the parent window size. std::array gl_viewport; @@ -293,6 +289,22 @@ void OpenGLViewportRenderer::render( glUseProgram(0); + /* Call the render functions of overlay plugins - for the BeforeImage pass, we only call + this if we have an alpha buffer that allows us to 'under' the image with the overlay + drawings. */ + if (use_alpha_) { + for (auto orf : overlay_renderers) { + if (orf.second->preferred_render_pass() == + plugin::ViewportOverlayRenderer::BeforeImage) { + orf.second->render_viewport_overlay( + window_to_viewport_matrix, + viewport_to_image_space, + abs(viewport_du_dx), + use_alpha_); + } + } + } + if (images && images->layout_data()) { glDisable(GL_DEPTH_TEST); @@ -303,10 +315,28 @@ void OpenGLViewportRenderer::render( textures()[0]->queue_image_set_for_upload(images); - for (const auto &idx: images->layout_data()->image_draw_order_hint_) { - __draw_image(images, idx, window_to_viewport_matrix, viewport_to_image_space, viewport_du_dx, overlay_renderers); + for (const auto &idx : images->layout_data()->image_draw_order_hint_) { + __draw_image( + images, + idx, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + overlay_renderers); } + } + if (!use_alpha_) { + for (auto orf : overlay_renderers) { + if (orf.second->preferred_render_pass() == + plugin::ViewportOverlayRenderer::BeforeImage) { + orf.second->render_viewport_overlay( + window_to_viewport_matrix, + viewport_to_image_space, + abs(viewport_du_dx), + use_alpha_); + } + } } #ifdef DEBUG_GRAB_FRAMEBUFFER @@ -325,7 +355,6 @@ void OpenGLViewportRenderer::render( std::chrono::duration_cast(utility::clock::now()-t0).count() << "\n";*/ - } void OpenGLViewportRenderer::__draw_image( @@ -343,7 +372,8 @@ void OpenGLViewportRenderer::__draw_image( image_to_be_drawn = images->onscreen_image(index); } - Imath::M44f to_image_matrix = (image_to_be_drawn.layout_transform() * viewport_to_image_space.inverse()).inverse(); + Imath::M44f to_image_matrix = + (image_to_be_drawn.layout_transform() * viewport_to_image_space.inverse()).inverse(); /* Here we allow plugins to run arbitrary GPU draw & computation routines. This will allow pixel data to be rendered to textures (offscreen), for example, @@ -352,10 +382,7 @@ void OpenGLViewportRenderer::__draw_image( for (auto hook : pre_render_gpu_hooks_) { hook.second->pre_viewport_draw_gpu_hook( - window_to_viewport_matrix, - to_image_matrix, - viewport_du_dx, - image_to_be_drawn); + window_to_viewport_matrix, to_image_matrix, viewport_du_dx, image_to_be_drawn); } // if we've received a new image and/or colour pipeline data (LUTs etc) since the last @@ -367,11 +394,7 @@ void OpenGLViewportRenderer::__draw_image( // visible underneath QML elements with opactity etc. or other viewports in the // same window glEnable(GL_SCISSOR_TEST); - glScissor( - scissor_coords_[0], - scissor_coords_[1], - scissor_coords_[2], - scissor_coords_[3]); + glScissor(scissor_coords_[0], scissor_coords_[1], scissor_coords_[2], scissor_coords_[3]); /* 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 @@ -380,7 +403,7 @@ void OpenGLViewportRenderer::__draw_image( for (auto orf : overlay_renderers) { if (orf.second->preferred_render_pass() == plugin::ViewportOverlayRenderer::BeforeImage) { - orf.second->render_opengl( + orf.second->render_image_overlay( window_to_viewport_matrix, to_image_matrix, abs(viewport_du_dx), @@ -391,7 +414,13 @@ void OpenGLViewportRenderer::__draw_image( } if (image_to_be_drawn) { // scissor test on main draw - draw_image(image_to_be_drawn, images->layout_data(), index, window_to_viewport_matrix, viewport_to_image_space, viewport_du_dx); + draw_image( + image_to_be_drawn, + images->layout_data(), + index, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx); } /* Call the render functions of overlay plugins - note that if the overlay prefers to draw @@ -402,7 +431,7 @@ void OpenGLViewportRenderer::__draw_image( if (orf.second->preferred_render_pass() == plugin::ViewportOverlayRenderer::AfterImage || !use_alpha_) { - orf.second->render_opengl( + orf.second->render_image_overlay( window_to_viewport_matrix, to_image_matrix, abs(viewport_du_dx), @@ -412,7 +441,6 @@ void OpenGLViewportRenderer::__draw_image( } } glDisable(GL_SCISSOR_TEST); - } void OpenGLViewportRenderer::draw_image( @@ -433,15 +461,14 @@ void OpenGLViewportRenderer::draw_image( viewport_to_image_space, viewport_du_dx, layout_data->custom_layout_data_, - index - ); + index); active_shader_program_->set_shader_parameters(shader_params); if (use_alpha_) { - // this set-up allows the image to be drawn 'under' overlays that are + // this set-up allows the image to be drawn 'under' overlays that are // drawn first onto the black canvas - (e.g. annotations that can // be drawn better this way) glEnable(GL_BLEND); @@ -459,12 +486,11 @@ void OpenGLViewportRenderer::draw_image( glUseProgram(0); if (use_alpha_) { - // this set-up allows the image to be drawn 'under' overlays that are + // this set-up allows the image to be drawn 'under' overlays that are // drawn first onto the black canvas - (e.g. annotations that can // be drawn better this way) glDisable(GL_BLEND); } - } bool OpenGLViewportRenderer::activate_shader( @@ -536,20 +562,22 @@ void OpenGLViewportRenderer::pre_init() { glGetIntegerv(GL_ALPHA_BITS, &alpha_bits); // TODO: not using alpha buffer at all right now, making offscreen rendering // of annotations go wrong. - use_alpha_ = false;// alpha_bits != 0; + use_alpha_ = false; // alpha_bits != 0; resources_->init(); - } OpenGLViewportRenderer::SharedResources::~SharedResources() { - glDeleteBuffers(1, &vbo_); - glDeleteVertexArrays(1, &vao_); + if (vbo_) + glDeleteBuffers(1, &vbo_); + if (vao_) + glDeleteVertexArrays(1, &vao_); } void OpenGLViewportRenderer::SharedResources::init() { - if (textures_.size()) return; + if (textures_.size()) + return; glewInit(); @@ -571,14 +599,32 @@ void OpenGLViewportRenderer::SharedResources::init() { static std::array vertices = { // 1st triangle - -1.0f, 1.0f, 0.0f, 1.0f, // top left - 1.0f, 1.0f, 0.0f, 1.0f, // top right - 1.0f, -1.0f, 0.0f, 1.0f, // bottom right + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + 1.0f, + 1.0f, + 0.0f, + 1.0f, // top right + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right // 2nd triangle - 1.0f, -1.0f, 0.0f, 1.0f, // bottom right - -1.0f, 1.0f, 0.0f, 1.0f, // top left - -1.0f, -1.0f, 0.0f, 1.0f // bottom left - }; + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + -1.0f, + -1.0f, + 0.0f, + 1.0f // bottom left + }; glBindVertexArray(vao_); // 2. copy our vertices array in a buffer for OpenGL to use @@ -590,10 +636,9 @@ void OpenGLViewportRenderer::SharedResources::init() { glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); - //glDeleteBuffers(1, &vbo); + // glDeleteBuffers(1, &vbo); // add shader for no image render no_image_shader_program_ = GLShaderProgramPtr(static_cast(new NoImageShaderProgram())); } - diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index 6b8c632ad..70c72f012 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -329,6 +329,10 @@ BookmarkModel::BookmarkModel(QObject *parent) : super(parent) { QVector BookmarkModel::getRoleChanges( const bookmark::BookmarkDetail &ood, const bookmark::BookmarkDetail &nbd) const { QVector roles; + if (ood.annotation_hash_ && nbd.annotation_hash_ && + *(ood.annotation_hash_) != *(nbd.annotation_hash_)) { + roles.push_back(thumbnailRole); + } return roles; } @@ -415,7 +419,7 @@ void BookmarkModel::init(caf::actor_system &_system) { // a note, for example auto change = getRoleChanges(bookmarks_.at(ua.uuid()), detail); - if (change.empty() || change.contains(thumbnailRole)) { + if (change.contains(thumbnailRole)) { out_of_date_thumbnails_.insert(detail.uuid_); } @@ -591,6 +595,9 @@ BookmarkModel::createJsonFromDetail(const bookmark::BookmarkDetail &detail) cons auto result = R"({"uuid": null, "thumbnailURL": "qrc:///feather_icons/film.svg"})"_json; result["uuid"] = detail.uuid_; + if (detail.annotation_hash_) { + result["annotation_hash"] = *(detail.annotation_hash_); + } // spdlog::warn("{} {} {}", bool(detail.owner_) , to_string((*(detail.owner_)).actor()), // bool(detail.logical_start_frame_) ); diff --git a/src/ui/qml/conform/src/conform_ui.cpp b/src/ui/qml/conform/src/conform_ui.cpp index 39cde7218..17c43ecd1 100644 --- a/src/ui/qml/conform/src/conform_ui.cpp +++ b/src/ui/qml/conform/src/conform_ui.cpp @@ -579,16 +579,15 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( auto media = UuidActorVector(); auto src_playlist_index = QPersistentModelIndex(); - SessionModel *smodel = nullptr; + SessionModel *smodel = nullptr; // get media uuidactors for (const auto &i : mediaIndexes) { if (i.data(SessionModel::Roles::typeRole) != "Media") continue; - if(smodel == nullptr) { - smodel = qobject_cast( - const_cast(i.model())); + if (smodel == nullptr) { + smodel = qobject_cast(const_cast(i.model())); src_playlist_index = QPersistentModelIndex(smodel->getPlaylistIndex(i)); } @@ -615,7 +614,7 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( } auto nquuid = QUuid(); - if(smodel and not media.empty()) + if (smodel and not media.empty()) nquuid = smodel->progressRangeNotification( src_playlist_index, "Conforming Media", 0, media.size()); @@ -637,7 +636,7 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( auto seq_to_media = std::map>(); auto seq_to_name = std::map(); - auto count = 0; + auto count = 0; auto media_done = 0; for (const auto &i : reply) { @@ -806,14 +805,16 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( timeline, UuidActor(), playlist_media); - if(not nquuid.isNull()) { + if (not nquuid.isNull()) { media_done += playlist_media.size(); - smodel->updateProgressNotification(src_playlist_index, nquuid, media_done); + smodel->updateProgressNotification( + src_playlist_index, nquuid, media_done); } } } else { - if(not nquuid.isNull()) - smodel->warnNotification(src_playlist_index, "No sequence found for media", 10, nquuid); + if (not nquuid.isNull()) + smodel->warnNotification( + src_playlist_index, "No sequence found for media", 10, nquuid); throw std::runtime_error("No sequence found for media"); } @@ -821,7 +822,7 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - if(not nquuid.isNull()) + if (not nquuid.isNull()) smodel->warnNotification(src_playlist_index, err.what(), 10, nquuid); throw; @@ -830,8 +831,9 @@ QFuture> ConformEngineUI::conformToNewSequenceFuture( // return uuid of each timeline containing copied media ? - if(not nquuid.isNull()) - smodel->infoNotification(src_playlist_index, "Media Conformed to Sequences", 5, nquuid); + if (not nquuid.isNull()) + smodel->infoNotification( + src_playlist_index, "Media Conformed to Sequences", 5, nquuid); return result; }); 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 6be52a5de..8481bf9e7 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 @@ -126,7 +126,7 @@ void GlobalStoreModel::setAutosave(const bool enabled) { scoped_actor sys{system()}; autosave_ = enabled; sys->send(gsh_->get_actor(), xstudio::global_store::autosave_atom_v, enabled); - gsh_->set_value(enabled, "/ui/qml/autosave"); + gsh_->set_value(enabled, "/core/global_store/autosave_enable"); emit autosaveChanged(); } @@ -432,7 +432,7 @@ void PublicPreferencesModel::setAutosave(const bool enabled) { scoped_actor sys{system()}; autosave_ = enabled; sys->send(gsh_->get_actor(), xstudio::global_store::autosave_atom_v, enabled); - gsh_->set_value(enabled, "/ui/qml/autosave"); + gsh_->set_value(enabled, "/core/global_store/autosave_enable"); emit autosaveChanged(); } diff --git a/src/ui/qml/helper/src/CMakeLists.txt b/src/ui/qml/helper/src/CMakeLists.txt index d29e2d9a7..1af9fd719 100644 --- a/src/ui/qml/helper/src/CMakeLists.txt +++ b/src/ui/qml/helper/src/CMakeLists.txt @@ -1,14 +1,27 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - Qt5::Qml - Qt5::Gui - Qt5::Quick - # Qt5::DBus - xstudio::global_store - xstudio::media - xstudio::utility -) +if(WIN32) + SET(LINK_DEPS + ${CAF_LIBRARY_core} + Qt5::Core + Qt5::Qml + Qt5::Gui + Qt5::Quick + xstudio::global_store + xstudio::media + xstudio::utility + ) +else() + SET(LINK_DEPS + ${CAF_LIBRARY_core} + Qt5::Core + Qt5::Qml + Qt5::Gui + Qt5::Quick + Qt5::DBus + xstudio::global_store + xstudio::media + xstudio::utility + ) +endif() SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/thumbnail_provider_ui.hpp" diff --git a/src/ui/qml/helper/src/QTreeModelToTableModel.cpp b/src/ui/qml/helper/src/QTreeModelToTableModel.cpp index 70cef3540..b96e27cdd 100644 --- a/src/ui/qml/helper/src/QTreeModelToTableModel.cpp +++ b/src/ui/qml/helper/src/QTreeModelToTableModel.cpp @@ -6,6 +6,7 @@ // #include // #include #include +#include #include "xstudio/ui/qml/QTreeModelToTableModel.hpp" @@ -90,7 +91,13 @@ void QTreeModelToTableModel::clearModelData() { QModelIndex QTreeModelToTableModel::rootIndex() const { return m_rootIndex; } void QTreeModelToTableModel::setRootIndex(const QModelIndex &idx) { - if (m_rootIndex == idx) + + // sometimes, idx is not valid and neither is m_rootIndex but length > 0. + // This can happen if m_rootIndex has gone invalid because the node it + // points to has been deleted from the source model. + // In this case, we don't want to return here but continue to rebuild the + // model. + if (m_rootIndex == idx && m_rootIndex.isValid() || !length()) return; if (m_model) diff --git a/src/ui/qml/helper/src/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index 561748b24..3eb48f199 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -119,6 +119,9 @@ QString xstudio::ui::qml::getThumbnailURL( nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { + if (not var.isValid()) + return nlohmann::json(); + switch (QMetaType::Type(var.type())) { case QMetaType::Bool: return nlohmann::json(var.toBool()); @@ -176,6 +179,7 @@ nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { } break; default: { // No QMetaType for QJSValue + // watchout there is a bug in toVariant when the QJSValue is an object.. if (var.canConvert()) { const auto m = var.value(); return xstudio::ui::qml::qvariant_to_json(m.toVariant()); @@ -453,6 +457,32 @@ void Helpers::inhibitScreenSaver(const bool inhibit) const { #endif } +QVariant Helpers::python_callback( + QString method_name, QUuid python_plugin_uuid, const QVariant args) const { + try { + + utility::JsonStore packed_args(xstudio::ui::qml::qvariant_to_json(args)); + auto python_interp_actor = + CafSystemObject::get_actor_system().registry().template get( + embedded_python_registry); + + scoped_actor sys{CafSystemObject::get_actor_system()}; + auto return_val = utility::request_receive( + *sys, + python_interp_actor, + embedded_python::python_exec_atom_v, + UuidFromQUuid(python_plugin_uuid), + StdFromQString(method_name), + packed_args); + + return json_to_qvariant(return_val); + + } catch (std::exception &e) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return QVariant(); +} bool Helpers::startDetachedProcess( const QString &program, diff --git a/src/ui/qml/helper/src/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 5f5ef5d05..858285429 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -923,10 +923,23 @@ bool MediaListFilterModel::filterAcceptsRow( QModelIndex MediaListFilterModel::rowToSourceIndex(const int row) const { - QModelIndex srcIdx = mapToSource(index(row, 0)); - QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); - if (mdl) { - srcIdx = mdl->mapToModel(srcIdx); + QModelIndex srcIdx; + if (row == -1) { + // last row + srcIdx = mapToSource(index(rowCount() - 1, 0)); + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + srcIdx = mdl->mapToModel(srcIdx); + } + // next item + srcIdx = srcIdx.siblingAtRow(srcIdx.row() + 1); + + } else { + srcIdx = mapToSource(index(row, 0)); + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + srcIdx = mdl->mapToModel(srcIdx); + } } return srcIdx; } diff --git a/src/ui/qml/helper/src/model_helper_ui.cpp b/src/ui/qml/helper/src/model_helper_ui.cpp index e71b2053a..d81703b34 100644 --- a/src/ui/qml/helper/src/model_helper_ui.cpp +++ b/src/ui/qml/helper/src/model_helper_ui.cpp @@ -469,7 +469,9 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { nlohmann::json result; if (value.userType() == qMetaTypeId()) { - QVariant v = qvariant_cast(value).toVariant(); + auto qjsvalue = qvariant_cast(value); + + QVariant v = qjsvalue.toVariant(); switch (static_cast(v.type())) { case QMetaType::QVariantMap: diff --git a/src/ui/qml/session/src/caf_response_ui.cpp b/src/ui/qml/session/src/caf_response_ui.cpp index d028ea605..ef1adf9e1 100644 --- a/src/ui/qml/session/src/caf_response_ui.cpp +++ b/src/ui/qml/session/src/caf_response_ui.cpp @@ -268,6 +268,7 @@ class CafRequest : public ControllableJob> { *sys, actorFromString(system_, json_.at("actor")), utility::notification_atom_v); + result[SessionModel::Roles::notificationRole] = QStringFromStd(n.dump()); } } @@ -278,12 +279,14 @@ class CafRequest : public ControllableJob> { caf::actor_system &system_, QMap &result) { if (type == "Session") { + auto pt = request_receive>( *sys, actorFromString(system_, json_.at("actor")), session::path_atom_v); result[SessionModel::Roles::pathRole] = QStringFromStd(json(to_string(pt.first)).dump()); result[SessionModel::Roles::mtimeRole] = QStringFromStd(json(pt.second).dump()); + } else if (type == "MediaSource") { auto rr = request_receive( *sys, @@ -305,8 +308,9 @@ class CafRequest : public ControllableJob> { scoped_actor &sys, caf::actor_system &system_, QMap &result) { - if (type == "Session" or type == "Playlist" or type == "ContactSheet" or type == "Subset" or type == "Timeline" or - type == "Media" or type == "PlayheadSelection" or type == "Playhead") { + if (type == "Session" or type == "Playlist" or type == "ContactSheet" or + type == "Subset" or type == "Timeline" or type == "Media" or + type == "PlayheadSelection" or type == "Playhead") { auto actor = caf::actor(); 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 b9f912417..fcd5b3e8b 100644 --- a/src/ui/qml/session/src/session_model_core_ui.cpp +++ b/src/ui/qml/session/src/session_model_core_ui.cpp @@ -479,24 +479,27 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { case notificationRole: if (j.count("notification")) { - auto type = j.value("type", ""); - if (j.at("notification").is_null()) { - if (type == "Audio Track" or type == "Video Track") - requestData( - QVariant::fromValue(QUuidFromUuid(j.at("id"))), - idRole, - index, - index, - role); - else - requestData( - QVariant::fromValue(QUuidFromUuid(j.at("actor_uuid"))), - actorUuidRole, - getPlaylistIndex(index), - index, - role); - } else { - result = QVariant::fromValue(mapFromValue(j.at("notification"))); + try { + auto type = j.value("type", ""); + if (j.at("notification").is_null()) { + if (type == "Audio Track" or type == "Video Track") + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + else + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("actor_uuid"))), + actorUuidRole, + getPlaylistIndex(index), + index, + role); + } else { + result = QVariant::fromValue(mapFromValue(j.at("notification"))); + } + } catch (...) { } } break; @@ -1312,8 +1315,8 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int case nameRole: if (j.count("name") and j["name"] != value) { - if ((type == "Session" or type == "ContactSheet" or type == "Subset" or type == "Timeline" or - type == "Playlist") and + if ((type == "Session" or type == "ContactSheet" or type == "Subset" or + type == "Timeline" or type == "Playlist") and actor) { // spdlog::warn("Send update {} {}", j["name"], value); anon_send(actor, utility::name_atom_v, value.get()); 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 c1dec577c..bb4511e83 100644 --- a/src/ui/qml/session/src/session_model_handler_ui.cpp +++ b/src/ui/qml/session/src/session_model_handler_ui.cpp @@ -161,7 +161,7 @@ void SessionModel::init(caf::actor_system &_system) { try { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - // src_str); search from index.. + receivedData( json(src_str), actorRole, QModelIndex(), notificationRole, digest); } catch (...) { @@ -282,8 +282,8 @@ void SessionModel::init(caf::actor_system &_system) { if (index.isValid()) { const nlohmann::json &j = indexToData(index); - if (j.at("type") == "ContactSheet" or j.at("type") == "Subset" or j.at("type") == "Timeline" or - j.at("type") == "Playlist") { + if (j.at("type") == "ContactSheet" or j.at("type") == "Subset" or + j.at("type") == "Timeline" or j.at("type") == "Playlist") { // get media container index index = index.model()->index(0, 0, index); const nlohmann::json &jj = indexToData(index); @@ -974,7 +974,8 @@ void SessionModel::init(caf::actor_system &_system) { if (index.isValid()) { const nlohmann::json &j = indexToData(index); // spdlog::warn("{}", j.dump(2)); - if (j.at("type") == "Subset" or j.at("type") == "Timeline" or j.at("type") == "ContactSheet") { + if (j.at("type") == "Subset" or j.at("type") == "Timeline" or + j.at("type") == "ContactSheet") { const auto tree = *(indexToTree(index)->child(0)); auto media_id = tree.data().at("id"); requestData( @@ -1122,11 +1123,10 @@ void SessionModel::init(caf::actor_system &_system) { [=](utility::event_atom, playlist::create_contact_sheet_atom, const utility::UuidActor &ua) { - try { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - auto index = searchRecursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); /*spdlog::info( 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 069fc0d54..bfafaa828 100644 --- a/src/ui/qml/session/src/session_model_manip_ui.cpp +++ b/src/ui/qml/session/src/session_model_manip_ui.cpp @@ -92,7 +92,7 @@ SessionModel::removeRows(int row, int count, const bool deep, const QModelIndex result = JSONTreeModel::removeRows(row, count, parent); if (media) { - //spdlog::warn("mediaCountRole 2 {}", rowCount(parent)); + // spdlog::warn("mediaCountRole 2 {}", rowCount(parent)); setData(parent.parent(), QVariant::fromValue(rowCount(parent)), mediaCountRole); } } @@ -176,7 +176,8 @@ bool SessionModel::duplicateRows(int row, int count, const QModelIndex &parent) nlohmann::json &j = indexToData(index); if (j.at("type") == "ContainerDivider" or j.at("type") == "Subset" or - j.at("type") == "Timeline" or j.at("type") == "Playlist" or j.at("type") == "ContactSheet") { + j.at("type") == "Timeline" or j.at("type") == "Playlist" or + j.at("type") == "ContactSheet") { auto pactor = actorFromIndex(index.parent(), true); if (pactor) { @@ -578,7 +579,11 @@ QModelIndexList SessionModel::insertRows( for (auto i = 0; i < count; i++) { anon_send( - actor, playlist::create_contact_sheet_atom_v, name, before, false); + actor, + playlist::create_contact_sheet_atom_v, + name, + before, + false); result.push_back(index(row + i, 0, parent)); } } @@ -724,9 +729,6 @@ QModelIndexList SessionModel::insertRows( } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - - qDebug() << "result " << result << "\n"; - return result; } 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 328a0846e..4218e343b 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -30,6 +30,36 @@ void SessionModel::removeNotification(const QModelIndex &index, const QUuid &uui } } +QString SessionModel::getNextName(const QString &nameTemplate) const { + QString result = nameTemplate; + + scoped_actor sys{system()}; + try { + result = QStringFromStd(request_receive( + *sys, session_actor_, name_atom_v, StdFromQString(nameTemplate), true)); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +void SessionModel::setSessionSelection(const QModelIndexList &indexes) const { + try { + UuidActorVector selection; + + for (auto &i : indexes) { + selection.emplace_back(UuidActor( + UuidFromQUuid(i.data(actorUuidRole).toUuid()), + actorFromQString(system(), i.data(actorRole).toString()))); + } + anon_send(session_actor_, timeline::item_selection_atom_v, selection); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + QUuid SessionModel::infoNotification( const QModelIndex &index, const QString &text, @@ -407,6 +437,7 @@ void SessionModel::setSessionActorAddr(const QString &addr) { "name": null, "actor_uuid": null, "actor": null, + "notification": null, "group_actor": null, "container_uuid": null, "rate": null, @@ -979,6 +1010,8 @@ QFuture> SessionModel::handleUriListDropFuture( } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); + } else if (type == "Media List") { + target = actorFromIndex(index.parent(), true); } else { spdlog::warn("UNHANDLED {}", ij.at("type").get()); } @@ -1123,6 +1156,8 @@ QFuture> SessionModel::handleOtherDropFuture( } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); + } else if (type == "Media List") { + target = actorFromIndex(index.parent(), true); } else { spdlog::warn("UNHANDLED {}", ij.at("type").get()); } @@ -1217,6 +1252,11 @@ QFuture> SessionModel::handleOtherDropFuture( } QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) { + auto notify_processing = Notification::ProcessingNotification( + "Importing Session " + StdFromQString(path.path())); + auto notification_uuid = notify_processing.uuid(); + anon_send(session_actor_, utility::notification_atom_v, notify_processing); + return QtConcurrent::run([=]() { bool result = false; JsonStore js; @@ -1225,6 +1265,10 @@ QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) try { js = utility::open_session(UriFromQUrl(path)); } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_send(session_actor_, utility::notification_atom_v, notify); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return false; } @@ -1232,6 +1276,10 @@ QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) try { js = JsonStore(qvariant_to_json(json)); } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_send(session_actor_, utility::notification_atom_v, notify); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return false; } @@ -1249,8 +1297,17 @@ QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) spdlog::info( "Session {} merged in {:.3} seconds.", StdFromQString(path.path()), sw); - result = true; + result = true; + auto notify = Notification::InfoNotification( + "Import Session Succeeded", std::chrono::seconds(5)); + notify.uuid(notification_uuid); + anon_send(session_actor_, utility::notification_atom_v, notify); + } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_send(session_actor_, utility::notification_atom_v, notify); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -1352,8 +1409,8 @@ void SessionModel::gatherMediaFor(const QModelIndex &index, const QModelIndexLis if (index.isValid()) { nlohmann::json &j = indexToData(index); - if (j.at("type") == "Playlist" or j.at("type") == "ContactSheet" or j.at("type") == "Subset" or - j.at("type") == "Timeline") { + if (j.at("type") == "Playlist" or j.at("type") == "ContactSheet" or + j.at("type") == "Subset" or j.at("type") == "Timeline") { auto actor = actorFromString(system(), j.at("actor")); if (actor) { UuidList uv; @@ -1542,7 +1599,8 @@ void SessionModel::sortByMediaDisplayInfo( 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 == "ContactSheet" or type == "Playlist" or type == "Timeline")) { + if (actor and (type == "Subset" or type == "ContactSheet" or type == "Playlist" or + type == "Timeline")) { anon_send( actor, playlist::sort_by_media_display_info_atom_v, diff --git a/src/ui/qml/session/src/session_model_timeline_ui.cpp b/src/ui/qml/session/src/session_model_timeline_ui.cpp index d3aab339f..2c8f514f8 100644 --- a/src/ui/qml/session/src/session_model_timeline_ui.cpp +++ b/src/ui/qml/session/src/session_model_timeline_ui.cpp @@ -40,6 +40,28 @@ void SessionModel::setTimelineFocus( } } +void SessionModel::setTimelineSelection( + const QModelIndex &timeline, const QModelIndexList &indexes) const { + try { + UuidActorVector selection; + + if (timeline.isValid()) { + auto tactor = actorFromQString(system(), timeline.data(actorRole).toString()); + + for (auto &i : indexes) { + auto uuid = UuidFromQUuid(i.data(idRole).toUuid()); + auto actor = actorFromQString(system(), i.data(actorRole).toString()); + + selection.emplace_back(UuidActor(uuid, actor)); + } + + anon_send(tactor, timeline::item_selection_atom_v, selection); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + QRect SessionModel::timelineRect(const QModelIndexList &indexes) const { auto result = QRect(); auto box_inited = false; @@ -306,6 +328,100 @@ int SessionModel::getTimelineFrameFromClip( return result; } +int SessionModel::getNextTimelineClipFrame(const QModelIndex &timelineIndex, const int frame) { + auto result = frame; + + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + auto frstart = FrameRate(item.rate().to_flicks() * frame); + auto frdur = FrameRate( + item.front().trimmed_duration().to_flicks() - + (frstart.to_flicks() - item.front().trimmed_start().to_flicks())); + + scoped_actor sys{system()}; + try { + auto ri = request_receive>>( + *sys, tactor, timeline::bake_atom_v, frstart, frdur); + + if (not ri.empty()) { + auto fuuid = Uuid(); + if (ri.front()) + fuuid = ri.front()->first.uuid(); + // scan.. + auto diff = FrameRate(); + for (const auto &i : ri) { + if ((fuuid.is_null() and i) or (not fuuid.is_null() and not i) or + (not fuuid.is_null() and i and fuuid != i->first.uuid())) { + + result += diff / item.rate().to_flicks(); + break; + } + diff += item.rate(); + } + } + } catch (...) { + } + } + + return result; +} + +int SessionModel::getPreviousTimelineClipFrame( + const QModelIndex &timelineIndex, const int frame) { + auto result = 0; + + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + auto frstart = item.front().trimmed_start(); + auto frdur = FrameRate( + FrameRate(item.rate().to_flicks() * frame).to_flicks() - frstart.to_flicks()); + + scoped_actor sys{system()}; + try { + auto ri = request_receive>>( + *sys, tactor, timeline::bake_atom_v, frstart, frdur); + + // slightly different behavior, as we jump to start of current clip before + + if (not ri.empty()) { + + // scan.. + auto diff = item.rate() * ri.size(); + auto fuuid = Uuid(); + if (ri.back()) + fuuid = ri.back()->first.uuid(); + + for (auto it = ri.rbegin(); it != ri.rend(); ++it) { + if ((fuuid.is_null() and *it) or (not fuuid.is_null() and not *it) or + (not fuuid.is_null() and *it and fuuid != (*it)->first.uuid()) + + ) { + // handle at front of clip.. + if (frame == diff / item.rate().to_flicks()) { + auto prev = std::next(it, 1); + if (prev != ri.rend()) { + fuuid = Uuid(); + if (*prev) + fuuid = (*prev)->first.uuid(); + } + } else { + result = diff / item.rate().to_flicks(); + break; + } + } + diff -= item.rate(); + } + } + } catch (...) { + } + } + + return result; +} + + QModelIndex SessionModel::getTimelineClipIndex(const QModelIndex &timelineIndex, const int frame) { auto result = QModelIndex(); @@ -419,7 +535,6 @@ QModelIndexList SessionModel::getTimelineAudioClipIndexesFromRect( skipLocked); } - QModelIndex SessionModel::getTimelineIndex(const QModelIndex &index) const { try { if (index.isValid()) { @@ -436,6 +551,20 @@ QModelIndex SessionModel::getTimelineIndex(const QModelIndex &index) const { return QModelIndex(); } +QVariantList SessionModel::getTimelineExportTypes() const { + auto result = QVariantList(); + caf::scoped_actor sys(system()); + auto global = system().registry().template get(global_registry); + auto epa = request_receive(*sys, global, global::get_python_atom_v); + + auto supported = + request_receive>(*sys, epa, session::export_atom_v); + for (const auto &i : supported) + result.push_back(QVariant::fromValue(QStringFromStd(to_upper(i) + " (*." + i + ")"))); + + return result; +} + QFuture SessionModel::exportOTIO(const QModelIndex &timeline, const QUrl &path, const QString &type) { @@ -2287,150 +2416,156 @@ void SessionModel::beginTimelineItemDrag( auto orig_data = mapFromValue(i.data(userDataRole)); auto data = orig_data; - // reset all - data["adjust_duration"] = 0; - data["adjust_start"] = 0; - data["drag_value"] = 0; - data["is_adjusting_start"] = false; - data["is_adjusting_duration"] = false; - data["is_adjusting_preceeding"] = false; - data["is_adjusting_anteceeding"] = false; - data["is_anteceeding_track"] = false; - data["adjust_track"] = 0; - data["adjust_preceeding_gap"] = 0; - data["adjust_anteceeding_gap"] = 0; - data["move_x"] = 0; - data["move_Y"] = 0; - data["is_floating"] = false; - - if (mode == QString("roll")) { - data["is_adjusting_start"] = true; - } else if (mode == QString("leftleft")) { - data["is_adjusting_start"] = true; - data["is_adjusting_duration"] = true; - - auto preceeding = index(i.row() - 1, 0, i.parent()); - auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); - auto data2 = orig_data2; + data["drag_value"] = 0; - data2["adjust_duration"] = 0; - data2["is_adjusting_duration"] = true; + if (mode == QString("track")) { + data["move_y"] = 0; + } else { + // reset all + data["adjust_duration"] = 0; + data["adjust_start"] = 0; + data["is_adjusting_start"] = false; + data["is_adjusting_duration"] = false; + data["is_adjusting_preceeding"] = false; + data["is_adjusting_anteceeding"] = false; + data["is_anteceeding_track"] = false; + data["adjust_track"] = 0; + data["adjust_preceeding_gap"] = 0; + data["adjust_anteceeding_gap"] = 0; + data["move_x"] = 0; + data["move_y"] = 0; + data["is_floating"] = false; - if (orig_data2 != data2) - setData(preceeding, mapFromValue(data2), userDataRole); - } else if (mode == QString("rightright")) { - data["is_adjusting_duration"] = true; + if (mode == QString("roll")) { + data["is_adjusting_start"] = true; + } else if (mode == QString("leftleft")) { + data["is_adjusting_start"] = true; + data["is_adjusting_duration"] = true; - auto anteceeding = index(i.row() + 1, 0, i.parent()); - auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); - auto data2 = orig_data2; + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; - data2["adjust_duration"] = 0; - data2["adjust_start"] = 0; - data2["drag_value"] = 0; - data2["is_adjusting_start"] = true; - data2["is_adjusting_duration"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; - if (orig_data2 != data2) - setData(anteceeding, mapFromValue(data2), userDataRole); - } else if (mode == QString("left")) { - data["is_adjusting_start"] = true; - data["is_adjusting_duration"] = true; + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } else if (mode == QString("rightright")) { + data["is_adjusting_duration"] = true; - if (isOverwrite) { - data["is_floating"] = true; - } else { - auto preceeding = index(i.row() - 1, 0, i.parent()); - if (preceeding.isValid()) { - auto type = preceeding.data(typeRole).toString(); - if (type == QString("Gap")) { - data["is_adjusting_preceeding"] = true; - auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); - auto data2 = orig_data2; + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; - data2["adjust_duration"] = 0; - data2["is_adjusting_duration"] = true; + data2["adjust_duration"] = 0; + data2["adjust_start"] = 0; + data2["drag_value"] = 0; + data2["is_adjusting_start"] = true; + data2["is_adjusting_duration"] = true; - if (orig_data2 != data2) - setData(preceeding, mapFromValue(data2), userDataRole); - } - } - } - } else if (mode == QString("right")) { - data["is_adjusting_duration"] = true; + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (mode == QString("left")) { + data["is_adjusting_start"] = true; + data["is_adjusting_duration"] = true; - if (not isRipple) { if (isOverwrite) { data["is_floating"] = true; } else { - auto anteceeding = index(i.row() + 1, 0, i.parent()); - if (anteceeding.isValid()) { - auto type = anteceeding.data(typeRole).toString(); + auto preceeding = index(i.row() - 1, 0, i.parent()); + if (preceeding.isValid()) { + auto type = preceeding.data(typeRole).toString(); if (type == QString("Gap")) { - auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + data["is_adjusting_preceeding"] = true; + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); auto data2 = orig_data2; - data["is_adjusting_anteceeding"] = true; - data2["adjust_duration"] = 0; data2["is_adjusting_duration"] = true; if (orig_data2 != data2) - setData(anteceeding, mapFromValue(data2), userDataRole); + setData(preceeding, mapFromValue(data2), userDataRole); } - } else { - data["is_anteceeding_track"] = true; } } - } - } else if (mode == QString("middle")) { - if (isOverwrite) { - data["is_floating"] = true; - } else { - auto preceeding = index(i.row() - 1, 0, i.parent()); - auto anteceeding = index(i.row() + 1, 0, i.parent()); + } else if (mode == QString("right")) { + data["is_adjusting_duration"] = true; - auto preceeding_type = preceeding.isValid() - ? preceeding.data(typeRole).toString() - : QString("Track"); - auto anteceeding_type = anteceeding.isValid() - ? anteceeding.data(typeRole).toString() - : QString("Track"); + if (not isRipple) { + if (isOverwrite) { + data["is_floating"] = true; + } else { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + if (anteceeding.isValid()) { + auto type = anteceeding.data(typeRole).toString(); + if (type == QString("Gap")) { + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; - if (preceeding_type == QString("Gap")) { - auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); - auto data2 = orig_data2; + data["is_adjusting_anteceeding"] = true; - data["is_adjusting_preceeding"] = true; - data2["adjust_duration"] = 0; - data2["is_adjusting_duration"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; - if (orig_data2 != data2) - setData(preceeding, mapFromValue(data2), userDataRole); - } else { - data["is_adjusting_preceeding"] = true; + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } + } else { + data["is_anteceeding_track"] = true; + } + } } + } else if (mode == QString("middle")) { + if (isOverwrite) { + data["is_floating"] = true; + } else { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto anteceeding = index(i.row() + 1, 0, i.parent()); - if (not isRipple) { - if (anteceeding_type == QString("Gap")) { - auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto preceeding_type = preceeding.isValid() + ? preceeding.data(typeRole).toString() + : QString("Track"); + auto anteceeding_type = anteceeding.isValid() + ? anteceeding.data(typeRole).toString() + : QString("Track"); + + if (preceeding_type == QString("Gap")) { + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); auto data2 = orig_data2; - data["is_adjusting_anteceeding"] = true; - data2["adjust_duration"] = 0; - data2["is_adjusting_duration"] = true; + data["is_adjusting_preceeding"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; if (orig_data2 != data2) - setData(anteceeding, mapFromValue(data2), userDataRole); - } else if (anteceeding_type != QString("Track")) { - data["is_adjusting_anteceeding"] = true; + setData(preceeding, mapFromValue(data2), userDataRole); } else { - data["is_anteceeding_track"] = true; + data["is_adjusting_preceeding"] = true; + } + + if (not isRipple) { + if (anteceeding_type == QString("Gap")) { + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + + data["is_adjusting_anteceeding"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (anteceeding_type != QString("Track")) { + data["is_adjusting_anteceeding"] = true; + } else { + data["is_anteceeding_track"] = true; + } } } } } + if (orig_data != data) setData(i, mapFromValue(data), userDataRole); } @@ -2453,7 +2588,19 @@ void SessionModel::updateTimelineItemDrag( auto orig_data = mapFromValue(i.data(userDataRole)); auto data = orig_data; - if (mode == QString("roll")) { + + if (mode == QString("track")) { + + if (i.row() + trackChange < 0) + trackChange = -i.row(); + else if (i.row() + trackChange > rowCount(i.parent())) { + trackChange = (rowCount(i.parent()) - 1) - i.row(); + } + + data["move_y"] = trackChange; + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } else if (mode == QString("roll")) { auto trimmedStart = i.data(trimmedStartRole).toInt(); auto availableStart = i.data(availableStartRole).toInt(); auto trimmedDuration = i.data(trimmedDurationRole).toInt(); @@ -2625,7 +2772,7 @@ void SessionModel::endTimelineItemDrag( data["adjust_anteceeding_gap"] = 0; data["adjust_track"] = 0; data["move_x"] = 0; - data["move_Y"] = 0; + data["move_y"] = 0; data["is_floating"] = false; @@ -2635,128 +2782,38 @@ void SessionModel::endTimelineItemDrag( } }; - if (mode == QString("roll")) { - auto trimmedStart = i.data(trimmedStartRole).toInt(); - setData( - i, - QVariant::fromValue(trimmedStart + data["adjust_start"].get()), - activeStartRole); - } else if (mode == "leftleft") { - auto preceeding = index(i.row() - 1, 0, i.parent()); - auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); - auto data2 = orig_data2; - auto trimmedStart = i.data(trimmedStartRole).toInt(); - auto trimmedDuration = i.data(trimmedDurationRole).toInt(); - auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and - data["is_adjusting_start"] - ? data["adjust_start"].get() - : 0); - auto durationFrame = - trimmedDuration + - (not data["is_adjusting_duration"].is_null() and data["is_adjusting_duration"] - ? data["adjust_duration"].get() - : 0); - auto trimmedDuration2 = preceeding.data(trimmedDurationRole).toInt(); - auto durationFrame2 = - trimmedDuration2 + - (not data2["is_adjusting_duration"].is_null() and data2["is_adjusting_duration"] - ? data2["adjust_duration"].get() - : 0); - - setData(i, startFrame, activeStartRole); - setData(i, durationFrame, activeDurationRole); - setData(preceeding, durationFrame2, activeDurationRole); - - data2["is_adjusting_start"] = false; - data2["is_adjusting_duration"] = false; - data2["adjust_start"] = 0; - data2["adjust_duration"] = 0; - data2["drag_value"] = 0; - - if (orig_data2 != data2) - setData(preceeding, mapFromValue(data2), userDataRole); - } else if (mode == "rightright") { - auto anteceeding = index(i.row() + 1, 0, i.parent()); - auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); - auto data2 = orig_data2; - auto trimmedDuration = i.data(trimmedDurationRole).toInt(); - auto durationFrame = - trimmedDuration + - (not data["is_adjusting_duration"].is_null() and data["is_adjusting_duration"] - ? data["adjust_duration"].get() - : 0); - auto trimmedStart2 = anteceeding.data(trimmedStartRole).toInt(); - auto trimmedDuration2 = anteceeding.data(trimmedDurationRole).toInt(); - auto startFrame2 = trimmedStart2 + (not data2["is_adjusting_start"].is_null() and - data2["is_adjusting_start"] - ? data2["adjust_start"].get() - : 0); - auto durationFrame2 = - trimmedDuration2 + - (not data2["is_adjusting_duration"].is_null() and data2["is_adjusting_duration"] - ? data2["adjust_duration"].get() - : 0); - - setData(i, durationFrame, activeDurationRole); - setData(anteceeding, startFrame2, activeStartRole); - setData(anteceeding, durationFrame2, activeDurationRole); - - data2["is_adjusting_start"] = false; - data2["is_adjusting_duration"] = false; - data2["adjust_start"] = 0; - data2["adjust_duration"] = 0; - data2["drag_value"] = 0; - - if (orig_data2 != data2) - setData(anteceeding, mapFromValue(data2), userDataRole); - } else if (mode == "left") { - auto trimmedStart = i.data(trimmedStartRole).toInt(); - auto trimmedDuration = i.data(trimmedDurationRole).toInt(); - auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and - data["is_adjusting_start"] - ? data["adjust_start"].get() - : 0); - auto durationFrame = - trimmedDuration + - (not data["is_adjusting_duration"].is_null() and data["is_adjusting_duration"] - ? data["adjust_duration"].get() - : 0); - setData(i, startFrame, activeStartRole); - setData(i, durationFrame, activeDurationRole); - - // remove material we overwrote - if (isOverwrite) { - // we got smaller, either extend or insert gap - if (durationFrame < trimmedDuration) { - auto gap = trimmedDuration - durationFrame; - auto fps = i.data(rateFPSRole).toDouble(); - - auto previous = index(i.row() - 1, 0, i.parent()); - - if (not previous.isValid() or - previous.data(typeRole) != QVariant::fromValue(QString("Gap"))) { - flushChange(); - insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); - } else { - auto gduration = previous.data(trimmedDurationRole).toInt(); - setData(previous, gduration + gap, activeDurationRole); - setData(previous, gduration + gap, availableDurationRole); - } - } else if (durationFrame > trimmedDuration and i.row()) { - // expanding.. - // delete preceeding - auto start = - std::max(0, startFrameInParent(i) - (durationFrame - trimmedDuration)); - flushChange(); - removeTimelineItems( - getTimelineTrackIndex(i), start, startFrameInParent(i) - start); - } + if (mode == QString("track")) { + auto val = data["move_y"].get(); + if (val != 0) { + moveTimelineItem(i, val > 0 ? val + 1 : val); + data["move_y"] = 0; } - - if (data["is_adjusting_preceeding"]) { - auto preceeding = index(i.row() - 1, 0, i.parent()); - auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); - auto data2 = orig_data2; + if (orig_data != data) { + setData(i, mapFromValue(data), userDataRole); + orig_data = data; + } + } else { + if (mode == QString("roll")) { + auto trimmedStart = i.data(trimmedStartRole).toInt(); + setData( + i, + QVariant::fromValue(trimmedStart + data["adjust_start"].get()), + activeStartRole); + } else if (mode == "leftleft") { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedStart = i.data(trimmedStartRole).toInt(); + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and + data["is_adjusting_start"] + ? data["adjust_start"].get() + : 0); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); auto trimmedDuration2 = preceeding.data(trimmedDurationRole).toInt(); auto durationFrame2 = trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and @@ -2764,296 +2821,413 @@ void SessionModel::endTimelineItemDrag( ? data2["adjust_duration"].get() : 0); - if (durationFrame2 == 0) { - flushChange(); - removeTimelineItems(QModelIndexList({preceeding})); - } else { - setData(preceeding, durationFrame2, activeDurationRole); - setData(preceeding, durationFrame2, availableDurationRole); - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - if (orig_data2 != data2) - setData(preceeding, mapFromValue(data2), userDataRole); - } - } else if (data["adjust_preceeding_gap"] > 0) { - auto gap = data["adjust_preceeding_gap"].get(); - auto fps = i.data(rateFPSRole).toDouble(); - flushChange(); - insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); - } - } else if (mode == "right") { - auto trimmedDuration = i.data(trimmedDurationRole).toInt(); - auto durationFrame = - trimmedDuration + - (not data["is_adjusting_duration"].is_null() and data["is_adjusting_duration"] - ? data["adjust_duration"].get() - : 0); - setData(i, durationFrame, activeDurationRole); - - // remove material we overwrote - if (isOverwrite) { - // only remove if we're not the last item. - if (durationFrame > trimmedDuration and rowCount(i.parent()) - 1 != i.row()) { - flushChange(); + setData(i, startFrame, activeStartRole); + setData(i, durationFrame, activeDurationRole); + setData(preceeding, durationFrame2, activeDurationRole); - removeTimelineItems( - getTimelineTrackIndex(i), - startFrameInParent(i) + durationFrame, - durationFrame - trimmedDuration); - } else if ( - durationFrame < trimmedDuration and rowCount(i.parent()) - 1 != i.row()) { - auto gap = trimmedDuration - durationFrame; - auto fps = i.data(rateFPSRole).toDouble(); + data2["is_adjusting_start"] = false; + data2["is_adjusting_duration"] = false; + data2["adjust_start"] = 0; + data2["adjust_duration"] = 0; + data2["drag_value"] = 0; - // check next item isn't a gap.. - auto next_item = index(i.row() + 1, 0, i.parent()); - if (next_item.data(typeRole) == QVariant::fromValue(QString("Gap"))) { - auto gduration = next_item.data(trimmedDurationRole).toInt(); - setData(next_item, gduration + gap, activeDurationRole); - setData(next_item, gduration + gap, availableDurationRole); - } else { - flushChange(); - insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); - } - } - } - - if (data["is_adjusting_anteceeding"]) { - auto anteceeding = index(i.row() + 1, 0, i.parent()); - auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); - auto data2 = orig_data2; + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } else if (mode == "rightright") { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + auto trimmedStart2 = anteceeding.data(trimmedStartRole).toInt(); auto trimmedDuration2 = anteceeding.data(trimmedDurationRole).toInt(); + auto startFrame2 = + trimmedStart2 + + (not data2["is_adjusting_start"].is_null() and data2["is_adjusting_start"] + ? data2["adjust_start"].get() + : 0); auto durationFrame2 = trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and data2["is_adjusting_duration"] ? data2["adjust_duration"].get() : 0); - if (durationFrame2 == 0) { - removeTimelineItems(QModelIndexList({anteceeding})); - } else { - setData(anteceeding, durationFrame2, activeDurationRole); - setData(anteceeding, durationFrame2, availableDurationRole); + setData(i, durationFrame, activeDurationRole); + setData(anteceeding, startFrame2, activeStartRole); + setData(anteceeding, durationFrame2, activeDurationRole); - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - if (orig_data2 != data2) - setData(anteceeding, mapFromValue(data2), userDataRole); - } - } else if (data["adjust_anteceeding_gap"] > 0) { - auto gap = data["adjust_anteceeding_gap"].get(); - auto fps = i.data(rateFPSRole).toDouble(); + data2["is_adjusting_start"] = false; + data2["is_adjusting_duration"] = false; + data2["adjust_start"] = 0; + data2["adjust_duration"] = 0; + data2["drag_value"] = 0; - insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); - } - } else if (mode == "middle") { - auto preceeding = index(i.row() - 1, 0, i.parent()); - auto anteceeding = index(i.row() + 1, 0, i.parent()); - auto preceeding_type = - preceeding.isValid() ? preceeding.data(typeRole).toString() : QString("Track"); - auto anteceeding_type = anteceeding.isValid() - ? anteceeding.data(typeRole).toString() - : QString("Track"); + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (mode == "left") { + auto trimmedStart = i.data(trimmedStartRole).toInt(); + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and + data["is_adjusting_start"] + ? data["adjust_start"].get() + : 0); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + setData(i, startFrame, activeStartRole); + setData(i, durationFrame, activeDurationRole); + + // remove material we overwrote + if (isOverwrite) { + // we got smaller, either extend or insert gap + if (durationFrame < trimmedDuration) { + auto gap = trimmedDuration - durationFrame; + auto fps = i.data(rateFPSRole).toDouble(); - if (isOverwrite) { - auto frame_offset = data["move_x"].get(); - auto track_offset = data["move_y"].get(); - auto duration = i.data(trimmedDurationRole).toInt(); - auto start = startFrameInParent(i); - - // order of indexes is important, - // check for correct order .. - if (i == pitems.front()) { - auto sorted = QModelIndexList(items.begin(), items.end()); - - if (frame_offset > 0) { - std::sort( - sorted.begin(), sorted.end(), [](QModelIndex &a, QModelIndex &b) { - return a.row() > b.row(); - }); - - if (sorted != items) { - endTimelineItemDrag(sorted, mode, isOverwrite); - return; - } - } else { - std::sort( - sorted.begin(), sorted.end(), [](QModelIndex &a, QModelIndex &b) { - return a.row() < b.row(); - }); - - if (sorted != items) { - endTimelineItemDrag(sorted, mode, isOverwrite); - return; + auto previous = index(i.row() - 1, 0, i.parent()); + + if (not previous.isValid() or + previous.data(typeRole) != QVariant::fromValue(QString("Gap"))) { + flushChange(); + insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); + } else { + auto gduration = previous.data(trimmedDurationRole).toInt(); + setData(previous, gduration + gap, activeDurationRole); + setData(previous, gduration + gap, availableDurationRole); } + } else if (durationFrame > trimmedDuration and i.row()) { + // expanding.. + // delete preceeding + auto start = std::max( + 0, startFrameInParent(i) - (durationFrame - trimmedDuration)); + flushChange(); + removeTimelineItems( + getTimelineTrackIndex(i), start, startFrameInParent(i) - start); } } - // ouch... - if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { - auto data2 = mapFromValue(preceeding.data(userDataRole)); - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - setData(preceeding, mapFromValue(data2), userDataRole); + if (data["is_adjusting_preceeding"]) { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration2 = preceeding.data(trimmedDurationRole).toInt(); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + if (durationFrame2 == 0) { + flushChange(); + removeTimelineItems(QModelIndexList({preceeding})); + } else { + setData(preceeding, durationFrame2, activeDurationRole); + setData(preceeding, durationFrame2, availableDurationRole); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } + } else if (data["adjust_preceeding_gap"] > 0) { + auto gap = data["adjust_preceeding_gap"].get(); + auto fps = i.data(rateFPSRole).toDouble(); + flushChange(); + insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); } - if (data["is_adjusting_anteceeding"] and anteceeding_type == QString("Gap")) { - auto data2 = mapFromValue(anteceeding.data(userDataRole)); - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (mode == "right") { + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + setData(i, durationFrame, activeDurationRole); + + // remove material we overwrote + if (isOverwrite) { + // only remove if we're not the last item. + if (durationFrame > trimmedDuration and + rowCount(i.parent()) - 1 != i.row()) { + flushChange(); + + removeTimelineItems( + getTimelineTrackIndex(i), + startFrameInParent(i) + durationFrame, + durationFrame - trimmedDuration); + } else if ( + durationFrame < trimmedDuration and + rowCount(i.parent()) - 1 != i.row()) { + auto gap = trimmedDuration - durationFrame; + auto fps = i.data(rateFPSRole).toDouble(); + + // check next item isn't a gap.. + auto next_item = index(i.row() + 1, 0, i.parent()); + if (next_item.data(typeRole) == QVariant::fromValue(QString("Gap"))) { + auto gduration = next_item.data(trimmedDurationRole).toInt(); + setData(next_item, gduration + gap, activeDurationRole); + setData(next_item, gduration + gap, availableDurationRole); + } else { + flushChange(); + insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); + } + } } - flushChange(); - // this involves a ton of track modifications.. - if (track_offset) { - auto target_track_index = - index(i.parent().row() + track_offset, 0, i.parent().parent()); + if (data["is_adjusting_anteceeding"]) { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration2 = anteceeding.data(trimmedDurationRole).toInt(); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + if (durationFrame2 == 0) { + removeTimelineItems(QModelIndexList({anteceeding})); + } else { + setData(anteceeding, durationFrame2, activeDurationRole); + setData(anteceeding, durationFrame2, availableDurationRole); - if (track_offset < 0) { - for (auto tr = 0; tr >= track_offset; tr--) { - target_track_index = - index(i.parent().row() + tr, 0, i.parent().parent()); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } + } else if (data["adjust_anteceeding_gap"] > 0) { + auto gap = data["adjust_anteceeding_gap"].get(); + auto fps = i.data(rateFPSRole).toDouble(); - if (not target_track_index.isValid()) { - // create new track - auto type_role = i.parent().data(typeRole).toString(); + insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); + } + } else if (mode == "middle") { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto preceeding_type = preceeding.isValid() + ? preceeding.data(typeRole).toString() + : QString("Track"); + auto anteceeding_type = anteceeding.isValid() + ? anteceeding.data(typeRole).toString() + : QString("Track"); - target_track_index = insertRowsSync( - type_role == "Video Track" ? 0 - : rowCount(i.parent().parent()), - 1, - type_role, - type_role, - i.parent().parent())[0]; + if (isOverwrite) { + auto frame_offset = data["move_x"].get(); + auto track_offset = data["move_y"].get(); + auto duration = i.data(trimmedDurationRole).toInt(); + auto start = startFrameInParent(i); + + // order of indexes is important, + // check for correct order .. + if (i == pitems.front()) { + auto sorted = QModelIndexList(items.begin(), items.end()); + + if (frame_offset > 0) { + std::sort( + sorted.begin(), + sorted.end(), + [](QModelIndex &a, QModelIndex &b) { + return a.row() > b.row(); + }); + + if (sorted != items) { + endTimelineItemDrag(sorted, mode, isOverwrite); + return; } - } - } else if (track_offset > 0) { - for (auto tr = 0; tr <= track_offset; tr++) { - target_track_index = - index(i.parent().row() + tr, 0, i.parent().parent()); - - if (not target_track_index.isValid()) { - // create new track - auto type_role = i.parent().data(typeRole).toString(); - - target_track_index = insertRowsSync( - type_role == "Video Track" ? 0 - : rowCount(i.parent().parent()), - 1, - type_role, - type_role, - i.parent().parent())[0]; + } else { + std::sort( + sorted.begin(), + sorted.end(), + [](QModelIndex &a, QModelIndex &b) { + return a.row() < b.row(); + }); + + if (sorted != items) { + endTimelineItemDrag(sorted, mode, isOverwrite); + return; } } } - auto new_clips = - duplicateTimelineClipsTo(QModelIndexList({i}), target_track_index); - moveRangeTimelineItems( - getTimelineTrackIndex(new_clips[0].parent()), - startFrameInParent(new_clips[0]), - duration, - start + frame_offset, - false); - - removeTimelineItems(QModelIndexList({i})); - // wait for index to become invalidated.. - while (i.isValid()) { - QCoreApplication::processEvents( - QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, - 50); + // ouch... + if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { + auto data2 = mapFromValue(preceeding.data(userDataRole)); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(preceeding, mapFromValue(data2), userDataRole); } - - } else if (frame_offset) - moveRangeTimelineItems( - getTimelineTrackIndex(i.parent()), - start, - duration, - start + frame_offset, - false); - } else { - auto delete_preceeding = false; - auto delete_anteceeding = false; - auto adjustPreceedingGap = data["adjust_preceeding_gap"].get(); - auto adjustAnteceedingGap = data["adjust_anteceeding_gap"].get(); - auto insert_preceeding = - data["is_adjusting_preceeding"] and adjustPreceedingGap; - auto insert_anteceeding = - data["is_adjusting_anteceeding"] and adjustAnteceedingGap; - - // adjust duration of preceeding gap or delete - if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { - auto trimmedDuration = preceeding.data(trimmedDurationRole).toInt(); - auto data2 = mapFromValue(preceeding.data(userDataRole)); - auto preceedingDurationFrame = - trimmedDuration + (not data2["is_adjusting_duration"].is_null() and - data2["is_adjusting_duration"] - ? data2["adjust_duration"].get() - : 0); - if (preceedingDurationFrame) { - setData(preceeding, preceedingDurationFrame, activeDurationRole); - setData(preceeding, preceedingDurationFrame, availableDurationRole); - } else { - delete_preceeding = true; + if (data["is_adjusting_anteceeding"] and + anteceeding_type == QString("Gap")) { + auto data2 = mapFromValue(anteceeding.data(userDataRole)); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(anteceeding, mapFromValue(data2), userDataRole); } + flushChange(); - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - setData(preceeding, mapFromValue(data2), userDataRole); - } + // this involves a ton of track modifications.. + if (track_offset) { + auto target_track_index = + index(i.parent().row() + track_offset, 0, i.parent().parent()); + + if (track_offset < 0) { + for (auto tr = 0; tr >= track_offset; tr--) { + target_track_index = + index(i.parent().row() + tr, 0, i.parent().parent()); + + if (not target_track_index.isValid()) { + // create new track + auto type_role = i.parent().data(typeRole).toString(); + + target_track_index = insertRowsSync( + type_role == "Video Track" + ? 0 + : rowCount(i.parent().parent()), + 1, + type_role, + type_role, + i.parent().parent())[0]; + } + } + } else if (track_offset > 0) { + for (auto tr = 0; tr <= track_offset; tr++) { + target_track_index = + index(i.parent().row() + tr, 0, i.parent().parent()); + + if (not target_track_index.isValid()) { + // create new track + auto type_role = i.parent().data(typeRole).toString(); + + target_track_index = insertRowsSync( + type_role == "Video Track" + ? 0 + : rowCount(i.parent().parent()), + 1, + type_role, + type_role, + i.parent().parent())[0]; + } + } + } - // adjust duration of anteceeding gap or delete - if (data["is_adjusting_anteceeding"] and anteceeding_type == QString("Gap")) { - auto trimmedDuration = anteceeding.data(trimmedDurationRole).toInt(); - auto data2 = mapFromValue(anteceeding.data(userDataRole)); - auto anteceedingDurationFrame = - trimmedDuration + (not data2["is_adjusting_duration"].is_null() and - data2["is_adjusting_duration"] - ? data2["adjust_duration"].get() - : 0); - if (anteceedingDurationFrame) { - setData(anteceeding, anteceedingDurationFrame, activeDurationRole); - setData(anteceeding, anteceedingDurationFrame, availableDurationRole); - } else { - delete_anteceeding = true; - } - data2["is_adjusting_duration"] = false; - data2["adjust_duration"] = 0; - setData(anteceeding, mapFromValue(data2), userDataRole); - } + auto new_clips = + duplicateTimelineClipsTo(QModelIndexList({i}), target_track_index); + moveRangeTimelineItems( + getTimelineTrackIndex(new_clips[0].parent()), + startFrameInParent(new_clips[0]), + duration, + start + frame_offset, + false); + + removeTimelineItems(QModelIndexList({i})); + // wait for index to become invalidated.. + while (i.isValid()) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | + QEventLoop::ExcludeUserInputEvents, + 50); + } - flushChange(); - // spdlog::warn("insert_preceeding {} delete_preceeding {} insert_anteceeding {} - // delete_anteceeding {}", insert_preceeding, delete_preceeding, - // insert_anteceeding, delete_anteceeding ); - - // some operations are moves - if (insert_preceeding and delete_anteceeding) - moveTimelineItem(i, 1); - else if (delete_preceeding and insert_anteceeding) - moveTimelineItem(i, -1); - else { - if (delete_preceeding) - removeTimelineItems(QModelIndexList({preceeding})); + } else if (frame_offset) + moveRangeTimelineItems( + getTimelineTrackIndex(i.parent()), + start, + duration, + start + frame_offset, + false); + } else { + auto delete_preceeding = false; + auto delete_anteceeding = false; + auto adjustPreceedingGap = data["adjust_preceeding_gap"].get(); + auto adjustAnteceedingGap = data["adjust_anteceeding_gap"].get(); + auto insert_preceeding = + data["is_adjusting_preceeding"] and adjustPreceedingGap; + auto insert_anteceeding = + data["is_adjusting_anteceeding"] and adjustAnteceedingGap; + + // adjust duration of preceeding gap or delete + if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { + auto trimmedDuration = preceeding.data(trimmedDurationRole).toInt(); + auto data2 = mapFromValue(preceeding.data(userDataRole)); + auto preceedingDurationFrame = + trimmedDuration + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + if (preceedingDurationFrame) { + setData(preceeding, preceedingDurationFrame, activeDurationRole); + setData(preceeding, preceedingDurationFrame, availableDurationRole); + } else { + delete_preceeding = true; + } - if (delete_anteceeding) - removeTimelineItems(QModelIndexList({anteceeding})); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(preceeding, mapFromValue(data2), userDataRole); + } - if (insert_preceeding) { - auto fps = i.data(rateFPSRole).toDouble(); - insertTimelineGap(i.row(), i.parent(), adjustPreceedingGap, fps, "Gap"); + // adjust duration of anteceeding gap or delete + if (data["is_adjusting_anteceeding"] and + anteceeding_type == QString("Gap")) { + auto trimmedDuration = anteceeding.data(trimmedDurationRole).toInt(); + auto data2 = mapFromValue(anteceeding.data(userDataRole)); + auto anteceedingDurationFrame = + trimmedDuration + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + if (anteceedingDurationFrame) { + setData(anteceeding, anteceedingDurationFrame, activeDurationRole); + setData( + anteceeding, anteceedingDurationFrame, availableDurationRole); + } else { + delete_anteceeding = true; + } + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(anteceeding, mapFromValue(data2), userDataRole); } - if (insert_anteceeding) { - auto fps = i.data(rateFPSRole).toDouble(); - insertTimelineGap( - i.row() + 1, i.parent(), adjustAnteceedingGap, fps, "Gap"); + flushChange(); + // spdlog::warn("insert_preceeding {} delete_preceeding {} + // insert_anteceeding {} delete_anteceeding {}", insert_preceeding, + // delete_preceeding, insert_anteceeding, delete_anteceeding ); + + // some operations are moves + if (insert_preceeding and delete_anteceeding) + moveTimelineItem(i, 1); + else if (delete_preceeding and insert_anteceeding) + moveTimelineItem(i, -1); + else { + if (delete_preceeding) + removeTimelineItems(QModelIndexList({preceeding})); + + if (delete_anteceeding) + removeTimelineItems(QModelIndexList({anteceeding})); + + if (insert_preceeding) { + auto fps = i.data(rateFPSRole).toDouble(); + insertTimelineGap( + i.row(), i.parent(), adjustPreceedingGap, fps, "Gap"); + } + + if (insert_anteceeding) { + auto fps = i.data(rateFPSRole).toDouble(); + insertTimelineGap( + i.row() + 1, i.parent(), adjustAnteceedingGap, fps, "Gap"); + } } } } - } - flushChange(); + flushChange(); + } } } diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index 33474f74c..0327ead61 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -172,7 +172,8 @@ void SessionModel::forcePopulate( // spdlog::warn("{} {}", type, item.dump(2)); // if subset or playlist, trigger auto population of children - if (type == "Playlist" or type == "ContactSheet" or type == "Subset" or type == "Timeline") { + if (type == "Playlist" or type == "ContactSheet" or type == "Subset" or + type == "Timeline") { try { requestData( @@ -198,6 +199,16 @@ void SessionModel::forcePopulate( } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + } else if (type == "Media") { + if (tjson.count("media_status")) { + if (tjson.at("media_status").is_null()) + requestData( + QVariant::fromValue(QUuidFromUuid(tjson.at("id"))), + JSONTreeModel::Roles::idRole, + search_hint, + tjson, + Roles::mediaStatusRole); + } } else if (type == "MediaSource") { // for grab of path data.. // might be over kill ? @@ -608,7 +619,7 @@ 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()); + // spdlog::warn("mediaCountRole {}", ptree->size()); setData(parent_index.parent(), QVariant::fromValue(ptree->size()), mediaCountRole); } @@ -1043,6 +1054,7 @@ nlohmann::json SessionModel::playlistTreeToJson( n["children"].push_back(createEntry( R"({"type": "TimelineItem", "name": null, "actor_owner": null})"_json)); n["children"][2]["actor_owner"] = n["actor"]; + n["notification"] = nullptr; } n["children"].push_back(createEntry( @@ -1229,9 +1241,7 @@ void SessionModel::moveSelectionByIndex(const QModelIndex &index, const int offs } void SessionModel::updateSelection( - const QModelIndex &index, - const QModelIndexList &selection, - const QItemSelectionModel::SelectionFlags &qmode) { + const QModelIndex &index, const QModelIndexList &selection, const int qmode) { try { if (index.isValid()) { nlohmann::json &j = indexToData(index); @@ -1244,6 +1254,7 @@ void SessionModel::updateSelection( uv.emplace_back(UuidFromQUuid(i.data(actorUuidRole).toUuid())); } playhead::SelectionMode mode = playhead::SelectionMode::SM_NO_UPDATE; + switch (qmode) { case QItemSelectionModel::Clear: mode = playhead::SelectionMode::SM_CLEAR; diff --git a/src/ui/qml/studio/src/studio_ui.cpp b/src/ui/qml/studio/src/studio_ui.cpp index 583f96f7b..cc2f232cf 100644 --- a/src/ui/qml/studio/src/studio_ui.cpp +++ b/src/ui/qml/studio/src/studio_ui.cpp @@ -188,19 +188,22 @@ bool StudioUI::clearImageCache() { QUrl StudioUI::userDocsUrl() const { std::string docs_index = utility::xstudio_root("/../docs/index.html"); - if (docs_index.find("/") == 0) docs_index.erase(docs_index.begin()); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } QUrl StudioUI::apiDocsUrl() const { std::string docs_index = utility::xstudio_root("/../docs/api/index.html"); - if (docs_index.find("/") == 0) docs_index.erase(docs_index.begin()); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } QUrl StudioUI::releaseDocsUrl() const { std::string docs_index = utility::xstudio_root("/user_docs/release_notes/index.html"); - if (docs_index.find("/") == 0) docs_index.erase(docs_index.begin()); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } diff --git a/src/ui/qml/viewport/src/hotkey_ui.cpp b/src/ui/qml/viewport/src/hotkey_ui.cpp index 9b34b5a16..d8fe53ce1 100644 --- a/src/ui/qml/viewport/src/hotkey_ui.cpp +++ b/src/ui/qml/viewport/src/hotkey_ui.cpp @@ -285,52 +285,140 @@ HotkeyReferenceUI::HotkeyReferenceUI(QObject *parent) : QMLActor(parent) { init(CafSystemObject::get_actor_system()); } +HotkeyReferenceUI::~HotkeyReferenceUI() { + + if (exclusive_) { + exclusive_ = false; + notifyExclusiveChanged(); + } + +} + void HotkeyReferenceUI::init(caf::actor_system &system_) { QMLActor::init(system_); scoped_actor sys{system()}; + try { + + auto keyboard_manager = system().registry().template get(keyboard_events); + + auto hotkeys_config_events_group = utility::request_receive( + *sys, + keyboard_manager, + utility::get_event_group_atom_v, + keypress_monitor::hotkey_event_atom_v); + + anon_send(hotkeys_config_events_group, broadcast::join_broadcast_atom_v, as_actor()); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + set_message_handler([=](actor_companion * /*self_*/) -> message_handler { return { + [=](keypress_monitor::hotkey_event_atom, const std::vector &hotkeys) { + // hotkeys have been updated + for (const auto &hk : hotkeys) { + if (hk.hotkey_name() == StdFromQString(hotkey_name_)) { + if (QStringFromStd(hk.hotkey_sequence()) != sequence_) { + sequence_ = QStringFromStd(hk.hotkey_sequence()); + Q_EMIT sequenceChanged(); + } + QUuid uuid = QUuidFromUuid(hk.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) notifyExclusiveChanged(); + } + } + } + }, + [=](keypress_monitor::hotkey_event_atom, Hotkey &hotkey) { + // a hotkey has changed + if (hotkey.hotkey_name() == StdFromQString(hotkey_name_)) { + if (QStringFromStd(hotkey.hotkey_sequence()) != sequence_) { + sequence_ = QStringFromStd(hotkey.hotkey_sequence()); + Q_EMIT sequenceChanged(); + } + QUuid uuid = QUuidFromUuid(hotkey.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) notifyExclusiveChanged(); - }; + } + } + }, + [=](keypress_monitor::hotkey_event_atom, + const utility::Uuid kotkey_uuid, + const bool pressed, + const std::string &context, + const std::string &window) { + // actual hotkey pressed or release ... we ignore + if (pressed && QUuidFromUuid(kotkey_uuid) == hotkey_uuid_ && (context_.empty() || context_ == context)) { + activated(QStringFromStd(context)); + } + }}; }); } -void HotkeyReferenceUI::setHotkeyUuid(const QUuid &uuid) { +void HotkeyReferenceUI::setHotkeyName(const QString &name) { - uuid_ = uuid; - Q_EMIT hotkeyUuidChanged(); + if (hotkey_name_ == name) + return; + hotkey_name_ = name; + Q_EMIT hotkeyNameChanged(); - if (!uuid_.isNull()) { - try { + try { - scoped_actor sys{system()}; + scoped_actor sys{system()}; - auto keyboard_manager = - system().registry().template get(keyboard_events); + auto keyboard_manager = system().registry().template get(keyboard_events); - auto hotkeys_config_events_group = utility::request_receive( - *sys, - keyboard_manager, - utility::get_event_group_atom_v, - keypress_monitor::hotkey_event_atom_v); + auto hotkeys_config_events_group = utility::request_receive( + *sys, + keyboard_manager, + utility::get_event_group_atom_v, + keypress_monitor::hotkey_event_atom_v); - const auto hk = request_receive( - *sys, - keyboard_manager, - ui::keypress_monitor::hotkey_atom_v, - UuidFromQUuid(uuid_)); + const auto hk = request_receive( + *sys, keyboard_manager, ui::keypress_monitor::hotkey_atom_v, StdFromQString(name)); - QString seq = QStringFromStd(hk.hotkey_sequence()); - if (seq != sequence_) { - sequence_ = seq; - Q_EMIT sequenceChanged(); - } + QString seq = QStringFromStd(hk.hotkey_sequence()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + if (seq != sequence_) { + sequence_ = seq; + Q_EMIT sequenceChanged(); } + + QUuid uuid = QUuidFromUuid(hk.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) notifyExclusiveChanged(); + + } + + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } + +void HotkeyReferenceUI::setExclusive(const bool exclusive) { + + if (exclusive == exclusive_) return; + exclusive_ = exclusive; + emit exclusiveChanged(); + notifyExclusiveChanged(); + +} + +void HotkeyReferenceUI::notifyExclusiveChanged() { + + auto keyboard_manager = system().registry().template get(keyboard_events); + anon_send(keyboard_manager, keypress_monitor::watch_hotkey_atom_v, UuidFromQUuid(hotkey_uuid_), as_actor(), exclusive_); + +} diff --git a/src/ui/qml/viewport/src/qml_viewport.cpp b/src/ui/qml/viewport/src/qml_viewport.cpp index 9d0ea37c3..643d0cbfb 100644 --- a/src/ui/qml/viewport/src/qml_viewport.cpp +++ b/src/ui/qml/viewport/src/qml_viewport.cpp @@ -76,10 +76,7 @@ QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::A SIGNAL(imageBoundariesInViewportChanged())); connect( - renderer_actor, - SIGNAL(resolutionsChanged()), - this, - SIGNAL(imageResolutionsChanged())); + renderer_actor, SIGNAL(resolutionsChanged()), this, SIGNAL(imageResolutionsChanged())); connect( this, @@ -230,7 +227,6 @@ void QMLViewport::sync() { window()->devicePixelRatio()); renderer_actor->prepareRenderData(); - } void QMLViewport::cleanup() { @@ -450,9 +446,7 @@ void QMLViewport::showCursor() { } } -QVariantList QMLViewport::imageResolutions() { - return renderer_actor->imageResolutions(); -} +QVariantList QMLViewport::imageResolutions() { return renderer_actor->imageResolutions(); } QVariantList QMLViewport::imageBoundariesInViewport() { return renderer_actor->imageBoundariesInViewport(); diff --git a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp index ac32a034b..e37a67b53 100644 --- a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp +++ b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp @@ -25,11 +25,7 @@ QMLViewportRenderer::QMLViewportRenderer(QObject *parent) init_system(); } -QMLViewportRenderer::~QMLViewportRenderer() { - - delete viewport_renderer_; - -} +QMLViewportRenderer::~QMLViewportRenderer() { delete viewport_renderer_; } static QQuickWindow *win = nullptr; @@ -116,7 +112,8 @@ void QMLViewportRenderer::setSceneCoordinates( } void QMLViewportRenderer::prepareRenderData() { - if (viewport_renderer_) viewport_renderer_->prepare_render_data(); + if (viewport_renderer_) + viewport_renderer_->prepare_render_data(); } void QMLViewportRenderer::init_system() { @@ -149,9 +146,8 @@ void QMLViewportRenderer::make_xstudio_viewport() { }; viewport_renderer_->set_change_callback(callback); - viewport_qml_item_->setPlayheadUuid( - QUuidFromUuid(viewport_renderer_->playhead_uuid())); - + viewport_qml_item_->setPlayheadUuid(QUuidFromUuid(viewport_renderer_->playhead_uuid())); + /* The Viewport object provides a message handler that will process update events like new frame buffers coming from the playhead and so-on. Instead of being an actor itself, the Viewport is a regular class but it will process messages received by a parent actor (like @@ -212,7 +208,6 @@ void QMLViewportRenderer::make_xstudio_viewport() { holds a key down, and sends messages back to the viewport only once when a key is pressed and released */ keypress_monitor_ = system().registry().template get(keyboard_events); - } void QMLViewportRenderer::set_playhead(caf::actor playhead) { if (viewport_renderer_) @@ -245,9 +240,9 @@ bool QMLViewportRenderer::pointerEvent(const PointerEvent &e) { QVariantList QMLViewportRenderer::imageResolutions() const { QVariantList v; - const auto & image_resolutions = viewport_renderer_->image_resolutions(); - for (const auto &r: image_resolutions) { - v.append(QSize(r.x ,r.y)); + const auto &image_resolutions = viewport_renderer_->image_resolutions(); + for (const auto &r : image_resolutions) { + v.append(QSize(r.x, r.y)); } return v; } @@ -255,13 +250,11 @@ QVariantList QMLViewportRenderer::imageResolutions() const { QVariantList QMLViewportRenderer::imageBoundariesInViewport() const { QVariantList v; - const std::vector image_boxes = viewport_renderer_->image_bounds_in_viewport_pixels(); - for (const auto &box: image_boxes) { + const std::vector image_boxes = + viewport_renderer_->image_bounds_in_viewport_pixels(); + for (const auto &box : image_boxes) { QRectF imageBoundsInViewportPixels( - box.min.x, - box.min.y, - box.max.x - box.min.x, - box.max.y - box.min.y); + box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); v.append(imageBoundsInViewportPixels); } return v; diff --git a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp index 867d2aec3..f5aa90993 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -41,11 +41,10 @@ namespace { // More testing needed to check when this actually benefits us and on what // platforms, but for copying from memory mapped texture buffers into CPU // RAM it is required for high frame-rate offscreen rendering (e.g. 4k 60Hz -// display on SDI card) +// display on SDI card) // class ThreadedMemCopy { - public: - + public: ThreadedMemCopy() : num_threads_(8) { for (int i = 0; i < num_threads_; ++i) { threads_.emplace_back(std::thread(&ThreadedMemCopy::run, this)); @@ -54,7 +53,7 @@ class ThreadedMemCopy { ~ThreadedMemCopy() { - for (auto &t: threads_) { + for (auto &t : threads_) { // when any thread picks up an em { std::lock_guard lk(m); @@ -63,7 +62,7 @@ class ThreadedMemCopy { cv.notify_one(); } - for (auto &t: threads_) { + for (auto &t : threads_) { t.join(); } } @@ -77,15 +76,13 @@ class ThreadedMemCopy { void *src; size_t n; - void do_job() { - memcpy(dst, src, n); - } + void do_job() { memcpy(dst, src, n); } }; Job get_job() { std::unique_lock lk(m); if (queue.empty()) { - cv.wait(lk, [=]{ return !queue.empty(); }); + cv.wait(lk, [=] { return !queue.empty(); }); } auto rt = queue.front(); queue.pop_front(); @@ -94,7 +91,7 @@ class ThreadedMemCopy { void do_memcpy(void *_dst, void *_src, size_t n) { - size_t step = (((n / num_threads_) / 4096)+1) * 4096; + size_t step = (((n / num_threads_) / 4096) + 1) * 4096; uint8_t *dst = (uint8_t *)_dst; uint8_t *src = (uint8_t *)_src; @@ -107,27 +104,26 @@ class ThreadedMemCopy { cv.notify_one(); dst += step; src += step; - if (n < step) break; + if (n < step) + break; n -= step; } std::unique_lock lk(m); if (!queue.empty()) { - cv2.wait(lk, [=]{ return queue.empty(); }); + cv2.wait(lk, [=] { return queue.empty(); }); } - } - void run() - { - while(1) { - + void run() { + while (1) { + // this blocks until there is something in queue for us Job j = get_job(); - if (!j.dst) break; // exit - j.do_job(); + if (!j.dst) + break; // exit + j.do_job(); cv2.notify_one(); - } } @@ -187,8 +183,7 @@ OffscreenViewport::OffscreenViewport(const std::string name, bool include_qml_ov utility::JsonStore jsn; jsn["base"] = utility::JsonStore(); jsn["window_id"] = name; - viewport_renderer_ = new Viewport( - jsn, as_actor(), name); + viewport_renderer_ = new Viewport(jsn, as_actor(), name); /* Provide a callback so the Viewport can tell this class when some property of the viewport has changed and such events can be propagated to other QT components, for example */ @@ -567,7 +562,13 @@ void OffscreenViewport::exportToEXR(const media_reader::ImageBufPtr &buf, const header.dataWindow() = header.displayWindow() = box; header.compression() = Imf::PIZ_COMPRESSION; Imf::RgbaOutputFile outFile(utility::uri_to_posix_path(path).c_str(), header); - outFile.setFrameBuffer((Imf::Rgba *)buf->buffer(), 1, dim.x); + Imf::Rgba *bptr = (Imf::Rgba *)buf->buffer(); + bptr += (dim.y - 1) * dim.x; // move to final scanline + outFile.setFrameBuffer( + bptr, + 1, // pix stride + -dim.x // line stride (i.e.) step backwards through buffer + ); outFile.writePixels(dim.y); } @@ -762,10 +763,11 @@ bool OffscreenViewport::loadQMLOverlays() { void OffscreenViewport::renderToImageBuffer( const int w, const int h, - media_reader::ImageBufPtr &image, + media_reader::ImageBufPtr &destination_image, const ImageFormat format, const bool sync_fetch_playhead_image, - const utility::time_point &tp) { + const utility::time_point &tp, + const media_reader::ImageBufPtr &image_to_use) { auto t0 = utility::clock::now(); // ensure our GLContext is current @@ -795,12 +797,16 @@ void OffscreenViewport::renderToImageBuffer( Imath::V2i(w, h), 1.0f); - if (sync_fetch_playhead_image) { - media_reader::ImageBufPtr image = viewport_renderer_->get_onscreen_image(true); - viewport_renderer_->render(image); - } else if (tp != utility::time_point()) { - viewport_renderer_->render(tp); + if (image_to_use) { + viewport_renderer_->render(image_to_use); } else { + if (sync_fetch_playhead_image) { + viewport_renderer_->prepare_render_data(utility::clock::now(), true); + } else if (tp != utility::time_point()) { + viewport_renderer_->prepare_render_data(tp); + } else { + viewport_renderer_->prepare_render_data(); + } viewport_renderer_->render(); } @@ -816,31 +822,26 @@ void OffscreenViewport::renderToImageBuffer( root_qml_overlays_item_->setHeight(h); // convert the image boundary in the viewport into plain pixels - const std::vector image_boxes = viewport_renderer_->image_bounds_in_viewport_pixels(); + const std::vector image_boxes = + viewport_renderer_->image_bounds_in_viewport_pixels(); QVariantList v; - for (const auto &box: image_boxes) { + for (const auto &box : image_boxes) { QRectF imageBoundsInViewportPixels( - box.min.x, - box.min.y, - box.max.x - box.min.x, - box.max.y - box.min.y); + box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); v.append(imageBoundsInViewportPixels); } // these properties on XsOffscreenViewportOverlays mirror the same // properties provided by XsViewport - some overlay/HUD QML items access // these properties so they know how to compute their geometrty in // the QML coordinates to overlay the xSTUDIO image. - root_qml_overlays_item_->setProperty( - "imageBoundariesInViewport", v); + root_qml_overlays_item_->setProperty("imageBoundariesInViewport", v); const std::vector resolutions = viewport_renderer_->image_resolutions(); QVariantList rs; - for (const auto &r: resolutions) { + for (const auto &r : resolutions) { rs.append(QSize(r.x, r.y)); } - root_qml_overlays_item_->setProperty( - "imageResolutions", - rs); + root_qml_overlays_item_->setProperty("imageResolutions", rs); root_qml_overlays_item_->setProperty("sessionActorAddr", session_actor_addr_); quick_win_->setWidth(w); @@ -866,10 +867,10 @@ void OffscreenViewport::renderToImageBuffer( size_t pix_buf_size = w * h * format_to_bytes_per_pixel[vid_out_format_]; // init RGBA float array - image->allocate(pix_buf_size); - image->set_image_dimensions(Imath::V2i(w, h)); - image.when_to_display_ = utility::clock::now(); - image->params()["pixel_format"] = (int)format; + destination_image->allocate(pix_buf_size); + destination_image->set_image_dimensions(Imath::V2i(w, h)); + destination_image.when_to_display_ = utility::clock::now(); + destination_image->params()["pixel_format"] = (int)format; if (!pixel_buffer_object_) { glGenBuffers(1, &pixel_buffer_object_); @@ -904,7 +905,7 @@ void OffscreenViewport::renderToImageBuffer( auto t4 = utility::clock::now(); - threaded_memcpy(image->buffer(), mappedBuffer, pix_buf_size); + threaded_memcpy(destination_image->buffer(), mappedBuffer, pix_buf_size); // now mapped buffer contains the pixel data @@ -1045,7 +1046,8 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( media_reader::ImageBufPtr image(new media_reader::ImageBuffer()); - renderToImageBuffer(width, height, image, ImageFormat::RGBA_16F, true); + renderToImageBuffer( + width, height, image, ImageFormat::RGBA_16F, true, utility::clock::now(), image2); thumbnail::ThumbnailBufferPtr r = rgb96thumbFromHalfFloatImage(image); r->convert_to(format); return r; @@ -1059,10 +1061,12 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( const int width, const bool auto_scale, const bool show_annotations) { + if (!local_playhead_) { - auto a = caf::actor_cast(as_actor()); - local_playhead_ = - a->spawn("Offscreen Viewport Local Playhead"); + auto a = caf::actor_cast(as_actor()); + local_playhead_ = a->spawn( + "Offscreen Viewport Local Playhead", playhead::NO_AUDIO); + a->link_to(local_playhead_); } // first, set the local playhead to be our image source @@ -1088,11 +1092,12 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( 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"); + auto a = caf::actor_cast(as_actor()); + local_playhead_ = a->spawn( + "Offscreen Viewport Local Playhead", playhead::NO_AUDIO); a->link_to(local_playhead_); } + // first, set the local playhead to be our image source viewport_renderer_->set_playhead(local_playhead_); diff --git a/src/ui/qt/viewport_widget/src/viewport_widget.cpp b/src/ui/qt/viewport_widget/src/viewport_widget.cpp index 4dce20ef2..12f72c39a 100644 --- a/src/ui/qt/viewport_widget/src/viewport_widget.cpp +++ b/src/ui/qt/viewport_widget/src/viewport_widget.cpp @@ -39,9 +39,7 @@ void ViewportGLWidget::init(caf::actor_system &system) { utility::JsonStore jsn; jsn["base"] = utility::JsonStore(); - the_viewport_.reset(new ui::viewport::Viewport( - jsn, - as_actor())); + the_viewport_.reset(new ui::viewport::Viewport(jsn, as_actor())); auto callback = [this](auto &&PH1) { receive_change_notification(std::forward(PH1)); diff --git a/src/ui/viewport/src/keypress_monitor.cpp b/src/ui/viewport/src/keypress_monitor.cpp index c90acb80f..cf63549d4 100644 --- a/src/ui/viewport/src/keypress_monitor.cpp +++ b/src/ui/viewport/src/keypress_monitor.cpp @@ -110,6 +110,24 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto actor_grabbing_all_mouse_input_.begin(), actor); } }, + + [=](watch_hotkey_atom, const utility::Uuid &hk_uuid, caf::actor watcher) { + + auto p = active_hotkeys_.find(hk_uuid); + if (p != active_hotkeys_.end()) { + p->second.add_watcher(caf::actor_cast(watcher)); + } + + }, + + [=](watch_hotkey_atom, const utility::Uuid &hk_uuid, caf::actor watcher, bool exclusive_watcher) { + + auto p = active_hotkeys_.find(hk_uuid); + if (p != active_hotkeys_.end()) { + p->second.exclusive_watcher(caf::actor_cast(watcher), exclusive_watcher); + } + + }, [=](register_hotkey_atom, const Hotkey &hk) { if (active_hotkeys_.find(hk.uuid()) != active_hotkeys_.end()) { @@ -148,6 +166,17 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto return make_error(xstudio_error::error, "Invalid hotkey uuid"); }, + [=](hotkey_atom, const std::string &hotkey_name) -> result { + auto p = active_hotkeys_.begin(); + while (p != active_hotkeys_.end()) { + if (p->second.hotkey_name() == hotkey_name) { + return p->second; + } + p++; + } + return make_error(xstudio_error::error, "Invalid hotkey name"); + }, + [=](keypress_monitor::hotkey_event_atom, const utility::Uuid kotkey_uuid, const bool pressed, diff --git a/src/ui/viewport/src/viewport.cpp b/src/ui/viewport/src/viewport.cpp index 82b39f118..e2ed12783 100644 --- a/src/ui/viewport/src/viewport.cpp +++ b/src/ui/viewport/src/viewport.cpp @@ -107,9 +107,7 @@ std::string make_viewport_name() { Viewport::Viewport( - const utility::JsonStore &state_data, - caf::actor parent_actor, - const std::string &_name) + const utility::JsonStore &state_data, caf::actor parent_actor, const std::string &_name) : Module(_name.empty() ? make_viewport_name() : _name), parent_actor_(std::move(parent_actor)) { @@ -123,7 +121,7 @@ Viewport::Viewport( // so they get their own OpenGL resource object (so it has its own // texture etc.) static int idx = 0; - window_id_ = window_id_ + fmt::format("{}", idx++); + window_id_ = window_id_ + fmt::format("{}", idx++); } // TODO: set these up via Json prefs coming in from framework @@ -271,16 +269,14 @@ Viewport::Viewport( {"Mirror Horizontally", "Mirror Vertically", "Mirror Both", "Off"}, {"Mirror Horizontally", "Mirror Vertically", "Mirror Both", "Off"}); - // window_id_ will be "xstudio_main_window" for any viewport embedded in + // window_id_ will be "xstudio_main_window" for any viewport embedded in // the main interface or "xstudio_popout_window" for the pop-out window. // These viewports should be zoom/pan/fit/compare mode synced. All other // viewport must not sync these settings. - sync_to_main_viewport_ = - add_boolean_attribute( - "Sync To Main Viewport", - "Sync To Main Viewport", - window_id_ == "xstudio_main_window" || window_id_ == "xstudio_popout_window" - ); + sync_to_main_viewport_ = add_boolean_attribute( + "Sync To Main Viewport", + "Sync To Main Viewport", + window_id_ == "xstudio_main_window" || window_id_ == "xstudio_popout_window"); filter_mode_preference_ = add_string_choice_attribute( "Viewport Filter Mode", "Vp. Filtering", ViewportRenderer::pixel_filter_mode_names); @@ -360,7 +356,8 @@ Viewport::Viewport( module::Module::set_parent_actor_addr(caf::actor_cast(parent_actor_)); global_playhead_events_group_ = - self()->home_system().registry().template get(global_playhead_events_actor); + self()->home_system().registry().template get( + global_playhead_events_actor); caf::scoped_actor sys(self()->home_system()); fps_monitor_ = sys->spawn(); @@ -499,6 +496,14 @@ void Viewport::register_hotkeys() { false, "Viewer"); + hover_select_ = register_hotkey( + "F7", + "Pointer select media in viewport", + "While holding this key, the image in the viewport under the mouse pointer sets the " + "'hero' media in the selected media set.", + false, + "Viewer"); + zoom_mode_toggle_->set_role_data(module::Attribute::HotkeyUuid, zoom_hotkey_); pan_mode_toggle_->set_role_data(module::Attribute::HotkeyUuid, pan_hotkey_); @@ -581,15 +586,13 @@ void Viewport::set_pointer_event_viewport_coords(PointerEvent &pointer_event) { // coordinates, which tells us how much the effective viewport zoom is in // display pixels. We can use this when detecting if the mouse cursor is within // N screen pixels of something in the viewport regardless of the zoom - Imath::V4f d_zero = Imath::V4f(1.0f / state_.size_.x, 0.0f, 0.0f, 1.0f) * - projection_matrix_; - Imath::V4f delta = - Imath::V4f(0.0f, 0.0f, 0.0f, 1.0f) * projection_matrix_; + Imath::V4f d_zero = + Imath::V4f(1.0f / state_.size_.x, 0.0f, 0.0f, 1.0f) * projection_matrix_; + Imath::V4f delta = Imath::V4f(0.0f, 0.0f, 0.0f, 1.0f) * projection_matrix_; Imath::V4f p = state_.pointer_position_; pointer_event.set_pos_in_coord_sys(p.x, p.y, (d_zero - delta).x); - } bool Viewport::process_pointer_event(PointerEvent &pointer_event) { @@ -618,10 +621,11 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { if (pointer_event.buttons() == ui::Signature::Button::Left && compare_mode_ == "Grid") { - grid_mode_media_select(pointer_event); - + pointer_select_media(pointer_event); } + } else if (hover_image_select_ && pointer_event.type() == Signature::EventType::Move) { + pointer_select_media(pointer_event); } if (pointer_event_handlers_.find(pointer_event.signature()) != @@ -750,7 +754,6 @@ void Viewport::apply_fit_mode() { state_.translate_.y = 0.5f / screen_pix_size_y; } } - } float Viewport::pixel_zoom() const { @@ -918,19 +921,19 @@ void Viewport::calc_image_bounds_in_viewport_pixels() { return; } - const auto old = image_bounds_in_viewport_pixels_; - const auto & im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + const auto old = image_bounds_in_viewport_pixels_; + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; image_bounds_in_viewport_pixels_.clear(); - for (const auto &i: im_order) { + for (const auto &i : im_order) { const media_reader::ImageBufPtr &im = on_screen_frames_->onscreen_image(i); - const float aspect = 1.0f / (im ? im->image_aspect() : 16.0f/9.0f); + const float aspect = 1.0f / (im ? im->image_aspect() : 16.0f / 9.0f); Imath::Vec4 a(-1.0f, -aspect, 0.0f, 1.0f); - a *= im.layout_transform()*m; + a *= im.layout_transform() * m; Imath::Vec4 b(1.0f, aspect, 0.0f, 1.0f); - b *= im.layout_transform()*m; + b *= im.layout_transform() * m; // note projection matrix includes the 'Flip' mode so bottom left corner // of image might be drawn top right etc. @@ -940,11 +943,12 @@ void Viewport::calc_image_bounds_in_viewport_pixels() { const float y0 = (-a.y / a.w + 1.0f) / 2.0f; const float y1 = (-b.y / b.w + 1.0f) / 2.0f; - Imath::V2f bottomLeft(std::min(x0, x1)*state_.size_.x, std::min(y0, y1)*state_.size_.y); - Imath::V2f topRight(std::max(x0, x1)*state_.size_.x, std::max(y0, y1)*state_.size_.y); + Imath::V2f bottomLeft( + std::min(x0, x1) * state_.size_.x, std::min(y0, y1) * state_.size_.y); + Imath::V2f topRight( + std::max(x0, x1) * state_.size_.x, std::max(y0, y1) * state_.size_.y); image_bounds_in_viewport_pixels_.emplace_back(Imath::Box2f(bottomLeft, topRight)); - } if (old != image_bounds_in_viewport_pixels_) { @@ -961,10 +965,10 @@ void Viewport::update_image_resolutions() { } return; } - auto old = image_resolutions_; - const auto & im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + auto old = image_resolutions_; + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; image_resolutions_.clear(); - for (const auto &i: im_order) { + for (const auto &i : im_order) { const media_reader::ImageBufPtr &im = on_screen_frames_->onscreen_image(i); if (im) { image_resolutions_.push_back(im->image_size_in_pixels()); @@ -975,7 +979,6 @@ void Viewport::update_image_resolutions() { if (image_resolutions_ != old) { event_callback(ImageResolutionsChanged); } - } std::list> Viewport::fit_modes() { @@ -1025,12 +1028,12 @@ caf::message_handler Viewport::message_handler() { const Imath::V2f pan, const std::string &viewport_name, const std::string &window_id) { - if (viewport_name == name()) return; - if (sync_to_main_viewport_->value() && - (window_id == "xstudio_popout_window" || window_id == "xstudio_main_window")) { + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { broadcast_fit_details_ = false; @@ -1095,8 +1098,9 @@ caf::message_handler Viewport::message_handler() { // To use // we only sync changes in main window to popout and vice versa. Two // viewport in the same window do not sync. quickview windows do not sync - if (sync_to_main_viewport_->value() && - (window_id == "xstudio_popout_window" || window_id == "xstudio_main_window")) { + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { set_pan(xpan, ypan); event_callback(Redraw); } @@ -1125,8 +1129,9 @@ caf::message_handler Viewport::message_handler() { const std::string &window_id) { // we only sync changes in main window to popout and vice versa. Two // viewport in the same window do not sync. quickview windows do not sync - if (sync_to_main_viewport_->value() && - (window_id == "xstudio_popout_window" || window_id == "xstudio_main_window")) { + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { set_scale(scale); event_callback(Redraw); } @@ -1187,7 +1192,7 @@ caf::message_handler Viewport::message_handler() { serialNumber); }, - [=](playhead::compare_mode_atom, const std::string & compare_mode) { + [=](playhead::compare_mode_atom, const std::string &compare_mode) { // the message comes from current playhead when compare mode // is changed set_compare_mode(compare_mode); @@ -1260,7 +1265,10 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { if (playhead_viewport_events_group_) utility::request_receive( - *sys, playhead_viewport_events_group_, broadcast::join_broadcast_atom_v, self()); + *sys, + playhead_viewport_events_group_, + broadcast::join_broadcast_atom_v, + self()); playhead_uuid_ = utility::request_receive(*sys, playhead, utility::uuid_atom_v); @@ -1290,11 +1298,19 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { // pass the new playhead to the actor that manages the queue of images // to be display now and in the near future utility::request_receive( - *sys, display_frames_queue_actor_, viewport_playhead_atom_v, playhead, true); + *sys, + display_frames_queue_actor_, + viewport_playhead_atom_v, + UuidActor(playhead_uuid_, playhead), + true); } else { - anon_send(display_frames_queue_actor_, viewport_playhead_atom_v, playhead, false); + anon_send( + display_frames_queue_actor_, + viewport_playhead_atom_v, + UuidActor(playhead_uuid_, playhead), + false); // here we tell any viewport plugins that the playhead has changed for (auto p : overlay_plugin_instances_) { @@ -1306,7 +1322,8 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { } } - set_compare_mode(utility::request_receive(*sys, playhead, playhead::compare_mode_atom_v)); + set_compare_mode(utility::request_receive( + *sys, playhead, playhead::compare_mode_atom_v)); // tell the playhead events actor that the on-screen playhead has changed anon_send( @@ -1437,8 +1454,7 @@ void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) set_mirror_mode(MirrorMode::Both); else set_mirror_mode(MirrorMode::Off); - - } + } } void Viewport::menu_item_activated( @@ -1458,7 +1474,8 @@ void Viewport::update_attrs_from_preferences(const utility::JsonStore &j) { // TODO: proper preferences handling for the viewport renderer class utility::JsonStore p; p["texture_mode"] = texture_mode_preference_->value(); - if (active_renderer_) active_renderer_->set_prefs(p); + if (active_renderer_) + active_renderer_->set_prefs(p); } void Viewport::hotkey_pressed( @@ -1483,6 +1500,8 @@ void Viewport::hotkey_pressed( switch_mirror_mode(); } else if (hotkey_uuid == reset_hotkey_) { reset(); + } else if (hotkey_uuid == hover_select_) { + hover_image_select_ = true; } } @@ -1494,6 +1513,8 @@ void Viewport::hotkey_released( } else if (hotkey_uuid == pan_hotkey_) { pan_mode_toggle_->set_role_data(module::Attribute::Activated, false); pan_mode_toggle_->set_value(false); + } else if (hotkey_uuid == hover_select_) { + hover_image_select_ = false; } } @@ -1504,6 +1525,7 @@ void Viewport::reset() { pan_mode_toggle_->set_value(false); zoom_mode_toggle_->set_value(false); + hover_image_select_ = false; if (colour_pipeline_) anon_send(colour_pipeline_, module::reset_module_atom_v); @@ -1523,7 +1545,7 @@ media_reader::ImageBufPtr Viewport::get_onscreen_image(const bool force_playhead void Viewport::update_onscreen_frame_info(const media_reader::ImageBufDisplaySetPtr &images) { on_screen_frames_ = images; - needs_redraw_ = false; + needs_redraw_ = false; if (images && images->images_layout_hash() != image_bounds_hash_) { calc_image_bounds_in_viewport_pixels(); @@ -1539,7 +1561,9 @@ void Viewport::update_onscreen_frame_info(const media_reader::ImageBufDisplaySet return; } - if (state_.layout_aspect_ != images->layout_aspect()) { + if (state_.layout_aspect_ != images->layout_aspect() || + state_.image_size_ != images->hero_image()->image_size_in_pixels()) { + state_.image_size_ = images->hero_image()->image_size_in_pixels(); state_.layout_aspect_ = images->layout_aspect(); update_matrix(); } @@ -1565,6 +1589,19 @@ void Viewport::framebuffer_swapped(const utility::time_point swap_time) { swap_time, screen_refresh_period_); + /*if (on_screen_hero_frame_) { + int f = on_screen_hero_frame_.playhead_logical_frame(); + static auto f0 = f; + if (f != f0 && (f-f0) != 1) { + std::cerr << " dropped "; + } + f0 = f; + auto t = utility::clock::now(); + static auto t0 = t; + std::cerr << std::chrono::duration_cast(t-t0).count() << " "; + t0 = t; + }*/ + if (next_on_screen_hero_frame_ != on_screen_hero_frame_) { on_screen_hero_frame_ = next_on_screen_hero_frame_; @@ -1572,16 +1609,12 @@ void Viewport::framebuffer_swapped(const utility::time_point swap_time) { fps_monitor(), ui::fps_monitor::framebuffer_swapped_atom_v, swap_time, - on_screen_hero_frame_.playhead_logical_frame() - ); - + on_screen_hero_frame_.playhead_logical_frame()); } - } media_reader::ImageBufDisplaySetPtr Viewport::get_frames_for_display( - const bool force_playhead_sync, - const utility::time_point &when_being_displayed) { + const bool force_playhead_sync, const utility::time_point &when_being_displayed) { media_reader::ImageBufDisplaySetPtr result; @@ -1592,26 +1625,35 @@ media_reader::ImageBufDisplaySetPtr Viewport::get_frames_for_display( try { result = when_being_displayed == utility::time_point() - ? request_receive_wait( - *sys, - display_frames_queue_actor_, - std::chrono::milliseconds(1000), - viewport_get_next_frames_for_display_atom_v, - force_playhead_sync) - : request_receive_wait( - *sys, - display_frames_queue_actor_, - std::chrono::milliseconds(1000), - viewport_get_next_frames_for_display_atom_v, - when_being_displayed); - - size_t image_set_hash = result ? result->images_layout_hash() + result->images_keys_hash() : 0; - if (last_images_hash_ !=image_set_hash) { + ? request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + force_playhead_sync) + : request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + when_being_displayed); + + // TED - note, I've removed the check on image_keys_hash to test if + // the images have changed. Reason is that bookmarks might change which + // (at the moment) doesn't change the image hash. + + /*size_t image_set_hash = + result ? result->images_layout_hash() + result->images_keys_hash() : 0; + if (last_images_hash_ != image_set_hash) { last_images_hash_ = image_set_hash; // pass on-screen images to overlay plugins for (auto p : overlay_plugin_instances_) { anon_send(p.second, playhead::show_atom_v, result, name(), playing_); } + }*/ + + for (auto p : overlay_plugin_instances_) { + anon_send(p.second, playhead::show_atom_v, result, name(), playing_); } } catch (const std::exception &e) { @@ -1622,7 +1664,8 @@ media_reader::ImageBufDisplaySetPtr Viewport::get_frames_for_display( return result; } -media_reader::ImageBufDisplaySetPtr Viewport::prepare_image_for_display(const media_reader::ImageBufPtr &image_buf) const { +media_reader::ImageBufDisplaySetPtr +Viewport::prepare_image_for_display(const media_reader::ImageBufPtr &image_buf) const { media_reader::ImageBufDisplaySetPtr result; @@ -1633,11 +1676,11 @@ media_reader::ImageBufDisplaySetPtr Viewport::prepare_image_for_display(const me try { result = request_receive_wait( - *sys, - display_frames_queue_actor_, - std::chrono::milliseconds(1000), - viewport_get_next_frames_for_display_atom_v, - image_buf); + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + image_buf); } catch (const std::exception &e) { @@ -1645,7 +1688,6 @@ media_reader::ImageBufDisplaySetPtr Viewport::prepare_image_for_display(const me } return result; - } void Viewport::instance_overlay_plugins() { @@ -1660,7 +1702,8 @@ void Viewport::instance_overlay_plugins() { utility::JsonStore plugin_init_data; // get the OCIO colour pipeline plugin (the only one implemented right now) - auto pm = self()->home_system().registry().template get(plugin_manager_registry); + auto pm = + self()->home_system().registry().template get(plugin_manager_registry); auto overlay_plugin_details = request_receive>( *sys, @@ -1688,14 +1731,14 @@ void Viewport::instance_overlay_plugins() { anon_send(overlay_actor, module::connect_to_ui_atom_v); auto overlay_renderer = request_receive( - *sys, overlay_actor, overlay_render_function_atom_v); + *sys, overlay_actor, overlay_render_function_atom_v, name()); if (overlay_renderer) { viewport_overlay_renderers_[pd.uuid_] = overlay_renderer; } auto pre_render_hook = request_receive( - *sys, overlay_actor, pre_render_gpu_hook_atom_v); + *sys, overlay_actor, pre_render_gpu_hook_atom_v, name()); if (pre_render_hook) { overlay_pre_render_hooks_[pd.uuid_] = pre_render_hook; @@ -1729,14 +1772,14 @@ void Viewport::instance_overlay_plugins() { anon_send(overlay_actor, module::connect_to_ui_atom_v); auto overlay_renderer = request_receive( - *sys, overlay_actor, overlay_render_function_atom_v); + *sys, overlay_actor, overlay_render_function_atom_v, name()); if (overlay_renderer) { viewport_overlay_renderers_[pd.uuid_] = overlay_renderer; } auto pre_render_hook = request_receive( - *sys, overlay_actor, pre_render_gpu_hook_atom_v); + *sys, overlay_actor, pre_render_gpu_hook_atom_v, name()); if (pre_render_hook) { overlay_pre_render_hooks_[pd.uuid_] = pre_render_hook; @@ -1748,14 +1791,14 @@ void Viewport::instance_overlay_plugins() { } } - display_frames_queue_actor_ = - sys->spawn(self(), overlay_plugin_instances_, name(), colour_pipeline_); + display_frames_queue_actor_ = sys->spawn( + self(), overlay_plugin_instances_, name(), colour_pipeline_); } catch (std::exception &e) { if (!display_frames_queue_actor_) { - display_frames_queue_actor_ = - sys->spawn(self(), overlay_plugin_instances_, name(), colour_pipeline_); + display_frames_queue_actor_ = sys->spawn( + self(), overlay_plugin_instances_, name(), colour_pipeline_); } spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -1763,7 +1806,6 @@ void Viewport::instance_overlay_plugins() { auto a = caf::actor_cast(self()); a->link_to(display_frames_queue_actor_); - } void Viewport::get_colour_pipeline() { @@ -1784,9 +1826,10 @@ void Viewport::get_colour_pipeline() { colour_pipeline_ = colour_pipe; auto colour_pipe_gpu_hook = request_receive( - *sys, colour_pipeline_, pre_render_gpu_hook_atom_v); + *sys, colour_pipeline_, pre_render_gpu_hook_atom_v, name()); if (colour_pipe_gpu_hook) { - overlay_pre_render_hooks_[utility::Uuid("4aefe9d8-a53d-46a3-9237-9ff686790c46")] = colour_pipe_gpu_hook; + overlay_pre_render_hooks_[utility::Uuid( + "4aefe9d8-a53d-46a3-9237-9ff686790c46")] = colour_pipe_gpu_hook; } } @@ -1845,7 +1888,8 @@ void Viewport::quickview_media( auto playhead = quickview_playhead_; if (!playhead) { // create a new quickview playhead, or use existing one. - playhead = sys->spawn("QuickviewPlayhead"); + playhead = sys->spawn( + "QuickviewPlayhead", playhead::INDEPENDENT_AUDIO); } // set the compare mode anon_send( @@ -1877,7 +1921,6 @@ void Viewport::quickview_media( set_playhead(playhead, true); quickview_playhead_ = playhead; - } void Viewport::set_visibility(bool is_visible) { @@ -1894,13 +1937,12 @@ utility::JsonStore ViewportRenderer::core_shader_params( const Imath::M44f &window_to_viewport_matrix, const Imath::M44f &viewport_to_image_space, const float viewport_du_dx, - const utility::JsonStore & layout_data, - const int image_index) const -{ + const utility::JsonStore &layout_data, + const int image_index) const { utility::JsonStore shader_params; - shader_params["to_coord_system"] = viewport_to_image_space.inverse(); - shader_params["to_canvas"] = window_to_viewport_matrix; + shader_params["to_coord_system"] = viewport_to_image_space.inverse(); + shader_params["to_canvas"] = window_to_viewport_matrix; if (image_to_be_drawn) { // here we can work out the ratio of image pixels to screen pixels @@ -1912,9 +1954,8 @@ utility::JsonStore ViewportRenderer::core_shader_params( else if (render_hints_ == BilinearWhenZoomedOut) shader_params["use_bilinear_filtering"] = image_pix_to_screen_pix > 1.00001f; // filter_mode_ == BilinearWhenZoomedOut - shader_params["image_aspect"] = image_to_be_drawn->image_aspect(); + shader_params["image_aspect"] = image_to_be_drawn->image_aspect(); shader_params["image_transform_matrix"] = image_to_be_drawn.layout_transform(); - } return shader_params; } @@ -1927,8 +1968,7 @@ void Viewport::render() const { render_data_->images, render_data_->window_to_viewport_matrix, render_data_->projection_matrix, - render_data_->overlay_renderers - ); + render_data_->overlay_renderers); } } @@ -1941,36 +1981,38 @@ void Viewport::render(const utility::time_point &when_going_on_screen) { void Viewport::render(const media_reader::ImageBufPtr &image_buf) { - // rendering in the same thread, rendering a single image + // rendering in the same thread, rendering a single image // - used by offscreen renderer - RenderData * rdata = new RenderData; - rdata->images = prepare_image_for_display(image_buf); + RenderData *rdata = new RenderData; + rdata->images = prepare_image_for_display(image_buf); update_onscreen_frame_info(rdata->images); rdata->window_to_viewport_matrix = window_to_viewport_matrix(); - rdata->projection_matrix = projection_matrix(); - rdata->overlay_renderers = viewport_overlay_renderers_; - rdata->renderer = active_renderer_; + rdata->projection_matrix = projection_matrix(); + rdata->overlay_renderers = viewport_overlay_renderers_; + rdata->renderer = active_renderer_; render_data_.reset(rdata); render(); } -void Viewport::prepare_render_data(const utility::time_point &when_going_on_screen) { +void Viewport::prepare_render_data( + const utility::time_point &when_going_on_screen, const bool sync_to_playhead) { // here we make data for rendering in separate thread - RenderData * rdata = new RenderData; - rdata->images = get_frames_for_display(false, when_going_on_screen); + RenderData *rdata = new RenderData; + rdata->images = get_frames_for_display(sync_to_playhead, when_going_on_screen); update_onscreen_frame_info(rdata->images); rdata->window_to_viewport_matrix = window_to_viewport_matrix(); - rdata->projection_matrix = projection_matrix(); - rdata->overlay_renderers = viewport_overlay_renderers_; - rdata->renderer = active_renderer_; + rdata->projection_matrix = projection_matrix(); + rdata->overlay_renderers = viewport_overlay_renderers_; + rdata->renderer = active_renderer_; render_data_.reset(rdata); } void Viewport::set_compare_mode(const std::string &compare_mode) { - if (compare_mode_ == compare_mode) return; + if (compare_mode_ == compare_mode) + return; compare_mode_ = compare_mode; @@ -1979,7 +2021,7 @@ void Viewport::set_compare_mode(const std::string &compare_mode) { caf::scoped_actor sys(self()->home_system()); auto layouts_manager = - self()->home_system().registry().template get(viewport_layouts_manager); + self()->home_system().registry().template get(viewport_layouts_manager); // get the actor that provides the layout auto layout_actor = request_receive( @@ -1988,23 +2030,16 @@ void Viewport::set_compare_mode(const std::string &compare_mode) { viewport_layout_atom_v, compare_mode, sync_to_main_viewport_->value(), - name() - ); + name()); // pass it over to the frame queue actor, which sends the image // set to the layout actor to do the actual layout anon_send( - display_frames_queue_actor_, - viewport_layout_atom_v, - layout_actor, - compare_mode); + display_frames_queue_actor_, viewport_layout_atom_v, layout_actor, compare_mode); // get the viewport renderer for the layout/compare mode active_renderer_ = request_receive( - *sys, - layout_actor, - viewport_renderer_atom_v, - window_id_); + *sys, layout_actor, viewport_renderer_atom_v, window_id_); active_renderer_->set_pre_renderer_hooks(overlay_pre_render_hooks_); @@ -2014,45 +2049,52 @@ void Viewport::set_compare_mode(const std::string &compare_mode) { } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - } -void Viewport::grid_mode_media_select(const PointerEvent &pointer_event) { +void Viewport::pointer_select_media(const PointerEvent &pointer_event) { - if (!(pointer_event.modifiers() == Signature::Modifier::ControlModifier || - pointer_event.modifiers() == Signature::Modifier::NoModifier)) return; + if (!(pointer_event.modifiers() == Signature::Modifier::ControlModifier || + pointer_event.modifiers() == Signature::Modifier::NoModifier)) + return; - // special case. In grid mode, detect which image was clicked on - // to adjust the selection, where possible. - // We should already have up-to-date info on the positions of the images - // in the viewport.. - int idx = 0; + if (!on_screen_frames_ || !on_screen_frames_->layout_data()) + return; + + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + + if (im_order.size() != image_bounds_in_viewport_pixels_.size()) + return; + + // lazy! + thread_local utility::Uuid last_im_id; + + // lreverse oop through image bounds (last one is drawn on-top) + // these are the images visible in the layout + for (int i = (int(image_bounds_in_viewport_pixels_.size()) - 1); i >= 0; --i) { - // loop through image bounds - these are the images visible in the layout - for (const auto &im_bounds: image_bounds_in_viewport_pixels_) { + const auto &im_bounds = image_bounds_in_viewport_pixels_[i]; + // is the mouse inside the image boundary? if (im_bounds.min.x <= pointer_event.x() && im_bounds.max.x >= pointer_event.x() && im_bounds.min.y <= pointer_event.y() && im_bounds.max.y >= pointer_event.y()) { - // send the playhead the index - if (on_screen_frames_ && on_screen_frames_->layout_data()) { - // resolve the image idx in the layout to the image index in the - // onscreen image set ... - const auto & im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; - if (idx < im_order.size()) { - const media_reader::ImageBufPtr & im = on_screen_frames_->onscreen_image(im_order[idx]); - if (im && playhead_addr_) { - anon_send( - caf::actor_cast(playhead_addr_), - playlist::select_media_atom_v, - im.frame_id().media_uuid(), - pointer_event.modifiers() == Signature::Modifier::ControlModifier - ); - } - } + // .. yes. send the corresponding media UUID to the playhead + + // resolve the image idx in the layout to the image index in the + // onscreen image set ... + const media_reader::ImageBufPtr &im = + on_screen_frames_->onscreen_image(im_order[i]); + if (im && playhead_addr_ && last_im_id != im.frame_id().media_uuid()) { + last_im_id = im.frame_id().media_uuid(); + anon_send( + caf::actor_cast(playhead_addr_), + playlist::select_media_atom_v, + im.frame_id().media_uuid(), + i, + pointer_event.modifiers() == Signature::Modifier::ControlModifier); + break; } } - idx++; } } diff --git a/src/ui/viewport/src/viewport_frame_queue_actor.cpp b/src/ui/viewport/src/viewport_frame_queue_actor.cpp index 82036e767..a62c9012e 100644 --- a/src/ui/viewport/src/viewport_frame_queue_actor.cpp +++ b/src/ui/viewport/src/viewport_frame_queue_actor.cpp @@ -11,11 +11,16 @@ using namespace xstudio::utility; using namespace xstudio; ViewportFrameQueueActor::ViewportFrameQueueActor( - caf::actor_config &cfg, caf::actor viewport, std::map overlay_actors, - const std::string &viewport_name, caf::actor colour_pipeline) - : caf::event_based_actor(cfg), viewport_(viewport), viewport_overlay_plugins_(std::move(overlay_actors)), - viewport_name_(viewport_name), - colour_pipeline_(colour_pipeline) { + caf::actor_config &cfg, + caf::actor viewport, + std::map overlay_actors, + const std::string &viewport_name, + caf::actor colour_pipeline) + : caf::event_based_actor(cfg), + viewport_(viewport), + viewport_overlay_plugins_(std::move(overlay_actors)), + viewport_name_(viewport_name), + colour_pipeline_(colour_pipeline) { print_on_exit(this, "ViewportFrameQueueActor"); @@ -23,9 +28,9 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( set_down_handler([=](down_msg &msg) { // find in playhead list.. - if (msg.source == playhead_) { - demonitor(playhead_); - playhead_ = caf::actor(); + if (msg.source == playhead_.actor()) { + demonitor(playhead_.actor()); + playhead_ = utility::UuidActor(); frames_to_draw_per_playhead_.clear(); } }); @@ -34,9 +39,9 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](viewport_playhead_atom, - caf::actor playhead, + utility::UuidActor playhead, const bool prefetch_inital_image) -> result { - return set_playhead(playhead, prefetch_inital_image); + return set_playhead(playhead, prefetch_inital_image); }, [=](playhead::child_playheads_deleted_atom, @@ -129,7 +134,6 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( [=](ui::fps_monitor::framebuffer_swapped_atom, const utility::time_point &message_send_tp, const timebase::flicks video_refresh_rate_hint) { - // this incoming message originates from the video layer and 'message_send_tp' // should be, as accurately as possible, the actual time that the framebuffer was // swapped to the screen. @@ -141,7 +145,6 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( } video_refresh_data_.refresh_rate_hint_ = video_refresh_rate_hint; video_refresh_data_.last_video_refresh_ = message_send_tp; - }, [=](playhead::show_atom, @@ -150,18 +153,16 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( const utility::FrameRate & /*rate*/, const bool is_playing, const bool is_onscreen_frame) { - playing_ = is_playing; queue_image_buffer_for_drawing(buf, playhead_uuid); drop_old_frames(utility::clock::now() - std::chrono::milliseconds(100)); anon_send(viewport_, playhead::redraw_viewport_atom_v); - }, // these are frame bufs that we expect to draw in the very near future [=](playhead::show_atom, const utility::Uuid &playhead_uuid, - std::vector future_bufs) { + std::vector future_bufs) { // now insert the new future frames ready for drawing for (auto &buf : future_bufs) { if (buf) { @@ -202,23 +203,24 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( }, [=](viewport_get_next_frames_for_display_atom, - const media_reader::ImageBufPtr &lone_image) -> result { - - // If something wants the viewport to render a single image that has - // been fetched independently (for example this is how offscreen - // generation of thumbnail images is donw) we need to run this + const media_reader::ImageBufPtr &lone_image) + -> result { + // If something wants the viewport to render a single image that has + // been fetched independently (for example this is how offscreen + // generation of thumbnail images is donw) we need to run this // through our routine that adds colour management and overlay data // to the ImageBuffer and buid an ImageBufDisplaySetPtr for rendering. auto rp = make_response_promise(); static const utility::Uuid dummy_playhead_id = utility::Uuid::generate(); - media_reader::ImageBufDisplaySet * result = new media_reader::ImageBufDisplaySet({dummy_playhead_id}); + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet({dummy_playhead_id}); result->add_on_screen_image(dummy_playhead_id, lone_image); append_overlays_data(rp, result); return rp; }, - [=](viewport_layout_atom, caf::actor layout_manager, const std::string & layout_name) { - viewport_layout_manager_ = layout_manager; + [=](viewport_layout_atom, caf::actor layout_manager, const std::string &layout_name) { + viewport_layout_manager_ = layout_manager; viewport_layout_mode_name_ = layout_name; }, @@ -239,12 +241,11 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( [=](const error &err) mutable { aout(this) << err << std::endl; }); } -ViewportFrameQueueActor::~ViewportFrameQueueActor() { -} +ViewportFrameQueueActor::~ViewportFrameQueueActor() {} void ViewportFrameQueueActor::on_exit() { viewport_layout_manager_ = caf::actor(); - playhead_ = caf::actor(); + playhead_ = utility::UuidActor(); caf::event_based_actor::on_exit(); } @@ -295,19 +296,18 @@ void ViewportFrameQueueActor::queue_image_buffer_for_drawing( auto &frames_queued_for_display = frames_to_draw_per_playhead_[playhead_id]; frames_queued_for_display[buf.timeline_timestamp()] = buf; - } void ViewportFrameQueueActor::get_frames_for_display_sync( - caf::typed_response_promise rp) -{ + caf::typed_response_promise rp) { // in 'force_sync' mode we request and wait for the playhead // to read all images for the current set of on-screen sources - media_reader::ImageBufDisplaySet * result = new media_reader::ImageBufDisplaySet(sub_playhead_ids_); + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet(sub_playhead_ids_); auto count = std::make_shared(sub_playhead_ids_.size()); - int k_idx = 0; - for (const auto playhead_id: sub_playhead_ids_) { + int k_idx = 0; + for (const auto playhead_id : sub_playhead_ids_) { if (current_key_sub_playhead_id_ == playhead_id) { result->set_hero_sub_playhead_index(k_idx); @@ -316,35 +316,32 @@ void ViewportFrameQueueActor::get_frames_for_display_sync( // here we fetch the on-screen image buffer for the given sub-playhead // from the playhead const auto id = playhead_id; - request(playhead_, infinite, playhead::buffer_atom_v, playhead_id) + request(playhead_.actor(), infinite, playhead::buffer_atom_v, playhead_id) .then( - [=](const media_reader::ImageBufPtr &buf) mutable { - if (buf) { - result->add_on_screen_image(id, buf); - } - (*count)--; - if (!(*count)) { - // we have all images, now proceed to add extra data - append_overlays_data(rp, result); - } - - }, - [=](caf::error &err) mutable { - rp.deliver(err); - (*count)--; - if (!(*count)) { - append_overlays_data(rp, result); - } - }); + [=](const media_reader::ImageBufPtr &buf) mutable { + if (buf) { + result->add_on_screen_image(id, buf); + } + (*count)--; + if (!(*count)) { + // we have all images, now proceed to add extra data + append_overlays_data(rp, result); + } + }, + [=](caf::error &err) mutable { + rp.deliver(err); + (*count)--; + if (!(*count)) { + append_overlays_data(rp, result); + } + }); } - } - + void ViewportFrameQueueActor::get_frames_for_display( caf::typed_response_promise rp, - const utility::time_point &when_going_on_screen - ) { + const utility::time_point &when_going_on_screen) { // evaluate the position of the playhead at the timepoint when the viewport // redraw happens (or, more precisely, when the buffer that it is drawn @@ -353,10 +350,11 @@ void ViewportFrameQueueActor::get_frames_for_display( ? predicted_playhead_position_at_next_video_refresh() : predicted_playhead_position(when_going_on_screen); - media_reader::ImageBufDisplaySet * result = new media_reader::ImageBufDisplaySet(sub_playhead_ids_); + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet(sub_playhead_ids_); int k_idx = 0; - for (const auto playhead_id: sub_playhead_ids_) { + for (const auto playhead_id : sub_playhead_ids_) { if (current_key_sub_playhead_id_ == playhead_id) { result->set_hero_sub_playhead_index(k_idx); @@ -366,7 +364,8 @@ void ViewportFrameQueueActor::get_frames_for_display( } k_idx++; - if (frames_to_draw_per_playhead_.find(playhead_id) == frames_to_draw_per_playhead_.end()) { + if (frames_to_draw_per_playhead_.find(playhead_id) == + frames_to_draw_per_playhead_.end()) { // no images queued for display for the indicated playhead continue; } @@ -396,7 +395,7 @@ void ViewportFrameQueueActor::get_frames_for_display( result->add_on_screen_image(playhead_id, r->second); - // now we add 'future frames' - i.e. frames that are not onscreen now + // now we add 'future frames' - i.e. frames that are not onscreen now // but will be going on-screen next. We supply these to the viewport so // that it can do asynchronous transfers of data to VRAM, i.e. copying // the image data to the GPU for the next image(s) while the current image @@ -404,7 +403,8 @@ void ViewportFrameQueueActor::get_frames_for_display( auto r_next = r; if (playing_forwards_) { r_next++; - while (r_next != frames_queued_for_display.end() && result->num_future_images(playhead_id) < 2) { + while (r_next != frames_queued_for_display.end() && + result->num_future_images(playhead_id) < 2) { result->append_future_image(playhead_id, r_next->second); r_next++; @@ -413,7 +413,8 @@ void ViewportFrameQueueActor::get_frames_for_display( } } } else { - while (r_next != frames_queued_for_display.begin() && result->num_future_images(playhead_id) < 2) { + while (r_next != frames_queued_for_display.begin() && + result->num_future_images(playhead_id) < 2) { r_next--; result->append_future_image(playhead_id, r_next->second); if (r_next == frames_queued_for_display.begin()) { @@ -422,7 +423,6 @@ void ViewportFrameQueueActor::get_frames_for_display( } } } - } // now that we have picked frames for display, the first frame in 'next_images' @@ -433,12 +433,11 @@ void ViewportFrameQueueActor::get_frames_for_display( } append_overlays_data(rp, result); - } void ViewportFrameQueueActor::append_overlays_data( caf::typed_response_promise rp, - media_reader::ImageBufDisplaySet * result) { + media_reader::ImageBufDisplaySet *result) { // In the next steps we are doing a-sync requests to get data added to // 'result'... we make a counter of the number of requests we'll be making @@ -446,7 +445,8 @@ void ViewportFrameQueueActor::append_overlays_data( // counter has to be a shared pointer as the lambda request response handlers // make their own copy of response_count. If it weren't a shared pointer the // handler would be decrementing their copy, not a global (shared) counter. - auto response_count = std::make_shared(viewport_overlay_plugins_.size()*result->num_onscreen_images()); + auto response_count = + std::make_shared(viewport_overlay_plugins_.size() * result->num_onscreen_images()); if (!*response_count) { result->finalise(); rp.deliver(media_reader::ImageBufDisplaySetPtr(result)); @@ -458,7 +458,7 @@ void ViewportFrameQueueActor::append_overlays_data( // plugins an opportunity to add data to the images that they will need // at draw time to draw graphics onto the screen for (int img_idx = 0; img_idx < result->num_onscreen_images(); ++img_idx) { - + if (!result->onscreen_image(img_idx)) { // no image ? Not expected, but we'll handle this just in case. Skip // adding overlay data or colour data as there is no image. @@ -476,27 +476,24 @@ void ViewportFrameQueueActor::append_overlays_data( colour_pipeline_, infinite, colour_pipeline::get_colour_pipe_data_atom_v, - result->onscreen_image(img_idx)).then( + result->onscreen_image(img_idx)) + .then( [=](media_reader::ImageBufPtr image_with_colour_data) mutable { result->set_on_screen_image(img_idx, image_with_colour_data); append_overlays_data(rp, img_idx, result, response_count); - }, - [=](caf::error & err) mutable { + [=](caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); append_overlays_data(rp, img_idx, result, response_count); }); } - } - + void ViewportFrameQueueActor::append_overlays_data( caf::typed_response_promise rp, const int img_idx, - media_reader::ImageBufDisplaySet * result, - std::shared_ptr response_count - ) -{ + media_reader::ImageBufDisplaySet *result, + std::shared_ptr response_count) { auto check_and_respond = [=]() mutable { (*response_count)--; @@ -507,27 +504,32 @@ void ViewportFrameQueueActor::append_overlays_data( // for the layout. Some layout plugins are provided by python, // and could be slower so we have 0.25s timeout result->finalise(); - request(viewport_layout_manager_, std::chrono::milliseconds(250), viewport_layout_atom_v, viewport_layout_mode_name_, v).then( - [=](const media_reader::ImageSetLayoutDataPtr &layout_data) mutable { - - result->set_layout_data(layout_data); - - // here we set the layout transform matrix on the - // image buffers, if available - for (int i = 0; i< result->num_onscreen_images(); ++i) { - if (i <= (int)layout_data->image_transforms_.size()) { - media_reader::ImageBufPtr &im = result->onscreen_image_m(i); - im.set_layout_transform(layout_data->image_transforms_[i]); + request( + viewport_layout_manager_, + std::chrono::milliseconds(250), + viewport_layout_atom_v, + viewport_layout_mode_name_, + v) + .then( + [=](const media_reader::ImageSetLayoutDataPtr &layout_data) mutable { + result->set_layout_data(layout_data); + + // here we set the layout transform matrix on the + // image buffers, if available + for (int i = 0; i < result->num_onscreen_images(); ++i) { + if (i <= (int)layout_data->image_transforms_.size()) { + media_reader::ImageBufPtr &im = result->onscreen_image_m(i); + im.set_layout_transform(layout_data->image_transforms_[i]); + } } - } - rp.deliver(v); - }, - [=](caf::error & err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - result->finalise(); - rp.deliver(v); - }); + rp.deliver(v); + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + result->finalise(); + rp.deliver(v); + }); } else { result->finalise(); rp.deliver(v); @@ -535,7 +537,7 @@ void ViewportFrameQueueActor::append_overlays_data( } }; - for (auto p: viewport_overlay_plugins_) { + for (auto p : viewport_overlay_plugins_) { utility::Uuid overlay_plugin_uuid = p.first; caf::actor overlay_plugin = p.second; @@ -545,20 +547,19 @@ void ViewportFrameQueueActor::append_overlays_data( infinite, prepare_overlay_render_data_atom_v, result->onscreen_image(img_idx), - viewport_name_).then( - [=](const utility::BlindDataObjectPtr &bdata) mutable { - - result->onscreen_image_m(img_idx).add_plugin_blind_data( - overlay_plugin_uuid, bdata); - check_and_respond(); - - }, - [=](caf::error & err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - check_and_respond(); - }); + viewport_name_, + playhead_.uuid()) + .then( + [=](const utility::BlindDataObjectPtr &bdata) mutable { + result->onscreen_image_m(img_idx).add_plugin_blind_data( + overlay_plugin_uuid, bdata); + check_and_respond(); + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + check_and_respond(); + }); } - } void ViewportFrameQueueActor::clear_images_from_old_playheads() { @@ -594,7 +595,7 @@ ViewportFrameQueueActor::predicted_playhead_position(const utility::time_point & // show we actually show. auto estimate_playhead_position_at_next_redraw = request_receive_wait( *sys, - playhead_, + playhead_.actor(), std::chrono::milliseconds(100), playhead::position_atom_v, when, @@ -615,28 +616,25 @@ ViewportFrameQueueActor::predicted_playhead_position(const utility::time_point & // The key is that we only change this phase adjustment occasionally as the phase // between the playhead and the video refresh beats drifts. - const long playhead_velocity_ct = long(double(video_refresh_period.count())*playhead_velocity_); + const long playhead_velocity_ct = + long(double(video_refresh_period.count()) * playhead_velocity_); timebase::flicks phase_adjusted_tp = estimate_playhead_position_at_next_redraw + playhead_vid_sync_phase_adjust_; timebase::flicks rounded_phase_adjusted_tp = timebase::flicks( - playhead_velocity_ct * - (phase_adjusted_tp.count() / playhead_velocity_ct)); + playhead_velocity_ct * (phase_adjusted_tp.count() / playhead_velocity_ct)); const double phase = timebase::to_seconds(phase_adjusted_tp - rounded_phase_adjusted_tp) / timebase::to_seconds(video_refresh_period); if (phase < 0.1 || phase > 0.9) { playhead_vid_sync_phase_adjust_ = timebase::flicks( - playhead_velocity_ct / 2 - - estimate_playhead_position_at_next_redraw.count() + + playhead_velocity_ct / 2 - estimate_playhead_position_at_next_redraw.count() + playhead_velocity_ct * - (estimate_playhead_position_at_next_redraw.count() / - playhead_velocity_ct)); + (estimate_playhead_position_at_next_redraw.count() / playhead_velocity_ct)); phase_adjusted_tp = estimate_playhead_position_at_next_redraw + playhead_vid_sync_phase_adjust_; rounded_phase_adjusted_tp = timebase::flicks( - playhead_velocity_ct * - (phase_adjusted_tp.count() / playhead_velocity_ct)); + playhead_velocity_ct * (phase_adjusted_tp.count() / playhead_velocity_ct)); } return rounded_phase_adjusted_tp; @@ -805,58 +803,47 @@ double ViewportFrameQueueActor::average_video_refresh_period() const { return timebase::to_seconds(t) / (double(deltas.size() - 16)); } -caf::typed_response_promise ViewportFrameQueueActor::set_playhead(caf::actor playhead, const bool prefetch_inital_image) -{ +caf::typed_response_promise ViewportFrameQueueActor::set_playhead( + utility::UuidActor playhead, const bool prefetch_inital_image) { auto self = caf::actor_cast(this); - if (playhead_broadcast_group_) { - // stop getting broadcasts from the previous playhead - anon_send(playhead_broadcast_group_, broadcast::leave_broadcast_atom_v, self); - playhead_broadcast_group_ = caf::actor(); - } - auto rp = make_response_promise(); // join the playhead's broadcast group - image buffers are streamed to // us via the broacast group - request(playhead, infinite, broadcast::join_broadcast_atom_v, self).then( - [=](const bool) mutable { - - // Get the 'key' child playhead UUID - request(playhead, infinite, playhead::key_child_playhead_atom_v).then( - [=](utility::UuidVector curr_playhead_uuids) mutable { - - if (playhead_) - demonitor(playhead_); - playhead_ = playhead; - monitor(playhead_); - - // this message will make the playhead re-broadcaset the media_source_atom - // event to it's 'broacast' group (of which we are a member). This info - // from the playhead is received in a message handler below and we - // send on the info about the media source to our colour pipeline - // which needs to do some set-up. - send(playhead_, playhead::media_source_atom_v, true, true); - send(playhead_, playhead::jump_atom_v); - request(playhead_, infinite, playhead::velocity_atom_v).then( - [=](float v) { - playhead_velocity_ = v; + request(playhead.actor(), infinite, broadcast::join_broadcast_atom_v, self) + .then( + [=](const bool) mutable { + // Get the 'key' child playhead UUID + request(playhead.actor(), infinite, playhead::key_child_playhead_atom_v) + .then( + [=](utility::UuidVector curr_playhead_uuids) mutable { + if (playhead_) + demonitor(playhead_.actor()); + playhead_ = playhead; + monitor(playhead_.actor()); + + // this message will make the playhead re-broadcaset the + // media_source_atom event to it's 'broacast' group (of which we are + // a member). This info from the playhead is received in a message + // handler below and we send on the info about the media source to + // our colour pipeline which needs to do some set-up. + send(playhead_.actor(), playhead::media_source_atom_v, true, true); + send(playhead_.actor(), playhead::jump_atom_v); + request(playhead_.actor(), infinite, playhead::velocity_atom_v) + .then( + [=](float v) { playhead_velocity_ = v; }, + [=](caf::error &err) {}); + + if (curr_playhead_uuids.empty()) + return; + current_key_sub_playhead_id_ = curr_playhead_uuids.back(); + curr_playhead_uuids.pop_back(); + sub_playhead_ids_ = curr_playhead_uuids; + rp.deliver(true); }, - [=](caf::error &err) {}); - - if (curr_playhead_uuids.empty()) return; - current_key_sub_playhead_id_ = curr_playhead_uuids.back(); - curr_playhead_uuids.pop_back(); - sub_playhead_ids_ = curr_playhead_uuids; - rp.deliver(true); - - }, - [=](const error &err) mutable { - rp.deliver(err); - }); - }, - [=](const error &err) mutable { - rp.deliver(err); - }); + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); return rp; } \ No newline at end of file diff --git a/src/ui/viewport/src/viewport_layout_plugin.cpp b/src/ui/viewport/src/viewport_layout_plugin.cpp index ec96a8383..c4c9092b8 100644 --- a/src/ui/viewport/src/viewport_layout_plugin.cpp +++ b/src/ui/viewport/src/viewport_layout_plugin.cpp @@ -13,15 +13,15 @@ using namespace xstudio::utility; using namespace xstudio::ui::viewport; ViewportLayoutPlugin::ViewportLayoutPlugin( - caf::actor_config &cfg, - const utility::JsonStore &init_settings) : - plugin::StandardPlugin(cfg, init_settings.value("name", "ViewportLayoutPlugin"), init_settings), is_python_plugin_(init_settings.value("is_python", false)) -{ + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::StandardPlugin( + cfg, init_settings.value("name", "ViewportLayoutPlugin"), init_settings), + is_python_plugin_(init_settings.value("is_python", false)) { init(); } void ViewportLayoutPlugin::init() { - + handler_ = { [=](utility::get_event_group_atom) -> caf::actor { if (!event_group_) { @@ -35,47 +35,50 @@ void ViewportLayoutPlugin::init() { }, [=](viewport_layout_atom, const std::string &layout_mode, - const JsonStore &python_plugin_layout) { - + const JsonStore &python_plugin_layout) { // this comes in from Python - we can't get exchange size_t between // C++ and python, PyBind and caf get their knickers in a twist trying // to infer the integer type, so the hash is stringified at both // ends. try { - + size_t hash; auto h = python_plugin_layout["hash"].get(); sscanf(h.c_str(), "%zu", &hash); - delegate(caf::actor_cast(this), viewport_layout_atom_v, layout_mode, python_plugin_layout, hash); - - } catch ( std::exception & e ) { + delegate( + caf::actor_cast(this), + viewport_layout_atom_v, + layout_mode, + python_plugin_layout, + hash); + + } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - }, [=](viewport_layout_atom, const std::string &layout_mode, const JsonStore &python_plugin_layout, - size_t hash) { - - media_reader::ImageSetLayoutDataPtr layout_data = python_layout_data_to_ours(python_plugin_layout); + size_t hash) { + media_reader::ImageSetLayoutDataPtr layout_data = + python_layout_data_to_ours(python_plugin_layout); if (layout_data) { layouts_cache_[layout_mode][hash] = layout_data; - auto p = pending_responses_.find(hash); + auto p = pending_responses_.find(hash); if (p != pending_responses_.end()) { - for(auto &rp: p->second) { + for (auto &rp : p->second) { rp.deliver(layout_data); } pending_responses_.erase(p); } } - }, [=](viewport_layout_atom, const std::string &layout_mode, - const media_reader::ImageBufDisplaySetPtr &image_set) -> result { + const media_reader::ImageBufDisplaySetPtr &image_set) + -> result { auto rp = make_response_promise(); - auto p = layouts_cache_[layout_mode].find(image_set->images_layout_hash()); + auto p = layouts_cache_[layout_mode].find(image_set->images_layout_hash()); if (p == layouts_cache_[layout_mode].end()) { __do_layout(layout_mode, image_set, rp); } else { @@ -83,15 +86,19 @@ void ViewportLayoutPlugin::init() { } return rp; }, - [=](viewport_layout_atom, const std::string &layout_name, const xstudio::playhead::AssemblyMode mode, const xstudio::playhead::AutoAlignMode auto_align) { + [=](viewport_layout_atom, + const std::string &layout_name, + const xstudio::playhead::AssemblyMode mode, + const xstudio::playhead::AutoAlignMode auto_align) { // used by Python ViewportLayoutPlugin api add_layout_mode(layout_name, mode, auto_align); }, - }; + }; make_behavior(); - settings_toggle_ = add_boolean_attribute(module::Module::name(), module::Module::name(), true); + settings_toggle_ = + add_boolean_attribute(module::Module::name(), module::Module::name(), true); settings_toggle_->expose_in_ui_attrs_group("layout_plugins"); // this tells the pop-up menu for Compare/Layouts that this plugin doesn't @@ -99,16 +106,19 @@ void ViewportLayoutPlugin::init() { settings_toggle_->set_role_data(module::Attribute::DisabledValue, true); connect_to_ui(); - - layouts_manager_ = - system().registry().template get(viewport_layouts_manager); - gobal_playhead_events_ = system().registry().template get(global_playhead_events_actor); - anon_send(layouts_manager_, ui::viewport::viewport_layout_atom_v, caf::actor_cast(this), Module::name()); + layouts_manager_ = system().registry().template get(viewport_layouts_manager); + gobal_playhead_events_ = + system().registry().template get(global_playhead_events_actor); + anon_send( + layouts_manager_, + ui::viewport::viewport_layout_atom_v, + caf::actor_cast(this), + Module::name()); } void ViewportLayoutPlugin::on_exit() { - layouts_manager_ = caf::actor(); + layouts_manager_ = caf::actor(); gobal_playhead_events_ = caf::actor(); plugin::StandardPlugin::on_exit(); } @@ -116,17 +126,18 @@ void ViewportLayoutPlugin::on_exit() { void ViewportLayoutPlugin::do_layout( const std::string &layout_mode, const media_reader::ImageBufDisplaySetPtr &image_set, - media_reader::ImageSetLayoutData &layout_data - ) -{ + media_reader::ImageSetLayoutData &layout_data) { // For the default layout, the image(s) are not transformed in any way. - // We just need to set the aspect of the layout, which is the same as + // We just need to set the aspect of the layout, which is the same as // the image aspect for the hero image - const media_reader::ImageBufPtr &hero_image = image_set->onscreen_image(image_set->hero_sub_playhead_index()); + const media_reader::ImageBufPtr &hero_image = + image_set->onscreen_image(image_set->hero_sub_playhead_index()); if (hero_image) { - layout_data.layout_aspect_ = hero_image->pixel_aspect()*hero_image->image_size_in_pixels().x/hero_image->image_size_in_pixels().y; + layout_data.layout_aspect_ = hero_image->pixel_aspect() * + hero_image->image_size_in_pixels().x / + hero_image->image_size_in_pixels().y; } else { - layout_data.layout_aspect_ = 16.0/9.0f; + layout_data.layout_aspect_ = 16.0 / 9.0f; } // this fills image_transform_matrices with unity matrices @@ -134,51 +145,43 @@ void ViewportLayoutPlugin::do_layout( // we only draw the 'hero' image. No compositing or any other layout stuff // to do. - layout_data.image_draw_order_hint_ = std::vector(1, image_set->hero_sub_playhead_index()); - + layout_data.image_draw_order_hint_ = + std::vector(1, image_set->hero_sub_playhead_index()); } void ViewportLayoutPlugin::__do_layout( const std::string &layout_mode, const media_reader::ImageBufDisplaySetPtr &image_set, - caf::typed_response_promise rp - ) -{ + caf::typed_response_promise rp) { // if we have an event_group_ actor, this means there is a Python // plugin for doing viewport layouts. We send a message to the event_group_ // which calls the do_layout' function of if (is_python_plugin_ && event_group_) { - // We need python side to receive the hash for the image layout inputs (image sizes and pixel aspects) - // but the hash is size_t which we can't interchange directly with python as it handles integers - // differently, so we stringify the hash. Python plugin then sends back the layout data - // with the hash as a string which we need to decode to size_t again. + // We need python side to receive the hash for the image layout inputs (image sizes and + // pixel aspects) but the hash is size_t which we can't interchange directly with python + // as it handles integers differently, so we stringify the hash. Python plugin then + // sends back the layout data with the hash as a string which we need to decode to + // size_t again. send( event_group_, viewport_layout_atom_v, layout_mode, image_set->as_json(), - fmt::format("{}", - image_set->images_layout_hash()) - ); + fmt::format("{}", image_set->images_layout_hash())); pending_responses_[image_set->images_layout_hash()].push_back(rp); return; } auto layout_data = new media_reader::ImageSetLayoutData; - // this will callback to the plugins' implementation (or our default + // this will callback to the plugins' implementation (or our default // implementation) above. - do_layout( - layout_mode, - image_set, - *layout_data - ); + do_layout(layout_mode, image_set, *layout_data); auto r = media_reader::ImageSetLayoutDataPtr(layout_data); rp.deliver(r); layouts_cache_[layout_mode][image_set->images_layout_hash()] = r; - } /*void HUDPluginBase::hud_element_qml( @@ -206,22 +209,20 @@ void ViewportLayoutPlugin::__do_layout( } }*/ -media_reader::ImageSetLayoutDataPtr ViewportLayoutPlugin::python_layout_data_to_ours( - const utility::JsonStore &python_data -) const { +media_reader::ImageSetLayoutDataPtr +ViewportLayoutPlugin::python_layout_data_to_ours(const utility::JsonStore &python_data) const { auto result = new media_reader::ImageSetLayoutData; try { - if (python_data.contains("layout_aspect_ratio")) { result->layout_aspect_ = python_data["layout_aspect_ratio"].get(); } if (python_data.contains("image_draw_order")) { if (python_data["image_draw_order"].is_array()) { - for (const auto &v: python_data["image_draw_order"]) { + for (const auto &v : python_data["image_draw_order"]) { result->image_draw_order_hint_.push_back(v.get()); } } else { @@ -233,17 +234,18 @@ media_reader::ImageSetLayoutDataPtr ViewportLayoutPlugin::python_layout_data_to_ if (python_data.contains("transforms")) { if (python_data["transforms"].is_array()) { - for (const auto &v: python_data["transforms"]) { + for (const auto &v : python_data["transforms"]) { if (v.is_array() && v.size() == 3) { float tx = v[0].get(); float ty = v[1].get(); - float s = v[2].get(); + float s = v[2].get(); Imath::M44f m; m.translate(Imath::V3f(tx, ty, 0.0f)); m.scale(Imath::V3f(s, s, 1.0f)); result->image_transforms_.push_back(m); } else { - throw std::runtime_error("Elements of transforms entry must be an array of 3 floats."); + throw std::runtime_error( + "Elements of transforms entry must be an array of 3 floats."); } } } else { @@ -253,36 +255,33 @@ media_reader::ImageSetLayoutDataPtr ViewportLayoutPlugin::python_layout_data_to_ throw std::runtime_error("expected transforms entry"); } - } catch (std::exception & e) { + } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } return media_reader::ImageSetLayoutDataPtr(result); } -void ViewportLayoutPlugin::add_layout_settings_attribute(module::Attribute *attr, const std::string &layout_name) { +void ViewportLayoutPlugin::add_layout_settings_attribute( + module::Attribute *attr, const std::string &layout_name) { attr->expose_in_ui_attrs_group(layout_name + " Settings"); // this means the 'settings' button WILL be visible! settings_toggle_->set_role_data(module::Attribute::DisabledValue, false); } void ViewportLayoutPlugin::add_viewport_layout_qml_overlay( - const std::string &layout_name, - const std::string &qml_code) { + const std::string &layout_name, const std::string &qml_code) { auto attr = add_boolean_attribute(layout_name, layout_name, true); attr->set_role_data(module::Attribute::QmlCode, qml_code); attr->expose_in_ui_attrs_group("viewport_overlay_plugins"); - } void ViewportLayoutPlugin::add_layout_mode( - const std::string &name, - const playhead::AssemblyMode mode, - const playhead::AutoAlignMode default_auto_align - ) -{ + const std::string &name, + const playhead::AssemblyMode mode, + const playhead::AutoAlignMode default_auto_align) { request( layouts_manager_, @@ -291,7 +290,8 @@ void ViewportLayoutPlugin::add_layout_mode( caf::actor_cast(this), name, mode, - default_auto_align).then( + default_auto_align) + .then( [=](bool accepted) { if (accepted) { auto layout_toggle = add_string_attribute(name, name, ""); @@ -305,9 +305,7 @@ void ViewportLayoutPlugin::add_layout_mode( } void ViewportLayoutPlugin::attribute_changed( - const utility::Uuid &attribute_uuid, const int role - ) -{ + const utility::Uuid &attribute_uuid, const int role) { // this forces re-computation of the layout geometry layouts_cache_.clear(); // now force viewports to redraw @@ -315,13 +313,13 @@ void ViewportLayoutPlugin::attribute_changed( StandardPlugin::attribute_changed(attribute_uuid, role); } -ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) : caf::event_based_actor(cfg) -{ +ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) + : caf::event_based_actor(cfg) { spdlog::debug("Created ViewportLayoutManager"); print_on_exit(this, "ViewportLayoutManager"); system().registry().put(viewport_layouts_manager, this); - event_group_ = spawn(this); + event_group_ = spawn(this); link_to(event_group_); behavior_ = { @@ -355,7 +353,8 @@ ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) : caf::even fmt::format( "A viewport layout name \"{}\" is already registered.", layout_name)); } - viewport_layouts_[layout_name] = std::make_pair(layout_actor,std::make_pair(default_align_mode, mode)); + viewport_layouts_[layout_name] = + std::make_pair(layout_actor, std::make_pair(default_align_mode, mode)); return true; }, [=](viewport_layout_atom, @@ -371,8 +370,9 @@ ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) : caf::even } return viewport_layouts_[layout_name].first; }, - [=](playhead::compare_mode_atom, - const std::string &layout_name) -> result> { + [=](playhead::compare_mode_atom, const std::string &layout_name) + -> result< + std::pair> { if (not viewport_layouts_.contains(layout_name)) { return make_error( xstudio_error::error, @@ -384,11 +384,9 @@ ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) : caf::even }}; spawn_plugins(); - } -ViewportLayoutManager::~ViewportLayoutManager() { -} +ViewportLayoutManager::~ViewportLayoutManager() {} void ViewportLayoutManager::on_exit() { viewport_layouts_.clear(); @@ -399,47 +397,34 @@ void ViewportLayoutManager::spawn_plugins() { // get the plugin manager auto pm = system().registry().template get(plugin_manager_registry); - - const auto ptype = plugin_manager::PluginType( - plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER - ); + + const auto ptype = + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER); // SPAWN C++ PLUGINS (Python plugins are loaded in python startup script) // get details of viewport layout plugins - request( - pm, - infinite, utility::detail_atom_v, - ptype).then( - - [=](const std::vector &renderer_plugin_details) { - - // loop over plugin details - for (const auto &pd : renderer_plugin_details) { - - // instance the plugin. Each plugin automatically registeres - // itself with this class on construction (see above) - utility::JsonStore j; - j["name"] = pd.name_; - j["is_python_plugin"] = false; - request( - pm, - infinite, - plugin_manager::spawn_plugin_atom_v, - pd.uuid_, - j - ).then( - [=](caf::actor) { - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - - } - }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - - + request(pm, infinite, utility::detail_atom_v, ptype) + .then( + + [=](const std::vector &renderer_plugin_details) { + // loop over plugin details + for (const auto &pd : renderer_plugin_details) { + + // instance the plugin. Each plugin automatically registeres + // itself with this class on construction (see above) + utility::JsonStore j; + j["name"] = pd.name_; + j["is_python_plugin"] = false; + request(pm, infinite, plugin_manager::spawn_plugin_atom_v, pd.uuid_, j) + .then( + [=](caf::actor) {}, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); } \ No newline at end of file diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 1ba2b4814..94a27fbf3 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -110,6 +110,8 @@ 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) { + if (!actor) + return; source->request(actor, caf::infinite, broadcast::join_broadcast_atom_v) .then( [=](const bool) mutable {}, @@ -119,6 +121,8 @@ void xstudio::utility::join_broadcast(caf::event_based_actor *source, caf::actor } void xstudio::utility::join_broadcast(caf::blocking_actor *source, caf::actor actor) { + if (!actor) + return; source->request(actor, caf::infinite, broadcast::join_broadcast_atom_v) .receive( [=](const bool) mutable {}, @@ -128,6 +132,8 @@ void xstudio::utility::join_broadcast(caf::blocking_actor *source, caf::actor ac } void xstudio::utility::leave_broadcast(caf::blocking_actor *source, caf::actor actor) { + if (!actor || !caf::actor_cast(source)) + return; source->request(actor, caf::infinite, broadcast::leave_broadcast_atom_v) .receive( [=](const bool) mutable {}, @@ -137,6 +143,8 @@ void xstudio::utility::leave_broadcast(caf::blocking_actor *source, caf::actor a } void xstudio::utility::leave_broadcast(caf::event_based_actor *source, caf::actor actor) { + if (!actor || !caf::actor_cast(source)) + return; source->request(actor, caf::infinite, broadcast::leave_broadcast_atom_v) .then( [=](const bool) mutable {}, @@ -146,9 +154,13 @@ void xstudio::utility::leave_broadcast(caf::event_based_actor *source, caf::acto } void xstudio::utility::join_event_group(caf::event_based_actor *source, caf::actor actor) { + if (!actor) + return; source->request(actor, caf::infinite, utility::get_event_group_atom_v) .then( [=](caf::actor grp) mutable { + if (!grp) + return; source->request(grp, caf::infinite, broadcast::join_broadcast_atom_v) .then( [=](const bool) mutable {}, @@ -162,9 +174,13 @@ void xstudio::utility::join_event_group(caf::event_based_actor *source, caf::act } void xstudio::utility::leave_event_group(caf::event_based_actor *source, caf::actor actor) { + if (!actor) + return; source->request(actor, caf::infinite, utility::get_event_group_atom_v) .then( [=](caf::actor grp) mutable { + if (!grp) + return; source->request(grp, caf::infinite, broadcast::leave_broadcast_atom_v) .then( [=](const bool) mutable {}, diff --git a/src/utility/src/notification_handler.cpp b/src/utility/src/notification_handler.cpp index d3d05d99d..b61c1d1b6 100644 --- a/src/utility/src/notification_handler.cpp +++ b/src/utility/src/notification_handler.cpp @@ -5,6 +5,41 @@ namespace xstudio::utility { +Notification::Notification(const JsonStore &jsn) { + uuid_ = jsn.value("uuid", Uuid()); + + auto type = jsn.value("type", "UNKNOWN"); + type_ = NT_UNKNOWN; + + if (type == "INFO") + type_ = NT_INFO; + else if (type == "WARN") + type_ = NT_WARN; + else if (type == "PROCESSING") + type_ = NT_PROCESSING; + else if (type == "PROGRESS_RANGE") + type_ = NT_PROGRESS_RANGE; + else if (type == "PROGRESS_PERCENTAGE") + type_ = NT_PROGRESS_PERCENTAGE; + + expires_ = jsn.value("expires", utility::sysclock::now()); + + if (type_ == NT_PROGRESS_RANGE) { + progress_minimum_ = jsn.value("min_progress", 0.f); + progress_maximum_ = jsn.value("max_progress", 0.f); + progress_ = jsn.value("progress", 0.f); + text_ = jsn.value("_text", ""); + } else if (type_ == NT_PROGRESS_PERCENTAGE) { + progress_minimum_ = jsn.value("min_progress", 0.f); + progress_maximum_ = jsn.value("max_progress", 0.f); + progress_ = jsn.value("progress", 0.f); + text_ = jsn.value("_text", ""); + } else { + text_ = jsn.value("_text", ""); + } +} + + void to_json(nlohmann::json &j, const Notification &n) { j["uuid"] = n.uuid_; switch (n.type_) { @@ -35,14 +70,17 @@ void to_json(nlohmann::json &j, const Notification &n) { j["progress"] = n.progress_; j["progress_percent"] = n.progress_percentage(); j["text"] = n.progress_text_range(); + j["_text"] = n.text_; } else if (n.type_ == NT_PROGRESS_PERCENTAGE) { j["min_progress"] = n.progress_minimum_; j["max_progress"] = n.progress_maximum_; j["progress"] = n.progress_; j["progress_percent"] = n.progress_percentage(); j["text"] = n.progress_text_percentage(); + j["_text"] = n.text_; } else { - j["text"] = n.text_; + j["text"] = n.text_; + j["_text"] = n.text_; } } @@ -57,6 +95,12 @@ NotificationHandler::message_handler(caf::event_based_actor *act, caf::actor eve return caf::message_handler( {[=](notification_atom) -> JsonStore { return digest(); }, + [=](notification_atom, notification_atom) -> std::vector { + auto result = std::vector(); + result.reserve(notifications_.size()); + result.insert(result.end(), notifications_.begin(), notifications_.end()); + return result; + }, [=](notification_atom, const Uuid &uuid) -> bool { auto result = remove_notification(uuid); if (result) diff --git a/src/utility/src/remote_session_file.cpp b/src/utility/src/remote_session_file.cpp index 20ab3d800..756913f92 100644 --- a/src/utility/src/remote_session_file.cpp +++ b/src/utility/src/remote_session_file.cpp @@ -14,7 +14,7 @@ using namespace xstudio::utility; -static std::regex parse_re(R"((.+)_(.+)_(.+)_(.+))"); +static std::regex parse_re(R"((.+)_.+_(.+)_(.+))"); RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { // build entry.. @@ -30,9 +30,8 @@ RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { std::smatch match; if (std::regex_search(file_name, match, parse_re)) { session_name_ = match[1]; - sync_ = (match[2] == "sync" ? true : false); - host_ = match[3]; - port_ = std::stoi(match[4], nullptr, 0); + host_ = match[2]; + port_ = std::stoi(match[3], nullptr, 0); } else { throw std::runtime_error("Invalid remote session file. " + file_name); } @@ -53,8 +52,8 @@ RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { last_write_ = fs::last_write_time(filepath()); } -RemoteSessionFile::RemoteSessionFile(const std::string path, const int port, const bool sync) - : path_(std::move(path)), port_(port), sync_(sync) { +RemoteSessionFile::RemoteSessionFile(const std::string path, const int port) + : path_(std::move(path)), port_(port) { pid_ = get_pid(); for (auto i = 0; i < 100; i++) { @@ -72,15 +71,13 @@ RemoteSessionFile::RemoteSessionFile(const std::string path, const int port, con RemoteSessionFile::RemoteSessionFile( const std::string path, const int port, - const bool sync, const std::string session_name, const std::string host, const bool force_cleanup) : path_(std::move(path)), session_name_(std::move(session_name)), host_(std::move(host)), - port_(port), - sync_(sync) { + port_(port) { // we can't test if remote is still valid. // so we should only use this to create a new connection ? @@ -176,16 +173,7 @@ RemoteSessionManager::~RemoteSessionManager() { std::optional RemoteSessionManager::first_api() const { for (const auto &i : sessions_) { - if (i.host() == "localhost" and not i.sync()) - return i; - } - - return {}; -} - -std::optional RemoteSessionManager::first_sync() const { - for (const auto &i : sessions_) { - if (i.host() == "localhost" and i.sync()) + if (i.host() == "localhost") return i; } @@ -207,8 +195,8 @@ void RemoteSessionManager::add_session_file(const RemoteSessionFile rsm) { } -std::string RemoteSessionManager::create_session_file(const int port, const bool sync) { - auto i = RemoteSessionFile(path_, port, sync); +std::string RemoteSessionManager::create_session_file(const int port) { + auto i = RemoteSessionFile(path_, port); std::string session_name = i.session_name(); sessions_.emplace_front(i); return session_name; @@ -216,12 +204,10 @@ std::string RemoteSessionManager::create_session_file(const int port, const bool void RemoteSessionManager::create_session_file( const int port, - const bool sync, const std::string session_name, const std::string host, const bool force_cleanup) { - sessions_.emplace_front( - RemoteSessionFile(path_, port, sync, session_name, host, force_cleanup)); + sessions_.emplace_front(RemoteSessionFile(path_, port, session_name, host, force_cleanup)); } void RemoteSessionManager::remove_session(const std::string &session_name) { diff --git a/src/utility/test/frame_time_test.cpp b/src/utility/test/frame_time_test.cpp index 670382fd7..23c870c7b 100644 --- a/src/utility/test/frame_time_test.cpp +++ b/src/utility/test/frame_time_test.cpp @@ -106,7 +106,7 @@ TEST(FrameRateTest, Test) { EXPECT_EQ(r24.to_microseconds().count(), std::chrono::microseconds(41666).count()); - EXPECT_EQ((FrameRate(1.0) / r24), 24); + // EXPECT_EQ((FrameRate(1.0) / r24).count(), 24); FrameRate r0; @@ -246,4 +246,3 @@ TEST(FrameRateDurationTest3, Test) { // EXPECT_EQ(a.duration().seconds(), 0.0); // } - diff --git a/src/utility/test/remote_session_file_test.cpp b/src/utility/test/remote_session_file_test.cpp index c464a2044..265c6c48a 100644 --- a/src/utility/test/remote_session_file_test.cpp +++ b/src/utility/test/remote_session_file_test.cpp @@ -5,8 +5,8 @@ using namespace xstudio::utility; TEST(SessionFileTest, Test) { - RemoteSessionFile local(".", 12345, false); - RemoteSessionFile remote(".", 12346, false, "remotetest", "cookham", true); + RemoteSessionFile local(".", 12345); + RemoteSessionFile remote(".", 12346, "remotetest", "cookham", true); RemoteSessionFile remote2(remote.filepath()); remote2.set_remove_on_delete(); @@ -21,8 +21,8 @@ TEST(SessionFileTest, Test) { TEST(SessionFileManager, Test) { RemoteSessionManager rsm("."); rsm.create_session_file(1234); - rsm.create_session_file(12346, false, "remotetest", "cookham", true); - rsm.create_session_file(12346, false, "local", "localhost"); + rsm.create_session_file(12346, "remotetest", "cookham", true); + rsm.create_session_file(12346, "local", "localhost"); auto first_local = rsm.first_api(); auto find_remote = rsm.find("remotetest"); diff --git a/ui/qml/xstudio/assets/icons/communities.svg b/ui/qml/xstudio/assets/icons/communities.svg new file mode 100644 index 000000000..655e68d5b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/communities.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/view_object_track.svg b/ui/qml/xstudio/assets/icons/view_object_track.svg new file mode 100644 index 000000000..3651c31f8 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/view_object_track.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/helpers/XsDialogHelpers.qml b/ui/qml/xstudio/helpers/XsDialogHelpers.qml index ed8866979..a3cc3f41c 100644 --- a/ui/qml/xstudio/helpers/XsDialogHelpers.qml +++ b/ui/qml/xstudio/helpers/XsDialogHelpers.qml @@ -117,6 +117,7 @@ Item { id: errorDialog title: "Error" property string body: "" + property bool is_error: true width: 400 height: 200 @@ -140,6 +141,7 @@ Item { width: 40 height: 40 source: "qrc:/icons/error.svg" + visible: is_error } XsText { Layout.fillWidth: true @@ -173,6 +175,15 @@ Item { loader.sourceComponent = errorDialog loader.item.title = error_title loader.item.body = error_body + loader.item.is_error = true + showDialog(undefined) + } + + function messageDialogFunc(message_title, message_body) { + loader.sourceComponent = errorDialog + loader.item.title = message_title + loader.item.body = message_body + loader.item.is_error = false showDialog(undefined) } diff --git a/ui/qml/xstudio/layout_framework/XsViewContainer.qml b/ui/qml/xstudio/layout_framework/XsViewContainer.qml index daf61de38..c9f19c62a 100644 --- a/ui/qml/xstudio/layout_framework/XsViewContainer.qml +++ b/ui/qml/xstudio/layout_framework/XsViewContainer.qml @@ -152,15 +152,16 @@ Item { Rectangle { + id: theTabBar color: XsStyleSheet.panelBgColor visible: tabsVisible - id: theTabBar width: parent.width height: tabsHeight + clip: true RowLayout { - + anchors.fill: parent spacing: 0 Repeater { @@ -191,17 +192,37 @@ Item { } + Item { + Layout.fillWidth: true + } + + // XsSecondaryButton{ + // id: menuBtn + // Layout.minimumWidth: buttonSize + panelPadding + // Layout.preferredWidth: buttonSize + panelPadding + // Layout.maximumWidth: buttonSize + panelPadding + // Layout.preferredHeight: tabsHeight + // imgSrc: "qrc:/icons/menu.svg" + // isActive: panelMenu.visible + // forcedBgColorNormal: XsStyleSheet.baseColor + // onClicked: { + // panelMenu.showMenu(menuBtn, 0, 0) + // } + // } + } + XsSecondaryButton{ id: menuBtn - width: buttonSize + width: buttonSize + panelPadding height: tabsHeight anchors.right: parent.right - anchors.rightMargin: panelPadding + // anchors.rightMargin: panelPadding anchors.verticalCenter: parent.verticalCenter imgSrc: "qrc:/icons/menu.svg" isActive: panelMenu.visible + forcedBgColorNormal: XsStyleSheet.baseColor onClicked: { panelMenu.showMenu(menuBtn, 0, 0) } diff --git a/ui/qml/xstudio/qml.qrc b/ui/qml/xstudio/qml.qrc index b70587035..63dc009f5 100644 --- a/ui/qml/xstudio/qml.qrc +++ b/ui/qml/xstudio/qml.qrc @@ -84,6 +84,7 @@ views/timeline/delegates/XsDelegateGap.qml views/timeline/delegates/XsDelegateStack.qml views/timeline/delegates/XsDelegateVideoTrack.qml + views/timeline/delegates/XsDelegateTrack.qml views/timeline/widgets/XsClipItem.qml views/timeline/widgets/XsClipDragBoth.qml views/timeline/widgets/XsClipDragLeft.qml @@ -118,9 +119,10 @@ views/viewport/widgets/XsViewportToolBar.qml views/viewport/widgets/XsViewportInfoBar.qml views/viewport/widgets/toolbar/XsViewerAnyMenuButton.qml - views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml + views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml views/viewport/widgets/toolbar/XsViewerToggleButton.qml views/viewport/widgets/toolbar/XsViewerSourceSelectorButton.qml + views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml views/viewport/widgets/toolbar/XsViewerHUDButton.qml views/viewport/widgets/toolbar/XsViewerToolbarButtonBase.qml views/viewport/widgets/toolbar/XsViewerToolbarValueScrubber.qml @@ -183,6 +185,7 @@ widgets/dialogs/hotkeys/delegates/XsHotkeyDetails.qml widgets/dialogs/preferences/XsPreferencesDialog.qml widgets/dialogs/preferences/delegates/XsColourPreference.qml + widgets/dialogs/preferences/delegates/XsCompareModePref.qml widgets/dialogs/preferences/delegates/XsPreferenceCategory.qml widgets/dialogs/preferences/delegates/XsFloatPreference.qml widgets/dialogs/preferences/delegates/XsIntegerPreference.qml @@ -282,6 +285,7 @@ assets/icons/chevron_right.svg assets/icons/close.svg assets/icons/cloud-download.svg + assets/icons/communities.svg assets/icons/content_copy.svg assets/icons/content_cut.svg assets/icons/content_paste.svg @@ -384,6 +388,7 @@ assets/icons/video_cam.svg assets/icons/view.svg assets/icons/view_grid.svg + assets/icons/view_object_track.svg assets/icons/view_timeline.svg assets/icons/visibility.svg assets/icons/visibility_off.svg diff --git a/ui/qml/xstudio/session_data/XsPlayhead.qml b/ui/qml/xstudio/session_data/XsPlayhead.qml index dddc281e1..954cd140c 100644 --- a/ui/qml/xstudio/session_data/XsPlayhead.qml +++ b/ui/qml/xstudio/session_data/XsPlayhead.qml @@ -177,22 +177,35 @@ Item { id: __mediaTransitionFrames attributeTitle: "Media Transition Frames" model: playhead_attrs_model - } + } XsAttributeValue { id: __sourceOffsetFrames attributeTitle: "Source Offset Frames" model: playhead_attrs_model - } + } XsAttributeValue { id: __pinnedSourceMode attributeTitle: "Pinned Source Mode" model: playhead_attrs_model - } + } + + XsAttributeValue { + id: __stream + attributeTitle: "Stream" + model: playhead_attrs_model + } + + XsAttributeValue { + id: __stream_options + role: "combo_box_options" + attributeTitle: "Stream" + model: playhead_attrs_model + } // access the value of the attribute called 'Compare' which is exposed in the - // viewport _toolbar. + // viewport _toolbar. XsAttributeValue { id: __compare_mode attributeTitle: "Compare" @@ -229,13 +242,15 @@ Item { property alias timecode: __playheadTimeCode.value property alias timecodeAsFrame: __playheadTimeCodeAsFrame.value property alias keySubplayheadIndex: __playheadKeyPlayheadIndex.value - property alias numSubPlayheads: __playheadNumSubPlayheads.value + property alias numSubPlayheads: __playheadNumSubPlayheads.value property alias scrubbingFrames: __playheadScrubbing.value property alias mediaTransitionFrames: __mediaTransitionFrames.value property alias sourceOffsetFrames: __sourceOffsetFrames.value property alias pinnedSourceMode: __pinnedSourceMode.value property alias compare_mode: __compare_mode.value property alias timelineMode: __timelineMode.value + property alias current_image_stream: __stream.value + property alias image_stream_options: __stream_options.value /* This gives us a 'model' with one row - the row is the attribute data for the "Auto Align" attribute of the current playhead. We use it below diff --git a/ui/qml/xstudio/session_data/XsSessionData.qml b/ui/qml/xstudio/session_data/XsSessionData.qml index 706e906c7..176b139d2 100644 --- a/ui/qml/xstudio/session_data/XsSessionData.qml +++ b/ui/qml/xstudio/session_data/XsSessionData.qml @@ -28,7 +28,7 @@ Item { // a new session has been created/loaded. Pick the first playlist // after 200ms to give the session data a chance to build itself callbackTimer.setTimeout(function() { return function() { - mediaSelectionModel.select( + sessionSelectionModel.select( helpers.createItemSelection([session.searchRecursive("Playlist", "typeRole")]), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) }}(), 200); @@ -89,7 +89,7 @@ Item { } - function createPlaylist(name, sync=true) { + function createPlaylist(name, sync=true, select=true) { // always add at end of playlists list var insert_row = rowCount(index(0,0)); @@ -102,9 +102,10 @@ Item { index(0,0) )[0] - sessionSelectionModel.setCurrentIndex( - idx, - ItemSelectionModel.ClearAndSelect) + if(select) + sessionSelectionModel.setCurrentIndex( + idx, + ItemSelectionModel.ClearAndSelect) return idx } @@ -166,7 +167,7 @@ Item { } // can't find a selected playlist to add to. Need a new playlist - var p = createPlaylist("New Playlist") + var p = createPlaylist(theSessionData.getNextName("Playlist {}")) theSessionData.set(p, true, "expandedRole") // need a delay to allow the playlist node in the model to be // filled out by the backend @@ -270,6 +271,9 @@ Item { onCurrentIndexChanged: { currentMediaContainerIndex = currentIndex } + onSelectionChanged: { + sessionData.setSessionSelection(sessionSelectionModel.selectedIndexes) + } } property alias sessionSelectionModel: sessionSelectionModel @@ -387,9 +391,10 @@ Item { let type = index.model.get(index,"typeRole") if(quuids.length && ["Playlist", "Subset", "Timeline", "ContactSheet"].includes(type)) { // selects the playlist (or subset or timeline) corresponding to 'index' - mediaSelectionModel.select( - helpers.createItemSelection([index]), - ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) + // mediaSelectionModel.select( + // helpers.createItemSelection([index]), + // ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) + callbackTimer.setTimeout(function(plindex, new_items) { return function() { let indexes = [] @@ -413,7 +418,7 @@ Item { if(playhead_index != -1) current_playhead.keySubplayheadIndex = playhead_index } - } ( index, quuids ), 1000); + } ( index, quuids ), 250); } } @@ -508,6 +513,9 @@ Item { onMediaListSearchStringChanged: { session.updateMediaListFilterString(playheadSelectionIndex, mediaListSearchString) } + onPlayheadSelectionIndexChanged: { + session.updateMediaListFilterString(playheadSelectionIndex, mediaListSearchString) + } property alias mediaSelectionModel: mediaSelectionModel diff --git a/ui/qml/xstudio/views/media/XsMediaPanel.qml b/ui/qml/xstudio/views/media/XsMediaPanel.qml index 8f4256d7a..477a641f2 100644 --- a/ui/qml/xstudio/views/media/XsMediaPanel.qml +++ b/ui/qml/xstudio/views/media/XsMediaPanel.qml @@ -90,6 +90,7 @@ Item{ sourceComponent: is_list_view ? list_view : grid_view Layout.fillWidth: true Layout.fillHeight: true + clip: true } Component { diff --git a/ui/qml/xstudio/views/media/XsMediaToolBar.qml b/ui/qml/xstudio/views/media/XsMediaToolBar.qml index 5ea426bba..4918461d5 100644 --- a/ui/qml/xstudio/views/media/XsMediaToolBar.qml +++ b/ui/qml/xstudio/views/media/XsMediaToolBar.qml @@ -116,7 +116,7 @@ Item { XsIntegerValueControl { visible: !is_list_view - Layout.minimumWidth: btnWidth + Layout.minimumWidth: btnWidth*1.5 Layout.preferredWidth: btnWidth*2 Layout.maximumWidth: btnWidth*2 Layout.preferredHeight: btnHeight diff --git a/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailHighlight.qml b/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailHighlight.qml index 7f63a4c27..d4a24c84d 100644 --- a/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailHighlight.qml +++ b/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailHighlight.qml @@ -6,7 +6,7 @@ Item { anchors.fill: thumbnailImgDiv visible: showBorder z: 100 - property int borderThickness: 5 + property int borderThickness: 6 LinearGradient { width: parent.width height: borderThickness diff --git a/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailImage.qml b/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailImage.qml index 3d064e179..9db2d8157 100644 --- a/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailImage.qml +++ b/ui/qml/xstudio/views/media/common_delegates/XsMediaThumbnailImage.qml @@ -14,7 +14,7 @@ Rectangle{ color: "transparent" property bool showBorder: false property bool forcedHover: hovered - property int highlightBorderThickness: 5 + property int highlightBorderThickness: 6 clip: true XsText{ diff --git a/ui/qml/xstudio/views/media/functions/XsMediaListFunctions.qml b/ui/qml/xstudio/views/media/functions/XsMediaListFunctions.qml index 02fcb1662..63b0cca0b 100644 --- a/ui/qml/xstudio/views/media/functions/XsMediaListFunctions.qml +++ b/ui/qml/xstudio/views/media/functions/XsMediaListFunctions.qml @@ -136,18 +136,18 @@ Item { } function deleteIndexes(indexes) { - let items = [] - for(let i=0;i b.row - a.row ) + if(indexes.length) { + let items = [].concat(indexes) + items = items.sort((a,b) => b.row - a.row ) - var onscreen_idx = mediaIndexAfterRemoved(items) - mediaSelectionModel.setCurrentIndex(onscreen_idx, ItemSelectionModel.setCurrentIndex) - mediaSelectionModel.select(onscreen_idx, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) + var onscreen_idx = mediaIndexAfterRemoved(items) + mediaSelectionModel.setCurrentIndex(onscreen_idx, ItemSelectionModel.setCurrentIndex) + mediaSelectionModel.select(onscreen_idx, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) - items.forEach(function (item, index) { - item.model.removeRows(item.row, 1, false, item.parent) - }) + items.forEach(function (item, index) { + item.model.removeRows(item.row, 1, false, item.parent) + }) + } } function deleteSelectedCallback(response) { @@ -220,5 +220,5 @@ Item { theSessionData.copyRows(mediaSelectionModel.selectedIndexes, 0, sub) } - + } \ No newline at end of file diff --git a/ui/qml/xstudio/views/media/grid_view/XsMediaGrid.qml b/ui/qml/xstudio/views/media/grid_view/XsMediaGrid.qml index be17599e0..a02609cb2 100644 --- a/ui/qml/xstudio/views/media/grid_view/XsMediaGrid.qml +++ b/ui/qml/xstudio/views/media/grid_view/XsMediaGrid.qml @@ -126,14 +126,14 @@ XsGridView { if (idx == undefined || !idx.valid) { idx = mediaListModelDataRoot if (idx == undefined || !idx.valid) { - idx = theSessionData.createPlaylist("New Playlist") + idx = theSessionData.createPlaylist(theSessionData.getNextName("Playlist {}")) } else { idx = idx.parent // mediaListModelDataRoot is the 'MediaList' underneath a Playist, Subset etc. } } if (source == "External URIS") { - + Future.promise( theSessionData.handleDropFuture( Qt.CopyAction, diff --git a/ui/qml/xstudio/views/media/grid_view/delegates/XsMediaGridItemDelegate.qml b/ui/qml/xstudio/views/media/grid_view/delegates/XsMediaGridItemDelegate.qml index e67e39c54..20cbb8cab 100644 --- a/ui/qml/xstudio/views/media/grid_view/delegates/XsMediaGridItemDelegate.qml +++ b/ui/qml/xstudio/views/media/grid_view/delegates/XsMediaGridItemDelegate.qml @@ -169,30 +169,42 @@ Rectangle{ anchors.fill: parent showBorder: isOnScreen highlightBorderThickness: 10 + + LinearGradient { + anchors.fill: parent + start: Qt.point(0, 0) + end: Qt.point(width, height) + gradient: Gradient { + GradientStop { position: 0.0; color: isSelected ? palette.highlight : "#44000000" } + GradientStop { position: 0.15; color: isSelected ? palette.highlight : "#44000000" } + GradientStop { position: 0.25; color: "transparent" } + GradientStop { position: 1.0; color: "transparent" } + } + } } Rectangle{ id: flagIndicator width: 5 * cellSize/standardCellSize height: parent.height color: flagColourRole == undefined ? "transparent" : flagColourRole } - Rectangle{ - anchors.left: thumb.left - anchors.leftMargin: -width/2 - anchors.top: thumb.top - anchors.topMargin: -width/2 - color: isSelected ? palette.highlight : "black" - width: 50 - height: width - // radius: width/2 - border.width: 0 - border.color: palette.text - scale: cellSize/standardCellSize - opacity: isSelected? 0.9 : 0.5 - clip: true - rotation: -45 - - visible: indexDiv.text != "" - } + // Rectangle{ + // anchors.left: thumb.left + // anchors.leftMargin: -width/2 + // anchors.top: thumb.top + // anchors.topMargin: -width/2 + // color: "transparent" //isSelected ? "red" : "black" + // width: 50 + // height: width + // // radius: width/2 + // border.width: 0 + // border.color: palette.text + // scale: cellSize/standardCellSize + // opacity: isSelected? 0.9 : 0.5 + // clip: true + // rotation: -45 + // visible: indexDiv.text != "" + // } + XsText{ id: indexDiv text: selectionIndex ? selectionIndex : "" anchors.left: thumb.left diff --git a/ui/qml/xstudio/views/media/list_view/XsMediaHeader.qml b/ui/qml/xstudio/views/media/list_view/XsMediaHeader.qml index e7afaeaf9..1c5f17256 100644 --- a/ui/qml/xstudio/views/media/list_view/XsMediaHeader.qml +++ b/ui/qml/xstudio/views/media/list_view/XsMediaHeader.qml @@ -13,6 +13,7 @@ import xStudio 1.0 import "./widgets" import "./delegates" +import "./data_indicators" Rectangle{ id: header width: parent.width @@ -22,6 +23,7 @@ Rectangle{ id: header property bool isSomeColumnResizedByDrag: false property alias model: repeater.model + property alias barWidth: titleBar.width property alias columns_model: columns_model @@ -83,7 +85,6 @@ Rectangle{ id: header spacing: 0 Repeater{ - id: repeater model: columns_model } @@ -92,8 +93,7 @@ Rectangle{ id: header XsSecondaryButton{ id: addBtn - // visible: false - Layout.preferredWidth: 20 + Layout.preferredWidth: 25 Layout.minimumHeight: XsStyleSheet.widgetStdHeight z: 1 imgSrc: "qrc:/icons/add.svg" @@ -116,13 +116,15 @@ Rectangle{ id: header "resizable": true, "size": 120, "sortable": false, - "title": "Frame Range" + "title": "New Column" } - columns_root_model.insertRowsData( - columns_root_model.length-1, + var r = columns_root_model.rowCount() + columns_root_model.insertRowsSync( + r, 1, - columns_root_model.index(-1, -1), - new_column) + columns_root_model.index(-1, -1)) + columns_root_model.set(columns_root_model.index(r,0), new_column, "jsonRole") + } @@ -130,4 +132,12 @@ Rectangle{ id: header } + // Expandable empty item + Item{ + height: parent.height + anchors.left: titleBar.right + anchors.right: parent.right + } + + } \ No newline at end of file diff --git a/ui/qml/xstudio/views/media/list_view/XsMediaList.qml b/ui/qml/xstudio/views/media/list_view/XsMediaList.qml index 02abeeaa7..8c357d226 100644 --- a/ui/qml/xstudio/views/media/list_view/XsMediaList.qml +++ b/ui/qml/xstudio/views/media/list_view/XsMediaList.qml @@ -27,9 +27,10 @@ XsListView { cacheBuffer: 80 boundsBehavior: Flickable.StopAtBounds - // model: filteredMediaListData + isScrollbarVisibile: false property var dragTargetIndex + property bool dragToEnd: false property real itemRowHeight: rowHeight property real itemRowWidth: width @@ -38,12 +39,6 @@ XsListView { width: itemRowWidth } - Rectangle{ id: resultsBg - anchors.fill: parent - color: XsStyleSheet.panelBgColor - z: -1 - } - model: mediaListModelData PropertyAnimation{ @@ -53,6 +48,35 @@ XsListView { duration: 100 } + XsPopupMenu { + id: flagMenu + visible: false + menu_model_name: "media_flag_menu_"+flagMenu + property var panelContext: helpers.contextPanel(flagMenu) + property var mediaIndex: null + + XsFlagMenuInserter { + text: "" + menuPath: "" + panelContext: flagMenu.panelContext + menuModelName: flagMenu.menu_model_name + onFlagSet: { + theSessionData.set(flagMenu.mediaIndex, flag, "flagColourRole") + + if (flag_text) + theSessionData.set(flagMenu.mediaIndex, flag_text, "flagTextRole") + } + } + } + + function showFlagMenu(mx, my, source, mediaIndex) { + let sp = mapFromItem(source, mx, my) + flagMenu.x = sp.x + flagMenu.y = sp.y + flagMenu.mediaIndex = mediaIndex + flagMenu.visible = true + } + // here we auto-scroll the list so that the on-screen media is visible in // the panel property var onScreenMediaUuid: currentPlayhead.mediaUuid @@ -80,15 +104,6 @@ XsListView { } - XsLabel { - anchors.fill: parent - visible: !mediaList.count - text: 'Click the "+" button or drop files/folders here to add Media' - color: XsStyleSheet.hintColor - font.pixelSize: XsStyleSheet.fontSize*1.2 - font.weight: Font.Medium - } - XsMediaListMouseArea { id: mouseArea anchors.fill: parent @@ -131,26 +146,27 @@ XsListView { if (idx == undefined || !idx.valid) { idx = mediaListModelDataRoot if (idx == undefined || !idx.valid) { - idx = theSessionData.createPlaylist("New Playlist") + idx = theSessionData.createPlaylist(theSessionData.getNextName("Playlist {}")) } else { // mediaListModelDataRoot is the 'MediaList' that lives // underneath a Playist, Subset etc. We want to call // handleDropFuture with the Playlist/Subset/Timeline index - idx = idx.parent + idx = idx.parent } } if (source == "External URIS") { - + Future.promise( theSessionData.handleDropFuture( Qt.CopyAction, {"text/uri-list": data}, - idx) + dragToEnd ? idx.parent : idx) ).then(function(quuids){ if (idx) mediaSelectionModel.selectNewMedia(idx, quuids) }) + dragTargetIndex = undefined return } else if (source == "External JSON") { @@ -159,10 +175,11 @@ XsListView { theSessionData.handleDropFuture( Qt.CopyAction, data, - idx) + dragToEnd ? idx.parent : idx) ).then(function(quuids){ if (idx) mediaSelectionModel.selectNewMedia(idx, quuids) }) + dragTargetIndex = undefined return } @@ -178,10 +195,9 @@ XsListView { beforeMoveContentY = contentY theSessionData.moveRows( data, - dragTargetIndex.row, + dragToEnd ? -1 : dragTargetIndex.row, dragTargetIndex.parent.parent ) - dragTargetIndex = undefined } } } @@ -196,6 +212,7 @@ XsListView { function computeTargetDropIndex(dropCoordY) { var oldDragTarget = dragTargetIndex + dragToEnd = false if (dropCoordY < 0 || dropCoordY > height) { dragTargetIndex = undefined @@ -206,12 +223,20 @@ XsListView { } var idx = mediaList.indexAt(10, dropCoordY + contentY) - if (idx != -1) { + if (idx == -1 && mediaList.count) { + dragTargetIndex = mediaList.itemAtIndex(mediaList.count-1).modelIndex() + dragToEnd = true + } else if (idx != -1) { var y = mediaList.mapToItem(mediaList.itemAtIndex(idx), 10, dropCoordY).y - if (y > itemRowHeight/2 && idx < (mediaList.count-1)) { + if (y > itemRowHeight/2 && idx < mediaList.count) { idx = idx+1 } + if (idx == mediaList.count) { + idx--; + dragToEnd = true + } + // the index that we are going to drop items into cannot // be one of the selected items. Find the nearest unselected // index diff --git a/ui/qml/xstudio/views/media/list_view/XsMediaListLayout.qml b/ui/qml/xstudio/views/media/list_view/XsMediaListLayout.qml index 3e35e1e59..ab40f16e6 100644 --- a/ui/qml/xstudio/views/media/list_view/XsMediaListLayout.qml +++ b/ui/qml/xstudio/views/media/list_view/XsMediaListLayout.qml @@ -1,28 +1,98 @@ // SPDX-License-Identifier: Apache-2.0 import QtQuick 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.14 import xStudio 1.0 import "." -ColumnLayout { +Item { - spacing: 0 + id: root - XsMediaHeader{ + property var scrollPosition: verticalScroll.position + property var scrollPressed: verticalScroll.pressed - id: titleBar - Layout.fillWidth: true - height: XsStyleSheet.widgetStdHeight + Rectangle{ id: resultsBg + anchors.fill: parent + color: XsStyleSheet.panelBgColor + z: -1 } + XsLabel { + anchors.fill: parent + visible: !mediaList.count + text: 'Click the "+" button or drop files/folders here to add Media' + color: XsStyleSheet.hintColor + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + Flickable { + + id: flick + anchors.fill: parent + contentWidth: Math.max(titleBar.barWidth, width) + interactive: false + + ScrollBar.horizontal: XsScrollBar { + id: scrollbar + height: 10 + orientation: Qt.Horizontal + visible: flick.width < flick.contentWidth + } + + ColumnLayout { - XsMediaList { + id: layout + spacing: 0 + height: root.height + width: parent.width - id: mediaList - Layout.fillWidth: true - Layout.fillHeight: true - itemRowHeight: rowHeight + XsMediaHeader{ + id: titleBar + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight + } + XsMediaList { + id: mediaList + Layout.fillWidth: true + Layout.fillHeight: true + itemRowHeight: rowHeight + + property var yPos: visibleArea.yPosition + onYPosChanged: { + if (!verticalScroll.pressed) { + verticalScroll.position = yPos + } + } + + property var heightRatio: visibleArea.heightRatio + onHeightRatioChanged: { + verticalScroll.size = heightRatio + } + + property var scrollPositionTracker: scrollPosition + onScrollPositionTrackerChanged: { + if (scrollPressed) { + contentY = (scrollPosition * contentHeight) + originY + } + } + + } + + } } + XsScrollBar { + id: verticalScroll + width: 10 + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.right: parent.right + orientation: Qt.Vertical + anchors.topMargin: titleBar.height + visible: mediaList.height < mediaList.contentHeight + } + } diff --git a/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaFlagIndicator.qml b/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaFlagIndicator.qml index 025e93679..2c5bdfc68 100644 --- a/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaFlagIndicator.qml +++ b/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaFlagIndicator.qml @@ -4,7 +4,6 @@ import QtQml.Models 2.14 import xStudio 1.0 Item { - Rectangle{ width: parent.width height: parent.height @@ -19,4 +18,11 @@ Item { color: headerThumbColor } -} \ No newline at end of file + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + showFlagMenu(mouse.x, mouse.y, this, modelIndex()) + } + } +} diff --git a/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaTextItem.qml b/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaTextItem.qml index c62553e61..56d73f881 100644 --- a/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaTextItem.qml +++ b/ui/qml/xstudio/views/media/list_view/data_indicators/XsMediaTextItem.qml @@ -3,11 +3,34 @@ import QtQuick 2.12 import QtQml.Models 2.14 import xStudio 1.0 +import "../../common_delegates" + Item{ id: dateDiv property var leftMargin: 12 property var text + property var isIndex: false + property bool showBorder: actorUuidRole == currentPlayhead.mediaUuid + + Component { + id: highlight + XsMediaThumbnailHighlight { + anchors.fill: parent + z: 100 + borderThickness: 5 + } + } + Loader { + id: loader + anchors.fill: parent + } + onShowBorderChanged: { + if (showBorder && isIndex) loader.sourceComponent = highlight + else loader.sourceComponent = undefined + } + + XsText{ property bool invalidValue: dateDiv.text == undefined || dateDiv.text == "null" @@ -16,6 +39,10 @@ Item{ anchors.verticalCenter: parent.verticalCenter font.weight: isActive? Font.ExtraBold : Font.Normal color: invalidValue ? XsStyleSheet.widgetBgNormalColor : palette.text + + horizontalAlignment: position == "left" ? Text.AlignLeft : Text.AlignHCenter + elide: Text.ElideMiddle + width: parent.width - leftMargin*2 } Rectangle{ diff --git a/ui/qml/xstudio/views/media/list_view/delegates/XsMediaHeaderColumn.qml b/ui/qml/xstudio/views/media/list_view/delegates/XsMediaHeaderColumn.qml index 1cde8e5f4..281cafd29 100644 --- a/ui/qml/xstudio/views/media/list_view/delegates/XsMediaHeaderColumn.qml +++ b/ui/qml/xstudio/views/media/list_view/delegates/XsMediaHeaderColumn.qml @@ -48,6 +48,10 @@ Item{ text: title ? title : "" anchors.verticalCenter: parent.verticalCenter x: position == "left" ? leftMargin : (parent.width-width)/2 + + horizontalAlignment: position == "left" ? Text.AlignLeft : Text.AlignHCenter + elide: Text.ElideRight + width: parent.width - leftMargin } MouseArea { diff --git a/ui/qml/xstudio/views/media/list_view/delegates/XsMediaItemDelegate.qml b/ui/qml/xstudio/views/media/list_view/delegates/XsMediaItemDelegate.qml index 7fb8ea6f3..9ed7c5115 100644 --- a/ui/qml/xstudio/views/media/list_view/delegates/XsMediaItemDelegate.qml +++ b/ui/qml/xstudio/views/media/list_view/delegates/XsMediaItemDelegate.qml @@ -72,8 +72,9 @@ Rectangle { } property color highlightColor: palette.highlight - property color bgColorPressed: Qt.darker(palette.highlight, 3) //XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" //XsStyleSheet.widgetBgNormalColor + property color bgColorPressed: Qt.darker(palette.highlight, 3) + property color bgColorNormal: "transparent" + //background: Rectangle { id: itemBg @@ -88,7 +89,9 @@ Rectangle { visible: isDragTarget color: palette.highlight Behavior on height { NumberAnimation{duration: 250} } + y: dragToEnd ? parent.height-height : 0 } + Rectangle { id: drag_item_bg anchors.fill: parent @@ -157,6 +160,8 @@ Rectangle { text: selectionIndex ? selectionIndex : "" width: size height: itemRowHeight + leftMargin: 2 + isIndex: true } } Component { @@ -189,7 +194,7 @@ Rectangle { width: parent.width height: itemRowHeight - y: drag_target_indicator.height + y: dragToEnd ? 0 : drag_target_indicator.height Rectangle{ id: rowDividerLine diff --git a/ui/qml/xstudio/views/media/list_view/widgets/XsMediaListConfigureDialog.qml b/ui/qml/xstudio/views/media/list_view/widgets/XsMediaListConfigureDialog.qml index e4df1bd4b..8d6c364fb 100644 --- a/ui/qml/xstudio/views/media/list_view/widgets/XsMediaListConfigureDialog.qml +++ b/ui/qml/xstudio/views/media/list_view/widgets/XsMediaListConfigureDialog.qml @@ -513,6 +513,8 @@ metadata with media that isn't from your pipeline.` "Are you sure you want to remove the " + column_title + " column?", ["Remove Column", "Cancel"], undefined) + + // dialog.visible = false } } @@ -521,9 +523,10 @@ metadata with media that isn't from your pipeline.` Layout.preferredHeight: XsStyleSheet.widgetStdHeight visible: !is_backup onClicked: { + console.log("model_index", model_index) var r = model_index.row if (r) { - var p = model_index.parent + var p = model_index model_index.model.moveRows( model_index.parent, model_index.row, @@ -531,7 +534,7 @@ metadata with media that isn't from your pipeline.` model_index.parent, model_index.row-1 ) - model_index = p.model.index(r-1, 0, p) + model_index = p.model.index(r-1, 0) } } } @@ -543,7 +546,7 @@ metadata with media that isn't from your pipeline.` onClicked: { var r = model_index.row if (r < (model_index.model.rowCount(model_index.parent)-1)) { - var p = model_index.parent + var p = model_index model_index.model.moveRows( model_index.parent, model_index.row, @@ -551,7 +554,7 @@ metadata with media that isn't from your pipeline.` model_index.parent, model_index.row+2 ) - model_index = p.model.index(r+1, 0, p) + model_index = p.model.index(r+1, 0) } } } diff --git a/ui/qml/xstudio/views/media/list_view/widgets/XsMediaMetadataWindow.qml b/ui/qml/xstudio/views/media/list_view/widgets/XsMediaMetadataWindow.qml index 54d137b06..02462c994 100644 --- a/ui/qml/xstudio/views/media/list_view/widgets/XsMediaMetadataWindow.qml +++ b/ui/qml/xstudio/views/media/list_view/widgets/XsMediaMetadataWindow.qml @@ -14,6 +14,7 @@ XsWindow { property var metadata_digest minimumWidth: 600 minimumHeight: 600 + property bool no_data: false property int highlightIndex: -1 property bool globalPressed: false @@ -21,7 +22,7 @@ XsWindow { if (globalPressed) { metadataSelected(r1.itemAt(highlightIndex).text) } - } + } signal metadataSelected(string metadataPath) @@ -72,6 +73,9 @@ XsWindow { columnSpacing: 0 Repeater { id: r1 + onCountChanged: { + no_data = count == 0 + } model: metadata_digest XsLabel { text: metadata_digest[index][0] @@ -165,4 +169,10 @@ XsWindow { } + XsLabel { + anchors.centerIn: parent + text: "No Metadata" + visible: no_data + } + } \ No newline at end of file diff --git a/ui/qml/xstudio/views/media/widgets/XsMediaListContextMenu.qml b/ui/qml/xstudio/views/media/widgets/XsMediaListContextMenu.qml index 09f2a328c..f83e22891 100644 --- a/ui/qml/xstudio/views/media/widgets/XsMediaListContextMenu.qml +++ b/ui/qml/xstudio/views/media/widgets/XsMediaListContextMenu.qml @@ -74,7 +74,7 @@ XsPopupMenu { }, "Add To New Playlist", "Enter New Playlist Name", - "Untitled Playlist", + theSessionData.getNextName("Playlist {}"), ["Cancel", "Move Media", "Copy Media"]) } @@ -96,7 +96,7 @@ XsPopupMenu { }, "Add To New Subset", "Enter New Subset Name", - "Untitled Subset", + theSessionData.getNextName("Subset {}"), ["Cancel", "Add Media"]) } panelContext: btnMenu.panelContext @@ -117,7 +117,7 @@ XsPopupMenu { }, "Add To New Contact Sheet", "Enter New Contact Sheet Name", - "Untitled Contact Sheet", + theSessionData.getNextName("Contact Sheet {}"), ["Cancel", "Add Media"]) } } @@ -136,7 +136,7 @@ XsPopupMenu { }, "Add To New Sequence", "Enter New Sequence Name", - "Untitled Sequence", + theSessionData.getNextName("Sequence {}"), ["Cancel", "Add Media"]) } panelContext: btnMenu.panelContext diff --git a/ui/qml/xstudio/views/media/widgets/XsMediaListPlusMenu.qml b/ui/qml/xstudio/views/media/widgets/XsMediaListPlusMenu.qml index 6040c5b2f..8437f864e 100644 --- a/ui/qml/xstudio/views/media/widgets/XsMediaListPlusMenu.qml +++ b/ui/qml/xstudio/views/media/widgets/XsMediaListPlusMenu.qml @@ -31,7 +31,7 @@ XsPopupMenu { plusMenu.addPlaylist, "Add Playlist", "Enter a name for the new playlist.", - "New Playlist", + theSessionData.getNextName("Playlist {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -48,7 +48,7 @@ XsPopupMenu { plusMenu.addSubset, "Add Subset", "Enter a name for the new subset.", - "New Subset", + theSessionData.getNextName("Subset {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -65,7 +65,7 @@ XsPopupMenu { plusMenu.addTimeline, "Add Sequence", "Enter a name for the new sequence.", - "New Sequence", + theSessionData.getNextName("Sequence {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -82,7 +82,7 @@ XsPopupMenu { plusMenu.addContactSheet, "Add Contact Sheet", "Enter a name for the new contact sheet.", - "New Contact Sheet", + theSessionData.getNextName("Contact Sheet {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -107,6 +107,14 @@ XsPopupMenu { panelContext: plusMenu.panelContext } + XsMenuModelItem { + text: "Add Media From Clipboard" + menuPath: "" + menuItemPosition: 6 + menuModelName: plusMenu.menu_model_name + onActivated: file_functions.addMediaFromClipboard() + } + function addPlaylist(new_name, button) { if (button == "Add") { let pl = theSessionData.createPlaylist(new_name) @@ -124,7 +132,7 @@ XsPopupMenu { if (button == "Add") { addToNewContactSheet(new_name) } - } + } function addTimeline(new_name, button) { if (button == "Add") { diff --git a/ui/qml/xstudio/views/notes/sections/XsNoteSection1.qml b/ui/qml/xstudio/views/notes/sections/XsNoteSection1.qml index 60182bb10..4e48a5831 100644 --- a/ui/qml/xstudio/views/notes/sections/XsNoteSection1.qml +++ b/ui/qml/xstudio/views/notes/sections/XsNoteSection1.qml @@ -85,7 +85,7 @@ Rectangle{ id: sec1 XsText{ visible: sec1.isHovered - text: "Go To Frame: "+startFrameRole + text: "Go To Frame"//: "+startFrameRole anchors.centerIn: thumb style: Text.Outline font.pixelSize: XsStyleSheet.fontSize + 4 diff --git a/ui/qml/xstudio/views/notes/sections/XsNoteSection2.qml b/ui/qml/xstudio/views/notes/sections/XsNoteSection2.qml index daf23ca98..1759ba264 100644 --- a/ui/qml/xstudio/views/notes/sections/XsNoteSection2.qml +++ b/ui/qml/xstudio/views/notes/sections/XsNoteSection2.qml @@ -82,23 +82,50 @@ Rectangle{ width: flickDiv.width height: lineCount<=6? flickDiv.height : flickDiv.height*lineCount - text: noteRole + property var noteRoleFollower: noteRole + onNoteRoleFollowerChanged: { + if (!activeFocus) text = noteRole + } + hint: activeFocus? "" : "Enter note here..." onCursorRectangleChanged: flickDiv.ensureVisible(cursorRectangle) focus: (mArea.containsMouse || activeFocus) onFocusChanged: if(focus) forceActiveFocus() - onEditingFinished: { //#TODO + // N.B. It's possible for the user to finish entering text + // but we don't get the onEditingFinished signal - it + // depends on where they go next with the mouse pointer. + // For this reason we 'brute force' update the backend + // noteRole but do it with a timer so it's not too + // granular. But we do the update here anyway incase the + // QML objects here are destroyed before the timer has + // timed out. + onEditingFinished: { noteRole = text } onTextChanged: { - if (noteRole != text) { - noteRole = text + // user is entering text - push the entered text to + // the backend but not on every key stroke + if (activeFocus && !update_backend_timer.running) { + console.log("Starting timer") + update_backend_timer.running = true } } + XsTimer { + id: update_backend_timer + interval: 5000 + running: false + repeat: false + onTriggered: { + if (noteRole != notesEdit.text) { + console.log("Setting backend") + noteRole = notesEdit.text + } + } + } } } diff --git a/ui/qml/xstudio/views/playlists/XsPlaylists.qml b/ui/qml/xstudio/views/playlists/XsPlaylists.qml index 28c321761..2687231db 100644 --- a/ui/qml/xstudio/views/playlists/XsPlaylists.qml +++ b/ui/qml/xstudio/views/playlists/XsPlaylists.qml @@ -7,6 +7,7 @@ import Qt.labs.qmlmodels 1.0 import QtQuick.Layouts 1.15 import xStudio 1.0 +import xstudio.qml.helpers 1.0 import "./widgets" Item{ @@ -50,6 +51,12 @@ Item{ } } + XsModelProperty { + id: notificationProperty + role: "notificationRole" + index: theSessionData.playlistsRootIdx + } + ColumnLayout { id: titleDiv @@ -101,6 +108,21 @@ Item{ // property string filename: path ? path.substring(path.lastIndexOf("/")+1) : sessionProperties.values.nameRole ? sessionProperties.values.nameRole : "" // } + Repeater { + Layout.preferredHeight: btnHeight + model: notificationProperty.value == undefined ? [] : notificationProperty.value + + // notify widget + XsNotification { + Layout.preferredHeight: btnHeight + Layout.preferredWidth: height + text: modelData.text + type: modelData.type + percentage: modelData.progress_percent || 0.0 + } + } + + XsPrimaryButton{ id: morePlaylistBtn Layout.preferredWidth: btnWidth @@ -121,10 +143,41 @@ Item{ } + + XsPopupMenu { + id: flagMenu + visible: false + menu_model_name: "playlist_flag_menu_"+flagMenu + property var panelContext: helpers.contextPanel(flagMenu) + property var itemIndex: null + + XsFlagMenuInserter { + text: "" + menuPath: "" + panelContext: flagMenu.panelContext + menuModelName: flagMenu.menu_model_name + onFlagSet: { + theSessionData.set(flagMenu.itemIndex, flag, "flagColourRole") + + if (flag_text) + theSessionData.set(flagMenu.itemIndex, flag_text, "flagTextRole") + } + } + } + + function showFlagMenu(mx, my, source, itemIndex) { + let sp = mapFromItem(source, mx, my) + flagMenu.x = sp.x + flagMenu.y = sp.y + flagMenu.itemIndex = itemIndex + flagMenu.visible = true + } + Loader { id: menu_loader } + Component { id: plusMenuComponent XsPlaylistPlusMenu { @@ -141,7 +194,6 @@ Item{ } } - function showMenu(mx, my, parent) { if (menu_loader.item == undefined) { menu_loader.sourceComponent = plusMenuComponent diff --git a/ui/qml/xstudio/views/playlists/delegates/XsPlaylistItemBase.qml b/ui/qml/xstudio/views/playlists/delegates/XsPlaylistItemBase.qml index 3cf5a9960..1b20b64d9 100644 --- a/ui/qml/xstudio/views/playlists/delegates/XsPlaylistItemBase.qml +++ b/ui/qml/xstudio/views/playlists/delegates/XsPlaylistItemBase.qml @@ -103,14 +103,6 @@ Item { } - // flag - Rectangle{ - y: indicator_height - color: flagColourRole - height: itemRowStdHeight - width: flagIndicatorWidth - } - /* modelIndex should be set to point into the session data model and get to the playlist that we are representing */ property var modelIndex @@ -155,6 +147,23 @@ Item { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton + // flag + MouseArea { + id: fma + y: indicator_height + height: itemRowStdHeight + width: flagIndicatorWidth + hoverEnabled: true + onClicked: showFlagMenu(mouse.x, mouse.y, this, modelIndex) + cursorShape: Qt.PointingHandCursor + Rectangle{ + anchors.fill: parent + color: flagColourRole + border.width: 1 + border.color: fma.containsMouse ? palette.highlight : "transparent" + } + } + /*onMouseXChanged: checkInspectHover() onMouseYChanged: checkInspectHover() diff --git a/ui/qml/xstudio/views/playlists/delegates/XsSubsetItemDelegate.qml b/ui/qml/xstudio/views/playlists/delegates/XsSubsetItemDelegate.qml index da43ca8b7..e2545405f 100644 --- a/ui/qml/xstudio/views/playlists/delegates/XsSubsetItemDelegate.qml +++ b/ui/qml/xstudio/views/playlists/delegates/XsSubsetItemDelegate.qml @@ -5,6 +5,6 @@ import xStudio 1.0 import "." XsPlaylistItemBase { - iconSource: "qrc:/icons/featured_play_list.svg" + iconSource: "qrc:/icons/communities.svg" indent: true } \ No newline at end of file diff --git a/ui/qml/xstudio/views/playlists/delegates/XsTimelineItemDelegate.qml b/ui/qml/xstudio/views/playlists/delegates/XsTimelineItemDelegate.qml index 88778f45f..4d88e20cb 100644 --- a/ui/qml/xstudio/views/playlists/delegates/XsTimelineItemDelegate.qml +++ b/ui/qml/xstudio/views/playlists/delegates/XsTimelineItemDelegate.qml @@ -5,6 +5,6 @@ import xStudio 1.0 import "." XsPlaylistItemBase { - iconSource: "qrc:/icons/view_timeline.svg" + iconSource: "qrc:/icons/view_object_track.svg" indent: true } \ No newline at end of file diff --git a/ui/qml/xstudio/views/playlists/widgets/XsPlaylistPlusMenu.qml b/ui/qml/xstudio/views/playlists/widgets/XsPlaylistPlusMenu.qml index c5954c19c..a8154d5ac 100644 --- a/ui/qml/xstudio/views/playlists/widgets/XsPlaylistPlusMenu.qml +++ b/ui/qml/xstudio/views/playlists/widgets/XsPlaylistPlusMenu.qml @@ -29,7 +29,7 @@ XsPopupMenu { plusMenu.addPlaylist, "Add Playlist", "Enter a name for the new playlist.", - "New Playlist", + theSessionData.getNextName("Playlist {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -46,7 +46,7 @@ XsPopupMenu { plusMenu.addSubset, "Add Subset", "Enter a name for the new subset.", - "New Subset", + theSessionData.getNextName("Subset {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -63,7 +63,7 @@ XsPopupMenu { plusMenu.addTimeline, "Add Sequence", "Enter a name for the new sequence.", - "New Sequence", + theSessionData.getNextName("Sequence {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -80,7 +80,7 @@ XsPopupMenu { plusMenu.addContactSheet, "Add Contact Sheet", "Enter a name for the new contact sheet.", - "New Contact Sheet", + theSessionData.getNextName("Contact Sheet {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext @@ -104,7 +104,7 @@ XsPopupMenu { plusMenu.addDivider, "Add Playlist Divider", "Enter a name for the new divider.", - "New Divider", + theSessionData.getNextName("Divider {}"), ["Cancel", "Add"]) } panelContext: plusMenu.panelContext diff --git a/ui/qml/xstudio/views/timeline/XsTimeline.qml b/ui/qml/xstudio/views/timeline/XsTimeline.qml index 1bd50155e..23d1a0d84 100644 --- a/ui/qml/xstudio/views/timeline/XsTimeline.qml +++ b/ui/qml/xstudio/views/timeline/XsTimeline.qml @@ -234,15 +234,12 @@ Rectangle { onSelectionChanged: { updateFocus() updateLoop() + theSessionData.setTimelineSelection(timeline_items.rootIndex, timelineSelection.selectedIndexes) } } - function viewedMediaSetChanged() { - - if(viewedMediaSetProperties.index.valid && viewedMediaSetProperties.values.typeRole == "Timeline") { - forceActiveFocus() + function initTimeline(retry=true) { let timelineIndex = theSessionData.index(2, 0, viewedMediaSetProperties.index) - if (timelineIndex == timeline_items.rootIndex) return if (theSessionData.rowCount(timelineIndex) == 0) { // the timeline item data hasn't been 'fetched' from the backend. @@ -252,14 +249,10 @@ Rectangle { // the timeline UI components) timeline_items.srcModel = null_list theSessionData.fetchMore(timelineIndex) - callbackTimer.setTimeout(function(index) { return function() { - timeline_items.srcModel = theSessionData - timeline_items.rootIndex = helpers.makePersistent(index) - have_timeline = true - updateConformSourceIndex() - fitItems() - // conformSourceIndex = getVideoTrackIndex(1) - }}( timelineIndex ), 200); + callbackTimer.setTimeout(function() { return function() { + initTimeline() + }}(), 200); + } else { timeline_items.srcModel = theSessionData timeline_items.rootIndex = helpers.makePersistent(timelineIndex) @@ -269,6 +262,19 @@ Rectangle { fitItems() }}(), 200); } + } + + function viewedMediaSetChanged() { + + if(viewedMediaSetProperties.index.valid && viewedMediaSetProperties.values.typeRole == "Timeline") { + forceActiveFocus() + + if (theSessionData.index(2, 0, viewedMediaSetProperties.index) == timeline_items.rootIndex) + return + + callbackTimer.setTimeout(function() { return function() { + initTimeline() + }}(), 50); } else if (!timeline_items.rootIndex.valid) { // if the user has selected something that is not a timeline (playlist, @@ -779,10 +785,6 @@ Rectangle { vmedia.push(ind) } - if(timelinePlayheadSelectionIndex.valid) - theSessionData.updateSelection(timelinePlayheadSelectionIndex, vmedia, mode & Qt.ShiftModifier ? ItemSelectionModel.Deselect : mode & Qt.ControlModifier ? ItemSelectionModel.Select : ItemSelectionModel.ClearAndSelect) - - // mediaSelectionModel.select( // helpers.createItemSelection(vmedia), // mode & Qt.ShiftModifier ? ItemSelectionModel.Deselect : mode & Qt.ControlModifier ? ItemSelectionModel.Select : ItemSelectionModel.ClearAndSelect) @@ -820,9 +822,22 @@ Rectangle { amedia.push(ind) } - if(timelinePlayheadSelectionIndex.valid) - theSessionData.updateSelection(timelinePlayheadSelectionIndex, amedia, - mode & Qt.ShiftModifier ? ItemSelectionModel.Deselect : ItemSelectionModel.Select) + let msel = vmedia.concat(amedia) + if(timelinePlayheadSelectionIndex.valid && msel.length) + theSessionData.updateSelection( + timelinePlayheadSelectionIndex, + msel, + mode & Qt.ShiftModifier ? + ItemSelectionModel.Deselect : + (mode & Qt.ControlModifier ? ItemSelectionModel.Select : ItemSelectionModel.ClearAndSelect) + ) + + + // if(timelinePlayheadSelectionIndex.valid) + // theSessionData.updateSelection( + // timelinePlayheadSelectionIndex, amedia, + // mode & Qt.ShiftModifier ? ItemSelectionModel.Deselect : ItemSelectionModel.Select + // ) // mediaSelectionModel.select( // helpers.createItemSelection(amedia), @@ -905,14 +920,14 @@ Rectangle { XsHotkeyArea { id: hotkey_area anchors.fill: parent - context: "timeline" + context: hotkey_area_id focus: true } Keys.forwardTo: hotkey_area XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+Z" name: "Timeline Redo" description: "Re-does the last undone edit in the timeline" @@ -923,7 +938,7 @@ Rectangle { } XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "SHIFT+C" name: "Change Clip Colour" description: "Change Active Clip Colour" @@ -960,7 +975,7 @@ Rectangle { XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+U" name: "Timeline Undo" description: "Jumps to the end frame" @@ -971,7 +986,7 @@ Rectangle { } XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "Z" name: "Timeline Zoom" description: "Enables timeline zooming mode" @@ -994,7 +1009,7 @@ Rectangle { XsHotkey { id: select_up_hotkey - context: "timeline" + context: hotkey_area_id sequence: "PgUp" name: "Move Selection Up" description: "Move Selection Up" @@ -1004,7 +1019,7 @@ Rectangle { XsHotkey { id: select_down_hotkey - context: "timeline" + context: hotkey_area_id sequence: "PgDown" name: "Move Selection Down" description: "Move Selection Down" @@ -1014,7 +1029,7 @@ Rectangle { XsHotkey { id: expand_up_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+PgUp" name: "Expand Selection Up" description: "Expand Selection Up" @@ -1024,7 +1039,7 @@ Rectangle { XsHotkey { id: expand_down_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+PgDown" name: "Expand Selection Down" description: "Expand Selection Down" @@ -1034,7 +1049,7 @@ Rectangle { XsHotkey { id: contract_up_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Shift+PgUp" name: "Contract Selection Up" description: "Contract Selection Up" @@ -1044,7 +1059,7 @@ Rectangle { XsHotkey { id: contract_down_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Shift+PgDown" name: "Contract Selection Down" description: "Contract Selection Down" @@ -1054,7 +1069,7 @@ Rectangle { XsHotkey { id: expand_up_down_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Alt+PgUp" name: "Expand Selection Up and Down" description: "Expand Selection Up and Down" @@ -1064,7 +1079,7 @@ Rectangle { XsHotkey { id: contract_up_down_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Alt+PgDown" name: "Contract Selection Up and Down" description: "Contract Selection Up and Down" @@ -1072,29 +1087,43 @@ Rectangle { componentName: "Timeline" } - XsHotkey { + // This hotkey is already regisetered by the playhead, but we can + // still get activation signal, filtered by our context so we can run + // our own logic. Playhead won't do anything if the context starts with + // "timeline" + XsHotkeyReference { id: select_next_hotkey - context: "timeline" - sequence: "DOWN" - name: "Move Selection Right" - description: "Move Clip Selection Right" - onActivated: updateItemSelectionHorizontal(-1,1) - componentName: "Timeline" + hotkeyName: "Move Forwards through media/clips" + // Because the Playhead also watches this hotkey, we want to exclusively + // grab it if the timeline is active + exclusive: isPlayheadActive + onActivated: { + if(!timeline.timelineSelection.selectedIndexes.length){ + // transportBar.skipToNext(true) + timelinePlayhead.logicalFrame = theSessionData.getNextTimelineClipFrame(timeline_items.rootIndex, timelinePlayhead.logicalFrame) + } + else + updateItemSelectionHorizontal(-1,1) + } } - XsHotkey { + // See note above + XsHotkeyReference { id: select_previous_hotkey - context: "timeline" - sequence: "UP" - name: "Move Selection Left" - description: "Move Clip Selection Left" - onActivated: updateItemSelectionHorizontal(1,-1) - componentName: "Timeline" + hotkeyName: "Move backwards through media/clips" + exclusive: isPlayheadActive + onActivated: { + if(!timeline.timelineSelection.selectedIndexes.length) { + timelinePlayhead.logicalFrame = theSessionData.getPreviousTimelineClipFrame(timeline_items.rootIndex, timelinePlayhead.logicalFrame) + } + else + updateItemSelectionHorizontal(1,-1) + } } XsHotkey { id: expand_next_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+DOWN" name: "Expand Selection Right" description: "Expand Clip Selection Right" @@ -1104,7 +1133,7 @@ Rectangle { XsHotkey { id: expand_previous_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Ctrl+UP" name: "Expand Selection Left" description: "Expand Clip Selection Left" @@ -1114,7 +1143,7 @@ Rectangle { XsHotkey { id: contract_next_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Shift+DOWN" name: "Contract Selection Right" description: "Contract Clip Selection Right" @@ -1124,7 +1153,7 @@ Rectangle { XsHotkey { id: contract_previous_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Shift+UP" name: "Contract Selection Left" description: "Contract Clip Selection Left" @@ -1134,7 +1163,7 @@ Rectangle { XsHotkey { id: expand_both_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Alt+DOWN" name: "Expand Selection" description: "Expand Selection" @@ -1144,7 +1173,7 @@ Rectangle { XsHotkey { id: contract_both_hotkey - context: "timeline" + context: hotkey_area_id sequence: "Alt+UP" name: "Contract Selection" description: "Contract Clip Selection" @@ -1153,7 +1182,7 @@ Rectangle { } XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "X" name: "Timeline Scroll" description: "Enables timeline scrolling mode" @@ -1165,7 +1194,7 @@ Rectangle { } XsHotkey { - context: "timeline" + context: hotkey_area_id sequence: "F" name: "Timeline fit" description: "Fits the timeline view to selected items" @@ -1400,7 +1429,10 @@ Rectangle { // set the aux playhead to drive the viewer - it will not // show the timeline but just the bit of media we have // double clicked on - viewportCurrentMediaContainerIndex = currentMediaContainerIndex + + // make sure we're looking at the right thing. + currentMediaContainerIndex = timelineModel.rootIndex.parent + viewportCurrentMediaContainerIndex = timelineModel.rootIndex.parent callbackTimer.setTimeout(function() { return function() { // we 'un-pin' the timeline playhead so that instead of using @@ -1566,6 +1598,8 @@ Rectangle { theSessionData.beginTimelineItemDrag(timelineSelection.selectedIndexes, mode, rippleMode, overwriteMode) else if(mode == "roll") theSessionData.beginTimelineItemDrag(timelineSelection.selectedIndexes, mode) + else if(mode == "track") + theSessionData.beginTimelineItemDrag(timelineSelection.selectedIndexes, mode) } function dragging(index, item, mode, x, y) { @@ -1631,6 +1665,8 @@ Rectangle { theSessionData.updateTimelineItemDrag(timelineSelection.selectedIndexes, mode, x, y, rippleMode, overwriteMode) else if(mode == "roll") theSessionData.updateTimelineItemDrag(timelineSelection.selectedIndexes, mode, x, 0) + else if(mode == "track") + theSessionData.updateTimelineItemDrag(timelineSelection.selectedIndexes, mode, 0, y) } @@ -1659,6 +1695,8 @@ Rectangle { theSessionData.endTimelineItemDrag(timelineSelection.selectedIndexes, mode, overwriteMode) else if(mode == "roll") theSessionData.endTimelineItemDrag(timelineSelection.selectedIndexes, mode) + else if(mode == "track") + theSessionData.endTimelineItemDrag(timelineSelection.selectedIndexes, mode) snapLine = -1 } @@ -1750,6 +1788,9 @@ Rectangle { timelineSelection.select(hovered.modelIndex(), new_state) if(hovered.itemTypeRole == "Clip" && hovered.mediaIndex.valid && timelinePlayheadSelectionIndex.valid) { + console.log(timelinePlayheadSelectionIndex, + [hovered.mediaIndex], + new_state) theSessionData.updateSelection( timelinePlayheadSelectionIndex, [hovered.mediaIndex], diff --git a/ui/qml/xstudio/views/timeline/XsTimelineMenu.qml b/ui/qml/xstudio/views/timeline/XsTimelineMenu.qml index f7bc4fda6..9bd672195 100644 --- a/ui/qml/xstudio/views/timeline/XsTimelineMenu.qml +++ b/ui/qml/xstudio/views/timeline/XsTimelineMenu.qml @@ -151,22 +151,37 @@ XsPopupMenu { id: createTrackRepeater model: createTrack.value Item { - XsMenuModelItem { - text: modelData["name"] - menuItemType: "button" - menuPath: "Create Tracks" - menuItemPosition: index - menuModelName: timelineMenu.menu_model_name - onActivated: { - for(let i=0;i 60 - property var setTrackHeaderWidth: ListView.view.setTrackHeaderWidth - property var setSizerHovered: ListView.view.setSizerHovered - property var setSizerDragging: ListView.view.setSizerDragging - - property bool isSizerHovered: ListView.view.isSizerHovered - property bool isSizerDragging: ListView.view.isSizerDragging - - property var draggingStarted: ListView.view.draggingStarted - property var dragging: ListView.view.dragging - property var draggingStopped: ListView.view.draggingStopped - property var doubleTapped: ListView.view.doubleTapped - property var tapped: ListView.view.tapped - - width: ListView.view.width - height: itemHeight * scaleY - - property bool isHovered: hoveredItem == control - property bool isSelected: false - property bool isConformSource: DelegateModel.model.modelIndex(index) == conformSourceIndex - property var timelineSelection: ListView.view.timelineSelection - property var hoveredItem: ListView.view.hoveredItem - property var itemTypeRole: typeRole - property alias list_view: list_view - - function modelIndex() { - return helpers.makePersistent(DelegateModel.model.modelIndex(index)) - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, control.DelegateModel.model.modelIndex(index))) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, control.DelegateModel.model.modelIndex(index))) - isSelected = true - } - } - - 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 - title: "Audio Track" - isEnabled: enabledRole - isLocked: lockedRole - isSelected: control.isSelected - isConformSource: control.isConformSource - isSizerHovered: control.isSizerHovered - isSizerDragging: control.isSizerDragging - onSizerHovered: setSizerHovered(hovered) - onSizerDragging: setSizerDragging(dragging) - notificationModel: notificationRole - - - onEnabledClicked: enabledRole = !enabledRole - onLockedClicked: lockedRole = !lockedRole - onConformSourceClicked: conformSourceIndex = helpers.makePersistent(DelegateModel.model.modelIndex(index)) - onFlagSet: flagItems([DelegateModel.model.modelIndex(index)], flag == "#00000000" ? "": flag) - } - - Flickable { - property bool forceEval: false - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: track_header.right - anchors.right: parent.right - - interactive: false - - contentWidth: contentItem.childrenRect.width - contentHeight: contentItem.childrenRect.height - contentX: (forceEval && !forceEval) || control.cX - - onContentWidthChanged: { - if(contentX != control.cX) { - forceEval = !forceEval - } - } - - Row { - id:list_view - // opacity: isHovered ? 1.0 : enabledRole ? (lockedRole ? 0.6 : 1.0) : 0.3 - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - 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 - property var parentLV: control.parentLV - - property var draggingStarted: control.draggingStarted - property var dragging: control.dragging - property var draggingStopped: control.draggingStopped - property var doubleTapped: control.doubleTapped - property var tapped: control.tapped - - property bool isParentLocked: lockedRole - property bool isParentEnabled: enabledRole - - Repeater { - id: item_repeater - model: DelegateModel { - id: track_items - property var srcModel: theSessionData - model: srcModel - rootIndex: helpers.makePersistent(control.DelegateModel.model.modelIndex(index)) - - delegate: DelegateChooser { - role: "typeRole" - - DelegateChoice { - roleValue: "Clip" - XsDelegateClip {} - } - - DelegateChoice { - roleValue: "Gap" - XsDelegateGap {} - } - } - } - } - } - } +XsDelegateTrack { + title: "Audio Track" } diff --git a/ui/qml/xstudio/views/timeline/delegates/XsDelegateClip.qml b/ui/qml/xstudio/views/timeline/delegates/XsDelegateClip.qml index 9df950bf2..2433f0d8f 100644 --- a/ui/qml/xstudio/views/timeline/delegates/XsDelegateClip.qml +++ b/ui/qml/xstudio/views/timeline/delegates/XsDelegateClip.qml @@ -195,14 +195,6 @@ RowLayout { onDraggingStarted: { control.draggingStarted(modelIndex(), control, mode) isDragging = true - - // if(mode == "middle" && !rippleMode) { - // let new_parent = control.parent.parent.parent.parent - // let orig = mapFromItem(new_parent, x, y) - // clip.parent = new_parent - // mappedX = -orig.x - // mappedY = -orig.y - // } } onDragging: control.dragging(modelIndex(), control, mode, x / scaleX, y / scaleY / config.itemHeight) onDoubleTapped: control.doubleTapped(control, mode) @@ -210,11 +202,6 @@ RowLayout { onDraggingStopped: { control.draggingStopped(modelIndex(), control, mode) isDragging = false - // if(mode == "middle" && !rippleMode) { - // clip.parent = control - // mappedX = 0 - // mappedY = 0 - // } } Connections { diff --git a/ui/qml/xstudio/views/timeline/delegates/XsDelegateStack.qml b/ui/qml/xstudio/views/timeline/delegates/XsDelegateStack.qml index c6d88eb51..c4eebc5f7 100644 --- a/ui/qml/xstudio/views/timeline/delegates/XsDelegateStack.qml +++ b/ui/qml/xstudio/views/timeline/delegates/XsDelegateStack.qml @@ -482,7 +482,7 @@ Rectangle { Rectangle { color: XsStyleSheet.accentColor opacity: 0.3 - visible: timelinePlayhead && timelinePlayhead.enableLoopRange + visible: timelinePlayhead != undefined && timelinePlayhead.enableLoopRange anchors.fill: parent property int start: (timelinePlayhead.loopStartFrame - (frameTrack.offset / control.scaleX)) * control.scaleX property int end: (timelinePlayhead.loopEndFrame - (frameTrack.offset / control.scaleX)) * control.scaleX diff --git a/ui/qml/xstudio/views/timeline/delegates/XsDelegateTrack.qml b/ui/qml/xstudio/views/timeline/delegates/XsDelegateTrack.qml new file mode 100644 index 000000000..f906e36dc --- /dev/null +++ b/ui/qml/xstudio/views/timeline/delegates/XsDelegateTrack.qml @@ -0,0 +1,220 @@ +// 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.helpers 1.0 + +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 + + property var title: "" + + property bool isSizerHovered: ListView.view.isSizerHovered + property bool isSizerDragging: ListView.view.isSizerDragging + property var setSizerHovered: ListView.view.setSizerHovered + property var setSizerDragging: ListView.view.setSizerDragging + + property var draggingStarted: ListView.view.draggingStarted + property var dragging: ListView.view.dragging + property var draggingStopped: ListView.view.draggingStopped + property var doubleTapped: ListView.view.doubleTapped + property var tapped: ListView.view.tapped + + + property bool isDragging: false + property int moveY: "move_y" in userDataRole ? userDataRole.move_y : 0 + + width: ListView.view.width + height: itemHeight * scaleY + + property bool isHovered: hoveredItem == control + property bool isSelected: false + property bool isConformSource: DelegateModel.model.modelIndex(index) == conformSourceIndex + property var timelineSelection: ListView.view.timelineSelection + property var hoveredItem: ListView.view.hoveredItem + property var itemTypeRole: typeRole + property alias list_view: list_view + + function modelIndex() { + return helpers.makePersistent(DelegateModel.model.modelIndex(index)) + } + + Connections { + target: timelineSelection + function onSelectionChanged(selected, deselected) { + if(isSelected && helpers.itemSelectionContains(deselected, control.DelegateModel.model.modelIndex(index))) + isSelected = false + else if(!isSelected && helpers.itemSelectionContains(selected, control.DelegateModel.model.modelIndex(index))) + isSelected = true + } + } + + onIsDraggingChanged: { + if(isDragging) { + // let new_parent = control.parent.parent.parent.parent + let new_parent = control.parent + let orig = track_header.mapFromItem(new_parent, track_header.x, track_header.y) + track_header.parent = new_parent + track_header.mappedX = (-orig.x) + 10 + track_header.mappedY = -orig.y + } else { + track_header.parent = control + track_header.mappedX = 0 + track_header.mappedY = 0 + } + + } + + XsTrackHeader { + id: track_header + z: 2 + + anchors.top: isDragging ? undefined : parent.top + anchors.left: isDragging ? undefined : parent.left + + property real mappedX: 0 + property real mappedY: 0 + x: mappedX + y: mappedY + ( + moveY > 0 ? (moveY * height) + height/2 : + (moveY < 0 ? ((moveY-1) * height) + height/2 : 0) + ) + + + width: trackHeaderWidth + height: Math.ceil(control.itemHeight * control.scaleY) + + isHovered: control.isHovered + itemFlag: control.itemFlag + trackIndex: trackIndexRole + setTrackHeaderWidth: control.setTrackHeaderWidth + text: nameRole + title: control.title + isEnabled: enabledRole + isLocked: lockedRole + isSelected: control.isSelected + isConformSource: control.isConformSource + isSizerHovered: control.isSizerHovered + isSizerDragging: control.isSizerDragging + notificationModel: notificationRole + + onSizerHovered: setSizerHovered(hovered) + onSizerDragging: setSizerDragging(dragging) + + onEnabledClicked: enabledRole = !enabledRole + onLockedClicked: lockedRole = !lockedRole + onConformSourceClicked: conformSourceIndex = helpers.makePersistent(modelIndex()) + onFlagSet: flagItems([modelIndex()], flag == "#00000000" ? "": flag) + + onDraggingStarted: { + control.draggingStarted(modelIndex(), control, mode) + isDragging = true + } + + onDragging: { + control.dragging(modelIndex(), control, mode, x / scaleX, y / scaleY / control.itemHeight) + } + // onDoubleTapped: control.doubleTapped(control, mode) + onTapped: control.tapped(button, x, y, modifiers, control) + onDraggingStopped: { + control.draggingStopped(modelIndex(), control, mode) + isDragging = false + } + + } + + Component.onCompleted: { + flicker.forceEval = !flicker.forceEval + } + + Flickable { + id: flicker + property bool forceEval: false + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: track_header.right + anchors.right: parent.right + + interactive: false + + contentWidth: contentItem.childrenRect.width + contentHeight: contentItem.childrenRect.height + contentX: (forceEval && !forceEval) || control.cX + + onContentWidthChanged: { + if(contentX != control.cX) { + forceEval = !forceEval + } + } + + Row { + id:list_view + // opacity: isHovered ? 1.0 : enabledRole ? (lockedRole ? 0.6 : 1.0) : 0.3 + + property real scaleX: control.scaleX + property real scaleY: control.scaleY + property real itemHeight: control.itemHeight + property var timelineSelection: control.timelineSelection + property var timelineItem: control.timelineItem + property var hoveredItem: control.hoveredItem + property real trackHeaderWidth: control.trackHeaderWidth + property string itemFlag: control.itemFlag + // property bool isParentLocked: lockedRole + property var itemAtIndex: item_repeater.itemAt + property var parentLV: control.parentLV + + property var draggingStarted: control.draggingStarted + property var dragging: control.dragging + property var draggingStopped: control.draggingStopped + property var doubleTapped: control.doubleTapped + property var tapped: control.tapped + + property bool isParentLocked: lockedRole + property bool isParentEnabled: enabledRole + + Repeater { + id: item_repeater + model: DelegateModel { + id: track_items + property var srcModel: theSessionData + model: srcModel + rootIndex: helpers.makePersistent(control.DelegateModel.model.modelIndex(index)) + + delegate: DelegateChooser { + role: "typeRole" + + DelegateChoice { + roleValue: "Clip" + XsDelegateClip {} + } + + DelegateChoice { + roleValue: "Gap" + XsDelegateGap {} + } + } + } + } + } + } +} diff --git a/ui/qml/xstudio/views/timeline/delegates/XsDelegateVideoTrack.qml b/ui/qml/xstudio/views/timeline/delegates/XsDelegateVideoTrack.qml index 10dac3cab..0db9a0b97 100644 --- a/ui/qml/xstudio/views/timeline/delegates/XsDelegateVideoTrack.qml +++ b/ui/qml/xstudio/views/timeline/delegates/XsDelegateVideoTrack.qml @@ -11,160 +11,6 @@ import QuickPromise 1.0 import xStudio 1.0 import xstudio.qml.helpers 1.0 -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 - - property bool isSizerHovered: ListView.view.isSizerHovered - property bool isSizerDragging: ListView.view.isSizerDragging - property var setSizerHovered: ListView.view.setSizerHovered - property var setSizerDragging: ListView.view.setSizerDragging - - property var draggingStarted: ListView.view.draggingStarted - property var dragging: ListView.view.dragging - property var draggingStopped: ListView.view.draggingStopped - property var doubleTapped: ListView.view.doubleTapped - property var tapped: ListView.view.tapped - - width: ListView.view.width - height: itemHeight * scaleY - - property bool isHovered: hoveredItem == control - property bool isSelected: false - property bool isConformSource: DelegateModel.model.modelIndex(index) == conformSourceIndex - property var timelineSelection: ListView.view.timelineSelection - property var hoveredItem: ListView.view.hoveredItem - property var itemTypeRole: typeRole - property alias list_view: list_view - - function modelIndex() { - return helpers.makePersistent(DelegateModel.model.modelIndex(index)) - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, control.DelegateModel.model.modelIndex(index))) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, control.DelegateModel.model.modelIndex(index))) - isSelected = true - } - } - - 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 - title: "Video Track" - isEnabled: enabledRole - isLocked: lockedRole - isSelected: control.isSelected - isConformSource: control.isConformSource - isSizerHovered: control.isSizerHovered - isSizerDragging: control.isSizerDragging - notificationModel: notificationRole - - onSizerHovered: setSizerHovered(hovered) - onSizerDragging: setSizerDragging(dragging) - - onEnabledClicked: enabledRole = !enabledRole - onLockedClicked: lockedRole = !lockedRole - onConformSourceClicked: conformSourceIndex = helpers.makePersistent(control.DelegateModel.model.modelIndex(index)) - onFlagSet: flagItems([control.DelegateModel.model.modelIndex(index)], flag == "#00000000" ? "": flag) - } - - Flickable { - id: flicker - property bool forceEval: false - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: track_header.right - anchors.right: parent.right - - interactive: false - - contentWidth: contentItem.childrenRect.width - contentHeight: contentItem.childrenRect.height - contentX: (forceEval && !forceEval) || control.cX - - onContentWidthChanged: { - if(contentX != control.cX) { - forceEval = !forceEval - } - } - clip: false - - Row { - id:list_view - clip: false - // opacity: isHovered ? 1.0 : enabledRole ? (lockedRole ? 0.6 : 1.0) : 0.3 - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - property var timelineItem: control.timelineItem - property var hoveredItem: control.hoveredItem - property real trackHeaderWidth: control.trackHeaderWidth - property string itemFlag: control.itemFlag - // property bool isParentLocked: lockedRole - property var itemAtIndex: item_repeater.itemAt - property var parentLV: control.parentLV - - property var draggingStarted: control.draggingStarted - property var dragging: control.dragging - property var draggingStopped: control.draggingStopped - property var doubleTapped: control.doubleTapped - property var tapped: control.tapped - - property bool isParentLocked: lockedRole - property bool isParentEnabled: enabledRole - - Repeater { - id: item_repeater - model: DelegateModel { - id: track_items - property var srcModel: theSessionData - model: srcModel - rootIndex: helpers.makePersistent(control.DelegateModel.model.modelIndex(index)) - - delegate: DelegateChooser { - role: "typeRole" - - DelegateChoice { - roleValue: "Clip" - XsDelegateClip {} - } - - DelegateChoice { - roleValue: "Gap" - XsDelegateGap {} - } - } - } - } - } - } +XsDelegateTrack { + title: "Video Track" } diff --git a/ui/qml/xstudio/views/timeline/widgets/XsTrackHeader.qml b/ui/qml/xstudio/views/timeline/widgets/XsTrackHeader.qml index 5fa6ab760..4297106c4 100644 --- a/ui/qml/xstudio/views/timeline/widgets/XsTrackHeader.qml +++ b/ui/qml/xstudio/views/timeline/widgets/XsTrackHeader.qml @@ -33,6 +33,12 @@ Item { signal conformSourceClicked() signal flagSet(string flag, string flag_text) + signal draggingStarted(mode: string) + signal dragging(mode: string, x: real, y: real) + signal draggingStopped(mode: string) + signal tapped(button: int, x: real, y: real, modifiers: int) + + XsGradientRectangle { id: control_background @@ -47,6 +53,46 @@ Item { topColor: isSelected ? Qt.darker(palette.highlight, 2) : XsStyleSheet.panelBgGradTopColor bottomColor: isSelected ? Qt.darker(palette.highlight, 2) : XsStyleSheet.panelBgGradBottomColor + TapHandler { + acceptedModifiers: Qt.NoModifier + onSingleTapped: { + let g = mapToGlobal(0,0) + control.tapped(Qt.LeftButton, g.x, g.y, Qt.NoModifier) + } + } + + TapHandler { + acceptedModifiers: Qt.ShiftModifier + onSingleTapped: { + let g = mapToGlobal(0,0) + control.tapped(Qt.LeftButton, g.x, g.y, Qt.ShiftModifier) + } + } + + TapHandler { + acceptedModifiers: Qt.ControlModifier + onSingleTapped: { + let g = mapToGlobal(0,0) + control.tapped(Qt.LeftButton, g.x, g.y, Qt.ControlModifier) + } + } + + DragHandler { + cursorShape: Qt.PointingHandCursor + xAxis.enabled: false + + dragThreshold: 5 + + onTranslationChanged: dragging("track", translation.x, translation.y) + onActiveChanged: { + if(active) { + draggingStarted("track") + } else { + draggingStopped("track") + } + } + } + RowLayout { spacing: 10 anchors.fill: parent diff --git a/ui/qml/xstudio/views/viewport/XsOffscreenViewportOverlays.qml b/ui/qml/xstudio/views/viewport/XsOffscreenViewportOverlays.qml index 14d6225dd..473abe83e 100644 --- a/ui/qml/xstudio/views/viewport/XsOffscreenViewportOverlays.qml +++ b/ui/qml/xstudio/views/viewport/XsOffscreenViewportOverlays.qml @@ -52,6 +52,8 @@ Item { property var sessionPath: sessionProperties.values.pathRole + property alias viewportPlayhead: sessionData.current_playhead + XsViewportHUD {} XsViewportOverlays {} diff --git a/ui/qml/xstudio/views/viewport/XsViewport.qml b/ui/qml/xstudio/views/viewport/XsViewport.qml index 1b3ef793b..b65bd255b 100644 --- a/ui/qml/xstudio/views/viewport/XsViewport.qml +++ b/ui/qml/xstudio/views/viewport/XsViewport.qml @@ -30,7 +30,7 @@ Viewport { elementsVisible = !elementsVisible } } - + property alias hide_ui_hotkey: hide_ui_hotkey onPointerEntered: { diff --git a/ui/qml/xstudio/views/viewport/XsViewportPanel.qml b/ui/qml/xstudio/views/viewport/XsViewportPanel.qml index 1414ff3df..2541024e3 100644 --- a/ui/qml/xstudio/views/viewport/XsViewportPanel.qml +++ b/ui/qml/xstudio/views/viewport/XsViewportPanel.qml @@ -239,10 +239,9 @@ Rectangle{ onCurrentLayoutChanged:{ if (currentLayout !== "Present") { menuBarVisible = true + } else { + menuBarVisible = elementsVisible } - // else{ - // menuBarVisible = elementsVisible - // } } property bool elementsVisible: true diff --git a/ui/qml/xstudio/views/viewport/widgets/XsViewportInfoBar.qml b/ui/qml/xstudio/views/viewport/widgets/XsViewportInfoBar.qml index 0d5e4675b..c647af298 100644 --- a/ui/qml/xstudio/views/viewport/widgets/XsViewportInfoBar.qml +++ b/ui/qml/xstudio/views/viewport/widgets/XsViewportInfoBar.qml @@ -86,6 +86,7 @@ Rectangle { shortText: abbr_title valueText: "" + value isBgGradientVisible: false + enabled: attr_enabled } } diff --git a/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml index 8294a2ce3..b36615139 100644 --- a/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml +++ b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerCompareModeButton.qml @@ -50,6 +50,7 @@ XsViewerAnyMenuButton { // Awkward two way binding needed here... onActivated: { compare_mode = title + hide_menu() } enabled: title == "String" ? !viewportPlayhead.timelineMode : true } diff --git a/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorButton.qml b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorButton.qml index b9c9548f8..8f93232e8 100644 --- a/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorButton.qml +++ b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorButton.qml @@ -2,10 +2,12 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtGraphicalEffects 1.12 +import QtQuick.Layouts 1.15 import xStudio 1.0 import xstudio.qml.models 1.0 import xstudio.qml.helpers 1.0 +import xstudio.qml.viewport 1.0 // 'XsViewerAnyMenuButton' can be used as a base for a button that appears // in the viewport toolbar and that shows a pop-up menu when it is clicked @@ -25,4 +27,47 @@ XsViewerAnyMenuButton { shortText: "Src" valueText: viewportPlayhead.imageSourceName ? viewportPlayhead.imageSourceName : "" + XsMenuModelItem { + id: foo + text: "Select ..." + menuItemType: "button" + menuPath: "" + menuItemPosition: 2.75 + menuModelName: source_button.menuModelName + onActivated: { + // awkward! We have an instance of this XsMenuModelItem for each viewport + // toolbar instance. When the user clicks on "Select ..." button in the + // menu, every XsMenuModelItem instance here will get onActivated signal. + // This means we will end up running the code here multiple times and + // would get multiple XsViewerSourceSelectorPopupWindow showing. + // However, the onActivated signal has a 'menuContext' argument which is the + // high level XsViewportPanel object. + // Thus we filter on that here to ensure we don't have multiple source + // selector windows pop-up. + if (menuContext == viewportWidget) { + loader.sourceComponent = dialog + loader.item.visible = true + } + } + } + + Loader { + id: loader + } + + Component { + id: dialog + + XsFloatingViewWindow { + + minimumWidth: 250 + minimumHeight:220 + defaultWidth: 250 + defaultHeight: 300 + + name: "Source Selector" + content_qml: "qrc:/views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml" + } + + } } diff --git a/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml new file mode 100644 index 000000000..4dacc2ed0 --- /dev/null +++ b/ui/qml/xstudio/views/viewport/widgets/toolbar/XsViewerSourceSelectorPopupWindow.qml @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 +import QtQuick.Layouts 1.15 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.viewport 1.0 + + +Item{ + + property real maxTextWidth: 0 + + XsHotkeyReference { + id: cycle_up_hotkey + hotkeyName: "Cycle Image Layer / EXR Part (Up)" + } + + XsHotkeyReference { + id: cycle_down_hotkey + hotkeyName: "Cycle Image Layer / EXR Part (Down)" + } + + + ColumnLayout { + anchors.fill: parent + spacing: 1 + + XsText { id: textDiv + Layout.margins: 10 + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight + + text: "Select Image Layer (EXR group/part): " + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + + XsListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + anchors.margins: 6 + model: currentPlayhead.image_stream_options + delegate: listItem + } + + XsText { + Layout.margins: 10 + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight*2 + + text: "(Use hotkeys "+cycle_up_hotkey.sequence +"/"+ cycle_down_hotkey.sequence + " to cycle through the image layers.)" + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.italic: true + wrapMode: Text.Wrap + } + + XsSimpleButton { + + Layout.alignment: Qt.AlignRight|Qt.AlignVCenter + Layout.rightMargin: 10 + Layout.bottomMargin: 10 + + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + onClicked: { + close() + } + } + } + + Component{ + id: listItem + + Item{ + width: listView.width + height: XsStyleSheet.widgetStdHeight + + property var currentText: currentPlayhead.image_stream_options[index] + property bool isChecked: currentText == currentPlayhead.current_image_stream + + MouseArea{ id: mArea + hoverEnabled: true + anchors.fill: parent + + onClicked: { + currentPlayhead.current_image_stream = currentText + //listView.currentIndex = index + } + } + + RowLayout{ + anchors.fill: parent + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + + Rectangle{ + Layout.preferredWidth: radioDiv.width + textDiv.width + 12 + Layout.fillHeight: true + color: "transparent" + border.width: mArea.containsMouse? 1:0 + border.color: XsStyleSheet.accentColor + + RowLayout{ + anchors.fill: parent + spacing: 3 + + Item{ + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 + Layout.fillHeight: true + } + Item{ id: radioDiv + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + + XsImage { + anchors.fill: parent + source: isChecked ? + "qrc:/icons/radio_button_checked.svg" : + "qrc:/icons/radio_button_unchecked.svg" + } + } + + XsText { id: textDiv + Layout.minimumWidth: 100 + Layout.preferredWidth: maxTextWidth + Layout.fillHeight: true + + text: currentPlayhead.image_stream_options[index] + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + // color: isChecked? XsStyleSheet.accentColor : XsStyleSheet.primaryTextColor + + Component.onCompleted: { + if(maxTextWidth < textWidth) maxTextWidth = textWidth + } + } + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 + Layout.fillHeight: true + } + } + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + + } + + } + + + } +} diff --git a/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerTimeline.qml b/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerTimeline.qml index 9ee62fc9d..2e3608adb 100644 --- a/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerTimeline.qml +++ b/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerTimeline.qml @@ -81,7 +81,7 @@ Item { anchors.bottom: parent.bottom x: scaleFactor*viewportPlayhead.loopStartFrame width: scaleFactor*(viewportPlayhead.loopEndFrame - viewportPlayhead.loopStartFrame) - visible: viewportPlayhead.enableLoopRange != 0 && viewportPlayhead.loopStartFrame != 0 && viewportPlayhead.loopEndFrame < viewportPlayhead.durationFrames + visible: viewportPlayhead.enableLoopRange && (viewportPlayhead.loopStartFrame || viewportPlayhead.loopEndFrame < (viewportPlayhead.durationFrames-1)) color: XsStyleSheet.accentColor } diff --git a/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerVolumeButton.qml b/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerVolumeButton.qml index e5cc763f0..d007ec6ab 100644 --- a/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerVolumeButton.qml +++ b/ui/qml/xstudio/views/viewport/widgets/transport_bar/XsViewerVolumeButton.qml @@ -24,7 +24,11 @@ XsPrimaryButton{ id: volumeButton property alias slider: volumeSlider onClicked:{ - popup.open() + if (popup.opened) { + popup.close() + } else { + popup.open() + } } /* This connects to the backend annotations tool object and exposes its @@ -69,7 +73,7 @@ XsPrimaryButton{ id: volumeButton XsText{ id: valueDisplay Layout.preferredHeight: XsStyleSheet.widgetStdHeight+(2*2) Layout.preferredWidth: parent.width - text: volume/10 + text: parseInt(volume/10 + 0.5) // opacity: muted? 0.7:1 font.bold: true } @@ -84,13 +88,18 @@ XsPrimaryButton{ id: volumeButton handleColor: muted? Qt.darker(palette.text,1.2) : palette.text onValueChanged: { if (pressed) { - volume = parseInt(value)*10 + volume = value*10.0 if (value) muted = false } } onReleased:{ popup.close() } + onHoveredChanged: { + if (!hovered && !pressed) { + popup.close() + } + } } // Item{ diff --git a/ui/qml/xstudio/widgets/buttons/XsSearchButton.qml b/ui/qml/xstudio/widgets/buttons/XsSearchButton.qml index e545ddaca..1e369e7e4 100644 --- a/ui/qml/xstudio/widgets/buttons/XsSearchButton.qml +++ b/ui/qml/xstudio/widgets/buttons/XsSearchButton.qml @@ -26,6 +26,10 @@ Item { signal editingCompleted() + onVisibleChanged: { + //to expand if text exists, while swtiching layout + isExpanded = searchBar.text + } XsPrimaryButton{ id: searchBtn x: isExpandedToLeft? searchBar.width : 0 diff --git a/ui/qml/xstudio/widgets/controls/XsComboBox.qml b/ui/qml/xstudio/widgets/controls/XsComboBox.qml index a3e843c76..54724bb14 100644 --- a/ui/qml/xstudio/widgets/controls/XsComboBox.qml +++ b/ui/qml/xstudio/widgets/controls/XsComboBox.qml @@ -32,6 +32,8 @@ T.ComboBox { id: widget property real borderRadius: 0 property real framePadding: 6 + property string defaultText: "" + property real fontSize: XsStyleSheet.fontSize property var fontFamily: XsStyleSheet.fontFamily @@ -82,7 +84,7 @@ T.ComboBox { id: widget contentItem: T.TextField{ id: textField - text: activeFocus && widget.editable ? widget.displayText: textMetrics.elidedText + text: activeFocus && widget.editable ? widget.displayText : (textMetrics.elidedText ? textMetrics.elidedText : defaultText) onFocusChanged: { if(focus) { diff --git a/ui/qml/xstudio/widgets/dialogs/XsSnapshotDialog.qml b/ui/qml/xstudio/widgets/dialogs/XsSnapshotDialog.qml index 7e367d09a..c70e8b240 100644 --- a/ui/qml/xstudio/widgets/dialogs/XsSnapshotDialog.qml +++ b/ui/qml/xstudio/widgets/dialogs/XsSnapshotDialog.qml @@ -73,10 +73,12 @@ XsWindow { 0, parseInt(widthInput.text), parseInt(heightInput.text)) + if (result != "") { dialogHelpers.errorDialogFunc("Snapshot Failed", result) } else { - dlg.close() + dialog.close() + dialogHelpers.messageDialogFunc("Snapshot Saved", "Snapshot image saved to path " + fixedfileUrl) } } } @@ -182,7 +184,7 @@ XsWindow { Layout.fillWidth: true Layout.preferredHeight: XsStyleSheet.widgetStdHeight attr_title: "Display" - attr_model_name: "offscreen_viewport1_toolbar" + attr_model_name: "snapshot_viewport_toolbar" } XsLabel { @@ -194,7 +196,7 @@ XsWindow { Layout.fillWidth: true Layout.preferredHeight: XsStyleSheet.widgetStdHeight attr_title: "View" - attr_model_name: "offscreen_viewport1_toolbar" + attr_model_name: "snapshot_viewport_toolbar" } } diff --git a/ui/qml/xstudio/widgets/dialogs/XsWindow.qml b/ui/qml/xstudio/widgets/dialogs/XsWindow.qml index 85ef94651..e1ecccf99 100644 --- a/ui/qml/xstudio/widgets/dialogs/XsWindow.qml +++ b/ui/qml/xstudio/widgets/dialogs/XsWindow.qml @@ -6,6 +6,8 @@ import xStudio 1.0 ApplicationWindow { id: window + minimumWidth: 150 + minimumHeight: 100 flags: Qt.platform.os === "windows" ? Qt.Window : Qt.WindowStaysOnTopHint | Qt.Dialog diff --git a/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsCompareModePref.qml b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsCompareModePref.qml new file mode 100644 index 000000000..dcb6e83c1 --- /dev/null +++ b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsCompareModePref.qml @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 +import Qt.labs.qmlmodels 1.0 + +import xstudio.qml.models 1.0 +import xStudio 1.0 + +import "../widgets" + +RowLayout { + + width: parent.width + height: XsStyleSheet.widgetStdHeight + + property var compare_options: ["Off", "A/B", "Grid", "Wipe", "Horizontal", "Vertical", "String", "PiP"] + property var align_options: ["Off", "On", "On (Trim)"] + + property var value__: valueRole + + // use this to prevent circular update + property bool updating: false + + onValue__Changed: { + if (!updating) { + updating = true + var idx = compare_options.indexOf(valueRole[0]) + combo_box.currentIndex = idx + idx = align_options.indexOf(valueRole[1]) + combo_box2.currentIndex = idx + updating = false + } + } + + function updateBackend() { + if (!updating) { + updating = true + var v = [] + v.push(compare_options[combo_box.currentIndex]) + v.push(align_options[combo_box2.currentIndex]) + valueRole = v + updating = false + } + } + + XsLabel { + Layout.alignment: Qt.AlignVCenter|Qt.AlignRight + Layout.preferredWidth: parent.width/3 //prefsLabelWidth + Layout.maximumWidth: parent.width/2 + + text: displayNameRole + horizontalAlignment: Text.AlignRight + } + + RowLayout { + + Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft + Layout.preferredWidth: prefsLabelWidth + Layout.minimumWidth: prefsLabelWidth/2 + Layout.fillHeight: true + + XsComboBox { + + id: combo_box + Layout.fillHeight: true + Layout.fillWidth: true + model: compare_options + onCurrentIndexChanged: { + updateBackend() + } + } + + XsLabel { + Layout.leftMargin: 10 + Layout.alignment: Qt.AlignVCenter|Qt.AlignRight + text: "Auto Align:" + } + + XsComboBox { + + id: combo_box2 + Layout.fillHeight: true + Layout.fillWidth: true + model: align_options + onCurrentIndexChanged: { + updateBackend() + } + + } + } + + XsPreferenceInfoButton { + } + +} diff --git a/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsPreferenceCategory.qml b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsPreferenceCategory.qml index ebb530610..22d60adcc 100644 --- a/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsPreferenceCategory.qml +++ b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsPreferenceCategory.qml @@ -62,6 +62,20 @@ XsListView { XsColourPreference { } } + DelegateChoice { + roleValue: "json" + Item { + + id: parent_item + width: parent.width + height: XsStyleSheet.widgetStdHeight + property var dynamic_widget + property var qml_code_: optionsRole + onQml_code_Changed: { + dynamic_widget = Qt.createQmlObject(qml_code_, parent_item) + } + } + } } } diff --git a/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsStringMultichoicePreference.qml b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsStringMultichoicePreference.qml index d5349fed2..ddd0bde42 100644 --- a/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsStringMultichoicePreference.qml +++ b/ui/qml/xstudio/widgets/dialogs/preferences/delegates/XsStringMultichoicePreference.qml @@ -21,7 +21,6 @@ RowLayout { } function spangle(oof) { - console.log("Setting ", valueRole, oof) if (valueRole != oof) { valueRole = oof } diff --git a/ui/qml/xstudio/widgets/menus/XsMenu.qml b/ui/qml/xstudio/widgets/menus/XsMenu.qml index 60c00780c..ce13918b9 100644 --- a/ui/qml/xstudio/widgets/menus/XsMenu.qml +++ b/ui/qml/xstudio/widgets/menus/XsMenu.qml @@ -259,7 +259,8 @@ XsPopup { onClicked: { menu_model.nodeActivated(menu_model_index, "menu", helpers.contextPanel(the_popup)) - the_popup.closeAll() + // if(mouse.modifiers == Qt.NoModifier) + // the_popup.closeAll() } width: view.width @@ -286,33 +287,7 @@ XsPopup { isChecked: is_checked onClicked:{ is_checked = !is_checked - the_popup.closeAll() - } - onMinWidthChanged: { - view.setMinWidth(minWidth) - } - onLeftIconSizeChanged: { - view.setIndent(leftIconSize) - } - - } - - } - - DelegateChoice { - roleValue: "toggle_checkbox" - - XsMenuItemToggle { - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index(index, 0, the_popup.menu_model_index) - - isRadioButton: true - parent_menu: the_popup - - onClicked: { - // note 'is_checked' data in this context is provided - // by the menu_model - is_checked = !is_checked + // the_popup.closeAll() } onMinWidthChanged: { view.setMinWidth(minWidth) diff --git a/ui/qml/xstudio/widgets/menus/XsMenuItemToggle.qml b/ui/qml/xstudio/widgets/menus/XsMenuItemToggle.qml index 99565382b..0bf432ed6 100644 --- a/ui/qml/xstudio/widgets/menus/XsMenuItemToggle.qml +++ b/ui/qml/xstudio/widgets/menus/XsMenuItemToggle.qml @@ -41,7 +41,7 @@ Item { property color textColor: palette.text property color hotKeyColor: XsStyleSheet.secondaryTextColor - + property real borderWidth: XsStyleSheet.widgetBorderWidth enabled: is_enabled @@ -49,7 +49,7 @@ Item { function hideSubMenus() {} - signal clicked() + signal clicked(mouse: var) MouseArea{ id: menuMouseArea @@ -57,7 +57,7 @@ Item { hoverEnabled: true propagateComposedEvents: true onClicked: { - widget.clicked() + widget.clicked(mouse) } } diff --git a/ui/qml/xstudio/widgets/outputs/XsSplitView.qml b/ui/qml/xstudio/widgets/outputs/XsSplitView.qml index 8e5024cde..e19a44258 100644 --- a/ui/qml/xstudio/widgets/outputs/XsSplitView.qml +++ b/ui/qml/xstudio/widgets/outputs/XsSplitView.qml @@ -10,6 +10,7 @@ SplitView { property real thumbWidth: XsStyleSheet.panelPadding/2 property color colorNormal: XsStyleSheet.primaryTextColor property color colorActive: XsStyleSheet.accentColor + property color colorHandleBg: XsStyleSheet.panelBgColor focus: false @@ -18,7 +19,7 @@ SplitView { handle: Rectangle { implicitWidth: thumbWidth implicitHeight: thumbWidth - color: XsStyleSheet.panelBgColor + color: colorHandleBg Rectangle{ anchors.fill: parent diff --git a/ui/qml/xstudio/windows/XsFloatingViewWindow.qml b/ui/qml/xstudio/windows/XsFloatingViewWindow.qml index 7139df84c..6d732bfe0 100644 --- a/ui/qml/xstudio/windows/XsFloatingViewWindow.qml +++ b/ui/qml/xstudio/windows/XsFloatingViewWindow.qml @@ -12,10 +12,14 @@ import xstudio.qml.helpers 1.0 XsWindow { id: floatingWindow - title: view_name - - property var name: view_name // 'view_name' provided by model - property var content_qml: view_qml_source // 'view_qml_source' provided by model + title: name + minimumWidth: 150 + minimumHeight: 100 + property real defaultWidth: 1024 + property real defaultHeight: 400 + + property var name // 'view_name' provided by model + property var content_qml // 'view_qml_source' provided by model property var content_item // we need this to wipe hotkey_uuid property that might be visible in the @@ -28,8 +32,9 @@ XsWindow { uiLayoutsModel.storeFloatingWindowData(name, user_data) } - onClosing: { - window_is_visible = false + onClosing: { + if(typeof window_is_visible != 'undefined') + window_is_visible = false } onWidthChanged: { @@ -56,8 +61,8 @@ XsWindow { // for now, hardcode panel size x = 200 y = 200 - width = 1024 - height = 400 + width = defaultWidth + height = defaultHeight } if (content_item) content_item.visible = visible diff --git a/ui/qml/xstudio/windows/XsPopoutViewerWindow.qml b/ui/qml/xstudio/windows/XsPopoutViewerWindow.qml index ff2313c7f..5d93c0624 100644 --- a/ui/qml/xstudio/windows/XsPopoutViewerWindow.qml +++ b/ui/qml/xstudio/windows/XsPopoutViewerWindow.qml @@ -15,10 +15,12 @@ import xstudio.qml.viewport 1.0 ApplicationWindow { id: appWindow - visible: true + visible: false color: "#00000000" title: fileName objectName: "xstudio_popout_window" + minimumWidth: 150 + minimumHeight: 100 // this gives us access to the 'role' data of the entry in the session model // for the current on-screen media SOURCE @@ -49,6 +51,7 @@ ApplicationWindow { appWindow.width = ui_layouts_model.get(ui_layouts_model.root_index, "width") appWindow.height = ui_layouts_model.get(ui_layouts_model.root_index, "height") appWindow.user_data = ui_layouts_model.get(ui_layouts_model.root_index, "user_data") + visible = true } } } diff --git a/ui/qml/xstudio/windows/XsSessionWindow.qml b/ui/qml/xstudio/windows/XsSessionWindow.qml index 3a013c3e7..bc8b8fd7f 100644 --- a/ui/qml/xstudio/windows/XsSessionWindow.qml +++ b/ui/qml/xstudio/windows/XsSessionWindow.qml @@ -19,10 +19,12 @@ import "./main_menu_bar/help_menu" // for XsReleaseNotes ApplicationWindow { id: appWindow - visible: true + visible: false color: "#000000" title: (sessionPathNative ? (theSessionData.modified ? sessionPathNative + " - modified": sessionPathNative) : "xSTUDIO") objectName: "xstudio_main_window" + minimumWidth: 150 + minimumHeight: 100 property var window_name: "main_window" @@ -54,6 +56,7 @@ ApplicationWindow { appWindow.width = ui_layouts_model.get(ui_layouts_model.root_index, "width") appWindow.height = ui_layouts_model.get(ui_layouts_model.root_index, "height") numLayouts = ui_layouts_model.rowCount(root_index) + visible = true } property var numLayouts: 0 @@ -459,7 +462,6 @@ ApplicationWindow { // Now the main window is complete, we can load video output plugins studio.loadVideoOutputPlugins() - } // here we instance 'singleton' items that may or may not be provided @@ -530,7 +532,10 @@ ApplicationWindow { } Component { id: popout + XsFloatingViewWindow { + name: view_name + content_qml: view_qml_source } } diff --git a/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileFunctions.qml b/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileFunctions.qml index ff15c6196..449058e20 100644 --- a/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileFunctions.qml +++ b/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileFunctions.qml @@ -55,12 +55,12 @@ Item { function exportSequencePath(chaserFunc=undefined) { dialogHelpers.showFileDialog( function(fileUrl, undefined, func) { - exportSequence(fileUrl, "otio", func) + exportSequence(fileUrl, "", func) }, defaultSessionFolder(), "Export Sequence", - "otio", - ["OTIO (*.otio)"], + ".otio", + ["All Files (*.*)"].concat(theSessionData.getTimelineExportTypes()), false, false, chaserFunc @@ -305,7 +305,7 @@ Item { if(!index.valid) { // create new playlist.. - index = theSessionData.createPlaylist("New Playlist") + index = theSessionData.createPlaylist(theSessionData.getNextName("Playlist {}")) } callbackTimer.setTimeout(function(capture) { return function(){ diff --git a/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileMenu.qml b/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileMenu.qml index 38e232bd2..ab32f0566 100644 --- a/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileMenu.qml +++ b/ui/qml/xstudio/windows/main_menu_bar/file_menu/XsFileMenu.qml @@ -55,6 +55,11 @@ Item { description: "Closes the xSTUDIO session and application." } + XsPreference { + id: autoSavePath + path: "/core/session/autosave/last_auto_save" + } + /************************************************************** MENU ITEMS @@ -91,9 +96,8 @@ Item { } XsMenuModelItem { - id: me - text: "Import Session ..." - menuPath: "File" + text: "Session ..." + menuPath: "File|Import" menuItemPosition: 3 menuModelName: "main menu bar" onActivated: { @@ -103,13 +107,26 @@ Item { } XsMenuModelItem { - text: "Import OTIO ..." - menuPath: "File" + text: "OTIO ..." + menuPath: "File|Import" menuItemPosition: 3.1 menuModelName: "main menu bar" onActivated: { file_functions.importOTIO() } + Component.onCompleted: { + setMenuPathPosition("File|Import", 3) + } + } + + XsMenuModelItem { + text: "Open Last Auto-Save" + menuPath: "File" + menuItemPosition: 3.2 + menuModelName: "main menu bar" + onActivated: { + file_functions.doLoadSession(autoSavePath.value) + } } Repeater { diff --git a/ui/qml/xstudio/windows/quickview/XsQuickViewLauncher.qml b/ui/qml/xstudio/windows/quickview/XsQuickViewLauncher.qml index 5bf581682..7cf632ded 100644 --- a/ui/qml/xstudio/windows/quickview/XsQuickViewLauncher.qml +++ b/ui/qml/xstudio/windows/quickview/XsQuickViewLauncher.qml @@ -36,9 +36,11 @@ Item { closeButton ? ["Close"] : []) // try again in 200 milliseconds - callbackTimer.setTimeout(function() { return function() { - dialogHelpers.hideLastDialog() - }}(), timeoutSecs*1000); + if (timeoutSecs != 0) { + callbackTimer.setTimeout(function() { return function() { + dialogHelpers.hideLastDialog() + }}(), timeoutSecs*1000); + } } diff --git a/ui/qml/xstudio/windows/quickview/XsQuickViewWindow.qml b/ui/qml/xstudio/windows/quickview/XsQuickViewWindow.qml index 144e6c69e..2dbfebfa0 100644 --- a/ui/qml/xstudio/windows/quickview/XsQuickViewWindow.qml +++ b/ui/qml/xstudio/windows/quickview/XsQuickViewWindow.qml @@ -19,6 +19,8 @@ ApplicationWindow { color: "#00000000" title: "xSTUDIO QuickView - "+fileName objectName: "xstudio_quickview_window" + minimumWidth: 150 + minimumHeight: 100 // this gives us access to the 'role' data of the entry in the session model // for the current on-screen media SOURCE diff --git a/ui/qml/xstudio/windows/runtime/XsRuntimeQMLItems.qml b/ui/qml/xstudio/windows/runtime/XsRuntimeQMLItems.qml index b04949013..2e09291ef 100644 --- a/ui/qml/xstudio/windows/runtime/XsRuntimeQMLItems.qml +++ b/ui/qml/xstudio/windows/runtime/XsRuntimeQMLItems.qml @@ -49,6 +49,7 @@ Item { dynamic_widget.visible = true } } + function xstudio_callback(data) { if (typeof data == "object") { // 'callback_data' is role data on the attribute in the 'dynamic_items' @@ -56,6 +57,20 @@ Item { callback_data = data } } + + function python_callback(python_func_name, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) { + + var v = [_1, _2, _3, _4, _5, _6, _7, _8, _9, _10] + while (v.length) { + if (v[v.length-1] == null || v[v.length-1] == undefined) { + v.pop() + } else { + break; + } + } + return helpers.python_callback(python_func_name, module_uuid, v) + + } } } } \ No newline at end of file diff --git a/ui/qml/xstudio/xStudio/qmldir b/ui/qml/xstudio/xStudio/qmldir index a08c83dcd..9c6e2926c 100644 --- a/ui/qml/xstudio/xStudio/qmldir +++ b/ui/qml/xstudio/xStudio/qmldir @@ -49,6 +49,7 @@ XsDelegateClip 1.0 ../views/timeline/delegates/XsDelegateClip.qml XsDelegateGap 1.0 ../views/timeline/delegates/XsDelegateGap.qml XsDelegateStack 1.0 ../views/timeline/delegates/XsDelegateStack.qml XsDelegateVideoTrack 1.0 ../views/timeline/delegates/XsDelegateVideoTrack.qml +XsDelegateTrack 1.0 ../views/timeline/delegates/XsDelegateTrack.qml XsClipItem 1.0 ../views/timeline/widgets/XsClipItem.qml XsClipDragBoth 1.0 ../views/timeline/widgets/XsClipDragBoth.qml @@ -113,6 +114,7 @@ XsSnapshotDialog 1.0 ../widgets/dialogs/XsSnapshotDialog.qml XsStringRequestDialog 1.0 ../widgets/dialogs/XsStringRequestDialog.qml XsWindow 1.0 ../widgets/dialogs/XsWindow.qml XsPreferencesDialog 1.0 ../widgets/dialogs/preferences/XsPreferencesDialog.qml +XsCompareModePref 1.0 ../widgets/dialogs/preferences/delegates/XsCompareModePref.qml XsHotkeysDialog 1.0 ../widgets/dialogs/hotkeys/XsHotkeysDialog.qml XsBusyIndicator 1.0 ../widgets/indicators/XsBusyIndicator.qml