diff --git a/.github/actions/qt-xml-validator/action.yaml b/.github/actions/qt-xml-validator/action.yaml index 695be4482..6539e3118 100644 --- a/.github/actions/qt-xml-validator/action.yaml +++ b/.github/actions/qt-xml-validator/action.yaml @@ -39,7 +39,7 @@ runs: uses: ./.github/actions/check-changes id: checks with: - checkGlob: 'UI/forms/**/*.ui' + checkGlob: 'frontend/forms/**/*.ui' - name: Validate XML 💯 if: fromJSON(steps.checks.outputs.hasChangedFiles) @@ -56,6 +56,6 @@ runs: if (( ${#CHANGED_FILES[@]} )); then if [[ '${{ inputs.failCondition }}' == never ]]; then set +e; fi xmllint \ - --schema ${{ github.workspace }}/UI/forms/XML-Schema-Qt5.15.xsd \ + --schema ${{ github.workspace }}/frontend/forms/XML-Schema-Qt5.15.xsd \ --noout "${CHANGED_FILES[@]}" fi diff --git a/.github/scripts/.build.zsh b/.github/scripts/.build.zsh index 2de6c29b8..f579aca3f 100755 --- a/.github/scripts/.build.zsh +++ b/.github/scripts/.build.zsh @@ -201,7 +201,7 @@ build() { rm -rf OBS.app mkdir OBS.app - ditto UI/${config}/OBS.app OBS.app + ditto frontend/${config}/OBS.app OBS.app } } popd diff --git a/.gitignore b/.gitignore index 7c1082426..d108e96e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ !/cmake !/deps !/docs +!/frontend !/libobs* !/plugins !/shared diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a45780d1..d3fcec13b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,6 @@ add_subdirectory(plugins) add_subdirectory(test/test-input) -add_subdirectory(UI) +add_subdirectory(frontend) message_configuration() diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp deleted file mode 100644 index bb3715017..000000000 --- a/UI/api-interface.cpp +++ /dev/null @@ -1,636 +0,0 @@ -#include -#include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" - -#include - -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -extern volatile bool streaming_active; -extern volatile bool recording_active; -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; -extern volatile bool virtualcam_active; - -/* ------------------------------------------------------------------------- */ - -template struct OBSStudioCallback { - T callback; - void *private_data; - - inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} -}; - -template -inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) -{ - for (size_t i = 0; i < callbacks.size(); i++) { - OBSStudioCallback curCB = callbacks[i]; - if (curCB.callback == callback && curCB.private_data == private_data) - return i; - } - - return (size_t)-1; -} - -struct OBSStudioAPI : obs_frontend_callbacks { - OBSBasic *main; - vector> callbacks; - vector> saveCallbacks; - vector> preloadCallbacks; - - inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} - - void *obs_frontend_get_main_window(void) override { return (void *)main; } - - void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); } - - void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); } - - void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->scenes->count(); i++) { - QListWidgetItem *item = main->ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - obs_source_t *source = obs_scene_get_source(scene); - - if (obs_source_get_ref(source) != nullptr) - da_push_back(sources->sources, &source); - } - } - - obs_source_t *obs_frontend_get_current_scene(void) override - { - if (main->IsPreviewProgramMode()) { - return obs_weak_source_get_source(main->programScene); - } else { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - } - - void obs_frontend_set_current_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene))); - } else { - QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); - } - } - - void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override - { - for (int i = 0; i < main->ui->transitions->count(); i++) { - OBSSource tr = main->ui->transitions->itemData(i).value(); - - if (!tr) - continue; - - if (obs_source_get_ref(tr) != nullptr) - da_push_back(sources->sources, &tr); - } - } - - obs_source_t *obs_frontend_get_current_transition(void) override - { - OBSSource tr = main->GetCurrentTransition(); - return obs_source_get_ref(tr); - } - - void obs_frontend_set_current_transition(obs_source_t *transition) override - { - QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); - } - - int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); } - - void obs_frontend_set_transition_duration(int duration) override - { - QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); - } - - void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); } - - void obs_frontend_set_tbar_position(int position) override - { - QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); - } - - int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); } - - void obs_frontend_get_scene_collections(std::vector &strings) override - { - for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { - strings.emplace_back(collectionName); - } - } - - char *obs_frontend_get_current_scene_collection(void) override - { - const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); - return bstrdup(currentCollection.name.c_str()); - } - - void obs_frontend_set_current_scene_collection(const char *collection) override - { - QList menuActions = main->ui->sceneCollectionMenu->actions(); - QString qstrCollection = QT_UTF8(collection); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrCollection) { - action->trigger(); - break; - } - } - } - } - - bool obs_frontend_add_scene_collection(const char *name) override - { - bool success = false; - QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), - Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name))); - return success; - } - - void obs_frontend_get_profiles(std::vector &strings) override - { - const OBSProfileCache &profiles = main->GetProfileCache(); - - for (auto &[profileName, profile] : profiles) { - strings.emplace_back(profileName); - } - } - - char *obs_frontend_get_current_profile(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); - return bstrdup(profile.name.c_str()); - } - - char *obs_frontend_get_current_profile_path(void) override - { - const OBSProfile &profile = main->GetCurrentProfile(); - - return bstrdup(profile.path.u8string().c_str()); - } - - void obs_frontend_set_current_profile(const char *profile) override - { - QList menuActions = main->ui->profileMenu->actions(); - QString qstrProfile = QT_UTF8(profile); - - for (int i = 0; i < menuActions.count(); i++) { - QAction *action = menuActions[i]; - QVariant v = action->property("file_name"); - - if (v.typeName() != nullptr) { - if (action->text() == qstrProfile) { - action->trigger(); - break; - } - } - } - } - - void obs_frontend_create_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); - } - - void obs_frontend_duplicate_profile(const char *name) override - { - QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); - } - - void obs_frontend_delete_profile(const char *profile) override - { - QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); - } - - void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); } - - void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); } - - bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); } - - void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); } - - void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); } - - bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); } - - void obs_frontend_recording_pause(bool pause) override - { - QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); - } - - bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); } - - bool obs_frontend_recording_split_file(void) override - { - if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { - proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - uint8_t stack[128]; - calldata cd; - calldata_init_fixed(&cd, stack, sizeof(stack)); - proc_handler_call(ph, "split_file", &cd); - bool result = calldata_bool(&cd, "split_file_enabled"); - return result; - } else { - return false; - } - } - - bool obs_frontend_recording_add_chapter(const char *name) override - { - if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) - return false; - - proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); - - calldata cd; - calldata_init(&cd); - calldata_set_string(&cd, "chapter_name", name); - bool result = proc_handler_call(ph, "add_chapter", &cd); - calldata_free(&cd); - return result; - } - - void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); } - - void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); } - - void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); } - - bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); } - - void *obs_frontend_add_tools_menu_qaction(const char *name) override - { - main->ui->menuTools->setEnabled(true); - return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); - } - - void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override - { - main->ui->menuTools->setEnabled(true); - - auto func = [private_data, callback]() { - callback(private_data); - }; - - QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); - QObject::connect(action, &QAction::triggered, func); - } - - void *obs_frontend_add_dock(void *dock) override - { - QDockWidget *d = reinterpret_cast(dock); - - QString name = d->objectName(); - if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { - blog(LOG_WARNING, "The object name of the added dock is empty or already used," - " a temporary one will be set to avoid conflicts"); - - char *uuid = os_generate_uuid(); - name = QT_UTF8(uuid); - bfree(uuid); - name.append("_oldExtraDock"); - - d->setObjectName(name); - } - - return (void *)main->AddDockWidget(d); - } - - bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - OBSDock *dock = new OBSDock(main); - dock->setWidget((QWidget *)widget); - dock->setWindowTitle(QT_UTF8(title)); - dock->setObjectName(QT_UTF8(id)); - - main->AddDockWidget(dock, Qt::RightDockWidgetArea); - - dock->setVisible(false); - dock->setFloating(true); - - return true; - } - - void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); } - - bool obs_frontend_add_custom_qdock(const char *id, void *dock) override - { - if (main->IsDockObjectNameUsed(QT_UTF8(id))) { - blog(LOG_WARNING, - "Dock id '%s' already used! " - "Duplicate library?", - id); - return false; - } - - QDockWidget *d = reinterpret_cast(dock); - d->setObjectName(QT_UTF8(id)); - - main->AddCustomDockWidget(d); - - return true; - } - - void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - callbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(callbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - callbacks.erase(callbacks.begin() + idx); - } - - obs_output_t *obs_frontend_get_streaming_output(void) override - { - auto multitrackVideo = main->outputHandler->multitrackVideo.get(); - auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; - if (mtvOutput) - return mtvOutput; - - OBSOutput output = main->outputHandler->streamOutput.Get(); - return obs_output_get_ref(output); - } - - obs_output_t *obs_frontend_get_recording_output(void) override - { - OBSOutput out = main->outputHandler->fileOutput.Get(); - return obs_output_get_ref(out); - } - - obs_output_t *obs_frontend_get_replay_buffer_output(void) override - { - OBSOutput out = main->outputHandler->replayBuffer.Get(); - return obs_output_get_ref(out); - } - - config_t *obs_frontend_get_profile_config(void) override { return main->activeConfiguration; } - - config_t *obs_frontend_get_global_config(void) override - { - blog(LOG_WARNING, - "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); - return App()->GetAppConfig(); - } - - config_t *obs_frontend_get_app_config(void) override { return App()->GetAppConfig(); } - - config_t *obs_frontend_get_user_config(void) override { return App()->GetUserConfig(); } - - void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) override - { - SavedProjectorInfo proj = { - ProjectorType::Preview, - monitor, - geometry ? geometry : "", - name ? name : "", - }; - if (type) { - if (astrcmpi(type, "Source") == 0) - proj.type = ProjectorType::Source; - else if (astrcmpi(type, "Scene") == 0) - proj.type = ProjectorType::Scene; - else if (astrcmpi(type, "StudioProgram") == 0) - proj.type = ProjectorType::StudioProgram; - else if (astrcmpi(type, "Multiview") == 0) - proj.type = ProjectorType::Multiview; - } - QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), - Q_ARG(SavedProjectorInfo *, &proj)); - } - - void obs_frontend_save(void) override { main->SaveProject(); } - - void obs_frontend_defer_save_begin(void) override { QMetaObject::invokeMethod(main, "DeferSaveBegin"); } - - void obs_frontend_defer_save_end(void) override { QMetaObject::invokeMethod(main, "DeferSaveEnd"); } - - void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - saveCallbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - saveCallbacks.erase(saveCallbacks.begin() + idx); - } - - void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - preloadCallbacks.emplace_back(callback, private_data); - } - - void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); - if (idx == (size_t)-1) - return; - - preloadCallbacks.erase(preloadCallbacks.begin() + idx); - } - - void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override - { - App()->PushUITranslation(translate); - } - - void obs_frontend_pop_ui_translation(void) override { App()->PopUITranslation(); } - - void obs_frontend_set_streaming_service(obs_service_t *service) override { main->SetService(service); } - - obs_service_t *obs_frontend_get_streaming_service(void) override { return main->GetService(); } - - void obs_frontend_save_streaming_service(void) override { main->SaveService(); } - - bool obs_frontend_preview_program_mode_active(void) override { return main->IsPreviewProgramMode(); } - - void obs_frontend_set_preview_program_mode(bool enable) override { main->SetPreviewProgramMode(enable); } - - void obs_frontend_preview_program_trigger_transition(void) override - { - QMetaObject::invokeMethod(main, "TransitionClicked"); - } - - bool obs_frontend_preview_enabled(void) override { return main->previewEnabled; } - - void obs_frontend_set_preview_enabled(bool enable) override - { - if (main->previewEnabled != enable) - main->EnablePreviewDisplay(enable); - } - - obs_source_t *obs_frontend_get_current_preview_scene(void) override - { - if (main->IsPreviewProgramMode()) { - OBSSource source = main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - - return nullptr; - } - - void obs_frontend_set_current_preview_scene(obs_source_t *scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), - Q_ARG(bool, false)); - } - } - - void obs_frontend_take_screenshot(void) override { QMetaObject::invokeMethod(main, "Screenshot"); } - - void obs_frontend_take_source_screenshot(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); - } - - obs_output_t *obs_frontend_get_virtualcam_output(void) override - { - OBSOutput output = main->outputHandler->virtualCam.Get(); - return obs_output_get_ref(output); - } - - void obs_frontend_start_virtualcam(void) override { QMetaObject::invokeMethod(main, "StartVirtualCam"); } - - void obs_frontend_stop_virtualcam(void) override { QMetaObject::invokeMethod(main, "StopVirtualCam"); } - - bool obs_frontend_virtualcam_active(void) override { return os_atomic_load_bool(&virtualcam_active); } - - void obs_frontend_reset_video(void) override { main->ResetVideo(); } - - void obs_frontend_open_source_properties(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_source_filters(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_source_interaction(obs_source_t *source) override - { - QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); - } - - void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override - { - QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); - } - - char *obs_frontend_get_current_record_output_path(void) override - { - const char *recordOutputPath = main->GetCurrentOutputPath(); - - return bstrdup(recordOutputPath); - } - - const char *obs_frontend_get_locale_string(const char *string) override { return Str(string); } - - bool obs_frontend_is_theme_dark(void) override { return App()->IsThemeDark(); } - - char *obs_frontend_get_last_recording(void) override - { - return bstrdup(main->outputHandler->lastRecordingPath.c_str()); - } - - char *obs_frontend_get_last_screenshot(void) override { return bstrdup(main->lastScreenshot.c_str()); } - - char *obs_frontend_get_last_replay(void) override { return bstrdup(main->lastReplay.c_str()); } - - void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, - const char *undo_data, const char *redo_data, bool repeatable) override - { - main->undo_s.add_action( - name, [undo](const std::string &data) { undo(data.c_str()); }, - [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); - } - - void on_load(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_preload(obs_data_t *settings) override - { - for (size_t i = preloadCallbacks.size(); i > 0; i--) { - auto cb = preloadCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_save(obs_data_t *settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, true, cb.private_data); - } - } - - void on_event(enum obs_frontend_event event) override - { - if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && - event != OBS_FRONTEND_EVENT_EXIT) - return; - - for (size_t i = callbacks.size(); i > 0; i--) { - auto cb = callbacks[i - 1]; - cb.callback(event, cb.private_data); - } - } -}; - -obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) -{ - obs_frontend_callbacks *api = new OBSStudioAPI(main); - obs_frontend_set_callbacks_internal(api); - return api; -} diff --git a/UI/cmake/feature-browserpanels.cmake b/UI/cmake/feature-browserpanels.cmake deleted file mode 100644 index 73ef6d2e4..000000000 --- a/UI/cmake/feature-browserpanels.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(TARGET OBS::browser-panels) - target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE) - - target_link_libraries(obs-studio PRIVATE OBS::browser-panels) - - target_sources( - obs-studio - PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp - ) -endif() diff --git a/UI/cmake/feature-importers.cmake b/UI/cmake/feature-importers.cmake deleted file mode 100644 index d11ab27a2..000000000 --- a/UI/cmake/feature-importers.cmake +++ /dev/null @@ -1,10 +0,0 @@ -target_sources( - obs-studio - PRIVATE - importers/classic.cpp - importers/importers.cpp - importers/importers.hpp - importers/sl.cpp - importers/studio.cpp - importers/xsplit.cpp -) diff --git a/UI/cmake/ui-elements.cmake b/UI/cmake/ui-elements.cmake deleted file mode 100644 index d68890945..000000000 --- a/UI/cmake/ui-elements.cmake +++ /dev/null @@ -1,78 +0,0 @@ -if(NOT TARGET OBS::properties-view) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view") -endif() - -if(NOT TARGET OBS::qt-plain-text-edit) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/plain-text-edit" "${CMAKE_BINARY_DIR}/shared/qt/plain-text-edit") -endif() - -if(NOT TARGET OBS::qt-slider-ignorewheel) - add_subdirectory( - "${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel" - "${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel" - ) -endif() - -if(NOT TARGET OBS::qt-vertical-scroll-area) - add_subdirectory( - "${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area" - "${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area" - ) -endif() - -target_link_libraries( - obs-studio - PRIVATE OBS::properties-view OBS::qt-plain-text-edit OBS::qt-slider-ignorewheel OBS::qt-vertical-scroll-area -) - -target_sources( - obs-studio - PRIVATE - absolute-slider.cpp - absolute-slider.hpp - adv-audio-control.cpp - adv-audio-control.hpp - audio-encoders.cpp - audio-encoders.hpp - balance-slider.hpp - basic-controls.cpp - basic-controls.hpp - clickable-label.hpp - context-bar-controls.cpp - context-bar-controls.hpp - focus-list.cpp - focus-list.hpp - horizontal-scroll-area.cpp - horizontal-scroll-area.hpp - hotkey-edit.cpp - hotkey-edit.hpp - item-widget-helpers.cpp - item-widget-helpers.hpp - log-viewer.cpp - log-viewer.hpp - media-controls.cpp - media-controls.hpp - menu-button.cpp - menu-button.hpp - mute-checkbox.hpp - noncheckable-button.hpp - preview-controls.cpp - preview-controls.hpp - remote-text.cpp - remote-text.hpp - scene-tree.cpp - scene-tree.hpp - screenshot-obj.hpp - source-label.cpp - source-label.hpp - source-tree.cpp - source-tree.hpp - undo-stack-obs.cpp - undo-stack-obs.hpp - url-push-button.cpp - url-push-button.hpp - visibility-item-widget.cpp - visibility-item-widget.hpp - volume-control.cpp - volume-control.hpp -) diff --git a/UI/cmake/ui-qt.cmake b/UI/cmake/ui-qt.cmake deleted file mode 100644 index 53d53671a..000000000 --- a/UI/cmake/ui-qt.cmake +++ /dev/null @@ -1,65 +0,0 @@ -find_package(Qt6 REQUIRED Widgets Network Svg Xml) - -if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) - find_package(Qt6 REQUIRED Gui DBus) -endif() - -if(NOT TARGET OBS::qt-wrappers) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers") -endif() - -target_link_libraries( - obs-studio - PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers -) - -set_target_properties( - obs-studio - PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON -) - -set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar) - -set( - _qt_sources - forms/AutoConfigFinishPage.ui - forms/AutoConfigStartPage.ui - forms/AutoConfigStartPage.ui - forms/AutoConfigStreamPage.ui - forms/AutoConfigTestPage.ui - forms/AutoConfigVideoPage.ui - forms/ColorSelect.ui - forms/obs.qrc - forms/OBSAbout.ui - forms/OBSAdvAudio.ui - forms/OBSBasic.ui - forms/OBSBasicControls.ui - forms/OBSBasicFilters.ui - forms/OBSBasicInteraction.ui - forms/OBSBasicProperties.ui - forms/OBSBasicSettings.ui - forms/OBSBasicSourceSelect.ui - forms/OBSBasicTransform.ui - forms/OBSBasicVCamConfig.ui - forms/OBSExtraBrowsers.ui - forms/OBSImporter.ui - forms/OBSLogReply.ui - forms/OBSLogViewer.ui - forms/OBSMissingFiles.ui - forms/OBSRemux.ui - forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui - forms/source-toolbar/browser-source-toolbar.ui - forms/source-toolbar/color-source-toolbar.ui - forms/source-toolbar/device-select-toolbar.ui - forms/source-toolbar/game-capture-toolbar.ui - forms/source-toolbar/image-source-toolbar.ui - forms/source-toolbar/media-controls.ui - forms/source-toolbar/text-source-toolbar.ui -) - -target_sources(obs-studio PRIVATE ${_qt_sources}) - -source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/forms" PREFIX "UI Files" FILES ${_qt_sources}) - -unset(_qt_sources) diff --git a/UI/cmake/ui-windows.cmake b/UI/cmake/ui-windows.cmake deleted file mode 100644 index 84f561edb..000000000 --- a/UI/cmake/ui-windows.cmake +++ /dev/null @@ -1,63 +0,0 @@ -target_sources( - obs-studio - PRIVATE - window-basic-about.cpp - window-basic-about.hpp - window-basic-adv-audio.cpp - window-basic-adv-audio.hpp - window-basic-auto-config-test.cpp - window-basic-auto-config.cpp - window-basic-auto-config.hpp - window-basic-filters.cpp - window-basic-filters.hpp - window-basic-interaction.cpp - window-basic-interaction.hpp - window-basic-main-browser.cpp - window-basic-main-dropfiles.cpp - window-basic-main-icons.cpp - window-basic-main-outputs.cpp - window-basic-main-outputs.hpp - window-basic-main-profiles.cpp - window-basic-main-scene-collections.cpp - window-basic-main-screenshot.cpp - window-basic-main-transitions.cpp - window-basic-main.cpp - window-basic-main.hpp - window-basic-preview.cpp - window-basic-preview.hpp - window-basic-properties.cpp - window-basic-properties.hpp - window-basic-settings-a11y.cpp - window-basic-settings-appearance.cpp - window-basic-settings-stream.cpp - window-basic-settings.cpp - window-basic-settings.hpp - window-basic-source-select.cpp - window-basic-source-select.hpp - window-basic-stats.cpp - window-basic-stats.hpp - window-basic-status-bar.cpp - window-basic-status-bar.hpp - window-basic-transform.cpp - window-basic-transform.hpp - window-basic-vcam-config.cpp - window-basic-vcam-config.hpp - window-basic-vcam.hpp - window-dock.cpp - window-dock.hpp - window-importer.cpp - window-importer.hpp - window-log-reply.cpp - window-log-reply.hpp - window-main.hpp - window-missing-files.cpp - window-missing-files.hpp - window-namedialog.cpp - window-namedialog.hpp - window-projector.cpp - window-projector.hpp - window-remux.cpp - window-remux.hpp - window-whats-new.cpp - window-whats-new.hpp -) diff --git a/UI/context-bar-controls.cpp b/UI/context-bar-controls.cpp deleted file mode 100644 index c45d06976..000000000 --- a/UI/context-bar-controls.cpp +++ /dev/null @@ -1,707 +0,0 @@ -#include "window-basic-main.hpp" -#include "moc_context-bar-controls.cpp" -#include "obs-app.hpp" - -#include -#include -#include -#include - -#include "ui_browser-source-toolbar.h" -#include "ui_device-select-toolbar.h" -#include "ui_game-capture-toolbar.h" -#include "ui_image-source-toolbar.h" -#include "ui_color-source-toolbar.h" -#include "ui_text-source-toolbar.h" - -#ifdef _WIN32 -#define get_os_module(win, mac, linux) obs_get_module(win) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) -#elif __APPLE__ -#define get_os_module(win, mac, linux) obs_get_module(mac) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) -#else -#define get_os_module(win, mac, linux) obs_get_module(linux) -#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) -#endif - -/* ========================================================================= */ - -SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - props(obs_source_properties(source), obs_properties_destroy) -{ -} - -void SourceToolbar::SaveOldProperties(obs_source_t *source) -{ - oldData = obs_data_create(); - - OBSDataAutoRelease oldSettings = obs_source_get_settings(source); - obs_data_apply(oldData, oldSettings); - obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); -} - -void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) -{ - if (!oldData) { - blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - OBSSource currentSceneSource = main->GetCurrentSceneSource(); - if (!currentSceneSource) - return; - std::string scene_uuid = obs_source_get_uuid(currentSceneSource); - auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { - OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); - obs_source_reset_settings(source, settings); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - main->UpdateContextBar(); - }; - - OBSDataAutoRelease new_settings = obs_data_create(); - OBSDataAutoRelease curr_settings = obs_source_get_settings(source); - obs_data_apply(new_settings, curr_settings); - obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); - - std::string undo_data(obs_data_get_json(oldData)); - std::string redo_data(obs_data_get_json(new_settings)); - - if (undo_data.compare(redo_data) != 0) - main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, - undo_data, redo_data, repeatable); - - oldData = nullptr; -} - -/* ========================================================================= */ - -BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_BrowserSourceToolbar) -{ - ui->setupUi(this); -} - -BrowserToolbar::~BrowserToolbar() {} - -void BrowserToolbar::on_refresh_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); - obs_property_button_clicked(p, source.Get()); -} - -/* ========================================================================= */ - -ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); -} - -ComboSelectToolbar::~ComboSelectToolbar() {} - -static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) -{ - size_t count = obs_property_list_item_count(p); - int cur_idx = -1; - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - std::string id; - - if (is_int) { - id = std::to_string(obs_property_list_item_int(p, i)); - } else { - const char *val = obs_property_list_item_string(p, i); - id = val ? val : ""; - } - - if (cur_id == id) - cur_idx = (int)i; - - c->addItem(name, id.c_str()); - } - - return cur_idx; -} - -void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, - const char *prop_name, bool is_int) -{ - std::string cur_id; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (is_int) { - cur_id = std::to_string(obs_data_get_int(settings, prop_name)); - } else { - cur_id = obs_data_get_string(settings, prop_name); - } - - combo->blockSignals(true); - - obs_property_t *p = obs_properties_get(props, prop_name); - int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); - - if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { - if (cur_idx == -1) { - combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); - cur_idx = 0; - } - - SetComboItemEnabled(combo, cur_idx, false); - } - - combo->setCurrentIndex(cur_idx); - combo->blockSignals(false); -} - -void ComboSelectToolbar::Init() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); -} - -void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) -{ - QString id = combo->itemData(idx).toString(); - - OBSDataAutoRelease settings = obs_data_create(); - if (is_int) { - obs_data_set_int(settings, prop_name, id.toInt()); - } else { - obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); - } - obs_source_update(source, settings); -} - -void ComboSelectToolbar::on_device_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - SaveOldProperties(source); - UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); - SetUndoProperties(source); -} - -AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void AudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); - ui->deviceLabel->setText(device_str); - - prop_name = "device_id"; - - ComboSelectToolbar::Init(); -} - -WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void WindowCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); - ui->deviceLabel->setText(device_str); - -#if !defined(_WIN32) && !defined(__APPLE__) //linux - prop_name = "capture_window"; -#else - prop_name = "window"; -#endif - -#ifdef __APPLE__ - is_int = true; -#endif - - ComboSelectToolbar::Init(); -} - -ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) - : ComboSelectToolbar(parent, source) -{ -} - -void ApplicationAudioCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = obs_get_module("win-wasapi"); - const char *device_str = obs_module_get_locale_text(mod, "Window"); - ui->deviceLabel->setText(device_str); - - prop_name = "window"; - - ComboSelectToolbar::Init(); -} - -DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} - -void DisplayCaptureToolbar::Init() -{ - delete ui->activateButton; - ui->activateButton = nullptr; - - obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); - if (!mod) - return; - - const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); - ui->deviceLabel->setText(device_str); - -#ifdef _WIN32 - prop_name = "monitor_id"; -#elif __APPLE__ - prop_name = "display_uuid"; -#else - is_int = true; - prop_name = "screen"; -#endif - - ComboSelectToolbar::Init(); -} - -/* ========================================================================= */ - -DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) - : QWidget(parent), - weakSource(OBSGetWeakRef(source)), - ui(new Ui_DeviceSelectToolbar) -{ - ui->setupUi(this); - - delete ui->deviceLabel; - delete ui->device; - ui->deviceLabel = nullptr; - ui->device = nullptr; - - OBSDataAutoRelease settings = obs_source_get_settings(source); - active = obs_data_get_bool(settings, "active"); - - obs_module_t *mod = obs_get_module("win-dshow"); - if (!mod) - return; - - activateText = obs_module_get_locale_text(mod, "Activate"); - deactivateText = obs_module_get_locale_text(mod, "Deactivate"); - - ui->activateButton->setText(active ? deactivateText : activateText); -} - -DeviceCaptureToolbar::~DeviceCaptureToolbar() {} - -void DeviceCaptureToolbar::on_activateButton_clicked() -{ - OBSSource source = OBSGetStrongRef(weakSource); - if (!source) { - return; - } - - OBSDataAutoRelease settings = obs_source_get_settings(source); - bool now_active = obs_data_get_bool(settings, "active"); - - bool desyncedSetting = now_active != active; - - active = !active; - - const char *text = active ? deactivateText : activateText; - ui->activateButton->setText(text); - - if (desyncedSetting) { - return; - } - - calldata_t cd = {}; - calldata_set_bool(&cd, "active", active); - proc_handler_t *ph = obs_source_get_proc_handler(source); - proc_handler_call(ph, "activate", &cd); - calldata_free(&cd); -} - -/* ========================================================================= */ - -GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_GameCaptureToolbar) -{ - obs_property_t *p; - int cur_idx; - - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("win-capture"); - if (!mod) - return; - - ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); - ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string cur_mode = obs_data_get_string(settings, "capture_mode"); - std::string cur_window = obs_data_get_string(settings, "window"); - - ui->mode->blockSignals(true); - p = obs_properties_get(props.get(), "capture_mode"); - cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); - ui->mode->setCurrentIndex(cur_idx); - ui->mode->blockSignals(false); - - ui->window->blockSignals(true); - p = obs_properties_get(props.get(), "window"); - cur_idx = FillPropertyCombo(ui->window, p, cur_window); - ui->window->setCurrentIndex(cur_idx); - ui->window->blockSignals(false); - - if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { - SetComboItemEnabled(ui->window, cur_idx, false); - } - - UpdateWindowVisibility(); -} - -GameCaptureToolbar::~GameCaptureToolbar() {} - -void GameCaptureToolbar::UpdateWindowVisibility() -{ - QString mode = ui->mode->currentData().toString(); - bool is_window = (mode == "window"); - ui->windowLabel->setVisible(is_window); - ui->window->setVisible(is_window); -} - -void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->mode->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); - - UpdateWindowVisibility(); -} - -void GameCaptureToolbar::on_window_currentIndexChanged(int idx) -{ - OBSSource source = GetSource(); - if (idx == -1 || !source) { - return; - } - - QString id = ui->window->itemData(idx).toString(); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "window", QT_TO_UTF8(id)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ImageSourceToolbar) -{ - ui->setupUi(this); - - obs_module_t *mod = obs_get_module("image-source"); - ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - std::string file = obs_data_get_string(settings, "file"); - - ui->path->setText(file.c_str()); -} - -ImageSourceToolbar::~ImageSourceToolbar() {} - -void ImageSourceToolbar::on_browse_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "file"); - const char *desc = obs_property_description(p); - const char *filter = obs_property_path_filter(p); - const char *default_path = obs_property_path_default_path(p); - - QString startDir = ui->path->text(); - if (startDir.isEmpty()) - startDir = default_path; - - QString path = OpenFile(this, desc, startDir, filter); - if (path.isEmpty()) { - return; - } - - ui->path->setText(path); - - SaveOldProperties(source); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "file", QT_TO_UTF8(path)); - obs_source_update(source, settings); - SetUndoProperties(source); -} - -/* ========================================================================= */ - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -static inline long long color_to_int(QColor color) -{ - auto shift = [&](unsigned val, int shift) { - return ((val & 0xff) << shift); - }; - - return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); -} - -ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_ColorSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); - - color = color_from_int(val); - UpdateColor(); -} - -ColorSourceToolbar::~ColorSourceToolbar() {} - -void ColorSourceToolbar::UpdateColor() -{ - QPalette palette = QPalette(color); - ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); - ui->color->setText(color.name(QColor::HexRgb)); - ui->color->setPalette(palette); - ui->color->setStyleSheet(QString("background-color :%1; color: %2;") - .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) - .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); - ui->color->setAutoFillBackground(true); - ui->color->setAlignment(Qt::AlignCenter); -} - -void ColorSourceToolbar::on_choose_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - obs_property_t *p = obs_properties_get(props.get(), "color"); - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - UpdateColor(); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "color", color_to_int(color)); - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -/* ========================================================================= */ - -extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); - -TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) - : SourceToolbar(parent, source), - ui(new Ui_TextSourceToolbar) -{ - ui->setupUi(this); - - OBSDataAutoRelease settings = obs_source_get_settings(source); - - const char *id = obs_source_get_unversioned_id(source); - bool ft2 = strcmp(id, "text_ft2_source") == 0; - bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); - - OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); - MakeQFont(font_obj, font); - - // Use "color1" if it's a freetype source and "color" elsewise - unsigned int val = (unsigned int)obs_data_get_int( - settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); - - color = color_from_int(val); - - const char *text = obs_data_get_string(settings, "text"); - - bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); - ui->emptySpace->setVisible(!single_line); - ui->text->setVisible(single_line); - if (single_line) - ui->text->setText(text); -} - -TextSourceToolbar::~TextSourceToolbar() {} - -void TextSourceToolbar::on_selectFont_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - QFontDialog::FontDialogOptions options; - uint32_t flags; - bool success; - -#ifndef _WIN32 - options = QFontDialog::DontUseNativeDialog; -#endif - - font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), - options); - if (!success) { - return; - } - - OBSDataAutoRelease font_obj = obs_data_create(); - - obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); - obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); - obs_data_set_int(font_obj, "size", font.pointSize()); - flags = font.bold() ? OBS_FONT_BOLD : 0; - flags |= font.italic() ? OBS_FONT_ITALIC : 0; - flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; - flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; - obs_data_set_int(font_obj, "flags", flags); - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_obj(settings, "font", font_obj); - - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_selectColor_clicked() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - - bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; - - obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); - - const char *desc = obs_property_description(p); - - QColorDialog::ColorDialogOptions options; - - options |= QColorDialog::ShowAlphaChannel; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColor newColor = QColorDialog::getColor(color, this, desc, options); - if (!newColor.isValid()) { - return; - } - - color = newColor; - - SaveOldProperties(source); - - OBSDataAutoRelease settings = obs_data_create(); - if (freetype) { - obs_data_set_int(settings, "color1", color_to_int(color)); - obs_data_set_int(settings, "color2", color_to_int(color)); - } else { - obs_data_set_int(settings, "color", color_to_int(color)); - } - obs_source_update(source, settings); - - SetUndoProperties(source); -} - -void TextSourceToolbar::on_text_textChanged() -{ - OBSSource source = GetSource(); - if (!source) { - return; - } - std::string newText = QT_TO_UTF8(ui->text->text()); - OBSDataAutoRelease settings = obs_source_get_settings(source); - if (newText == obs_data_get_string(settings, "text")) { - return; - } - SaveOldProperties(source); - - obs_data_set_string(settings, "text", newText.c_str()); - obs_source_update(source, nullptr); - - SetUndoProperties(source, true); -} diff --git a/UI/context-bar-controls.hpp b/UI/context-bar-controls.hpp deleted file mode 100644 index acf88a5fb..000000000 --- a/UI/context-bar-controls.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#pragma once - -#include -#include -#include - -class Ui_BrowserSourceToolbar; -class Ui_DeviceSelectToolbar; -class Ui_GameCaptureToolbar; -class Ui_ImageSourceToolbar; -class Ui_ColorSourceToolbar; -class Ui_TextSourceToolbar; - -class SourceToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - -protected: - using properties_delete_t = decltype(&obs_properties_destroy); - using properties_t = std::unique_ptr; - - properties_t props; - OBSDataAutoRelease oldData; - - void SaveOldProperties(obs_source_t *source); - void SetUndoProperties(obs_source_t *source, bool repeatable = false); - -public: - SourceToolbar(QWidget *parent, OBSSource source); - - OBSSource GetSource() { return OBSGetStrongRef(weakSource); } - -public slots: - virtual void Update() {} -}; - -class BrowserToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - BrowserToolbar(QWidget *parent, OBSSource source); - ~BrowserToolbar(); - -public slots: - void on_refresh_clicked(); -}; - -class ComboSelectToolbar : public SourceToolbar { - Q_OBJECT - -protected: - std::unique_ptr ui; - const char *prop_name; - bool is_int = false; - -public: - ComboSelectToolbar(QWidget *parent, OBSSource source); - ~ComboSelectToolbar(); - virtual void Init(); - -public slots: - void on_device_currentIndexChanged(int idx); -}; - -class AudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - AudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class WindowCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - WindowCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DisplayCaptureToolbar : public ComboSelectToolbar { - Q_OBJECT - -public: - DisplayCaptureToolbar(QWidget *parent, OBSSource source); - void Init() override; -}; - -class DeviceCaptureToolbar : public QWidget { - Q_OBJECT - - OBSWeakSource weakSource; - - std::unique_ptr ui; - const char *activateText; - const char *deactivateText; - bool active; - -public: - DeviceCaptureToolbar(QWidget *parent, OBSSource source); - ~DeviceCaptureToolbar(); - -public slots: - void on_activateButton_clicked(); -}; - -class GameCaptureToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - - void UpdateWindowVisibility(); - -public: - GameCaptureToolbar(QWidget *parent, OBSSource source); - ~GameCaptureToolbar(); - -public slots: - void on_mode_currentIndexChanged(int idx); - void on_window_currentIndexChanged(int idx); -}; - -class ImageSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - -public: - ImageSourceToolbar(QWidget *parent, OBSSource source); - ~ImageSourceToolbar(); - -public slots: - void on_browse_clicked(); -}; - -class ColorSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QColor color; - - void UpdateColor(); - -public: - ColorSourceToolbar(QWidget *parent, OBSSource source); - ~ColorSourceToolbar(); - -public slots: - void on_choose_clicked(); -}; - -class TextSourceToolbar : public SourceToolbar { - Q_OBJECT - - std::unique_ptr ui; - QFont font; - QColor color; - -public: - TextSourceToolbar(QWidget *parent, OBSSource source); - ~TextSourceToolbar(); - -public slots: - void on_selectFont_clicked(); - void on_selectColor_clicked(); - void on_text_textChanged(); -}; diff --git a/UI/obs.manifest b/UI/obs.manifest deleted file mode 100644 index b20d07746..000000000 --- a/UI/obs.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - OBS Studio - - - - - - - - - - - - - - diff --git a/UI/obs.rc.in b/UI/obs.rc.in deleted file mode 100644 index f1f06b697..000000000 --- a/UI/obs.rc.in +++ /dev/null @@ -1,26 +0,0 @@ -IDI_ICON1 ICON DISCARDABLE "../cmake/bundle/windows/obs-studio.ico" - -1 VERSIONINFO -FILEVERSION ${UI_VERSION_MAJOR},${UI_VERSION_MINOR},${UI_VERSION_PATCH},0 -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904B0" - BEGIN - VALUE "CompanyName", "OBS" - VALUE "FileDescription", "OBS Studio" - VALUE "FileVersion", "${UI_VERSION}" - VALUE "InternalName", "obs" - VALUE "OriginalFilename", "obs" - VALUE "ProductName", "OBS Studio" - VALUE "ProductVersion", "${UI_VERSION}" - VALUE "Comments", "Free and open source software for video recording and live streaming" - VALUE "LegalCopyright", "(C) Lain Bailey" - END - END - - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0409, 0x04B0 - END -END diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp deleted file mode 100644 index a0242e3cd..000000000 --- a/UI/source-tree.cpp +++ /dev/null @@ -1,1613 +0,0 @@ -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "source-tree.hpp" -#include "platform.hpp" -#include "source-label.hpp" - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -static inline OBSScene GetCurrentScene() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - return main->GetCurrentScene(); -} - -/* ========================================================================= */ - -SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) -{ - setAttribute(Qt::WA_TranslucentBackground); - setMouseTracking(true); - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - const char *name = obs_source_get_name(source); - - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); - int preset = obs_data_get_int(privData, "color-preset"); - - if (preset == 1) { - const char *color = obs_data_get_string(privData, "color"); - std::string col = "background: "; - col += color; - setStyleSheet(col.c_str()); - } else if (preset > 1) { - setStyleSheet(""); - setProperty("bgColor", preset - 1); - } else { - setStyleSheet("background: none"); - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *id = obs_source_get_id(source); - - bool sourceVisible = obs_sceneitem_visible(sceneitem); - - if (tree->iconsVisible) { - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = main->GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = main->GetGroupIcon(); - else - icon = main->GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - - iconLabel = new QLabel(); - iconLabel->setPixmap(pixmap); - iconLabel->setEnabled(sourceVisible); - iconLabel->setStyleSheet("background: none"); - iconLabel->setProperty("class", "source-icon"); - } - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setChecked(sourceVisible); - vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); - vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); - - lock = new QCheckBox(); - lock->setProperty("class", "checkbox-icon indicator-lock"); - lock->setChecked(obs_sceneitem_locked(sceneitem)); - lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); - lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - label->setAttribute(Qt::WA_TranslucentBackground); - label->setEnabled(sourceVisible); - -#ifdef __APPLE__ - vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); - lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - boxLayout = new QHBoxLayout(); - - boxLayout->setContentsMargins(0, 0, 0, 0); - boxLayout->setSpacing(0); - if (iconLabel) { - boxLayout->addWidget(iconLabel); - boxLayout->addSpacing(2); - } - boxLayout->addWidget(label); - boxLayout->addWidget(vis); - boxLayout->addWidget(lock); -#ifdef __APPLE__ - /* Hack: Fixes a bug where scrollbars would be above the lock icon */ - boxLayout->addSpacing(16); -#endif - - Update(false); - - setLayout(boxLayout); - - /* --------------------------------------------------------- */ - - auto setItemVisible = [this](bool val) { - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *scenesource = obs_scene_get_source(scene); - int64_t id = obs_sceneitem_get_id(sceneitem); - const char *name = obs_source_get_name(scenesource); - const char *uuid = obs_source_get_uuid(scenesource); - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - - auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { - OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); - obs_scene_t *sc = obs_group_or_scene_from_source(s); - obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); - if (si) - obs_sceneitem_set_visible(si, val); - }; - - QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); - - OBSBasic *main = OBSBasic::Get(); - main->undo_s.add_action(str.arg(obs_source_get_name(source), name), - std::bind(undo_redo, std::placeholders::_1, id, !val), - std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); - - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_visible(sceneitem, val); - }; - - auto setItemLocked = [this](bool checked) { - QSignalBlocker sourcesSignalBlocker(this); - obs_sceneitem_set_locked(sceneitem, checked); - }; - - connect(vis, &QAbstractButton::clicked, setItemVisible); - connect(lock, &QAbstractButton::clicked, setItemLocked); -} - -void SourceTreeItem::paintEvent(QPaintEvent *event) -{ - QStyleOption opt; - opt.initFrom(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - QWidget::paintEvent(event); -} - -void SourceTreeItem::DisconnectSignals() -{ - sigs.clear(); -} - -void SourceTreeItem::Clear() -{ - DisconnectSignals(); - sceneitem = nullptr; -} - -void SourceTreeItem::ReconnectSignals() -{ - if (!sceneitem) - return; - - DisconnectSignals(); - - /* --------------------------------------------------------- */ - - auto removeItem = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); - - if (curItem == this_->sceneitem) { - QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), - Q_ARG(OBSScene, curScene)); - curItem = nullptr; - } - if (!curItem) - QMetaObject::invokeMethod(this_, "Clear"); - }; - - auto itemVisible = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool visible = calldata_bool(cd, "visible"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); - }; - - auto itemLocked = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - bool locked = calldata_bool(cd, "locked"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); - }; - - auto itemSelect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Select"); - }; - - auto itemDeselect = [](void *data, calldata_t *cd) { - SourceTreeItem *this_ = reinterpret_cast(data); - obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); - - if (curItem == this_->sceneitem) - QMetaObject::invokeMethod(this_, "Deselect"); - }; - - auto reorderGroup = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - QMetaObject::invokeMethod(this_->tree, "ReorderItems"); - }; - - obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); - obs_source_t *sceneSource = obs_scene_get_source(scene); - signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); - - sigs.emplace_back(signal, "remove", removeItem, this); - sigs.emplace_back(signal, "item_remove", removeItem, this); - sigs.emplace_back(signal, "item_visible", itemVisible, this); - sigs.emplace_back(signal, "item_locked", itemLocked, this); - sigs.emplace_back(signal, "item_select", itemSelect, this); - sigs.emplace_back(signal, "item_deselect", itemDeselect, this); - - if (obs_sceneitem_is_group(sceneitem)) { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - - sigs.emplace_back(signal, "reorder", reorderGroup, this); - } - - /* --------------------------------------------------------- */ - - auto removeSource = [](void *data, calldata_t *) { - SourceTreeItem *this_ = reinterpret_cast(data); - this_->DisconnectSignals(); - this_->sceneitem = nullptr; - QMetaObject::invokeMethod(this_->tree, "RefreshItems"); - }; - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - signal = obs_source_get_signal_handler(source); - sigs.emplace_back(signal, "remove", removeSource, this); -} - -void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); - - if (expand) { - expand->setChecked(!expand->isChecked()); - } else { - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - if (obs_source_configurable(source)) { - main->CreatePropertiesWindow(source); - } - } -} - -void SourceTreeItem::enterEvent(QEnterEvent *event) -{ - QWidget::enterEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); - preview->hoveredPreviewItems.push_back(sceneitem); -} - -void SourceTreeItem::leaveEvent(QEvent *event) -{ - QWidget::leaveEvent(event); - - OBSBasicPreview *preview = OBSBasicPreview::Get(); - - std::lock_guard lock(preview->selectMutex); - preview->hoveredPreviewItems.clear(); -} - -bool SourceTreeItem::IsEditing() -{ - return editor != nullptr; -} - -void SourceTreeItem::EnterEditMode() -{ - setFocusPolicy(Qt::StrongFocus); - int index = boxLayout->indexOf(label); - boxLayout->removeWidget(label); - editor = new QLineEdit(label->text()); - editor->setStyleSheet("background: none"); - editor->selectAll(); - editor->installEventFilter(this); - boxLayout->insertWidget(index, editor); - setFocusProxy(editor); -} - -void SourceTreeItem::ExitEditMode(bool save) -{ - ExitEditModeInternal(save); - - if (tree->undoSceneData) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); - main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); - - tree->undoSceneData = nullptr; - } -} - -void SourceTreeItem::ExitEditModeInternal(bool save) -{ - if (!editor) { - return; - } - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSScene scene = main->GetCurrentScene(); - - newName = QT_TO_UTF8(editor->text()); - - setFocusProxy(nullptr); - int index = boxLayout->indexOf(editor); - boxLayout->removeWidget(editor); - delete editor; - editor = nullptr; - setFocusPolicy(Qt::NoFocus); - boxLayout->insertWidget(index, label); - setFocus(); - - /* ----------------------------------------- */ - /* check for empty string */ - - if (!save) - return; - - if (newName.empty()) { - OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; - } - - /* ----------------------------------------- */ - /* Check for same name */ - - obs_source_t *source = obs_sceneitem_get_source(sceneitem); - if (newName == obs_source_get_name(source)) - return; - - /* ----------------------------------------- */ - /* check for existing source */ - - OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); - bool exists = !!existingSource; - - if (exists) { - OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - return; - } - - /* ----------------------------------------- */ - /* rename */ - - QSignalBlocker sourcesSignalBlocker(this); - std::string prevName(obs_source_get_name(source)); - std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); - auto undo = [scene_uuid, prevName, main](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prevName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - std::string editedName = newName; - - auto redo = [scene_uuid, main, editedName](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, editedName.c_str()); - - OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - - const char *uuid = obs_source_get_uuid(source); - main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); - - obs_source_set_name(source, newName.c_str()); -} - -bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) -{ - if (editor != object) - return false; - - if (LineEditCanceled(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); - return true; - } - if (LineEditChanged(event)) { - QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); - return true; - } - - return false; -} - -void SourceTreeItem::VisibilityChanged(bool visible) -{ - if (iconLabel) { - iconLabel->setEnabled(visible); - } - label->setEnabled(visible); - vis->setChecked(visible); -} - -void SourceTreeItem::LockedChanged(bool locked) -{ - lock->setChecked(locked); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Update(bool force) -{ - OBSScene scene = GetCurrentScene(); - obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); - - Type newType; - - /* ------------------------------------------------- */ - /* if it's a group item, insert group checkbox */ - - if (obs_sceneitem_is_group(sceneitem)) { - newType = Type::Group; - - /* ------------------------------------------------- */ - /* if it's a group sub-item */ - - } else if (itemScene != scene) { - newType = Type::SubItem; - - /* ------------------------------------------------- */ - /* if it's a regular item */ - - } else { - newType = Type::Item; - } - - /* ------------------------------------------------- */ - - if (!force && newType == type) { - return; - } - - /* ------------------------------------------------- */ - - ReconnectSignals(); - - if (spacer) { - boxLayout->removeItem(spacer); - delete spacer; - spacer = nullptr; - } - - if (type == Type::Group) { - boxLayout->removeWidget(expand); - expand->deleteLater(); - expand = nullptr; - } - - type = newType; - - if (type == Type::SubItem) { - spacer = new QSpacerItem(16, 1); - boxLayout->insertItem(0, spacer); - - } else if (type == Type::Group) { - expand = new QCheckBox(); - expand->setProperty("class", "checkbox-icon indicator-expand"); -#ifdef __APPLE__ - expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - boxLayout->insertWidget(0, expand); - - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - expand->blockSignals(true); - expand->setChecked(obs_data_get_bool(data, "collapsed")); - expand->blockSignals(false); - - connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); - - } else { - spacer = new QSpacerItem(3, 1); - boxLayout->insertItem(0, spacer); - } -} - -void SourceTreeItem::ExpandClicked(bool checked) -{ - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); - - obs_data_set_bool(data, "collapsed", checked); - - if (!checked) - tree->GetStm()->ExpandGroup(sceneitem); - else - tree->GetStm()->CollapseGroup(sceneitem); -} - -void SourceTreeItem::Select() -{ - tree->SelectItem(sceneitem, true); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -void SourceTreeItem::Deselect() -{ - tree->SelectItem(sceneitem, false); - OBSBasic::Get()->UpdateContextBarDeferred(); - OBSBasic::Get()->UpdateEditMenu(); -} - -/* ========================================================================= */ - -void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) -{ - SourceTreeModel *stm = reinterpret_cast(ptr); - - switch (event) { - case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: - stm->SceneChanged(); - break; - case OBS_FRONTEND_EVENT_EXIT: - stm->Clear(); - obs_frontend_remove_event_callback(OBSFrontendEvent, stm); - break; - case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: - stm->Clear(); - break; - default: - break; - } -} - -void SourceTreeModel::Clear() -{ - beginResetModel(); - items.clear(); - endResetModel(); - - hasGroups = false; -} - -static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) -{ - QVector &items = *reinterpret_cast *>(ptr); - - obs_source_t *src = obs_sceneitem_get_source(item); - if (obs_source_removed(src)) { - return true; - } - - if (obs_sceneitem_is_group(item)) { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); - - bool collapse = obs_data_get_bool(data, "collapsed"); - if (!collapse) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - obs_scene_enum_items(scene, enumItem, &items); - } - } - - items.insert(0, item); - return true; -} - -void SourceTreeModel::SceneChanged() -{ - OBSScene scene = GetCurrentScene(); - - beginResetModel(); - items.clear(); - obs_scene_enum_items(scene, enumItem, &items); - endResetModel(); - - UpdateGroupState(false); - st->ResetWidgets(); - - for (int i = 0; i < items.count(); i++) { - bool select = obs_sceneitem_selected(items[i]); - QModelIndex index = createIndex(i, 0); - - st->selectionModel()->select(index, - select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); - } -} - -/* moves a scene item index (blame linux distros for using older Qt builds) */ -static inline void MoveItem(QVector &items, int oldIdx, int newIdx) -{ - OBSSceneItem item = items[oldIdx]; - items.remove(oldIdx); - items.insert(newIdx, item); -} - -/* reorders list optimally with model reorder funcs */ -void SourceTreeModel::ReorderItems() -{ - OBSScene scene = GetCurrentScene(); - - QVector newitems; - obs_scene_enum_items(scene, enumItem, &newitems); - - /* if item list has changed size, do full reset */ - if (newitems.count() != items.count()) { - SceneChanged(); - return; - } - - for (;;) { - int idx1Old = 0; - int idx1New = 0; - int count; - int i; - - /* find first starting changed item index */ - for (i = 0; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[i]; - obs_sceneitem_t *newItem = newitems[i]; - if (oldItem != newItem) { - idx1Old = i; - break; - } - } - - /* if everything is the same, break */ - if (i == newitems.count()) { - break; - } - - /* find new starting index */ - for (i = idx1Old + 1; i < newitems.count(); i++) { - obs_sceneitem_t *oldItem = items[idx1Old]; - obs_sceneitem_t *newItem = newitems[i]; - - if (oldItem == newItem) { - idx1New = i; - break; - } - } - - /* if item could not be found, do full reset */ - if (i == newitems.count()) { - SceneChanged(); - return; - } - - /* get move count */ - for (count = 1; (idx1New + count) < newitems.count(); count++) { - int oldIdx = idx1Old + count; - int newIdx = idx1New + count; - - obs_sceneitem_t *oldItem = items[oldIdx]; - obs_sceneitem_t *newItem = newitems[newIdx]; - - if (oldItem != newItem) { - break; - } - } - - /* move items */ - beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); - for (i = 0; i < count; i++) { - int to = idx1New + count; - if (to > idx1Old) - to--; - MoveItem(items, idx1Old, to); - } - endMoveRows(); - } -} - -void SourceTreeModel::Add(obs_sceneitem_t *item) -{ - if (obs_sceneitem_is_group(item)) { - SceneChanged(); - } else { - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, item); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), item); - } -} - -void SourceTreeModel::Remove(obs_sceneitem_t *item) -{ - int idx = -1; - for (int i = 0; i < items.count(); i++) { - if (items[i] == item) { - idx = i; - break; - } - } - - if (idx == -1) - return; - - int startIdx = idx; - int endIdx = idx; - - bool is_group = obs_sceneitem_is_group(item); - if (is_group) { - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = endIdx + 1; i < items.count(); i++) { - obs_sceneitem_t *subitem = items[i]; - obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); - - if (subscene == scene) - endIdx = i; - else - break; - } - } - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(idx, endIdx - startIdx + 1); - endRemoveRows(); - - if (is_group) - UpdateGroupState(true); - - OBSBasic::Get()->UpdateContextBarDeferred(); -} - -OBSSceneItem SourceTreeModel::Get(int idx) -{ - if (idx == -1 || idx >= items.count()) - return OBSSceneItem(); - return items[idx]; -} - -SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) -{ - obs_frontend_add_event_callback(OBSFrontendEvent, this); -} - -int SourceTreeModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : items.count(); -} - -QVariant SourceTreeModel::data(const QModelIndex &index, int role) const -{ - if (role == Qt::AccessibleTextRole) { - OBSSceneItem item = items[index.row()]; - obs_source_t *source = obs_sceneitem_get_source(item); - return QVariant(QT_UTF8(obs_source_get_name(source))); - } - - return QVariant(); -} - -Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; - - obs_sceneitem_t *item = items[index.row()]; - bool is_group = obs_sceneitem_is_group(item); - - return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | - (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); -} - -Qt::DropActions SourceTreeModel::supportedDropActions() const -{ - return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; -} - -QString SourceTreeModel::GetNewGroupName() -{ - OBSScene scene = GetCurrentScene(); - QString name = QTStr("Group"); - - int i = 2; - for (;;) { - OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); - if (!group) - break; - name = QTStr("Basic.Main.Group").arg(QString::number(i++)); - } - - return name; -} - -void SourceTreeModel::AddGroup() -{ - QString name = GetNewGroupName(); - obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); - if (!group) - return; - - beginInsertRows(QModelIndex(), 0, 0); - items.insert(0, group); - endInsertRows(); - - st->UpdateWidget(createIndex(0, 0, nullptr), group); - UpdateGroupState(true); - - QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); -} - -void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) -{ - if (indices.count() == 0) - return; - - OBSBasic *main = OBSBasic::Get(); - OBSScene scene = GetCurrentScene(); - QString name = GetNewGroupName(); - - QVector item_order; - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - item_order << item; - } - - st->undoSceneData = main->BackupScene(scene); - - obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); - if (!item) { - st->undoSceneData = nullptr; - return; - } - - main->undo_s.push_disabled(); - - for (obs_sceneitem_t *item : item_order) - obs_sceneitem_select(item, false); - - hasGroups = true; - st->UpdateWidgets(true); - - obs_sceneitem_select(item, true); - - /* ----------------------------------------------------------------- */ - /* obs_scene_insert_group triggers a full refresh of scene items via */ - /* the item_add signal. No need to insert a row, just edit the one */ - /* that's created automatically. */ - - int newIdx = indices[0].row(); - QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); -} - -void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) -{ - OBSBasic *main = OBSBasic::Get(); - if (indices.count() == 0) - return; - - OBSScene scene = main->GetCurrentScene(); - OBSData undoData = main->BackupScene(scene); - - for (int i = indices.count() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_sceneitem_group_ungroup(item); - } - - SceneChanged(); - - OBSData redoData = main->BackupScene(scene); - main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); -} - -void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) -{ - int itemIdx = items.indexOf(item); - if (itemIdx == -1) - return; - - itemIdx++; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - QVector subItems; - obs_scene_enum_items(scene, enumItem, &subItems); - - if (!subItems.size()) - return; - - beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); - for (int i = 0; i < subItems.size(); i++) - items.insert(i + itemIdx, subItems[i]); - endInsertRows(); - - st->UpdateWidgets(); -} - -void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) -{ - int startIdx = -1; - int endIdx = -1; - - obs_scene_t *scene = obs_sceneitem_group_get_scene(item); - - for (int i = 0; i < items.size(); i++) { - obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); - - if (itemScene == scene) { - if (startIdx == -1) - startIdx = i; - endIdx = i; - } - } - - if (startIdx == -1) - return; - - beginRemoveRows(QModelIndex(), startIdx, endIdx); - items.remove(startIdx, endIdx - startIdx + 1); - endRemoveRows(); -} - -void SourceTreeModel::UpdateGroupState(bool update) -{ - bool nowHasGroups = false; - for (auto &item : items) { - if (obs_sceneitem_is_group(item)) { - nowHasGroups = true; - break; - } - } - - if (nowHasGroups != hasGroups) { - hasGroups = nowHasGroups; - if (update) { - st->UpdateWidgets(true); - } - } -} - -/* ========================================================================= */ - -SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) -{ - SourceTreeModel *stm_ = new SourceTreeModel(this); - setModel(stm_); - setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - UpdateNoSourcesMessage(); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); - connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); - - setItemDelegate(new SourceTreeDelegate(this)); -} - -void SourceTree::UpdateIcons() -{ - SourceTreeModel *stm = GetStm(); - stm->SceneChanged(); -} - -void SourceTree::SetIconsVisible(bool visible) -{ - SourceTreeModel *stm = GetStm(); - - iconsVisible = visible; - stm->SceneChanged(); -} - -void SourceTree::ResetWidgets() -{ - OBSScene scene = GetCurrentScene(); - - SourceTreeModel *stm = GetStm(); - stm->UpdateGroupState(false); - - for (int i = 0; i < stm->items.count(); i++) { - QModelIndex index = stm->createIndex(i, 0, nullptr); - setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); - } -} - -void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) -{ - setIndexWidget(idx, new SourceTreeItem(this, item)); -} - -void SourceTree::UpdateWidgets(bool force) -{ - SourceTreeModel *stm = GetStm(); - - for (int i = 0; i < stm->items.size(); i++) { - obs_sceneitem_t *item = stm->items[i]; - SourceTreeItem *widget = GetItemWidget(i); - - if (!widget) { - UpdateWidget(stm->createIndex(i, 0), item); - } else { - widget->Update(force); - } - } -} - -void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) -{ - SourceTreeModel *stm = GetStm(); - int i = 0; - - for (; i < stm->items.count(); i++) { - if (stm->items[i] == sceneitem) - break; - } - - if (i == stm->items.count()) - return; - - QModelIndex index = stm->createIndex(i, 0); - if (index.isValid() && select != selectionModel()->isSelected(index)) - selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); -} - -Q_DECLARE_METATYPE(OBSSceneItem); - -void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - QListView::mouseDoubleClickEvent(event); -} - -void SourceTree::dropEvent(QDropEvent *event) -{ - if (event->source() != this) { - QListView::dropEvent(event); - return; - } - - OBSBasic *main = OBSBasic::Get(); - - OBSScene scene = GetCurrentScene(); - obs_source_t *scenesource = obs_scene_get_source(scene); - SourceTreeModel *stm = GetStm(); - auto &items = stm->items; - QModelIndexList indices = selectedIndexes(); - - DropIndicatorPosition indicator = dropIndicatorPosition(); - int row = indexAt(event->position().toPoint()).row(); - bool emptyDrop = row == -1; - - if (emptyDrop) { - if (!items.size()) { - QListView::dropEvent(event); - return; - } - - row = items.size() - 1; - indicator = QAbstractItemView::BelowItem; - } - - /* --------------------------------------- */ - /* store destination group if moving to a */ - /* group */ - - obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ - bool itemIsGroup = obs_sceneitem_is_group(dropItem); - - obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); - - /* not a group if moving above the group */ - if (indicator == QAbstractItemView::AboveItem && itemIsGroup) - dropGroup = nullptr; - if (emptyDrop) - dropGroup = nullptr; - - /* --------------------------------------- */ - /* remember to remove list items if */ - /* dropping on collapsed group */ - - bool dropOnCollapsed = false; - if (dropGroup) { - obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); - dropOnCollapsed = obs_data_get_bool(data, "collapsed"); - obs_data_release(data); - } - - if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || - indicator == QAbstractItemView::OnViewport) - row++; - - if (row < 0 || row > stm->items.count()) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* determine if any base group is selected */ - - bool hasGroups = false; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_is_group(item)) { - hasGroups = true; - break; - } - } - - /* --------------------------------------- */ - /* if dropping a group, detect if it's */ - /* below another group */ - - obs_sceneitem_t *itemBelow; - if (row == stm->items.count()) - itemBelow = nullptr; - else - itemBelow = stm->items[row]; - - if (hasGroups) { - if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { - dropGroup = nullptr; - dropOnCollapsed = false; - } - } - - /* --------------------------------------- */ - /* if dropping groups on other groups, */ - /* disregard as invalid drag/drop */ - - if (dropGroup && hasGroups) { - QListView::dropEvent(event); - return; - } - - /* --------------------------------------- */ - /* save undo data */ - std::vector sources; - for (int i = 0; i < indices.size(); i++) { - obs_sceneitem_t *item = items[indices[i].row()]; - if (obs_sceneitem_get_scene(item) != scene) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - } - if (dropGroup) - sources.push_back(obs_sceneitem_get_source(dropGroup)); - OBSData undo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* if selection includes base group items, */ - /* include all group sub-items and treat */ - /* them all as one */ - - if (hasGroups) { - /* remove sub-items if selected */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - obs_scene_t *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - indices.removeAt(i); - } - } - - /* add all sub-items of selected groups */ - for (int i = indices.size() - 1; i >= 0; i--) { - obs_sceneitem_t *item = items[indices[i].row()]; - - if (obs_sceneitem_is_group(item)) { - for (int j = items.size() - 1; j >= 0; j--) { - obs_sceneitem_t *subitem = items[j]; - obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); - - if (subitemGroup == item) { - QModelIndex idx = stm->createIndex(j, 0); - indices.insert(i + 1, idx); - } - } - } - } - } - - /* --------------------------------------- */ - /* build persistent indices */ - - QList persistentIndices; - persistentIndices.reserve(indices.count()); - for (QModelIndex &index : indices) - persistentIndices.append(index); - std::sort(persistentIndices.begin(), persistentIndices.end()); - - /* --------------------------------------- */ - /* move all items to destination index */ - - int r = row; - for (auto &persistentIdx : persistentIndices) { - int from = persistentIdx.row(); - int to = r; - int itemTo = to; - - if (itemTo > from) - itemTo--; - - if (itemTo != from) { - stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); - MoveItem(items, from, itemTo); - stm->endMoveRows(); - } - - r = persistentIdx.row() + 1; - } - - std::sort(persistentIndices.begin(), persistentIndices.end()); - int firstIdx = persistentIndices.front().row(); - int lastIdx = persistentIndices.back().row(); - - /* --------------------------------------- */ - /* reorder scene items in back-end */ - - QVector orderList; - obs_sceneitem_t *lastGroup = nullptr; - int insertCollapsedIdx = 0; - - auto insertCollapsed = [&](obs_sceneitem_t *item) { - struct obs_sceneitem_order_info info; - info.group = lastGroup; - info.item = item; - - orderList.insert(insertCollapsedIdx++, info); - }; - - using insertCollapsed_t = decltype(insertCollapsed); - - auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - (*reinterpret_cast(param))(item); - return true; - }; - - auto insertLastGroup = [&]() { - OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); - bool collapsed = obs_data_get_bool(data, "collapsed"); - - if (collapsed) { - insertCollapsedIdx = 0; - obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); - } - - struct obs_sceneitem_order_info info; - info.group = nullptr; - info.item = lastGroup; - orderList.insert(0, info); - }; - - auto updateScene = [&]() { - struct obs_sceneitem_order_info info; - - for (int i = 0; i < items.size(); i++) { - obs_sceneitem_t *item = items[i]; - obs_sceneitem_t *group; - - if (obs_sceneitem_is_group(item)) { - if (lastGroup) { - insertLastGroup(); - } - lastGroup = item; - continue; - } - - if (!hasGroups && i >= firstIdx && i <= lastIdx) - group = dropGroup; - else - group = obs_sceneitem_get_group(scene, item); - - if (lastGroup && lastGroup != group) { - insertLastGroup(); - } - - lastGroup = group; - - info.group = group; - info.item = item; - orderList.insert(0, info); - } - - if (lastGroup) { - insertLastGroup(); - } - - obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); - }; - - using updateScene_t = decltype(updateScene); - - auto preUpdateScene = [](void *data, obs_scene_t *) { - (*reinterpret_cast(data))(); - }; - - ignoreReorder = true; - obs_scene_atomic_update(scene, preUpdateScene, &updateScene); - ignoreReorder = false; - - /* --------------------------------------- */ - /* save redo data */ - - OBSData redo_data = main->BackupScene(scene, &sources); - - /* --------------------------------------- */ - /* add undo/redo action */ - - const char *scene_name = obs_source_get_name(scenesource); - QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); - main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); - - /* --------------------------------------- */ - /* remove items if dropped in to collapsed */ - /* group */ - - if (dropOnCollapsed) { - stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); - items.remove(firstIdx, lastIdx - firstIdx + 1); - stm->endRemoveRows(); - } - - /* --------------------------------------- */ - /* update widgets and accept event */ - - UpdateWidgets(true); - - event->accept(); - event->setDropAction(Qt::CopyAction); - - QListView::dropEvent(event); -} - -void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) -{ - { - QSignalBlocker sourcesSignalBlocker(this); - SourceTreeModel *stm = GetStm(); - - QModelIndexList selectedIdxs = selected.indexes(); - QModelIndexList deselectedIdxs = deselected.indexes(); - - for (int i = 0; i < selectedIdxs.count(); i++) { - int idx = selectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], true); - } - - for (int i = 0; i < deselectedIdxs.count(); i++) { - int idx = deselectedIdxs[i].row(); - obs_sceneitem_select(stm->items[idx], false); - } - } - QListView::selectionChanged(selected, deselected); -} - -void SourceTree::NewGroupEdit(int row) -{ - if (!Edit(row)) { - OBSBasic *main = OBSBasic::Get(); - main->undo_s.pop_disabled(); - - blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " - "code should never be reached.\nAnd by " - "\"never be reached\", I mean that " - "theoretically, it should be\nimpossible " - "for this code to be reached. But if this " - "code is reached,\nfeel free to laugh at " - "Lain, because apparently it is, in fact, " - "actually\npossible for this code to be " - "reached. But I mean, again, theoretically\n" - "it should be impossible. So if you see " - "this in your log, just know that\nit's " - "really dumb, and depressing. But at least " - "the undo/redo action is\nstill covered, so " - "in theory things *should* be fine. But " - "it's entirely\npossible that they might " - "not be exactly. But again, yea. This " - "really\nshould not be possible."); - - OBSData redoSceneData = main->BackupScene(GetCurrentScene()); - - QString text = QTStr("Undo.GroupItems").arg("Unknown"); - main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); - - undoSceneData = nullptr; - } -} - -bool SourceTree::Edit(int row) -{ - SourceTreeModel *stm = GetStm(); - if (row < 0 || row >= stm->items.count()) - return false; - - QModelIndex index = stm->createIndex(row, 0); - QWidget *widget = indexWidget(index); - SourceTreeItem *itemWidget = reinterpret_cast(widget); - if (itemWidget->IsEditing()) { -#ifdef __APPLE__ - itemWidget->ExitEditMode(true); -#endif - return false; - } - - itemWidget->EnterEditMode(); - edit(index); - return true; -} - -bool SourceTree::MultipleBaseSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (obs_sceneitem_is_group(item)) { - return false; - } - - obs_scene *itemScene = obs_sceneitem_get_scene(item); - if (itemScene != scene) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - - OBSScene scene = GetCurrentScene(); - - if (selectedIndices.size() < 1) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - if (!obs_sceneitem_is_group(item)) { - return false; - } - } - - return true; -} - -bool SourceTree::GroupedItemsSelected() const -{ - SourceTreeModel *stm = GetStm(); - QModelIndexList selectedIndices = selectedIndexes(); - OBSScene scene = GetCurrentScene(); - - if (!selectedIndices.size()) { - return false; - } - - for (auto &idx : selectedIndices) { - obs_sceneitem_t *item = stm->items[idx.row()]; - obs_scene *itemScene = obs_sceneitem_get_scene(item); - - if (itemScene != scene) { - return true; - } - } - - return false; -} - -void SourceTree::Remove(OBSSceneItem item, OBSScene scene) -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - GetStm()->Remove(item); - main->SaveProject(); - - if (!main->SavingDisabled()) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - } -} - -void SourceTree::GroupSelectedItems() -{ - QModelIndexList indices = selectedIndexes(); - std::sort(indices.begin(), indices.end()); - GetStm()->GroupSelectedItems(indices); -} - -void SourceTree::UngroupSelectedGroups() -{ - QModelIndexList indices = selectedIndexes(); - GetStm()->UngroupSelectedGroups(indices); -} - -void SourceTree::AddGroup() -{ - GetStm()->AddGroup(); -} - -void SourceTree::UpdateNoSourcesMessage() -{ - QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; - iconNoSources.load(file); - - QTextOption opt(Qt::AlignHCenter); - opt.setWrapMode(QTextOption::WordWrap); - textNoSources.setTextOption(opt); - textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); - - textPrepared = false; -} - -void SourceTree::paintEvent(QPaintEvent *event) -{ - SourceTreeModel *stm = GetStm(); - if (stm && !stm->items.count()) { - QPainter p(viewport()); - - if (!textPrepared) { - textNoSources.prepare(QTransform(), p.font()); - textPrepared = true; - } - - QRectF iconRect = iconNoSources.viewBoxF(); - iconRect.setSize(QSizeF(32.0, 32.0)); - - QSizeF iconSize = iconRect.size(); - QSizeF textSize = textNoSources.size(); - QSizeF thisSize = size(); - const qreal spacing = 16.0; - - qreal totalHeight = iconSize.height() + spacing + textSize.height(); - - qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; - qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; - iconRect.moveTo(std::round(x), std::round(y)); - iconNoSources.render(&p, iconRect); - - x = thisSize.width() / 2.0 - textSize.width() / 2.0; - y += spacing + iconSize.height(); - p.drawStaticText(x, y, textNoSources); - } else { - QListView::paintEvent(event); - } -} - -SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - SourceTree *tree = qobject_cast(parent()); - QWidget *item = tree->indexWidget(index); - - if (!item) - return (QSize(0, 0)); - - return (QSize(option.widget->minimumWidth(), item->height())); -} diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp deleted file mode 100644 index 8f8922d52..000000000 --- a/UI/source-tree.hpp +++ /dev/null @@ -1,202 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class QLabel; -class OBSSourceLabel; -class QCheckBox; -class QLineEdit; -class SourceTree; -class QSpacerItem; -class QHBoxLayout; -class VisibilityItemWidget; - -class SourceTreeItem : public QFrame { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeModel; - - void mouseDoubleClickEvent(QMouseEvent *event) override; - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual bool eventFilter(QObject *object, QEvent *event) override; - - void Update(bool force); - - enum class Type { - Unknown, - Item, - Group, - SubItem, - }; - - void DisconnectSignals(); - void ReconnectSignals(); - - Type type = Type::Unknown; - -public: - explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); - bool IsEditing(); - -private: - QSpacerItem *spacer = nullptr; - QCheckBox *expand = nullptr; - QLabel *iconLabel = nullptr; - QCheckBox *vis = nullptr; - QCheckBox *lock = nullptr; - QHBoxLayout *boxLayout = nullptr; - OBSSourceLabel *label = nullptr; - - QLineEdit *editor = nullptr; - - std::string newName; - - SourceTree *tree; - OBSSceneItem sceneitem; - std::vector sigs; - - virtual void paintEvent(QPaintEvent *event) override; - - void ExitEditModeInternal(bool save); - -private slots: - void Clear(); - - void EnterEditMode(); - void ExitEditMode(bool save); - - void VisibilityChanged(bool visible); - void LockedChanged(bool locked); - - void ExpandClicked(bool checked); - - void Select(); - void Deselect(); -}; - -class SourceTreeModel : public QAbstractListModel { - Q_OBJECT - - friend class SourceTree; - friend class SourceTreeItem; - - SourceTree *st; - QVector items; - bool hasGroups = false; - - static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); - void Clear(); - void SceneChanged(); - void ReorderItems(); - - void Add(obs_sceneitem_t *item); - void Remove(obs_sceneitem_t *item); - OBSSceneItem Get(int idx); - QString GetNewGroupName(); - void AddGroup(); - - void GroupSelectedItems(QModelIndexList &indices); - void UngroupSelectedGroups(QModelIndexList &indices); - - void ExpandGroup(obs_sceneitem_t *item); - void CollapseGroup(obs_sceneitem_t *item); - - void UpdateGroupState(bool update); - -public: - explicit SourceTreeModel(SourceTree *st); - - virtual int rowCount(const QModelIndex &parent) const override; - virtual QVariant data(const QModelIndex &index, int role) const override; - - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - virtual Qt::DropActions supportedDropActions() const override; -}; - -class SourceTree : public QListView { - Q_OBJECT - - bool ignoreReorder = false; - - friend class SourceTreeModel; - friend class SourceTreeItem; - - bool textPrepared = false; - QStaticText textNoSources; - QSvgRenderer iconNoSources; - - OBSData undoSceneData; - - bool iconsVisible = true; - - void UpdateNoSourcesMessage(); - - void ResetWidgets(); - void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); - void UpdateWidgets(bool force = false); - - inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } - -public: - inline SourceTreeItem *GetItemWidget(int idx) - { - QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); - return reinterpret_cast(widget); - } - - explicit SourceTree(QWidget *parent = nullptr); - - inline bool IgnoreReorder() const { return ignoreReorder; } - inline void Clear() { GetStm()->Clear(); } - - inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } - inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } - inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } - - void SelectItem(obs_sceneitem_t *sceneitem, bool select); - - bool MultipleBaseSelected() const; - bool GroupsSelected() const; - bool GroupedItemsSelected() const; - - void UpdateIcons(); - void SetIconsVisible(bool visible); - -public slots: - inline void ReorderItems() { GetStm()->ReorderItems(); } - inline void RefreshItems() { GetStm()->SceneChanged(); } - void Remove(OBSSceneItem item, OBSScene scene); - void GroupSelectedItems(); - void UngroupSelectedGroups(); - void AddGroup(); - bool Edit(int idx); - void NewGroupEdit(int idx); - -protected: - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void dropEvent(QDropEvent *event) override; - virtual void paintEvent(QPaintEvent *event) override; - - virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; -}; - -class SourceTreeDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - SourceTreeDelegate(QObject *parent); - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; -}; diff --git a/UI/update/mac-update.hpp b/UI/update/mac-update.hpp deleted file mode 100644 index cedac60eb..000000000 --- a/UI/update/mac-update.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef MAC_UPDATER_H -#define MAC_UPDATER_H - -#include - -#include -#include -#include - -class QAction; - -class MacUpdateThread : public QThread { - Q_OBJECT - - bool manualUpdate; - - virtual void run() override; - - void info(const QString &title, const QString &text); - -signals: - void Result(const QString &branch, bool manual); - -private slots: - void infoMsg(const QString &title, const QString &text); - -public: - MacUpdateThread(bool manual) : manualUpdate(manual) {} -}; - -#ifdef __OBJC__ -@class OBSUpdateDelegate; -#endif - -class OBSSparkle : public QObject { - Q_OBJECT - -public: - OBSSparkle(const char *branch, QAction *checkForUpdatesAction); - void setBranch(const char *branch); - void checkForUpdates(bool manualCheck); - -private: -#ifdef __OBJC__ - OBSUpdateDelegate *updaterDelegate; -#else - void *updaterDelegate; -#endif -}; - -#endif diff --git a/UI/update/sparkle-updater.mm b/UI/update/sparkle-updater.mm deleted file mode 100644 index 2afc7e83c..000000000 --- a/UI/update/sparkle-updater.mm +++ /dev/null @@ -1,79 +0,0 @@ -#include "mac-update.hpp" - -#include - -#import -#import - -@interface OBSUpdateDelegate : NSObject { -} -@property (copy) NSString *branch; -@property (nonatomic) SPUStandardUpdaterController *updaterController; -@end - -@implementation OBSUpdateDelegate { -} - -@synthesize branch; - -- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater -{ - return [NSSet setWithObject:branch]; -} - -- (void)observeCanCheckForUpdatesWithAction:(QAction *)action -{ - [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) - options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) - context:(void *) action]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context -{ - if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { - QAction *menuAction = (QAction *) context; - menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); - } else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -- (void)dealloc -{ - @autoreleasepool { - [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; - } -} - -@end - -OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) -{ - @autoreleasepool { - updaterDelegate = [[OBSUpdateDelegate alloc] init]; - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; - updaterDelegate.updaterController = - [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate - userDriverDelegate:nil]; - [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; - } -} - -void OBSSparkle::setBranch(const char *branch) -{ - updaterDelegate.branch = [NSString stringWithUTF8String:branch]; -} - -void OBSSparkle::checkForUpdates(bool manualCheck) -{ - @autoreleasepool { - if (manualCheck) { - [updaterDelegate.updaterController checkForUpdates:nil]; - } else { - [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; - } - } -} diff --git a/UI/visibility-item-widget.cpp b/UI/visibility-item-widget.cpp deleted file mode 100644 index 50ea425ad..000000000 --- a/UI/visibility-item-widget.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "moc_visibility-item-widget.cpp" -#include "obs-app.hpp" -#include "source-label.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_) - : source(source_), - enabledSignal(obs_source_get_signal_handler(source), "enable", OBSSourceEnabled, this) -{ - bool enabled = obs_source_enabled(source); - - vis = new QCheckBox(); - vis->setProperty("class", "checkbox-icon indicator-visibility"); - vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - vis->setChecked(enabled); - - label = new OBSSourceLabel(source); - label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - QHBoxLayout *itemLayout = new QHBoxLayout(); - itemLayout->addWidget(vis); - itemLayout->addWidget(label); - itemLayout->setContentsMargins(0, 0, 0, 0); - - setLayout(itemLayout); - - connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); }); -} - -void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data) -{ - VisibilityItemWidget *window = reinterpret_cast(param); - bool enabled = calldata_bool(data, "enabled"); - - QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled)); -} - -void VisibilityItemWidget::SourceEnabled(bool enabled) -{ - if (vis->isChecked() != enabled) - vis->setChecked(enabled); -} - -void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_) -{ - /* Do not update unless the state has actually changed */ - if (active_ == active && selected_ == selected) - return; - - QPalette pal = vis->palette(); - pal.setColor(QPalette::WindowText, color); - vis->setPalette(pal); - - label->setStyleSheet(QString("color: %1;").arg(color.name())); - - active = active_; - selected = selected_; -} - -VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyledItemDelegate::paint(painter, option, index); - - QObject *parentObj = parent(); - QListWidget *list = qobject_cast(parentObj); - if (!list) - return; - - QListWidgetItem *item = list->item(index.row()); - VisibilityItemWidget *widget = qobject_cast(list->itemWidget(item)); - if (!widget) - return; - - bool selected = option.state.testFlag(QStyle::State_Selected); - bool active = option.state.testFlag(QStyle::State_Active); - - QPalette palette = list->palette(); -#if defined(_WIN32) || defined(__APPLE__) - QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Inactive; -#else - QPalette::ColorGroup group = QPalette::Active; -#endif - -#ifdef _WIN32 - QPalette::ColorRole highlightRole = QPalette::WindowText; -#else - QPalette::ColorRole highlightRole = QPalette::HighlightedText; -#endif - - QPalette::ColorRole role; - - if (selected && active) - role = highlightRole; - else - role = QPalette::WindowText; - - widget->SetColor(palette.color(group, role), active, selected); -} - -bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event) -{ - QWidget *editor = qobject_cast(object); - if (!editor) - return false; - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - - if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) { - return false; - } - } - - return QStyledItemDelegate::eventFilter(object, event); -} - -void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source) -{ - VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source); - - list->setItemWidget(item, baseWidget); -} diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp deleted file mode 100644 index 7e31bdf89..000000000 --- a/UI/window-basic-auto-config.hpp +++ /dev/null @@ -1,288 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -class Ui_AutoConfigStartPage; -class Ui_AutoConfigVideoPage; -class Ui_AutoConfigStreamPage; -class Ui_AutoConfigTestPage; - -class AutoConfigStreamPage; -class Auth; - -class AutoConfig : public QWizard { - Q_OBJECT - - friend class AutoConfigStartPage; - friend class AutoConfigVideoPage; - friend class AutoConfigStreamPage; - friend class AutoConfigTestPage; - - enum class Type { - Invalid, - Streaming, - Recording, - VirtualCam, - }; - - enum class Service { - Twitch, - YouTube, - AmazonIVS, - Other, - }; - - enum class Encoder { - x264, - NVENC, - QSV, - AMD, - Apple, - Stream, - }; - - enum class Quality { - Stream, - High, - }; - - enum class FPSType : int { - PreferHighFPS, - PreferHighRes, - UseCurrent, - fps30, - fps60, - }; - - struct StreamServer { - std::string name; - std::string address; - }; - - static inline const char *GetEncoderId(Encoder enc); - - AutoConfigStreamPage *streamPage = nullptr; - - Service service = Service::Other; - Quality recordingQuality = Quality::Stream; - Encoder recordingEncoder = Encoder::Stream; - Encoder streamingEncoder = Encoder::x264; - Type type = Type::Streaming; - FPSType fpsType = FPSType::PreferHighFPS; - int idealBitrate = 2500; - struct { - std::optional targetBitrate; - std::optional bitrate; - bool testSuccessful = false; - } multitrackVideo; - int baseResolutionCX = 1920; - int baseResolutionCY = 1080; - int idealResolutionCX = 1280; - int idealResolutionCY = 720; - int idealFPSNum = 60; - int idealFPSDen = 1; - std::string serviceName; - std::string serverName; - std::string server; - std::vector serviceConfigServers; - std::string key; - - bool hardwareEncodingAvailable = false; - bool nvencAvailable = false; - bool qsvAvailable = false; - bool vceAvailable = false; - bool appleAvailable = false; - - int startingBitrate = 2500; - bool customServer = false; - bool bandwidthTest = false; - bool testMultitrackVideo = false; - bool testRegions = true; - bool twitchAuto = false; - bool amazonIVSAuto = false; - bool regionUS = true; - bool regionEU = true; - bool regionAsia = true; - bool regionOther = true; - bool preferHighFPS = false; - bool preferHardware = false; - int specificFPSNum = 0; - int specificFPSDen = 0; - - void TestHardwareEncoding(); - bool CanTestServer(const char *server); - - virtual void done(int result) override; - - void SaveStreamSettings(); - void SaveSettings(); - -public: - AutoConfig(QWidget *parent); - ~AutoConfig(); - - enum Page { - StartPage, - VideoPage, - StreamPage, - TestPage, - }; -}; - -class AutoConfigStartPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigStartPage(QWidget *parent = nullptr); - ~AutoConfigStartPage(); - - virtual int nextId() const override; - -public slots: - void on_prioritizeStreaming_clicked(); - void on_prioritizeRecording_clicked(); - void PrioritizeVCam(); -}; - -class AutoConfigVideoPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - std::unique_ptr ui; - -public: - AutoConfigVideoPage(QWidget *parent = nullptr); - ~AutoConfigVideoPage(); - - virtual int nextId() const override; - virtual bool validatePage() override; -}; - -class AutoConfigStreamPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - - std::unique_ptr ui; - QString lastService; - bool ready = false; - - void LoadServices(bool showAll); - inline bool IsCustomService() const; - -public: - AutoConfigStreamPage(QWidget *parent = nullptr); - ~AutoConfigStreamPage(); - - virtual bool isComplete() const override; - virtual int nextId() const override; - virtual bool validatePage() override; - - void OnAuthConnected(); - void OnOAuthStreamKeyConnected(); - -public slots: - void on_show_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_preferHardware_clicked(); - void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); - void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); -}; - -class AutoConfigTestPage : public QWizardPage { - Q_OBJECT - - friend class AutoConfig; - - QPointer results; - - std::unique_ptr ui; - std::thread testThread; - std::condition_variable cv; - std::mutex m; - bool cancel = false; - bool started = false; - - enum class Stage { - Starting, - BandwidthTest, - StreamEncoder, - RecordingEncoder, - Finished, - }; - - Stage stage = Stage::Starting; - bool softwareTested = false; - - void StartBandwidthStage(); - void StartStreamEncoderStage(); - void StartRecordingEncoderStage(); - - void FindIdealHardwareResolution(); - bool TestSoftwareEncoding(); - - void TestBandwidthThread(); - void TestStreamEncoderThread(); - void TestRecordingEncoderThread(); - - void FinalizeResults(); - - struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} - }; - - void GetServers(std::vector &servers); - -public: - AutoConfigTestPage(QWidget *parent = nullptr); - ~AutoConfigTestPage(); - - virtual void initializePage() override; - virtual void cleanupPage() override; - virtual bool isComplete() const override; - virtual int nextId() const override; - -public slots: - void NextStage(); - void UpdateMessage(QString message); - void Failure(QString message); - void Progress(int percentage); -}; diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp deleted file mode 100644 index 30d203174..000000000 --- a/UI/window-basic-main-outputs.cpp +++ /dev/null @@ -1,2541 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "audio-encoders.hpp" -#include "multitrack-video-error.hpp" -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam.hpp" - -using namespace std; - -extern bool EncoderAvailable(const char *encoder); - -volatile bool streaming_active = false; -volatile bool recording_active = false; -volatile bool recording_paused = false; -volatile bool replaybuf_active = false; -volatile bool virtualcam_active = false; - -#define RTMP_PROTOCOL "rtmp" -#define SRT_PROTOCOL "srt" -#define RIST_PROTOCOL "rist" - -static void OBSStreamStarting(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); - - int sec = (int)obs_output_get_active_delay(obj); - if (sec == 0) - return; - - output->delayActive = true; - QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); -} - -static void OBSStreamStopping(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); - - int sec = (int)obs_output_get_active_delay(obj); - if (sec == 0) - QMetaObject::invokeMethod(output->main, "StreamStopping"); - else - QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); -} - -static void OBSStartStreaming(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->streamingActive = true; - os_atomic_set_bool(&streaming_active, true); - QMetaObject::invokeMethod(output->main, "StreamingStart"); -} - -static void OBSStopStreaming(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->streamingActive = false; - output->delayActive = false; - output->multitrackVideoActive = false; - os_atomic_set_bool(&streaming_active, false); - QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSStartRecording(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->recordingActive = true; - os_atomic_set_bool(&recording_active, true); - QMetaObject::invokeMethod(output->main, "RecordingStart"); -} - -static void OBSStopRecording(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - const char *last_error = calldata_string(params, "last_error"); - - QString arg_last_error = QString::fromUtf8(last_error); - - output->recordingActive = false; - os_atomic_set_bool(&recording_active, false); - os_atomic_set_bool(&recording_paused, false); - QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); -} - -static void OBSRecordStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "RecordStopping"); -} - -static void OBSRecordFileChanged(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - const char *next_file = calldata_string(params, "next_file"); - - QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); - - QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); - - output->lastRecordingPath = next_file; -} - -static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->replayBufferActive = true; - os_atomic_set_bool(&replaybuf_active, true); - QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); -} - -static void OBSStopReplayBuffer(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->replayBufferActive = false; - os_atomic_set_bool(&replaybuf_active, false); - QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); -} - -static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); -} - -static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); -} - -static void OBSStartVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - - output->virtualCamActive = true; - os_atomic_set_bool(&virtualcam_active, true); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); -} - -static void OBSStopVirtualCam(void *data, calldata_t *params) -{ - BasicOutputHandler *output = static_cast(data); - int code = (int)calldata_int(params, "code"); - - output->virtualCamActive = false; - os_atomic_set_bool(&virtualcam_active, false); - QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); -} - -static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) -{ - BasicOutputHandler *output = static_cast(data); - output->DestroyVirtualCamView(); -} - -/* ------------------------------------------------------------------------ */ - -struct StartMultitrackVideoStreamingGuard { - StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; - ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } - - std::shared_future GetFuture() const { return future; } - - static std::shared_future MakeReadyFuture() - { - StartMultitrackVideoStreamingGuard guard; - return guard.GetFuture(); - } - -private: - std::promise guard; - std::shared_future future; -}; - -/* ------------------------------------------------------------------------ */ - -static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) -{ - const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); - if (!id_) { - res = nullptr; - return false; - } - - res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); - - if (res) { - obs_encoder_release(res); - return true; - } - - return false; -} - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} - -static bool return_first_id(void *data, const char *id) -{ - const char **output = (const char **)data; - - *output = id; - return false; -} - -static const char *GetStreamOutputType(const obs_service_t *service) -{ - const char *protocol = obs_service_get_protocol(service); - const char *output = nullptr; - - if (!protocol) { - blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); - return nullptr; - } - - if (!obs_is_output_protocol_registered(protocol)) { - blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); - return nullptr; - } - - /* Check if the service has a preferred output type */ - output = obs_service_get_preferred_output_type(service); - if (output) { - if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) - return output; - - blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); - } - - /* Otherwise, prefer first-party output types */ - if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { - return "rtmp_output"; - } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { - return "ffmpeg_hls_muxer"; - } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { - return "ffmpeg_mpegts_muxer"; - } - - /* If third-party protocol, use the first enumerated type */ - obs_enum_output_types_with_protocol(protocol, &output, return_first_id); - if (output) - return output; - - blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); - - return nullptr; -} - -/* ------------------------------------------------------------------------ */ - -inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) -{ - if (main->vcamEnabled) { - virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); - - signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); - startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); - stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); - deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); - } - - auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); - if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { - auto service = main_->GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); - } - if (multitrack_enabled) - multitrackVideo = make_unique(); -} - -extern void log_vcam_changed(const VCamConfig &config, bool starting); - -bool BasicOutputHandler::StartVirtualCam() -{ - if (!main->vcamEnabled) - return false; - - bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; - - if (!virtualCamView && !typeIsProgram) - virtualCamView = obs_view_create(); - - UpdateVirtualCamOutputSource(); - - if (!virtualCamVideo) { - virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); - - if (!virtualCamVideo) - return false; - } - - obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); - if (!Active()) - SetupOutputs(); - - bool success = obs_output_start(virtualCam); - if (!success) { - QString errorReason; - - const char *error = obs_output_get_last_error(virtualCam); - if (error) { - errorReason = QT_UTF8(error); - } else { - errorReason = QTStr("Output.StartFailedGeneric"); - } - - QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); - - DestroyVirtualCamView(); - } - - log_vcam_changed(main->vcamConfig, true); - - return success; -} - -void BasicOutputHandler::StopVirtualCam() -{ - if (main->vcamEnabled) { - obs_output_stop(virtualCam); - } -} - -bool BasicOutputHandler::VirtualCamActive() const -{ - if (main->vcamEnabled) { - return obs_output_active(virtualCam); - } - return false; -} - -void BasicOutputHandler::UpdateVirtualCamOutputSource() -{ - if (!main->vcamEnabled || !virtualCamView) - return; - - OBSSourceAutoRelease source; - - switch (main->vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - DestroyVirtualCameraScene(); - return; - case VCamOutputType::PreviewOutput: { - DestroyVirtualCameraScene(); - OBSSource s = main->GetCurrentSceneSource(); - obs_source_get_ref(s); - source = s.Get(); - break; - } - case VCamOutputType::SceneOutput: - DestroyVirtualCameraScene(); - source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); - - if (!vCamSourceScene) - vCamSourceScene = obs_scene_create_private("vcam_source"); - source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); - - if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { - obs_sceneitem_remove(vCamSourceSceneItem); - vCamSourceSceneItem = nullptr; - } - - if (!vCamSourceSceneItem) { - vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); - - obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); - obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); - - const struct vec2 size = { - (float)obs_source_get_width(source), - (float)obs_source_get_height(source), - }; - obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); - } - break; - } - - OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); - if (source != current) - obs_view_set_source(virtualCamView, 0, source); -} - -void BasicOutputHandler::DestroyVirtualCamView() -{ - if (main->vcamConfig.type == VCamOutputType::ProgramView) { - virtualCamVideo = nullptr; - return; - } - - obs_view_remove(virtualCamView); - obs_view_set_source(virtualCamView, 0, nullptr); - virtualCamVideo = nullptr; - - obs_view_destroy(virtualCamView); - virtualCamView = nullptr; - - DestroyVirtualCameraScene(); -} - -void BasicOutputHandler::DestroyVirtualCameraScene() -{ - if (!vCamSourceScene) - return; - - obs_scene_release(vCamSourceScene); - vCamSourceScene = nullptr; - vCamSourceSceneItem = nullptr; -} - -/* ------------------------------------------------------------------------ */ - -struct SimpleOutput : BasicOutputHandler { - OBSEncoder audioStreaming; - OBSEncoder videoStreaming; - OBSEncoder audioRecording; - OBSEncoder audioArchive; - OBSEncoder videoRecording; - OBSEncoder audioTrack[MAX_AUDIO_MIXES]; - - string videoEncoder; - string videoQuality; - bool usingRecordingPreset = false; - bool recordingConfigured = false; - bool ffmpegOutput = false; - bool lowCPUx264 = false; - - SimpleOutput(OBSBasic *main_); - - int CalcCRF(int crf); - - void UpdateRecordingSettings_x264_crf(int crf); - void UpdateRecordingSettings_qsv11(int crf, bool av1); - void UpdateRecordingSettings_nvenc(int cqp); - void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); - void UpdateRecordingSettings_amd_cqp(int cqp); - void UpdateRecordingSettings_apple(int quality); -#ifdef ENABLE_HEVC - void UpdateRecordingSettings_apple_hevc(int quality); -#endif - void UpdateRecordingSettings(); - void UpdateRecordingAudioSettings(); - virtual void Update() override; - - void SetupOutputs() override; - int GetAudioBitrate() const; - - void LoadRecordingPreset_Lossy(const char *encoder); - void LoadRecordingPreset_Lossless(); - void LoadRecordingPreset(); - - void LoadStreamingPreset_Lossy(const char *encoder); - - void UpdateRecording(); - bool ConfigureRecording(bool useReplayBuffer); - - bool IsVodTrackEnabled(obs_service_t *service); - void SetupVodTrack(obs_service_t *service); - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; -}; - -void SimpleOutput::LoadRecordingPreset_Lossless() -{ - fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(simple output)"; - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "format_name", "avi"); - obs_data_set_string(settings, "video_encoder", "utvideo"); - obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - - obs_output_update(fileOutput, settings); -} - -void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) -{ - videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); - if (!videoRecording) - throw "Failed to create video recording encoder (simple output)"; - obs_encoder_release(videoRecording); -} - -void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) -{ - videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); - if (!videoStreaming) - throw "Failed to create video streaming encoder (simple output)"; - obs_encoder_release(videoStreaming); -} - -/* mistakes have been made to lead us to this. */ -const char *get_simple_output_encoder(const char *encoder) -{ - if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { - return "obs_x264"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - return "obs_qsv11_v2"; - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - return "obs_qsv11_av1"; - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - return "h264_texture_amf"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - return "h265_texture_amf"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - return "av1_texture_amf"; - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; -#endif - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - return "obs_nvenc_av1_tex"; - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.avc"; -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { - return "com.apple.videotoolbox.videoencoder.ave.hevc"; -#endif - } - - return "obs_x264"; -} - -void SimpleOutput::LoadRecordingPreset() -{ - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); - - videoEncoder = encoder; - videoQuality = quality; - ffmpegOutput = false; - - if (strcmp(quality, "Stream") == 0) { - videoRecording = videoStreaming; - audioRecording = audioStreaming; - usingRecordingPreset = false; - return; - - } else if (strcmp(quality, "Lossless") == 0) { - LoadRecordingPreset_Lossless(); - usingRecordingPreset = true; - ffmpegOutput = true; - return; - - } else { - lowCPUx264 = false; - - if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) - lowCPUx264 = true; - LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); - usingRecordingPreset = true; - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); - else - success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); - - if (!success) - throw "Failed to create audio recording encoder " - "(simple output)"; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[23]; - if (strcmp(audio_encoder, "opus") == 0) { - snprintf(name, sizeof name, "simple_opus_recording%d", i); - success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } else { - snprintf(name, sizeof name, "simple_aac_recording%d", i); - success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); - } - if (!success) - throw "Failed to create multi-track audio recording encoder " - "(simple output)"; - } - } -} - -#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" - -SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - - LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - - bool success = false; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); - else - success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); - - if (!success) - throw "Failed to create audio streaming encoder (simple output)"; - - if (strcmp(audio_encoder, "opus") == 0) - success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - else - success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); - - if (!success) - throw "Failed to create audio archive encoder (simple output)"; - - LoadRecordingPreset(); - - if (!ffmpegOutput) { - bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", - nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(simple output)"; - } - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); -} - -int SimpleOutput::GetAudioBitrate() const -{ - const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - - if (strcmp(audio_encoder, "opus") == 0) - return FindClosestAvailableSimpleOpusBitrate(bitrate); - - return FindClosestAvailableSimpleAACBitrate(bitrate); -} - -void SimpleOutput::Update() -{ - OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease audioSettings = obs_data_create(); - - int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); - const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); - const char *encoder_id = obs_encoder_get_id(videoStreaming); - const char *presetType; - const char *preset; - - if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { - presetType = "QSVPreset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { - presetType = "AMDPreset"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { - presetType = "AMDPreset"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { - presetType = "NVENCPreset2"; - -#ifdef ENABLE_HEVC - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { - presetType = "NVENCPreset2"; -#endif - - } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { - presetType = "AMDAV1Preset"; - - } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { - presetType = "NVENCPreset2"; - - } else { - presetType = "Preset"; - } - - preset = config_get_string(main->Config(), "SimpleOutput", presetType); - - /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ - if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { - obs_data_set_string(videoSettings, "preset2", preset); - } else { - obs_data_set_string(videoSettings, "preset", preset); - } - - obs_data_set_string(videoSettings, "rate_control", "CBR"); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - - if (advanced) - obs_data_set_string(videoSettings, "x264opts", custom); - - obs_data_set_string(audioSettings, "rate_control", "CBR"); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - - obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); - - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(audioSettings, "bitrate", audioBitrate); - } - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(audioStreaming, audioSettings); - obs_encoder_update(audioArchive, audioSettings); -} - -void SimpleOutput::UpdateRecordingAudioSettings() -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", 192); - obs_data_set_string(settings, "rate_control", "CBR"); - - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv || strcmp(quality, "Stream") == 0) { - obs_encoder_update(audioRecording, settings); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_update(audioTrack[i], settings); - } - } - } -} - -#define CROSS_DIST_CUTOFF 2000.0 - -int SimpleOutput::CalcCRF(int crf) -{ - int cx = config_get_uint(main->Config(), "Video", "OutputCX"); - int cy = config_get_uint(main->Config(), "Video", "OutputCY"); - double fCX = double(cx); - double fCY = double(cy); - - if (lowCPUx264) - crf -= 2; - - double crossDist = sqrt(fCX * fCX + fCY * fCY); - double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; - crfResReduction = (1.0 - crfResReduction) * 10.0; - - return crf - int(crfResReduction); -} - -void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "crf", crf); - obs_data_set_bool(settings, "use_bufsize", true); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); - - obs_encoder_update(videoRecording, settings); -} - -static bool icq_available(obs_encoder_t *encoder) -{ - obs_properties_t *props = obs_encoder_properties(encoder); - obs_property_t *p = obs_properties_get(props, "rate_control"); - bool icq_found = false; - - size_t num = obs_property_list_item_count(p); - for (size_t i = 0; i < num; i++) { - const char *val = obs_property_list_item_string(p, i); - if (strcmp(val, "ICQ") == 0) { - icq_found = true; - break; - } - } - - obs_properties_destroy(props); - return icq_found; -} - -void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) -{ - bool icq = icq_available(videoRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "profile", "high"); - - if (icq && !av1) { - obs_data_set_string(settings, "rate_control", "ICQ"); - obs_data_set_int(settings, "icq_quality", crf); - } else { - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_int(settings, "cqp", crf); - } - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "cqp", cqp); - - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings_apple(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} - -#ifdef ENABLE_HEVC -void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CRF"); - obs_data_set_string(settings, "profile", "main"); - obs_data_set_int(settings, "quality", quality); - - obs_encoder_update(videoRecording, settings); -} -#endif - -void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "rate_control", "CQP"); - obs_data_set_string(settings, "profile", "high"); - obs_data_set_string(settings, "preset", "quality"); - obs_data_set_int(settings, "cqp", cqp); - obs_encoder_update(videoRecording, settings); -} - -void SimpleOutput::UpdateRecordingSettings() -{ - bool ultra_hq = (videoQuality == "HQ"); - int crf = CalcCRF(ultra_hq ? 16 : 23); - - if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { - UpdateRecordingSettings_x264_crf(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV) { - UpdateRecordingSettings_qsv11(crf, false); - - } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { - UpdateRecordingSettings_qsv11(crf, true); - - } else if (videoEncoder == SIMPLE_ENCODER_AMD) { - UpdateRecordingSettings_amd_cqp(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { - UpdateRecordingSettings_amd_cqp(crf); -#endif - - } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { - UpdateRecordingSettings_amd_cqp(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { - UpdateRecordingSettings_nvenc(crf); - -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); -#endif - } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { - UpdateRecordingSettings_nvenc_hevc_av1(crf); - - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { - /* These are magic numbers. 0 - 100, more is better. */ - UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); -#ifdef ENABLE_HEVC - } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { - UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); -#endif - } - UpdateRecordingAudioSettings(); -} - -inline void SimpleOutput::SetupOutputs() -{ - SimpleOutput::Update(); - obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(audioStreaming, obs_get_audio()); - obs_encoder_set_audio(audioArchive, obs_get_audio()); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - - if (usingRecordingPreset) { - if (ffmpegOutput) { - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - } else { - obs_encoder_set_video(videoRecording, obs_get_video()); - if (flv) { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_encoder_set_audio(audioTrack[i], obs_get_audio()); - } - } - } - } - } else { - obs_encoder_set_audio(audioRecording, obs_get_audio()); - } -} - -const char *FindAudioEncoderFromCodec(const char *type) -{ - const char *alt_enc_id = nullptr; - size_t i = 0; - - while (obs_enum_encoder_types(i++, &alt_enc_id)) { - const char *codec = obs_get_encoder_codec(alt_enc_id); - if (strcmp(type, codec) == 0) { - return alt_enc_id; - } - } - - return nullptr; -} - -std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) -{ - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - auto audio_bitrate = GetAudioBitrate(); - auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); - - startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, - this); - stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; - }; - - return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, - [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -static inline bool ServiceSupportsVodTrack(const char *service); - -static void clear_archive_encoder(obs_output_t *output, const char *expected_name) -{ - obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); - bool clear = false; - - /* ensures that we don't remove twitch's soundtrack encoder */ - if (last) { - const char *name = obs_encoder_get_name(last); - clear = name && strcmp(name, expected_name) == 0; - obs_encoder_release(last); - } - - if (clear) - obs_output_set_audio_encoder(output, nullptr, 1); -} - -bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) -{ - bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); - bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) - return enableForCustomServer ? enable : false; - else - return advanced && enable && ServiceSupportsVodTrack(name); -} - -void SimpleOutput::SetupVodTrack(obs_service_t *service) -{ - if (IsVodTrackEnabled(service)) - obs_output_set_audio_encoder(streamOutput, audioArchive, 1); - else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); -} - -bool SimpleOutput::StartStreaming(obs_service_t *service) -{ - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); - bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); - int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); - bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - if (!reconnect) - maxRetries = 0; - - obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); - - obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); - - if (!multitrackVideo || !multitrackVideoActive) - SetupVodTrack(service); - - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -void SimpleOutput::UpdateRecording() -{ - const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - bool flv = strcmp(recFormat, "flv") == 0; - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - int idx = 0; - int idx2 = 0; - const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); - - if (replayBufferActive || recordingActive) - return; - - if (usingRecordingPreset) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { - Update(); - } - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput) { - obs_output_set_video_encoder(fileOutput, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(fileOutput, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); - } - } - } - } - if (replayBuffer) { - obs_output_set_video_encoder(replayBuffer, videoRecording); - if (flv || strcmp(quality, "Stream") == 0) { - obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); - } - } - } - } - - recordingConfigured = true; -} - -bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) -{ - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); - const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); - int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); - int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); - - bool is_fragmented = strncmp(format, "fragmented", 10) == 0; - bool is_lossless = videoQuality == "Lossless"; - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - if (updateReplayBuffer) { - f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(format); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); - } else { - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, - f.c_str(), ffmpegOutput); - obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); - if (ffmpegOutput) - obs_output_set_mixers(fileOutput, tracks); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented && !is_lossless) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - if (updateReplayBuffer) - obs_output_update(replayBuffer, settings); - else - obs_output_update(fileOutput, settings); - - return true; -} - -bool SimpleOutput::StartRecording() -{ - UpdateRecording(); - if (!ConfigureRecording(false)) - return false; - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool SimpleOutput::StartReplayBuffer() -{ - UpdateRecording(); - if (!ConfigureRecording(true)) - return false; - if (!obs_output_start(replayBuffer)) { - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); - return false; - } - - return true; -} - -void SimpleOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void SimpleOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void SimpleOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool SimpleOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool SimpleOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool SimpleOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -struct AdvancedOutput : BasicOutputHandler { - OBSEncoder streamAudioEnc; - OBSEncoder streamArchiveEnc; - OBSEncoder streamTrack[MAX_AUDIO_MIXES]; - OBSEncoder recordTrack[MAX_AUDIO_MIXES]; - OBSEncoder videoStreaming; - OBSEncoder videoRecording; - - bool ffmpegOutput; - bool ffmpegRecording; - bool useStreamEncoder; - bool useStreamAudioEncoder; - bool usesBitrate = false; - - AdvancedOutput(OBSBasic *main_); - - inline void UpdateStreamSettings(); - inline void UpdateRecordingSettings(); - inline void UpdateAudioSettings(); - virtual void Update() override; - - inline std::optional VodTrackMixerIdx(obs_service_t *service); - inline void SetupVodTrack(obs_service_t *service); - - inline void SetupStreaming(); - inline void SetupRecording(); - inline void SetupFFmpeg(); - void SetupOutputs() override; - int GetAudioBitrate(size_t i, const char *id) const; - - virtual std::shared_future SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) override; - virtual bool StartStreaming(obs_service_t *service) override; - virtual bool StartRecording() override; - virtual bool StartReplayBuffer() override; - virtual void StopStreaming(bool force) override; - virtual void StopRecording(bool force) override; - virtual void StopReplayBuffer(bool force) override; - virtual bool StreamingActive() const override; - virtual bool RecordingActive() const override; - virtual bool ReplayBufferActive() const override; - bool allowsMultiTrack(); -}; - -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); - - OBSDataAutoRelease data = nullptr; - - if (!jsonFilePath.empty()) { - BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); - - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) { - data = obs_data_create(); - } - - return data.Get(); -} - -static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) -{ - OBSData dataRet = obs_encoder_get_defaults(encoder); - obs_data_release(dataRet); - - if (!!settings) - obs_data_apply(dataRet, settings); - settings = std::move(dataRet); -} - -#define ADV_ARCHIVE_NAME "adv_archive_audio" - -#ifdef __APPLE__ -static void translate_macvth264_encoder(const char *&encoder) -{ - if (strcmp(encoder, "vt_h264_hw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; - } else if (strcmp(encoder, "vt_h264_sw") == 0) { - encoder = "com.apple.videotoolbox.videoencoder.h264"; - } -} -#endif - -AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) -{ - const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); -#ifdef __APPLE__ - translate_macvth264_encoder(streamEncoder); - translate_macvth264_encoder(recordEncoder); -#endif - - ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); - useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; - - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); - - if (ffmpegOutput) { - fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); - if (!fileOutput) - throw "Failed to create recording FFmpeg output " - "(advanced output)"; - } else { - bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); - if (useReplayBuffer) { - OBSDataAutoRelease hotkey; - const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); - if (str) - hotkey = obs_data_create_from_json(str); - else - hotkey = nullptr; - - replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); - - if (!replayBuffer) - throw "Failed to create replay buffer output " - "(simple output)"; - - signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); - - startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); - stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); - replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); - replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); - } - - bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; - fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, - nullptr); - if (!fileOutput) - throw "Failed to create recording output " - "(advanced output)"; - - if (!useStreamEncoder) { - videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", - recordEncSettings, nullptr); - if (!videoRecording) - throw "Failed to create recording video " - "encoder (advanced output)"; - obs_encoder_release(videoRecording); - } - } - - videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); - if (!videoStreaming) - throw "Failed to create streaming video encoder " - "(advanced output)"; - obs_encoder_release(videoStreaming); - - const char *rate_control = - obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); - if (!rate_control) - rate_control = ""; - usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || - astrcmpi(rate_control, "ABR") == 0; - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[19]; - snprintf(name, sizeof(name), "adv_record_audio_%d", i); - - recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, - name, nullptr, i, nullptr); - - if (!recordTrack[i]) { - throw "Failed to create audio encoder " - "(advanced output)"; - } - - obs_encoder_release(recordTrack[i]); - - snprintf(name, sizeof(name), "adv_stream_audio_%d", i); - streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); - - if (!streamTrack[i]) { - throw "Failed to create streaming audio encoders " - "(advanced output)"; - } - - obs_encoder_release(streamTrack[i]); - } - - std::string id; - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - streamAudioEnc = - obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); - if (!streamAudioEnc) - throw "Failed to create streaming audio encoder " - "(advanced output)"; - obs_encoder_release(streamAudioEnc); - - id = ""; - int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); - if (!streamArchiveEnc) - throw "Failed to create archive audio encoder " - "(advanced output)"; - obs_encoder_release(streamArchiveEnc); - - startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); - stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); - recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); - recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, - this); -} - -void AdvancedOutput::UpdateStreamSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); - ApplyEncoderDefaults(settings, videoStreaming); - - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings, "bitrate"); - int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - if (!enforceBitrate) { - blog(LOG_INFO, "User is ignoring service bitrate limits."); - obs_data_set_int(settings, "bitrate", bitrate); - } - - int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) - obs_data_set_int(settings, "keyint_sec", keyint_sec); - } else { - blog(LOG_WARNING, "User is ignoring service settings."); - } - - if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) - obs_data_set_bool(settings, "lookahead", false); - - video_t *video = obs_get_video(); - enum video_format format = video_output_get_format(video); - - switch (format) { - case VIDEO_FORMAT_I420: - case VIDEO_FORMAT_NV12: - case VIDEO_FORMAT_I010: - case VIDEO_FORMAT_P010: - break; - default: - obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); - } - - obs_encoder_update(videoStreaming, settings); -} - -inline void AdvancedOutput::UpdateRecordingSettings() -{ - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); - obs_encoder_update(videoRecording, settings); -} - -void AdvancedOutput::Update() -{ - UpdateStreamSettings(); - if (!useStreamEncoder && !ffmpegOutput) - UpdateRecordingSettings(); - UpdateAudioSettings(); -} - -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - -inline bool AdvancedOutput::allowsMultiTrack() -{ - const char *protocol = nullptr; - obs_service_t *service_obj = main->GetService(); - protocol = obs_service_get_protocol(service_obj); - if (!protocol) - return false; - return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || - astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; -} - -inline void AdvancedOutput::SetupStreaming() -{ - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - bool is_multitrack_output = allowsMultiTrack(); - - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - obs_encoder_set_scaled_size(videoStreaming, cx, cy); - obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); - - const char *id = obs_service_get_id(main->GetService()); - if (strcmp(id, "rtmp_custom") == 0) { - OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); - obs_encoder_update(videoStreaming, settings); - } -} - -inline void AdvancedOutput::SetupRecording() -{ - const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); - const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); - int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); - int tracks; - - const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); - - bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; - bool flv = strcmp(recFormat, "flv") == 0; - - if (flv) - tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); - else - tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); - - OBSDataAutoRelease settings = obs_data_create(); - unsigned int cx = 0; - unsigned int cy = 0; - int idx = 0; - - /* Hack to allow recordings without any audio tracks selected. It is no - * longer possible to select such a configuration in settings, but legacy - * configurations might still have this configured and we don't want to - * just break them. */ - if (tracks == 0) - tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - - if (useStreamEncoder) { - obs_output_set_video_encoder(fileOutput, videoStreaming); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoStreaming); - } else { - if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { - if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { - cx = 0; - cy = 0; - } - } - - obs_encoder_set_scaled_size(videoRecording, cx, cy); - obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); - obs_output_set_video_encoder(fileOutput, videoRecording); - if (replayBuffer) - obs_output_set_video_encoder(replayBuffer, videoRecording); - } - - if (!flv) { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); - idx++; - } - } - } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); - - if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); - } - - // Use fragmented MOV/MP4 if user has not already specified custom movflags - if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { - string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; - if (mux) { - mux_frag += " "; - mux_frag += mux; - } - obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); - } else { - if (is_fragmented) - blog(LOG_WARNING, "User enabled fragmented recording, " - "but custom muxer settings contained movflags."); - obs_data_set_string(settings, "muxer_settings", mux); - } - - obs_data_set_string(settings, "path", path); - obs_output_update(fileOutput, settings); - if (replayBuffer) - obs_output_update(replayBuffer, settings); -} - -inline void AdvancedOutput::SetupFFmpeg() -{ - const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); - int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); - int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); - bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); - const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); - const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); - const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); - const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); - const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); - int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); - const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); - int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); - int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); - const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); - int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); - const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); - - OBSDataArrayAutoRelease audio_names = obs_data_array_create(); - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - - const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - OBSDataAutoRelease item = obs_data_create(); - obs_data_set_string(item, "name", audioName); - obs_data_array_push_back(audio_names, item); - } - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_array(settings, "audio_names", audio_names); - obs_data_set_string(settings, "url", url); - obs_data_set_string(settings, "format_name", formatName); - obs_data_set_string(settings, "format_mime_type", mimeType); - obs_data_set_string(settings, "muxer_settings", muxCustom); - obs_data_set_int(settings, "gop_size", gopSize); - obs_data_set_int(settings, "video_bitrate", vBitrate); - obs_data_set_string(settings, "video_encoder", vEncoder); - obs_data_set_int(settings, "video_encoder_id", vEncoderId); - obs_data_set_string(settings, "video_settings", vEncCustom); - obs_data_set_int(settings, "audio_bitrate", aBitrate); - obs_data_set_string(settings, "audio_encoder", aEncoder); - obs_data_set_int(settings, "audio_encoder_id", aEncoderId); - obs_data_set_string(settings, "audio_settings", aEncCustom); - - if (rescale && rescaleRes && *rescaleRes) { - int width; - int height; - int val = sscanf(rescaleRes, "%dx%d", &width, &height); - - if (val == 2 && width && height) { - obs_data_set_int(settings, "scale_width", width); - obs_data_set_int(settings, "scale_height", height); - } - } - - obs_output_set_mixers(fileOutput, aMixes); - obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); - obs_output_update(fileOutput, settings); -} - -static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) -{ - obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); -} - -inline void AdvancedOutput::UpdateAudioSettings() -{ - bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); - bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); - - bool is_multitrack_output = allowsMultiTrack(); - - OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - string cfg_name = "Track"; - cfg_name += to_string((int)i + 1); - cfg_name += "Name"; - const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); - - string def_name = "Track"; - def_name += to_string((int)i + 1); - SetEncoderName(recordTrack[i], name, def_name.c_str()); - SetEncoderName(streamTrack[i], name, def_name.c_str()); - } - - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - int track = (int)(i + 1); - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); - - obs_encoder_update(recordTrack[i], settings[i]); - - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); - - if (!is_multitrack_output) { - if (track == streamTrackIndex || track == vodTrackIndex) { - if (applyServiceSettings) { - int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); - - if (!enforceBitrate) - obs_data_set_int(settings[i], "bitrate", bitrate); - } - } - - if (track == streamTrackIndex) - obs_encoder_update(streamAudioEnc, settings[i]); - if (track == vodTrackIndex) - obs_encoder_update(streamArchiveEnc, settings[i]); - } else { - obs_encoder_update(streamTrack[i], settings[i]); - } - } -} - -void AdvancedOutput::SetupOutputs() -{ - obs_encoder_set_video(videoStreaming, obs_get_video()); - if (videoRecording) - obs_encoder_set_video(videoRecording, obs_get_video()); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_set_audio(streamTrack[i], obs_get_audio()); - obs_encoder_set_audio(recordTrack[i], obs_get_audio()); - } - obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); - obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); - - SetupStreaming(); - - if (ffmpegOutput) - SetupFFmpeg(); - else - SetupRecording(); -} - -int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAudioBitrate(id, bitrate); -} - -inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) -{ - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); - bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); - int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); - bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); - - const char *id = obs_service_get_id(service); - if (strcmp(id, "rtmp_custom") == 0) { - vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; - } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) - vodTrackEnabled = false; - } - - if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) - return {vodTrackIndex - 1}; - return std::nullopt; -} - -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) -{ - if (VodTrackMixerIdx(service).has_value()) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); - else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); -} - -std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, - SetupStreamingContinuation_t continuation) -{ - int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); - - bool is_multitrack_output = allowsMultiTrack(); - - if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - - /* --------------------- */ - - const char *type = GetStreamOutputType(service); - if (!type) { - continuation(false); - return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); - } - - const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - - auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { - if (multitrackVideoResult.has_value()) - return multitrackVideoResult.value(); - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); - - startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, - this); - stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, - this); - - outputType = type; - } - - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - int idx = 0; - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); - idx++; - } - } - } - - return true; - }; - - return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), - VodTrackMixerIdx(service), [=](std::optional res) { - continuation(handle_multitrack_video_result(res)); - }); -} - -bool AdvancedOutput::StartStreaming(obs_service_t *service) -{ - obs_output_set_service(streamOutput, service); - - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); - int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); - int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); - bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); - int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); - bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); - const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); - const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); -#ifdef _WIN32 - bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); - bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); -#else - bool enableNewSocketLoop = false; -#endif - bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); - - if (multitrackVideo && multitrackVideoActive && - !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, - enableDynBitrate)) { - multitrackVideoActive = false; - return false; - } - - bool is_rtmp = false; - obs_service_t *service_obj = main->GetService(); - const char *protocol = obs_service_get_protocol(service_obj); - if (protocol) { - if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) - is_rtmp = true; - } - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "bind_ip", bindIP); - obs_data_set_string(settings, "ip_family", ipFamily); -#ifdef _WIN32 - obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); - obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); -#endif - obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - - auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient - - obs_output_update(streamOutput, settings); - - if (!reconnect) - maxRetries = 0; - - obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); - - obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); - if (is_rtmp) { - SetupVodTrack(service); - } - if (obs_output_start(streamOutput)) { - if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StartedStreaming(); - return true; - } - - if (multitrackVideo && multitrackVideoActive) - multitrackVideoActive = false; - - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); - - const char *type = obs_output_get_id(streamOutput); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", - hasLastError ? error : ""); - return false; -} - -bool AdvancedOutput::StartRecording() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - bool splitFile; - const char *splitFileType; - int splitFileTime; - int splitFileSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) { - UpdateRecordingSettings(); - } - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); - - string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, - ffmpegRecording); - - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); - - if (splitFile) { - splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); - splitFileTime = (astrcmpi(splitFileType, "Time") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") - : 0; - splitFileSize = (astrcmpi(splitFileType, "Size") == 0) - ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") - : 0; - string ext = GetFormatExt(recFormat); - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", filenameFormat); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); - obs_data_set_bool(settings, "split_file", true); - obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); - obs_data_set_int(settings, "max_size_mb", splitFileSize); - } - - obs_output_update(fileOutput, settings); - } - - if (!obs_output_start(fileOutput)) { - QString error_reason; - const char *error = obs_output_get_last_error(fileOutput); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); - return false; - } - - return true; -} - -bool AdvancedOutput::StartReplayBuffer() -{ - const char *path; - const char *recFormat; - const char *filenameFormat; - bool noSpace = false; - bool overwriteIfExists = false; - const char *rbPrefix; - const char *rbSuffix; - int rbTime; - int rbSize; - - if (!useStreamEncoder) { - if (!ffmpegOutput) - UpdateRecordingSettings(); - } else if (!obs_output_active(StreamingOutput())) { - UpdateStreamSettings(); - } - - UpdateAudioSettings(); - - if (!Active()) - SetupOutputs(); - - if (!ffmpegOutput || ffmpegRecording) { - path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); - recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); - filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - noSpace = config_get_bool(main->Config(), "AdvOut", - ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); - rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); - rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); - rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); - rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); - - string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); - string ext = GetFormatExt(recFormat); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "directory", path); - obs_data_set_string(settings, "format", f.c_str()); - obs_data_set_string(settings, "extension", ext.c_str()); - obs_data_set_bool(settings, "allow_spaces", !noSpace); - obs_data_set_int(settings, "max_time_sec", rbTime); - obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); - - obs_output_update(replayBuffer, settings); - } - - if (!obs_output_start(replayBuffer)) { - QString error_reason; - const char *error = obs_output_get_last_error(replayBuffer); - if (error) - error_reason = QT_UTF8(error); - else - error_reason = QTStr("Output.StartFailedGeneric"); - QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); - return false; - } - - return true; -} - -void AdvancedOutput::StopStreaming(bool force) -{ - auto output = StreamingOutput(); - if (force && output) - obs_output_force_stop(output); - else if (multitrackVideo && multitrackVideoActive) - multitrackVideo->StopStreaming(); - else - obs_output_stop(output); -} - -void AdvancedOutput::StopRecording(bool force) -{ - if (force) - obs_output_force_stop(fileOutput); - else - obs_output_stop(fileOutput); -} - -void AdvancedOutput::StopReplayBuffer(bool force) -{ - if (force) - obs_output_force_stop(replayBuffer); - else - obs_output_stop(replayBuffer); -} - -bool AdvancedOutput::StreamingActive() const -{ - return obs_output_active(StreamingOutput()); -} - -bool AdvancedOutput::RecordingActive() const -{ - return obs_output_active(fileOutput); -} - -bool AdvancedOutput::ReplayBufferActive() const -{ - return obs_output_active(replayBuffer); -} - -/* ------------------------------------------------------------------------ */ - -void BasicOutputHandler::SetupAutoRemux(const char *&container) -{ - bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); - if (autoRemux && strcmp(container, "mp4") == 0) - container = "mkv"; -} - -std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, - bool overwrite, const char *format, bool ffmpeg) -{ - if (!ffmpeg) - SetupAutoRemux(container); - - string dst = GetOutputFilename(path, container, noSpace, overwrite, format); - lastRecordingPath = dst; - return dst; -} - -extern std::string DeserializeConfigText(const char *text); - -std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, - size_t main_audio_mixer, - std::optional vod_track_mixer, - std::function)> continuation) -{ - auto start_streaming_guard = std::make_shared(); - if (!multitrackVideo) { - continuation(std::nullopt); - return start_streaming_guard->GetFuture(); - } - - multitrackVideoActive = false; - - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); - - bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; - - std::optional custom_config = std::nullopt; - if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) - custom_config = DeserializeConfigText( - config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - QString key = obs_data_get_string(settings, "key"); - - const char *service_name = ""; - if (is_custom && obs_data_has_user_value(settings, "service_name")) { - service_name = obs_data_get_string(settings, "service_name"); - } else if (!is_custom) { - service_name = obs_data_get_string(settings, "service"); - } - - std::optional custom_rtmp_url; - std::optional use_rtmps; - auto server = obs_data_get_string(settings, "server"); - if (strncmp(server, "auto", 4) != 0) { - custom_rtmp_url = server; - } else { - QString server_ = server; - use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); - } - - auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); - if (custom_rtmp_url.has_value()) { - blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); - } - - auto maximum_aggregate_bitrate = - config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") - ? std::nullopt - : std::make_optional( - config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); - - auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") - ? std::nullopt - : std::make_optional(config_get_int( - main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); - - auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); - - auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, - continuation = - std::move(continuation)](std::optional error) { - if (error) { - OBSDataAutoRelease service_settings = obs_service_get_settings(service); - auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); - if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { - multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); - } - - multitrackVideoActive = false; - if (!error->ShowDialog(main, multitrack_video_name)) - return continuation(false); - return continuation(std::nullopt); - } - - multitrackVideoActive = true; - - auto signal_handler = multitrackVideo->StreamingSignalHandler(); - - streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); - streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); - - startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); - stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); - return continuation(true); - }; - - QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), - service_name = std::string{service_name}, service = OBSService{service}, - stream_dump_config = OBSData{stream_dump_config}, - start_streaming_guard = start_streaming_guard]() mutable { - std::optional error; - try { - multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, - audio_encoder_id.c_str(), maximum_aggregate_bitrate, - maximum_video_tracks, custom_config, stream_dump_config, - main_audio_mixer, vod_track_mixer, use_rtmps); - } catch (const MultitrackVideoError &error_) { - error.emplace(error_); - } - - QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); - }); - - return start_streaming_guard->GetFuture(); -} - -OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() -{ - auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); - - if (!stream_dump_enabled) - return nullptr; - - const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); - bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); - const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); - bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); - bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); - - string f; - - OBSDataAutoRelease settings = obs_data_create(); - f = GetFormatString(filenameFormat, nullptr, nullptr); - string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), - // never remux stream dump - false); - obs_data_set_string(settings, "path", strPath.c_str()); - - if (useMP4) { - obs_data_set_bool(settings, "use_mp4", true); - obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); - } - - return settings; -} - -BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) -{ - return new SimpleOutput(main); -} - -BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) -{ - return new AdvancedOutput(main); -} diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp deleted file mode 100644 index bb24f4bcd..000000000 --- a/UI/window-basic-main-scene-collections.cpp +++ /dev/null @@ -1,720 +0,0 @@ -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include "item-widget-helpers.hpp" -#include "window-basic-main.hpp" -#include "window-importer.hpp" -#include "window-namedialog.hpp" - -// MARK: Constant Expressions - -constexpr std::string_view OBSSceneCollectionPath = "/obs-studio/basic/scenes/"; - -// MARK: - Anonymous Namespace -namespace { -QList sortedSceneCollections{}; - -void updateSortedSceneCollections(const OBSSceneCollectionCache &collections) -{ - const QLocale locale = QLocale::system(); - QList newList{}; - - for (auto [collectionName, _] : collections) { - QString entry = QString::fromStdString(collectionName); - newList.append(entry); - } - - std::sort(newList.begin(), newList.end(), [&locale](const QString &lhs, const QString &rhs) -> bool { - int result = QString::localeAwareCompare(locale.toLower(lhs), locale.toLower(rhs)); - - return (result < 0); - }); - - sortedSceneCollections.swap(newList); -} - -void cleanBackupCollision(const OBSSceneCollection &collection) -{ - std::filesystem::path backupFilePath = collection.collectionFile; - backupFilePath.replace_extension(".json.bak"); - - if (std::filesystem::exists(backupFilePath)) { - try { - std::filesystem::remove(backupFilePath); - } catch (std::filesystem::filesystem_error &) { - throw std::logic_error("Failed to remove pre-existing scene collection backup file: " + - backupFilePath.u8string()); - } - } -} -} // namespace - -// MARK: - Main Scene Collection Management Functions - -void OBSBasic::SetupNewSceneCollection(const std::string &collectionName) -{ - const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); - - cleanBackupCollision(newCollection); - ActivateSceneCollection(newCollection); - - blog(LOG_INFO, "Created scene collection '%s' (clean, %s)", newCollection.name.c_str(), - newCollection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::SetupDuplicateSceneCollection(const std::string &collectionName) -{ - const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - SaveProjectNow(); - - const auto copyOptions = std::filesystem::copy_options::overwrite_existing; - - try { - std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_DEBUG, "%s", error.what()); - throw std::logic_error("Failed to copy file for cloned scene collection: " + newCollection.name); - } - - OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str()); - - obs_data_set_string(collection, "name", newCollection.name.c_str()); - - OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources"); - - if (sources) { - obs_data_erase(collection, "sources"); - - obs_data_array_enum( - sources, - [](obs_data_t *data, void *) -> void { - const char *uuid = os_generate_uuid(); - - obs_data_set_string(data, "uuid", uuid); - - bfree((void *)uuid); - }, - nullptr); - - obs_data_set_array(collection, "sources", sources); - } - - obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr); - - cleanBackupCollision(newCollection); - ActivateSceneCollection(newCollection); - - blog(LOG_INFO, "Created scene collection '%s' (duplicate, %s)", newCollection.name.c_str(), - newCollection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::SetupRenameSceneCollection(const std::string &collectionName) -{ - const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); - const OBSSceneCollection currentCollection = GetCurrentSceneCollection(); - - SaveProjectNow(); - - const auto copyOptions = std::filesystem::copy_options::overwrite_existing; - - try { - std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_DEBUG, "%s", error.what()); - throw std::logic_error("Failed to copy file for scene collection: " + currentCollection.name); - } - - collections.erase(currentCollection.name); - - OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str()); - - obs_data_set_string(collection, "name", newCollection.name.c_str()); - - obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr); - - cleanBackupCollision(newCollection); - ActivateSceneCollection(newCollection); - RemoveSceneCollection(currentCollection); - - blog(LOG_INFO, "Renamed scene collection '%s' to '%s' (%s)", currentCollection.name.c_str(), - newCollection.name.c_str(), newCollection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_RENAMED); -} - -// MARK: - Scene Collection File Management Functions - -const OBSSceneCollection &OBSBasic::CreateSceneCollection(const std::string &collectionName) -{ - if (const auto &foundCollection = GetSceneCollectionByName(collectionName)) { - throw std::invalid_argument("Scene collection already exists: " + collectionName); - } - - std::string fileName; - if (!GetFileSafeName(collectionName.c_str(), fileName)) { - throw std::invalid_argument("Failed to create safe directory for new scene collection: " + - collectionName); - } - - std::string collectionFile; - collectionFile.reserve(App()->userScenesLocation.u8string().size() + OBSSceneCollectionPath.size() + - fileName.size()); - collectionFile.append(App()->userScenesLocation.u8string()).append(OBSSceneCollectionPath).append(fileName); - - if (!GetClosestUnusedFileName(collectionFile, "json")) { - throw std::invalid_argument("Failed to get closest file name for new scene collection: " + fileName); - } - - const std::filesystem::path collectionFilePath = std::filesystem::u8path(collectionFile); - - auto [iterator, success] = collections.try_emplace( - collectionName, - OBSSceneCollection{collectionName, collectionFilePath.filename().u8string(), collectionFilePath}); - - return iterator->second; -} - -void OBSBasic::RemoveSceneCollection(OBSSceneCollection collection) -{ - try { - std::filesystem::remove(collection.collectionFile); - } catch (const std::filesystem::filesystem_error &error) { - blog(LOG_DEBUG, "%s", error.what()); - throw std::logic_error("Failed to remove scene collection file: " + collection.fileName); - } - - blog(LOG_INFO, "Removed scene collection '%s' (%s)", collection.name.c_str(), collection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); -} - -// MARK: - Scene Collection UI Handling Functions - -bool OBSBasic::CreateNewSceneCollection(const QString &name) -{ - try { - SetupNewSceneCollection(name.toStdString()); - return true; - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } -} - -bool OBSBasic::CreateDuplicateSceneCollection(const QString &name) -{ - try { - SetupDuplicateSceneCollection(name.toStdString()); - return true; - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } -} - -void OBSBasic::DeleteSceneCollection(const QString &name) -{ - const std::string_view currentCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - - if (currentCollectionName == name.toStdString()) { - on_actionRemoveSceneCollection_triggered(); - return; - } - - OBSSceneCollection currentCollection = GetCurrentSceneCollection(); - - RemoveSceneCollection(currentCollection); - - collections.erase(name.toStdString()); - - RefreshSceneCollections(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); -} - -void OBSBasic::ChangeSceneCollection() -{ - QAction *action = reinterpret_cast(sender()); - - if (!action) { - return; - } - - const std::string_view currentCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const QVariant qCollectionName = action->property("collection_name"); - const std::string selectedCollectionName{qCollectionName.toString().toStdString()}; - - if (currentCollectionName == selectedCollectionName) { - action->setChecked(true); - return; - } - - const std::optional foundCollection = GetSceneCollectionByName(selectedCollectionName); - - if (!foundCollection) { - const std::string errorMessage{"Selected scene collection not found: "}; - - throw std::invalid_argument(errorMessage + currentCollectionName.data()); - } - - const OBSSceneCollection &selectedCollection = foundCollection.value(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); - - ActivateSceneCollection(selectedCollection); - - blog(LOG_INFO, "Switched to scene collection '%s' (%s)", selectedCollection.name.c_str(), - selectedCollection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::RefreshSceneCollections(bool refreshCache) -{ - std::string_view currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - - QList menuActions = ui->sceneCollectionMenu->actions(); - - for (auto &action : menuActions) { - QVariant variant = action->property("file_name"); - if (variant.typeName() != nullptr) { - delete action; - } - } - - if (refreshCache) { - RefreshSceneCollectionCache(); - } - - updateSortedSceneCollections(collections); - - size_t numAddedCollections = 0; - for (auto &name : sortedSceneCollections) { - const std::string collectionName = name.toStdString(); - try { - const OBSSceneCollection &collection = collections.at(collectionName); - const QString qCollectionName = QString().fromStdString(collectionName); - - QAction *action = new QAction(qCollectionName, this); - action->setProperty("collection_name", qCollectionName); - action->setProperty("file_name", QString().fromStdString(collection.fileName)); - connect(action, &QAction::triggered, this, &OBSBasic::ChangeSceneCollection); - action->setCheckable(true); - action->setChecked(collectionName == currentCollectionName); - - ui->sceneCollectionMenu->addAction(action); - - numAddedCollections += 1; - } catch (const std::out_of_range &error) { - blog(LOG_ERROR, "No scene collection with name %s found in scene collection cache.\n%s", - collectionName.c_str(), error.what()); - } - } - - ui->actionRemoveSceneCollection->setEnabled(numAddedCollections > 1); - - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - main->ui->actionPasteFilters->setEnabled(false); - main->ui->actionPasteRef->setEnabled(false); - main->ui->actionPasteDup->setEnabled(false); -} - -// MARK: - Scene Collection Cache Functions - -void OBSBasic::RefreshSceneCollectionCache() -{ - OBSSceneCollectionCache foundCollections{}; - - const std::filesystem::path collectionsPath = - App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath.substr(1)); - - if (!std::filesystem::exists(collectionsPath)) { - blog(LOG_WARNING, "Failed to get scene collections config path"); - return; - } - - for (const auto &entry : std::filesystem::directory_iterator(collectionsPath)) { - if (entry.is_directory()) { - continue; - } - - if (entry.path().extension().u8string() != ".json") { - continue; - } - - OBSDataAutoRelease collectionData = - obs_data_create_from_json_file_safe(entry.path().u8string().c_str(), "bak"); - - std::string candidateName; - const char *collectionName = obs_data_get_string(collectionData, "name"); - - if (!collectionName) { - candidateName = entry.path().filename().u8string(); - } else { - candidateName = collectionName; - } - - foundCollections.try_emplace(candidateName, - OBSSceneCollection{candidateName, entry.path().filename().u8string(), - entry.path()}); - } - - collections.swap(foundCollections); -} - -const OBSSceneCollection &OBSBasic::GetCurrentSceneCollection() const -{ - std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - - if (currentCollectionName.empty()) { - throw std::invalid_argument("No valid scene collection name in configuration Basic->SceneCollection"); - } - - const auto &foundCollection = collections.find(currentCollectionName); - - if (foundCollection != collections.end()) { - return foundCollection->second; - } else { - throw std::invalid_argument("Scene collection not found in collection list: " + currentCollectionName); - } -} - -std::optional OBSBasic::GetSceneCollectionByName(const std::string &collectionName) const -{ - auto foundCollection = collections.find(collectionName); - - if (foundCollection == collections.end()) { - return {}; - } else { - return foundCollection->second; - } -} - -std::optional OBSBasic::GetSceneCollectionByFileName(const std::string &fileName) const -{ - for (auto &[iterator, collection] : collections) { - if (collection.fileName == fileName) { - return collection; - } - } - - return {}; -} - -// MARK: - Qt Slot Functions - -void OBSBasic::on_actionNewSceneCollection_triggered() -{ - const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { - if (GetSceneCollectionByName(result.promptValue)) { - return false; - } - - return true; - }; - - const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"), - Str("Basic.Main.AddSceneCollection.Text")}; - - OBSPromptResult result = PromptForName(request, sceneCollectionCallback); - - if (!result.success) { - return; - } - - try { - SetupNewSceneCollection(result.promptValue); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -void OBSBasic::on_actionDupSceneCollection_triggered() -{ - const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { - if (GetSceneCollectionByName(result.promptValue)) { - return false; - } - - return true; - }; - - const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"), - Str("Basic.Main.AddSceneCollection.Text")}; - - OBSPromptResult result = PromptForName(request, sceneCollectionCallback); - - if (!result.success) { - return; - } - - try { - SetupDuplicateSceneCollection(result.promptValue); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -void OBSBasic::on_actionRenameSceneCollection_triggered() -{ - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { - if (GetSceneCollectionByName(result.promptValue)) { - return false; - } - - return true; - }; - - const OBSPromptRequest request{Str("Basic.Main.RenameSceneCollection.Title"), - Str("Basic.Main.AddSceneCollection.Text"), currentCollection.name}; - - OBSPromptResult result = PromptForName(request, sceneCollectionCallback); - - if (!result.success) { - return; - } - - try { - SetupRenameSceneCollection(result.promptValue); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -void OBSBasic::on_actionRemoveSceneCollection_triggered(bool skipConfirmation) -{ - if (collections.size() < 2) { - return; - } - - OBSSceneCollection currentCollection; - - try { - currentCollection = GetCurrentSceneCollection(); - - if (!skipConfirmation) { - const QString confirmationText = - QTStr("ConfirmRemove.Text").arg(QString::fromStdString(currentCollection.name)); - const QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmRemove.Title"), confirmationText); - - if (button == QMessageBox::No) { - return; - } - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); - - collections.erase(currentCollection.name); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } catch (const std::logic_error &error) { - blog(LOG_ERROR, "%s", error.what()); - } - - const OBSSceneCollection &newCollection = collections.begin()->second; - - ActivateSceneCollection(newCollection); - RemoveSceneCollection(currentCollection); - - blog(LOG_INFO, "Switched to scene collection '%s' (%s)", newCollection.name.c_str(), - newCollection.fileName.c_str()); - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::on_actionImportSceneCollection_triggered() -{ - OBSImporter imp(this); - imp.exec(); - - RefreshSceneCollections(true); -} - -void OBSBasic::on_actionExportSceneCollection_triggered() -{ - SaveProjectNow(); - - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - const QString home = QDir::homePath(); - - const QString destinationFileName = SaveFile(this, QTStr("Basic.MainMenu.SceneCollection.Export"), - home + "/" + currentCollection.fileName.c_str(), - "JSON Files (*.json)"); - - if (!destinationFileName.isEmpty() && !destinationFileName.isNull()) { - const std::filesystem::path sourceFile = currentCollection.collectionFile; - const std::filesystem::path destinationFile = - std::filesystem::u8path(destinationFileName.toStdString()); - - OBSDataAutoRelease collection = obs_data_create_from_json_file(sourceFile.u8string().c_str()); - - OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources"); - if (!sources) { - blog(LOG_WARNING, "No sources in exported scene collection"); - return; - } - - obs_data_erase(collection, "sources"); - - using OBSDataVector = std::vector; - - OBSDataVector sourceItems; - obs_data_array_enum( - sources, - [](obs_data_t *data, void *vector) -> void { - OBSDataVector &sourceItems{*static_cast(vector)}; - sourceItems.push_back(data); - }, - &sourceItems); - - std::sort(sourceItems.begin(), sourceItems.end(), [](const OBSData &a, const OBSData &b) { - return astrcmpi(obs_data_get_string(a, "name"), obs_data_get_string(b, "name")) < 0; - }); - - OBSDataArrayAutoRelease newSources = obs_data_array_create(); - for (auto &item : sourceItems) { - obs_data_array_push_back(newSources, item); - } - - obs_data_set_array(collection, "sources", newSources); - obs_data_save_json_pretty_safe(collection, destinationFile.u8string().c_str(), "tmp", "bak"); - } -} - -void OBSBasic::on_actionRemigrateSceneCollection_triggered() -{ - if (Active()) { - OBSMessageBox::warning(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), - QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.Active")); - return; - } - - OBSDataAutoRelease priv = obs_get_private_data(); - - if (!usingAbsoluteCoordinates && !migrationBaseResolution) { - OBSMessageBox::warning( - this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), - QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.UnknownBaseResolution")); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - if (!usingAbsoluteCoordinates && migrationBaseResolution->first == ovi.base_width && - migrationBaseResolution->second == ovi.base_height) { - OBSMessageBox::warning( - this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), - QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.BaseResolutionMatches")); - return; - } - - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - QString name = QString::fromStdString(currentCollection.name); - QString message = - QTStr("Basic.Main.RemigrateSceneCollection.Text").arg(name).arg(ovi.base_width).arg(ovi.base_height); - - auto answer = OBSMessageBox::question(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), message); - - if (answer == QMessageBox::No) - return; - - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - if (!usingAbsoluteCoordinates) { - /* Temporarily change resolution to migration resolution */ - ovi.base_width = migrationBaseResolution->first; - ovi.base_height = migrationBaseResolution->second; - - if (obs_reset_video(&ovi) != OBS_VIDEO_SUCCESS) { - OBSMessageBox::critical( - this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), - QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.FailedVideoReset")); - return; - } - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); - - /* Save and immediately reload to (re-)run migrations. */ - SaveProjectNow(); - /* Reset video if we potentially changed to a temporary resolution */ - if (!usingAbsoluteCoordinates) { - ResetVideo(); - } - - ActivateSceneCollection(currentCollection); -} - -// MARK: - Scene Collection Management Helper Functions - -void OBSBasic::ActivateSceneCollection(const OBSSceneCollection &collection) -{ - const std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - - if (auto foundCollection = GetSceneCollectionByName(currentCollectionName)) { - if (collection.name != foundCollection.value().name) { - SaveProjectNow(); - } - } - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", collection.name.c_str()); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", collection.fileName.c_str()); - - Load(collection.collectionFile.u8string().c_str()); - - RefreshSceneCollections(); - - UpdateTitleBar(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); -} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp deleted file mode 100644 index 47cac8dcc..000000000 --- a/UI/window-basic-main.cpp +++ /dev/null @@ -1,10203 +0,0 @@ -/****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Zachary Lund - Philippe Groarke - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ -#include "ui-config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "platform.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "basic-controls.hpp" -#include "window-basic-settings.hpp" -#include "window-namedialog.hpp" -#include "window-basic-auto-config.hpp" -#include "window-basic-source-select.hpp" -#include "window-basic-main.hpp" -#include "window-basic-stats.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "window-log-reply.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-projector.hpp" -#include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif -#include "window-whats-new.hpp" -#include "context-bar-controls.hpp" -#include "obs-proxy-style.hpp" -#include "display-helpers.hpp" -#include "volume-control.hpp" -#include "remote-text.hpp" -#include "ui-validation.hpp" -#include "media-controls.hpp" -#include "undo-stack-obs.hpp" -#include -#include - -#ifdef _WIN32 -#include "update/win-update.hpp" -#include "update/shared-update.hpp" -#include "windows.h" -#endif - -#ifdef WHATSNEW_ENABLED -#include "update/models/whatsnew.hpp" -#endif - -#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) -#include "update/shared-update.hpp" -#endif - -#ifdef ENABLE_SPARKLE_UPDATER -#include "update/mac-update.hpp" -#endif - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" - -#include - -#ifdef ENABLE_WAYLAND -#include -#endif - -using namespace std; - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "ui-config.h" - -struct QCef; -struct QCefCookieManager; - -QCef *cef = nullptr; -QCefCookieManager *panel_cookies = nullptr; -bool cef_js_avail = false; - -extern std::string opt_starting_profile; -extern std::string opt_starting_collection; - -void DestroyPanelCookieManager(); - -namespace { - -QPointer obsWhatsNew; - -template struct SignalContainer { - OBSRef ref; - vector> handlers; -}; -} // namespace - -extern volatile long insideEventLoop; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(obs_order_movement); -Q_DECLARE_METATYPE(SignalContainer); - -QDataStream &operator<<(QDataStream &out, const SignalContainer &v) -{ - out << v.ref; - return out; -} - -QDataStream &operator>>(QDataStream &in, SignalContainer &v) -{ - in >> v.ref; - return in; -} - -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - -template static void SetOBSRef(QListWidgetItem *item, T &&val) -{ - item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); -} - -static void AddExtraModulePaths() -{ - string plugins_path, plugins_data_path; - char *s; - - s = getenv("OBS_PLUGINS_PATH"); - if (s) - plugins_path = s; - - s = getenv("OBS_PLUGINS_DATA_PATH"); - if (s) - plugins_data_path = s; - - if (!plugins_path.empty() && !plugins_data_path.empty()) { -#if defined(__APPLE__) - plugins_path += "/%module%.plugin/Contents/MacOS"; - plugins_data_path += "/%module%.plugin/Contents/Resources"; - obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); -#else - string data_path_with_module_suffix; - data_path_with_module_suffix += plugins_data_path; - data_path_with_module_suffix += "/%module%"; - obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); -#endif - } - - if (portable_mode) - return; - - char base_module_dir[512]; -#if defined(_WIN32) - int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#elif defined(__APPLE__) - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); -#else - int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); -#endif - - if (ret <= 0) - return; - - string path = base_module_dir; -#if defined(__APPLE__) - /* User Application Support Search Path */ - obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); - -#ifndef __aarch64__ - /* Legacy System Library Search Path */ - char system_legacy_module_dir[PATH_MAX]; - GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_system_legacy = system_legacy_module_dir; - obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); - - /* Legacy User Application Support Search Path */ - char user_legacy_module_dir[PATH_MAX]; - GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); - std::string path_user_legacy = user_legacy_module_dir; - obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); -#endif -#else -#if ARCH_BITS == 64 - obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); -#else - obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); -#endif -#endif -} - -/* First-party modules considered to be potentially unsafe to load in Safe Mode - * due to them allowing external code (e.g. scripts) to modify OBS's state. */ -static const unordered_set unsafe_modules = { - "frontend-tools", // Scripting - "obs-websocket", // Allows outside modifications -}; - -static void SetSafeModuleNames() -{ -#ifndef SAFE_MODULES - return; -#else - string module; - stringstream modules(SAFE_MODULES); - - while (getline(modules, module, '|')) { - /* When only disallowing third-party plugins, still add - * "unsafe" bundled modules to the safe list. */ - if (disable_3p_plugins || !unsafe_modules.count(module)) - obs_add_safe_module(module.c_str()); - } -#endif -} - -extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); - -void assignDockToggle(QDockWidget *dock, QAction *action) -{ - auto handleWindowToggle = [action](bool vis) { - action->blockSignals(true); - action->setChecked(vis); - action->blockSignals(false); - }; - auto handleMenuToggle = [dock](bool check) { - dock->blockSignals(true); - dock->setVisible(check); - dock->blockSignals(false); - }; - - dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); - dock->connect(action, &QAction::toggled, handleMenuToggle); -} - -void setupDockAction(QDockWidget *dock) -{ - QAction *action = dock->toggleViewAction(); - - auto neverDisable = [action]() { - QSignalBlocker block(action); - action->setEnabled(true); - }; - - auto newToggleView = [dock](bool check) { - QSignalBlocker block(dock); - dock->setVisible(check); - }; - - // Replace the slot connected by default - QObject::disconnect(action, &QAction::triggered, nullptr, 0); - dock->connect(action, &QAction::triggered, newToggleView); - - // Make the action unable to be disabled - action->connect(action, &QAction::enabledChanged, neverDisable); -} - -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - -OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) -{ - setAttribute(Qt::WA_NativeWindow); - -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - - setAcceptDrops(true); - - setContextMenuPolicy(Qt::CustomContextMenu); - - QEvent::registerEventType(QEvent::User + QEvent::Close); - - api = InitializeAPIInterface(this); - - ui->setupUi(this); - ui->previewDisabledWidget->setVisible(false); - - /* Set up streaming connections */ - connect( - this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, - Qt::DirectConnection); - - /* Set up recording connections */ - connect( - this, &OBSBasic::RecordingStarted, this, - [this]() { - this->recordingStarted = true; - this->recordingPaused = false; - }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, - Qt::DirectConnection); - connect( - this, &OBSBasic::RecordingStopped, this, - [this]() { - this->recordingStarted = false; - this->recordingPaused = false; - }, - Qt::DirectConnection); - - /* Add controls dock */ - OBSBasicControls *controls = new OBSBasicControls(this); - controlsDock = new OBSDock(this); - controlsDock->setObjectName(QString::fromUtf8("controlsDock")); - controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); - /* Parenting is done there so controls will be deleted alongside controlsDock */ - controlsDock->setWidget(controls); - addDockWidget(Qt::BottomDockWidgetArea, controlsDock); - - connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); - - connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); - connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); - connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); - - connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); - - connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); - connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); - - connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); - connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); - - connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); - connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); - - connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); - - connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); - - connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); - - startingDockLayout = saveState(); - - statsDock = new OBSDock(); - statsDock->setObjectName(QStringLiteral("statsDock")); - statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - statsDock->setWindowTitle(QTStr("Basic.Stats")); - addDockWidget(Qt::BottomDockWidgetArea, statsDock); - statsDock->setVisible(false); - statsDock->setFloating(true); - statsDock->resize(700, 200); - - copyActionsDynamicProperties(); - - qRegisterMetaType("int64_t"); - qRegisterMetaType("uint32_t"); - qRegisterMetaType("OBSScene"); - qRegisterMetaType("OBSSceneItem"); - qRegisterMetaType("OBSSource"); - qRegisterMetaType("obs_hotkey_id"); - qRegisterMetaType("SavedProjectorInfo *"); - - ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); - ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); - - bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); - ui->scenes->SetGridMode(sceneGrid); - - if (sceneGrid) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - - UpdateContextBarVisibility(); - UpdatePreviewScrollbars(); - dpi = devicePixelRatioF(); - }; - dpi = devicePixelRatioF(); - - connect(windowHandle(), &QWindow::screenChanged, displayResize); - connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); - - /* TODO: Move these into window-basic-preview */ - /* Preview Scaling label */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, - &OBSPreviewScalingLabel::PreviewScaleChanged); - - /* Preview Scaling dropdown */ - connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewScaleChanged); - - connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, - &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); - - connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, - &OBSBasic::PreviewScalingModeChanged); - - connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); - connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); - - delete shortcutFilter; - shortcutFilter = CreateShortcutFilter(); - installEventFilter(shortcutFilter); - - stringstream name; - name << "OBS " << App()->GetVersionString(); - blog(LOG_INFO, "%s", name.str().c_str()); - blog(LOG_INFO, "---------------------------------"); - - UpdateTitleBar(); - - connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); - - cpuUsageInfo = os_cpu_usage_info_start(); - cpuUsageTimer = new QTimer(this); - connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); - cpuUsageTimer->start(3000); - - diskFullTimer = new QTimer(this); - connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); - - renameScene = new QAction(QTStr("Rename"), ui->scenesDock); - renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); - ui->scenesDock->addAction(renameScene); - - renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); - renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); - connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); - ui->sourcesDock->addAction(renameSource); - -#ifdef __APPLE__ - renameScene->setShortcut({Qt::Key_Return}); - renameSource->setShortcut({Qt::Key_Return}); - - ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); - ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); - - ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); - ui->action_Settings->setMenuRole(QAction::PreferencesRole); - ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); - ui->actionE_xit->setMenuRole(QAction::QuitRole); -#else - renameScene->setShortcut({Qt::Key_F2}); - renameSource->setShortcut({Qt::Key_F2}); -#endif - -#ifdef __linux__ - ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); -#endif - - auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { - QAction *nudge = new QAction(ui->preview); - nudge->setShortcut(seq); - nudge->setShortcutContext(Qt::WidgetShortcut); - ui->preview->addAction(nudge); - connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); - }; - - addNudge(Qt::Key_Up, MoveDir::Up, 1); - addNudge(Qt::Key_Down, MoveDir::Down, 1); - addNudge(Qt::Key_Left, MoveDir::Left, 1); - addNudge(Qt::Key_Right, MoveDir::Right, 1); - addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); - addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); - addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); - addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); - - /* Setup dock toggle action - * And hide all docks before restoring parent geometry */ -#define SETUP_DOCK(dock) \ - setupDockAction(dock); \ - ui->menuDocks->addAction(dock->toggleViewAction()); \ - dock->setVisible(false); - - SETUP_DOCK(ui->scenesDock); - SETUP_DOCK(ui->sourcesDock); - SETUP_DOCK(ui->mixerDock); - SETUP_DOCK(ui->transitionsDock); - SETUP_DOCK(controlsDock); - SETUP_DOCK(statsDock); -#undef SETUP_DOCK - - // Register shortcuts for Undo/Redo - ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); - QList shrt; - shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); - ui->actionMainRedo->setShortcuts(shrt); - - ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); - ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); - - QPoint curPos; - - //restore parent window geometry - const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); - if (geometry != NULL) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); - restoreGeometry(byteArray); - - QRect windowGeometry = normalGeometry(); - if (!WindowPositionValid(windowGeometry)) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - curPos = pos(); - } else { - QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); - QSize adjSize = desktopRect.size() / 2 - size() / 2; - curPos = QPoint(adjSize.width(), adjSize.height()); - } - - QPoint curSize(width(), height()); - - QPoint statsDockSize(statsDock->width(), statsDock->height()); - QPoint statsDockPos = curSize / 2 - statsDockSize / 2; - QPoint newPos = curPos + statsDockPos; - statsDock->move(newPos); - -#ifdef HAVE_OBSCONFIG_H - ui->actionReleaseNotes->setVisible(true); -#endif - - ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); - - connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); - - connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); - - QActionGroup *actionGroup = new QActionGroup(this); - actionGroup->addAction(ui->actionSceneListMode); - actionGroup->addAction(ui->actionSceneGridMode); - - UpdatePreviewSafeAreas(); - UpdatePreviewSpacingHelpers(); - UpdatePreviewOverflowSettings(); -} - -static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) -{ - OBSSourceAutoRelease source = obs_get_output_source(channel); - if (!source) - return; - - audioSources.push_back(source.Get()); - - OBSDataAutoRelease data = obs_save_source(source); - - obs_data_set_obj(parent, name, data); -} - -static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, - int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, - OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) -{ - obs_data_t *saveData = obs_data_create(); - - vector audioSources; - audioSources.reserve(6); - - SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); - SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); - SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); - - /* -------------------------------- */ - /* save non-group sources */ - - auto FilterAudioSources = [&](obs_source_t *source) { - if (obs_source_is_group(source)) - return false; - - return find(begin(audioSources), end(audioSources), source) == end(audioSources); - }; - using FilterAudioSources_t = decltype(FilterAudioSources); - - obs_data_array_t *sourcesArray = obs_save_sources_filtered( - [](void *data, obs_source_t *source) { - auto &func = *static_cast(data); - return func(source); - }, - static_cast(&FilterAudioSources)); - - /* -------------------------------- */ - /* save group sources separately */ - - /* saving separately ensures they won't be loaded in older versions */ - obs_data_array_t *groupsArray = obs_save_sources_filtered( - [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); - - /* -------------------------------- */ - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *currentScene = obs_scene_get_source(scene); - const char *sceneName = obs_source_get_name(currentScene); - const char *programName = obs_source_get_name(curProgramScene); - - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_string(saveData, "current_scene", sceneName); - obs_data_set_string(saveData, "current_program_scene", programName); - obs_data_set_array(saveData, "scene_order", sceneOrder); - obs_data_set_string(saveData, "name", sceneCollection); - obs_data_set_array(saveData, "sources", sourcesArray); - obs_data_set_array(saveData, "groups", groupsArray); - obs_data_set_array(saveData, "quick_transitions", quickTransitionData); - obs_data_set_array(saveData, "transitions", transitions); - obs_data_set_array(saveData, "saved_projectors", savedProjectorList); - obs_data_array_release(sourcesArray); - obs_data_array_release(groupsArray); - - obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); - obs_data_set_int(saveData, "transition_duration", transitionDuration); - - return saveData; -} - -void OBSBasic::copyActionsDynamicProperties() -{ - // Themes need the QAction dynamic properties - for (QAction *x : ui->scenesToolbar->actions()) { - QWidget *temp = ui->scenesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->sourcesToolbar->actions()) { - QWidget *temp = ui->sourcesToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } - - for (QAction *x : ui->mixerToolbar->actions()) { - QWidget *temp = ui->mixerToolbar->widgetForAction(x); - - if (!temp) - continue; - - for (QByteArray &y : x->dynamicPropertyNames()) { - temp->setProperty(y, x->property(y)); - } - } -} - -void OBSBasic::UpdateVolumeControlsDecayRate() -{ - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->SetMeterDecayRate(meterDecayRate); - } -} - -void OBSBasic::UpdateVolumeControlsPeakMeterType() -{ - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - for (size_t i = 0; i < volumes.size(); i++) { - volumes[i]->setPeakMeterType(peakMeterType); - } -} - -void OBSBasic::ClearVolumeControls() -{ - for (VolControl *vol : volumes) - delete vol; - - volumes.clear(); -} - -void OBSBasic::RefreshVolumeColors() -{ - for (VolControl *vol : volumes) { - vol->refreshColors(); - } -} - -obs_data_array_t *OBSBasic::SaveSceneListOrder() -{ - obs_data_array_t *sceneOrder = obs_data_array_create(); - - for (int i = 0; i < ui->scenes->count(); i++) { - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); - obs_data_array_push_back(sceneOrder, data); - } - - return sceneOrder; -} - -obs_data_array_t *OBSBasic::SaveProjectors() -{ - obs_data_array_t *savedProjectors = obs_data_array_create(); - - auto saveProjector = [savedProjectors](OBSProjector *projector) { - if (!projector) - return; - - OBSDataAutoRelease data = obs_data_create(); - ProjectorType type = projector->GetProjectorType(); - - switch (type) { - case ProjectorType::Scene: - case ProjectorType::Source: { - OBSSource source = projector->GetSource(); - const char *name = obs_source_get_name(source); - obs_data_set_string(data, "name", name); - break; - } - default: - break; - } - - obs_data_set_int(data, "monitor", projector->GetMonitor()); - obs_data_set_int(data, "type", static_cast(type)); - obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); - - if (projector->IsAlwaysOnTopOverridden()) - obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); - - obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); - - obs_data_array_push_back(savedProjectors, data); - }; - - for (size_t i = 0; i < projectors.size(); i++) - saveProjector(static_cast(projectors[i])); - - return savedProjectors; -} - -void OBSBasic::Save(const char *file) -{ - OBSScene scene = GetCurrentScene(); - OBSSource curProgramScene = OBSGetStrongRef(programScene); - if (!curProgramScene) - curProgramScene = obs_scene_get_source(scene); - - OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); - OBSDataArrayAutoRelease transitions = SaveTransitions(); - OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), - transitions, scene, curProgramScene, savedProjectorList); - - obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); - obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); - obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); - obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); - obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_create(); - - obs_data_set_int(obj, "type2", (int)vcamConfig.type); - switch (vcamConfig.type) { - case VCamOutputType::Invalid: - case VCamOutputType::ProgramView: - case VCamOutputType::PreviewOutput: - break; - case VCamOutputType::SceneOutput: - obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - obs_data_set_string(obj, "source", vcamConfig.source.c_str()); - break; - } - - obs_data_set_obj(saveData, "virtual-camera", obj); - } - - if (api) { - if (!collectionModuleData) - collectionModuleData = obs_data_create(); - - api->on_save(collectionModuleData); - obs_data_set_obj(saveData, "modules", collectionModuleData); - } - - if (lastOutputResolution) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", lastOutputResolution->first); - obs_data_set_int(res, "y", lastOutputResolution->second); - obs_data_set_obj(saveData, "resolution", res); - } - - obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); - - if (migrationBaseResolution && !usingAbsoluteCoordinates) { - OBSDataAutoRelease res = obs_data_create(); - obs_data_set_int(res, "x", migrationBaseResolution->first); - obs_data_set_int(res, "y", migrationBaseResolution->second); - obs_data_set_obj(saveData, "migration_resolution", res); - } - - if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) - blog(LOG_ERROR, "Could not save scene data to %s", file); -} - -void OBSBasic::DeferSaveBegin() -{ - os_atomic_inc_long(&disableSaving); -} - -void OBSBasic::DeferSaveEnd() -{ - long result = os_atomic_dec_long(&disableSaving); - if (result == 0) { - SaveProject(); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); - -static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) -{ - OBSDataAutoRelease data = obs_data_get_obj(parent, name); - if (!data) - return; - - OBSSourceAutoRelease source = obs_load_source(data); - if (!source) - return; - - obs_set_output_source(channel, source); - - const char *source_name = obs_source_get_name(source); - blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " - monitoring: %s", type); - } -} - -static inline bool HasAudioDevices(const char *source_id) -{ - const char *output_id = source_id; - obs_properties_t *props = obs_get_source_properties(output_id); - size_t count = 0; - - if (!props) - return false; - - obs_property_t *devices = obs_properties_get(props, "device_id"); - if (devices) - count = obs_property_list_item_count(devices); - - obs_properties_destroy(props); - - return count != 0; -} - -void OBSBasic::CreateFirstRunSources() -{ - bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); - bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); - -#ifdef __APPLE__ - /* On macOS 13 and above, the SCK based audio capture provides a - * better alternative to the device-based audio capture. */ - if (__builtin_available(macOS 13.0, *)) { - hasDesktopAudio = false; - } -#endif - - if (hasDesktopAudio) - ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); - if (hasInputAudio) - ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); -} - -void OBSBasic::DisableRelativeCoordinates(bool enable) -{ - /* Allow disabling relative positioning to allow loading collections - * that cannot yet be migrated. */ - OBSDataAutoRelease priv = obs_get_private_data(); - obs_data_set_bool(priv, "AbsoluteCoordinates", enable); - usingAbsoluteCoordinates = enable; - - ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") - : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); - ui->actionRemigrateSceneCollection->setEnabled(enable); -} - -void OBSBasic::CreateDefaultScene(bool firstStart) -{ - disableSaving++; - - ClearSceneData(); - InitDefaultTransitions(); - CreateDefaultQuickTransitions(); - ui->transitionDuration->setValue(300); - SetTransition(fadeTransition); - - DisableRelativeCoordinates(false); - OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); - - if (firstStart) - CreateFirstRunSources(); - - SetCurrentScene(scene, true); - - disableSaving--; -} - -static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) -{ - for (int i = 0; i < lw->count(); i++) { - QListWidgetItem *item = lw->item(i); - - if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { - if (newIndex != i) { - item = lw->takeItem(i); - lw->insertItem(newIndex, item); - } - break; - } - } -} - -void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) -{ - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - ReorderItemByName(ui->scenes, name, (int)i); - } -} - -void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - delete info; - } - savedProjectorsArray.clear(); - - size_t num = obs_data_array_count(array); - - for (size_t i = 0; i < num; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - - SavedProjectorInfo *info = new SavedProjectorInfo(); - info->monitor = obs_data_get_int(data, "monitor"); - info->type = static_cast(obs_data_get_int(data, "type")); - info->geometry = std::string(obs_data_get_string(data, "geometry")); - info->name = std::string(obs_data_get_string(data, "name")); - info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); - info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); - - savedProjectorsArray.emplace_back(info); - } -} - -static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) -{ - const char *name = obs_source_get_name(filter); - const char *id = obs_source_get_id(filter); - int val = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < val; i++) - indent += " "; - - blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); -} - -static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_id(source); - int indent_count = (int)(intptr_t)v_val; - string indent; - - for (int i = 0; i < indent_count; i++) - indent += " "; - - blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); - - obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); - - if (monitoring_type != OBS_MONITORING_TYPE_NONE) { - const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" - : "monitor and output"; - - blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); - } - int child_indent = 1 + indent_count; - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); - - obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); - obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); - if (show_tn) - blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), - obs_source_get_id(show_tn)); - if (hide_tn) - blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), - obs_source_get_id(hide_tn)); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); - return true; -} - -void OBSBasic::LogScenes() -{ - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Loaded scenes:"); - - for (int i = 0; i < ui->scenes->count(); i++) { - QListWidgetItem *item = ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - - obs_source_t *source = obs_scene_get_source(scene); - const char *name = obs_source_get_name(source); - - blog(LOG_INFO, "- scene '%s':", name); - obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); - obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); - } - - blog(LOG_INFO, "------------------------------------------------"); -} - -void OBSBasic::Load(const char *file, bool remigrate) -{ - disableSaving++; - lastOutputResolution.reset(); - migrationBaseResolution.reset(); - - obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); - if (!data) { - disableSaving--; - const auto path = filesystem::u8path(file); - const string name = path.stem().u8string(); - /* Check if file exists but failed to load. */ - if (filesystem::exists(path)) { - /* Assume the file is corrupt and rename it to allow - * for manual recovery if possible. */ - auto newPath = path; - newPath.concat(".invalid"); - - blog(LOG_WARNING, - "File exists but appears to be corrupt, renaming " - "to \"%s\" before continuing.", - newPath.filename().u8string().c_str()); - - error_code ec; - filesystem::rename(path, newPath, ec); - if (ec) { - blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); - } - } - - blog(LOG_INFO, "No scene file found, creating default scene"); - - bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - CreateDefaultScene(!hasFirstRun); - SaveProject(); - return; - } - - LoadData(data, file, remigrate); -} - -static inline void AddMissingFiles(void *data, obs_source_t *source) -{ - obs_missing_files_t *f = (obs_missing_files_t *)data; - obs_missing_files_t *sf = obs_source_get_missing_files(source); - - obs_missing_files_append(f, sf); - obs_missing_files_destroy(sf); -} - -static void ClearRelativePosCb(obs_data_t *data, void *) -{ - const string_view id = obs_data_get_string(data, "id"); - if (id != "scene" && id != "group") - return; - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - obs_data_array_enum( - items, - [](obs_data_t *data, void *) { - obs_data_unset_user_value(data, "pos_rel"); - obs_data_unset_user_value(data, "scale_rel"); - obs_data_unset_user_value(data, "scale_ref"); - obs_data_unset_user_value(data, "bounds_rel"); - }, - nullptr); -} - -void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) -{ - ClearSceneData(); - ClearContextBar(); - - /* Exit OBS if clearing scene data failed for some reason. */ - if (clearingFailed) { - OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); - close(); - return; - } - - InitDefaultTransitions(); - - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); - if (api) - api->on_preload(modulesObj); - - /* Keep a reference to "modules" data so plugins that are not loaded do - * not have their collection specific data lost. */ - collectionModuleData = obs_data_get_obj(data, "modules"); - - OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); - OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); - OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); - OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); - const char *sceneName = obs_data_get_string(data, "current_scene"); - const char *programSceneName = obs_data_get_string(data, "current_program_scene"); - const char *transitionName = obs_data_get_string(data, "current_transition"); - - if (!opt_starting_scene.empty()) { - programSceneName = opt_starting_scene.c_str(); - if (!IsPreviewProgramMode()) - sceneName = opt_starting_scene.c_str(); - } - - int newDuration = obs_data_get_int(data, "transition_duration"); - if (!newDuration) - newDuration = 300; - - if (!transitionName) - transitionName = obs_source_get_name(fadeTransition); - - const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - obs_data_set_default_string(data, "name", curSceneCollection); - - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease curScene; - OBSSourceAutoRelease curProgramScene; - obs_source_t *curTransition; - - if (!name || !*name) - name = curSceneCollection; - - LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); - LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); - LoadAudioDevice(AUX_AUDIO_1, 3, data); - LoadAudioDevice(AUX_AUDIO_2, 4, data); - LoadAudioDevice(AUX_AUDIO_3, 5, data); - LoadAudioDevice(AUX_AUDIO_4, 6, data); - - if (!sources) { - sources = std::move(groups); - } else { - obs_data_array_push_back_array(sources, groups); - } - - /* Reset relative coordinate data if forcefully remigrating. */ - if (remigrate) { - obs_data_set_int(data, "version", 1); - obs_data_array_enum(sources, ClearRelativePosCb, nullptr); - } - - bool resetVideo = false; - bool disableRelativeCoords = false; - obs_video_info ovi; - - int64_t version = obs_data_get_int(data, "version"); - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (res) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - - /* Only migrate legacy collection if resolution is saved. */ - if (version < 2 && lastOutputResolution) { - obs_get_video_info(&ovi); - - uint32_t width = obs_data_get_int(res, "x"); - uint32_t height = obs_data_get_int(res, "y"); - - migrationBaseResolution = {width, height}; - - if (ovi.base_height != height || ovi.base_width != width) { - ovi.base_width = width; - ovi.base_height = height; - - /* Attempt to reset to last known canvas resolution for migration. */ - resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; - disableRelativeCoords = !resetVideo; - } - - /* If migration is possible, and it wasn't forced, back up the original file. */ - if (!disableRelativeCoords && !remigrate) { - auto path = filesystem::u8path(file); - auto backupPath = path.concat(".v1"); - if (!filesystem::exists(backupPath)) { - if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { - blog(LOG_WARNING, - "Failed to create a backup of existing scene collection data!"); - } - } - } - } else if (version < 2) { - disableRelativeCoords = true; - } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { - migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; - } - - DisableRelativeCoordinates(disableRelativeCoords); - - obs_missing_files_t *files = obs_missing_files_create(); - obs_load_sources(sources, AddMissingFiles, files); - - if (resetVideo) - ResetVideo(); - if (transitions) - LoadTransitions(transitions, AddMissingFiles, files); - if (sceneOrder) - LoadSceneListOrder(sceneOrder); - - curTransition = FindTransition(transitionName); - if (!curTransition) - curTransition = fadeTransition; - - ui->transitionDuration->setValue(newDuration); - SetTransition(curTransition); - -retryScene: - curScene = obs_get_source_by_name(sceneName); - curProgramScene = obs_get_source_by_name(programSceneName); - - /* if the starting scene command line parameter is bad at all, - * fall back to original settings */ - if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { - sceneName = obs_data_get_string(data, "current_scene"); - programSceneName = obs_data_get_string(data, "current_program_scene"); - opt_starting_scene.clear(); - goto retryScene; - } - - if (!curScene) { - auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { - *static_cast(source_ptr) = obs_source_get_ref(scene); - return false; - }; - obs_enum_scenes(find_scene_cb, &curScene); - } - - SetCurrentScene(curScene.Get(), true); - - if (!curProgramScene) - curProgramScene = std::move(curScene); - if (IsPreviewProgramMode()) - TransitionToScene(curProgramScene.Get(), true); - - /* ------------------- */ - - bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); - - if (projectorSave) { - OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); - - if (savedProjectors) { - LoadSavedProjectors(savedProjectors); - OpenSavedProjectors(); - activateWindow(); - } - } - - /* ------------------- */ - - std::string file_base = strrchr(file, '/') + 1; - file_base.erase(file_base.size() - 5, 5); - - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); - config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); - - OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); - LoadQuickTransitions(quickTransitionData); - - RefreshQuickTransitions(); - - bool previewLocked = obs_data_get_bool(data, "preview_locked"); - ui->preview->SetLocked(previewLocked); - ui->actionLockPreview->setChecked(previewLocked); - - /* ---------------------- */ - - bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); - int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); - float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); - float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); - - if (fixedScaling) { - ui->preview->SetScalingLevel(scalingLevel); - ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); - } - ui->preview->SetFixedScaling(fixedScaling); - - emit ui->preview->DisplayResized(); - - if (vcamEnabled) { - OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); - - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); - if (vcamConfig.type == VCamOutputType::Invalid) - vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); - - if (vcamConfig.type == VCamOutputType::Invalid) { - VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); - - switch (internal) { - case VCamInternalType::Default: - vcamConfig.type = VCamOutputType::ProgramView; - break; - case VCamInternalType::Preview: - vcamConfig.type = VCamOutputType::PreviewOutput; - break; - } - } - vcamConfig.scene = obs_data_get_string(obj, "scene"); - vcamConfig.source = obs_data_get_string(obj, "source"); - } - - if (obs_data_has_user_value(data, "resolution")) { - OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); - if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { - lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; - } - } - - /* ---------------------- */ - - if (api) - api->on_load(modulesObj); - - obs_data_release(data); - - if (!opt_starting_scene.empty()) - opt_starting_scene.clear(); - - if (opt_start_streaming && !safe_mode) { - blog(LOG_INFO, "Starting stream due to command line parameter"); - QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); - opt_start_streaming = false; - } - - if (opt_start_recording && !safe_mode) { - blog(LOG_INFO, "Starting recording due to command line parameter"); - QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); - opt_start_recording = false; - } - - if (opt_start_replaybuffer && !safe_mode) { - QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); - opt_start_replaybuffer = false; - } - - if (opt_start_virtualcam && !safe_mode) { - QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); - opt_start_virtualcam = false; - } - - LogScenes(); - - if (!App()->IsMissingFilesCheckDisabled()) - ShowMissingFilesDialog(files); - - disableSaving--; - - if (vcamEnabled) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); -} - -constexpr std::string_view OBSServiceFileName = "service.json"; - -void OBSBasic::SaveService() -{ - if (!service) - return; - - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { - blog(LOG_WARNING, "Failed to save service"); - } -} - -bool OBSBasic::LoadService() -{ - OBSDataAutoRelease data; - - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = - currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - if (!data) { - return false; - } - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - return false; - } - - const char *type; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on WHIP if needed */ - if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - - option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); - - const char *encoder_codec = obs_get_encoder_codec(option); - if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - -static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; - -extern void CheckExistingCookieId(); - -#ifdef __APPLE__ -#define DEFAULT_CONTAINER "fragmented_mov" -#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 -#define DEFAULT_CONTAINER "mkv" -#else -#define DEFAULT_CONTAINER "hybrid_mp4" -#endif - -bool OBSBasic::InitBasicConfigDefaults() -{ - QList screens = QGuiApplication::screens(); - - if (!screens.size()) { - OBSErrorBox(NULL, "There appears to be no monitors. Er, this " - "technically shouldn't be possible."); - return false; - } - - QScreen *primaryScreen = QGuiApplication::primaryScreen(); - - uint32_t cx = primaryScreen->size().width(); - uint32_t cy = primaryScreen->size().height(); - - cx *= devicePixelRatioF(); - cy *= devicePixelRatioF(); - - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - - bool changed = false; - - /* ----------------------------------------------------- */ - /* move over old FFmpeg track settings */ - if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && - !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { - - int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move over mixer values in advanced if older config */ - if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { - - uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); - track = 1ULL << (track - 1); - config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); - config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); - changed = true; - } - - /* ----------------------------------------------------- */ - /* set twitch chat extensions to "both" if prev version */ - /* is under 24.1 */ - if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && - !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { - config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); - changed = true; - } - - /* ----------------------------------------------------- */ - /* move bitrate enforcement setting to new value */ - if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); - config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); - config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); - changed = true; - } - - /* ----------------------------------------------------- */ - /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { - int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); - if (retryDelay < 1) { - config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); - changed = true; - } - } - - /* ----------------------------------------------------- */ - /* Migrate old container selection (if any) to new key. */ - - auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); - bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); - if (!has_new_key && !has_old_key) - return; - - string old_format = - config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); - string new_format = old_format; - if (old_format == "ts") - new_format = "mpegts"; - else if (old_format == "m3u8") - new_format = "hls"; - else if (old_format == "fmp4") - new_format = "fragmented_mp4"; - else if (old_format == "fmov") - new_format = "fragmented_mov"; - - if (new_format != old_format || !has_new_key) { - config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); - changed = true; - } - }; - - MigrateFormat("AdvOut"); - MigrateFormat("SimpleOutput"); - - /* ----------------------------------------------------- */ - /* Migrate output scale setting to GPU scaling options. */ - - if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); - } - - if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && - !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { - config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); - } - - /* ----------------------------------------------------- */ - - if (changed) { - activeConfiguration.SaveSafe("tmp"); - } - - /* ----------------------------------------------------- */ - - config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); - - config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); - config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - - config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); - config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); - config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); - - config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); - config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); - - config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); - config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); - config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); - config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); - - config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); - - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); - - config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); - config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); - config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - - config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); - - /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || - !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { - config_set_uint(activeConfiguration, "Video", "BaseCX", cx); - config_set_uint(activeConfiguration, "Video", "BaseCY", cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - - config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); - config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); - config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); - - config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); - config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); - config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); - - config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); - config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); - config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); - - int i = 0; - uint32_t scale_cx = cx; - uint32_t scale_cy = cy; - - /* use a default scaled resolution that has a pixel count no higher - * than 1280x720 */ - while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { - double scale = scaled_vals[i++]; - scale_cx = uint32_t(double(cx) / scale); - scale_cy = uint32_t(double(cy) / scale); - } - - config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - - /* don't allow OutputCX/OutputCY to be susceptible to defaults - * changing */ - if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || - !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { - config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); - config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); - config_save_safe(activeConfiguration, "tmp", nullptr); - } - - config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); - config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); - config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); - config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); - config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); - config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); - config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); - config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); - - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); - config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", - Str("Basic.Settings.Advanced.Audio.MonitoringDevice" - ".Default")); - config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); - config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); - config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); - - CheckExistingCookieId(); - - return true; -} - -extern bool EncoderAvailable(const char *encoder); - -void OBSBasic::InitBasicConfigDefaults2() -{ - bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - - config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); - - const char *aac_default = "ffmpeg_aac"; - if (EncoderAvailable("CoreAudio_AAC")) - aac_default = "CoreAudio_AAC"; - else if (EncoderAvailable("libfdk_aac")) - aac_default = "libfdk_aac"; - - config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); - config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); -} - -bool OBSBasic::InitBasicConfig() -{ - ProfileScope("OBSBasic::InitBasicConfig"); - - RefreshProfiles(true); - - const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - const std::optional currentProfile = GetProfileByName(currentProfileName); - const std::optional foundProfile = GetProfileByName(opt_starting_profile); - - try { - if (foundProfile) { - ActivateProfile(foundProfile.value()); - } else if (currentProfile) { - ActivateProfile(currentProfile.value()); - } else { - const OBSProfile &newProfile = CreateProfile(currentProfileName); - ActivateProfile(newProfile); - } - } catch (const std::logic_error &) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); - return false; - } - - return true; -} - -void OBSBasic::InitOBSCallbacks() -{ - ProfileScope("OBSBasic::InitOBSCallbacks"); - - signalHandlers.reserve(signalHandlers.size() + 9); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, - this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", - OBSBasic::SourceAudioDeactivated, this); - signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_add", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); - signalHandlers.emplace_back( - obs_get_signal_handler(), "source_filter_remove", - [](void *data, calldata_t *) { - QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", - Qt::QueuedConnection); - }, - this); -} - -void OBSBasic::InitPrimitives() -{ - ProfileScope("OBSBasic::InitPrimitives"); - - obs_enter_graphics(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - box = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(0.0f, 1.0f); - boxLeft = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 0.0f); - gs_vertex2f(1.0f, 0.0f); - boxTop = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(1.0f, 0.0f); - gs_vertex2f(1.0f, 1.0f); - boxRight = gs_render_save(); - - gs_render_start(true); - gs_vertex2f(0.0f, 1.0f); - gs_vertex2f(1.0f, 1.0f); - boxBottom = gs_render_save(); - - gs_render_start(true); - for (int i = 0; i <= 360; i += (360 / 20)) { - float pos = RAD(float(i)); - gs_vertex2f(cosf(pos), sinf(pos)); - } - circle = gs_render_save(); - - InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); - obs_leave_graphics(); -} - -void OBSBasic::ReplayBufferActionTriggered() -{ - if (outputHandler->ReplayBufferActive()) - StopReplayBuffer(); - else - StartReplayBuffer(); -}; - -void OBSBasic::ResetOutputs() -{ - ProfileScope("OBSBasic::ResetOutputs"); - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool advOut = astrcmpi(mode, "Advanced") == 0; - - if ((!outputHandler || !outputHandler->Active()) && - (!setupStreamingGuard.valid() || - setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { - outputHandler.reset(); - outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); - - emit ReplayBufEnabled(outputHandler->replayBuffer); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); - - UpdateIsRecordingPausable(); - } else { - outputHandler->Update(); - } -} - -#define STARTUP_SEPARATOR "==== Startup complete ===============================================" -#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" - -#define UNSUPPORTED_ERROR \ - "Failed to initialize video:\n\nRequired graphics API functionality " \ - "not found. Your GPU may not be supported." - -#define UNKNOWN_ERROR \ - "Failed to initialize video. Your GPU may not be supported, " \ - "or your graphics drivers may need to be updated." - -static inline void LogEncoders() -{ - constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; - - auto list_encoders = [](obs_encoder_type type) { - size_t idx = 0; - const char *encoder_type; - - while (obs_enum_encoder_types(idx++, &encoder_type)) { - if (obs_get_encoder_caps(encoder_type) & hide_flags || - obs_get_encoder_type(encoder_type) != type) { - continue; - } - - blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); - } - }; - - blog(LOG_INFO, "---------------------------------"); - blog(LOG_INFO, "Available Encoders:"); - blog(LOG_INFO, " Video Encoders:"); - list_encoders(OBS_ENCODER_VIDEO); - blog(LOG_INFO, " Audio Encoders:"); - list_encoders(OBS_ENCODER_AUDIO); -} - -void OBSBasic::OBSInit() -{ - ProfileScope("OBSBasic::OBSInit"); - - if (!InitBasicConfig()) - throw "Failed to load basic.ini"; - if (!ResetAudio()) - throw "Failed to initialize audio"; - - int ret = 0; - - ret = ResetVideo(); - - switch (ret) { - case OBS_VIDEO_MODULE_NOT_FOUND: - throw "Failed to initialize video: Graphics module not found"; - case OBS_VIDEO_NOT_SUPPORTED: - throw UNSUPPORTED_ERROR; - case OBS_VIDEO_INVALID_PARAM: - throw "Failed to initialize video: Invalid parameters"; - default: - if (ret != OBS_VIDEO_SUCCESS) - throw UNKNOWN_ERROR; - } - - /* load audio monitoring */ - if (obs_audio_monitoring_available()) { - const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); - - obs_set_audio_monitoring_device(device_name, device_id); - - blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); - } - - InitOBSCallbacks(); - InitHotkeys(); - ui->preview->Init(); - - /* hack to prevent elgato from loading its own QtNetwork that it tries - * to ship with */ -#if defined(_WIN32) && !defined(_DEBUG) - LoadLibraryW(L"Qt6Network"); -#endif - struct obs_module_failure_info mfi; - - /* Safe Mode disables third-party plugins so we don't need to add earch - * paths outside the OBS bundle/installation. */ - if (safe_mode || disable_3p_plugins) { - SetSafeModuleNames(); - } else { - AddExtraModulePaths(); - } - - /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. - - Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. - */ - RefreshSceneCollections(true); - - blog(LOG_INFO, "---------------------------------"); - obs_load_all_modules2(&mfi); - blog(LOG_INFO, "---------------------------------"); - obs_log_loaded_modules(); - blog(LOG_INFO, "---------------------------------"); - obs_post_load_modules(); - - BPtr failed_modules = mfi.failed_modules; - -#ifdef BROWSER_AVAILABLE - cef = obs_browser_init_panel(); - cef_js_avail = cef && obs_browser_qcef_version() >= 3; -#endif - - vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; - if (vcamEnabled) { - emit VirtualCamEnabled(); - } - - UpdateProfileEncoders(); - - LogEncoders(); - - blog(LOG_INFO, STARTUP_SEPARATOR); - - if (!InitService()) - throw "Failed to initialize service"; - - ResetOutputs(); - CreateHotkeys(); - - InitPrimitives(); - - sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); - swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); - editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); - - if (!opt_studio_mode) { - SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); - } else { - SetPreviewProgramMode(true); - opt_studio_mode = false; - } - -#define SET_VISIBILITY(name, control) \ - do { \ - if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ - bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ - ui->control->setChecked(visible); \ - } \ - } while (false) - - SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); - SET_VISIBILITY("ShowStatusBar", toggleStatusBar); -#undef SET_VISIBILITY - - bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - ui->toggleSourceIcons->setChecked(sourceIconsVisible); - - bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); - ui->toggleContextBar->setChecked(contextVisible); - ui->contextContainer->setVisible(contextVisible); - if (contextVisible) - UpdateContextBar(true); - UpdateEditMenu(); - - { - ProfileScope("OBSBasic::Load"); - const std::string sceneCollectionName{ - config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; - const std::optional configuredCollection = - GetSceneCollectionByName(sceneCollectionName); - const std::optional foundCollection = - GetSceneCollectionByName(opt_starting_collection); - - if (foundCollection) { - ActivateSceneCollection(foundCollection.value()); - } else if (configuredCollection) { - ActivateSceneCollection(configuredCollection.value()); - } else { - disableSaving--; - SetupNewSceneCollection(sceneCollectionName); - disableSaving++; - } - - disableSaving--; - if (foundCollection || configuredCollection) { - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); - } - OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - disableSaving++; - } - - loaded = true; - - previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); - - if (!previewEnabled && !IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, - Q_ARG(bool, previewEnabled)); - else if (!previewEnabled && IsPreviewProgramMode()) - QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); - - disableSaving--; - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizePreview(ovi.base_width, ovi.base_height); - }; - - connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); - - /* Show the main window, unless the tray icon isn't available - * or neither the setting nor flag for starting minimized is set. */ - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && - (opt_minimize_tray || sysTrayWhenStarted); - -#ifdef _WIN32 - SetWin32DropStyle(this); - - if (!hideWindowOnStart) - show(); -#endif - - bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); - -#ifdef ENABLE_WAYLAND - bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; -#else - bool isWayland = false; -#endif - - if (!isWayland && (alwaysOnTop || opt_always_on_top)) { - SetAlwaysOnTop(this, true); - ui->actionAlwaysOnTop->setChecked(true); - } else if (isWayland) { - if (opt_always_on_top) - blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); - ui->actionAlwaysOnTop->setEnabled(false); - ui->actionAlwaysOnTop->setVisible(false); - } - -#ifndef _WIN32 - if (!hideWindowOnStart) - show(); -#endif - - /* setup stats dock */ - OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); - statsDock->setWidget(statsDlg); - - /* ----------------------------- */ - /* add custom browser docks */ -#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) - YouTubeAppDock::CleanupYouTubeUrls(); -#endif - -#ifdef BROWSER_AVAILABLE - if (cef) { - QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." - "CustomBrowserDocks"), - this); - ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); - connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); - ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); - - LoadExtraBrowserDocks(); - } -#endif - -#ifdef YOUTUBE_ENABLED - /* setup YouTube app dock */ - if (YouTubeAppDock::IsYTServiceSelected()) - NewYouTubeAppDock(); -#endif - - const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); - config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - - SystemTray(true); - - TaskbarOverlayInit(); - -#ifdef __APPLE__ - disableColorSpaceConversion(this); -#endif - - bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); - bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); - - if (!first_run) { - config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - - if (!first_run && !has_last_version && !Active()) - QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); - -#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) - /* Automatically set branch to "beta" the first time a pre-release build is run. */ - if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { - config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); - config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - } -#endif - TimedCheckForUpdates(); - - ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) - on_stats_triggered(); - - OBSBasicStats::InitializeValues(); - - /* ----------------------- */ - /* Add multiview menu */ - - ui->viewMenu->addSeparator(); - - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); - connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); - - ui->sources->UpdateIcons(); - -#if !defined(_WIN32) - delete ui->actionShowCrashLogs; - delete ui->actionUploadLastCrashLog; - delete ui->menuCrashLogs; - delete ui->actionRepair; - ui->actionShowCrashLogs = nullptr; - ui->actionUploadLastCrashLog = nullptr; - ui->menuCrashLogs = nullptr; - ui->actionRepair = nullptr; -#if !defined(__APPLE__) - delete ui->actionCheckForUpdates; - ui->actionCheckForUpdates = nullptr; -#endif -#endif - -#ifdef __APPLE__ - /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ - delete ui->actionFullscreenInterface; - ui->actionFullscreenInterface = nullptr; -#else - /* Don't show menu to raise macOS-only permissions dialog */ - delete ui->actionShowMacPermissions; - ui->actionShowMacPermissions = nullptr; -#endif - -#if defined(_WIN32) || defined(__APPLE__) - if (App()->IsUpdaterDisabled()) { - ui->actionCheckForUpdates->setEnabled(false); -#if defined(_WIN32) - ui->actionRepair->setEnabled(false); -#endif - } -#endif - -#ifndef WHATSNEW_ENABLED - delete ui->actionShowWhatsNew; - ui->actionShowWhatsNew = nullptr; -#endif - - if (safe_mode) { - ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); - } - - UpdatePreviewProgramIndicators(); - OnFirstLoad(); - - if (!hideWindowOnStart) - activateWindow(); - - /* ------------------------------------------- */ - /* display warning message for failed modules */ - - if (mfi.count) { - QString failed_plugins; - - char **plugin = mfi.failed_modules; - while (*plugin) { - failed_plugins += *plugin; - failed_plugins += "\n"; - plugin++; - } - - QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); - OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); - } -} - -void OBSBasic::OnFirstLoad() -{ - OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); - -#ifdef WHATSNEW_ENABLED - /* Attempt to load init screen if available */ - if (cef) { - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); - } -#endif - - Auth::Load(); - - bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); - - if (showLogViewerOnStartup) - on_actionViewCurrentLog_triggered(); -} - -/* shows a "what's new" page on startup of new versions using CEF */ -void OBSBasic::ReceivedIntroJson(const QString &text) -{ -#ifdef WHATSNEW_ENABLED - if (closing) - return; - - WhatsNewList items; - try { - nlohmann::json json = nlohmann::json::parse(text.toStdString()); - items = json.get(); - } catch (nlohmann::json::exception &e) { - blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); - return; - } - - std::string info_url; - int info_increment = -1; - - /* check to see if there's an info page for this version */ - for (const WhatsNewItem &item : items) { - if (item.os) { - WhatsNewPlatforms platforms = *item.os; -#ifdef _WIN32 - if (!platforms.windows) - continue; -#elif defined(__APPLE__) - if (!platforms.macos) - continue; -#else - if (!platforms.linux) - continue; -#endif - } - - int major = 0; - int minor = 0; - - sscanf(item.version.c_str(), "%d.%d", &major, &minor); - if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && - item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { - info_url = item.url; - info_increment = item.increment; - } - } - - /* this version was not found, or no info for this version */ - if (info_increment == -1) { - return; - } - -#if OBS_RELEASE_CANDIDATE > 0 - constexpr const char *lastInfoVersion = "InfoLastRCVersion"; -#elif OBS_BETA > 0 - constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; -#else - constexpr const char *lastInfoVersion = "InfoLastVersion"; -#endif - constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | - OBS_BETA; - uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); - int current_version_increment = -1; - - if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); - } else { - current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); - } - - if (info_increment <= current_version_increment) { - return; - } - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - cef->init_browser(); - - WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); - - connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); - - whatsNewInitThread.reset(wnbit); - whatsNewInitThread->start(); - -#else - UNUSED_PARAMETER(text); -#endif -} - -void OBSBasic::ShowWhatsNew(const QString &url) -{ -#ifdef BROWSER_AVAILABLE - if (closing) - return; - - if (obsWhatsNew) { - obsWhatsNew->close(); - } - - obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); -#else - UNUSED_PARAMETER(url); -#endif -} - -void OBSBasic::UpdateMultiviewProjectorMenu() -{ - ui->multiviewProjectorMenu->clear(); - AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); -} - -void OBSBasic::InitHotkeys() -{ - ProfileScope("OBSBasic::InitHotkeys"); - - struct obs_hotkeys_translations t = {}; - t.insert = Str("Hotkeys.Insert"); - t.del = Str("Hotkeys.Delete"); - t.home = Str("Hotkeys.Home"); - t.end = Str("Hotkeys.End"); - t.page_up = Str("Hotkeys.PageUp"); - t.page_down = Str("Hotkeys.PageDown"); - t.num_lock = Str("Hotkeys.NumLock"); - t.scroll_lock = Str("Hotkeys.ScrollLock"); - t.caps_lock = Str("Hotkeys.CapsLock"); - t.backspace = Str("Hotkeys.Backspace"); - t.tab = Str("Hotkeys.Tab"); - t.print = Str("Hotkeys.Print"); - t.pause = Str("Hotkeys.Pause"); - t.left = Str("Hotkeys.Left"); - t.right = Str("Hotkeys.Right"); - t.up = Str("Hotkeys.Up"); - t.down = Str("Hotkeys.Down"); -#ifdef _WIN32 - t.meta = Str("Hotkeys.Windows"); -#else - t.meta = Str("Hotkeys.Super"); -#endif - t.menu = Str("Hotkeys.Menu"); - t.space = Str("Hotkeys.Space"); - t.numpad_num = Str("Hotkeys.NumpadNum"); - t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); - t.numpad_divide = Str("Hotkeys.NumpadDivide"); - t.numpad_plus = Str("Hotkeys.NumpadAdd"); - t.numpad_minus = Str("Hotkeys.NumpadSubtract"); - t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); - t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); - t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); - t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); - t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); - t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); - t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); - t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); - t.mouse_num = Str("Hotkeys.MouseButton"); - t.escape = Str("Hotkeys.Escape"); - obs_hotkeys_set_translations(&t); - - obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), - Str("Push-to-talk")); - - obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); - - obs_hotkey_enable_callback_rerouting(true); - obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); -} - -void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) -{ - obs_hotkey_trigger_routed_callback(id, pressed); -} - -void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) -{ - OBSBasic &basic = *static_cast(data); - QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); -} - -void OBSBasic::CreateHotkeys() -{ - ProfileScope("OBSBasic::CreateHotkeys"); - - auto LoadHotkeyData = [&](const char *name) -> OBSData { - const char *info = config_get_string(activeConfiguration, "Hotkeys", name); - if (!info) - return {}; - - OBSDataAutoRelease data = obs_data_create_from_json(info); - if (!data) - return {}; - - return data.Get(); - }; - - auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { - OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); - - obs_hotkey_load(id, array); - }; - - auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, - const char *oldName = NULL) { - if (oldName) { - const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); - if (info) { - config_set_string(activeConfiguration, "Hotkeys", name0, info); - config_set_string(activeConfiguration, "Hotkeys", name1, info); - config_remove_value(activeConfiguration, "Hotkeys", oldName); - activeConfiguration.Save(); - } - } - OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); - OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); - - obs_hotkey_pair_load(id, array0, array1); - }; - -#define MAKE_CALLBACK(pred, method, log_action) \ - [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ - OBSBasic &basic = *static_cast(data); \ - if ((pred) && pressed) { \ - blog(LOG_INFO, log_action " due to hotkey"); \ - method(); \ - return true; \ - } \ - return false; \ - } - - streamingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", - Str("Basic.Main.StopStreaming"), - MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, - "Starting stream"), - MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, - "Stopping stream"), - this, this); - LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); - - auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic &basic = *static_cast(data); - if (basic.outputHandler->StreamingActive() && pressed) { - basic.ForceStopStreaming(); - } - }; - - forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", - Str("Basic.Main.ForceStopStreaming"), cb, this); - LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); - - recordingHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", - Str("Basic.Main.StopRecording"), - MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, - "Starting recording"), - MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, - "Stopping recording"), - this, this); - LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); - - pauseHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), - "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), - MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, - basic.PauseRecording, "Pausing recording"), - MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, - basic.UnpauseRecording, "Unpausing recording"), - this, this); - LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); - - splitFileHotkey = obs_hotkey_register_frontend( - "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_split_file(); - }, - this); - LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); - - addChapterHotkey = obs_hotkey_register_frontend( - "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), - [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - obs_frontend_recording_add_chapter(nullptr); - }, - this); - LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); - - replayBufHotkeys = - obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), - "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), - MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), - basic.StartReplayBuffer, "Starting replay buffer"), - MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), - basic.StopReplayBuffer, "Stopping replay buffer"), - this, this); - LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); - - if (vcamEnabled) { - vcamHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", - Str("Basic.Main.StopVirtualCam"), - MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, - "Starting virtual camera"), - MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, - "Stopping virtual camera"), - this, this); - LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); - } - - togglePreviewHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", - Str("Basic.Main.Preview.Disable"), - MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), - MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); - LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); - - togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), - "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), - MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), - MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), - this, this); - LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", - "OBSBasic.TogglePreviewProgram"); - - contextBarHotkeys = obs_hotkey_pair_register_frontend( - "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", - Str("Basic.Main.HideContextBar"), - MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), - MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), - this, this); - LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); -#undef MAKE_CALLBACK - - auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", - Qt::QueuedConnection); - }; - - transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); - LoadHotkey(transitionHotkey, "OBSBasic.Transition"); - - auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", - Qt::QueuedConnection); - }; - - statsHotkey = - obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); - LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); - - auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); - }; - - screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); - LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); - - auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - if (pressed) - QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", - Qt::QueuedConnection); - }; - - sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", - Str("Screenshot.SourceHotkey"), screenshotSource, this); - LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); -} - -void OBSBasic::ClearHotkeys() -{ - obs_hotkey_pair_unregister(streamingHotkeys); - obs_hotkey_pair_unregister(recordingHotkeys); - obs_hotkey_pair_unregister(pauseHotkeys); - obs_hotkey_unregister(splitFileHotkey); - obs_hotkey_unregister(addChapterHotkey); - obs_hotkey_pair_unregister(replayBufHotkeys); - obs_hotkey_pair_unregister(vcamHotkeys); - obs_hotkey_pair_unregister(togglePreviewHotkeys); - obs_hotkey_pair_unregister(contextBarHotkeys); - obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); - obs_hotkey_unregister(forceStreamingStopHotkey); - obs_hotkey_unregister(transitionHotkey); - obs_hotkey_unregister(statsHotkey); - obs_hotkey_unregister(screenshotHotkey); - obs_hotkey_unregister(sourceScreenshotHotkey); -} - -OBSBasic::~OBSBasic() -{ - /* clear out UI event queue */ - QApplication::sendPostedEvents(nullptr); - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - if (updateCheckThread && updateCheckThread->isRunning()) - updateCheckThread->wait(); - - if (patronJsonThread && patronJsonThread->isRunning()) - patronJsonThread->wait(); - - delete screenshotData; - delete previewProjector; - delete studioProgramProjector; - delete previewProjectorSource; - delete previewProjectorMain; - delete sourceProjector; - delete sceneProjectorMenu; - delete scaleFilteringMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - delete perSceneTransitionMenu; - delete shortcutFilter; - delete trayMenu; - delete programOptions; - delete program; - - /* XXX: any obs data must be released before calling obs_shutdown. - * currently, we can't automate this with C++ RAII because of the - * delicate nature of obs_shutdown needing to be freed before the UI - * can be freed, and we have no control over the destruction order of - * the Qt UI stuff, so we have to manually clear any references to - * libobs. */ - delete cpuUsageTimer; - os_cpu_usage_info_destroy(cpuUsageInfo); - - obs_hotkey_set_callback_routing_func(nullptr, nullptr); - ClearHotkeys(); - - service = nullptr; - outputHandler.reset(); - - delete interaction; - delete properties; - delete filters; - delete transformWindow; - delete advAudioWindow; - delete about; - delete remux; - - obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); - - obs_enter_graphics(); - gs_vertexbuffer_destroy(box); - gs_vertexbuffer_destroy(boxLeft); - gs_vertexbuffer_destroy(boxTop); - gs_vertexbuffer_destroy(boxRight); - gs_vertexbuffer_destroy(boxBottom); - gs_vertexbuffer_destroy(circle); - gs_vertexbuffer_destroy(actionSafeMargin); - gs_vertexbuffer_destroy(graphicsSafeMargin); - gs_vertexbuffer_destroy(fourByThreeSafeMargin); - gs_vertexbuffer_destroy(leftLine); - gs_vertexbuffer_destroy(topLine); - gs_vertexbuffer_destroy(rightLine); - obs_leave_graphics(); - - /* When shutting down, sometimes source references can get in to the - * event queue, and if we don't forcibly process those events they - * won't get processed until after obs_shutdown has been called. I - * really wish there were a more elegant way to deal with this via C++, - * but Qt doesn't use C++ in a normal way, so you can't really rely on - * normal C++ behavior for your data to be freed in the order that you - * expect or want it to. */ - QApplication::sendPostedEvents(nullptr); - - config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - -#ifdef BROWSER_AVAILABLE - DestroyPanelCookieManager(); - delete cef; - cef = nullptr; -#endif -} - -void OBSBasic::SaveProjectNow() -{ - if (disableSaving) - return; - - projectChanged = true; - SaveProjectDeferred(); -} - -void OBSBasic::SaveProject() -{ - if (disableSaving) - return; - - projectChanged = true; - QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); -} - -void OBSBasic::SaveProjectDeferred() -{ - if (disableSaving) - return; - - if (!projectChanged) - return; - - projectChanged = false; - - try { - const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); - - Save(currentCollection.collectionFile.u8string().c_str()); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -OBSSource OBSBasic::GetProgramSource() -{ - return OBSGetStrongRef(programScene); -} - -OBSScene OBSBasic::GetCurrentScene() -{ - return currentScene.load(); -} - -OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) -{ - return item ? GetOBSRef(item) : nullptr; -} - -OBSSceneItem OBSBasic::GetCurrentSceneItem() -{ - return ui->sources->Get(GetTopSelectedSourceItem()); -} - -void OBSBasic::UpdatePreviewScalingMenu() -{ - bool fixedScaling = ui->preview->IsFixedScaling(); - float scalingAmount = ui->preview->GetScalingAmount(); - if (!fixedScaling) { - ui->actionScaleWindow->setChecked(true); - ui->actionScaleCanvas->setChecked(false); - ui->actionScaleOutput->setChecked(false); - return; - } - - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->actionScaleWindow->setChecked(false); - ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); - ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); -} - -void OBSBasic::CreateInteractionWindow(obs_source_t *source) -{ - bool closed = true; - if (interaction) - closed = interaction->close(); - - if (!closed) - return; - - interaction = new OBSBasicInteraction(this, source); - interaction->Init(); - interaction->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreatePropertiesWindow(obs_source_t *source) -{ - bool closed = true; - if (properties) - closed = properties->close(); - - if (!closed) - return; - - properties = new OBSBasicProperties(this, source); - properties->Init(); - properties->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::CreateFiltersWindow(obs_source_t *source) -{ - bool closed = true; - if (filters) - closed = filters->close(); - - if (!closed) - return; - - filters = new OBSBasicFilters(this, source); - filters->Init(); - filters->setAttribute(Qt::WA_DeleteOnClose, true); -} - -/* Qt callbacks for invokeMethod */ - -void OBSBasic::AddScene(OBSSource source) -{ - const char *name = obs_source_get_name(source); - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); - SetOBSRef(item, OBSScene(scene)); - ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); - - obs_hotkey_register_source( - source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), - [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - auto potential_source = static_cast(data); - OBSSourceAutoRelease source = obs_source_get_ref(potential_source); - if (source && pressed) - main->SetCurrentScene(source.Get()); - }, - static_cast(source)); - - signal_handler_t *handler = obs_source_get_signal_handler(source); - - SignalContainer container; - container.ref = scene; - container.handlers.assign({ - std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), - std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), - std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), - }); - - item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); - - /* if the scene already has items (a duplicated scene) add them */ - auto addSceneItem = [this](obs_sceneitem_t *item) { - AddSceneItem(item); - }; - - using addSceneItem_t = decltype(addSceneItem); - - obs_scene_enum_items( - scene, - [](obs_scene_t *, obs_sceneitem_t *item, void *param) { - addSceneItem_t *func; - func = reinterpret_cast(param); - (*func)(item); - return true; - }, - &addSceneItem); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *source = obs_scene_get_source(scene); - blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::RemoveScene(OBSSource source) -{ - obs_scene_t *scene = obs_scene_from_source(source); - - QListWidgetItem *sel = nullptr; - int count = ui->scenes->count(); - - for (int i = 0; i < count; i++) { - auto item = ui->scenes->item(i); - auto cur_scene = GetOBSRef(item); - if (cur_scene != scene) - continue; - - sel = item; - break; - } - - if (sel != nullptr) { - if (sel == ui->scenes->currentItem()) - ui->sources->Clear(); - delete sel; - } - - SaveProject(); - - if (!disableSaving) { - blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); - - OBSProjector::UpdateMultiviewProjectors(); - } - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_sceneitem_t *selectedItem = reinterpret_cast(param); - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, select_one, param); - - obs_sceneitem_select(item, (selectedItem == item)); - - return true; -} - -void OBSBasic::AddSceneItem(OBSSceneItem item) -{ - obs_scene_t *scene = obs_sceneitem_get_scene(item); - - if (GetCurrentScene() == scene) - ui->sources->Add(item); - - SaveProject(); - - if (!disableSaving) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); - blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), - obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); - - obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); - } -} - -static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) -{ - QList items = listWidget->findItems(prevName, Qt::MatchExactly); - - for (int i = 0; i < items.count(); i++) - items[i]->setText(newName); -} - -void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) -{ - RenameListValues(ui->scenes, newName, prevName); - - if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) - vcamConfig.source = newName.toStdString(); - if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) - vcamConfig.scene = newName.toStdString(); - - SaveProject(); - - obs_scene_t *scene = obs_scene_from_source(source); - if (scene) - OBSProjector::UpdateMultiviewProjectors(); - - UpdateContextBar(); - UpdatePreviewProgramIndicators(); -} - -void OBSBasic::ClearContextBar() -{ - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - delete la->widget(); - ui->emptySpace->layout()->removeItem(la); - } -} - -void OBSBasic::UpdateContextBarVisibility() -{ - int width = ui->centralwidget->size().width(); - - ContextBarSize contextBarSizeNew; - if (width >= 740) { - contextBarSizeNew = ContextBarSize_Normal; - } else if (width >= 600) { - contextBarSizeNew = ContextBarSize_Reduced; - } else { - contextBarSizeNew = ContextBarSize_Minimized; - } - - if (contextBarSize == contextBarSizeNew) - return; - - contextBarSize = contextBarSizeNew; - UpdateContextBarDeferred(); -} - -static bool is_network_media_source(obs_source_t *source, const char *id) -{ - if (strcmp(id, "ffmpeg_source") != 0) - return false; - - OBSDataAutoRelease s = obs_source_get_settings(source); - bool is_local_file = obs_data_get_bool(s, "is_local_file"); - - return !is_local_file; -} - -void OBSBasic::UpdateContextBarDeferred(bool force) -{ - QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); -} - -void OBSBasic::SourceToolBarActionsSetEnabled() -{ - bool enable = false; - bool disableProps = false; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - OBSSource source = obs_sceneitem_get_source(item); - disableProps = !obs_source_configurable(source); - - enable = true; - } - - if (disableProps) - ui->actionSourceProperties->setEnabled(false); - else - ui->actionSourceProperties->setEnabled(enable); - - ui->actionRemoveSource->setEnabled(enable); - ui->actionSourceUp->setEnabled(enable); - ui->actionSourceDown->setEnabled(enable); - - RefreshToolBarStyling(ui->sourcesToolbar); -} - -void OBSBasic::UpdateContextBar(bool force) -{ - SourceToolBarActionsSetEnabled(); - - if (!ui->contextContainer->isVisible() && !force) - return; - - OBSSceneItem item = GetCurrentSceneItem(); - - if (item) { - obs_source_t *source = obs_sceneitem_get_source(item); - - bool updateNeeded = true; - QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); - if (la) { - if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { - if (toolbar->GetSource() == source) - updateNeeded = false; - } - } - - const char *id = obs_source_get_unversioned_id(source); - uint32_t flags = obs_source_get_output_flags(source); - - ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); - - if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { - ClearContextBar(); - if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { - if (!is_network_media_source(source, id)) { - MediaControls *mediaControls = new MediaControls(ui->emptySpace); - mediaControls->SetSource(source); - - ui->emptySpace->layout()->addWidget(mediaControls); - } - } else if (strcmp(id, "browser_source") == 0) { - BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_input_capture") == 0 || - strcmp(id, "wasapi_output_capture") == 0 || - strcmp(id, "coreaudio_input_capture") == 0 || - strcmp(id, "coreaudio_output_capture") == 0 || - strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || - strcmp(id, "alsa_input_capture") == 0) { - AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "wasapi_process_output_capture") == 0) { - ApplicationAudioCaptureToolbar *c = - new ApplicationAudioCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { - WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || - strcmp(id, "xshm_input") == 0) { - DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); - c->Init(); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "dshow_input") == 0) { - DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "game_capture") == 0) { - GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "image_source") == 0) { - ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "color_source") == 0) { - ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - - } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { - TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); - ui->emptySpace->layout()->addWidget(c); - } - } else if (contextBarSize == ContextBarSize_Minimized) { - ClearContextBar(); - } - - QIcon icon; - - if (strcmp(id, "scene") == 0) - icon = GetSceneIcon(); - else if (strcmp(id, "group") == 0) - icon = GetGroupIcon(); - else - icon = GetSourceIcon(id); - - QPixmap pixmap = icon.pixmap(QSize(16, 16)); - ui->contextSourceIcon->setPixmap(pixmap); - ui->contextSourceIconSpacer->hide(); - ui->contextSourceIcon->show(); - - const char *name = obs_source_get_name(source); - ui->contextSourceLabel->setText(name); - - ui->sourceFiltersButton->setEnabled(true); - ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); - } else { - ClearContextBar(); - ui->contextSourceIcon->hide(); - ui->contextSourceIconSpacer->show(); - ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); - - ui->sourceFiltersButton->setEnabled(false); - ui->sourcePropertiesButton->setEnabled(false); - ui->sourceInteractButton->setVisible(false); - } - - if (contextBarSize == ContextBarSize_Normal) { - ui->sourcePropertiesButton->setText(QTStr("Properties")); - ui->sourceFiltersButton->setText(QTStr("Filters")); - ui->sourceInteractButton->setText(QTStr("Interact")); - } else { - ui->sourcePropertiesButton->setText(""); - ui->sourceFiltersButton->setText(""); - ui->sourceInteractButton->setText(""); - } -} - -static inline bool SourceMixerHidden(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); - - return hidden; -} - -static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "mixer_hidden", hidden); -} - -void OBSBasic::GetAudioSourceFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreateFiltersWindow(source); -} - -void OBSBasic::GetAudioSourceProperties() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - CreatePropertiesWindow(source); -} - -void OBSBasic::HideAudioControl() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } -} - -void OBSBasic::UnhideAllAudioControls() -{ - auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ - { - if (!obs_source_active(source)) - return true; - if (!SourceMixerHidden(source)) - return true; - - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - return true; - }; - - using UnhideAudioMixer_t = decltype(UnhideAudioMixer); - - auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ - { - return (*reinterpret_cast(data))(source); - }; - - obs_enum_sources(PreEnum, &UnhideAudioMixer); -} - -void OBSBasic::ToggleHideMixer() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (!SourceMixerHidden(source)) { - SetSourceMixerHidden(source, true); - DeactivateAudioSource(source); - } else { - SetSourceMixerHidden(source, false); - ActivateAudioSource(source); - } -} - -void OBSBasic::MixerRenameSource() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - OBSSource source = vol->GetSource(); - - const char *prevName = obs_source_get_name(source); - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), - QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); - - if (sourceTest) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - obs_source_set_name(source, name.c_str()); - break; - } -} - -static inline bool SourceVolumeLocked(obs_source_t *source) -{ - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - bool lock = obs_data_get_bool(priv_settings, "volume_locked"); - - return lock; -} - -void OBSBasic::LockVolumeControl(bool lock) -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); - obs_data_set_bool(priv_settings, "volume_locked", lock); - - vol->EnableSlider(!lock); -} - -void OBSBasic::VolControlContextMenu() -{ - VolControl *vol = reinterpret_cast(sender()); - - /* ------------------- */ - - QAction lockAction(QTStr("LockVolume"), this); - lockAction.setCheckable(true); - lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); - - QAction hideAction(QTStr("Hide"), this); - QAction unhideAllAction(QTStr("UnhideAll"), this); - QAction mixerRenameAction(QTStr("Rename"), this); - - QAction copyFiltersAction(QTStr("Copy.Filters"), this); - QAction pasteFiltersAction(QTStr("Paste.Filters"), this); - - QAction filtersAction(QTStr("Filters"), this); - QAction propertiesAction(QTStr("Properties"), this); - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); - connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); - - connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); - connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, - Qt::DirectConnection); - - connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); - connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, - Qt::DirectConnection); - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - hideAction.setProperty("volControl", QVariant::fromValue(vol)); - lockAction.setProperty("volControl", QVariant::fromValue(vol)); - mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); - - copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); - - filtersAction.setProperty("volControl", QVariant::fromValue(vol)); - propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); - - /* ------------------- */ - - copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); - pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); - - QMenu popup; - vol->SetContextMenu(&popup); - popup.addAction(&lockAction); - popup.addSeparator(); - popup.addAction(&unhideAllAction); - popup.addAction(&hideAction); - popup.addAction(&mixerRenameAction); - popup.addSeparator(); - popup.addAction(©FiltersAction); - popup.addAction(&pasteFiltersAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&filtersAction); - popup.addAction(&propertiesAction); - popup.addAction(&advPropAction); - - // toggleControlLayoutAction deletes and re-creates the volume controls - // meaning that "vol" would be pointing to freed memory. - if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) - vol->SetContextMenu(nullptr); -} - -void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() -{ - StackedMixerAreaContextMenuRequested(); -} - -void OBSBasic::StackedMixerAreaContextMenuRequested() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - - QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - - /* ------------------- */ - - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, - Qt::DirectConnection); - - /* ------------------- */ - - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - /* ------------------- */ - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.addSeparator(); - popup.addAction(&advPropAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::ToggleMixerLayout(bool vertical) -{ - if (vertical) { - ui->stackedMixerArea->setMinimumSize(180, 220); - ui->stackedMixerArea->setCurrentIndex(1); - } else { - ui->stackedMixerArea->setMinimumSize(220, 0); - ui->stackedMixerArea->setCurrentIndex(0); - } -} - -void OBSBasic::ToggleVolControlLayout() -{ - bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); - ToggleMixerLayout(vertical); - - // We need to store it so we can delete current and then add - // at the right order - vector sources; - for (size_t i = 0; i != volumes.size(); i++) - sources.emplace_back(volumes[i]->GetSource()); - - ClearVolumeControls(); - - for (const auto &source : sources) - ActivateAudioSource(source); -} - -void OBSBasic::ActivateAudioSource(OBSSource source) -{ - if (SourceMixerHidden(source)) - return; - if (!obs_source_active(source)) - return; - if (!obs_source_audio_active(source)) - return; - - bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); - VolControl *vol = new VolControl(source, true, vertical); - - vol->EnableSlider(!SourceVolumeLocked(source)); - - double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); - vol->SetMeterDecayRate(meterDecayRate); - - uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); - - enum obs_peak_meter_type peakMeterType; - switch (peakMeterTypeIdx) { - case 0: - peakMeterType = SAMPLE_PEAK_METER; - break; - case 1: - peakMeterType = TRUE_PEAK_METER; - break; - default: - peakMeterType = SAMPLE_PEAK_METER; - break; - } - - vol->setPeakMeterType(peakMeterType); - - vol->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); - connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); - - InsertQObjectByName(volumes, vol); - - for (auto volume : volumes) { - if (vertical) - ui->vVolControlLayout->addWidget(volume); - else - ui->hVolControlLayout->addWidget(volume); - } -} - -void OBSBasic::DeactivateAudioSource(OBSSource source) -{ - for (size_t i = 0; i < volumes.size(); i++) { - if (volumes[i]->GetSource() == source) { - delete volumes[i]; - volumes.erase(volumes.begin() + i); - break; - } - } -} - -bool OBSBasic::QueryRemoveSource(obs_source_t *source) -{ - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { - int count = ui->scenes->count(); - - if (count == 1) { - OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); - return false; - } - } - - const char *name = obs_source_get_name(source); - - QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); - - QMessageBox remove_source(this); - remove_source.setText(text); - QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_source.setDefaultButton(Yes); - remove_source.addButton(QTStr("No"), QMessageBox::NoRole); - remove_source.setIcon(QMessageBox::Question); - remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_source.exec(); - - return Yes == remove_source.clickedButton(); -} - -#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ - -void OBSBasic::TimedCheckForUpdates() -{ - if (App()->IsUpdaterDisabled()) - return; - if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) - return; - -#if defined(ENABLE_SPARKLE_UPDATER) - CheckForUpdates(false); -#elif _WIN32 - long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); - uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); - - if (lastVersion < LIBOBS_API_VER) { - lastUpdate = 0; - config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); - } - - long long t = (long long)time(nullptr); - long long secs = t - lastUpdate; - - if (secs > UPDATE_CHECK_INTERVAL) - CheckForUpdates(false); -#endif -} - -void OBSBasic::CheckForUpdates(bool manualUpdate) -{ -#if _WIN32 - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); - updateCheckThread->start(); -#elif defined(ENABLE_SPARKLE_UPDATER) - ui->actionCheckForUpdates->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - MacUpdateThread *mut = new MacUpdateThread(manualUpdate); - connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); - updateCheckThread.reset(mut); - updateCheckThread->start(); -#else - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) -{ -#ifdef ENABLE_SPARKLE_UPDATER - static OBSSparkle *updater; - - if (!updater) { - updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); - return; - } - - updater->setBranch(QT_TO_UTF8(branch)); - updater->checkForUpdates(manualUpdate); -#else - UNUSED_PARAMETER(branch); - UNUSED_PARAMETER(manualUpdate); -#endif -} - -void OBSBasic::updateCheckFinished() -{ - ui->actionCheckForUpdates->setEnabled(true); - ui->actionRepair->setEnabled(true); -} - -void OBSBasic::DuplicateSelectedScene() -{ - OBSScene curScene = GetCurrentScene(); - - if (!curScene) - return; - - OBSSource curSceneSource = obs_scene_get_source(curScene); - QString format{obs_source_get_name(curSceneSource)}; - format += " %1"; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - for (;;) { - string name; - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - if (!accepted) - return; - - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - obs_source_t *source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - obs_source_release(source); - continue; - } - - OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - - auto undo = [](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - }; - - auto redo = [this, name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_scene_t *scene = obs_scene_from_source(source); - scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); - source = obs_scene_get_source(scene); - SetCurrentScene(source.Get(), true); - }; - - undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, - obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); - - break; - } -} - -static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) -{ - obs_source_t *source = obs_sceneitem_get_source(item); - if (obs_obj_is_private(source) && !obs_source_removed(source)) - return true; - - obs_data_array_t *array = (obs_data_array_t *)p; - - /* check if the source is already stored in the array */ - const char *name = obs_source_get_name(source); - const size_t count = obs_data_array_count(array); - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease sourceData = obs_data_array_item(array, i); - if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) - return true; - } - - if (obs_source_is_group(source)) - obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); - - OBSDataAutoRelease source_data = obs_save_source(source); - obs_data_array_push_back(array, source_data); - return true; -} - -static inline void RemoveSceneAndReleaseNested(obs_source_t *source) -{ - obs_source_remove(source); - auto cb = [](void *, obs_source_t *source) { - if (strcmp(obs_source_get_id(source), "scene") == 0) - obs_scene_prune_sources(obs_scene_from_source(source)); - return true; - }; - obs_enum_scenes(cb, NULL); -} - -void OBSBasic::RemoveSelectedScene() -{ - OBSScene scene = GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - - if (!source || !QueryRemoveSource(source)) { - return; - } - - /* ------------------------------ */ - /* save all sources in scene */ - - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); - - obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); - - OBSDataAutoRelease scene_data = obs_save_source(source); - obs_data_array_push_back(sources_in_deleted_scene, scene_data); - - /* ----------------------------------------------- */ - /* save all scenes and groups the scene is used in */ - - OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); - - struct other_scenes_cb_data { - obs_source_t *oldScene; - obs_data_array_t *scene_used_in_other_scenes; - } other_scenes_cb_data; - other_scenes_cb_data.oldScene = source; - other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; - - auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { - struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; - if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) - return true; - obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), - obs_source_get_name(data->oldScene)); - if (item) { - OBSDataAutoRelease scene_data = - obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); - obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); - } - return true; - }; - obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); - - /* --------------------------- */ - /* undo/redo */ - - auto undo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); - OBSDataArrayAutoRelease scene_used_in_other_scenes = - obs_data_get_array(base, "scene_used_in_other_scenes"); - int savedIndex = (int)obs_data_get_int(base, "index"); - std::vector sources; - - /* create missing sources */ - size_t count = obs_data_array_count(sources_in_deleted_scene); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) { - source = obs_load_source(data); - sources.push_back(source.Get()); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - /* Add scene to scenes and groups it was nested in */ - for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { - OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); - const char *name = obs_data_get_string(data, "name"); - OBSSourceAutoRelease source = obs_get_source_by_name(name); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); - - /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ - std::vector existing_sources; - auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - std::vector *existing = (std::vector *)data; - OBSSource source = obs_sceneitem_get_source(item); - obs_sceneitem_remove(item); - existing->push_back(source); - return true; - }; - obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); - - /* Re-add sources to the scene */ - obs_sceneitems_add(obs_group_or_scene_from_source(source), items); - } - - obs_source_t *scene_source = sources.back(); - OBSScene scene = obs_scene_from_source(scene_source); - SetCurrentScene(scene, true); - - /* set original index in list box */ - ui->scenes->blockSignals(true); - int curIndex = ui->scenes->currentRow(); - QListWidgetItem *item = ui->scenes->takeItem(curIndex); - ui->scenes->insertItem(savedIndex, item); - ui->scenes->setCurrentRow(savedIndex); - currentScene = scene.Get(); - ui->scenes->blockSignals(false); - }; - - auto redo = [](const std::string &name) { - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - RemoveSceneAndReleaseNested(source); - }; - - OBSDataAutoRelease data = obs_data_create(); - obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); - obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); - obs_data_set_int(data, "index", ui->scenes->currentRow()); - - const char *scene_name = obs_source_get_name(source); - undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); - - /* --------------------------- */ - /* remove */ - - RemoveSceneAndReleaseNested(source); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::ReorderSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->ReorderItems(); - SaveProject(); -} - -void OBSBasic::RefreshSources(OBSScene scene) -{ - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) - return; - - ui->sources->RefreshItems(); - SaveProject(); -} - -/* OBS Callbacks */ - -void OBSBasic::SceneReordered(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneRefreshed(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); - - QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); -} - -void OBSBasic::SceneItemAdded(void *data, calldata_t *params) -{ - OBSBasic *window = static_cast(data); - - obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); - - QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); -} - -void OBSBasic::SourceCreated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRemoved(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_scene_from_source(source) != NULL) - QMetaObject::invokeMethod(static_cast(data), "RemoveScene", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - uint32_t flags = obs_source_get_output_flags(source); - - if (flags & OBS_SOURCE_AUDIO) - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - - if (obs_source_active(source)) - QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", - Q_ARG(OBSSource, OBSSource(source))); -} - -void OBSBasic::SourceRenamed(void *data, calldata_t *params) -{ - obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); - const char *newName = calldata_string(params, "new_name"); - const char *prevName = calldata_string(params, "prev_name"); - - QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), - Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); - - blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); -} - -void OBSBasic::DrawBackdrop(float cx, float cy) -{ - if (!box) - return; - - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); - - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - - vec4 colorVal; - vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); - gs_effect_set_vec4(color, &colorVal); - - gs_technique_begin(tech); - gs_technique_begin_pass(tech, 0); - gs_matrix_push(); - gs_matrix_identity(); - gs_matrix_scale3f(float(cx), float(cy), 1.0f); - - gs_load_vertexbuffer(box); - gs_draw(GS_TRISTRIP, 0, 0); - - gs_matrix_pop(); - gs_technique_end_pass(tech); - gs_technique_end(tech); - - gs_load_vertexbuffer(nullptr); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->previewCX = int(window->previewScale * float(ovi.base_width)); - window->previewCY = int(window->previewScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - obs_display_t *display = window->ui->preview->GetDisplay(); - uint32_t width, height; - obs_display_size(display, &width, &height); - float right = float(width) - window->previewX; - float bottom = float(height) - window->previewY; - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - - window->ui->preview->DrawOverflow(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); - - if (window->IsPreviewProgramMode()) { - window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); - - OBSScene scene = window->GetCurrentScene(); - obs_source_t *source = obs_scene_get_source(scene); - if (source) - obs_source_video_render(source); - } else { - obs_render_main_texture_src_color_only(); - } - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); - gs_reset_viewport(); - - uint32_t targetCX = window->previewCX; - uint32_t targetCY = window->previewCY; - - if (window->drawSafeAreas) { - RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); - RenderSafeAreas(window->leftLine, targetCX, targetCY); - RenderSafeAreas(window->topLine, targetCX, targetCY); - RenderSafeAreas(window->rightLine, targetCX, targetCY); - } - - window->ui->preview->DrawSceneEditing(); - - if (window->drawSpacingHelpers) - window->ui->preview->DrawSpacingHelpers(); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -/* Main class functions */ - -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - -int OBSBasic::GetTransitionDuration() -{ - return ui->transitionDuration->value(); -} - -bool OBSBasic::Active() const -{ - if (!outputHandler) - return false; - return outputHandler->Active(); -} - -#ifdef _WIN32 -#define IS_WIN32 1 -#else -#define IS_WIN32 0 -#endif - -static inline int AttemptToResetVideo(struct obs_video_info *ovi) -{ - return obs_reset_video(ovi); -} - -static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) -{ - const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); - - if (astrcmpi(scaleTypeStr, "bilinear") == 0) - return OBS_SCALE_BILINEAR; - else if (astrcmpi(scaleTypeStr, "lanczos") == 0) - return OBS_SCALE_LANCZOS; - else if (astrcmpi(scaleTypeStr, "area") == 0) - return OBS_SCALE_AREA; - else - return OBS_SCALE_BICUBIC; -} - -static inline enum video_format GetVideoFormatFromName(const char *name) -{ - if (astrcmpi(name, "I420") == 0) - return VIDEO_FORMAT_I420; - else if (astrcmpi(name, "NV12") == 0) - return VIDEO_FORMAT_NV12; - else if (astrcmpi(name, "I444") == 0) - return VIDEO_FORMAT_I444; - else if (astrcmpi(name, "I010") == 0) - return VIDEO_FORMAT_I010; - else if (astrcmpi(name, "P010") == 0) - return VIDEO_FORMAT_P010; - else if (astrcmpi(name, "P216") == 0) - return VIDEO_FORMAT_P216; - else if (astrcmpi(name, "P416") == 0) - return VIDEO_FORMAT_P416; -#if 0 //currently unsupported - else if (astrcmpi(name, "YVYU") == 0) - return VIDEO_FORMAT_YVYU; - else if (astrcmpi(name, "YUY2") == 0) - return VIDEO_FORMAT_YUY2; - else if (astrcmpi(name, "UYVY") == 0) - return VIDEO_FORMAT_UYVY; -#endif - else - return VIDEO_FORMAT_BGRA; -} - -static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) -{ - enum video_colorspace colorspace = VIDEO_CS_SRGB; - if (strcmp(name, "601") == 0) - colorspace = VIDEO_CS_601; - else if (strcmp(name, "709") == 0) - colorspace = VIDEO_CS_709; - else if (strcmp(name, "2100PQ") == 0) - colorspace = VIDEO_CS_2100_PQ; - else if (strcmp(name, "2100HLG") == 0) - colorspace = VIDEO_CS_2100_HLG; - - return colorspace; -} - -void OBSBasic::ResetUI() -{ - bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); - - if (studioPortraitLayout) - ui->previewLayout->setDirection(QBoxLayout::BottomToTop); - else - ui->previewLayout->setDirection(QBoxLayout::LeftToRight); - - UpdatePreviewProgramIndicators(); -} - -int OBSBasic::ResetVideo() -{ - if (outputHandler && outputHandler->Active()) - return OBS_VIDEO_CURRENTLY_ACTIVE; - - ProfileScope("OBSBasic::ResetVideo"); - - struct obs_video_info ovi; - int ret; - - GetConfigFPS(ovi.fps_num, ovi.fps_den); - - const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); - const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); - const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); - - ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); - ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); - ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); - ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); - ovi.output_format = GetVideoFormatFromName(colorFormat); - ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); - ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; - ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); - ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(activeConfiguration); - - if (ovi.base_width < 32 || ovi.base_height < 32) { - ovi.base_width = 1920; - ovi.base_height = 1080; - config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); - config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); - } - - if (ovi.output_width < 32 || ovi.output_height < 32) { - ovi.output_width = ovi.base_width; - ovi.output_height = ovi.base_height; - config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); - } - - ret = AttemptToResetVideo(&ovi); - if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { - blog(LOG_WARNING, "Tried to reset when already active"); - return ret; - } - - if (ret == OBS_VIDEO_SUCCESS) { - ResizePreview(ovi.base_width, ovi.base_height); - if (program) - ResizeProgram(ovi.base_width, ovi.base_height); - - const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); - const float hdr_nominal_peak_level = - (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); - obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); - OBSBasicStats::InitializeValues(); - OBSProjector::UpdateMultiviewProjectors(); - - bool canMigrate = usingAbsoluteCoordinates || - (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || - migrationBaseResolution->second != ovi.base_height)); - ui->actionRemigrateSceneCollection->setEnabled(canMigrate); - - emit CanvasResized(ovi.base_width, ovi.base_height); - emit OutputResized(ovi.output_width, ovi.output_height); - } - - return ret; -} - -bool OBSBasic::ResetAudio() -{ - ProfileScope("OBSBasic::ResetAudio"); - - struct obs_audio_info2 ai = {}; - ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); - - const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); - - if (strcmp(channelSetupStr, "Mono") == 0) - ai.speakers = SPEAKERS_MONO; - else if (strcmp(channelSetupStr, "2.1") == 0) - ai.speakers = SPEAKERS_2POINT1; - else if (strcmp(channelSetupStr, "4.0") == 0) - ai.speakers = SPEAKERS_4POINT0; - else if (strcmp(channelSetupStr, "4.1") == 0) - ai.speakers = SPEAKERS_4POINT1; - else if (strcmp(channelSetupStr, "5.1") == 0) - ai.speakers = SPEAKERS_5POINT1; - else if (strcmp(channelSetupStr, "7.1") == 0) - ai.speakers = SPEAKERS_7POINT1; - else - ai.speakers = SPEAKERS_STEREO; - - bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); - if (lowLatencyAudioBuffering) { - ai.max_buffering_ms = 20; - ai.fixed_buffering = true; - } - - return obs_reset_audio2(&ai); -} - -extern char *get_new_source_name(const char *name, const char *format); - -void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) -{ - bool disable = deviceId && strcmp(deviceId, "disabled") == 0; - OBSSourceAutoRelease source; - OBSDataAutoRelease settings; - - source = obs_get_output_source(channel); - if (source) { - if (disable) { - obs_set_output_source(channel, nullptr); - } else { - settings = obs_source_get_settings(source); - const char *oldId = obs_data_get_string(settings, "device_id"); - if (strcmp(oldId, deviceId) != 0) { - obs_data_set_string(settings, "device_id", deviceId); - obs_source_update(source, settings); - } - } - - } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); - - settings = obs_data_create(); - obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); - - obs_set_output_source(channel, source); - } -} - -void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - bool isFixedScaling; - obs_video_info ovi; - - /* resize preview panel to fix to the top section of the window */ - targetSize = GetPixelSize(ui->preview); - - isFixedScaling = ui->preview->IsFixedScaling(); - obs_get_video_info(&ovi); - - if (isFixedScaling) { - previewScale = ui->preview->GetScalingAmount(); - - ui->preview->ClampScrollingOffsets(); - - GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, - previewScale); - previewX += ui->preview->GetScrollX(); - previewY += ui->preview->GetScrollY(); - - } else { - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); - } - - ui->preview->SetScalingAmount(previewScale); - - previewX += float(PREVIEW_EDGE_SIZE); - previewY += float(PREVIEW_EDGE_SIZE); -} - -void OBSBasic::CloseDialogs() -{ - QList childDialogs = this->findChildren(); - if (!childDialogs.isEmpty()) { - for (int i = 0; i < childDialogs.size(); ++i) { - childDialogs.at(i)->close(); - } - } - - if (!stats.isNull()) - stats->close(); //call close to save Stats geometry - if (!remux.isNull()) - remux->close(); -} - -void OBSBasic::EnumDialogs() -{ - visDialogs.clear(); - modalDialogs.clear(); - visMsgBoxes.clear(); - - /* fill list of Visible dialogs and Modal dialogs */ - QList dialogs = findChildren(); - for (QDialog *dialog : dialogs) { - if (dialog->isVisible()) - visDialogs.append(dialog); - if (dialog->isModal()) - modalDialogs.append(dialog); - } - - /* fill list of Visible message boxes */ - QList msgBoxes = findChildren(); - for (QMessageBox *msgbox : msgBoxes) { - if (msgbox->isVisible()) - visMsgBoxes.append(msgbox); - } -} - -void OBSBasic::ClearProjectors() -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i]) - delete projectors[i]; - } - - projectors.clear(); -} - -void OBSBasic::ClearSceneData() -{ - disableSaving++; - - setCursor(Qt::WaitCursor); - - CloseDialogs(); - - ClearVolumeControls(); - ClearListItems(ui->scenes); - ui->sources->Clear(); - ClearQuickTransitions(); - ui->transitions->clear(); - - ClearProjectors(); - - for (int i = 0; i < MAX_CHANNELS; i++) - obs_set_output_source(i, nullptr); - - /* Reset VCam to default to clear its private scene and any references - * it holds. It will be reconfigured during loading. */ - if (vcamEnabled) { - vcamConfig.type = VCamOutputType::ProgramView; - outputHandler->UpdateVirtualCamOutputSource(); - } - - collectionModuleData = nullptr; - lastScene = nullptr; - swapScene = nullptr; - programScene = nullptr; - prevFTBSource = nullptr; - - clipboard.clear(); - copyFiltersSource = nullptr; - copyFilter = nullptr; - - auto cb = [](void *, obs_source_t *source) { - obs_source_remove(source); - return true; - }; - - obs_enum_scenes(cb, nullptr); - obs_enum_sources(cb, nullptr); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); - - undo_s.clear(); - - /* using QEvent::DeferredDelete explicitly is the only way to ensure - * that deleteLater events are processed at this point */ - QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - do { - QApplication::sendPostedEvents(nullptr); - } while (obs_wait_for_destroy_queue()); - - /* Pump Qt events one final time to give remaining signals time to be - * processed (since this happens after the destroy thread finishes and - * the audio/video threads have processed their tasks). */ - QApplication::sendPostedEvents(nullptr); - - unsetCursor(); - - /* If scene data wasn't actually cleared, e.g. faulty plugin holding a - * reference, they will still be in the hash table, enumerate them and - * store the names for logging purposes. */ - auto cb2 = [](void *param, obs_source_t *source) { - auto orphans = static_cast *>(param); - orphans->push_back(obs_source_get_name(source)); - return true; - }; - - vector orphan_sources; - obs_enum_sources(cb2, &orphan_sources); - - if (!orphan_sources.empty()) { - /* Avoid logging list twice in case it gets called after - * setting the flag the first time. */ - if (!clearingFailed) { - /* This ugly mess exists to join a vector of strings - * with a user-defined delimiter. */ - string orphan_names = - std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), - [](string a, string b) { return std::move(a) + "\n- " + b; }); - - blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", - orphan_names.c_str()); - } - - /* We do not decrement disableSaving here to avoid OBS - * overwriting user data with garbage. */ - clearingFailed = true; - } else { - disableSaving--; - - blog(LOG_INFO, "All scene data cleared"); - blog(LOG_INFO, "------------------------------------------------"); - } -} - -void OBSBasic::closeEvent(QCloseEvent *event) -{ - /* Wait for multitrack video stream to start/finish processing in the background */ - if (setupStreamingGuard.valid() && - setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); - return; - } - - /* Do not close window if inside of a temporary event loop because we - * could be inside of an Auth::LoadUI call. Keep trying once per - * second until we've exit any known sub-loops. */ - if (os_atomic_load_long(&insideEventLoop) != 0) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); - return; - } - -#ifdef YOUTUBE_ENABLED - /* Also don't close the window if the youtube stream check is active */ - if (youtubeStreamCheckThread) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); - return; - } -#endif - - if (isVisible()) - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); - - if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { - SetShowing(true); - - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); - - if (button == QMessageBox::No) { - event->ignore(); - restart = false; - return; - } - } - - if (remux && !remux->close()) { - event->ignore(); - restart = false; - return; - } - - QWidget::closeEvent(event); - if (!event->isAccepted()) - return; - - blog(LOG_INFO, SHUTDOWN_SEPARATOR); - - closing = true; - - /* While closing, a resize event to OBSQTDisplay could be triggered. - * The graphics thread on macOS dispatches a lambda function to be - * executed asynchronously in the main thread. However, the display is - * sometimes deleted before the lambda function is actually executed. - * To avoid such a case, destroy displays earlier than others such as - * deleting browser docks. */ - ui->preview->DestroyDisplay(); - if (program) - program->DestroyDisplay(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - if (introCheckThread) - introCheckThread->wait(); - if (whatsNewInitThread) - whatsNewInitThread->wait(); - if (updateCheckThread) - updateCheckThread->wait(); - if (logUploadThread) - logUploadThread->wait(); - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); - } - - QApplication::sendPostedEvents(nullptr); - - signalHandlers.clear(); - - Auth::Save(); - SaveProjectNow(); - auth.reset(); - - delete extraBrowsers; - - config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); - -#ifdef BROWSER_AVAILABLE - if (cef) - SaveExtraBrowserDocks(); - - ClearExtraBrowserDocks(); -#endif - - OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); - - disableSaving++; - - /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, - * sources, etc) so that all references are released before shutdown */ - ClearSceneData(); - - OnEvent(OBS_FRONTEND_EVENT_EXIT); - - // Destroys the frontend API so plugins can't continue calling it - obs_frontend_set_callbacks_internal(nullptr); - api = nullptr; - - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); -} - -bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) -{ -#ifdef _WIN32 - const MSG &msg = *static_cast(message); - switch (msg.message) { - case WM_MOVE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnMove(); - } - break; - case WM_DISPLAYCHANGE: - for (OBSQTDisplay *const display : findChildren()) { - display->OnDisplayChange(); - } - } -#else - UNUSED_PARAMETER(message); -#endif - - return false; -} - -void OBSBasic::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::WindowStateChange) { - QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; - - if (isMinimized()) { - if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { - ToggleShowHide(); - return; - } - - if (previewEnabled) - EnablePreviewDisplay(false); - } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { - if (previewEnabled) - EnablePreviewDisplay(true); - } - } -} - -void OBSBasic::on_actionShow_Recordings_triggered() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); - const char *adv_path = strcmp(type, "Standard") - ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : adv_path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); -} - -void OBSBasic::on_actionRemux_triggered() -{ - if (!remux.isNull()) { - remux->show(); - remux->raise(); - return; - } - - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); - - OBSRemux *remuxDlg; - remuxDlg = new OBSRemux(path, this); - remuxDlg->show(); - remux = remuxDlg; -} - -void OBSBasic::on_action_Settings_triggered() -{ - static bool settings_already_executing = false; - - /* Do not load settings window if inside of a temporary event loop - * because we could be inside of an Auth::LoadUI call. Keep trying - * once per second until we've exit any known sub-loops. */ - if (os_atomic_load_long(&insideEventLoop) != 0) { - QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); - return; - } - - if (settings_already_executing) { - return; - } - - settings_already_executing = true; - - { - OBSBasicSettings settings(this); - settings.exec(); - } - - settings_already_executing = false; - - if (restart) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); - - if (button == QMessageBox::Yes) - close(); - else - restart = false; - } -} - -void OBSBasic::on_actionShowMacPermissions_triggered() -{ -#ifdef __APPLE__ - OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), - CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); - check.exec(); -#endif -} - -void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) -{ - if (obs_missing_files_count(files) > 0) { - /* When loading the missing files dialog on launch, the - * window hasn't fully initialized by this point on macOS, - * so put this at the end of the current task queue. Fixes - * a bug where the window is behind OBS on startup. */ - QTimer::singleShot(0, [this, files] { - missDialog = new OBSMissingFiles(files, this); - missDialog->setAttribute(Qt::WA_DeleteOnClose, true); - missDialog->show(); - missDialog->raise(); - }); - } else { - obs_missing_files_destroy(files); - - /* Only raise dialog if triggered manually */ - if (!disableSaving) - OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), - QTStr("MissingFiles.NoMissing.Text")); - } -} - -void OBSBasic::on_actionShowMissingFiles_triggered() -{ - obs_missing_files_t *files = obs_missing_files_create(); - - auto cb_sources = [](void *data, obs_source_t *source) { - AddMissingFiles(data, source); - return true; - }; - - obs_enum_all_sources(cb_sources, files); - ShowMissingFilesDialog(files); -} - -void OBSBasic::on_actionAdvAudioProperties_triggered() -{ - if (advAudioWindow != nullptr) { - advAudioWindow->raise(); - return; - } - - bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); - - advAudioWindow = new OBSBasicAdvAudio(this); - advAudioWindow->show(); - advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); - advAudioWindow->SetIconsVisible(iconsVisible); -} - -void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() -{ - on_actionAdvAudioProperties_triggered(); -} - -void OBSBasic::on_actionMixerToolbarMenu_triggered() -{ - QAction unhideAllAction(QTStr("UnhideAll"), this); - connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); - - QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); - toggleControlLayoutAction.setCheckable(true); - toggleControlLayoutAction.setChecked( - config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, - Qt::DirectConnection); - - QMenu popup; - popup.addAction(&unhideAllAction); - popup.addSeparator(); - popup.addAction(&toggleControlLayoutAction); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - OBSSource source; - - if (current) { - OBSScene scene = GetOBSRef(current); - source = obs_scene_get_source(scene); - - currentScene = scene; - } else { - currentScene = NULL; - } - - SetCurrentScene(source); - - if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) - outputHandler->UpdateVirtualCamOutputSource(); - - OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); - - UpdateContextBar(); -} - -void OBSBasic::EditSceneName() -{ - ui->scenesDock->removeAction(renameScene); - QListWidgetItem *item = ui->scenes->currentItem(); - Qt::ItemFlags flags = item->flags(); - - item->setFlags(flags | Qt::ItemIsEditable); - ui->scenes->editItem(item); - item->setFlags(flags); -} - -QList OBSBasic::GetProjectorMenuMonitorsFormatted() -{ - QList projectorsFormatted; - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QRect screenGeometry = screen->geometry(); - qreal ratio = screen->devicePixelRatio(); - QString name = ""; -#if defined(__APPLE__) || defined(_WIN32) - name = screen->name(); -#else - name = screen->model().simplified(); - - if (name.length() > 1 && name.endsWith("-")) - name.chop(1); -#endif - name = name.simplified(); - - if (name.length() == 0) { - name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); - } - QString str = QString("%1: %2x%3 @ %4,%5") - .arg(name, QString::number(screenGeometry.width() * ratio), - QString::number(screenGeometry.height() * ratio), - QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); - projectorsFormatted.push_back(str); - } - return projectorsFormatted; -} - -void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *item = ui->scenes->itemAt(pos); - - QMenu popup(this); - QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); - - popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); - - if (item) { - QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); - copyFilters->setEnabled(false); - connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); - QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); - pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); - connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); - - popup.addSeparator(); - popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); - popup.addAction(copyFilters); - popup.addAction(pasteFilters); - popup.addSeparator(); - popup.addAction(renameScene); - popup.addAction(ui->actionRemoveScene); - popup.addSeparator(); - - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, - &OBSBasic::on_actionSceneDown_triggered); - order.addSeparator(); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); - order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); - popup.addMenu(&order); - - popup.addSeparator(); - - delete sceneProjectorMenu; - sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); - AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); - popup.addMenu(sceneProjectorMenu); - - QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); - - popup.addAction(sceneWindow); - popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); - popup.addSeparator(); - popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); - - popup.addSeparator(); - - delete perSceneTransitionMenu; - perSceneTransitionMenu = CreatePerSceneTransitionMenu(); - popup.addMenu(perSceneTransitionMenu); - - /* ---------------------- */ - - QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); - - OBSSource source = GetCurrentSceneSource(); - OBSDataAutoRelease data = obs_source_get_private_settings(source); - - obs_data_set_default_bool(data, "show_in_multiview", true); - bool show = obs_data_get_bool(data, "show_in_multiview"); - - multiviewAction->setCheckable(true); - multiviewAction->setChecked(show); - - auto showInMultiview = [](OBSData data) { - bool show = obs_data_get_bool(data, "show_in_multiview"); - obs_data_set_bool(data, "show_in_multiview", !show); - OBSProjector::UpdateMultiviewProjectors(); - }; - - connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); - - copyFilters->setEnabled(obs_source_filter_count(source) > 0); - } - - popup.addSeparator(); - - bool grid = ui->scenes->GetGridMode(); - - QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); - connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); - popup.addAction(gridAction); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionSceneListMode_triggered() -{ - ui->scenes->SetGridMode(false); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_actionSceneGridMode_triggered() -{ - ui->scenes->SetGridMode(true); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); -} - -void OBSBasic::GridActionClicked() -{ - bool gridMode = !ui->scenes->GetGridMode(); - ui->scenes->SetGridMode(gridMode); - - if (gridMode) - ui->actionSceneGridMode->setChecked(true); - else - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); -} - -void OBSBasic::on_actionAddScene_triggered() -{ - string name; - QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; - - int i = 2; - QString placeHolderText = format.arg(i); - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { - placeHolderText = format.arg(++i); - } - - bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), - QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); - - if (accepted) { - if (name.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - on_actionAddScene_triggered(); - return; - } - - OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); - if (source) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - - on_actionAddScene_triggered(); - return; - } - - auto undo_fn = [](const std::string &data) { - obs_source_t *t = obs_get_source_by_name(data.c_str()); - if (t) { - obs_source_remove(t); - obs_source_release(t); - } - }; - - auto redo_fn = [this](const std::string &data) { - OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); - obs_source_t *source = obs_scene_get_source(scene); - SetCurrentScene(source, true); - }; - undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); - - OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); - obs_source_t *scene_source = obs_scene_get_source(scene); - SetCurrentScene(scene_source); - } -} - -void OBSBasic::on_actionRemoveScene_triggered() -{ - RemoveSelectedScene(); -} - -void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) -{ - int idx = ui->scenes->currentRow(); - if (idx == -1 || idx == invalidIdx) - return; - - ui->scenes->blockSignals(true); - QListWidgetItem *item = ui->scenes->takeItem(idx); - - if (!relative) - idx = 0; - - ui->scenes->insertItem(idx + offset, item); - ui->scenes->setCurrentRow(idx + offset); - item->setSelected(true); - currentScene = GetOBSRef(item).Get(); - ui->scenes->blockSignals(false); - - OBSProjector::UpdateMultiviewProjectors(); -} - -void OBSBasic::on_actionSceneUp_triggered() -{ - ChangeSceneIndex(true, -1, 0); -} - -void OBSBasic::on_actionSceneDown_triggered() -{ - ChangeSceneIndex(true, 1, ui->scenes->count() - 1); -} - -void OBSBasic::MoveSceneToTop() -{ - ChangeSceneIndex(false, 0, 0); -} - -void OBSBasic::MoveSceneToBottom() -{ - ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); -} - -void OBSBasic::EditSceneItemName() -{ - int idx = GetTopSelectedSourceItem(); - ui->sources->Edit(idx); -} - -void OBSBasic::SetDeinterlacingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_mode(source, mode); -} - -void OBSBasic::SetDeinterlacingOrder() -{ - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - - obs_source_set_deinterlace_field_order(source, order); -} - -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) -{ - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE - - menu->addSeparator(); - -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); - - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER - - return menu; -} - -void OBSBasic::SetScaleFilter() -{ - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_scale_filter(sceneItem, mode); -} - -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); - - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMethod() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_method(sceneItem, method); -} - -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; - -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); - - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE - - return menu; -} - -void OBSBasic::SetBlendingMode() -{ - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_sceneitem_set_blending_mode(sceneItem, mode); -} - -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) -{ - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; - -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE - - return menu; -} - -QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item) -{ - QAction *action; - - menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" - "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" - "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" - "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" - "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" - "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" - "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" - "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); - - obs_data_t *privData = obs_sceneitem_get_private_settings(item); - obs_data_release(privData); - - obs_data_set_default_int(privData, "color-preset", 0); - int preset = obs_data_get_int(privData, "color-preset"); - - action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 0); - action->setChecked(preset == 0); - - action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); - action->setCheckable(true); - action->setProperty("bgColor", 1); - action->setChecked(preset == 1); - - menu->addSeparator(); - - widgetAction->setDefaultWidget(select); - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *colorButton = select->findChild(button.str().c_str()); - if (preset == i + 1) - colorButton->setStyleSheet("border: 2px solid black"); - - colorButton->setProperty("bgColor", i); - select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); - } - - menu->addAction(widgetAction); - - return menu; -} - -ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) -{ - ui->setupUi(this); -} - -void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) -{ - QMenu popup(this); - delete previewProjectorSource; - delete sourceProjector; - delete scaleFilteringMenu; - delete blendingMethodMenu; - delete blendingModeMenu; - delete colorMenu; - delete colorWidgetAction; - delete colorSelect; - delete deinterlaceMenu; - - if (preview) { - QAction *action = - popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - if (IsPreviewProgramMode()) - action->setEnabled(false); - - popup.addAction(ui->actionLockPreview); - popup.addMenu(ui->scalingMenu); - - previewProjectorSource = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); - - popup.addMenu(previewProjectorSource); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addAction(previewWindow); - - popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); - - popup.addSeparator(); - } - - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) - popup.addMenu(addSourceMenu); - - if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { - popup.addSeparator(); - popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); - } - - popup.addSeparator(); - popup.addAction(ui->actionCopySource); - popup.addAction(ui->actionPasteRef); - popup.addAction(ui->actionPasteDup); - popup.addSeparator(); - - popup.addSeparator(); - popup.addAction(ui->actionCopyFilters); - popup.addAction(ui->actionPasteFilters); - popup.addSeparator(); - - if (idx != -1) { - if (addSourceMenu) - popup.addSeparator(); - - OBSSceneItem sceneItem = ui->sources->Get(idx); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - uint32_t flags = obs_source_get_output_flags(source); - bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; - bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; - bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; - - colorMenu = new QMenu(QTStr("ChangeBG")); - colorWidgetAction = new QWidgetAction(colorMenu); - colorSelect = new ColorSelect(colorMenu); - popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); - popup.addAction(renameSource); - popup.addAction(ui->actionRemoveSource); - popup.addSeparator(); - - popup.addMenu(ui->orderMenu); - - if (hasVideo) - popup.addMenu(ui->transformMenu); - - popup.addSeparator(); - - if (hasAudio) { - QAction *actionHideMixer = - popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); - actionHideMixer->setCheckable(true); - actionHideMixer->setChecked(SourceMixerHidden(source)); - popup.addSeparator(); - } - - if (hasVideo) { - QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, - &OBSBasic::ResizeOutputSizeOfSource); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - resizeOutput->setEnabled(!obs_video_active()); - - if (width < 32 || height < 32) - resizeOutput->setEnabled(false); - - scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); - blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); - blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); - if (isAsyncVideo) { - deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); - } - - popup.addSeparator(); - - popup.addMenu(CreateVisibilityTransitionMenu(true)); - popup.addMenu(CreateVisibilityTransitionMenu(false)); - popup.addSeparator(); - - sourceProjector = new QMenu(QTStr("SourceProjector")); - AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); - popup.addMenu(sourceProjector); - popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); - - popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); - } - - popup.addSeparator(); - - if (flags & OBS_SOURCE_INTERACTION) - popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); - - popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); - QAction *action = - popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); - action->setEnabled(obs_source_configurable(source)); - } - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) -{ - if (ui->scenes->count()) { - QModelIndex idx = ui->sources->indexAt(pos); - CreateSourcePopupMenu(idx.row(), false); - } -} - -void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - if (IsPreviewProgramMode()) { - bool doubleClickSwitch = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); - - if (doubleClickSwitch) - TransitionClicked(); - } -} - -static inline bool should_show_properties(obs_source_t *source, const char *id) -{ - if (!source) - return false; - if (strcmp(id, "group") == 0) - return false; - if (!obs_source_configurable(source)) - return false; - - uint32_t caps = obs_source_get_output_flags(source); - if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) - return false; - - return true; -} - -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("Add"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); - return; - } - - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); -} - -void OBSBasic::on_actionAddSource_triggered() -{ - AddSourcePopupMenu(QCursor::pos()); -} - -static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - vector &items = *reinterpret_cast *>(param); - - if (obs_sceneitem_selected(item)) { - items.emplace_back(item); - } else if (obs_sceneitem_is_group(item)) { - obs_sceneitem_group_enum_items(item, remove_items, &items); - } - return true; -}; - -OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) -{ - OBSDataArrayAutoRelease undo_array = obs_data_array_create(); - - if (!sources) { - obs_scene_enum_items(scene, save_undo_source_enum, undo_array); - } else { - for (obs_source_t *source : *sources) { - obs_data_t *source_data = obs_save_source(source); - obs_data_array_push_back(undo_array, source_data); - obs_data_release(source_data); - } - } - - OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); - obs_data_array_push_back(undo_array, scene_data); - - OBSDataAutoRelease data = obs_data_create(); - - obs_data_set_array(data, "array", undo_array); - obs_data_get_json(data); - return data.Get(); -} - -static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) -{ - auto sources = static_cast *>(p); - sources->push_back(obs_sceneitem_get_source(item)); - return true; -} - -void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); - std::vector sources; - std::vector old_sources; - - /* create missing sources */ - const size_t count = obs_data_array_count(array); - sources.reserve(count); - - for (size_t i = 0; i < count; i++) { - OBSDataAutoRelease data = obs_data_array_item(array, i); - const char *name = obs_data_get_string(data, "name"); - - OBSSourceAutoRelease source = obs_get_source_by_name(name); - if (!source) - source = obs_load_source(data); - - sources.push_back(source.Get()); - - /* update scene/group settings to restore their - * contents to their saved settings */ - obs_scene_t *scene = obs_group_or_scene_from_source(source); - if (scene) { - obs_scene_enum_items(scene, add_source_enum, &old_sources); - OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); - obs_source_update(source, scene_settings); - } - } - - /* actually load sources now */ - for (obs_source_t *source : sources) - obs_source_load2(source); - - ui->sources->RefreshItems(); - }; - - const char *undo_json = obs_data_get_last_json(undo_data); - const char *redo_json = obs_data_get_last_json(redo_data); - - undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); -} - -void OBSBasic::on_actionRemoveSource_triggered() -{ - vector items; - OBSScene scene = GetCurrentScene(); - obs_source_t *scene_source = obs_scene_get_source(scene); - - obs_scene_enum_items(scene, remove_items, &items); - - if (!items.size()) - return; - - /* ------------------------------------- */ - /* confirm action with user */ - - bool confirmed = false; - - if (items.size() > 1) { - QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); - - QMessageBox remove_items(this); - remove_items.setText(text); - QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); - remove_items.setDefaultButton(Yes); - remove_items.addButton(QTStr("No"), QMessageBox::NoRole); - remove_items.setIcon(QMessageBox::Question); - remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); - remove_items.exec(); - - confirmed = Yes == remove_items.clickedButton(); - } else { - OBSSceneItem &item = items[0]; - obs_source_t *source = obs_sceneitem_get_source(item); - if (source && QueryRemoveSource(source)) - confirmed = true; - } - if (!confirmed) - return; - - /* ----------------------------------------------- */ - /* save undo data */ - - OBSData undo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* remove items */ - - for (auto &item : items) - obs_sceneitem_remove(item); - - /* ----------------------------------------------- */ - /* save redo data */ - - OBSData redo_data = BackupScene(scene_source); - - /* ----------------------------------------------- */ - /* add undo/redo action */ - - QString action_name; - if (items.size() > 1) { - action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); - } else { - QString str = QTStr("Undo.Delete"); - action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); - } - - CreateSceneUndoRedoAction(action_name, undo_data, redo_data); -} - -void OBSBasic::on_actionInteract_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreateInteractionWindow(source); -} - -void OBSBasic::on_actionSourceProperties_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); -} - -void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) -{ - OBSSceneItem item = GetCurrentSceneItem(); - obs_source_t *source = obs_sceneitem_get_source(item); - - if (!source) - return; - - OBSScene scene = GetCurrentScene(); - std::vector sources; - if (scene != obs_sceneitem_get_scene(item)) - sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); - - OBSData undo_data = BackupScene(scene, &sources); - - obs_sceneitem_set_order(item, movement); - - const char *source_name = obs_source_get_name(source); - const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); - - OBSData redo_data = BackupScene(scene, &sources); - CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionSourceUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionSourceDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveUp_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); -} - -void OBSBasic::on_actionMoveDown_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); -} - -void OBSBasic::on_actionMoveToTop_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); -} - -void OBSBasic::on_actionMoveToBottom_triggered() -{ - MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); -} - -static BPtr ReadLogFile(const char *subdir, const char *log) -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) - return nullptr; - - string path = logDir; - path += "/"; - path += log; - - BPtr file = os_quick_read_utf8_file(path.c_str()); - if (!file) - blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); - - return file; -} - -void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) -{ - BPtr fileString{ReadLogFile(subdir, file)}; - - if (!fileString) - return; - - if (!*fileString) - return; - - ui->menuLogFiles->setEnabled(false); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(false); -#endif - - stringstream ss; - ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" - << fileString; - - if (logUploadThread) { - logUploadThread->wait(); - } - - RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); - - logUploadThread.reset(thread); - if (crash) { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); - } else { - connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); - } - logUploadThread->start(); -} - -void OBSBasic::on_actionShowLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadCurrentLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); -} - -void OBSBasic::on_actionUploadLastLog_triggered() -{ - UploadLog("obs-studio/logs", App()->GetLastLog(), false); -} - -void OBSBasic::on_actionViewCurrentLog_triggered() -{ - if (!logView) - logView = new OBSLogViewer(); - - logView->show(); - logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - logView->activateWindow(); - logView->raise(); -} - -void OBSBasic::on_actionShowCrashLogs_triggered() -{ - char logDir[512]; - if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) - return; - - QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionUploadLastCrashLog_triggered() -{ - UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); -} - -void OBSBasic::on_actionCheckForUpdates_triggered() -{ - CheckForUpdates(true); -} - -void OBSBasic::on_actionRepair_triggered() -{ -#if defined(_WIN32) - ui->actionCheckForUpdates->setEnabled(false); - ui->actionRepair->setEnabled(false); - - if (updateCheckThread && updateCheckThread->isRunning()) - return; - - updateCheckThread.reset(new AutoUpdateThread(false, true)); - updateCheckThread->start(); -#endif -} - -void OBSBasic::on_actionRestartSafe_triggered() -{ - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); - - if (button == QMessageBox::Yes) { - restart = safe_mode; - restart_safe = !safe_mode; - close(); - } -} - -void OBSBasic::logUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, false); -} - -void OBSBasic::crashUploadFinished(const QString &text, const QString &error) -{ - ui->menuLogFiles->setEnabled(true); -#if defined(_WIN32) - ui->menuCrashLogs->setEnabled(true); -#endif - - if (text.isEmpty()) { - OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); - return; - } - openLogDialog(text, true); -} - -void OBSBasic::openLogDialog(const QString &text, const bool crash) -{ - - OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); - string resURL = obs_data_get_string(returnData, "url"); - QString logURL = resURL.c_str(); - - OBSLogReply logDialog(this, logURL, crash); - logDialog.exec(); -} - -static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) -{ - const char *prevName = obs_source_get_name(source); - if (name == prevName) - return; - - OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); - QListWidgetItem *listItem = listWidget->currentItem(); - - if (foundSource || name.empty()) { - listItem->setText(QT_UTF8(prevName)); - - if (foundSource) { - OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - } else if (name.empty()) { - OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - } - } else { - auto undo = [prev = std::string(prevName)](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, prev.c_str()); - }; - - auto redo = [name](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); - obs_source_set_name(source, name.c_str()); - }; - - std::string source_uuid(obs_source_get_uuid(source)); - parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); - - listItem->setText(QT_UTF8(name.c_str())); - obs_source_set_name(source, name.c_str()); - } -} - -void OBSBasic::SceneNameEdited(QWidget *editor) -{ - OBSScene scene = GetCurrentScene(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!scene) - return; - - obs_source_t *source = obs_scene_get_source(scene); - RenameListItem(this, ui->scenes, source, text); - - ui->scenesDock->addAction(renameScene); - - OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); -} - -void OBSBasic::OpenFilters(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateFiltersWindow(source); -} - -void OBSBasic::OpenProperties(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreatePropertiesWindow(source); -} - -void OBSBasic::OpenInteraction(OBSSource source) -{ - if (source == nullptr) { - OBSSceneItem item = GetCurrentSceneItem(); - source = obs_sceneitem_get_source(item); - } - CreateInteractionWindow(source); -} - -void OBSBasic::OpenEditTransform(OBSSceneItem item) -{ - if (!item) - item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::OpenSceneFilters() -{ - OBSScene scene = GetCurrentScene(); - OBSSource source = obs_scene_get_source(scene); - - CreateFiltersWindow(source); -} - -#define RECORDING_START "==== Recording Start ===============================================" -#define RECORDING_STOP "==== Recording Stop ================================================" -#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" -#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" -#define STREAMING_START "==== Streaming Start ===============================================" -#define STREAMING_STOP "==== Streaming Stop ================================================" -#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" -#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" - -void OBSBasic::DisplayStreamStartError() -{ - QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) - : QTStr("Output.StartFailedGeneric"); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); -} - -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string b_id = QT_TO_UTF8(broadcast_id); - obs_data_set_string(settings, "broadcast_id", b_id.c_str()); - - const std::string s_id = QT_TO_UTF8(stream_id); - obs_data_set_string(settings, "stream_id", s_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - emit BroadcastStreamReady(broadcastReady); - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); - if (!apiYouTube) { - /* technically we should never get here -Lain */ - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); - blog(LOG_ERROR, "=========================================="); - blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); - blog(LOG_ERROR, "=========================================="); - return; - } - - int timeout = 0; - json11::Json json; - QString id = key.c_str(); - - while (StreamingActive()) { - if (timeout == 14) { - QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - break; - } - - if (!apiYouTube->FindStream(id, json)) { - QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); - break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { - emit BroadcastStreamActive(); - break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} -#endif - -void OBSBasic::StartStreaming() -{ - if (outputHandler->StreamingActive()) - return; - if (disableOutputsRef) - return; - - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { - QMessageBox no_broadcast(this); - no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); - QPushButton *SetupBroadcast = - no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); - no_broadcast.setDefaultButton(SetupBroadcast); - no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); - no_broadcast.setIcon(QMessageBox::Information); - no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); - no_broadcast.exec(); - - if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, "SetupBroadcast"); - return; - } - } - - emit StreamingPreparing(); - - if (sysTrayStream) { - sysTrayStream->setEnabled(false); - sysTrayStream->setText("Basic.Main.PreparingStream"); - } - - auto finish_stream_setup = [&](bool setupStreamingResult) { - if (!setupStreamingResult) { - DisplayStreamStartError(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); - - emit StreamingStarting(autoStartBroadcast); - - if (sysTrayStream) - sysTrayStream->setText("Basic.Main.Connecting"); - - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (autoStartBroadcast) { - emit BroadcastStreamStarted(autoStopBroadcast); - broadcastActive = true; - } - - bool recordWhenStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif - }; - - setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); -} - -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - -#ifdef _WIN32 -static inline void UpdateProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority(priority); -} - -static inline void ClearProcessPriority() -{ - const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); - if (priority && strcmp(priority, "Normal") != 0) - SetProcessPriority("Normal"); -} -#else -#define UpdateProcessPriority() \ - do { \ - } while (false) -#define ClearProcessPriority() \ - do { \ - } while (false) -#endif - -inline void OBSBasic::OnActivate(bool force) -{ - if (ui->profileMenu->isEnabled() || force) { - ui->profileMenu->setEnabled(false); - ui->autoConfigure->setEnabled(false); - App()->IncrementSleepInhibition(); - UpdateProcessPriority(); - - struct obs_video_info ovi; - obs_get_video_info(&ovi); - lastOutputResolution = {ovi.base_width, ovi.base_height}; - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); - trayMask.setIsMask(true); - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); -#else - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); -#endif - } - } -} - -extern volatile bool recording_paused; -extern volatile bool replaybuf_active; - -inline void OBSBasic::OnDeactivate() -{ - if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { - ui->profileMenu->setEnabled(true); - ui->autoConfigure->setEnabled(true); - App()->DecrementSleepInhibition(); - ClearProcessPriority(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); - } - } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { - if (os_atomic_load_bool(&recording_paused)) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - } else { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - } - } -} - -void OBSBasic::StopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(streamingStopping); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::ForceStopStreaming() -{ - SaveProject(); - - if (outputHandler->StreamingActive()) - outputHandler->StopStreaming(true); - - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } - - emit BroadcastStreamReady(broadcastReady); - - OnDeactivate(); - - bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); - bool keepRecordingWhenStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); - if (recordWhenStreaming && !keepRecordingWhenStreamStops) - StopRecording(); - - bool replayBufferWhileStreaming = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - bool keepReplayBufferStreamStops = - config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); - if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) - StopReplayBuffer(); -} - -void OBSBasic::StreamDelayStarting(int sec) -{ - emit StreamingStarted(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStarting(sec); - - OnActivate(); -} - -void OBSBasic::StreamDelayStopping(int sec) -{ - emit StreamingStopped(true); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - ui->statusbar->StreamDelayStopping(sec); - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStart() -{ - emit StreamingStarted(); - OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); - ui->statusbar->StreamStarted(output); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); - sysTrayStream->setEnabled(true); - } - -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } - } -#endif - - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); - - OnActivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStarted(); -#endif - - blog(LOG_INFO, STREAMING_START); -} - -void OBSBasic::StreamStopping() -{ - emit StreamingStopping(); - - if (sysTrayStream) - sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); - - streamingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); -} - -void OBSBasic::StreamingStop(int code, QString last_error) -{ - const char *errorDescription = ""; - DStr errorMessage; - bool use_last_error = false; - bool encode_error = false; - - switch (code) { - case OBS_OUTPUT_BAD_PATH: - errorDescription = Str("Output.ConnectFail.BadPath"); - break; - - case OBS_OUTPUT_CONNECT_FAILED: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.ConnectFailed"); - break; - - case OBS_OUTPUT_INVALID_STREAM: - errorDescription = Str("Output.ConnectFail.InvalidStream"); - break; - - case OBS_OUTPUT_ENCODE_ERROR: - encode_error = true; - break; - - case OBS_OUTPUT_HDR_DISABLED: - errorDescription = Str("Output.ConnectFail.HdrDisabled"); - break; - - default: - case OBS_OUTPUT_ERROR: - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Error"); - break; - - case OBS_OUTPUT_DISCONNECTED: - /* doesn't happen if output is set to reconnect. note that - * reconnects are handled in the output, not in the UI */ - use_last_error = true; - errorDescription = Str("Output.ConnectFail.Disconnected"); - } - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - ui->statusbar->StreamStopped(); - - emit StreamingStopped(); - - if (sysTrayStream) { - sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); - sysTrayStream->setEnabled(true); - } - - streamingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); - - OnDeactivate(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) - youtubeAppDock->IngestionStopped(); -#endif - - blog(LOG_INFO, STREAMING_STOP); - - if (encode_error) { - QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") - : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); - OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); - } - - // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); -} - -void OBSBasic::AutoRemux(QString input, bool no_show) -{ - auto config = Config(); - - bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); - - if (!autoRemux) - return; - - bool isSimpleMode = false; - - const char *mode = config_get_string(config, "Output", "Mode"); - if (!mode) { - isSimpleMode = true; - } else { - isSimpleMode = strcmp(mode, "Simple") == 0; - } - - if (!isSimpleMode) { - const char *recType = config_get_string(config, "AdvOut", "RecType"); - - bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; - - if (ffmpegOutput) - return; - } - - if (input.isEmpty()) - return; - - QFileInfo fi(input); - QString suffix = fi.suffix(); - - /* do not remux if lossless */ - if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { - return; - } - - QString path = fi.path(); - - QString output = input; - output.resize(output.size() - suffix.size()); - - const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); - const char *vCodecName = obs_encoder_get_codec(videoEncoder); - const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); - - /* Retain original container for fMP4/fMOV */ - if (strncmp(format, "fragmented", 10) == 0) { - output += "remuxed." + suffix; - } else if (strcmp(vCodecName, "prores") == 0) { - output += "mov"; - } else { - output += "mp4"; - } - - OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - if (!no_show) - remux->show(); - remux->AutoRemux(input, output); -} - -void OBSBasic::StartRecording() -{ - if (outputHandler->RecordingActive()) - return; - if (disableOutputsRef) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (!IsFFmpegOutputToURL() && LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); - - SaveProject(); - - outputHandler->StartRecording(); -} - -void OBSBasic::RecordStopping() -{ - emit RecordingStopping(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); - - recordingStopping = true; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); -} - -void OBSBasic::StopRecording() -{ - SaveProject(); - - if (outputHandler->RecordingActive()) - outputHandler->StopRecording(recordingStopping); - - OnDeactivate(); -} - -void OBSBasic::RecordingStart() -{ - ui->statusbar->RecordingStarted(outputHandler->fileOutput); - emit RecordingStarted(isRecordingPausable); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); - - recordingStopping = false; - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); - - if (!diskFullTimer->isActive()) - diskFullTimer->start(1000); - - OnActivate(); - - blog(LOG_INFO, RECORDING_START); -} - -void OBSBasic::RecordingStop(int code, QString last_error) -{ - ui->statusbar->RecordingStopped(); - emit RecordingStopped(); - - if (sysTrayRecord) - sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); - - blog(LOG_INFO, RECORDING_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { - QString msg = last_error.isEmpty() - ? QTStr("Output.RecordError.EncodeErrorMsg") - : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); - OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - - const char *errorDescription; - DStr errorMessage; - bool use_last_error = true; - - errorDescription = Str("Output.RecordError.Msg"); - - if (use_last_error && !last_error.isEmpty()) - dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); - else - dstr_copy(errorMessage, errorDescription); - - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } else if (code == OBS_OUTPUT_SUCCESS) { - if (outputHandler) { - std::string path = outputHandler->lastRecordingPath; - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); - } - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); - - if (diskFullTimer->isActive()) - diskFullTimer->stop(); - - AutoRemux(outputHandler->lastRecordingPath.c_str()); - - OnDeactivate(); -} - -void OBSBasic::RecordingFileChanged(QString lastRecordingPath) -{ - QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); - ShowStatusBarMessage(str.arg(lastRecordingPath)); - - AutoRemux(lastRecordingPath, true); -} - -void OBSBasic::ShowReplayBufferPauseWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." - "PauseWarning.Title")); - msgbox.setText(QTStr("Output.ReplayBuffer." - "PauseWarning.Text")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); - } -} - -void OBSBasic::StartReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (outputHandler->ReplayBufferActive()) - return; - if (disableOutputsRef) - return; - - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - if (!OutputPathValid()) { - OutputPathInvalidMessage(); - return; - } - - if (LowDiskSpace()) { - DiskSpaceMessage(); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); - - SaveProject(); - - if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::ReplayBufferStopping() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopping(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); - - replayBufferStopping = true; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); -} - -void OBSBasic::StopReplayBuffer() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - SaveProject(); - - if (outputHandler->ReplayBufferActive()) - outputHandler->StopReplayBuffer(replayBufferStopping); - - OnDeactivate(); -} - -void OBSBasic::ReplayBufferStart() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStarted(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); - - replayBufferStopping = false; - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); - - OnActivate(); - - blog(LOG_INFO, REPLAY_BUFFER_START); -} - -void OBSBasic::ReplayBufferSave() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "save", &cd); - calldata_free(&cd); -} - -void OBSBasic::ReplayBufferSaved() -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - if (!outputHandler->ReplayBufferActive()) - return; - - calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); - proc_handler_call(ph, "get_last_replay", &cd); - std::string path = calldata_string(&cd, "path"); - QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); - ShowStatusBarMessage(msg); - lastReplay = path; - calldata_free(&cd); - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); - - AutoRemux(QT_UTF8(path.c_str())); -} - -void OBSBasic::ReplayBufferStop(int code) -{ - if (!outputHandler || !outputHandler->replayBuffer) - return; - - emit ReplayBufStopped(); - - if (sysTrayReplayBuffer) - sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); - - blog(LOG_INFO, REPLAY_BUFFER_STOP); - - if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); - - } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { - OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); - - } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { - OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); - - } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); - - } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); - - } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { - SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); - } - - OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); - - OnDeactivate(); -} - -void OBSBasic::StartVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - if (outputHandler->VirtualCamActive()) - return; - if (disableOutputsRef) - return; - - SaveProject(); - - outputHandler->StartVirtualCam(); -} - -void OBSBasic::StopVirtualCam() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - SaveProject(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - OnDeactivate(); -} - -void OBSBasic::OnVirtualCamStart() -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStarted(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); - - OnActivate(); - - blog(LOG_INFO, VIRTUAL_CAM_START); -} - -void OBSBasic::OnVirtualCamStop(int) -{ - if (!outputHandler || !outputHandler->virtualCam) - return; - - emit VirtualCamStopped(); - - if (sysTrayVirtualCam) - sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); - - OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); - - blog(LOG_INFO, VIRTUAL_CAM_STOP); - - OnDeactivate(); - - if (!restartingVCam) - return; - - /* Restarting needs to be delayed to make sure that the virtual camera - * implementation is stopped and avoid race condition. */ - QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); -} - -void OBSBasic::StreamActionTriggered() -{ - if (outputHandler->StreamingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - - confirm = false; - } -#endif - if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StopStreaming(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - Auth *auth = GetAuth(); - - auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation(this, service); - switch (action) { - case StreamSettingsAction::ContinueStream: - break; - case StreamSettingsAction::OpenSettings: - on_action_Settings_triggered(); - return; - case StreamSettingsAction::Cancel: - return; - } - - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); - - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) - confirm = false; - } - - if (bwtest && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), - QTStr("ConfirmBWTest.Text")); - - if (button == QMessageBox::No) - return; - } else if (confirm && isVisible()) { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - StartStreaming(); - } -} - -void OBSBasic::RecordActionTriggered() -{ - if (outputHandler->RecordingActive()) { - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); - - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - StopRecording(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartRecording(); - } -} - -void OBSBasic::VirtualCamActionTriggered() -{ - if (outputHandler->VirtualCamActive()) { - StopVirtualCam(); - } else { - if (!UIValidation::NoSourcesConfirmation(this)) - return; - - StartVirtualCam(); - } -} - -void OBSBasic::OpenVirtualCamConfig() -{ - OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); - - connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); - connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); - - dialog.exec(); -} - -void log_vcam_changed(const VCamConfig &config, bool starting) -{ - const char *action = starting ? "Starting" : "Changing"; - - switch (config.type) { - case VCamOutputType::Invalid: - break; - case VCamOutputType::ProgramView: - blog(LOG_INFO, "%s Virtual Camera output to Program", action); - break; - case VCamOutputType::PreviewOutput: - blog(LOG_INFO, "%s Virtual Camera output to Preview", action); - break; - case VCamOutputType::SceneOutput: - blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); - break; - case VCamOutputType::SourceOutput: - blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); - break; - } -} - -void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) -{ - vcamConfig = config; - - outputHandler->UpdateVirtualCamOutputSource(); - log_vcam_changed(config, false); -} - -void OBSBasic::RestartVirtualCam(const VCamConfig &config) -{ - restartingVCam = true; - - StopVirtualCam(); - - vcamConfig = config; -} - -void OBSBasic::RestartingVirtualCam() -{ - if (!restartingVCam) - return; - - outputHandler->UpdateVirtualCamOutputSource(); - StartVirtualCam(); - restartingVCam = false; -} - -void OBSBasic::on_actionHelpPortal_triggered() -{ - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionWebsite_triggered() -{ - QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionDiscord_triggered() -{ - QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowWhatsNew_triggered() -{ -#ifdef WHATSNEW_ENABLED - if (introCheckThread && introCheckThread->isRunning()) - return; - if (!cef) - return; - - config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); - - WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); - connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); - - introCheckThread.reset(wnit); - introCheckThread->start(); -#endif -} - -void OBSBasic::on_actionReleaseNotes_triggered() -{ - QString addr("https://github.com/obsproject/obs-studio/releases"); - QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); - QDesktopServices::openUrl(url); -} - -void OBSBasic::on_actionShowSettingsFolder_triggered() -{ - const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; - const QString userConfigLocation = QString::fromStdString(userConfigPath); - - QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); -} - -void OBSBasic::on_actionShowProfileFolder_triggered() -{ - try { - const OBSProfile ¤tProfile = GetCurrentProfile(); - QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); - - QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); - } catch (const std::invalid_argument &error) { - blog(LOG_ERROR, "%s", error.what()); - } -} - -int OBSBasic::GetTopSelectedSourceItem() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - return selectedItems.count() ? selectedItems[0].row() : -1; -} - -QModelIndexList OBSBasic::GetAllSelectedSourceItems() -{ - return ui->sources->selectionModel()->selectedIndexes(); -} - -void OBSBasic::on_preview_customContextMenuRequested() -{ - CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); -} - -void OBSBasic::ProgramViewContextMenuRequested() -{ - QMenu popup(this); - QPointer studioProgramProjector; - - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - - popup.addMenu(studioProgramProjector); - popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); - popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); - - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() -{ - QMenu popup(this); - delete previewProjectorMain; - - QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); - action->setCheckable(true); - action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); - - previewProjectorMain = new QMenu(QTStr("PreviewProjector")); - AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); - - QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); - - popup.addMenu(previewProjectorMain); - popup.addAction(previewWindow); - popup.exec(QCursor::pos()); -} - -void OBSBasic::on_actionAlwaysOnTop_triggered() -{ -#ifndef _WIN32 - /* Make sure all dialogs are safely and successfully closed before - * switching the always on top mode due to the fact that windows all - * have to be recreated, so queue the actual toggle to happen after - * all events related to closing the dialogs have finished */ - CloseDialogs(); -#endif - - QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); -} - -void OBSBasic::ToggleAlwaysOnTop() -{ - bool isAlwaysOnTop = IsAlwaysOnTop(this); - - ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); - SetAlwaysOnTop(this, !isAlwaysOnTop); - - show(); -} - -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - /* - * else if (false) //"Nanoseconds", currently not implemented - * GetFPSNanoseconds(num, den); - */ - else - GetFPSCommon(num, den); -} - -config_t *OBSBasic::Config() const -{ - return activeConfiguration; -} - -#ifdef YOUTUBE_ENABLED -YouTubeAppDock *OBSBasic::GetYouTubeAppDock() -{ - return youtubeAppDock; -} - -#ifndef SEC_TO_NSEC -#define SEC_TO_NSEC 1000000000 -#endif - -void OBSBasic::NewYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - /* make sure that the youtube app dock can't be immediately recreated. - * dumb hack. blame chromium. or this particular dock. or both. if CEF - * creates/destroys/creates a widget too quickly it can lead to a - * crash. */ - uint64_t ts = os_gettime_ns(); - if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) - return; - - lastYouTubeAppDockCreationTime = ts; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); -} - -void OBSBasic::DeleteYouTubeAppDock() -{ - if (!cef_js_avail) - return; - - if (youtubeAppDock) - RemoveDockWidget(youtubeAppDock->objectName()); - - youtubeAppDock = nullptr; -} -#endif - -void OBSBasic::UpdateEditMenu() -{ - QModelIndexList items = GetAllSelectedSourceItems(); - int totalCount = items.count(); - size_t filter_count = 0; - - if (totalCount == 1) { - OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); - OBSSource source = obs_sceneitem_get_source(sceneItem); - filter_count = obs_source_filter_count(source); - } - - bool allowPastingDuplicate = !!clipboard.size(); - for (size_t i = clipboard.size(); i > 0; i--) { - const size_t idx = i - 1; - OBSWeakSource &weak = clipboard[idx].weak_source; - if (obs_weak_source_expired(weak)) { - clipboard.erase(clipboard.begin() + idx); - continue; - } - OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); - if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) - allowPastingDuplicate = false; - } - - int videoCount = 0; - bool canTransformMultiple = false; - for (int i = 0; i < totalCount; i++) { - OBSSceneItem item = ui->sources->Get(items.value(i).row()); - OBSSource source = obs_sceneitem_get_source(item); - const uint32_t flags = obs_source_get_output_flags(source); - const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; - if (hasVideo && !obs_sceneitem_locked(item)) - canTransformMultiple = true; - - if (hasVideo) - videoCount++; - } - const bool canTransformSingle = videoCount == 1 && totalCount == 1; - - OBSSceneItem curItem = GetCurrentSceneItem(); - bool locked = curItem && obs_sceneitem_locked(curItem); - - ui->actionCopySource->setEnabled(totalCount > 0); - ui->actionEditTransform->setEnabled(canTransformSingle && !locked); - ui->actionCopyTransform->setEnabled(canTransformSingle); - ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); - ui->actionCopyFilters->setEnabled(filter_count > 0); - ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); - ui->actionPasteRef->setEnabled(!!clipboard.size()); - ui->actionPasteDup->setEnabled(allowPastingDuplicate); - - ui->actionMoveUp->setEnabled(totalCount > 0); - ui->actionMoveDown->setEnabled(totalCount > 0); - ui->actionMoveToTop->setEnabled(totalCount > 0); - ui->actionMoveToBottom->setEnabled(totalCount > 0); - - ui->actionResetTransform->setEnabled(canTransformMultiple); - ui->actionRotate90CW->setEnabled(canTransformMultiple); - ui->actionRotate90CCW->setEnabled(canTransformMultiple); - ui->actionRotate180->setEnabled(canTransformMultiple); - ui->actionFlipHorizontal->setEnabled(canTransformMultiple); - ui->actionFlipVertical->setEnabled(canTransformMultiple); - ui->actionFitToScreen->setEnabled(canTransformMultiple); - ui->actionStretchToScreen->setEnabled(canTransformMultiple); - ui->actionCenterToScreen->setEnabled(canTransformMultiple); - ui->actionVerticalCenter->setEnabled(canTransformMultiple); - ui->actionHorizontalCenter->setEnabled(canTransformMultiple); -} - -void OBSBasic::on_actionEditTransform_triggered() -{ - const auto item = GetCurrentSceneItem(); - if (!item) - return; - CreateEditTransformWindow(item); -} - -void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) -{ - if (transformWindow) - transformWindow->close(); - transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); - transformWindow->show(); - transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::on_actionCopyTransform_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - obs_sceneitem_get_info2(item, &copiedTransformInfo); - obs_sceneitem_get_crop(item, &copiedCropInfo); - - ui->actionPasteTransform->setEnabled(true); - hasCopiedTransform = true; -} - -void undo_redo(const std::string &data) -{ - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); - reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); - - obs_scene_load_transform_states(data.c_str()); -} - -void OBSBasic::on_actionPasteTransform_triggered() -{ - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - OBSBasic *main = reinterpret_cast(data); - - obs_sceneitem_defer_update_begin(item); - obs_sceneitem_set_info2(item, &main->copiedTransformInfo); - obs_sceneitem_set_crop(item, &main->copiedCropInfo); - obs_sceneitem_defer_update_end(item); - - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, this); - - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, - undo_redo, undo_data, redo_data); -} - -static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, reset_tr, nullptr); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_sceneitem_defer_update_begin(item); - - obs_transform_info info; - vec2_set(&info.pos, 0.0f, 0.0f); - vec2_set(&info.scale, 1.0f, 1.0f); - info.rot = 0.0f; - info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; - info.bounds_type = OBS_BOUNDS_NONE; - info.bounds_alignment = OBS_ALIGN_CENTER; - info.crop_to_bounds = false; - vec2_set(&info.bounds, 0.0f, 0.0f); - obs_sceneitem_set_info2(item, &info); - - obs_sceneitem_crop crop = {}; - obs_sceneitem_set_crop(item, &crop); - - obs_sceneitem_defer_update_end(item); - - return true; -} - -void OBSBasic::on_actionResetTransform_triggered() -{ - OBSScene scene = GetCurrentScene(); - - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); - obs_scene_enum_items(scene, reset_tr, nullptr); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), - undo_redo, undo_redo, undo_data, redo_data); - - obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); -} - -static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) -{ - matrix4 boxTransform; - obs_sceneitem_get_box_transform(item, &boxTransform); - - vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); - vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); - - auto GetMinPos = [&](float x, float y) { - vec3 pos; - vec3_set(&pos, x, y, 0.0f); - vec3_transform(&pos, &pos, &boxTransform); - vec3_min(&tl, &tl, &pos); - vec3_max(&br, &br, &pos); - }; - - GetMinPos(0.0f, 0.0f); - GetMinPos(1.0f, 0.0f); - GetMinPos(0.0f, 1.0f); - GetMinPos(1.0f, 1.0f); -} - -static vec3 GetItemTL(obs_sceneitem_t *item) -{ - vec3 tl, br; - GetItemBox(item, tl, br); - return tl; -} - -static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) -{ - vec3 newTL; - vec2 pos; - - obs_sceneitem_get_pos(item, &pos); - newTL = GetItemTL(item); - pos.x += tl.x - newTL.x; - pos.y += tl.y - newTL.y; - obs_sceneitem_set_pos(item, &pos); -} - -static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - float rot = *reinterpret_cast(param); - - vec3 tl = GetItemTL(item); - - rot += obs_sceneitem_get_rot(item); - if (rot >= 360.0f) - rot -= 360.0f; - else if (rot <= -360.0f) - rot += 360.0f; - obs_sceneitem_set_rot(item, rot); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -}; - -void OBSBasic::on_actionRotate90CW_triggered() -{ - float f90CW = 90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate90CCW_triggered() -{ - float f90CCW = -90.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionRotate180_triggered() -{ - float f180 = 180.0f; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - vec2 &mul = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - vec3 tl = GetItemTL(item); - - vec2 scale; - obs_sceneitem_get_scale(item, &scale); - vec2_mul(&scale, &scale, &mul); - obs_sceneitem_set_scale(item, &scale); - - obs_sceneitem_force_update_transform(item); - - SetItemTL(item, tl); - - return true; -} - -void OBSBasic::on_actionFlipHorizontal_triggered() -{ - vec2 scale; - vec2_set(&scale, -1.0f, 1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionFlipVertical_triggered() -{ - vec2 scale; - vec2_set(&scale, 1.0f, -1.0f); - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) -{ - obs_bounds_type boundsType = *reinterpret_cast(param); - - if (obs_sceneitem_is_group(item)) - obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); - if (!obs_sceneitem_selected(item)) - return true; - if (obs_sceneitem_locked(item)) - return true; - - obs_video_info ovi; - obs_get_video_info(&ovi); - - obs_transform_info itemInfo; - vec2_set(&itemInfo.pos, 0.0f, 0.0f); - vec2_set(&itemInfo.scale, 1.0f, 1.0f); - itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; - itemInfo.rot = 0.0f; - - vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); - itemInfo.bounds_type = boundsType; - itemInfo.bounds_alignment = OBS_ALIGN_CENTER; - itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); - - obs_sceneitem_set_info2(item, &itemInfo); - - return true; -} - -void OBSBasic::on_actionFitToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionStretchToScreen_triggered() -{ - obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") - .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) -{ - QModelIndexList selectedItems = GetAllSelectedSourceItems(); - - if (!selectedItems.count()) - return; - - vector items; - - // Filter out items that have no size - for (int x = 0; x < selectedItems.count(); x++) { - OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); - obs_transform_info oti; - obs_sceneitem_get_info2(item, &oti); - - obs_source_t *source = obs_sceneitem_get_source(item); - float width = float(obs_source_get_width(source)) * oti.scale.x; - float height = float(obs_source_get_height(source)) * oti.scale.y; - - if (width == 0.0f || height == 0.0f) - continue; - - items.emplace_back(item); - } - - if (!items.size()) - return; - - // Get center x, y coordinates of items - vec3 center; - - float top = M_INFINITE; - float left = M_INFINITE; - float right = 0.0f; - float bottom = 0.0f; - - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - left = std::min(tl.x, left); - top = std::min(tl.y, top); - right = std::max(br.x, right); - bottom = std::max(br.y, bottom); - } - - center.x = (right + left) / 2.0f; - center.y = (top + bottom) / 2.0f; - center.z = 0.0f; - - // Get coordinates of screen center - obs_video_info ovi; - obs_get_video_info(&ovi); - - vec3 screenCenter; - vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); - - vec3_mulf(&screenCenter, &screenCenter, 0.5f); - - // Calculate difference between screen center and item center - vec3 offset; - vec3_sub(&offset, &screenCenter, ¢er); - - // Shift items by offset - for (auto &item : items) { - vec3 tl, br; - - GetItemBox(item, tl, br); - - vec3_add(&tl, &tl, &offset); - - vec3 itemTL = GetItemTL(item); - - if (centerType == CenterType::Vertical) - tl.x = itemTL.x; - else if (centerType == CenterType::Horizontal) - tl.y = itemTL.y; - - SetItemTL(item, tl); - } -} - -void OBSBasic::on_actionCenterToScreen_triggered() -{ - CenterType centerType = CenterType::Scene; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionVerticalCenter_triggered() -{ - CenterType centerType = CenterType::Vertical; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::on_actionHorizontalCenter_triggered() -{ - CenterType centerType = CenterType::Horizontal; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - CenterSelectedSceneItems(centerType); - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); - - std::string undo_data(obs_data_get_json(wrapper)); - std::string redo_data(obs_data_get_json(rwrapper)); - undo_s.add_action( - QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), - undo_redo, undo_redo, undo_data, redo_data); -} - -void OBSBasic::EnablePreviewDisplay(bool enable) -{ - obs_display_set_enabled(ui->preview->GetDisplay(), enable); - ui->previewContainer->setVisible(enable); - ui->previewDisabledWidget->setVisible(!enable); -} - -void OBSBasic::TogglePreview() -{ - previewEnabled = !previewEnabled; - EnablePreviewDisplay(previewEnabled); -} - -void OBSBasic::EnablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = true; - EnablePreviewDisplay(true); -} - -void OBSBasic::DisablePreview() -{ - if (previewProgramMode) - return; - - previewEnabled = false; - EnablePreviewDisplay(false); -} - -void OBSBasic::EnablePreviewProgram() -{ - SetPreviewProgramMode(true); -} - -void OBSBasic::DisablePreviewProgram() -{ - SetPreviewProgramMode(false); -} - -static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) -{ - if (obs_sceneitem_locked(item)) - return true; - - struct vec2 &offset = *reinterpret_cast(param); - struct vec2 pos; - - if (!obs_sceneitem_selected(item)) { - if (obs_sceneitem_is_group(item)) { - struct vec3 offset3; - vec3_set(&offset3, offset.x, offset.y, 0.0f); - - struct matrix4 matrix; - obs_sceneitem_get_draw_transform(item, &matrix); - vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); - matrix4_inv(&matrix, &matrix); - vec3_transform(&offset3, &offset3, &matrix); - - struct vec2 new_offset; - vec2_set(&new_offset, offset3.x, offset3.y); - obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); - } - - return true; - } - - obs_sceneitem_get_pos(item, &pos); - vec2_add(&pos, &pos, &offset); - obs_sceneitem_set_pos(item, &pos); - return true; -} - -void OBSBasic::Nudge(int dist, MoveDir dir) -{ - if (ui->preview->Locked()) - return; - - struct vec2 offset; - vec2_set(&offset, 0.0f, 0.0f); - - switch (dir) { - case MoveDir::Up: - offset.y = (float)-dist; - break; - case MoveDir::Down: - offset.y = (float)dist; - break; - case MoveDir::Left: - offset.x = (float)-dist; - break; - case MoveDir::Right: - offset.x = (float)dist; - break; - } - - if (!recent_nudge) { - recent_nudge = true; - OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string undo_data(obs_data_get_json(wrapper)); - - nudge_timer = new QTimer; - QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { - OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); - std::string redo_data(obs_data_get_json(rwrapper)); - - undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), - undo_redo, undo_redo, undo_data, redo_data); - - recent_nudge = false; - }); - connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); - nudge_timer->setSingleShot(true); - } - - if (nudge_timer) { - nudge_timer->stop(); - nudge_timer->start(1000); - } else { - blog(LOG_ERROR, "No nudge timer!"); - } - - obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); -} - -void OBSBasic::DeleteProjector(OBSProjector *projector) -{ - for (size_t i = 0; i < projectors.size(); i++) { - if (projectors[i] == projector) { - projectors[i]->deleteLater(); - projectors.erase(projectors.begin() + i); - break; - } - } -} - -OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) -{ - /* seriously? 10 monitors? */ - if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) - return nullptr; - - bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); - - if (closeProjectors && monitor > -1) { - for (size_t i = projectors.size(); i > 0; i--) { - size_t idx = i - 1; - if (projectors[idx]->GetMonitor() == monitor) - DeleteProjector(projectors[idx]); - } - } - - OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); - - projectors.emplace_back(projector); - - return projector; -} - -void OBSBasic::OpenStudioProgramProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); -} - -void OBSBasic::OpenMultiviewProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OpenProjector(nullptr, monitor, ProjectorType::Multiview); -} - -void OBSBasic::OpenSceneProjector() -{ - int monitor = sender()->property("monitor").toInt(); - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); -} - -void OBSBasic::OpenStudioProgramWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::StudioProgram); -} - -void OBSBasic::OpenPreviewWindow() -{ - OpenProjector(nullptr, -1, ProjectorType::Preview); -} - -void OBSBasic::OpenSourceWindow() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); -} - -void OBSBasic::OpenSceneWindow() -{ - OBSScene scene = GetCurrentScene(); - if (!scene) - return; - - OBSSource source = obs_scene_get_source(scene); - - OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); -} - -void OBSBasic::OpenSavedProjectors() -{ - for (SavedProjectorInfo *info : savedProjectorsArray) { - OpenSavedProjector(info); - } -} - -void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) -{ - if (info) { - OBSProjector *projector = nullptr; - switch (info->type) { - case ProjectorType::Source: - case ProjectorType::Scene: { - OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); - if (!source) - return; - - projector = OpenProjector(source, info->monitor, info->type); - break; - } - default: { - projector = OpenProjector(nullptr, info->monitor, info->type); - break; - } - } - - if (projector && !info->geometry.empty() && info->monitor < 0) { - QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); - projector->restoreGeometry(byteArray); - - if (!WindowPositionValid(projector->normalGeometry())) { - QRect rect = QGuiApplication::primaryScreen()->geometry(); - projector->setGeometry( - QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); - } - - if (info->alwaysOnTopOverridden) - projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); - } - } -} - -void OBSBasic::on_actionFullscreenInterface_triggered() -{ - if (!isFullScreen()) - showFullScreen(); - else - showNormal(); -} - -void OBSBasic::UpdateTitleBar() -{ - stringstream name; - - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); - const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); - - name << "OBS "; - if (previewProgramMode) - name << "Studio "; - - name << App()->GetVersionString(false); - if (safe_mode) - name << " (" << Str("TitleBar.SafeMode") << ")"; - if (App()->IsPortableMode()) - name << " - " << Str("TitleBar.PortableMode"); - - name << " - " << Str("TitleBar.Profile") << ": " << profile; - name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; - - setWindowTitle(QT_UTF8(name.str().c_str())); -} - -int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const -{ - char profiles_path[512]; - const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); - int ret; - - if (!profile) - return -1; - if (!path) - return -1; - if (!file) - file = ""; - - ret = GetAppConfigPath(profiles_path, 512, "obs-studio/basic/profiles"); - if (ret <= 0) - return ret; - - if (!*file) - return snprintf(path, size, "%s/%s", profiles_path, profile); - - return snprintf(path, size, "%s/%s/%s", profiles_path, profile, file); -} - -void OBSBasic::on_resetDocks_triggered(bool force) -{ - /* prune deleted extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - -#ifdef BROWSER_AVAILABLE - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && - !force) -#else - if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) -#endif - { - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); - - if (button == QMessageBox::No) - return; - } - - /* undock/hide/center extra docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (oldExtraDocks[i]) { - oldExtraDocks[i]->setVisible(true); - oldExtraDocks[i]->setFloating(true); - oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - - oldExtraDocks[i]->rect().center()); - oldExtraDocks[i]->setVisible(false); - } - } - -#define RESET_DOCKLIST(dockList) \ - for (int i = dockList.size() - 1; i >= 0; i--) { \ - dockList[i]->setVisible(true); \ - dockList[i]->setFloating(true); \ - dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ - dockList[i]->setVisible(false); \ - } - - RESET_DOCKLIST(extraDocks) - RESET_DOCKLIST(extraCustomDocks) -#ifdef BROWSER_AVAILABLE - RESET_DOCKLIST(extraBrowserDocks) -#endif -#undef RESET_DOCKLIST - - restoreState(startingDockLayout); - - int cx = width(); - int cy = height(); - - int cx22_5 = cx * 225 / 1000; - int cx5 = cx * 5 / 100; - int cx21 = cx * 21 / 100; - - cy = cy * 225 / 1000; - - int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); - - QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; - - QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; - - ui->scenesDock->setVisible(true); - ui->sourcesDock->setVisible(true); - ui->mixerDock->setVisible(true); - ui->transitionsDock->setVisible(true); - controlsDock->setVisible(true); - statsDock->setVisible(false); - statsDock->setFloating(true); - - resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); - resizeDocks(docks, sizes, Qt::Horizontal); - - activateWindow(); -} - -void OBSBasic::on_lockDocks_toggled(bool lock) -{ - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - QDockWidget::DockWidgetFeatures mainFeatures = features; - mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; - - ui->scenesDock->setFeatures(mainFeatures); - ui->sourcesDock->setFeatures(mainFeatures); - ui->mixerDock->setFeatures(mainFeatures); - ui->transitionsDock->setFeatures(mainFeatures); - controlsDock->setFeatures(mainFeatures); - statsDock->setFeatures(features); - - for (int i = extraDocks.size() - 1; i >= 0; i--) - extraDocks[i]->setFeatures(features); - - for (int i = extraCustomDocks.size() - 1; i >= 0; i--) - extraCustomDocks[i]->setFeatures(features); - -#ifdef BROWSER_AVAILABLE - for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) - extraBrowserDocks[i]->setFeatures(features); -#endif - - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } else { - oldExtraDocks[i]->setFeatures(features); - } - } -} - -void OBSBasic::on_sideDocks_toggled(bool side) -{ - if (side) { - setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); - } else { - setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); - setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); - setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); - setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - } -} - -void OBSBasic::on_resetUI_triggered() -{ - on_resetDocks_triggered(); - - ui->toggleListboxToolbars->setChecked(true); - ui->toggleContextBar->setChecked(true); - ui->toggleSourceIcons->setChecked(true); - ui->toggleStatusBar->setChecked(true); - ui->scenes->SetGridMode(false); - ui->actionSceneListMode->setChecked(true); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); -} - -void OBSBasic::on_multiviewProjectorWindowed_triggered() -{ - OpenProjector(nullptr, -1, ProjectorType::Multiview); -} - -void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) -{ - ui->sourcesToolbar->setVisible(visible); - ui->scenesToolbar->setVisible(visible); - ui->mixerToolbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); -} - -void OBSBasic::ShowContextBar() -{ - on_toggleContextBar_toggled(true); - ui->toggleContextBar->setChecked(true); -} - -void OBSBasic::HideContextBar() -{ - on_toggleContextBar_toggled(false); - ui->toggleContextBar->setChecked(false); -} - -void OBSBasic::on_toggleContextBar_toggled(bool visible) -{ - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); - this->ui->contextContainer->setVisible(visible); - UpdateContextBar(true); -} - -void OBSBasic::on_toggleStatusBar_toggled(bool visible) -{ - ui->statusbar->setVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); -} - -void OBSBasic::on_toggleSourceIcons_toggled(bool visible) -{ - ui->sources->SetIconsVisible(visible); - if (advAudioWindow != nullptr) - advAudioWindow->SetIconsVisible(visible); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); -} - -void OBSBasic::on_actionLockPreview_triggered() -{ - ui->preview->ToggleLocked(); - ui->actionLockPreview->setChecked(ui->preview->Locked()); -} - -void OBSBasic::on_scalingMenu_aboutToShow() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - QAction *action = ui->actionScaleCanvas; - QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); - text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); - action->setText(text); - - action = ui->actionScaleOutput; - text = QTStr("Basic.MainMenu.Edit.Scale.Output"); - text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); - action->setText(text); - action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); - - UpdatePreviewScalingMenu(); -} - -void OBSBasic::on_actionScaleWindow_triggered() -{ - ui->preview->SetFixedScaling(false); - ui->preview->ResetScrollingOffset(); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleCanvas_triggered() -{ - ui->preview->SetFixedScaling(true); - ui->preview->SetScalingLevel(0); - - emit ui->preview->DisplayResized(); -} - -void OBSBasic::on_actionScaleOutput_triggered() -{ - obs_video_info ovi; - obs_get_video_info(&ovi); - - ui->preview->SetFixedScaling(true); - float scalingAmount = float(ovi.output_width) / float(ovi.base_width); - // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) - int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); - ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); - emit ui->preview->DisplayResized(); -} - -void OBSBasic::SetShowing(bool showing) -{ - if (!showing && isVisible()) { - config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", - saveGeometry().toBase64().constData()); - - /* hide all visible child dialogs */ - visDlgPositions.clear(); - if (!visDialogs.isEmpty()) { - for (QDialog *dlg : visDialogs) { - visDlgPositions.append(dlg->pos()); - dlg->hide(); - } - } - - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Show")); - QTimer::singleShot(0, this, &OBSBasic::hide); - - if (previewEnabled) - EnablePreviewDisplay(false); - -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - - } else if (showing && !isVisible()) { - if (showHide) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - QTimer::singleShot(0, this, &OBSBasic::show); - - if (previewEnabled) - EnablePreviewDisplay(true); - -#ifdef __APPLE__ - EnableOSXDockIcon(true); -#endif - - /* raise and activate window to ensure it is on top */ - raise(); - activateWindow(); - - /* show all child dialogs that was visible earlier */ - if (!visDialogs.isEmpty()) { - for (int i = 0; i < visDialogs.size(); ++i) { - QDialog *dlg = visDialogs[i]; - dlg->move(visDlgPositions[i]); - dlg->show(); - } - } - - /* Unminimize window if it was hidden to tray instead of task - * bar. */ - if (sysTrayMinimizeToTray()) { - Qt::WindowStates state; - state = windowState() & ~Qt::WindowMinimized; - state |= Qt::WindowActive; - setWindowState(state); - } - } -} - -void OBSBasic::ToggleShowHide() -{ - bool showing = isVisible(); - if (showing) { - /* check for modal dialogs */ - EnumDialogs(); - if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) - return; - } - SetShowing(!showing); -} - -void OBSBasic::SystemTrayInit() -{ -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs.png"); -#endif - trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); - trayIcon->setToolTip("OBS Studio"); - - showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); - sysTrayStream = - new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), - trayIcon.data()); - sysTrayRecord = - new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), - trayIcon.data()); - sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") - : QTStr("Basic.Main.StartReplayBuffer"), - trayIcon.data()); - sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") - : QTStr("Basic.Main.StartVirtualCam"), - trayIcon.data()); - exit = new QAction(QTStr("Exit"), trayIcon.data()); - - trayMenu = new QMenu; - previewProjector = new QMenu(QTStr("PreviewProjector")); - studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - trayMenu->addAction(showHide); - trayMenu->addSeparator(); - trayMenu->addMenu(previewProjector); - trayMenu->addMenu(studioProgramProjector); - trayMenu->addSeparator(); - trayMenu->addAction(sysTrayStream); - trayMenu->addAction(sysTrayRecord); - trayMenu->addAction(sysTrayReplayBuffer); - trayMenu->addAction(sysTrayVirtualCam); - trayMenu->addSeparator(); - trayMenu->addAction(exit); - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - if (outputHandler && !outputHandler->replayBuffer) - sysTrayReplayBuffer->setEnabled(false); - - sysTrayVirtualCam->setEnabled(vcamEnabled); - - if (Active()) - OnActivate(true); - - connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); - connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); - connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); - connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); - connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); - connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); - connect(exit, &QAction::triggered, this, &OBSBasic::close); -} - -void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) -{ - // Refresh projector list - previewProjector->clear(); - studioProgramProjector->clear(); - AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); - AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); - -#ifdef __APPLE__ - UNUSED_PARAMETER(reason); -#else - if (reason == QSystemTrayIcon::Trigger) { - EnablePreviewDisplay(previewEnabled && !isVisible()); - ToggleShowHide(); - } -#endif -} - -void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) -{ - if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { - QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); - trayIcon->showMessage("OBS Studio", text, icon, 10000); - } -} - -void OBSBasic::SystemTray(bool firstStarted) -{ - if (!QSystemTrayIcon::isSystemTrayAvailable()) - return; - if (!trayIcon && !firstStarted) - return; - - bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); - bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); - - if (firstStarted) - SystemTrayInit(); - - if (!sysTrayEnabled) { - trayIcon->hide(); - } else { - trayIcon->show(); - if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { - EnablePreviewDisplay(false); -#ifdef __APPLE__ - EnableOSXDockIcon(false); -#endif - opt_minimize_tray = false; - } - } - - if (isVisible()) - showHide->setText(QTStr("Basic.SystemTray.Hide")); - else - showHide->setText(QTStr("Basic.SystemTray.Show")); -} - -bool OBSBasic::sysTrayMinimizeToTray() -{ - return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); -} - -void OBSBasic::on_actionMainUndo_triggered() -{ - undo_s.undo(); -} - -void OBSBasic::on_actionMainRedo_triggered() -{ - undo_s.redo(); -} - -void OBSBasic::on_actionCopySource_triggered() -{ - clipboard.clear(); - - for (auto &selectedSource : GetAllSelectedSourceItems()) { - OBSSceneItem item = ui->sources->Get(selectedSource.row()); - if (!item) - continue; - - OBSSource source = obs_sceneitem_get_source(item); - - SourceCopyInfo copyInfo; - copyInfo.weak_source = OBSGetWeakRef(source); - obs_sceneitem_get_info2(item, ©Info.transform); - obs_sceneitem_get_crop(item, ©Info.crop); - copyInfo.blend_method = obs_sceneitem_get_blending_method(item); - copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); - copyInfo.visible = obs_sceneitem_visible(item); - - clipboard.push_back(copyInfo); - } - - UpdateEditMenu(); -} - -void OBSBasic::on_actionPasteRef_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - OBSScene scene = GetCurrentScene(); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - - OBSSource source = OBSGetStrongRef(copyInfo.weak_source); - if (!source) - continue; - - const char *name = obs_source_get_name(source); - - /* do not allow duplicate refs of the same group in the same - * scene */ - if (!!obs_scene_get_group(scene, name)) { - continue; - } - - OBSBasicSourceSelect::SourcePaste(copyInfo, false); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSourceRef"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::on_actionPasteDup_triggered() -{ - OBSSource scene_source = GetCurrentSceneSource(); - OBSData undo_data = BackupScene(scene_source); - - undo_s.push_disabled(); - - for (size_t i = clipboard.size(); i > 0; i--) { - SourceCopyInfo ©Info = clipboard[i - 1]; - OBSBasicSourceSelect::SourcePaste(copyInfo, true); - } - - undo_s.pop_disabled(); - - QString action_name = QTStr("Undo.PasteSource"); - const char *scene_name = obs_source_get_name(scene_source); - - OBSData redo_data = BackupScene(scene_source); - CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); -} - -void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) -{ - if (source == dstSource) - return; - - OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); - obs_source_copy_filters(dstSource, source); - OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); - - const char *srcName = obs_source_get_name(source); - const char *dstName = obs_source_get_name(dstSource); - QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); - - CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); -} - -void OBSBasic::AudioMixerCopyFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *source = vol->GetSource(); - - copyFiltersSource = obs_source_get_weak_source(source); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::AudioMixerPasteFilters() -{ - QAction *action = reinterpret_cast(sender()); - VolControl *vol = action->property("volControl").value(); - obs_source_t *dstSource = vol->GetSource(); - - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::SceneCopyFilters() -{ - copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::ScenePasteFilters() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSource dstSource = GetCurrentSceneSource(); - - SourcePasteFilters(source.Get(), dstSource); -} - -void OBSBasic::on_actionCopyFilters_triggered() -{ - OBSSceneItem item = GetCurrentSceneItem(); - - if (!item) - return; - - OBSSource source = obs_sceneitem_get_source(item); - - copyFiltersSource = obs_source_get_weak_source(source); - - ui->actionPasteFilters->setEnabled(true); -} - -void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array) -{ - auto undo_redo = [this](const std::string &json) { - OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); - OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); - OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); - - obs_source_restore_filters(source, array); - - if (filters) - filters->UpdateSource(source); - }; - - const char *uuid = obs_source_get_uuid(source); - - OBSDataAutoRelease undo_data = obs_data_create(); - OBSDataAutoRelease redo_data = obs_data_create(); - obs_data_set_array(undo_data, "array", undo_array); - obs_data_set_array(redo_data, "array", redo_array); - obs_data_set_string(undo_data, "uuid", uuid); - obs_data_set_string(redo_data, "uuid", uuid); - - undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); -} - -void OBSBasic::on_actionPasteFilters_triggered() -{ - OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); - - OBSSceneItem sceneItem = GetCurrentSceneItem(); - OBSSource dstSource = obs_sceneitem_get_source(sceneItem); - - SourcePasteFilters(source.Get(), dstSource); -} - -static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) -{ - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", 1); - obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); - } -} - -void OBSBasic::ColorChange() -{ - QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); - QAction *action = qobject_cast(sender()); - QPushButton *colorButton = qobject_cast(sender()); - - if (selectedItems.count() == 0) - return; - - if (colorButton) { - int preset = colorButton->property("bgColor").value(); - - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet(""); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset + 1); - obs_data_set_string(privData, "color", ""); - } - - for (int i = 1; i < 9; i++) { - stringstream button; - button << "preset" << i; - QPushButton *cButton = - colorButton->parentWidget()->findChild(button.str().c_str()); - cButton->setStyleSheet("border: 1px solid black"); - } - - colorButton->setStyleSheet("border: 2px solid black"); - } else if (action) { - int preset = action->property("bgColor").value(); - - if (preset == 1) { - OBSSceneItem curSceneItem = GetCurrentSceneItem(); - SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); - OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); - - int oldPreset = obs_data_get_int(curPrivData, "color-preset"); - const QString oldSheet = curTreeItem->styleSheet(); - - auto liveChangeColor = [=](const QColor &color) { - if (color.isValid()) { - curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); - } - }; - - auto changedColor = [=](const QColor &color) { - if (color.isValid()) { - ConfirmColor(ui->sources, color, selectedItems); - } - }; - - auto rejected = [=]() { - if (oldPreset == 1) { - curTreeItem->setStyleSheet(oldSheet); - curTreeItem->setProperty("bgColor", 0); - } else if (oldPreset == 0) { - curTreeItem->setStyleSheet("background: none"); - curTreeItem->setProperty("bgColor", 0); - } else { - curTreeItem->setStyleSheet(""); - curTreeItem->setProperty("bgColor", oldPreset - 1); - } - - curTreeItem->style()->unpolish(curTreeItem); - curTreeItem->style()->polish(curTreeItem); - }; - - QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - - const char *oldColor = obs_data_get_string(curPrivData, "color"); - const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; -#ifdef __linux__ - // TODO: Revisit hang on Ubuntu with native dialog - options |= QColorDialog::DontUseNativeDialog; -#endif - - QColorDialog *colorDialog = new QColorDialog(this); - colorDialog->setOptions(options); - colorDialog->setCurrentColor(QColor(customColor)); - connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); - connect(colorDialog, &QColorDialog::colorSelected, changedColor); - connect(colorDialog, &QColorDialog::rejected, rejected); - colorDialog->open(); - } else { - for (int x = 0; x < selectedItems.count(); x++) { - SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); - treeItem->setStyleSheet("background: none"); - treeItem->setProperty("bgColor", preset); - treeItem->style()->unpolish(treeItem); - treeItem->style()->polish(treeItem); - - OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); - OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); - obs_data_set_int(privData, "color-preset", preset); - obs_data_set_string(privData, "color", ""); - } - } - } -} - -SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) -{ - int i = 0; - SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); - OBSSceneItem item = ui->sources->Get(i); - int64_t id = obs_sceneitem_get_id(sceneItem); - while (treeItem && obs_sceneitem_get_id(item) != id) { - i++; - treeItem = ui->sources->GetItemWidget(i); - item = ui->sources->Get(i); - } - if (treeItem) - return treeItem; - - return nullptr; -} - -void OBSBasic::on_autoConfigure_triggered() -{ - AutoConfig test(this); - test.setModal(true); - test.show(); - test.exec(); -} - -void OBSBasic::on_stats_triggered() -{ - if (!stats.isNull()) { - stats->show(); - stats->raise(); - return; - } - - OBSBasicStats *statsDlg; - statsDlg = new OBSBasicStats(nullptr); - statsDlg->show(); - stats = statsDlg; -} - -void OBSBasic::on_actionShowAbout_triggered() -{ - if (about) - about->close(); - - about = new OBSAbout(this); - about->show(); - - about->setAttribute(Qt::WA_DeleteOnClose, true); -} - -void OBSBasic::ResizeOutputSizeOfSource() -{ - if (obs_video_active()) - return; - - QMessageBox resize_output(this); - resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + - QTStr("ResizeOutputSizeOfSource.Continue")); - QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); - resize_output.addButton(QTStr("No"), QMessageBox::NoRole); - resize_output.setIcon(QMessageBox::Warning); - resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); - resize_output.exec(); - - if (resize_output.clickedButton() != Yes) - return; - - OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); - - int width = obs_source_get_width(source); - int height = obs_source_get_height(source); - - config_set_uint(activeConfiguration, "Video", "BaseCX", width); - config_set_uint(activeConfiguration, "Video", "BaseCY", height); - config_set_uint(activeConfiguration, "Video", "OutputCX", width); - config_set_uint(activeConfiguration, "Video", "OutputCY", height); - - ResetVideo(); - ResetOutputs(); - activeConfiguration.SaveSafe("tmp"); - on_actionFitToScreen_triggered(); -} - -QAction *OBSBasic::AddDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); - -#ifdef BROWSER_AVAILABLE - QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); - - if (!extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); - else - ui->menuDocks->addAction(action); -#else - QAction *action = ui->menuDocks->addAction(dock->windowTitle()); -#endif - action->setCheckable(true); - assignDockToggle(dock, action); - oldExtraDocks.push_back(dock); - oldExtraDockNames.push_back(dock->objectName()); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - - /* prune deleted docks */ - for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { - if (!oldExtraDocks[i]) { - oldExtraDocks.removeAt(i); - oldExtraDockNames.removeAt(i); - } - } - - return action; -} - -void OBSBasic::RepairOldExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = oldExtraDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); - - dock->setObjectName(oldExtraDockNames[idx]); -} - -void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) -{ - if (dock->objectName().isEmpty()) - return; - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - setupDockAction(dock); - dock->setFeatures(features); - addDockWidget(area, dock); - -#ifdef BROWSER_AVAILABLE - if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) - ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); - else - ui->menuDocks->addAction(dock->toggleViewAction()); - - if (extraBrowser) - return; -#else - UNUSED_PARAMETER(extraBrowser); - - ui->menuDocks->addAction(dock->toggleViewAction()); -#endif - - extraDockNames.push_back(dock->objectName()); - extraDocks.push_back(std::shared_ptr(dock)); -} - -void OBSBasic::RemoveDockWidget(const QString &name) -{ - if (extraDockNames.contains(name)) { - int idx = extraDockNames.indexOf(name); - extraDockNames.removeAt(idx); - extraDocks[idx].reset(); - extraDocks.removeAt(idx); - } else if (extraCustomDockNames.contains(name)) { - int idx = extraCustomDockNames.indexOf(name); - extraCustomDockNames.removeAt(idx); - removeDockWidget(extraCustomDocks[idx]); - extraCustomDocks.removeAt(idx); - } -} - -bool OBSBasic::IsDockObjectNameUsed(const QString &name) -{ - QStringList list; - list << "scenesDock" - << "sourcesDock" - << "mixerDock" - << "transitionsDock" - << "controlsDock" - << "statsDock"; - list << oldExtraDockNames; - list << extraDockNames; - list << extraCustomDockNames; - - return list.contains(name); -} - -void OBSBasic::AddCustomDockWidget(QDockWidget *dock) -{ - // Prevent the object name from being changed - connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); - - bool lock = ui->lockDocks->isChecked(); - QDockWidget::DockWidgetFeatures features = - lock ? QDockWidget::NoDockWidgetFeatures - : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | - QDockWidget::DockWidgetFloatable); - - dock->setFeatures(features); - addDockWidget(Qt::RightDockWidgetArea, dock); - - extraCustomDockNames.push_back(dock->objectName()); - extraCustomDocks.push_back(dock); -} - -void OBSBasic::RepairCustomExtraDockName() -{ - QDockWidget *dock = reinterpret_cast(sender()); - int idx = extraCustomDocks.indexOf(dock); - QSignalBlocker block(dock); - - if (idx == -1) { - blog(LOG_WARNING, "A custom dock got its object name changed"); - return; - } - - blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); - - dock->setObjectName(extraCustomDockNames[idx]); -} - -OBSBasic *OBSBasic::Get() -{ - return reinterpret_cast(App()->GetMainWindow()); -} - -bool OBSBasic::StreamingActive() -{ - if (!outputHandler) - return false; - return outputHandler->StreamingActive(); -} - -bool OBSBasic::RecordingActive() -{ - if (!outputHandler) - return false; - return outputHandler->RecordingActive(); -} - -bool OBSBasic::ReplayBufferActive() -{ - if (!outputHandler) - return false; - return outputHandler->ReplayBufferActive(); -} - -bool OBSBasic::VirtualCamActive() -{ - if (!outputHandler) - return false; - return outputHandler->VirtualCamActive(); -} - -SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QStyledItemDelegate::setEditorData(editor, index); - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->selectAll(); -} - -bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - switch (keyEvent->key()) { - case Qt::Key_Escape: { - QLineEdit *lineEdit = qobject_cast(editor); - if (lineEdit) - lineEdit->undo(); - break; - } - case Qt::Key_Tab: - case Qt::Key_Backtab: - return false; - } - } - - return QStyledItemDelegate::eventFilter(editor, event); -} - -void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) -{ - if (!error.isEmpty()) - return; - - patronJson = QT_TO_UTF8(text); -} - -void OBSBasic::PauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, true)) { - os_atomic_set_bool(&recording_paused, true); - - emit RecordingPaused(); - - ui->statusbar->RecordingPaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); - - if (os_atomic_load_bool(&replaybuf_active)) - ShowReplayBufferPauseWarning(); - } -} - -void OBSBasic::UnpauseRecording() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || - !os_atomic_load_bool(&recording_paused)) - return; - - obs_output_t *output = outputHandler->fileOutput; - - if (obs_output_pause(output, false)) { - os_atomic_set_bool(&recording_paused, false); - - emit RecordingUnpaused(); - - ui->statusbar->RecordingUnpaused(); - - TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); - if (trayIcon && trayIcon->isVisible()) { -#ifdef __APPLE__ - QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); - trayIconFile.setIsMask(true); -#else - QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); -#endif - trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); - } - - OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); - } -} - -void OBSBasic::RecordPauseToggled() -{ - if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) - return; - - obs_output_t *output = outputHandler->fileOutput; - bool enable = !obs_output_paused(output); - - if (enable) - PauseRecording(); - else - UnpauseRecording(); -} - -void OBSBasic::UpdateIsRecordingPausable() -{ - const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); - bool adv = astrcmpi(mode, "Advanced") == 0; - bool shared = true; - - if (adv) { - const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); - - if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); - } else { - const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); - shared = astrcmpi(recordEncoder, "none") == 0; - } - } else { - const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); - shared = strcmp(quality, "Stream") == 0; - } - - isRecordingPausable = !shared; -} - -#define MBYTE (1024ULL * 1024ULL) -#define MBYTES_LEFT_STOP_REC 50ULL -#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) - -const char *OBSBasic::GetCurrentOutputPath() -{ - const char *path = nullptr; - const char *mode = config_get_string(Config(), "Output", "Mode"); - - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - - if (strcmp(advanced_mode, "FFmpeg") == 0) { - path = config_get_string(Config(), "AdvOut", "FFFilePath"); - } else { - path = config_get_string(Config(), "AdvOut", "RecFilePath"); - } - } else { - path = config_get_string(Config(), "SimpleOutput", "FilePath"); - } - - return path; -} - -void OBSBasic::OutputPathInvalidMessage() -{ - blog(LOG_ERROR, "Recording stopped because of bad output path"); - - OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); -} - -bool OBSBasic::IsFFmpegOutputToURL() const -{ - const char *mode = config_get_string(Config(), "Output", "Mode"); - if (strcmp(mode, "Advanced") == 0) { - const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); - if (strcmp(advanced_mode, "FFmpeg") == 0) { - bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); - if (!is_local) - return true; - } - } - - return false; -} - -bool OBSBasic::OutputPathValid() -{ - if (IsFFmpegOutputToURL()) - return true; - - const char *path = GetCurrentOutputPath(); - return path && *path && QDir(path).exists(); -} - -void OBSBasic::DiskSpaceMessage() -{ - blog(LOG_ERROR, "Recording stopped because of low disk space"); - - OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); -} - -bool OBSBasic::LowDiskSpace() -{ - const char *path; - - path = GetCurrentOutputPath(); - if (!path) - return false; - - uint64_t num_bytes = os_get_free_disk_space(path); - - if (num_bytes < (MAX_BYTES_LEFT)) - return true; - else - return false; -} - -void OBSBasic::CheckDiskSpaceRemaining() -{ - if (LowDiskSpace()) { - StopRecording(); - StopReplayBuffer(); - - DiskSpaceMessage(); - } -} - -void OBSBasic::ResetStatsHotkey() -{ - const QList list = findChildren(); - - for (OBSBasicStats *s : list) { - s->Reset(); - } -} - -void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) -{ - QWidget *widget = childAt(pos); - const char *className = nullptr; - QString objName; - if (widget != nullptr) { - className = widget->metaObject()->className(); - objName = widget->objectName(); - } - - QPoint globalPos = mapToGlobal(pos); - if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { - if (objName.compare("scenesDock") == 0) { - ui->scenes->customContextMenuRequested(globalPos); - } else if (objName.compare("sourcesDock") == 0) { - ui->sources->customContextMenuRequested(globalPos); - } else if (objName.compare("mixerDock") == 0) { - StackedMixerAreaContextMenuRequested(); - } - } else if (!className) { - ui->menuDocks->exec(globalPos); - } -} - -void OBSBasic::UpdateProjectorHideCursor() -{ - for (size_t i = 0; i < projectors.size(); i++) - projectors[i]->SetHideCursor(); -} - -void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) -{ - for (size_t i = 0; i < projectors.size(); i++) - SetAlwaysOnTop(projectors[i], top); -} - -void OBSBasic::ResetProjectors() -{ - OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); - ClearProjectors(); - LoadSavedProjectors(savedProjectorList); - OpenSavedProjectors(); -} - -void OBSBasic::on_sourcePropertiesButton_clicked() -{ - on_actionSourceProperties_triggered(); -} - -void OBSBasic::on_sourceFiltersButton_clicked() -{ - OpenFilters(); -} - -void OBSBasic::on_actionSceneFilters_triggered() -{ - OBSSource sceneSource = GetCurrentSceneSource(); - - if (sceneSource) - OpenFilters(sceneSource); -} - -void OBSBasic::on_sourceInteractButton_clicked() -{ - on_actionInteract_triggered(); -} - -void OBSBasic::ShowStatusBarMessage(const QString &message) -{ - ui->statusbar->clearMessage(); - ui->statusbar->showMessage(message, 10000); -} - -void OBSBasic::UpdatePreviewSafeAreas() -{ - drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); -} - -void OBSBasic::UpdatePreviewOverflowSettings() -{ - bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); - bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); - bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); - - ui->preview->SetOverflowHidden(hidden); - ui->preview->SetOverflowSelectionHidden(select); - ui->preview->SetOverflowAlwaysVisible(always); -} - -void OBSBasic::SetDisplayAffinity(QWindow *window) -{ - if (!SetDisplayAffinitySupported()) - return; - - bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); - - // Don't hide projectors, those are designed to be visible / captured - if (window->property("isOBSProjectorWindow") == true) - return; - -#ifdef _WIN32 - HWND hwnd = (HWND)window->winId(); - - DWORD curAffinity; - if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { - if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) - SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); - else if (!hideFromCapture && curAffinity != WDA_NONE) - SetWindowDisplayAffinity(hwnd, WDA_NONE); - } - -#else - // TODO: Implement for other platforms if possible. Don't forget to - // implement SetDisplayAffinitySupported too! - UNUSED_PARAMETER(hideFromCapture); -#endif -} - -static inline QColor color_from_int(long long val) -{ - return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); -} - -QColor OBSBasic::GetSelectionColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); - } else { - return QColor::fromRgb(255, 0, 0); - } -} - -QColor OBSBasic::GetCropColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); - } else { - return QColor::fromRgb(0, 255, 0); - } -} - -QColor OBSBasic::GetHoverColor() const -{ - if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { - return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); - } else { - return QColor::fromRgb(0, 127, 255); - } -} - -void OBSBasic::UpdatePreviewSpacingHelpers() -{ - drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); -} - -float OBSBasic::GetDevicePixelRatio() -{ - return dpi; -} - -void OBSBasic::OnEvent(enum obs_frontend_event event) -{ - if (api) - api->on_event(event); -} - -void OBSBasic::UpdatePreviewScrollbars() -{ - if (!ui->preview->IsFixedScaling()) { - ui->previewXScrollBar->setRange(0, 0); - ui->previewYScrollBar->setRange(0, 0); - } -} - -void OBSBasic::on_previewXScrollBar_valueChanged(int value) -{ - emit PreviewXScrollBarMoved(value); -} - -void OBSBasic::on_previewYScrollBar_valueChanged(int value) -{ - emit PreviewYScrollBarMoved(value); -} - -void OBSBasic::PreviewScalingModeChanged(int value) -{ - switch (value) { - case 0: - on_actionScaleWindow_triggered(); - break; - case 1: - on_actionScaleCanvas_triggered(); - break; - case 2: - on_actionScaleOutput_triggered(); - break; - }; -} - -// MARK: - Generic UI Helper Functions - -OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) -{ - OBSPromptResult result; - - for (;;) { - result.success = false; - - if (request.withOption && !request.optionPrompt.empty()) { - result.optionValue = request.optionValue; - - result.success = NameDialog::AskForNameWithOption( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - request.optionPrompt.c_str(), result.optionValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - - } else { - result.success = NameDialog::AskForName( - this, request.title.c_str(), request.prompt.c_str(), result.promptValue, - (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); - } - - if (!result.success) { - break; - } - - if (result.promptValue.empty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - continue; - } - - if (!callback(result)) { - OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); - continue; - } - - break; - } - - return result; -} diff --git a/UI/window-extra-browsers.cpp b/UI/window-extra-browsers.cpp deleted file mode 100644 index 8f4232e1d..000000000 --- a/UI/window-extra-browsers.cpp +++ /dev/null @@ -1,562 +0,0 @@ -#include "moc_window-extra-browsers.cpp" -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" - -#include -#include -#include -#include - -#include - -#include "ui_OBSExtraBrowsers.h" - -using namespace json11; - -#define OBJ_NAME_SUFFIX "_extraBrowser" - -enum class Column : int { - Title, - Url, - Delete, - - Count, -}; - -/* ------------------------------------------------------------------------- */ - -void ExtraBrowsersModel::Reset() -{ - items.clear(); - - OBSBasic *main = OBSBasic::Get(); - - for (int i = 0; i < main->extraBrowserDocks.size(); i++) { - Item item; - item.prevIdx = i; - item.title = main->extraBrowserDockNames[i]; - item.url = main->extraBrowserDockTargets[i]; - items.push_back(item); - } -} - -int ExtraBrowsersModel::rowCount(const QModelIndex &) const -{ - int count = items.size() + 1; - return count; -} - -int ExtraBrowsersModel::columnCount(const QModelIndex &) const -{ - return (int)Column::Count; -} - -QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const -{ - int column = index.column(); - int idx = index.row(); - int count = items.size(); - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (!validRole) - return QVariant(); - - if (idx >= 0 && idx < count) { - switch (column) { - case (int)Column::Title: - return items[idx].title; - case (int)Column::Url: - return items[idx].url; - } - } else if (idx == count) { - switch (column) { - case (int)Column::Title: - return newTitle; - case (int)Column::Url: - return newURL; - } - } - - return QVariant(); -} - -QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; - - if (validRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case (int)Column::Title: - return QTStr("ExtraBrowsers.DockName"); - case (int)Column::Url: - return QStringLiteral("URL"); - } - } - - return QVariant(); -} - -Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() != (int)Column::Delete) - flags |= Qt::ItemIsEditable; - - return flags; -} - -class DelButton : public QPushButton { -public: - inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} - - QPersistentModelIndex index; -}; - -class EditWidget : public QLineEdit { -public: - inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} - - QPersistentModelIndex index; -}; - -void ExtraBrowsersModel::AddDeleteButton(int idx) -{ - QTableView *widget = reinterpret_cast(parent()); - - QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); - - QPushButton *del = new DelButton(index); - del->setProperty("class", "icon-trash"); - del->setObjectName("extraPanelDelete"); - del->setMinimumSize(QSize(20, 20)); - connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); - - widget->setIndexWidget(index, del); - widget->setRowHeight(idx, 20); - widget->setColumnWidth(idx, 20); -} - -void ExtraBrowsersModel::CheckToAdd() -{ - if (newTitle.isEmpty() || newURL.isEmpty()) - return; - - int idx = items.size() + 1; - beginInsertRows(QModelIndex(), idx, idx); - - Item item; - item.prevIdx = -1; - item.title = newTitle; - item.url = newURL; - items.push_back(item); - - newTitle = ""; - newURL = ""; - - endInsertRows(); - - AddDeleteButton(idx - 1); -} - -void ExtraBrowsersModel::UpdateItem(Item &item) -{ - int idx = item.prevIdx; - - OBSBasic *main = OBSBasic::Get(); - BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); - dock->setWindowTitle(item.title); - dock->setObjectName(item.title + OBJ_NAME_SUFFIX); - - if (main->extraBrowserDockNames[idx] != item.title) { - main->extraBrowserDockNames[idx] = item.title; - dock->toggleViewAction()->setText(item.title); - dock->setTitle(item.title); - } - - if (main->extraBrowserDockTargets[idx] != item.url) { - dock->cefWidget->setURL(QT_TO_UTF8(item.url)); - main->extraBrowserDockTargets[idx] = item.url; - } -} - -void ExtraBrowsersModel::DeleteItem() -{ - QTableView *widget = reinterpret_cast(parent()); - - DelButton *del = reinterpret_cast(sender()); - int row = del->index.row(); - - /* there's some sort of internal bug in Qt and deleting certain index - * widgets or "editors" that can cause a crash inside Qt if the widget - * is not manually removed, at least on 5.7 */ - widget->setIndexWidget(del->index, nullptr); - del->deleteLater(); - - /* --------- */ - - beginRemoveRows(QModelIndex(), row, row); - - int prevIdx = items[row].prevIdx; - items.removeAt(row); - - if (prevIdx != -1) { - int i = 0; - for (; i < deleted.size() && deleted[i] < prevIdx; i++) - ; - deleted.insert(i, prevIdx); - } - - endRemoveRows(); -} - -void ExtraBrowsersModel::Apply() -{ - OBSBasic *main = OBSBasic::Get(); - - for (Item &item : items) { - if (item.prevIdx != -1) { - UpdateItem(item); - } else { - QString uuid = QUuid::createUuid().toString(); - uuid.replace(QRegularExpression("[{}-]"), ""); - main->AddExtraBrowserDock(item.title, item.url, uuid, true); - } - } - - for (int i = deleted.size() - 1; i >= 0; i--) { - int idx = deleted[i]; - main->extraBrowserDockTargets.removeAt(idx); - main->extraBrowserDockNames.removeAt(idx); - main->extraBrowserDocks.removeAt(idx); - } - - if (main->extraBrowserDocks.empty()) - main->extraBrowserMenuDocksSeparator.clear(); - - deleted.clear(); - - Reset(); -} - -void ExtraBrowsersModel::TabSelection(bool forward) -{ - QListView *widget = reinterpret_cast(parent()); - QItemSelectionModel *selModel = widget->selectionModel(); - - QModelIndex sel = selModel->currentIndex(); - int row = sel.row(); - int col = sel.column(); - - switch (sel.column()) { - case (int)Column::Title: - if (!forward) { - if (row == 0) { - return; - } - - row -= 1; - } - - col += 1; - break; - - case (int)Column::Url: - if (forward) { - if (row == items.size()) { - return; - } - - row += 1; - } - - col -= 1; - } - - sel = createIndex(row, col, nullptr); - selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); -} - -void ExtraBrowsersModel::Init() -{ - for (int i = 0; i < items.count(); i++) - AddDeleteButton(i); -} - -/* ------------------------------------------------------------------------- */ - -QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, - const QModelIndex &index) const -{ - QLineEdit *text = new EditWidget(parent, index); - text->installEventFilter(const_cast(this)); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - return text; -} - -void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = reinterpret_cast(editor); - text->blockSignals(true); - text->setText(index.data().toString()); - text->blockSignals(false); -} - -bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) -{ - QLineEdit *edit = qobject_cast(object); - if (!edit) - return false; - - if (LineEditCanceled(event)) { - RevertText(edit); - } - if (LineEditChanged(event)) { - UpdateText(edit); - - if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Tab) { - model->TabSelection(true); - } else if (keyEvent->key() == Qt::Key_Backtab) { - model->TabSelection(false); - } - } - return true; - } - - return false; -} - -bool ExtraBrowsersDelegate::ValidName(const QString &name) const -{ - for (auto &item : model->items) { - if (name.compare(item.title, Qt::CaseInsensitive) == 0) { - return false; - } - } - return true; -} - -void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString oldText; - if (col == (int)Column::Title) { - oldText = newItem ? model->newTitle : model->items[row].title; - } else { - oldText = newItem ? model->newURL : model->items[row].url; - } - - edit->setText(oldText); -} - -bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) -{ - EditWidget *edit = reinterpret_cast(edit_); - int row = edit->index.row(); - int col = edit->index.column(); - bool newItem = (row == model->items.size()); - - QString text = edit->text().trimmed(); - - if (!newItem && text.isEmpty()) { - return false; - } - - if (col == (int)Column::Title) { - QString oldText = newItem ? model->newTitle : model->items[row].title; - bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; - - if (!same && !ValidName(text)) { - edit->setText(oldText); - return false; - } - } - - if (!newItem) { - /* if edited existing item, update it*/ - switch (col) { - case (int)Column::Title: - model->items[row].title = text; - break; - case (int)Column::Url: - model->items[row].url = text; - break; - } - } else { - /* if both new values filled out, create new one */ - switch (col) { - case (int)Column::Title: - model->newTitle = text; - break; - case (int)Column::Url: - model->newURL = text; - break; - } - - model->CheckToAdd(); - } - - emit commitData(edit); - return true; -} - -/* ------------------------------------------------------------------------- */ - -OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) -{ - ui->setupUi(this); - - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - model = new ExtraBrowsersModel(ui->table); - - ui->table->setModel(model); - ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); - ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); - ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); - ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); -} - -OBSExtraBrowsers::~OBSExtraBrowsers() {} - -void OBSExtraBrowsers::closeEvent(QCloseEvent *event) -{ - QDialog::closeEvent(event); - model->Apply(); -} - -void OBSExtraBrowsers::on_apply_clicked() -{ - model->Apply(); -} - -/* ------------------------------------------------------------------------- */ - -void OBSBasic::ClearExtraBrowserDocks() -{ - extraBrowserDockTargets.clear(); - extraBrowserDockNames.clear(); - extraBrowserDocks.clear(); -} - -void OBSBasic::LoadExtraBrowserDocks() -{ - const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); - - std::string err; - Json json = Json::parse(jsonStr, err); - if (!err.empty()) - return; - - Json::array array = json.array_items(); - if (!array.empty()) - extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); - - for (Json &item : array) { - std::string title = item["title"].string_value(); - std::string url = item["url"].string_value(); - std::string uuid = item["uuid"].string_value(); - - AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); - } -} - -void OBSBasic::SaveExtraBrowserDocks() -{ - Json::array array; - for (int i = 0; i < extraBrowserDocks.size(); i++) { - QDockWidget *dock = extraBrowserDocks[i].get(); - QString title = extraBrowserDockNames[i]; - QString url = extraBrowserDockTargets[i]; - QString uuid = dock->property("uuid").toString(); - Json::object obj{ - {"title", QT_TO_UTF8(title)}, - {"url", QT_TO_UTF8(url)}, - {"uuid", QT_TO_UTF8(uuid)}, - }; - array.push_back(obj); - } - - std::string output = Json(array).dump(); - config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); -} - -void OBSBasic::ManageExtraBrowserDocks() -{ - if (!extraBrowsers.isNull()) { - extraBrowsers->show(); - extraBrowsers->raise(); - return; - } - - extraBrowsers = new OBSExtraBrowsers(this); - extraBrowsers->show(); -} - -void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) -{ - static int panel_version = -1; - if (panel_version == -1) { - panel_version = obs_browser_qcef_version(); - } - - BrowserDock *dock = new BrowserDock(title); - QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); - bId.replace(QRegularExpression("[{}-]"), ""); - dock->setProperty("uuid", bId); - dock->setObjectName(title + OBJ_NAME_SUFFIX); - dock->resize(460, 600); - dock->setMinimumSize(80, 80); - dock->setWindowTitle(title); - dock->setAllowedAreas(Qt::AllDockWidgetAreas); - - QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); - if (browser && panel_version >= 1) - browser->allowAllPopups(true); - - dock->SetWidget(browser); - - /* Add support for Twitch Dashboard panels */ - if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { - QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); - QRegularExpressionMatch match = re.match(url); - QString username = match.captured(1); - if (username.length() > 0) { - std::string script; - script = "Object.defineProperty(document, 'referrer', { get: () => '"; - script += "https://twitch.tv/"; - script += QT_TO_UTF8(username); - script += "/dashboard/live"; - script += "'});"; - browser->setStartupScript(script); - } - } - - AddDockWidget(dock, Qt::RightDockWidgetArea, true); - extraBrowserDocks.push_back(std::shared_ptr(dock)); - extraBrowserDockNames.push_back(title); - extraBrowserDockTargets.push_back(url); - - if (firstCreate) { - dock->setFloating(true); - - QPoint curPos = pos(); - QSize wSizeD2 = size() / 2; - QSize dSizeD2 = dock->size() / 2; - - curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); - curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); - - dock->move(curPos); - dock->setVisible(true); - } -} diff --git a/UI/window-extra-browsers.hpp b/UI/window-extra-browsers.hpp deleted file mode 100644 index 690d784dd..000000000 --- a/UI/window-extra-browsers.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class Ui_OBSExtraBrowsers; -class ExtraBrowsersModel; - -class QCefWidget; - -class OBSExtraBrowsers : public QDialog { - Q_OBJECT - - std::unique_ptr ui; - ExtraBrowsersModel *model; - -public: - OBSExtraBrowsers(QWidget *parent); - ~OBSExtraBrowsers(); - - void closeEvent(QCloseEvent *event) override; - -public slots: - void on_apply_clicked(); -}; - -class ExtraBrowsersModel : public QAbstractTableModel { - Q_OBJECT - -public: - inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) - { - Reset(); - QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); - } - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - - struct Item { - int prevIdx; - QString title; - QString url; - }; - - void TabSelection(bool forward); - - void AddDeleteButton(int idx); - void Reset(); - void CheckToAdd(); - void UpdateItem(Item &item); - void DeleteItem(); - void Apply(); - - QVector items; - QVector deleted; - - QString newTitle; - QString newURL; - -public slots: - void Init(); -}; - -class ExtraBrowsersDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} - - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - - void setEditorData(QWidget *editor, const QModelIndex &index) const override; - - bool eventFilter(QObject *object, QEvent *event) override; - void RevertText(QLineEdit *edit); - bool UpdateText(QLineEdit *edit); - bool ValidName(const QString &text) const; - - ExtraBrowsersModel *model; -}; diff --git a/UI/window-importer.cpp b/UI/window-importer.cpp deleted file mode 100644 index 88d4ad2d9..000000000 --- a/UI/window-importer.cpp +++ /dev/null @@ -1,570 +0,0 @@ -/****************************************************************************** - Copyright (C) 2019-2020 by Dillon Pentz - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include "moc_window-importer.cpp" - -#include "obs-app.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "importers/importers.hpp" - -extern bool SceneCollectionExists(const char *findName); - -enum ImporterColumn { - Selected, - Name, - Path, - Program, - - Count -}; - -enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} - -QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - bool empty = index.model() - ->index(index.row(), ImporterColumn::Path) - .data(ImporterEntryRole::CheckEmpty) - .value(); - - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; -} - -void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - model->setData(index, list, ImporterEntryRole::NewPath); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - - bool isSet = false; - QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } - - if (isSet) - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void ImporterEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/** - Model -**/ - -int ImporterModel::rowCount(const QModelIndex &) const -{ - return options.length() + 1; -} - -int ImporterModel::columnCount(const QModelIndex &) const -{ - return ImporterColumn::Count; -} - -QVariant ImporterModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= options.length()) { - if (role == ImporterEntryRole::CheckEmpty) - result = true; - else - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case ImporterColumn::Path: - result = options[index.row()].path; - break; - case ImporterColumn::Program: - result = options[index.row()].program; - break; - case ImporterColumn::Name: - result = options[index.row()].name; - } - } else if (role == Qt::EditRole) { - if (index.column() == ImporterColumn::Name) { - result = options[index.row()].name; - } - } else if (role == Qt::CheckStateRole) { - switch (index.column()) { - case ImporterColumn::Selected: - if (options[index.row()].program != "") - result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; - else - result = Qt::Unchecked; - } - } else if (role == ImporterEntryRole::CheckEmpty) { - result = options[index.row()].empty; - } - - return result; -} - -Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { - flags |= Qt::ItemIsUserCheckable; - } else if (index.column() == ImporterColumn::Path || - (index.column() == ImporterColumn::Name && index.row() != options.length())) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void ImporterModel::checkInputPath(int row) -{ - ImporterEntry &entry = options[row]; - - if (entry.path.isEmpty()) { - entry.program = ""; - entry.empty = true; - entry.selected = false; - entry.name = ""; - } else { - entry.empty = false; - - std::string program = DetectProgram(entry.path.toStdString()); - entry.program = QTStr(program.c_str()); - - if (program.empty()) { - entry.selected = false; - } else { - std::string name = GetSCName(entry.path.toStdString(), program); - entry.name = name.c_str(); - } - } - - emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); -} - -bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (role == ImporterEntryRole::NewPath) { - QStringList list = value.toStringList(); - - if (list.size() == 0) { - if (index.row() < options.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - options.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (list.size() > 0 && index.row() < options.length()) { - options[index.row()].path = list[0]; - checkInputPath(index.row()); - - list.removeAt(0); - } - - if (list.size() > 0) { - int row = index.row(); - int lastRow = row + list.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : list) { - ImporterEntry entry; - entry.path = path; - - options.insert(row, entry); - - row++; - } - - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - } - } - } else if (index.row() == options.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - ImporterEntry entry; - entry.path = path; - entry.selected = role != ImporterEntryRole::AutoPath; - entry.empty = false; - - beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); - options.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - } - } else if (index.column() == ImporterColumn::Selected) { - bool select = value.toBool(); - - options[index.row()].selected = select; - } else if (index.column() == ImporterColumn::Path) { - QString path = value.toString(); - options[index.row()].path = path; - - checkInputPath(index.row()); - } else if (index.column() == ImporterColumn::Name) { - QString name = value.toString(); - options[index.row()].name = name; - } - - emit dataChanged(index, index); - - return true; -} - -QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case ImporterColumn::Path: - result = QTStr("Importer.Path"); - break; - case ImporterColumn::Program: - result = QTStr("Importer.Program"); - break; - case ImporterColumn::Name: - result = QTStr("Name"); - } - } - - return result; -} - -/** - Window -**/ - -OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(optionsModel); - ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); - - connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); - - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); - ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, - &OBSImporter::importCollections); - connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); - - ImportersInit(); - - bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); - - if (!autoSearchPrompt) { - QMessageBox::StandardButton button = OBSMessageBox::question( - parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); - - if (button == QMessageBox::Yes) { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); - } else { - config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); - } - - config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); - } - - bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); - - OBSImporterFiles f; - if (autoSearch) - f = ImportersFindFiles(); - - for (size_t i = 0; i < f.size(); i++) { - QString path = f[i].c_str(); - path.replace("\\", "/"); - addImportOption(path, true); - } - - f.clear(); - - ui->tableView->resizeColumnsToContents(); - - QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -void OBSImporter::addImportOption(QString path, bool automatic) -{ - QStringList list; - - list.append(path); - - QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); - - optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); -} - -void OBSImporter::dropEvent(QDropEvent *ev) -{ - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - if (fileInfo.isDir()) { - - QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); - - while (dirIter.hasNext()) { - addImportOption(dirIter.next(), false); - } - } else { - addImportOption(fileInfo.canonicalFilePath(), false); - } - } -} - -void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls()) - ev->accept(); -} - -void OBSImporter::browseImport() -{ - QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; - - QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", - QTStr("Importer.Collection") + QString(" ") + Pattern); - - if (!paths.empty()) { - for (int i = 0; i < paths.count(); i++) { - addImportOption(paths[i], false); - } - } -} - -bool GetUnusedName(std::string &name) -{ - OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - - if (!basic->GetSceneCollectionByName(name)) { - return false; - } - - std::string newName; - int inc = 2; - do { - newName = name; - newName += " "; - newName += std::to_string(inc++); - } while (basic->GetSceneCollectionByName(newName)); - - name = newName; - return true; -} - -constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; - -void OBSImporter::importCollections() -{ - setEnabled(false); - - const std::filesystem::path sceneCollectionLocation = - App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); - - for (int i = 0; i < optionsModel->rowCount() - 1; i++) { - int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); - - if (selected == Qt::Unchecked) - continue; - - std::string pathStr = optionsModel->index(i, ImporterColumn::Path) - .data(Qt::DisplayRole) - .value() - .toStdString(); - std::string nameStr = optionsModel->index(i, ImporterColumn::Name) - .data(Qt::DisplayRole) - .value() - .toStdString(); - - json11::Json res; - ImportSC(pathStr, nameStr, res); - - if (res != json11::Json()) { - json11::Json::object out = res.object_items(); - std::string name = res["name"].string_value(); - std::string file; - - if (GetUnusedName(name)) { - json11::Json::object newOut = out; - newOut["name"] = name; - out = newOut; - } - - std::string fileName; - if (!GetFileSafeName(name.c_str(), fileName)) { - blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); - } - - std::string collectionFile; - collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); - collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); - - if (!GetClosestUnusedFileName(collectionFile, "json")) { - blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); - } - - std::string out_str = json11::Json(out).dump(); - - bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), - false); - - blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), - success ? "SUCCESS" : "FAILURE"); - } - } - - close(); -} - -void OBSImporter::dataChanged() -{ - ui->tableView->resizeColumnToContents(ImporterColumn::Name); -} diff --git a/UI/window-missing-files.cpp b/UI/window-missing-files.cpp deleted file mode 100644 index 57e641a74..000000000 --- a/UI/window-missing-files.cpp +++ /dev/null @@ -1,542 +0,0 @@ -/****************************************************************************** - Copyright (C) 2019 by Dillon Pentz - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include "moc_window-missing-files.cpp" -#include "window-basic-main.hpp" - -#include "obs-app.hpp" - -#include -#include -#include - -#include - -enum MissingFilesColumn { - Source, - OriginalPath, - NewPath, - State, - - Count -}; - -enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &) const -{ - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in input cells - if (isOutput) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - - return container; -} - -void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - model->setData(index, list); - } else - model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text(), 0); - } -} - -void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) -{ - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = - QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); - -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } - - if (isSet) - emit commitData(container); -} - -void MissingFilesPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); - container->findChild()->clearFocus(); - ((QWidget *)container->parent())->setFocus(); - emit commitData(container); -} - -/** - Model -**/ - -MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) -{ - QStyle *style = QApplication::style(); - - warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); -} - -int MissingFilesModel::rowCount(const QModelIndex &) const -{ - return files.length(); -} - -int MissingFilesModel::columnCount(const QModelIndex &) const -{ - return MissingFilesColumn::Count; -} - -int MissingFilesModel::found() const -{ - int res = 0; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != Missing && files[i].state != Cleared) - res++; - } - - return res; -} - -QVariant MissingFilesModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= files.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - QFileInfo fi(files[index.row()].originalPath); - - switch (index.column()) { - case MissingFilesColumn::Source: - result = files[index.row()].source; - break; - case MissingFilesColumn::OriginalPath: - result = fi.fileName(); - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - case MissingFilesColumn::State: - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - } - break; - } - } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); - - if (source) { - result = main->GetSourceIcon(obs_source_get_id(source)); - } - } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { - QFont font = QFont(); - font.setBold(true); - - result = font; - } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { - switch (files[index.row()].state) { - case MissingFilesState::Missing: - result = QTStr("MissingFiles.Missing"); - break; - - case MissingFilesState::Replaced: - result = QTStr("MissingFiles.Replaced"); - break; - - case MissingFilesState::Found: - result = QTStr("MissingFiles.Found"); - break; - - case MissingFilesState::Cleared: - result = QTStr("MissingFiles.Cleared"); - break; - - default: - break; - } - } else if (role == Qt::ToolTipRole) { - switch (index.column()) { - case MissingFilesColumn::OriginalPath: - result = files[index.row()].originalPath; - break; - case MissingFilesColumn::NewPath: - result = files[index.row()].newPath; - break; - default: - break; - } - } - - return result; -} - -Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == MissingFilesColumn::OriginalPath) { - flags &= ~Qt::ItemIsEditable; - } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) -{ - loop = false; - QUrl url = QUrl().fromLocalFile(path); - QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); - - bool prompted = skipPrompt; - - for (int i = 0; i < files.length(); i++) { - if (files[i].state != MissingFilesState::Missing) - continue; - - QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); - QString filename = origFile.fileName(); - QString testFile = dir + filename; - - if (os_file_exists(testFile.toStdString().c_str())) { - if (!prompted) { - QMessageBox::StandardButton button = - QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), - QTStr("MissingFiles.AutoSearchText")); - - if (button == QMessageBox::No) - break; - - prompted = true; - } - QModelIndex in = index(i, MissingFilesColumn::NewPath); - setData(in, testFile, 0); - } - } - loop = true; -} - -bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == MissingFilesRole::NewPathsToProcessRole) { - QStringList list = value.toStringList(); - - int row = index.row() + 1; - beginInsertRows(QModelIndex(), row, row); - - MissingFileEntry entry; - entry.originalPath = list[0].replace("\\", "/"); - entry.source = list[1]; - - files.insert(row, entry); - row++; - - endInsertRows(); - - success = true; - } else { - QString path = value.toString(); - if (index.column() == MissingFilesColumn::NewPath) { - files[index.row()].newPath = value.toString(); - QString fileName = QUrl(path).fileName(); - QString origFileName = QUrl(files[index.row()].originalPath).fileName(); - - if (path.isEmpty()) { - files[index.row()].state = MissingFilesState::Missing; - } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { - files[index.row()].state = MissingFilesState::Cleared; - } else if (fileName.compare(origFileName) == 0) { - files[index.row()].state = MissingFilesState::Found; - - if (loop) - fileCheckLoop(files, path, false); - } else { - files[index.row()].state = MissingFilesState::Replaced; - - if (loop) - fileCheckLoop(files, path, false); - } - - emit dataChanged(index, index); - success = true; - } - } - - return success; -} - -QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case MissingFilesColumn::State: - result = QTStr("MissingFiles.State"); - break; - case MissingFilesColumn::Source: - result = QTStr("Basic.Main.Source"); - break; - case MissingFilesColumn::OriginalPath: - result = QTStr("MissingFiles.MissingFile"); - break; - case MissingFilesColumn::NewPath: - result = QTStr("MissingFiles.NewFile"); - break; - } - } - - return result; -} - -OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) - : QDialog(parent), - filesModel(new MissingFilesModel), - ui(new Ui::OBSMissingFiles) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->tableView->setModel(filesModel); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, - new MissingFilesPathItemDelegate(false, "")); - ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, - new MissingFilesPathItemDelegate(true, "")); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); - ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, - QHeaderView::ResizeMode::ResizeToContents); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - - ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); - - for (size_t i = 0; i < obs_missing_files_count(files); i++) { - obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); - - const char *oldPath = obs_missing_file_get_path(f); - const char *name = obs_missing_file_get_source_name(f); - - addMissingFile(oldPath, name); - } - - QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); - - ui->found->setText(found); - - fileStore = files; - - connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); - connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); - connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); - connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); - - QModelIndex index = filesModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -OBSMissingFiles::~OBSMissingFiles() -{ - obs_missing_files_destroy(fileStore); -} - -void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) -{ - QStringList list; - - list.append(originalPath); - list.append(sourceName); - - QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); - - filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); -} - -void OBSMissingFiles::saveFiles() -{ - for (int i = 0; i < filesModel->files.length(); i++) { - MissingFilesState state = filesModel->files[i].state; - if (state != MissingFilesState::Missing) { - obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); - - QString path = filesModel->files[i].newPath; - - if (state == MissingFilesState::Cleared) { - obs_missing_file_issue_callback(f, ""); - } else { - char *p = bstrdup(path.toStdString().c_str()); - obs_missing_file_issue_callback(f, p); - bfree(p); - } - } - } - - QDialog::accept(); -} - -void OBSMissingFiles::browseFolders() -{ - QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if (dir != "") { - dir += "/"; - filesModel->fileCheckLoop(filesModel->files, dir, true); - } -} - -void OBSMissingFiles::dataChanged() -{ - QString found = - QTStr("MissingFiles.NumFound") - .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); - - ui->found->setText(found); - - ui->tableView->resizeColumnToContents(MissingFilesColumn::State); - ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); -} - -QIcon OBSMissingFiles::GetWarningIcon() -{ - return filesModel->warningIcon; -} - -void OBSMissingFiles::SetWarningIcon(const QIcon &icon) -{ - ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); - filesModel->warningIcon = icon; -} diff --git a/UI/window-remux.cpp b/UI/window-remux.cpp deleted file mode 100644 index d72ebbd9c..000000000 --- a/UI/window-remux.cpp +++ /dev/null @@ -1,924 +0,0 @@ -/****************************************************************************** - Copyright (C) 2014 by Ruwen Hahn - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include "moc_window-remux.cpp" - -#include "obs-app.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "window-basic-main.hpp" - -#include -#include - -using namespace std; - -enum RemuxEntryColumn { - State, - InputPath, - OutputPath, - - Count -}; - -enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; - -/********************************************************** - Delegate - Presents cells in the grid. -**********************************************************/ - -RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) - : QStyledItemDelegate(), - isOutput(isOutput), - defaultPath(defaultPath) -{ -} - -QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { - // Never allow modification of rows that are - // in progress. - return Q_NULLPTR; - } else if (isOutput && state != RemuxEntryState::Ready) { - // Do not allow modification of output rows - // that aren't associated with a valid input. - return Q_NULLPTR; - } else if (!isOutput && state == RemuxEntryState::Complete) { - // Don't allow modification of rows that are - // already complete. - return Q_NULLPTR; - } else { - QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::PushButton); - - QWidget *container = new QWidget(parent); - - auto browseCallback = [this, container]() { - const_cast(this)->handleBrowse(container); - }; - - auto clearCallback = [this, container]() { - const_cast(this)->handleClear(container); - }; - - QHBoxLayout *layout = new QHBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - QLineEdit *text = new QLineEdit(); - text->setObjectName(QStringLiteral("text")); - text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, - QSizePolicy::ControlType::LineEdit)); - layout->addWidget(text); - - QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); - - QToolButton *browseButton = new QToolButton(); - browseButton->setText("..."); - browseButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(browseButton); - - container->connect(browseButton, &QToolButton::clicked, browseCallback); - - // The "clear" button is not shown in output cells - // or the insertion point's input cell. - if (!isOutput && state != RemuxEntryState::Empty) { - QToolButton *clearButton = new QToolButton(); - clearButton->setText("X"); - clearButton->setSizePolicy(buttonSizePolicy); - layout->addWidget(clearButton); - - container->connect(clearButton, &QToolButton::clicked, clearCallback); - } - - container->setLayout(layout); - container->setFocusProxy(text); - return container; - } -} - -void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QLineEdit *text = editor->findChild(); - text->setText(index.data().toString()); - editor->setProperty(PATH_LIST_PROP, QVariant()); -} - -void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, - const QModelIndex &index) const -{ - // We use the PATH_LIST_PROP property to pass a list of - // path strings from the editor widget into the model's - // NewPathsToProcessRole. This is only used when paths - // are selected through the "browse" or "delete" buttons - // in the editor. If the user enters new text in the - // text box, we simply pass that text on to the model - // as normal text data in the default role. - QVariant pathListProp = editor->property(PATH_LIST_PROP); - if (pathListProp.isValid()) { - QStringList list = editor->property(PATH_LIST_PROP).toStringList(); - if (isOutput) { - if (list.size() > 0) - model->setData(index, list); - } else - model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); - } else { - QLineEdit *lineEdit = editor->findChild(); - model->setData(index, lineEdit->text()); - } -} - -void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - RemuxEntryState state = index.model() - ->index(index.row(), RemuxEntryColumn::State) - .data(RemuxEntryRole::EntryStateRole) - .value(); - - QStyleOptionViewItem localOption = option; - initStyleOption(&localOption, index); - - if (isOutput) { - if (state != Ready) { - QColor background = - localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); - - localOption.backgroundBrush = QBrush(background); - } - } - - QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); -} - -void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) -{ - QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; - - QLineEdit *text = container->findChild(); - - QString currentPath = text->text(); - if (currentPath.isEmpty()) - currentPath = defaultPath; - - bool isSet = false; - if (isOutput) { - QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); - - if (!newPath.isEmpty()) { - container->setProperty(PATH_LIST_PROP, QStringList() << newPath); - isSet = true; - } - } else { - QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, - QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); - - if (!paths.empty()) { - container->setProperty(PATH_LIST_PROP, paths); - isSet = true; - } -#ifdef __APPLE__ - // TODO: Revisit when QTBUG-42661 is fixed - container->window()->raise(); -#endif - } - - if (isSet) - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::handleClear(QWidget *container) -{ - // An empty string list will indicate that the entry is being - // blanked and should be deleted. - container->setProperty(PATH_LIST_PROP, QStringList()); - - emit commitData(container); -} - -void RemuxEntryPathItemDelegate::updateText() -{ - QLineEdit *lineEdit = dynamic_cast(sender()); - QWidget *editor = lineEdit->parentWidget(); - emit commitData(editor); -} - -/********************************************************** - Model - Manages the queue's data -**********************************************************/ - -int RemuxQueueModel::rowCount(const QModelIndex &) const -{ - return queue.length() + (isProcessing ? 0 : 1); -} - -int RemuxQueueModel::columnCount(const QModelIndex &) const -{ - return RemuxEntryColumn::Count; -} - -QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const -{ - QVariant result = QVariant(); - - if (index.row() >= queue.length()) { - return QVariant(); - } else if (role == Qt::DisplayRole) { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - result = queue[index.row()].sourcePath; - break; - case RemuxEntryColumn::OutputPath: - result = queue[index.row()].targetPath; - break; - } - } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { - result = getIcon(queue[index.row()].state); - } else if (role == RemuxEntryRole::EntryStateRole) { - result = queue[index.row()].state; - } - - return result; -} - -QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant result = QVariant(); - - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { - switch (section) { - case RemuxEntryColumn::State: - result = QString(); - break; - case RemuxEntryColumn::InputPath: - result = QTStr("Remux.SourceFile"); - break; - case RemuxEntryColumn::OutputPath: - result = QTStr("Remux.TargetFile"); - break; - } - } - - return result; -} - -Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags flags = QAbstractTableModel::flags(index); - - if (index.column() == RemuxEntryColumn::InputPath) { - flags |= Qt::ItemIsEditable; - } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { - flags |= Qt::ItemIsEditable; - } - - return flags; -} - -bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - bool success = false; - - if (role == RemuxEntryRole::NewPathsToProcessRole) { - QStringList pathList = value.toStringList(); - - if (pathList.size() == 0) { - if (index.row() < queue.size()) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - if (pathList.size() >= 1 && index.row() < queue.length()) { - queue[index.row()].sourcePath = pathList[0]; - checkInputPath(index.row()); - - pathList.removeAt(0); - - success = true; - } - - if (pathList.size() > 0) { - int row = index.row(); - int lastRow = row + pathList.size() - 1; - beginInsertRows(QModelIndex(), row, lastRow); - - for (QString path : pathList) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - queue.insert(row, entry); - row++; - } - endInsertRows(); - - for (row = index.row(); row <= lastRow; row++) { - checkInputPath(row); - } - - success = true; - } - } - } else if (index.row() == queue.length()) { - QString path = value.toString(); - - if (!path.isEmpty()) { - RemuxQueueEntry entry; - entry.sourcePath = path; - entry.state = RemuxEntryState::Empty; - - beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); - queue.append(entry); - endInsertRows(); - - checkInputPath(index.row()); - success = true; - } - } else { - QString path = value.toString(); - - if (path.isEmpty()) { - if (index.column() == RemuxEntryColumn::InputPath) { - beginRemoveRows(QModelIndex(), index.row(), index.row()); - queue.removeAt(index.row()); - endRemoveRows(); - } - } else { - switch (index.column()) { - case RemuxEntryColumn::InputPath: - queue[index.row()].sourcePath = value.toString(); - checkInputPath(index.row()); - success = true; - break; - case RemuxEntryColumn::OutputPath: - queue[index.row()].targetPath = value.toString(); - emit dataChanged(index, index); - success = true; - break; - } - } - } - - return success; -} - -QVariant RemuxQueueModel::getIcon(RemuxEntryState state) -{ - QVariant icon; - QStyle *style = QApplication::style(); - - switch (state) { - case RemuxEntryState::Complete: - icon = style->standardIcon(QStyle::SP_DialogApplyButton); - break; - - case RemuxEntryState::InProgress: - icon = style->standardIcon(QStyle::SP_ArrowRight); - break; - - case RemuxEntryState::Error: - icon = style->standardIcon(QStyle::SP_DialogCancelButton); - break; - - case RemuxEntryState::InvalidPath: - icon = style->standardIcon(QStyle::SP_MessageBoxWarning); - break; - - default: - break; - } - - return icon; -} - -void RemuxQueueModel::checkInputPath(int row) -{ - RemuxQueueEntry &entry = queue[row]; - - if (entry.sourcePath.isEmpty()) { - entry.state = RemuxEntryState::Empty; - } else { - entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); - QFileInfo fileInfo(entry.sourcePath); - if (fileInfo.exists()) - entry.state = RemuxEntryState::Ready; - else - entry.state = RemuxEntryState::InvalidPath; - - QString newExt = ".mp4"; - QString suffix = fileInfo.suffix(); - - if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { - newExt = ".remuxed." + suffix; - } - - if (entry.state == RemuxEntryState::Ready) - entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + - fileInfo.completeBaseName() + newExt); - } - - if (entry.state == RemuxEntryState::Ready && isProcessing) - entry.state = RemuxEntryState::Pending; - - emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); -} - -QFileInfoList RemuxQueueModel::checkForOverwrites() const -{ - QFileInfoList list; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Ready) { - QFileInfo fileInfo(entry.targetPath); - if (fileInfo.exists()) { - list.append(fileInfo); - } - } - } - - return list; -} - -bool RemuxQueueModel::checkForErrors() const -{ - bool hasErrors = false; - - for (const RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Error) { - hasErrors = true; - break; - } - } - - return hasErrors; -} - -void RemuxQueueModel::clearAll() -{ - beginRemoveRows(QModelIndex(), 0, queue.size() - 1); - queue.clear(); - endRemoveRows(); -} - -void RemuxQueueModel::clearFinished() -{ - int index = 0; - - for (index = 0; index < queue.size(); index++) { - const RemuxQueueEntry &entry = queue[index]; - if (entry.state == RemuxEntryState::Complete) { - beginRemoveRows(QModelIndex(), index, index); - queue.removeAt(index); - endRemoveRows(); - index--; - } - } -} - -bool RemuxQueueModel::canClearFinished() const -{ - bool canClearFinished = false; - for (const RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Complete) { - canClearFinished = true; - break; - } - - return canClearFinished; -} - -void RemuxQueueModel::beginProcessing() -{ - for (RemuxQueueEntry &entry : queue) - if (entry.state == RemuxEntryState::Ready) - entry.state = RemuxEntryState::Pending; - - // Signal that the insertion point no longer exists. - beginRemoveRows(QModelIndex(), queue.length(), queue.length()); - endRemoveRows(); - - isProcessing = true; - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -void RemuxQueueModel::endProcessing() -{ - for (RemuxQueueEntry &entry : queue) { - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::Ready; - } - } - - // Signal that the insertion point exists again. - isProcessing = false; - if (!autoRemux) { - beginInsertRows(QModelIndex(), queue.length(), queue.length()); - endInsertRows(); - } - - emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); -} - -bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) -{ - bool anyStarted = false; - - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::Pending) { - entry.state = RemuxEntryState::InProgress; - - inputPath = entry.sourcePath; - outputPath = entry.targetPath; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - anyStarted = true; - break; - } - } - - return anyStarted; -} - -void RemuxQueueModel::finishEntry(bool success) -{ - for (int row = 0; row < queue.length(); row++) { - RemuxQueueEntry &entry = queue[row]; - if (entry.state == RemuxEntryState::InProgress) { - if (success) - entry.state = RemuxEntryState::Complete; - else - entry.state = RemuxEntryState::Error; - - QModelIndex index = this->index(row, RemuxEntryColumn::State); - emit dataChanged(index, index); - - break; - } - } -} - -/********************************************************** - The actual remux window implementation -**********************************************************/ - -OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) - : QDialog(parent), - queueModel(new RemuxQueueModel), - worker(new RemuxWorker()), - ui(new Ui::OBSRemux), - recPath(path), - autoRemux(autoRemux_) -{ - setAcceptDrops(true); - - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - - if (autoRemux) { - resize(280, 40); - ui->tableView->hide(); - ui->buttonBox->hide(); - ui->label->hide(); - } - - ui->progressBar->setMinimum(0); - ui->progressBar->setMaximum(1000); - ui->progressBar->setValue(0); - - ui->tableView->setModel(queueModel); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, - new RemuxEntryPathItemDelegate(false, recPath)); - ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, - new RemuxEntryPathItemDelegate(true, recPath)); - ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, - QHeaderView::ResizeMode::Fixed); - ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); - ui->tableView->setTextElideMode(Qt::ElideMiddle); - ui->tableView->setWordWrap(false); - - installEventFilter(CreateShortcutFilter()); - - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); - ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); - - connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); - connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); - connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, - &OBSRemux::clearAll); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); - - worker->moveToThread(&remuxer); - remuxer.start(); - - connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); - connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); - connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); - connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); - - connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); - connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); - - QModelIndex index = queueModel->createIndex(0, 1); - QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, - Q_ARG(const QModelIndex &, index)); -} - -bool OBSRemux::stopRemux() -{ - if (!worker->isWorking) - return true; - - // By locking the worker thread's mutex, we ensure that its - // update poll will be blocked as long as we're in here with - // the popup open. - QMutexLocker lock(&worker->updateMutex); - - bool exit = false; - - if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { - exit = true; - } - - if (exit) { - // Inform the worker it should no longer be - // working. It will interrupt accordingly in - // its next update callback. - worker->isWorking = false; - } - - return exit; -} - -OBSRemux::~OBSRemux() -{ - stopRemux(); - remuxer.quit(); - remuxer.wait(); -} - -void OBSRemux::rowCountChanged(const QModelIndex &, int, int) -{ - // See if there are still any rows ready to remux. Change - // the state of the "go" button accordingly. - // There must be more than one row, since there will always be - // at least one row for the empty insertion point. - if (queueModel->rowCount() > 1) { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - } else { - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); - } -} - -void OBSRemux::dropEvent(QDropEvent *ev) -{ - QStringList urlList; - - for (QUrl url : ev->mimeData()->urls()) { - QFileInfo fileInfo(url.toLocalFile()); - - if (fileInfo.isDir()) { - QStringList directoryFilter; - directoryFilter << "*.flv" - << "*.mp4" - << "*.mov" - << "*.mkv" - << "*.ts" - << "*.m3u8"; - - QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, - QDirIterator::Subdirectories); - - while (dirIter.hasNext()) { - urlList.append(dirIter.next()); - } - } else { - urlList.append(fileInfo.canonicalFilePath()); - } - } - - if (urlList.empty()) { - QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), - QMessageBox::Ok); - } else if (!autoRemux) { - QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); - queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); - } -} - -void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) -{ - if (ev->mimeData()->hasUrls() && !worker->isWorking) - ev->accept(); -} - -void OBSRemux::beginRemux() -{ - if (worker->isWorking) { - stopRemux(); - return; - } - - bool proceedWithRemux = true; - QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); - - if (!overwriteFiles.empty()) { - QString message = QTStr("Remux.FileExists"); - message += "\n\n"; - - for (QFileInfo fileInfo : overwriteFiles) - message += fileInfo.canonicalFilePath() + "\n"; - - if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) - proceedWithRemux = false; - } - - if (!proceedWithRemux) - return; - - // Set all jobs to "pending" first. - queueModel->beginProcessing(); - - ui->progressBar->setVisible(true); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); - setAcceptDrops(false); - - remuxNextEntry(); -} - -void OBSRemux::AutoRemux(QString inFile, QString outFile) -{ - if (inFile != "" && outFile != "" && autoRemux) { - ui->progressBar->setVisible(true); - emit remux(inFile, outFile); - autoRemuxFile = outFile; - } -} - -void OBSRemux::remuxNextEntry() -{ - worker->lastProgress = 0.f; - - QString inputPath, outputPath; - if (queueModel->beginNextEntry(inputPath, outputPath)) { - emit remux(inputPath, outputPath); - } else { - queueModel->autoRemux = autoRemux; - queueModel->endProcessing(); - - if (!autoRemux) { - OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), - queueModel->checkForErrors() ? QTStr("Remux.FinishedError") - : QTStr("Remux.Finished")); - } - - ui->progressBar->setVisible(autoRemux); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); - ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); - ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); - setAcceptDrops(true); - } -} - -void OBSRemux::closeEvent(QCloseEvent *event) -{ - if (!stopRemux()) - event->ignore(); - else - QDialog::closeEvent(event); -} - -void OBSRemux::reject() -{ - if (!stopRemux()) - return; - - QDialog::reject(); -} - -void OBSRemux::updateProgress(float percent) -{ - ui->progressBar->setValue(percent * 10); -} - -void OBSRemux::remuxFinished(bool success) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - - queueModel->finishEntry(success); - - if (autoRemux && autoRemuxFile != "") { - QTimer::singleShot(3000, this, &OBSRemux::close); - - OBSBasic *main = OBSBasic::Get(); - main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); - } - - remuxNextEntry(); -} - -void OBSRemux::clearFinished() -{ - queueModel->clearFinished(); -} - -void OBSRemux::clearAll() -{ - queueModel->clearAll(); -} - -/********************************************************** - Worker thread - Executes the libobs remux operation as a - background process. -**********************************************************/ - -void RemuxWorker::UpdateProgress(float percent) -{ - if (abs(lastProgress - percent) < 0.1f) - return; - - emit updateProgress(percent); - lastProgress = percent; -} - -void RemuxWorker::remux(const QString &source, const QString &target) -{ - isWorking = true; - - auto callback = [](void *data, float percent) { - RemuxWorker *rw = static_cast(data); - - QMutexLocker lock(&rw->updateMutex); - - rw->UpdateProgress(percent); - - return rw->isWorking; - }; - - bool stopped = false; - bool success = false; - - media_remux_job_t mr_job = nullptr; - if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { - - success = media_remux_job_process(mr_job, callback, this); - - media_remux_job_destroy(mr_job); - - stopped = !isWorking; - } - - isWorking = false; - - emit remuxFinished(!stopped && success); -} diff --git a/UI/window-remux.hpp b/UI/window-remux.hpp deleted file mode 100644 index 20a13a780..000000000 --- a/UI/window-remux.hpp +++ /dev/null @@ -1,173 +0,0 @@ -/****************************************************************************** - Copyright (C) 2014 by Ruwen Hahn - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include "ui_OBSRemux.h" - -#include -#include - -class RemuxQueueModel; -class RemuxWorker; - -enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; -Q_DECLARE_METATYPE(RemuxEntryState); - -class OBSRemux : public QDialog { - Q_OBJECT - - QPointer queueModel; - QThread remuxer; - QPointer worker; - - std::unique_ptr ui; - - const char *recPath; - - virtual void closeEvent(QCloseEvent *event) override; - virtual void reject() override; - - bool autoRemux; - QString autoRemuxFile; - -public: - explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); - virtual ~OBSRemux() override; - - using job_t = std::shared_ptr; - - void AutoRemux(QString inFile, QString outFile); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - - void remuxNextEntry(); - -private slots: - void rowCountChanged(const QModelIndex &parent, int first, int last); - -public slots: - void updateProgress(float percent); - void remuxFinished(bool success); - void beginRemux(); - bool stopRemux(); - void clearFinished(); - void clearAll(); - -signals: - void remux(const QString &source, const QString &target); -}; - -class RemuxQueueModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSRemux; - -public: - RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - - QFileInfoList checkForOverwrites() const; - bool checkForErrors() const; - void beginProcessing(); - void endProcessing(); - bool beginNextEntry(QString &inputPath, QString &outputPath); - void finishEntry(bool success); - bool canClearFinished() const; - void clearFinished(); - void clearAll(); - - bool autoRemux = false; - -private: - struct RemuxQueueEntry { - RemuxEntryState state; - - QString sourcePath; - QString targetPath; - }; - - QList queue; - bool isProcessing; - - static QVariant getIcon(RemuxEntryState state); - - void checkInputPath(int row); -}; - -class RemuxWorker : public QObject { - Q_OBJECT - - QMutex updateMutex; - - bool isWorking; - - float lastProgress; - void UpdateProgress(float percent); - - explicit RemuxWorker() : isWorking(false) {} - virtual ~RemuxWorker(){}; - -private slots: - void remux(const QString &source, const QString &target); - -signals: - void updateProgress(float percent); - void remuxFinished(bool success); - - friend class OBSRemux; -}; - -class RemuxEntryPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); - -private slots: - void updateText(); -}; diff --git a/build-aux/.run-format.zsh b/build-aux/.run-format.zsh index 26c92f8f1..1596b8c72 100755 --- a/build-aux/.run-format.zsh +++ b/build-aux/.run-format.zsh @@ -54,7 +54,7 @@ invoke_formatter() { exit 2 fi - if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N)) + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N)) source_files=(${source_files:#*/(obs-websocket/deps|decklink/*/decklink-sdk|mac-syphon/syphon-framework|libdshowcapture)/*}) @@ -102,7 +102,7 @@ invoke_formatter() { fi } - if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|UI|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) + if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) source_files=(${source_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) source_files=(${source_files:#(cmake/Modules/*|*/legacy.cmake)}) @@ -150,7 +150,7 @@ invoke_formatter() { exit 2 } - if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins)/**/*.swift(.N)) + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N)) check_files() { local -i num_failures=0 diff --git a/cmake/linux/cpackconfig.cmake b/cmake/linux/cpackconfig.cmake index f8b7869db..37eeaa035 100644 --- a/cmake/linux/cpackconfig.cmake +++ b/cmake/linux/cpackconfig.cmake @@ -5,7 +5,7 @@ include_guard(GLOBAL) include(cpackconfig_common) # Add GPLv2 license file to CPack -set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt") set(CPACK_PACKAGE_EXECUTABLES "obs") if(ENABLE_RELEASE_BUILD) diff --git a/cmake/windows/cpackconfig.cmake b/cmake/windows/cpackconfig.cmake index 2191e34dc..f51832fbd 100644 --- a/cmake/windows/cpackconfig.cmake +++ b/cmake/windows/cpackconfig.cmake @@ -5,7 +5,7 @@ include_guard(GLOBAL) include(cpackconfig_common) # Add GPLv2 license file to CPack -set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt") set(CPACK_PACKAGE_VERSION "${OBS_VERSION_CANONICAL}") set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-windows-${CMAKE_VS_PLATFORM_NAME}") set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY FALSE) diff --git a/UI/CMakeLists.txt b/frontend/CMakeLists.txt similarity index 55% rename from UI/CMakeLists.txt rename to frontend/CMakeLists.txt index 0917c82c8..380941569 100644 --- a/UI/CMakeLists.txt +++ b/frontend/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.28...3.30) -add_subdirectory(obs-frontend-api) +add_subdirectory(api) -option(ENABLE_UI "Enable building with UI (requires Qt)" ON) +option(ENABLE_FRONTEND "Enable building with UI frontend (requires Qt6)" ON) -if(NOT ENABLE_UI) +if(NOT ENABLE_FRONTEND) target_disable_feature(obs "User Interface") return() else() @@ -15,11 +15,15 @@ find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) find_package(CURL REQUIRED) if(NOT TARGET OBS::json11) - add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" "${CMAKE_BINARY_DIR}/deps/json11") + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" json11) +endif() + +if(NOT TARGET OBS::libobs) + add_subdirectory("${CMAKE_SOURCE_DIR}/libobs" libobs) endif() if(NOT TARGET OBS::bpm) - add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" "${CMAKE_BINARY_DIR}/shared/bpm") + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" bpm) endif() add_executable(obs-studio) @@ -32,17 +36,23 @@ target_link_libraries( FFmpeg::avcodec FFmpeg::avutil FFmpeg::avformat - OBS::bpm OBS::libobs OBS::frontend-api OBS::json11 + OBS::bpm ) -include(cmake/ui-qt.cmake) -include(cmake/ui-elements.cmake) -include(cmake/ui-windows.cmake) +include(cmake/ui-components.cmake) +include(cmake/ui-dialogs.cmake) +include(cmake/ui-docks.cmake) include(cmake/feature-importers.cmake) +include(cmake/ui-oauth.cmake) include(cmake/feature-browserpanels.cmake) +include(cmake/ui-qt.cmake) +include(cmake/ui-settings.cmake) +include(cmake/ui-utility.cmake) +include(cmake/ui-widgets.cmake) +include(cmake/ui-wizards.cmake) if(NOT OAUTH_BASE_URL) set(OAUTH_BASE_URL "https://auth.obsproject.com/" CACHE STRING "Default OAuth base URL") @@ -53,56 +63,13 @@ include(cmake/feature-restream.cmake) include(cmake/feature-youtube.cmake) include(cmake/feature-whatsnew.cmake) -add_subdirectory(frontend-plugins) +add_subdirectory(plugins) -configure_file(ui-config.h.in ui-config.h) +configure_file(cmake/templates/ui-config.h.in ui-config.h) target_sources( obs-studio - PRIVATE - api-interface.cpp - auth-base.cpp - auth-base.hpp - auth-listener.cpp - auth-listener.hpp - auth-oauth.cpp - auth-oauth.hpp - display-helpers.hpp - ffmpeg-utils.cpp - ffmpeg-utils.hpp - multiview.cpp - multiview.hpp - obf.c - obf.h - obs-app-theming.cpp - obs-app-theming.hpp - obs-app.cpp - obs-app.hpp - obs-proxy-style.cpp - obs-proxy-style.hpp - platform.hpp - qt-display.cpp - qt-display.hpp - ui-config.h - ui-validation.cpp - ui-validation.hpp -) - -target_sources( - obs-studio - PRIVATE - goliveapi-censoredjson.cpp - goliveapi-censoredjson.hpp - goliveapi-network.cpp - goliveapi-network.hpp - goliveapi-postdata.cpp - goliveapi-postdata.hpp - models/multitrack-video.hpp - multitrack-video-error.cpp - multitrack-video-error.hpp - multitrack-video-output.cpp - multitrack-video-output.hpp - system-info.hpp + PRIVATE obs-main.cpp OBSStudioAPI.cpp OBSStudioAPI.hpp OBSApp.cpp OBSApp.hpp OBSApp_Themes.cpp ui-config.h ) if(OS_WINDOWS) @@ -132,4 +99,18 @@ get_property(obs_module_list GLOBAL PROPERTY OBS_MODULES_ENABLED) list(JOIN obs_module_list "|" SAFE_MODULES) target_compile_definitions(obs-studio PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"") +get_target_property(target_sources obs-studio SOURCES) +set(target_cpp_sources ${target_sources}) +set(target_hpp_sources ${target_sources}) +set(target_qt_sources ${target_sources}) +list(FILTER target_cpp_sources INCLUDE REGEX ".+\\.(cpp|mm|c|m)") +list(SORT target_cpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +list(FILTER target_hpp_sources INCLUDE REGEX ".+\\.(hpp|h)") +list(SORT target_hpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +list(FILTER target_qt_sources INCLUDE REGEX ".+\\.(ui|qrc)") +list(SORT target_qt_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${target_cpp_sources}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${target_hpp_sources}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Qt Files" FILES ${target_qt_sources}) + set_target_properties_obs(obs-studio PROPERTIES FOLDER frontend OUTPUT_NAME "$,obs64,obs>") diff --git a/UI/obs-app.cpp b/frontend/OBSApp.cpp similarity index 57% rename from UI/obs-app.cpp rename to frontend/OBSApp.cpp index 883bee21a..36f317728 100644 --- a/UI/obs-app.cpp +++ b/frontend/OBSApp.cpp @@ -15,108 +15,59 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "obs-proxy-style.hpp" -#include "log-viewer.hpp" -#include "volume-control.hpp" -#include "window-basic-main.hpp" -#ifdef __APPLE__ -#include "window-permissions.hpp" -#endif -#include "window-basic-settings.hpp" -#include "platform.hpp" - -#include - -#include - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#include -#include -#include -#endif +#include "OBSApp.hpp" +#include +#include #if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) -#include "update/models/branches.hpp" +#include #endif +#include #if !defined(_WIN32) && !defined(__APPLE__) #include +#endif +#include + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include +#endif + +#ifdef _WIN32 +#include +#else +#include +#endif +#if !defined(_WIN32) && !defined(__APPLE__) #include #endif -#include +#ifdef _WIN32 +#include +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif -#include "ui-config.h" +#include "moc_OBSApp.cpp" using namespace std; -static log_handler_t def_log_handler; +string currentLogFile; +string lastLogFile; +string lastCrashLogFile; -static string currentLogFile; -static string lastLogFile; -static string lastCrashLogFile; +extern bool portable_mode; +extern bool safe_mode; +extern bool disable_3p_plugins; +extern bool opt_disable_updater; +extern bool opt_disable_missing_files_check; +extern string opt_starting_collection; +extern string opt_starting_profile; -bool portable_mode = false; -bool steam = false; -bool safe_mode = false; -bool disable_3p_plugins = false; -bool unclean_shutdown = false; -bool disable_shutdown_check = false; -static bool multi = false; -static bool log_verbose = false; -static bool unfiltered_log = false; -bool opt_start_streaming = false; -bool opt_start_recording = false; -bool opt_studio_mode = false; -bool opt_start_replaybuffer = false; -bool opt_start_virtualcam = false; -bool opt_minimize_tray = false; -bool opt_allow_opengl = false; -bool opt_always_on_top = false; -bool opt_disable_updater = false; -bool opt_disable_missing_files_check = false; -string opt_starting_collection; -string opt_starting_profile; -string opt_starting_scene; - -bool restart = false; -bool restart_safe = false; -QStringList arguments; - -QPointer obsLogViewer; +extern QPointer obsLogViewer; #ifndef _WIN32 int OBSApp::sigintFd[2]; @@ -252,28 +203,6 @@ QObject *CreateShortcutFilter() }); } -string CurrentTimeString() -{ - using namespace std::chrono; - - struct tm tstruct; - char buf[80]; - - auto tp = system_clock::now(); - auto now = system_clock::to_time_t(tp); - tstruct = *localtime(&now); - - size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); - if (ratio_less::value && written && (sizeof(buf) - written) > 5) { - auto tp_secs = time_point_cast(tp); - auto millis = duration_cast(tp - tp_secs).count(); - - snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); - } - - return buf; -} - string CurrentDateTimeString() { time_t now = time(0); @@ -284,145 +213,6 @@ string CurrentDateTimeString() return buf; } -static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) -{ - static mutex logfile_mutex; - string msg; - msg += timeString; - msg += str; - - logfile_mutex.lock(); - logFile << msg << endl; - logfile_mutex.unlock(); - - if (!!obsLogViewer) - QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), - Q_ARG(QString, QString(msg.c_str()))); -} - -static inline void LogStringChunk(fstream &logFile, char *str, int log_level) -{ - char *nextLine = str; - string timeString = CurrentTimeString(); - timeString += ": "; - - while (*nextLine) { - char *nextLine = strchr(str, '\n'); - if (!nextLine) - break; - - if (nextLine != str && nextLine[-1] == '\r') { - nextLine[-1] = 0; - } else { - nextLine[0] = 0; - } - - LogString(logFile, timeString.c_str(), str, log_level); - nextLine++; - str = nextLine; - } - - LogString(logFile, timeString.c_str(), str, log_level); -} - -#define MAX_REPEATED_LINES 30 -#define MAX_CHAR_VARIATION (255 * 3) - -static inline int sum_chars(const char *str) -{ - int val = 0; - for (; *str != 0; str++) - val += *str; - - return val; -} - -static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) -{ - static mutex log_mutex; - static const char *last_msg_ptr = nullptr; - static int last_char_sum = 0; - static int rep_count = 0; - - int new_sum = sum_chars(output_str); - - lock_guard guard(log_mutex); - - if (unfiltered_log) { - return false; - } - - if (last_msg_ptr == msg) { - int diff = std::abs(new_sum - last_char_sum); - if (diff < MAX_CHAR_VARIATION) { - return (rep_count++ >= MAX_REPEATED_LINES); - } - } - - if (rep_count > MAX_REPEATED_LINES) { - logFile << CurrentTimeString() << ": Last log entry repeated for " - << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; - } - - last_msg_ptr = msg; - last_char_sum = new_sum; - rep_count = 0; - - return false; -} - -static void do_log(int log_level, const char *msg, va_list args, void *param) -{ - fstream &logFile = *static_cast(param); - char str[8192]; - -#ifndef _WIN32 - va_list args2; - va_copy(args2, args); -#endif - - vsnprintf(str, sizeof(str), msg, args); - -#ifdef _WIN32 - if (IsDebuggerPresent()) { - int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); - if (wNum > 1) { - static wstring wide_buf; - static mutex wide_mutex; - - lock_guard lock(wide_mutex); - wide_buf.reserve(wNum + 1); - wide_buf.resize(wNum - 1); - MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); - wide_buf.push_back('\n'); - - OutputDebugStringW(wide_buf.c_str()); - } - } -#endif - -#if !defined(_WIN32) && defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - - if (log_level <= LOG_INFO || log_verbose) { -#if !defined(_WIN32) && !defined(_DEBUG) - def_log_handler(log_level, msg, args2, nullptr); -#endif - if (!too_many_repeated_entries(logFile, msg, str)) - LogStringChunk(logFile, str, log_level); - } - -#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) - if (log_level <= LOG_ERROR && IsDebuggerPresent()) - __debugbreak(); -#endif - -#ifndef _WIN32 - va_end(args2); -#endif -} - #define DEFAULT_LANG "en-US" bool OBSApp::InitGlobalConfigDefaults() @@ -1214,8 +1004,6 @@ void OBSApp::DisableHotkeys() ResetHotkeyState(applicationState() == Qt::ApplicationActive); } -Q_DECLARE_METATYPE(VoidFunc) - void OBSApp::Exec(VoidFunc func) { func(); @@ -1437,172 +1225,6 @@ skip: return QApplication::notify(receiver, e); } -QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const -{ - const char *out = nullptr; - QString str(sourceText); - str.replace(" ", ""); - if (!App()->TranslateString(QT_TO_UTF8(str), &out)) - return QString(sourceText); - - return QT_UTF8(out); -} - -static bool get_token(lexer *lex, string &str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - str.assign(token.text.array, token.text.len); - return true; -} - -static bool expect_token(lexer *lex, const char *str, base_token_type type) -{ - base_token token; - if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) - return false; - if (token.type != type) - return false; - - return strref_cmp(&token.text, str) == 0; -} - -static uint64_t convert_log_name(bool has_prefix, const char *name) -{ - BaseLexer lex; - string year, month, day, hour, minute, second; - - lexer_start(lex, name); - - if (has_prefix) { - string temp; - if (!get_token(lex, temp, BASETOKEN_ALPHA)) - return 0; - } - - if (!get_token(lex, year, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, month, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, day, BASETOKEN_DIGIT)) - return 0; - if (!get_token(lex, hour, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, minute, BASETOKEN_DIGIT)) - return 0; - if (!expect_token(lex, "-", BASETOKEN_OTHER)) - return 0; - if (!get_token(lex, second, BASETOKEN_DIGIT)) - return 0; - - stringstream timestring; - timestring << year << month << day << hour << minute << second; - return std::stoull(timestring.str()); -} - -/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ -/* TODO: Remove after version 32.0. */ -#if defined(__FreeBSD__) -static void move_to_xdg(void) -{ - char old_path[512]; - char new_path[512]; - char *home = getenv("HOME"); - if (!home) - return; - - if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) - return; - - /* make base xdg path if it doesn't already exist */ - if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) - return; - if (os_mkdirs(new_path) == MKDIR_ERROR) - return; - - if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) - return; - - if (os_file_exists(old_path) && !os_file_exists(new_path)) { - rename(old_path, new_path); - } -} -#endif - -static void delete_oldest_file(bool has_prefix, const char *location) -{ - BPtr logDir(GetAppConfigPathPtr(location)); - string oldestLog; - uint64_t oldest_ts = (uint64_t)-1; - struct os_dirent *entry; - - unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); - - os_dir_t *dir = os_opendir(logDir); - if (dir) { - unsigned int count = 0; - - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts) { - if (ts < oldest_ts) { - oldestLog = entry->d_name; - oldest_ts = ts; - } - - count++; - } - } - - os_closedir(dir); - - if (count > maxLogs) { - stringstream delPath; - - delPath << logDir << "/" << oldestLog; - os_unlink(delPath.str().c_str()); - } - } -} - -static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) -{ - BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); - struct os_dirent *entry; - os_dir_t *dir = os_opendir(logDir); - uint64_t highest_ts = 0; - - if (dir) { - while ((entry = os_readdir(dir)) != NULL) { - if (entry->directory || *entry->d_name == '.') - continue; - - uint64_t ts = convert_log_name(has_prefix, entry->d_name); - - if (ts > highest_ts) { - last = entry->d_name; - highest_ts = ts; - } - } - - os_closedir(dir); - } -} - string GenerateTimeDateFilename(const char *extension, bool noSpace) { time_t now = time(0); @@ -1786,444 +1408,7 @@ vector> GetLocaleNames() return names; } -static void create_log_file(fstream &logFile) -{ - stringstream dst; - - get_last_log(false, "obs-studio/logs", lastLogFile); -#ifdef _WIN32 - get_last_log(true, "obs-studio/crashes", lastCrashLogFile); -#endif - - currentLogFile = GenerateTimeDateFilename("txt"); - dst << "obs-studio/logs/" << currentLogFile.c_str(); - - BPtr path(GetAppConfigPathPtr(dst.str().c_str())); - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); -#else - logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); -#endif - - if (logFile.is_open()) { - delete_oldest_file(false, "obs-studio/logs"); - base_set_log_handler(do_log, &logFile); - } else { - blog(LOG_ERROR, "Failed to open log file"); - } -} - -static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { - profiler_name_store_free(store); -}; - -using ProfilerNameStore = std::unique_ptr; - -ProfilerNameStore CreateNameStore() -{ - return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; -} - -static auto SnapshotRelease = [](profiler_snapshot_t *snap) { - profile_snapshot_free(snap); -}; - -using ProfilerSnapshot = std::unique_ptr; - -ProfilerSnapshot GetSnapshot() -{ - return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; -} - -static void SaveProfilerData(const ProfilerSnapshot &snap) -{ - if (currentLogFile.empty()) - return; - - auto pos = currentLogFile.rfind('.'); - if (pos == currentLogFile.npos) - return; - -#define LITERAL_SIZE(x) x, (sizeof(x) - 1) - ostringstream dst; - dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); - dst.write(currentLogFile.c_str(), pos); - dst.write(LITERAL_SIZE(".csv.gz")); -#undef LITERAL_SIZE - - BPtr path = GetAppConfigPathPtr(dst.str().c_str()); - if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) - blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); -} - -static auto ProfilerFree = [](void *) { - profiler_stop(); - - auto snap = GetSnapshot(); - - profiler_print(snap.get()); - profiler_print_time_between_calls(snap.get()); - - SaveProfilerData(snap); - - profiler_free(); -}; - -QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) -{ - if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) - return new VolumeAccessibleInterface(static_cast(object)); - - return nullptr; -} - -static const char *run_program_init = "run_program_init"; -static int run_program(fstream &logFile, int argc, char *argv[]) -{ - int ret = -1; - - auto profilerNameStore = CreateNameStore(); - - std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); - - profiler_start(); - profile_register_root(run_program_init, 0); - - ScopeProfiler prof{run_program_init}; - -#ifdef _WIN32 - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - - QCoreApplication::addLibraryPath("."); - -#if __APPLE__ - InstallNSApplicationSubclass(); - InstallNSThreadLocks(); - - if (!isInBundle()) { - blog(LOG_ERROR, - "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); - return ret; - } -#endif - -#if !defined(_WIN32) && !defined(__APPLE__) - /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and - * crashes loading saved geometry. Just turn off this theme and let users complain OBS - * looks ugly instead of crashing. */ - const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); - if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) - unsetenv("QT_QPA_PLATFORMTHEME"); -#endif - - /* NOTE: This disables an optimisation in Qt that attempts to determine if - * any "siblings" intersect with a widget when determining the approximate - * visible/unobscured area. However, by Qt's own admission this is slow - * and in the case of OBS it significantly slows down lists with many - * elements (e.g. Hotkeys) and it is actually faster to disable it. */ - qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); - - OBSApp program(argc, argv, profilerNameStore.get()); - try { - QAccessible::installFactory(accessibleFactory); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); - QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); - - bool created_log = false; - - program.AppInit(); - delete_oldest_file(false, "obs-studio/profiler_data"); - - OBSTranslator translator; - program.installTranslator(&translator); - - /* --------------------------------------- */ - /* check and warn if already running */ - - bool cancel_launch = false; - bool already_running = false; - -#ifdef _WIN32 - RunOnceMutex rom = -#endif - CheckIfAlreadyRunning(already_running); - - if (!already_running) { - goto run; - } - - if (!multi) { - QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), - QTStr("AlreadyRunning.Text")); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); - QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); - mb.setDefaultButton(cancelButton); - - mb.exec(); - cancel_launch = mb.clickedButton() == cancelButton; - } - - if (cancel_launch) - return 0; - - if (!created_log) { - create_log_file(logFile); - created_log = true; - } - - if (multi) { - blog(LOG_INFO, "User enabled --multi flag and is now " - "running multiple instances of OBS."); - } else { - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "Warning: OBS is already running!"); - blog(LOG_WARNING, "================================"); - blog(LOG_WARNING, "User is now running multiple " - "instances of OBS!"); - /* Clear unclean_shutdown flag as multiple instances - * running from the same config will lead to a - * false-positive detection.*/ - unclean_shutdown = false; - } - - /* --------------------------------------- */ - run: - -#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) - // Mounted by termina during chromeOS linux container startup - // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh - os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); - if (crosDir) { - QMessageBox::StandardButtons buttons(QMessageBox::Ok); - QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, - nullptr); - - mb.exec(); - return 0; - } -#endif - - if (!created_log) - create_log_file(logFile); - - if (unclean_shutdown) { - blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); - } - - if (unclean_shutdown && !safe_mode) { - QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); - QPushButton *launchSafeButton = - mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); - QPushButton *launchNormalButton = - mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); - mb.setDefaultButton(launchNormalButton); - mb.exec(); - - safe_mode = mb.clickedButton() == launchSafeButton; - if (safe_mode) { - blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); - } else { - blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); - } - } - - qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { - switch (type) { -#ifdef _DEBUG - case QtDebugMsg: - blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); - break; - case QtInfoMsg: - blog(LOG_INFO, "%s", QT_TO_UTF8(message)); - break; -#else - case QtDebugMsg: - case QtInfoMsg: - break; -#endif - case QtWarningMsg: - blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); - break; - case QtCriticalMsg: - case QtFatalMsg: - blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); - break; - } - }); - -#ifdef __APPLE__ - MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); - MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); - MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); - MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); - - int permissionsDialogLastShown = - config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); - if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { - OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, - accessibility_permission); - check.exec(); - } -#endif - -#ifdef _WIN32 - if (IsRunningOnWine()) { - QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); - mb.setTextFormat(Qt::RichText); - mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); - QPushButton *closeButton = mb.addButton(QMessageBox::Close); - mb.setDefaultButton(closeButton); - - mb.exec(); - if (mb.clickedButton() == closeButton) - return 0; - } -#endif - - if (argc > 1) { - stringstream stor; - stor << argv[1]; - for (int i = 2; i < argc; ++i) { - stor << " " << argv[i]; - } - blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); - } - - if (!program.OBSInit()) - return 0; - - prof.Stop(); - - ret = program.exec(); - - } catch (const char *error) { - blog(LOG_ERROR, "%s", error); - OBSErrorBox(nullptr, "%s", error); - } - - if (restart || restart_safe) { - arguments = qApp->arguments(); - - if (restart_safe) { - arguments.append("--safe-mode"); - } else { - arguments.removeAll("--safe-mode"); - } - } - - return ret; -} - -#define MAX_CRASH_REPORT_SIZE (150 * 1024) - -#ifdef _WIN32 - -#define CRASH_MESSAGE \ - "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ - "to the clipboard? The crash log will still be saved to:\n\n%s" - -static void main_crash_handler(const char *format, va_list args, void * /* param */) -{ - char *text = new char[MAX_CRASH_REPORT_SIZE]; - - vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); - text[MAX_CRASH_REPORT_SIZE - 1] = 0; - - string crashFilePath = "obs-studio/crashes"; - - delete_oldest_file(true, crashFilePath.c_str()); - - string name = crashFilePath + "/"; - name += "Crash " + GenerateTimeDateFilename("txt"); - - BPtr path(GetAppConfigPathPtr(name.c_str())); - - fstream file; - -#ifdef _WIN32 - BPtr wpath; - os_utf8_to_wcs_ptr(path, 0, &wpath); - file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#else - file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); -#endif - file << text; - file.close(); - - string pathString(path.Get()); - -#ifdef _WIN32 - std::replace(pathString.begin(), pathString.end(), '/', '\\'); -#endif - - string absolutePath = canonical(filesystem::path(pathString)).u8string(); - - size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); - - unique_ptr message_buffer(new char[size + 1]); - - snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); - - string finalMessage = string(message_buffer.get(), message_buffer.get() + size); - - int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); - - if (ret == IDYES) { - size_t len = strlen(text); - - HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); - memcpy(GlobalLock(mem), text, len); - GlobalUnlock(mem); - - OpenClipboard(0); - EmptyClipboard(); - SetClipboardData(CF_TEXT, mem); - CloseClipboard(); - } - - exit(-1); -} - -static void load_debug_privilege(void) -{ - const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; - TOKEN_PRIVILEGES tp; - HANDLE token; - LUID val; - - if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { - return; - } - - if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); - } - - if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { - blog(LOG_INFO, "Could not set privilege to " - "increase GPU priority"); - } - } - - CloseHandle(token); -} -#endif - -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__linux__) #define BASE_PATH ".." #else #define BASE_PATH "../.." @@ -2351,41 +1536,6 @@ bool WindowPositionValid(QRect rect) return false; } -static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) -{ - return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); -} - -static void check_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - /* Safe Mode detection is disabled in Debug builds to keep developers - * somewhat sane. */ - return; -#else - if (disable_shutdown_check) - return; - - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - if (os_file_exists(sentinelPath)) { - unclean_shutdown = true; - return; - } - - os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); -#endif -} - -static void delete_safe_mode_sentinel(void) -{ -#ifndef NDEBUG - return; -#else - BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); - os_unlink(sentinelPath); -#endif -} - #ifndef _WIN32 void OBSApp::SigIntSignalHandler(int s) { @@ -2423,240 +1573,3 @@ void OBSApp::commitData(QSessionManager &manager) } } #endif - -#ifdef _WIN32 -static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; -static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " - "Redistributables.\n\nYou will now be directed to the download page."; -static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; - -static bool vc_runtime_outdated() -{ - win_version_info ver; - if (!get_dll_ver(L"msvcp140.dll", &ver)) - return true; - /* Major is always 14 (hence 140.dll), so we only care about minor. */ - if (ver.minor >= 40) - return false; - - int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); - if (choice == IDOK) { - /* Open the URL in the default browser. */ - ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); - } - - return true; -} -#endif - -int main(int argc, char *argv[]) -{ -#ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); - - struct sigaction sig_handler; - - sig_handler.sa_handler = OBSApp::SigIntSignalHandler; - sigemptyset(&sig_handler.sa_mask); - sig_handler.sa_flags = 0; - - sigaction(SIGINT, &sig_handler, NULL); - - /* Block SIGPIPE in all threads, this can happen if a thread calls write on - a closed pipe. */ - sigset_t sigpipe_mask; - sigemptyset(&sigpipe_mask); - sigaddset(&sigpipe_mask, SIGPIPE); - sigset_t saved_mask; - if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { - perror("pthread_sigmask"); - exit(1); - } -#endif - -#ifdef _WIN32 - // Abort as early as possible if MSVC runtime is outdated - if (vc_runtime_outdated()) - return 1; - // Try to keep this as early as possible - install_dll_blocklist_hook(); - - obs_init_win32_crash_handler(); - SetErrorMode(SEM_FAILCRITICALERRORS); - load_debug_privilege(); - base_set_crash_handler(main_crash_handler, nullptr); - - const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); - PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); - func(); - } -#endif - - base_get_log_handler(&def_log_handler, nullptr); - -#if defined(__FreeBSD__) - move_to_xdg(); -#endif - - obs_set_cmdline_args(argc, argv); - - for (int i = 1; i < argc; i++) { - if (arg_is(argv[i], "--multi", "-m")) { - multi = true; - disable_shutdown_check = true; - -#if ALLOW_PORTABLE_MODE - } else if (arg_is(argv[i], "--portable", "-p")) { - portable_mode = true; - -#endif - } else if (arg_is(argv[i], "--verbose", nullptr)) { - log_verbose = true; - - } else if (arg_is(argv[i], "--safe-mode", nullptr)) { - safe_mode = true; - - } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { - disable_3p_plugins = true; - - } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { - /* This exists mostly to bypass the dialog during development. */ - disable_shutdown_check = true; - - } else if (arg_is(argv[i], "--always-on-top", nullptr)) { - opt_always_on_top = true; - - } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { - unfiltered_log = true; - - } else if (arg_is(argv[i], "--startstreaming", nullptr)) { - opt_start_streaming = true; - - } else if (arg_is(argv[i], "--startrecording", nullptr)) { - opt_start_recording = true; - - } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { - opt_start_replaybuffer = true; - - } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { - opt_start_virtualcam = true; - - } else if (arg_is(argv[i], "--collection", nullptr)) { - if (++i < argc) - opt_starting_collection = argv[i]; - - } else if (arg_is(argv[i], "--profile", nullptr)) { - if (++i < argc) - opt_starting_profile = argv[i]; - - } else if (arg_is(argv[i], "--scene", nullptr)) { - if (++i < argc) - opt_starting_scene = argv[i]; - - } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { - opt_minimize_tray = true; - - } else if (arg_is(argv[i], "--studio-mode", nullptr)) { - opt_studio_mode = true; - - } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { - opt_allow_opengl = true; - - } else if (arg_is(argv[i], "--disable-updater", nullptr)) { - opt_disable_updater = true; - - } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { - opt_disable_missing_files_check = true; - - } else if (arg_is(argv[i], "--steam", nullptr)) { - steam = true; - - } else if (arg_is(argv[i], "--help", "-h")) { - std::string help = - "--help, -h: Get list of available commands.\n\n" - "--startstreaming: Automatically start streaming.\n" - "--startrecording: Automatically start recording.\n" - "--startreplaybuffer: Start replay buffer.\n" - "--startvirtualcam: Start virtual camera (if available).\n\n" - "--collection : Use specific scene collection." - "\n" - "--profile : Use specific profile.\n" - "--scene : Start with specific scene.\n\n" - "--studio-mode: Enable studio mode.\n" - "--minimize-to-tray: Minimize to system tray.\n" -#if ALLOW_PORTABLE_MODE - "--portable, -p: Use portable mode.\n" -#endif - "--multi, -m: Don't warn when launching multiple instances.\n\n" - "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" - "--only-bundled-plugins: Only load included (first-party) plugins\n" - "--disable-shutdown-check: Disable unclean shutdown detection.\n" - "--verbose: Make log more verbose.\n" - "--always-on-top: Start in 'always on top' mode.\n\n" - "--unfiltered_log: Make log unfiltered.\n\n" - "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" - "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; - -#ifdef _WIN32 - MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); -#else - std::cout << help << "--version, -V: Get current version.\n"; -#endif - exit(0); - - } else if (arg_is(argv[i], "--version", "-V")) { - std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; - exit(0); - } - } - -#if ALLOW_PORTABLE_MODE - if (!portable_mode) { - portable_mode = os_file_exists(BASE_PATH "/portable_mode") || - os_file_exists(BASE_PATH "/obs_portable_mode") || - os_file_exists(BASE_PATH "/portable_mode.txt") || - os_file_exists(BASE_PATH "/obs_portable_mode.txt"); - } - - if (!opt_disable_updater) { - opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || - os_file_exists(BASE_PATH "/disable_updater.txt"); - } - - if (!opt_disable_missing_files_check) { - opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || - os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); - } -#endif - - check_safe_mode_sentinel(); - - fstream logFile; - - curl_global_init(CURL_GLOBAL_ALL); - int ret = run_program(logFile, argc, argv); - -#ifdef _WIN32 - if (hRtwq) { - typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); - PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); - func(); - FreeLibrary(hRtwq); - } - - log_blocked_dlls(); -#endif - - delete_safe_mode_sentinel(); - blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); - base_set_log_handler(nullptr, nullptr); - - if (restart || restart_safe) { - auto executable = arguments.takeFirst(); - QProcess::startDetached(executable, arguments); - } - - return ret; -} diff --git a/UI/obs-app.hpp b/frontend/OBSApp.hpp similarity index 82% rename from UI/obs-app.hpp rename to frontend/OBSApp.hpp index dea5e530b..ce0c4fb65 100644 --- a/UI/obs-app.hpp +++ b/frontend/OBSApp.hpp @@ -17,63 +17,30 @@ #pragma once -#include -#include -#include -#include +#include +#include -#ifndef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include #include +#include +#include +#include + +#include +#include +#include + +#include #include #include -#include #include -#include -#include - -#include "window-main.hpp" -#include "obs-app-theming.hpp" - -std::string CurrentTimeString(); -std::string CurrentDateTimeString(); -std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); -std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); -std::string GetFormatString(const char *format, const char *prefix, const char *suffix); -std::string GetFormatExt(const char *container); -std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, - const char *format); -QObject *CreateShortcutFilter(); - -struct BaseLexer { - lexer lex; - -public: - inline BaseLexer() { lexer_init(&lex); } - inline ~BaseLexer() { lexer_free(&lex); } - operator lexer *() { return &lex; } -}; - -class OBSTranslator : public QTranslator { - Q_OBJECT - -public: - virtual bool isEmpty() const override { return false; } - - virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, - int n) const override; -}; typedef std::function VoidFunc; +Q_DECLARE_METATYPE(VoidFunc) + +class QFileSystemWatcher; +class QSocketNotifier; + struct UpdateBranch { QString name; QString display_name; @@ -233,9 +200,6 @@ signals: int GetAppConfigPath(char *path, size_t size, const char *name); char *GetAppConfigPathPtr(const char *name); -int GetProgramDataPath(char *path, size_t size, const char *name); -char *GetProgramDataPathPtr(const char *name); - inline OBSApp *App() { return static_cast(qApp); @@ -251,30 +215,23 @@ inline QString QTStr(const char *lookupVal) return QString::fromUtf8(Str(lookupVal)); } +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + bool GetFileSafeName(const char *name, std::string &file); bool GetClosestUnusedFileName(std::string &path, const char *extension); -bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); bool WindowPositionValid(QRect rect); -extern bool portable_mode; -extern bool steam; -extern bool safe_mode; -extern bool disable_3p_plugins; - -extern bool opt_start_streaming; -extern bool opt_start_recording; -extern bool opt_start_replaybuffer; -extern bool opt_start_virtualcam; -extern bool opt_minimize_tray; -extern bool opt_studio_mode; -extern bool opt_allow_opengl; -extern bool opt_always_on_top; -extern std::string opt_starting_scene; -extern bool restart; -extern bool restart_safe; - #ifdef _WIN32 extern "C" void install_dll_blocklist_hook(void); extern "C" void log_blocked_dlls(void); #endif + +std::string CurrentDateTimeString(); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); diff --git a/UI/obs-app-theming.cpp b/frontend/OBSApp_Themes.cpp similarity index 99% rename from UI/obs-app-theming.cpp rename to frontend/OBSApp_Themes.cpp index 9698ac455..b4206bb66 100644 --- a/UI/obs-app-theming.cpp +++ b/frontend/OBSApp_Themes.cpp @@ -15,25 +15,23 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "OBSApp.hpp" +#include +#include +#include + +#include +#include #include #include -#include -#include -#include #include -#include +#include +#include +#include #include - -#include "qt-wrappers.hpp" -#include "obs-app.hpp" -#include "obs-app-theming.hpp" -#include "obs-proxy-style.hpp" -#include "platform.hpp" - -#include "ui-config.h" +#include using namespace std; diff --git a/frontend/OBSStudioAPI.cpp b/frontend/OBSStudioAPI.cpp new file mode 100644 index 000000000..6e84fdaed --- /dev/null +++ b/frontend/OBSStudioAPI.cpp @@ -0,0 +1,726 @@ +#include "OBSStudioAPI.hpp" + +#include +#include + +#include + +extern volatile bool streaming_active; +extern volatile bool recording_active; +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; +extern volatile bool virtualcam_active; + +template +inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) +{ + for (size_t i = 0; i < callbacks.size(); i++) { + OBSStudioCallback curCB = callbacks[i]; + if (curCB.callback == callback && curCB.private_data == private_data) + return i; + } + + return (size_t)-1; +} + +void *OBSStudioAPI::obs_frontend_get_main_window() +{ + return (void *)main; +} + +void *OBSStudioAPI::obs_frontend_get_main_window_handle() +{ + return (void *)main->winId(); +} + +void *OBSStudioAPI::obs_frontend_get_system_tray() +{ + return (void *)main->trayIcon.data(); +} + +void OBSStudioAPI::obs_frontend_get_scenes(struct obs_frontend_source_list *sources) +{ + for (int i = 0; i < main->ui->scenes->count(); i++) { + QListWidgetItem *item = main->ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + obs_source_t *source = obs_scene_get_source(scene); + + if (obs_source_get_ref(source) != nullptr) + da_push_back(sources->sources, &source); + } +} + +obs_source_t *OBSStudioAPI::obs_frontend_get_current_scene() +{ + if (main->IsPreviewProgramMode()) { + return obs_weak_source_get_source(main->programScene); + } else { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } +} + +void OBSStudioAPI::obs_frontend_set_current_scene(obs_source_t *scene) +{ + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene))); + } else { + QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), Q_ARG(OBSSource, OBSSource(scene)), + Q_ARG(bool, false)); + } +} + +void OBSStudioAPI::obs_frontend_get_transitions(struct obs_frontend_source_list *sources) +{ + for (int i = 0; i < main->ui->transitions->count(); i++) { + OBSSource tr = main->ui->transitions->itemData(i).value(); + + if (!tr) + continue; + + if (obs_source_get_ref(tr) != nullptr) + da_push_back(sources->sources, &tr); + } +} + +obs_source_t *OBSStudioAPI::obs_frontend_get_current_transition() +{ + OBSSource tr = main->GetCurrentTransition(); + return obs_source_get_ref(tr); +} + +void OBSStudioAPI::obs_frontend_set_current_transition(obs_source_t *transition) +{ + QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); +} + +int OBSStudioAPI::obs_frontend_get_transition_duration() +{ + return main->ui->transitionDuration->value(); +} + +void OBSStudioAPI::obs_frontend_set_transition_duration(int duration) +{ + QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); +} + +void OBSStudioAPI::obs_frontend_release_tbar() +{ + QMetaObject::invokeMethod(main, "TBarReleased"); +} + +void OBSStudioAPI::obs_frontend_set_tbar_position(int position) +{ + QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); +} + +int OBSStudioAPI::obs_frontend_get_tbar_position() +{ + return main->tBar->value(); +} + +void OBSStudioAPI::obs_frontend_get_scene_collections(std::vector &strings) +{ + for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { + strings.emplace_back(collectionName); + } +} + +char *OBSStudioAPI::obs_frontend_get_current_scene_collection() +{ + const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); + return bstrdup(currentCollection.name.c_str()); +} + +void OBSStudioAPI::obs_frontend_set_current_scene_collection(const char *collection) +{ + QList menuActions = main->ui->sceneCollectionMenu->actions(); + QString qstrCollection = QT_UTF8(collection); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrCollection) { + action->trigger(); + break; + } + } + } +} + +bool OBSStudioAPI::obs_frontend_add_scene_collection(const char *name) +{ + bool success = false; + QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), Q_RETURN_ARG(bool, success), + Q_ARG(QString, QT_UTF8(name))); + return success; +} + +void OBSStudioAPI::obs_frontend_get_profiles(std::vector &strings) +{ + const OBSProfileCache &profiles = main->GetProfileCache(); + + for (auto &[profileName, profile] : profiles) { + strings.emplace_back(profileName); + } +} + +char *OBSStudioAPI::obs_frontend_get_current_profile() +{ + const OBSProfile &profile = main->GetCurrentProfile(); + return bstrdup(profile.name.c_str()); +} + +char *OBSStudioAPI::obs_frontend_get_current_profile_path() +{ + const OBSProfile &profile = main->GetCurrentProfile(); + + return bstrdup(profile.path.u8string().c_str()); +} + +void OBSStudioAPI::obs_frontend_set_current_profile(const char *profile) +{ + QList menuActions = main->ui->profileMenu->actions(); + QString qstrProfile = QT_UTF8(profile); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrProfile) { + action->trigger(); + break; + } + } + } +} + +void OBSStudioAPI::obs_frontend_create_profile(const char *name) +{ + QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); +} + +void OBSStudioAPI::obs_frontend_duplicate_profile(const char *name) +{ + QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); +} + +void OBSStudioAPI::obs_frontend_delete_profile(const char *profile) +{ + QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); +} + +void OBSStudioAPI::obs_frontend_streaming_start() +{ + QMetaObject::invokeMethod(main, "StartStreaming"); +} + +void OBSStudioAPI::obs_frontend_streaming_stop() +{ + QMetaObject::invokeMethod(main, "StopStreaming"); +} + +bool OBSStudioAPI::obs_frontend_streaming_active() +{ + return os_atomic_load_bool(&streaming_active); +} + +void OBSStudioAPI::obs_frontend_recording_start() +{ + QMetaObject::invokeMethod(main, "StartRecording"); +} + +void OBSStudioAPI::obs_frontend_recording_stop() +{ + QMetaObject::invokeMethod(main, "StopRecording"); +} + +bool OBSStudioAPI::obs_frontend_recording_active() +{ + return os_atomic_load_bool(&recording_active); +} + +void OBSStudioAPI::obs_frontend_recording_pause(bool pause) +{ + QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); +} + +bool OBSStudioAPI::obs_frontend_recording_paused() +{ + return os_atomic_load_bool(&recording_paused); +} + +bool OBSStudioAPI::obs_frontend_recording_split_file() +{ + if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + uint8_t stack[128]; + calldata cd; + calldata_init_fixed(&cd, stack, sizeof(stack)); + proc_handler_call(ph, "split_file", &cd); + bool result = calldata_bool(&cd, "split_file_enabled"); + return result; + } else { + return false; + } +} + +bool OBSStudioAPI::obs_frontend_recording_add_chapter(const char *name) +{ + if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) + return false; + + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + + calldata cd; + calldata_init(&cd); + calldata_set_string(&cd, "chapter_name", name); + bool result = proc_handler_call(ph, "add_chapter", &cd); + calldata_free(&cd); + return result; +} + +void OBSStudioAPI::obs_frontend_replay_buffer_start() +{ + QMetaObject::invokeMethod(main, "StartReplayBuffer"); +} + +void OBSStudioAPI::obs_frontend_replay_buffer_save() +{ + QMetaObject::invokeMethod(main, "ReplayBufferSave"); +} + +void OBSStudioAPI::obs_frontend_replay_buffer_stop() +{ + QMetaObject::invokeMethod(main, "StopReplayBuffer"); +} + +bool OBSStudioAPI::obs_frontend_replay_buffer_active() +{ + return os_atomic_load_bool(&replaybuf_active); +} + +void *OBSStudioAPI::obs_frontend_add_tools_menu_qaction(const char *name) +{ + main->ui->menuTools->setEnabled(true); + return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); +} + +void OBSStudioAPI::obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) +{ + main->ui->menuTools->setEnabled(true); + + auto func = [private_data, callback]() { + callback(private_data); + }; + + QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); + QObject::connect(action, &QAction::triggered, func); +} + +void *OBSStudioAPI::obs_frontend_add_dock(void *dock) +{ + QDockWidget *d = reinterpret_cast(dock); + + QString name = d->objectName(); + if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { + blog(LOG_WARNING, "The object name of the added dock is empty or already used," + " a temporary one will be set to avoid conflicts"); + + char *uuid = os_generate_uuid(); + name = QT_UTF8(uuid); + bfree(uuid); + name.append("_oldExtraDock"); + + d->setObjectName(name); + } + + return (void *)main->AddDockWidget(d); +} + +bool OBSStudioAPI::obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) +{ + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + OBSDock *dock = new OBSDock(main); + dock->setWidget((QWidget *)widget); + dock->setWindowTitle(QT_UTF8(title)); + dock->setObjectName(QT_UTF8(id)); + + main->AddDockWidget(dock, Qt::RightDockWidgetArea); + + dock->setVisible(false); + dock->setFloating(true); + + return true; +} + +void OBSStudioAPI::obs_frontend_remove_dock(const char *id) +{ + main->RemoveDockWidget(QT_UTF8(id)); +} + +bool OBSStudioAPI::obs_frontend_add_custom_qdock(const char *id, void *dock) +{ + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + QDockWidget *d = reinterpret_cast(dock); + d->setObjectName(QT_UTF8(id)); + + main->AddCustomDockWidget(d); + + return true; +} + +void OBSStudioAPI::obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + callbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + callbacks.erase(callbacks.begin() + idx); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_streaming_output() +{ + auto multitrackVideo = main->outputHandler->multitrackVideo.get(); + auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; + if (mtvOutput) + return mtvOutput; + + OBSOutput output = main->outputHandler->streamOutput.Get(); + return obs_output_get_ref(output); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_recording_output() +{ + OBSOutput out = main->outputHandler->fileOutput.Get(); + return obs_output_get_ref(out); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_replay_buffer_output() +{ + OBSOutput out = main->outputHandler->replayBuffer.Get(); + return obs_output_get_ref(out); +} + +config_t *OBSStudioAPI::obs_frontend_get_profile_config() +{ + return main->activeConfiguration; +} + +config_t *OBSStudioAPI::obs_frontend_get_global_config() +{ + blog(LOG_WARNING, + "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); + return App()->GetAppConfig(); +} + +config_t *OBSStudioAPI::obs_frontend_get_app_config() +{ + return App()->GetAppConfig(); +} + +config_t *OBSStudioAPI::obs_frontend_get_user_config() +{ + return App()->GetUserConfig(); +} + +void OBSStudioAPI::obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) +{ + SavedProjectorInfo proj = { + ProjectorType::Preview, + monitor, + geometry ? geometry : "", + name ? name : "", + }; + if (type) { + if (astrcmpi(type, "Source") == 0) + proj.type = ProjectorType::Source; + else if (astrcmpi(type, "Scene") == 0) + proj.type = ProjectorType::Scene; + else if (astrcmpi(type, "StudioProgram") == 0) + proj.type = ProjectorType::StudioProgram; + else if (astrcmpi(type, "Multiview") == 0) + proj.type = ProjectorType::Multiview; + } + QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), Q_ARG(SavedProjectorInfo *, &proj)); +} + +void OBSStudioAPI::obs_frontend_save() +{ + main->SaveProject(); +} + +void OBSStudioAPI::obs_frontend_defer_save_begin() +{ + QMetaObject::invokeMethod(main, "DeferSaveBegin"); +} + +void OBSStudioAPI::obs_frontend_defer_save_end() +{ + QMetaObject::invokeMethod(main, "DeferSaveEnd"); +} + +void OBSStudioAPI::obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + saveCallbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + saveCallbacks.erase(saveCallbacks.begin() + idx); +} + +void OBSStudioAPI::obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + preloadCallbacks.emplace_back(callback, private_data); +} + +void OBSStudioAPI::obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) +{ + size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + preloadCallbacks.erase(preloadCallbacks.begin() + idx); +} + +void OBSStudioAPI::obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) +{ + App()->PushUITranslation(translate); +} + +void OBSStudioAPI::obs_frontend_pop_ui_translation() +{ + App()->PopUITranslation(); +} + +void OBSStudioAPI::obs_frontend_set_streaming_service(obs_service_t *service) +{ + main->SetService(service); +} + +obs_service_t *OBSStudioAPI::obs_frontend_get_streaming_service() +{ + return main->GetService(); +} + +void OBSStudioAPI::obs_frontend_save_streaming_service() +{ + main->SaveService(); +} + +bool OBSStudioAPI::obs_frontend_preview_program_mode_active() +{ + return main->IsPreviewProgramMode(); +} + +void OBSStudioAPI::obs_frontend_set_preview_program_mode(bool enable) +{ + main->SetPreviewProgramMode(enable); +} + +void OBSStudioAPI::obs_frontend_preview_program_trigger_transition() +{ + QMetaObject::invokeMethod(main, "TransitionClicked"); +} + +bool OBSStudioAPI::obs_frontend_preview_enabled() +{ + return main->previewEnabled; +} + +void OBSStudioAPI::obs_frontend_set_preview_enabled(bool enable) +{ + if (main->previewEnabled != enable) + main->EnablePreviewDisplay(enable); +} + +obs_source_t *OBSStudioAPI::obs_frontend_get_current_preview_scene() +{ + if (main->IsPreviewProgramMode()) { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } + + return nullptr; +} + +void OBSStudioAPI::obs_frontend_set_current_preview_scene(obs_source_t *scene) +{ + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)), + Q_ARG(bool, false)); + } +} + +void OBSStudioAPI::obs_frontend_take_screenshot() +{ + QMetaObject::invokeMethod(main, "Screenshot"); +} + +void OBSStudioAPI::obs_frontend_take_source_screenshot(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source))); +} + +obs_output_t *OBSStudioAPI::obs_frontend_get_virtualcam_output() +{ + OBSOutput output = main->outputHandler->virtualCam.Get(); + return obs_output_get_ref(output); +} + +void OBSStudioAPI::obs_frontend_start_virtualcam() +{ + QMetaObject::invokeMethod(main, "StartVirtualCam"); +} + +void OBSStudioAPI::obs_frontend_stop_virtualcam() +{ + QMetaObject::invokeMethod(main, "StopVirtualCam"); +} + +bool OBSStudioAPI::obs_frontend_virtualcam_active() +{ + return os_atomic_load_bool(&virtualcam_active); +} + +void OBSStudioAPI::obs_frontend_reset_video() +{ + main->ResetVideo(); +} + +void OBSStudioAPI::obs_frontend_open_source_properties(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_source_filters(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_source_interaction(obs_source_t *source) +{ + QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSStudioAPI::obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) +{ + QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +char *OBSStudioAPI::obs_frontend_get_current_record_output_path() +{ + const char *recordOutputPath = main->GetCurrentOutputPath(); + + return bstrdup(recordOutputPath); +} + +const char *OBSStudioAPI::obs_frontend_get_locale_string(const char *string) +{ + return Str(string); +} + +bool OBSStudioAPI::obs_frontend_is_theme_dark() +{ + return App()->IsThemeDark(); +} + +char *OBSStudioAPI::obs_frontend_get_last_recording() +{ + return bstrdup(main->outputHandler->lastRecordingPath.c_str()); +} + +char *OBSStudioAPI::obs_frontend_get_last_screenshot() +{ + return bstrdup(main->lastScreenshot.c_str()); +} + +char *OBSStudioAPI::obs_frontend_get_last_replay() +{ + return bstrdup(main->lastReplay.c_str()); +} + +void OBSStudioAPI::obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, + const char *undo_data, const char *redo_data, bool repeatable) +{ + main->undo_s.add_action( + name, [undo](const std::string &data) { undo(data.c_str()); }, + [redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable); +} + +void OBSStudioAPI::on_load(obs_data_t *settings) +{ + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } +} + +void OBSStudioAPI::on_preload(obs_data_t *settings) +{ + for (size_t i = preloadCallbacks.size(); i > 0; i--) { + auto cb = preloadCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } +} + +void OBSStudioAPI::on_save(obs_data_t *settings) +{ + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, true, cb.private_data); + } +} + +void OBSStudioAPI::on_event(enum obs_frontend_event event) +{ + if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && + event != OBS_FRONTEND_EVENT_EXIT) + return; + + for (size_t i = callbacks.size(); i > 0; i--) { + auto cb = callbacks[i - 1]; + cb.callback(event, cb.private_data); + } +} + +obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) +{ + obs_frontend_callbacks *api = new OBSStudioAPI(main); + obs_frontend_set_callbacks_internal(api); + return api; +} diff --git a/frontend/OBSStudioAPI.hpp b/frontend/OBSStudioAPI.hpp new file mode 100644 index 000000000..5a1872d85 --- /dev/null +++ b/frontend/OBSStudioAPI.hpp @@ -0,0 +1,236 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#pragma once + +#include + +class OBSBasic; + +using namespace std; + +template struct OBSStudioCallback { + T callback; + void *private_data; + + inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} +}; + +struct OBSStudioAPI : obs_frontend_callbacks { + OBSBasic *main; + vector> callbacks; + vector> saveCallbacks; + vector> preloadCallbacks; + + inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} + + void *obs_frontend_get_main_window(void) override; + + void *obs_frontend_get_main_window_handle(void) override; + + void *obs_frontend_get_system_tray(void) override; + + void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override; + + obs_source_t *obs_frontend_get_current_scene(void) override; + + void obs_frontend_set_current_scene(obs_source_t *scene) override; + + void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override; + + obs_source_t *obs_frontend_get_current_transition(void) override; + + void obs_frontend_set_current_transition(obs_source_t *transition) override; + + int obs_frontend_get_transition_duration(void) override; + + void obs_frontend_set_transition_duration(int duration) override; + + void obs_frontend_release_tbar(void) override; + + void obs_frontend_set_tbar_position(int position) override; + + int obs_frontend_get_tbar_position(void) override; + + void obs_frontend_get_scene_collections(std::vector &strings) override; + + char *obs_frontend_get_current_scene_collection(void) override; + + void obs_frontend_set_current_scene_collection(const char *collection) override; + + bool obs_frontend_add_scene_collection(const char *name) override; + + void obs_frontend_get_profiles(std::vector &strings) override; + + char *obs_frontend_get_current_profile(void) override; + + char *obs_frontend_get_current_profile_path(void) override; + + void obs_frontend_set_current_profile(const char *profile) override; + + void obs_frontend_create_profile(const char *name) override; + + void obs_frontend_duplicate_profile(const char *name) override; + + void obs_frontend_delete_profile(const char *profile) override; + + void obs_frontend_streaming_start(void) override; + + void obs_frontend_streaming_stop(void) override; + + bool obs_frontend_streaming_active(void) override; + + void obs_frontend_recording_start(void) override; + + void obs_frontend_recording_stop(void) override; + + bool obs_frontend_recording_active(void) override; + + void obs_frontend_recording_pause(bool pause) override; + + bool obs_frontend_recording_paused(void) override; + + bool obs_frontend_recording_split_file(void) override; + + bool obs_frontend_recording_add_chapter(const char *name) override; + + void obs_frontend_replay_buffer_start(void) override; + + void obs_frontend_replay_buffer_save(void) override; + + void obs_frontend_replay_buffer_stop(void) override; + + bool obs_frontend_replay_buffer_active(void) override; + + void *obs_frontend_add_tools_menu_qaction(const char *name) override; + + void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override; + + void *obs_frontend_add_dock(void *dock) override; + + bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override; + + void obs_frontend_remove_dock(const char *id) override; + + bool obs_frontend_add_custom_qdock(const char *id, void *dock) override; + + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override; + + void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override; + + obs_output_t *obs_frontend_get_streaming_output(void) override; + + obs_output_t *obs_frontend_get_recording_output(void) override; + + obs_output_t *obs_frontend_get_replay_buffer_output(void) override; + + config_t *obs_frontend_get_profile_config(void) override; + + config_t *obs_frontend_get_global_config(void) override; + + config_t *obs_frontend_get_app_config(void) override; + + config_t *obs_frontend_get_user_config(void) override; + + void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, + const char *name) override; + + void obs_frontend_save(void) override; + + void obs_frontend_defer_save_begin(void) override; + + void obs_frontend_defer_save_end(void) override; + + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override; + + void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override; + + void obs_frontend_pop_ui_translation(void) override; + + void obs_frontend_set_streaming_service(obs_service_t *service) override; + + obs_service_t *obs_frontend_get_streaming_service(void) override; + + void obs_frontend_save_streaming_service(void) override; + + bool obs_frontend_preview_program_mode_active(void) override; + + void obs_frontend_set_preview_program_mode(bool enable) override; + + void obs_frontend_preview_program_trigger_transition(void) override; + + bool obs_frontend_preview_enabled(void) override; + + void obs_frontend_set_preview_enabled(bool enable) override; + + obs_source_t *obs_frontend_get_current_preview_scene(void) override; + + void obs_frontend_set_current_preview_scene(obs_source_t *scene) override; + + void obs_frontend_take_screenshot(void) override; + + void obs_frontend_take_source_screenshot(obs_source_t *source) override; + + obs_output_t *obs_frontend_get_virtualcam_output(void) override; + + void obs_frontend_start_virtualcam(void) override; + + void obs_frontend_stop_virtualcam(void) override; + + bool obs_frontend_virtualcam_active(void) override; + + void obs_frontend_reset_video(void) override; + + void obs_frontend_open_source_properties(obs_source_t *source) override; + + void obs_frontend_open_source_filters(obs_source_t *source) override; + + void obs_frontend_open_source_interaction(obs_source_t *source) override; + + void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override; + + char *obs_frontend_get_current_record_output_path(void) override; + + const char *obs_frontend_get_locale_string(const char *string) override; + + bool obs_frontend_is_theme_dark(void) override; + + char *obs_frontend_get_last_recording(void) override; + + char *obs_frontend_get_last_screenshot(void) override; + + char *obs_frontend_get_last_replay(void) override; + + void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo, + const char *undo_data, const char *redo_data, bool repeatable) override; + + void on_load(obs_data_t *settings) override; + + void on_preload(obs_data_t *settings) override; + + void on_save(obs_data_t *settings) override; + + void on_event(enum obs_frontend_event event) override; +}; + +obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); diff --git a/UI/obs-frontend-api/CMakeLists.txt b/frontend/api/CMakeLists.txt similarity index 100% rename from UI/obs-frontend-api/CMakeLists.txt rename to frontend/api/CMakeLists.txt diff --git a/UI/obs-frontend-api/cmake/linux/obs-frontend-api.pc.in b/frontend/api/cmake/linux/obs-frontend-api.pc.in similarity index 100% rename from UI/obs-frontend-api/cmake/linux/obs-frontend-api.pc.in rename to frontend/api/cmake/linux/obs-frontend-api.pc.in diff --git a/UI/obs-frontend-api/cmake/obs-frontend-apiConfig.cmake.in b/frontend/api/cmake/obs-frontend-apiConfig.cmake.in similarity index 100% rename from UI/obs-frontend-api/cmake/obs-frontend-apiConfig.cmake.in rename to frontend/api/cmake/obs-frontend-apiConfig.cmake.in diff --git a/UI/obs-frontend-api/cmake/windows/obs-module.rc.in b/frontend/api/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/obs-frontend-api/cmake/windows/obs-module.rc.in rename to frontend/api/cmake/windows/obs-module.rc.in diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/frontend/api/obs-frontend-api.cpp similarity index 100% rename from UI/obs-frontend-api/obs-frontend-api.cpp rename to frontend/api/obs-frontend-api.cpp diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/frontend/api/obs-frontend-api.h similarity index 100% rename from UI/obs-frontend-api/obs-frontend-api.h rename to frontend/api/obs-frontend-api.h diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/frontend/api/obs-frontend-internal.hpp similarity index 100% rename from UI/obs-frontend-api/obs-frontend-internal.hpp rename to frontend/api/obs-frontend-internal.hpp diff --git a/frontend/cmake/feature-browserpanels.cmake b/frontend/cmake/feature-browserpanels.cmake new file mode 100644 index 000000000..04c36695b --- /dev/null +++ b/frontend/cmake/feature-browserpanels.cmake @@ -0,0 +1,18 @@ +if(TARGET OBS::browser-panels) + target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE) + + target_link_libraries(obs-studio PRIVATE OBS::browser-panels) + + target_sources( + obs-studio + PRIVATE + dialogs/OBSExtraBrowsers.cpp + dialogs/OBSExtraBrowsers.hpp + docks/BrowserDock.cpp + docks/BrowserDock.hpp + utility/ExtraBrowsersDelegate.cpp + utility/ExtraBrowsersDelegate.hpp + utility/ExtraBrowsersModel.cpp + utility/ExtraBrowsersModel.hpp + ) +endif() diff --git a/frontend/cmake/feature-importers.cmake b/frontend/cmake/feature-importers.cmake new file mode 100644 index 000000000..b4f54eaca --- /dev/null +++ b/frontend/cmake/feature-importers.cmake @@ -0,0 +1,16 @@ +target_sources( + obs-studio + PRIVATE + importer/ImporterEntryPathItemDelegate.cpp + importer/ImporterEntryPathItemDelegate.hpp + importer/ImporterModel.cpp + importer/ImporterModel.hpp + importer/OBSImporter.cpp + importer/OBSImporter.hpp + importers/classic.cpp + importers/importers.cpp + importers/importers.hpp + importers/sl.cpp + importers/studio.cpp + importers/xsplit.cpp +) diff --git a/UI/cmake/feature-macos-update.cmake b/frontend/cmake/feature-macos-update.cmake similarity index 51% rename from UI/cmake/feature-macos-update.cmake rename to frontend/cmake/feature-macos-update.cmake index 7c6327792..da654babf 100644 --- a/UI/cmake/feature-macos-update.cmake +++ b/frontend/cmake/feature-macos-update.cmake @@ -9,14 +9,16 @@ endif() target_sources( obs-studio PRIVATE - update/crypto-helpers-mac.mm - update/crypto-helpers.hpp - update/models/branches.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.hpp + utility/crypto-helpers-mac.mm + utility/crypto-helpers.hpp + utility/models/branches.hpp + utility/models/whatsnew.hpp + utility/update-helpers.cpp + utility/update-helpers.hpp + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp ) target_link_libraries( diff --git a/UI/cmake/feature-restream.cmake b/frontend/cmake/feature-restream.cmake similarity index 78% rename from UI/cmake/feature-restream.cmake rename to frontend/cmake/feature-restream.cmake index 65847a1ab..5e77f75dd 100644 --- a/UI/cmake/feature-restream.cmake +++ b/frontend/cmake/feature-restream.cmake @@ -1,5 +1,5 @@ if(RESTREAM_CLIENTID AND RESTREAM_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-restream.cpp auth-restream.hpp) + target_sources(obs-studio PRIVATE oauth/RestreamAuth.cpp oauth/RestreamAuth.hpp) target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED) else() target_disable_feature(obs-studio "Restream API connection") diff --git a/UI/cmake/feature-sparkle.cmake b/frontend/cmake/feature-sparkle.cmake similarity index 65% rename from UI/cmake/feature-sparkle.cmake rename to frontend/cmake/feature-sparkle.cmake index eb9fe7d92..3c9935702 100644 --- a/UI/cmake/feature-sparkle.cmake +++ b/frontend/cmake/feature-sparkle.cmake @@ -1,8 +1,17 @@ if(SPARKLE_APPCAST_URL AND SPARKLE_PUBLIC_KEY) find_library(SPARKLE Sparkle) mark_as_advanced(SPARKLE) - target_sources(obs-studio PRIVATE update/mac-update.cpp update/mac-update.hpp update/sparkle-updater.mm) - set_source_files_properties(update/sparkle-updater.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) + target_sources( + obs-studio + PRIVATE + utility/MacUpdateThread.cpp + utility/MacUpdateThread.hpp + utility/OBSSparkle.hpp + utility/OBSSparkle.mm + utility/OBSUpdateDelegate.h + utility/OBSUpdateDelegate.mm + ) + set_source_files_properties(utility/OBSSparkle.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) target_link_libraries(obs-studio PRIVATE "$") diff --git a/UI/cmake/feature-twitch.cmake b/frontend/cmake/feature-twitch.cmake similarity index 78% rename from UI/cmake/feature-twitch.cmake rename to frontend/cmake/feature-twitch.cmake index b8415749f..663c48e05 100644 --- a/UI/cmake/feature-twitch.cmake +++ b/frontend/cmake/feature-twitch.cmake @@ -1,5 +1,5 @@ if(TWITCH_CLIENTID AND TWITCH_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-twitch.cpp auth-twitch.hpp) + target_sources(obs-studio PRIVATE oauth/TwitchAuth.cpp oauth/TwitchAuth.hpp) target_enable_feature(obs-studio "Twitch API connection" TWITCH_ENABLED) else() target_disable_feature(obs-studio "Twitch API connection") diff --git a/UI/cmake/feature-whatsnew.cmake b/frontend/cmake/feature-whatsnew.cmake similarity index 68% rename from UI/cmake/feature-whatsnew.cmake rename to frontend/cmake/feature-whatsnew.cmake index 8e6771821..c7afb49a9 100644 --- a/UI/cmake/feature-whatsnew.cmake +++ b/frontend/cmake/feature-whatsnew.cmake @@ -20,13 +20,15 @@ if(ENABLE_WHATSNEW AND TARGET OBS::browser-panels) target_sources( obs-studio PRIVATE - update/crypto-helpers-mbedtls.cpp - update/crypto-helpers.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.hpp + utility/crypto-helpers-mbedtls.cpp + utility/crypto-helpers.hpp + utility/models/whatsnew.hpp + utility/update-helpers.cpp + utility/update-helpers.hpp + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp ) endif() diff --git a/UI/cmake/feature-youtube.cmake b/frontend/cmake/feature-youtube.cmake similarity index 55% rename from UI/cmake/feature-youtube.cmake rename to frontend/cmake/feature-youtube.cmake index 4db0e31ad..be87b509f 100644 --- a/UI/cmake/feature-youtube.cmake +++ b/frontend/cmake/feature-youtube.cmake @@ -8,14 +8,17 @@ if( target_sources( obs-studio PRIVATE - auth-youtube.cpp - auth-youtube.hpp - window-dock-youtube-app.cpp - window-dock-youtube-app.hpp - window-youtube-actions.cpp - window-youtube-actions.hpp - youtube-api-wrappers.cpp - youtube-api-wrappers.hpp + dialogs/OBSYoutubeActions.cpp + dialogs/OBSYoutubeActions.hpp + docks/YouTubeAppDock.cpp + docks/YouTubeAppDock.hpp + docks/YouTubeChatDock.cpp + docks/YouTubeChatDock.hpp + forms/OBSYoutubeActions.ui + oauth/YoutubeAuth.cpp + oauth/YoutubeAuth.hpp + utility/YoutubeApiWrappers.cpp + utility/YoutubeApiWrappers.hpp ) target_enable_feature(obs-studio "YouTube API connection" YOUTUBE_ENABLED) diff --git a/UI/cmake/linux/com.obsproject.Studio.desktop b/frontend/cmake/linux/com.obsproject.Studio.desktop similarity index 100% rename from UI/cmake/linux/com.obsproject.Studio.desktop rename to frontend/cmake/linux/com.obsproject.Studio.desktop diff --git a/UI/cmake/linux/com.obsproject.Studio.metainfo.xml.in b/frontend/cmake/linux/com.obsproject.Studio.metainfo.xml.in similarity index 100% rename from UI/cmake/linux/com.obsproject.Studio.metainfo.xml.in rename to frontend/cmake/linux/com.obsproject.Studio.metainfo.xml.in diff --git a/UI/cmake/linux/icons/obs-logo-128.png b/frontend/cmake/linux/icons/obs-logo-128.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-128.png rename to frontend/cmake/linux/icons/obs-logo-128.png diff --git a/UI/cmake/linux/icons/obs-logo-256.png b/frontend/cmake/linux/icons/obs-logo-256.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-256.png rename to frontend/cmake/linux/icons/obs-logo-256.png diff --git a/UI/cmake/linux/icons/obs-logo-512.png b/frontend/cmake/linux/icons/obs-logo-512.png similarity index 100% rename from UI/cmake/linux/icons/obs-logo-512.png rename to frontend/cmake/linux/icons/obs-logo-512.png diff --git a/UI/cmake/linux/icons/obs-logo-scalable.svg b/frontend/cmake/linux/icons/obs-logo-scalable.svg similarity index 100% rename from UI/cmake/linux/icons/obs-logo-scalable.svg rename to frontend/cmake/linux/icons/obs-logo-scalable.svg diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512.png diff --git a/UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png similarity index 100% rename from UI/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png rename to frontend/cmake/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png diff --git a/UI/cmake/macos/Assets.xcassets/Contents.json b/frontend/cmake/macos/Assets.xcassets/Contents.json similarity index 100% rename from UI/cmake/macos/Assets.xcassets/Contents.json rename to frontend/cmake/macos/Assets.xcassets/Contents.json diff --git a/UI/cmake/macos/Info.plist.in b/frontend/cmake/macos/Info.plist.in similarity index 100% rename from UI/cmake/macos/Info.plist.in rename to frontend/cmake/macos/Info.plist.in diff --git a/UI/cmake/macos/entitlements-extension.plist b/frontend/cmake/macos/entitlements-extension.plist similarity index 100% rename from UI/cmake/macos/entitlements-extension.plist rename to frontend/cmake/macos/entitlements-extension.plist diff --git a/UI/cmake/macos/entitlements.plist b/frontend/cmake/macos/entitlements.plist similarity index 100% rename from UI/cmake/macos/entitlements.plist rename to frontend/cmake/macos/entitlements.plist diff --git a/UI/cmake/macos/exportOptions-extension.plist.in b/frontend/cmake/macos/exportOptions-extension.plist.in similarity index 100% rename from UI/cmake/macos/exportOptions-extension.plist.in rename to frontend/cmake/macos/exportOptions-extension.plist.in diff --git a/UI/cmake/macos/exportOptions.plist.in b/frontend/cmake/macos/exportOptions.plist.in similarity index 100% rename from UI/cmake/macos/exportOptions.plist.in rename to frontend/cmake/macos/exportOptions.plist.in diff --git a/UI/cmake/macos/qt.conf b/frontend/cmake/macos/qt.conf similarity index 100% rename from UI/cmake/macos/qt.conf rename to frontend/cmake/macos/qt.conf diff --git a/UI/cmake/os-freebsd.cmake b/frontend/cmake/os-freebsd.cmake similarity index 92% rename from UI/cmake/os-freebsd.cmake rename to frontend/cmake/os-freebsd.cmake index 93544ad16..d06d73874 100644 --- a/UI/cmake/os-freebsd.cmake +++ b/frontend/cmake/os-freebsd.cmake @@ -1,9 +1,7 @@ -target_sources(obs-studio PRIVATE platform-x11.cpp) +target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp) target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}") target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus procstat) -target_sources(obs-studio PRIVATE system-info-posix.cpp) - if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-linux.cmake b/frontend/cmake/os-linux.cmake similarity index 95% rename from UI/cmake/os-linux.cmake rename to frontend/cmake/os-linux.cmake index ce9f873d7..a32a3312e 100644 --- a/UI/cmake/os-linux.cmake +++ b/frontend/cmake/os-linux.cmake @@ -1,12 +1,10 @@ -target_sources(obs-studio PRIVATE platform-x11.cpp) +target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp) target_compile_definitions( obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" $<$:ENABLE_PORTABLE_CONFIG> ) target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus) -target_sources(obs-studio PRIVATE system-info-posix.cpp) - if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-macos.cmake b/frontend/cmake/os-macos.cmake similarity index 75% rename from UI/cmake/os-macos.cmake rename to frontend/cmake/os-macos.cmake index a965f1bff..0bd51c923 100644 --- a/UI/cmake/os-macos.cmake +++ b/frontend/cmake/os-macos.cmake @@ -1,10 +1,16 @@ include(cmake/feature-sparkle.cmake) -target_sources(obs-studio PRIVATE platform-osx.mm forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) +target_sources( + obs-studio + PRIVATE + dialogs/OBSPermissions.cpp + dialogs/OBSPermissions.hpp + forms/OBSPermissions.ui + utility/platform-osx.mm + utility/system-info-macos.mm +) target_compile_options(obs-studio PRIVATE -Wno-quoted-include-in-framework-header -Wno-comma) -target_sources(obs-studio PRIVATE system-info-macos.mm) - set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 14.0.3) diff --git a/UI/cmake/os-windows.cmake b/frontend/cmake/os-windows.cmake similarity index 57% rename from UI/cmake/os-windows.cmake rename to frontend/cmake/os-windows.cmake index f781c9db5..9c77a0263 100644 --- a/UI/cmake/os-windows.cmake +++ b/frontend/cmake/os-windows.cmake @@ -18,42 +18,48 @@ target_sources( obs-studio PRIVATE cmake/windows/obs.manifest + dialogs/OBSUpdate.cpp + dialogs/OBSUpdate.hpp + forms/OBSUpdate.ui obs.rc - platform-windows.cpp - update/crypto-helpers-mbedtls.cpp - update/crypto-helpers.hpp - update/models/branches.hpp - update/models/whatsnew.hpp - update/shared-update.cpp - update/shared-update.hpp - update/update-helpers.cpp - update/update-helpers.hpp - update/update-window.cpp - update/update-window.hpp - update/win-update.cpp - update/win-update.hpp - win-dll-blocklist.c - win-update/updater/manifest.hpp + utility/AutoUpdateThread.cpp + utility/AutoUpdateThread.hpp + utility/crypto-helpers-mbedtls.cpp + utility/crypto-helpers.hpp + utility/models/branches.hpp + utility/models/whatsnew.hpp + utility/platform-windows.cpp + utility/system-info-windows.cpp + utility/update-helpers.cpp + utility/update-helpers.hpp + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp + utility/win-dll-blocklist.c ) -target_sources(obs-studio PRIVATE system-info-windows.cpp) +add_library(obs-updater-manifest INTERFACE) +add_library(OBS::updater-manifest ALIAS obs-updater-manifest) + +target_sources(obs-updater-manifest INTERFACE updater/manifest.hpp) target_link_libraries( obs-studio - PRIVATE crypt32 OBS::blake2 OBS::w32-pthreads MbedTLS::mbedtls nlohmann_json::nlohmann_json Detours::Detours + PRIVATE + crypt32 + OBS::blake2 + OBS::updater-manifest + OBS::w32-pthreads + MbedTLS::mbedtls + nlohmann_json::nlohmann_json + Detours::Detours ) target_compile_definitions(obs-studio PRIVATE PSAPI_VERSION=2) target_link_options(obs-studio PRIVATE /IGNORE:4099 $<$:/NODEFAULTLIB:MSVCRT>) -add_library(obs-update-helpers INTERFACE) -add_library(OBS::update-helpers ALIAS obs-update-helpers) - -target_sources(obs-update-helpers INTERFACE win-update/win-update-helpers.cpp win-update/win-update-helpers.hpp) - -target_include_directories(obs-update-helpers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/win-update") - # Set commit for untagged version comparisons in the Windows updater if(OBS_VERSION MATCHES ".+g[a-f0-9]+.*") string(REGEX REPLACE ".+g([a-f0-9]+).*$" "\\1" OBS_COMMIT ${OBS_VERSION}) @@ -61,9 +67,9 @@ else() set(OBS_COMMIT "") endif() -set_source_files_properties(update/win-update.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}") +set_source_files_properties(utility/AutoUpdateThread.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}") -add_subdirectory(win-update/updater) +add_subdirectory(updater) set_property(TARGET obs-studio APPEND PROPERTY AUTORCC_OPTIONS --format-version 1) diff --git a/UI/ui-config.h.in b/frontend/cmake/templates/ui-config.h.in similarity index 100% rename from UI/ui-config.h.in rename to frontend/cmake/templates/ui-config.h.in diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake new file mode 100644 index 000000000..acea2d66c --- /dev/null +++ b/frontend/cmake/ui-components.cmake @@ -0,0 +1,85 @@ +if(NOT TARGET OBS::qt-slider-ignorewheel) + add_subdirectory( + "${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel" + "${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel" + ) +endif() + +target_link_libraries(obs-studio PRIVATE OBS::qt-slider-ignorewheel) + +target_sources( + obs-studio + PRIVATE + components/AbsoluteSlider.cpp + components/AbsoluteSlider.hpp + components/ApplicationAudioCaptureToolbar.cpp + components/ApplicationAudioCaptureToolbar.hpp + components/AudioCaptureToolbar.cpp + components/AudioCaptureToolbar.hpp + components/BalanceSlider.hpp + components/BrowserToolbar.cpp + components/BrowserToolbar.hpp + components/ClickableLabel.hpp + components/ColorSourceToolbar.cpp + components/ColorSourceToolbar.hpp + components/ComboSelectToolbar.cpp + components/ComboSelectToolbar.hpp + components/DelButton.hpp + components/DeviceCaptureToolbar.cpp + components/DeviceCaptureToolbar.hpp + components/DisplayCaptureToolbar.cpp + components/DisplayCaptureToolbar.hpp + components/EditWidget.hpp + components/FocusList.cpp + components/FocusList.hpp + components/GameCaptureToolbar.cpp + components/GameCaptureToolbar.hpp + components/HScrollArea.cpp + components/HScrollArea.hpp + components/ImageSourceToolbar.cpp + components/ImageSourceToolbar.hpp + components/MediaControls.cpp + components/MediaControls.hpp + components/MenuButton.cpp + components/MenuButton.hpp + components/Multiview.cpp + components/Multiview.hpp + components/MuteCheckBox.hpp + components/NonCheckableButton.hpp + components/OBSAdvAudioCtrl.cpp + components/OBSAdvAudioCtrl.hpp + components/OBSPreviewScalingComboBox.cpp + components/OBSPreviewScalingComboBox.hpp + components/OBSPreviewScalingLabel.cpp + components/OBSPreviewScalingLabel.hpp + components/OBSSourceLabel.cpp + components/OBSSourceLabel.hpp + components/SceneTree.cpp + components/SceneTree.hpp + components/SilentUpdateCheckBox.hpp + components/SilentUpdateSpinBox.hpp + components/SourceToolbar.cpp + components/SourceToolbar.hpp + components/SourceTree.cpp + components/SourceTree.hpp + components/SourceTreeDelegate.cpp + components/SourceTreeDelegate.hpp + components/SourceTreeItem.cpp + components/SourceTreeItem.hpp + components/SourceTreeModel.cpp + components/SourceTreeModel.hpp + components/TextSourceToolbar.cpp + components/TextSourceToolbar.hpp + components/UIValidation.cpp + components/UIValidation.hpp + components/UrlPushButton.cpp + components/UrlPushButton.hpp + components/VisibilityItemDelegate.cpp + components/VisibilityItemDelegate.hpp + components/VisibilityItemWidget.cpp + components/VisibilityItemWidget.hpp + components/VolumeSlider.cpp + components/VolumeSlider.hpp + components/WindowCaptureToolbar.cpp + components/WindowCaptureToolbar.hpp +) diff --git a/frontend/cmake/ui-dialogs.cmake b/frontend/cmake/ui-dialogs.cmake new file mode 100644 index 000000000..dfedb2b21 --- /dev/null +++ b/frontend/cmake/ui-dialogs.cmake @@ -0,0 +1,40 @@ +if(NOT TARGET OBS::properties-view) + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view") +endif() + +target_link_libraries(obs-studio PRIVATE OBS::properties-view) + +target_sources( + obs-studio + PRIVATE + dialogs/NameDialog.cpp + dialogs/NameDialog.hpp + dialogs/OAuthLogin.cpp + dialogs/OAuthLogin.hpp + dialogs/OBSAbout.cpp + dialogs/OBSAbout.hpp + dialogs/OBSBasicAdvAudio.cpp + dialogs/OBSBasicAdvAudio.hpp + dialogs/OBSBasicFilters.cpp + dialogs/OBSBasicFilters.hpp + dialogs/OBSBasicInteraction.cpp + dialogs/OBSBasicInteraction.hpp + dialogs/OBSBasicProperties.cpp + dialogs/OBSBasicProperties.hpp + dialogs/OBSBasicSourceSelect.cpp + dialogs/OBSBasicSourceSelect.hpp + dialogs/OBSBasicTransform.cpp + dialogs/OBSBasicTransform.hpp + dialogs/OBSBasicVCamConfig.cpp + dialogs/OBSBasicVCamConfig.hpp + dialogs/OBSLogReply.cpp + dialogs/OBSLogReply.hpp + dialogs/OBSLogViewer.cpp + dialogs/OBSLogViewer.hpp + dialogs/OBSMissingFiles.cpp + dialogs/OBSMissingFiles.hpp + dialogs/OBSRemux.cpp + dialogs/OBSRemux.hpp + dialogs/OBSWhatsNew.cpp + dialogs/OBSWhatsNew.hpp +) diff --git a/frontend/cmake/ui-docks.cmake b/frontend/cmake/ui-docks.cmake new file mode 100644 index 000000000..0de10de2a --- /dev/null +++ b/frontend/cmake/ui-docks.cmake @@ -0,0 +1 @@ +target_sources(obs-studio PRIVATE docks/OBSDock.cpp docks/OBSDock.hpp) diff --git a/frontend/cmake/ui-oauth.cmake b/frontend/cmake/ui-oauth.cmake new file mode 100644 index 000000000..b48f845e8 --- /dev/null +++ b/frontend/cmake/ui-oauth.cmake @@ -0,0 +1,4 @@ +target_sources( + obs-studio + PRIVATE oauth/Auth.cpp oauth/Auth.hpp oauth/AuthListener.cpp oauth/AuthListener.hpp oauth/OAuth.cpp oauth/OAuth.hpp +) diff --git a/frontend/cmake/ui-qt.cmake b/frontend/cmake/ui-qt.cmake new file mode 100644 index 000000000..c4293e0eb --- /dev/null +++ b/frontend/cmake/ui-qt.cmake @@ -0,0 +1,58 @@ +find_package(Qt6 REQUIRED Widgets Network Svg Xml) + +if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD) + find_package(Qt6 REQUIRED Gui DBus) +endif() + +if(NOT TARGET OBS::qt-wrappers) + add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers") +endif() + +target_link_libraries( + obs-studio + PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers +) + +set_target_properties( + obs-studio + PROPERTIES AUTOMOC TRUE AUTOUIC TRUE AUTORCC TRUE AUTOGEN_PARALLEL AUTO +) + +set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar) + +target_sources( + obs-studio + PRIVATE + forms/AutoConfigFinishPage.ui + forms/AutoConfigStartPage.ui + forms/AutoConfigStartPage.ui + forms/AutoConfigStreamPage.ui + forms/AutoConfigTestPage.ui + forms/AutoConfigVideoPage.ui + forms/ColorSelect.ui + forms/obs.qrc + forms/OBSAbout.ui + forms/OBSAdvAudio.ui + forms/OBSBasic.ui + forms/OBSBasicControls.ui + forms/OBSBasicFilters.ui + forms/OBSBasicInteraction.ui + forms/OBSBasicProperties.ui + forms/OBSBasicSettings.ui + forms/OBSBasicSourceSelect.ui + forms/OBSBasicVCamConfig.ui + forms/OBSExtraBrowsers.ui + forms/OBSImporter.ui + forms/OBSLogReply.ui + forms/OBSLogReply.ui + forms/OBSMissingFiles.ui + forms/OBSRemux.ui + forms/source-toolbar/browser-source-toolbar.ui + forms/source-toolbar/color-source-toolbar.ui + forms/source-toolbar/device-select-toolbar.ui + forms/source-toolbar/game-capture-toolbar.ui + forms/source-toolbar/image-source-toolbar.ui + forms/source-toolbar/media-controls.ui + forms/source-toolbar/text-source-toolbar.ui + forms/StatusBarWidget.ui +) diff --git a/frontend/cmake/ui-settings.cmake b/frontend/cmake/ui-settings.cmake new file mode 100644 index 000000000..cea9e5d52 --- /dev/null +++ b/frontend/cmake/ui-settings.cmake @@ -0,0 +1,15 @@ +target_sources( + obs-studio + PRIVATE + settings/OBSBasicSettings_A11y.cpp + settings/OBSBasicSettings_Appearance.cpp + settings/OBSBasicSettings_Stream.cpp + settings/OBSBasicSettings.cpp + settings/OBSBasicSettings.hpp + settings/OBSHotkeyEdit.cpp + settings/OBSHotkeyEdit.hpp + settings/OBSHotkeyLabel.cpp + settings/OBSHotkeyLabel.hpp + settings/OBSHotkeyWidget.cpp + settings/OBSHotkeyWidget.hpp +) diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake new file mode 100644 index 000000000..88535faff --- /dev/null +++ b/frontend/cmake/ui-utility.cmake @@ -0,0 +1,69 @@ +target_sources( + obs-studio + PRIVATE + utility/AdvancedOutput.cpp + utility/AdvancedOutput.hpp + utility/audio-encoders.cpp + utility/audio-encoders.hpp + utility/BaseLexer.hpp + utility/BasicOutputHandler.cpp + utility/BasicOutputHandler.hpp + utility/display-helpers.hpp + utility/FFmpegCodec.cpp + utility/FFmpegCodec.hpp + utility/FFmpegFormat.cpp + utility/FFmpegFormat.hpp + utility/FFmpegShared.hpp + utility/GoLiveAPI_CensoredJson.cpp + utility/GoLiveAPI_CensoredJson.hpp + utility/GoLiveAPI_Network.cpp + utility/GoLiveAPI_Network.hpp + utility/GoLiveAPI_PostData.cpp + utility/GoLiveAPI_PostData.hpp + utility/item-widget-helpers.cpp + utility/item-widget-helpers.hpp + utility/MissingFilesModel.cpp + utility/MissingFilesModel.hpp + utility/MissingFilesPathItemDelegate.cpp + utility/MissingFilesPathItemDelegate.hpp + utility/models/multitrack-video.hpp + utility/MultitrackVideoError.cpp + utility/MultitrackVideoError.hpp + utility/MultitrackVideoOutput.cpp + utility/MultitrackVideoOutput.hpp + utility/obf.c + utility/obf.h + utility/OBSEventFilter.hpp + utility/OBSProxyStyle.cpp + utility/OBSProxyStyle.hpp + utility/OBSTheme.hpp + utility/OBSThemeVariable.hpp + utility/OBSTranslator.cpp + utility/OBSTranslator.hpp + utility/platform.hpp + utility/QuickTransition.cpp + utility/QuickTransition.hpp + utility/RemoteTextThread.cpp + utility/RemoteTextThread.hpp + utility/RemuxEntryPathItemDelegate.cpp + utility/RemuxEntryPathItemDelegate.hpp + utility/RemuxQueueModel.cpp + utility/RemuxQueueModel.hpp + utility/RemuxWorker.cpp + utility/RemuxWorker.hpp + utility/SceneRenameDelegate.cpp + utility/SceneRenameDelegate.hpp + utility/ScreenshotObj.cpp + utility/ScreenshotObj.hpp + utility/SettingsEventFilter.hpp + utility/SimpleOutput.cpp + utility/SimpleOutput.hpp + utility/StartMultiTrackVideoStreamingGuard.hpp + utility/SurfaceEventFilter.hpp + utility/system-info.hpp + utility/undo_stack.cpp + utility/undo_stack.hpp + utility/VCamConfig.hpp + utility/VolumeMeterTimer.cpp + utility/VolumeMeterTimer.hpp +) diff --git a/frontend/cmake/ui-widgets.cmake b/frontend/cmake/ui-widgets.cmake new file mode 100644 index 000000000..20befdeb0 --- /dev/null +++ b/frontend/cmake/ui-widgets.cmake @@ -0,0 +1,66 @@ +if(NOT TARGET OBS::qt-vertical-scroll-area) + add_subdirectory( + "${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area" + "${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area" + ) +endif() + +target_link_libraries(obs-studio PRIVATE OBS::qt-vertical-scroll-area) + +target_sources( + obs-studio + PRIVATE + widgets/ColorSelect.cpp + widgets/ColorSelect.hpp + widgets/OBSBasic.cpp + widgets/OBSBasic.hpp + widgets/OBSBasic_Browser.cpp + widgets/OBSBasic_Clipboard.cpp + widgets/OBSBasic_ContextToolbar.cpp + widgets/OBSBasic_Docks.cpp + widgets/OBSBasic_Dropfiles.cpp + widgets/OBSBasic_Hotkeys.cpp + widgets/OBSBasic_Icons.cpp + widgets/OBSBasic_MainControls.cpp + widgets/OBSBasic_OutputHandler.cpp + widgets/OBSBasic_Preview.cpp + widgets/OBSBasic_Profiles.cpp + widgets/OBSBasic_Projectors.cpp + widgets/OBSBasic_Recording.cpp + widgets/OBSBasic_ReplayBuffer.cpp + widgets/OBSBasic_SceneCollections.cpp + widgets/OBSBasic_SceneItems.cpp + widgets/OBSBasic_Scenes.cpp + widgets/OBSBasic_Screenshots.cpp + widgets/OBSBasic_Service.cpp + widgets/OBSBasic_StatusBar.cpp + widgets/OBSBasic_Streaming.cpp + widgets/OBSBasic_StudioMode.cpp + widgets/OBSBasic_SysTray.cpp + widgets/OBSBasic_Transitions.cpp + widgets/OBSBasic_Updater.cpp + widgets/OBSBasic_VirtualCam.cpp + widgets/OBSBasic_VolControl.cpp + widgets/OBSBasic_YouTube.cpp + widgets/OBSBasicControls.cpp + widgets/OBSBasicControls.hpp + widgets/OBSBasicPreview.cpp + widgets/OBSBasicPreview.hpp + widgets/OBSBasicStats.cpp + widgets/OBSBasicStats.hpp + widgets/OBSBasicStatusBar.cpp + widgets/OBSBasicStatusBar.hpp + widgets/OBSMainWindow.hpp + widgets/OBSProjector.cpp + widgets/OBSProjector.hpp + widgets/OBSQTDisplay.cpp + widgets/OBSQTDisplay.hpp + widgets/StatusBarWidget.cpp + widgets/StatusBarWidget.hpp + widgets/VolControl.cpp + widgets/VolControl.hpp + widgets/VolumeAccessibleInterface.cpp + widgets/VolumeAccessibleInterface.hpp + widgets/VolumeMeter.cpp + widgets/VolumeMeter.hpp +) diff --git a/frontend/cmake/ui-wizards.cmake b/frontend/cmake/ui-wizards.cmake new file mode 100644 index 000000000..2dfc3f647 --- /dev/null +++ b/frontend/cmake/ui-wizards.cmake @@ -0,0 +1,15 @@ +target_sources( + obs-studio + PRIVATE + wizards/AutoConfig.cpp + wizards/AutoConfig.hpp + wizards/AutoConfigStartPage.cpp + wizards/AutoConfigStartPage.hpp + wizards/AutoConfigStreamPage.cpp + wizards/AutoConfigStreamPage.hpp + wizards/AutoConfigTestPage.cpp + wizards/AutoConfigTestPage.hpp + wizards/AutoConfigVideoPage.cpp + wizards/AutoConfigVideoPage.hpp + wizards/TestMode.hpp +) diff --git a/UI/cmake/windows/obs-studio.ico b/frontend/cmake/windows/obs-studio.ico similarity index 100% rename from UI/cmake/windows/obs-studio.ico rename to frontend/cmake/windows/obs-studio.ico diff --git a/UI/cmake/windows/obs.manifest b/frontend/cmake/windows/obs.manifest similarity index 100% rename from UI/cmake/windows/obs.manifest rename to frontend/cmake/windows/obs.manifest diff --git a/UI/cmake/windows/obs.rc.in b/frontend/cmake/windows/obs.rc.in similarity index 100% rename from UI/cmake/windows/obs.rc.in rename to frontend/cmake/windows/obs.rc.in diff --git a/UI/absolute-slider.cpp b/frontend/components/AbsoluteSlider.cpp similarity index 97% rename from UI/absolute-slider.cpp rename to frontend/components/AbsoluteSlider.cpp index 8aedba825..9f2e76656 100644 --- a/UI/absolute-slider.cpp +++ b/frontend/components/AbsoluteSlider.cpp @@ -1,4 +1,5 @@ -#include "moc_absolute-slider.cpp" +#include "AbsoluteSlider.hpp" +#include "moc_AbsoluteSlider.cpp" AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent) { diff --git a/UI/absolute-slider.hpp b/frontend/components/AbsoluteSlider.hpp similarity index 96% rename from UI/absolute-slider.hpp rename to frontend/components/AbsoluteSlider.hpp index 67e0adb1f..8345037fe 100644 --- a/UI/absolute-slider.hpp +++ b/frontend/components/AbsoluteSlider.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include class AbsoluteSlider : public SliderIgnoreScroll { diff --git a/frontend/components/ApplicationAudioCaptureToolbar.cpp b/frontend/components/ApplicationAudioCaptureToolbar.cpp new file mode 100644 index 000000000..b91a00b11 --- /dev/null +++ b/frontend/components/ApplicationAudioCaptureToolbar.cpp @@ -0,0 +1,22 @@ +#include "ApplicationAudioCaptureToolbar.hpp" +#include "ui_device-select-toolbar.h" +#include "moc_ApplicationAudioCaptureToolbar.cpp" + +ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source) + : ComboSelectToolbar(parent, source) +{ +} + +void ApplicationAudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = obs_get_module("win-wasapi"); + const char *device_str = obs_module_get_locale_text(mod, "Window"); + ui->deviceLabel->setText(device_str); + + prop_name = "window"; + + ComboSelectToolbar::Init(); +} diff --git a/frontend/components/ApplicationAudioCaptureToolbar.hpp b/frontend/components/ApplicationAudioCaptureToolbar.hpp new file mode 100644 index 000000000..bb916e762 --- /dev/null +++ b/frontend/components/ApplicationAudioCaptureToolbar.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComboSelectToolbar.hpp" + +class ApplicationAudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; diff --git a/frontend/components/AudioCaptureToolbar.cpp b/frontend/components/AudioCaptureToolbar.cpp new file mode 100644 index 000000000..f61eb6eae --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.cpp @@ -0,0 +1,33 @@ +#include "AudioCaptureToolbar.hpp" +#include "ui_device-select-toolbar.h" +#include "moc_AudioCaptureToolbar.cpp" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void AudioCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device"); + ui->deviceLabel->setText(device_str); + + prop_name = "device_id"; + + ComboSelectToolbar::Init(); +} diff --git a/frontend/components/AudioCaptureToolbar.hpp b/frontend/components/AudioCaptureToolbar.hpp new file mode 100644 index 000000000..713ea322f --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComboSelectToolbar.hpp" + +class AudioCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + AudioCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; diff --git a/UI/balance-slider.hpp b/frontend/components/BalanceSlider.hpp similarity index 100% rename from UI/balance-slider.hpp rename to frontend/components/BalanceSlider.hpp index 06be1b5f8..780eb6310 100644 --- a/UI/balance-slider.hpp +++ b/frontend/components/BalanceSlider.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include class BalanceSlider : public QSlider { Q_OBJECT diff --git a/frontend/components/BrowserToolbar.cpp b/frontend/components/BrowserToolbar.cpp new file mode 100644 index 000000000..b41360dd5 --- /dev/null +++ b/frontend/components/BrowserToolbar.cpp @@ -0,0 +1,23 @@ +#include "BrowserToolbar.hpp" +#include "ui_browser-source-toolbar.h" +#include "moc_BrowserToolbar.cpp" + +BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_BrowserSourceToolbar) +{ + ui->setupUi(this); +} + +BrowserToolbar::~BrowserToolbar() {} + +void BrowserToolbar::on_refresh_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "refreshnocache"); + obs_property_button_clicked(p, source.Get()); +} diff --git a/frontend/components/BrowserToolbar.hpp b/frontend/components/BrowserToolbar.hpp new file mode 100644 index 000000000..6f37223c3 --- /dev/null +++ b/frontend/components/BrowserToolbar.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_BrowserSourceToolbar; + +class BrowserToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; diff --git a/UI/clickable-label.hpp b/frontend/components/ClickableLabel.hpp similarity index 100% rename from UI/clickable-label.hpp rename to frontend/components/ClickableLabel.hpp diff --git a/frontend/components/ColorSourceToolbar.cpp b/frontend/components/ColorSourceToolbar.cpp new file mode 100644 index 000000000..dc4081acf --- /dev/null +++ b/frontend/components/ColorSourceToolbar.cpp @@ -0,0 +1,83 @@ +#include "ColorSourceToolbar.hpp" +#include "ui_color-source-toolbar.h" + +#include + +#include "moc_ColorSourceToolbar.cpp" + +QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +long long color_to_int(QColor color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ColorSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + unsigned int val = (unsigned int)obs_data_get_int(settings, "color"); + + color = color_from_int(val); + UpdateColor(); +} + +ColorSourceToolbar::~ColorSourceToolbar() {} + +void ColorSourceToolbar::UpdateColor() +{ + QPalette palette = QPalette(color); + ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel); + ui->color->setText(color.name(QColor::HexRgb)); + ui->color->setPalette(palette); + ui->color->setStyleSheet(QString("background-color :%1; color: %2;") + .arg(palette.color(QPalette::Window).name(QColor::HexRgb)) + .arg(palette.color(QPalette::WindowText).name(QColor::HexRgb))); + ui->color->setAutoFillBackground(true); + ui->color->setAlignment(Qt::AlignCenter); +} + +void ColorSourceToolbar::on_choose_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "color"); + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + UpdateColor(); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "color", color_to_int(color)); + obs_source_update(source, settings); + + SetUndoProperties(source); +} diff --git a/frontend/components/ColorSourceToolbar.hpp b/frontend/components/ColorSourceToolbar.hpp new file mode 100644 index 000000000..dc05f0736 --- /dev/null +++ b/frontend/components/ColorSourceToolbar.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_ColorSourceToolbar; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QColor color; + + void UpdateColor(); + +public: + ColorSourceToolbar(QWidget *parent, OBSSource source); + ~ColorSourceToolbar(); + +public slots: + void on_choose_clicked(); +}; diff --git a/frontend/components/ComboSelectToolbar.cpp b/frontend/components/ComboSelectToolbar.cpp new file mode 100644 index 000000000..77a0364d7 --- /dev/null +++ b/frontend/components/ComboSelectToolbar.cpp @@ -0,0 +1,106 @@ +#include "ComboSelectToolbar.hpp" +#include "ui_device-select-toolbar.h" + +#include +#include + +#include "moc_ComboSelectToolbar.cpp" + +ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); +} + +ComboSelectToolbar::~ComboSelectToolbar() {} + +int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false) +{ + size_t count = obs_property_list_item_count(p); + int cur_idx = -1; + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + std::string id; + + if (is_int) { + id = std::to_string(obs_property_list_item_int(p, i)); + } else { + const char *val = obs_property_list_item_string(p, i); + id = val ? val : ""; + } + + if (cur_id == id) + cur_idx = (int)i; + + c->addItem(name, id.c_str()); + } + + return cur_idx; +} + +void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props, + const char *prop_name, bool is_int) +{ + std::string cur_id; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (is_int) { + cur_id = std::to_string(obs_data_get_int(settings, prop_name)); + } else { + cur_id = obs_data_get_string(settings, prop_name); + } + + combo->blockSignals(true); + + obs_property_t *p = obs_properties_get(props, prop_name); + int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int); + + if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) { + if (cur_idx == -1) { + combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice")); + cur_idx = 0; + } + + SetComboItemEnabled(combo, cur_idx, false); + } + + combo->setCurrentIndex(cur_idx); + combo->blockSignals(false); +} + +void ComboSelectToolbar::Init() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int); +} + +void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int) +{ + QString id = combo->itemData(idx).toString(); + + OBSDataAutoRelease settings = obs_data_create(); + if (is_int) { + obs_data_set_int(settings, prop_name, id.toInt()); + } else { + obs_data_set_string(settings, prop_name, QT_TO_UTF8(id)); + } + obs_source_update(source, settings); +} + +void ComboSelectToolbar::on_device_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + SaveOldProperties(source); + UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int); + SetUndoProperties(source); +} diff --git a/frontend/components/ComboSelectToolbar.hpp b/frontend/components/ComboSelectToolbar.hpp new file mode 100644 index 000000000..613709e13 --- /dev/null +++ b/frontend/components/ComboSelectToolbar.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_DeviceSelectToolbar; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr ui; + const char *prop_name; + bool is_int = false; + +public: + ComboSelectToolbar(QWidget *parent, OBSSource source); + ~ComboSelectToolbar(); + virtual void Init(); + +public slots: + void on_device_currentIndexChanged(int idx); +}; diff --git a/frontend/components/DelButton.hpp b/frontend/components/DelButton.hpp new file mode 100644 index 000000000..dab84e8b7 --- /dev/null +++ b/frontend/components/DelButton.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +class DelButton : public QPushButton { +public: + inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {} + + QPersistentModelIndex index; +}; diff --git a/frontend/components/DeviceCaptureToolbar.cpp b/frontend/components/DeviceCaptureToolbar.cpp new file mode 100644 index 000000000..c69179453 --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.cpp @@ -0,0 +1,58 @@ +#include "DeviceCaptureToolbar.hpp" +#include "ui_device-select-toolbar.h" +#include "moc_DeviceCaptureToolbar.cpp" + +DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + ui(new Ui_DeviceSelectToolbar) +{ + ui->setupUi(this); + + delete ui->deviceLabel; + delete ui->device; + ui->deviceLabel = nullptr; + ui->device = nullptr; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + active = obs_data_get_bool(settings, "active"); + + obs_module_t *mod = obs_get_module("win-dshow"); + if (!mod) + return; + + activateText = obs_module_get_locale_text(mod, "Activate"); + deactivateText = obs_module_get_locale_text(mod, "Deactivate"); + + ui->activateButton->setText(active ? deactivateText : activateText); +} + +DeviceCaptureToolbar::~DeviceCaptureToolbar() {} + +void DeviceCaptureToolbar::on_activateButton_clicked() +{ + OBSSource source = OBSGetStrongRef(weakSource); + if (!source) { + return; + } + + OBSDataAutoRelease settings = obs_source_get_settings(source); + bool now_active = obs_data_get_bool(settings, "active"); + + bool desyncedSetting = now_active != active; + + active = !active; + + const char *text = active ? deactivateText : activateText; + ui->activateButton->setText(text); + + if (desyncedSetting) { + return; + } + + calldata_t cd = {}; + calldata_set_bool(&cd, "active", active); + proc_handler_t *ph = obs_source_get_proc_handler(source); + proc_handler_call(ph, "activate", &cd); + calldata_free(&cd); +} diff --git a/frontend/components/DeviceCaptureToolbar.hpp b/frontend/components/DeviceCaptureToolbar.hpp new file mode 100644 index 000000000..17f76f4aa --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +class Ui_DeviceSelectToolbar; + +class DeviceCaptureToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + + std::unique_ptr ui; + const char *activateText; + const char *deactivateText; + bool active; + +public: + DeviceCaptureToolbar(QWidget *parent, OBSSource source); + ~DeviceCaptureToolbar(); + +public slots: + void on_activateButton_clicked(); +}; diff --git a/frontend/components/DisplayCaptureToolbar.cpp b/frontend/components/DisplayCaptureToolbar.cpp new file mode 100644 index 000000000..d32bdea84 --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.cpp @@ -0,0 +1,40 @@ +#include "DisplayCaptureToolbar.hpp" +#include "ui_device-select-toolbar.h" +#include "moc_DisplayCaptureToolbar.cpp" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void DisplayCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen"); + ui->deviceLabel->setText(device_str); + +#ifdef _WIN32 + prop_name = "monitor_id"; +#elif __APPLE__ + prop_name = "display_uuid"; +#else + is_int = true; + prop_name = "screen"; +#endif + + ComboSelectToolbar::Init(); +} diff --git a/frontend/components/DisplayCaptureToolbar.hpp b/frontend/components/DisplayCaptureToolbar.hpp new file mode 100644 index 000000000..0f2f3bb13 --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComboSelectToolbar.hpp" + +class DisplayCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + DisplayCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; diff --git a/frontend/components/EditWidget.hpp b/frontend/components/EditWidget.hpp new file mode 100644 index 000000000..dbc431228 --- /dev/null +++ b/frontend/components/EditWidget.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class QPersistentModelIndex; + +class EditWidget : public QLineEdit { +public: + inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {} + + QPersistentModelIndex index; +}; diff --git a/UI/focus-list.cpp b/frontend/components/FocusList.cpp similarity index 89% rename from UI/focus-list.cpp rename to frontend/components/FocusList.cpp index d5148b3a1..ea65cf207 100644 --- a/UI/focus-list.cpp +++ b/frontend/components/FocusList.cpp @@ -1,6 +1,9 @@ -#include "moc_focus-list.cpp" +#include "FocusList.hpp" + #include +#include "moc_FocusList.cpp" + FocusList::FocusList(QWidget *parent) : QListWidget(parent) {} void FocusList::focusInEvent(QFocusEvent *event) diff --git a/UI/focus-list.hpp b/frontend/components/FocusList.hpp similarity index 92% rename from UI/focus-list.hpp rename to frontend/components/FocusList.hpp index 370dcbcc1..83c44419c 100644 --- a/UI/focus-list.hpp +++ b/frontend/components/FocusList.hpp @@ -2,8 +2,6 @@ #include -class QDragMoveEvent; - class FocusList : public QListWidget { Q_OBJECT diff --git a/frontend/components/GameCaptureToolbar.cpp b/frontend/components/GameCaptureToolbar.cpp new file mode 100644 index 000000000..2a9f8afdf --- /dev/null +++ b/frontend/components/GameCaptureToolbar.cpp @@ -0,0 +1,91 @@ +#include "GameCaptureToolbar.hpp" +#include "ui_game-capture-toolbar.h" + +#include + +#include "moc_GameCaptureToolbar.cpp" + +extern int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false); + +GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_GameCaptureToolbar) +{ + obs_property_t *p; + int cur_idx; + + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("win-capture"); + if (!mod) + return; + + ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode")); + ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string cur_mode = obs_data_get_string(settings, "capture_mode"); + std::string cur_window = obs_data_get_string(settings, "window"); + + ui->mode->blockSignals(true); + p = obs_properties_get(props.get(), "capture_mode"); + cur_idx = FillPropertyCombo(ui->mode, p, cur_mode); + ui->mode->setCurrentIndex(cur_idx); + ui->mode->blockSignals(false); + + ui->window->blockSignals(true); + p = obs_properties_get(props.get(), "window"); + cur_idx = FillPropertyCombo(ui->window, p, cur_window); + ui->window->setCurrentIndex(cur_idx); + ui->window->blockSignals(false); + + if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) { + SetComboItemEnabled(ui->window, cur_idx, false); + } + + UpdateWindowVisibility(); +} + +GameCaptureToolbar::~GameCaptureToolbar() {} + +void GameCaptureToolbar::UpdateWindowVisibility() +{ + QString mode = ui->mode->currentData().toString(); + bool is_window = (mode == "window"); + ui->windowLabel->setVisible(is_window); + ui->window->setVisible(is_window); +} + +void GameCaptureToolbar::on_mode_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->mode->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); + + UpdateWindowVisibility(); +} + +void GameCaptureToolbar::on_window_currentIndexChanged(int idx) +{ + OBSSource source = GetSource(); + if (idx == -1 || !source) { + return; + } + + QString id = ui->window->itemData(idx).toString(); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "window", QT_TO_UTF8(id)); + obs_source_update(source, settings); + SetUndoProperties(source); +} diff --git a/frontend/components/GameCaptureToolbar.hpp b/frontend/components/GameCaptureToolbar.hpp new file mode 100644 index 000000000..0dcf769e3 --- /dev/null +++ b/frontend/components/GameCaptureToolbar.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_GameCaptureToolbar; + +class GameCaptureToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + + void UpdateWindowVisibility(); + +public: + GameCaptureToolbar(QWidget *parent, OBSSource source); + ~GameCaptureToolbar(); + +public slots: + void on_mode_currentIndexChanged(int idx); + void on_window_currentIndexChanged(int idx); +}; diff --git a/UI/horizontal-scroll-area.cpp b/frontend/components/HScrollArea.cpp similarity index 75% rename from UI/horizontal-scroll-area.cpp rename to frontend/components/HScrollArea.cpp index 82ffc0d28..01d804dca 100644 --- a/UI/horizontal-scroll-area.cpp +++ b/frontend/components/HScrollArea.cpp @@ -1,5 +1,8 @@ +#include "HScrollArea.hpp" + #include -#include "moc_horizontal-scroll-area.cpp" + +#include "moc_HScrollArea.cpp" void HScrollArea::resizeEvent(QResizeEvent *event) { diff --git a/UI/horizontal-scroll-area.hpp b/frontend/components/HScrollArea.hpp similarity index 100% rename from UI/horizontal-scroll-area.hpp rename to frontend/components/HScrollArea.hpp diff --git a/frontend/components/ImageSourceToolbar.cpp b/frontend/components/ImageSourceToolbar.cpp new file mode 100644 index 000000000..71458cef2 --- /dev/null +++ b/frontend/components/ImageSourceToolbar.cpp @@ -0,0 +1,53 @@ +#include "ImageSourceToolbar.hpp" +#include "ui_image-source-toolbar.h" + +#include + +#include "moc_ImageSourceToolbar.cpp" + +ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_ImageSourceToolbar) +{ + ui->setupUi(this); + + obs_module_t *mod = obs_get_module("image-source"); + ui->pathLabel->setText(obs_module_get_locale_text(mod, "File")); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + std::string file = obs_data_get_string(settings, "file"); + + ui->path->setText(file.c_str()); +} + +ImageSourceToolbar::~ImageSourceToolbar() {} + +void ImageSourceToolbar::on_browse_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + obs_property_t *p = obs_properties_get(props.get(), "file"); + const char *desc = obs_property_description(p); + const char *filter = obs_property_path_filter(p); + const char *default_path = obs_property_path_default_path(p); + + QString startDir = ui->path->text(); + if (startDir.isEmpty()) + startDir = default_path; + + QString path = OpenFile(this, desc, startDir, filter); + if (path.isEmpty()) { + return; + } + + ui->path->setText(path); + + SaveOldProperties(source); + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "file", QT_TO_UTF8(path)); + obs_source_update(source, settings); + SetUndoProperties(source); +} diff --git a/frontend/components/ImageSourceToolbar.hpp b/frontend/components/ImageSourceToolbar.hpp new file mode 100644 index 000000000..700b90b38 --- /dev/null +++ b/frontend/components/ImageSourceToolbar.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_ImageSourceToolbar; + +class ImageSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; diff --git a/UI/media-controls.cpp b/frontend/components/MediaControls.cpp similarity index 99% rename from UI/media-controls.cpp rename to frontend/components/MediaControls.cpp index df720cc3c..d19951dca 100644 --- a/UI/media-controls.cpp +++ b/frontend/components/MediaControls.cpp @@ -1,12 +1,12 @@ -#include "window-basic-main.hpp" -#include "moc_media-controls.cpp" -#include "obs-app.hpp" -#include -#include -#include - +#include "MediaControls.hpp" #include "ui_media-controls.h" +#include + +#include + +#include "moc_MediaControls.cpp" + void MediaControls::OBSMediaStopped(void *data, calldata_t *) { MediaControls *media = static_cast(data); diff --git a/UI/media-controls.hpp b/frontend/components/MediaControls.hpp similarity index 97% rename from UI/media-controls.hpp rename to frontend/components/MediaControls.hpp index 6a5ec5953..5a246facc 100644 --- a/UI/media-controls.hpp +++ b/frontend/components/MediaControls.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include -#include #include -#include + +#include +#include class Ui_MediaControls; diff --git a/UI/menu-button.cpp b/frontend/components/MenuButton.cpp similarity index 90% rename from UI/menu-button.cpp rename to frontend/components/MenuButton.cpp index 8c22efcfa..b67459563 100644 --- a/UI/menu-button.cpp +++ b/frontend/components/MenuButton.cpp @@ -1,7 +1,9 @@ -#include +#include "MenuButton.hpp" + #include #include -#include "moc_menu-button.cpp" + +#include "moc_MenuButton.cpp" void MenuButton::keyPressEvent(QKeyEvent *event) { diff --git a/UI/menu-button.hpp b/frontend/components/MenuButton.hpp similarity index 100% rename from UI/menu-button.hpp rename to frontend/components/MenuButton.hpp diff --git a/UI/multiview.cpp b/frontend/components/Multiview.cpp similarity index 99% rename from UI/multiview.cpp rename to frontend/components/Multiview.cpp index 9fc8883dc..c32c49377 100644 --- a/UI/multiview.cpp +++ b/frontend/components/Multiview.cpp @@ -1,8 +1,9 @@ -#include "multiview.hpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "display-helpers.hpp" +#include "Multiview.hpp" + +#include +#include + +#include Multiview::Multiview() { diff --git a/UI/multiview.hpp b/frontend/components/Multiview.hpp similarity index 99% rename from UI/multiview.hpp rename to frontend/components/Multiview.hpp index 61abd690b..5c62ee5b3 100644 --- a/UI/multiview.hpp +++ b/frontend/components/Multiview.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include enum class MultiviewLayout : uint8_t { diff --git a/UI/mute-checkbox.hpp b/frontend/components/MuteCheckBox.hpp similarity index 100% rename from UI/mute-checkbox.hpp rename to frontend/components/MuteCheckBox.hpp diff --git a/UI/noncheckable-button.hpp b/frontend/components/NonCheckableButton.hpp similarity index 92% rename from UI/noncheckable-button.hpp rename to frontend/components/NonCheckableButton.hpp index d5ef59a37..5892e996c 100644 --- a/UI/noncheckable-button.hpp +++ b/frontend/components/NonCheckableButton.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include /* Button with its checked property not changed when clicked. * Meant to be used in situations where manually changing the property diff --git a/UI/adv-audio-control.cpp b/frontend/components/OBSAdvAudioCtrl.cpp similarity index 98% rename from UI/adv-audio-control.cpp rename to frontend/components/OBSAdvAudioCtrl.cpp index 80c0a5c1c..01aa2e727 100644 --- a/UI/adv-audio-control.cpp +++ b/frontend/components/OBSAdvAudioCtrl.cpp @@ -1,14 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include +#include "OBSAdvAudioCtrl.hpp" + +#include +#include + #include -#include "obs-app.hpp" -#include "moc_adv-audio-control.cpp" -#include "window-basic-main.hpp" + +#include + +#include "moc_OBSAdvAudioCtrl.cpp" #ifndef NSEC_PER_MSEC #define NSEC_PER_MSEC 1000000 @@ -16,6 +15,7 @@ #define MIN_DB -96.0 #define MAX_DB 26.0 + static inline void setMixer(obs_source_t *source, const int mixerIdx, const bool checked); OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *, obs_source_t *source_) : source(source_) diff --git a/UI/adv-audio-control.hpp b/frontend/components/OBSAdvAudioCtrl.hpp similarity index 96% rename from UI/adv-audio-control.hpp rename to frontend/components/OBSAdvAudioCtrl.hpp index 143b66222..6d12b3044 100644 --- a/UI/adv-audio-control.hpp +++ b/frontend/components/OBSAdvAudioCtrl.hpp @@ -1,17 +1,19 @@ #pragma once #include -#include -#include -#include -#include -#include "balance-slider.hpp" +#include +#include + +class BalanceSlider; +class QCheckBox; +class QComboBox; +class QDoubleSpinBox; class QGridLayout; class QLabel; class QSpinBox; -class QCheckBox; -class QComboBox; +class QStackedWidget; +class QWidget; enum class VolumeType { dB, diff --git a/UI/preview-controls.cpp b/frontend/components/OBSPreviewScalingComboBox.cpp similarity index 89% rename from UI/preview-controls.cpp rename to frontend/components/OBSPreviewScalingComboBox.cpp index 7564e1bdb..c1a08fcb6 100644 --- a/UI/preview-controls.cpp +++ b/frontend/components/OBSPreviewScalingComboBox.cpp @@ -15,23 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "preview-controls.hpp" -#include +#include "OBSPreviewScalingComboBox.hpp" -/* Preview Scale Label */ -void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) -{ - previewScale = scale; - UpdateScaleLabel(); -} +#include -void OBSPreviewScalingLabel::UpdateScaleLabel() -{ - float previewScalePercent = floor(100.0f * previewScale); - setText(QString::number(previewScalePercent) + "%"); -} +#include "moc_OBSPreviewScalingComboBox.cpp" -/* Preview Scaling ComboBox */ void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed) { if (fixedScaling == fixed) diff --git a/UI/preview-controls.hpp b/frontend/components/OBSPreviewScalingComboBox.hpp similarity index 87% rename from UI/preview-controls.hpp rename to frontend/components/OBSPreviewScalingComboBox.hpp index 4078dbea9..73c424369 100644 --- a/UI/preview-controls.hpp +++ b/frontend/components/OBSPreviewScalingComboBox.hpp @@ -17,23 +17,8 @@ #pragma once -#include #include -class OBSPreviewScalingLabel : public QLabel { - Q_OBJECT - -public: - OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {} - -public slots: - void PreviewScaleChanged(float scale); - -private: - float previewScale = 0.0f; - void UpdateScaleLabel(); -}; - class OBSPreviewScalingComboBox : public QComboBox { Q_OBJECT diff --git a/frontend/components/OBSPreviewScalingLabel.cpp b/frontend/components/OBSPreviewScalingLabel.cpp new file mode 100644 index 000000000..580b05836 --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2024 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSPreviewScalingLabel.hpp" +#include "moc_OBSPreviewScalingLabel.cpp" + +void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) +{ + previewScale = scale; + UpdateScaleLabel(); +} + +void OBSPreviewScalingLabel::UpdateScaleLabel() +{ + float previewScalePercent = floor(100.0f * previewScale); + setText(QString::number(previewScalePercent) + "%"); +} diff --git a/frontend/components/OBSPreviewScalingLabel.hpp b/frontend/components/OBSPreviewScalingLabel.hpp new file mode 100644 index 000000000..82dbf39b8 --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.hpp @@ -0,0 +1,34 @@ +/****************************************************************************** + Copyright (C) 2024 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class OBSPreviewScalingLabel : public QLabel { + Q_OBJECT + +public: + OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {} + +public slots: + void PreviewScaleChanged(float scale); + +private: + float previewScale = 0.0f; + void UpdateScaleLabel(); +}; diff --git a/UI/source-label.cpp b/frontend/components/OBSSourceLabel.cpp similarity index 95% rename from UI/source-label.cpp rename to frontend/components/OBSSourceLabel.cpp index 1abc1fa9a..efe81d958 100644 --- a/UI/source-label.cpp +++ b/frontend/components/OBSSourceLabel.cpp @@ -15,7 +15,8 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_source-label.cpp" +#include "OBSSourceLabel.hpp" +#include "moc_OBSSourceLabel.cpp" void OBSSourceLabel::SourceRenamed(void *data, calldata_t *params) { diff --git a/UI/source-label.hpp b/frontend/components/OBSSourceLabel.hpp similarity index 99% rename from UI/source-label.hpp rename to frontend/components/OBSSourceLabel.hpp index a73fa3c6d..c0e9f434f 100644 --- a/UI/source-label.hpp +++ b/frontend/components/OBSSourceLabel.hpp @@ -17,9 +17,10 @@ #pragma once -#include #include +#include + class OBSSourceLabel : public QLabel { Q_OBJECT; diff --git a/UI/scene-tree.cpp b/frontend/components/SceneTree.cpp similarity index 98% rename from UI/scene-tree.cpp rename to frontend/components/SceneTree.cpp index fc64b11f7..f96dce68c 100644 --- a/UI/scene-tree.cpp +++ b/frontend/components/SceneTree.cpp @@ -1,11 +1,9 @@ -#include "moc_scene-tree.cpp" +#include "SceneTree.hpp" -#include #include -#include -#include #include -#include + +#include "moc_SceneTree.cpp" SceneTree::SceneTree(QWidget *parent_) : QListWidget(parent_) { diff --git a/UI/scene-tree.hpp b/frontend/components/SceneTree.hpp similarity index 95% rename from UI/scene-tree.hpp rename to frontend/components/SceneTree.hpp index 5fc765a92..b4fb7dc67 100644 --- a/UI/scene-tree.hpp +++ b/frontend/components/SceneTree.hpp @@ -1,8 +1,9 @@ #pragma once #include -#include -#include +#include +#include +#include class SceneTree : public QListWidget { Q_OBJECT diff --git a/frontend/components/SilentUpdateCheckBox.hpp b/frontend/components/SilentUpdateCheckBox.hpp new file mode 100644 index 000000000..ebbf1c916 --- /dev/null +++ b/frontend/components/SilentUpdateCheckBox.hpp @@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class SilentUpdateCheckBox : public QCheckBox { + Q_OBJECT + +public slots: + void setCheckedSilently(bool checked) + { + bool blocked = blockSignals(true); + setChecked(checked); + blockSignals(blocked); + } +}; diff --git a/frontend/components/SilentUpdateSpinBox.hpp b/frontend/components/SilentUpdateSpinBox.hpp new file mode 100644 index 000000000..eadbc9cc0 --- /dev/null +++ b/frontend/components/SilentUpdateSpinBox.hpp @@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class SilentUpdateSpinBox : public QSpinBox { + Q_OBJECT + +public slots: + void setValueSilently(int val) + { + bool blocked = blockSignals(true); + setValue(val); + blockSignals(blocked); + } +}; diff --git a/frontend/components/SourceToolbar.cpp b/frontend/components/SourceToolbar.cpp new file mode 100644 index 000000000..da8675aa6 --- /dev/null +++ b/frontend/components/SourceToolbar.cpp @@ -0,0 +1,60 @@ +#include "SourceToolbar.hpp" + +#include + +#include "moc_SourceToolbar.cpp" + +SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source) + : QWidget(parent), + weakSource(OBSGetWeakRef(source)), + props(obs_source_properties(source), obs_properties_destroy) +{ +} + +void SourceToolbar::SaveOldProperties(obs_source_t *source) +{ + oldData = obs_data_create(); + + OBSDataAutoRelease oldSettings = obs_source_get_settings(source); + obs_data_apply(oldData, oldSettings); + obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source)); +} + +void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable) +{ + if (!oldData) { + blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__); + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSSource currentSceneSource = main->GetCurrentSceneSource(); + if (!currentSceneSource) + return; + std::string scene_uuid = obs_source_get_uuid(currentSceneSource); + auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) { + OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid")); + obs_source_reset_settings(source, settings); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + main->UpdateContextBar(); + }; + + OBSDataAutoRelease new_settings = obs_data_create(); + OBSDataAutoRelease curr_settings = obs_source_get_settings(source); + obs_data_apply(new_settings, curr_settings); + obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source)); + + std::string undo_data(obs_data_get_json(oldData)); + std::string redo_data(obs_data_get_json(new_settings)); + + if (undo_data.compare(redo_data) != 0) + main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo, + undo_data, redo_data, repeatable); + + oldData = nullptr; +} diff --git a/frontend/components/SourceToolbar.hpp b/frontend/components/SourceToolbar.hpp new file mode 100644 index 000000000..8e3f9b5f7 --- /dev/null +++ b/frontend/components/SourceToolbar.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +class SourceToolbar : public QWidget { + Q_OBJECT + + OBSWeakSource weakSource; + +protected: + using properties_delete_t = decltype(&obs_properties_destroy); + using properties_t = std::unique_ptr; + + properties_t props; + OBSDataAutoRelease oldData; + + void SaveOldProperties(obs_source_t *source); + void SetUndoProperties(obs_source_t *source, bool repeatable = false); + +public: + SourceToolbar(QWidget *parent, OBSSource source); + + OBSSource GetSource() { return OBSGetStrongRef(weakSource); } + +public slots: + virtual void Update() {} +}; diff --git a/frontend/components/SourceTree.cpp b/frontend/components/SourceTree.cpp new file mode 100644 index 000000000..0325b59d8 --- /dev/null +++ b/frontend/components/SourceTree.cpp @@ -0,0 +1,638 @@ +#include "SourceTree.hpp" +#include "SourceTreeDelegate.hpp" + +#include + +#include + +#include "moc_SourceTree.cpp" + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} diff --git a/frontend/components/SourceTree.hpp b/frontend/components/SourceTree.hpp new file mode 100644 index 000000000..60cacaa3b --- /dev/null +++ b/frontend/components/SourceTree.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include "SourceTreeItem.hpp" +#include "SourceTreeModel.hpp" + +#include +#include +#include + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + bool textPrepared = false; + QStaticText textNoSources; + QSvgRenderer iconNoSources; + + OBSData undoSceneData; + + bool iconsVisible = true; + + void UpdateNoSourcesMessage(); + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const { return reinterpret_cast(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const { return ignoreReorder; } + inline void Clear() { GetStm()->Clear(); } + + inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); } + inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); } + inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); } + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + + void UpdateIcons(); + void SetIconsVisible(bool visible); + +public slots: + inline void ReorderItems() { GetStm()->ReorderItems(); } + inline void RefreshItems() { GetStm()->SceneChanged(); } + void Remove(OBSSceneItem item, OBSScene scene); + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + bool Edit(int idx); + void NewGroupEdit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + virtual void paintEvent(QPaintEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; diff --git a/frontend/components/SourceTreeDelegate.cpp b/frontend/components/SourceTreeDelegate.cpp new file mode 100644 index 000000000..b849e99ec --- /dev/null +++ b/frontend/components/SourceTreeDelegate.cpp @@ -0,0 +1,16 @@ +#include "SourceTree.hpp" +#include "SourceTreeDelegate.hpp" +#include "moc_SourceTreeDelegate.cpp" + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeDelegate.hpp b/frontend/components/SourceTreeDelegate.hpp new file mode 100644 index 000000000..c5489395f --- /dev/null +++ b/frontend/components/SourceTreeDelegate.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include +#include +#include + +class SourceTreeDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SourceTreeDelegate(QObject *parent); + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; diff --git a/frontend/components/SourceTreeItem.cpp b/frontend/components/SourceTreeItem.cpp new file mode 100644 index 000000000..7b6b7a7f4 --- /dev/null +++ b/frontend/components/SourceTreeItem.cpp @@ -0,0 +1,544 @@ +#include "SourceTreeItem.hpp" + +#include +#include + +#include + +#include +#include +#include + +#include "moc_SourceTreeItem.cpp" + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} diff --git a/frontend/components/SourceTreeItem.hpp b/frontend/components/SourceTreeItem.hpp new file mode 100644 index 000000000..38f981882 --- /dev/null +++ b/frontend/components/SourceTreeItem.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include + +class QSpacerItem; +class QCheckBox; +class QLabel; +class QHBoxLayout; +class OBSSourceLabel; +class QLineEdit; + +class SourceTree; + +class SourceTreeItem : public QFrame { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + bool IsEditing(); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + QLabel *iconLabel = nullptr; + QCheckBox *vis = nullptr; + QCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + OBSSourceLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + std::string newName; + + SourceTree *tree; + OBSSceneItem sceneitem; + std::vector sigs; + + virtual void paintEvent(QPaintEvent *event) override; + + void ExitEditModeInternal(bool save); + +private slots: + void Clear(); + + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void LockedChanged(bool locked); + + void ExpandClicked(bool checked); + + void Select(); + void Deselect(); +}; diff --git a/frontend/components/SourceTreeModel.cpp b/frontend/components/SourceTreeModel.cpp new file mode 100644 index 000000000..61e13d52b --- /dev/null +++ b/frontend/components/SourceTreeModel.cpp @@ -0,0 +1,434 @@ +#include "SourceTreeModel.hpp" + +#include + +#include + +#include "moc_SourceTreeModel.cpp" + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} diff --git a/frontend/components/SourceTreeModel.hpp b/frontend/components/SourceTreeModel.hpp new file mode 100644 index 000000000..a94835346 --- /dev/null +++ b/frontend/components/SourceTreeModel.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include + +class SourceTree; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; +}; diff --git a/frontend/components/TextSourceToolbar.cpp b/frontend/components/TextSourceToolbar.cpp new file mode 100644 index 000000000..be2d46525 --- /dev/null +++ b/frontend/components/TextSourceToolbar.cpp @@ -0,0 +1,151 @@ +#include "TextSourceToolbar.hpp" +#include "ui_text-source-toolbar.h" + +#include + +#include + +#include +#include + +#include "moc_TextSourceToolbar.cpp" + +extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false); +extern QColor color_from_int(long long val); +extern long long color_to_int(QColor color); + +TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source) + : SourceToolbar(parent, source), + ui(new Ui_TextSourceToolbar) +{ + ui->setupUi(this); + + OBSDataAutoRelease settings = obs_source_get_settings(source); + + const char *id = obs_source_get_unversioned_id(source); + bool ft2 = strcmp(id, "text_ft2_source") == 0; + bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file"); + + OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); + MakeQFont(font_obj, font); + + // Use "color1" if it's a freetype source and "color" elsewise + unsigned int val = (unsigned int)obs_data_get_int( + settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color"); + + color = color_from_int(val); + + const char *text = obs_data_get_string(settings, "text"); + + bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr)); + ui->emptySpace->setVisible(!single_line); + ui->text->setVisible(single_line); + if (single_line) + ui->text->setText(text); +} + +TextSourceToolbar::~TextSourceToolbar() {} + +void TextSourceToolbar::on_selectFont_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + QFontDialog::FontDialogOptions options; + uint32_t flags; + bool success; + +#ifndef _WIN32 + options = QFontDialog::DontUseNativeDialog; +#endif + + font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"), + options); + if (!success) { + return; + } + + OBSDataAutoRelease font_obj = obs_data_create(); + + obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family())); + obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName())); + obs_data_set_int(font_obj, "size", font.pointSize()); + flags = font.bold() ? OBS_FONT_BOLD : 0; + flags |= font.italic() ? OBS_FONT_ITALIC : 0; + flags |= font.underline() ? OBS_FONT_UNDERLINE : 0; + flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0; + obs_data_set_int(font_obj, "flags", flags); + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_obj(settings, "font", font_obj); + + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_selectColor_clicked() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + + bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0; + + obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color"); + + const char *desc = obs_property_description(p); + + QColorDialog::ColorDialogOptions options; + + options |= QColorDialog::ShowAlphaChannel; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColor newColor = QColorDialog::getColor(color, this, desc, options); + if (!newColor.isValid()) { + return; + } + + color = newColor; + + SaveOldProperties(source); + + OBSDataAutoRelease settings = obs_data_create(); + if (freetype) { + obs_data_set_int(settings, "color1", color_to_int(color)); + obs_data_set_int(settings, "color2", color_to_int(color)); + } else { + obs_data_set_int(settings, "color", color_to_int(color)); + } + obs_source_update(source, settings); + + SetUndoProperties(source); +} + +void TextSourceToolbar::on_text_textChanged() +{ + OBSSource source = GetSource(); + if (!source) { + return; + } + std::string newText = QT_TO_UTF8(ui->text->text()); + OBSDataAutoRelease settings = obs_source_get_settings(source); + if (newText == obs_data_get_string(settings, "text")) { + return; + } + SaveOldProperties(source); + + obs_data_set_string(settings, "text", newText.c_str()); + obs_source_update(source, nullptr); + + SetUndoProperties(source, true); +} diff --git a/frontend/components/TextSourceToolbar.hpp b/frontend/components/TextSourceToolbar.hpp new file mode 100644 index 000000000..38833803a --- /dev/null +++ b/frontend/components/TextSourceToolbar.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "SourceToolbar.hpp" + +class Ui_TextSourceToolbar; + +class TextSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr ui; + QFont font; + QColor color; + +public: + TextSourceToolbar(QWidget *parent, OBSSource source); + ~TextSourceToolbar(); + +public slots: + void on_selectFont_clicked(); + void on_selectColor_clicked(); + void on_text_textChanged(); +}; diff --git a/UI/ui-validation.cpp b/frontend/components/UIValidation.cpp similarity index 96% rename from UI/ui-validation.cpp rename to frontend/components/UIValidation.cpp index 482951604..3597949d0 100644 --- a/UI/ui-validation.cpp +++ b/frontend/components/UIValidation.cpp @@ -1,12 +1,11 @@ -#include "moc_ui-validation.cpp" +#include "UIValidation.hpp" + +#include -#include -#include #include #include -#include -#include +#include "moc_UIValidation.cpp" static int CountVideoSources() { diff --git a/UI/ui-validation.hpp b/frontend/components/UIValidation.hpp similarity index 97% rename from UI/ui-validation.hpp rename to frontend/components/UIValidation.hpp index 505db851d..dabccabd9 100644 --- a/UI/ui-validation.hpp +++ b/frontend/components/UIValidation.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include +#include + enum class StreamSettingsAction { OpenSettings, Cancel, diff --git a/UI/url-push-button.cpp b/frontend/components/UrlPushButton.cpp similarity index 83% rename from UI/url-push-button.cpp rename to frontend/components/UrlPushButton.cpp index 32c39620d..1e7b68d04 100644 --- a/UI/url-push-button.cpp +++ b/frontend/components/UrlPushButton.cpp @@ -1,9 +1,9 @@ -#include "moc_url-push-button.cpp" +#include "UrlPushButton.hpp" -#include -#include #include +#include "moc_UrlPushButton.cpp" + void UrlPushButton::setTargetUrl(QUrl url) { setToolTip(url.toString()); diff --git a/UI/url-push-button.hpp b/frontend/components/UrlPushButton.hpp similarity index 90% rename from UI/url-push-button.hpp rename to frontend/components/UrlPushButton.hpp index 2668ade27..bcf331738 100644 --- a/UI/url-push-button.hpp +++ b/frontend/components/UrlPushButton.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include class UrlPushButton : public QPushButton { Q_OBJECT diff --git a/frontend/components/VisibilityItemDelegate.cpp b/frontend/components/VisibilityItemDelegate.cpp new file mode 100644 index 000000000..d9e76ee5c --- /dev/null +++ b/frontend/components/VisibilityItemDelegate.cpp @@ -0,0 +1,66 @@ +#include "VisibilityItemWidget.hpp" +#include "VisibilityItemDelegate.hpp" + +#include + +#include "moc_VisibilityItemDelegate.cpp" + +VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyledItemDelegate::paint(painter, option, index); + + QObject *parentObj = parent(); + QListWidget *list = qobject_cast(parentObj); + if (!list) + return; + + QListWidgetItem *item = list->item(index.row()); + VisibilityItemWidget *widget = qobject_cast(list->itemWidget(item)); + if (!widget) + return; + + bool selected = option.state.testFlag(QStyle::State_Selected); + bool active = option.state.testFlag(QStyle::State_Active); + + QPalette palette = list->palette(); +#if defined(_WIN32) || defined(__APPLE__) + QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Inactive; +#else + QPalette::ColorGroup group = QPalette::Active; +#endif + +#ifdef _WIN32 + QPalette::ColorRole highlightRole = QPalette::WindowText; +#else + QPalette::ColorRole highlightRole = QPalette::HighlightedText; +#endif + + QPalette::ColorRole role; + + if (selected && active) + role = highlightRole; + else + role = QPalette::WindowText; + + widget->SetColor(palette.color(group, role), active, selected); +} + +bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event) +{ + QWidget *editor = qobject_cast(object); + if (!editor) + return false; + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) { + return false; + } + } + + return QStyledItemDelegate::eventFilter(object, event); +} diff --git a/frontend/components/VisibilityItemDelegate.hpp b/frontend/components/VisibilityItemDelegate.hpp new file mode 100644 index 000000000..d81b51dae --- /dev/null +++ b/frontend/components/VisibilityItemDelegate.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +class QObject; + +class VisibilityItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + VisibilityItemDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; +}; diff --git a/frontend/components/VisibilityItemWidget.cpp b/frontend/components/VisibilityItemWidget.cpp new file mode 100644 index 000000000..1283f6fbe --- /dev/null +++ b/frontend/components/VisibilityItemWidget.cpp @@ -0,0 +1,69 @@ +#include "VisibilityItemWidget.hpp" + +#include + +#include +#include + +#include "moc_VisibilityItemWidget.cpp" + +VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_) + : source(source_), + enabledSignal(obs_source_get_signal_handler(source), "enable", OBSSourceEnabled, this) +{ + bool enabled = obs_source_enabled(source); + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + vis->setChecked(enabled); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + QHBoxLayout *itemLayout = new QHBoxLayout(); + itemLayout->addWidget(vis); + itemLayout->addWidget(label); + itemLayout->setContentsMargins(0, 0, 0, 0); + + setLayout(itemLayout); + + connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); }); +} + +void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data) +{ + VisibilityItemWidget *window = reinterpret_cast(param); + bool enabled = calldata_bool(data, "enabled"); + + QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled)); +} + +void VisibilityItemWidget::SourceEnabled(bool enabled) +{ + if (vis->isChecked() != enabled) + vis->setChecked(enabled); +} + +void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_) +{ + /* Do not update unless the state has actually changed */ + if (active_ == active && selected_ == selected) + return; + + QPalette pal = vis->palette(); + pal.setColor(QPalette::WindowText, color); + vis->setPalette(pal); + + label->setStyleSheet(QString("color: %1;").arg(color.name())); + + active = active_; + selected = selected_; +} + +void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source) +{ + VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source); + + list->setItemWidget(item, baseWidget); +} diff --git a/UI/visibility-item-widget.hpp b/frontend/components/VisibilityItemWidget.hpp similarity index 60% rename from UI/visibility-item-widget.hpp rename to frontend/components/VisibilityItemWidget.hpp index ec6b35e37..3c4322c87 100644 --- a/UI/visibility-item-widget.hpp +++ b/frontend/components/VisibilityItemWidget.hpp @@ -1,15 +1,12 @@ #pragma once -#include -#include #include -class QLabel; -class QLineEdit; -class QListWidget; -class QListWidgetItem; -class QCheckBox; +#include +#include + class OBSSourceLabel; +class QCheckBox; class VisibilityItemWidget : public QWidget { Q_OBJECT @@ -35,16 +32,4 @@ public: void SetColor(const QColor &color, bool active, bool selected); }; -class VisibilityItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - VisibilityItemDelegate(QObject *parent = nullptr); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -protected: - bool eventFilter(QObject *object, QEvent *event) override; -}; - void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source); diff --git a/frontend/components/VolumeSlider.cpp b/frontend/components/VolumeSlider.cpp new file mode 100644 index 000000000..4ef9b5d22 --- /dev/null +++ b/frontend/components/VolumeSlider.cpp @@ -0,0 +1,80 @@ +#include "VolumeSlider.hpp" + +#include + +#include "moc_VolumeSlider.cpp" + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} diff --git a/frontend/components/VolumeSlider.hpp b/frontend/components/VolumeSlider.hpp new file mode 100644 index 000000000..10b794889 --- /dev/null +++ b/frontend/components/VolumeSlider.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; diff --git a/frontend/components/WindowCaptureToolbar.cpp b/frontend/components/WindowCaptureToolbar.cpp new file mode 100644 index 000000000..3f1e4ebba --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.cpp @@ -0,0 +1,41 @@ +#include "WindowCaptureToolbar.hpp" +#include "ui_device-select-toolbar.h" +#include "moc_WindowCaptureToolbar.cpp" + +#ifdef _WIN32 +#define get_os_module(win, mac, linux) obs_get_module(win) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win) +#elif __APPLE__ +#define get_os_module(win, mac, linux) obs_get_module(mac) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac) +#else +#define get_os_module(win, mac, linux) obs_get_module(linux) +#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux) +#endif + +WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {} + +void WindowCaptureToolbar::Init() +{ + delete ui->activateButton; + ui->activateButton = nullptr; + + obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture"); + if (!mod) + return; + + const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window"); + ui->deviceLabel->setText(device_str); + +#if !defined(_WIN32) && !defined(__APPLE__) //linux + prop_name = "capture_window"; +#else + prop_name = "window"; +#endif + +#ifdef __APPLE__ + is_int = true; +#endif + + ComboSelectToolbar::Init(); +} diff --git a/frontend/components/WindowCaptureToolbar.hpp b/frontend/components/WindowCaptureToolbar.hpp new file mode 100644 index 000000000..760a95402 --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComboSelectToolbar.hpp" + +class WindowCaptureToolbar : public ComboSelectToolbar { + Q_OBJECT + +public: + WindowCaptureToolbar(QWidget *parent, OBSSource source); + void Init() override; +}; diff --git a/UI/data/OBSPublicRSAKey.pem b/frontend/data/OBSPublicRSAKey.pem similarity index 100% rename from UI/data/OBSPublicRSAKey.pem rename to frontend/data/OBSPublicRSAKey.pem diff --git a/UI/data/images/overflow.png b/frontend/data/images/overflow.png similarity index 100% rename from UI/data/images/overflow.png rename to frontend/data/images/overflow.png diff --git a/UI/data/license/gplv2.txt b/frontend/data/license/gplv2.txt similarity index 100% rename from UI/data/license/gplv2.txt rename to frontend/data/license/gplv2.txt diff --git a/UI/data/locale.ini b/frontend/data/locale.ini similarity index 100% rename from UI/data/locale.ini rename to frontend/data/locale.ini diff --git a/UI/data/locale/af-ZA.ini b/frontend/data/locale/af-ZA.ini similarity index 100% rename from UI/data/locale/af-ZA.ini rename to frontend/data/locale/af-ZA.ini diff --git a/UI/data/locale/an-ES.ini b/frontend/data/locale/an-ES.ini similarity index 100% rename from UI/data/locale/an-ES.ini rename to frontend/data/locale/an-ES.ini diff --git a/UI/data/locale/ar-SA.ini b/frontend/data/locale/ar-SA.ini similarity index 100% rename from UI/data/locale/ar-SA.ini rename to frontend/data/locale/ar-SA.ini diff --git a/UI/data/locale/az-AZ.ini b/frontend/data/locale/az-AZ.ini similarity index 100% rename from UI/data/locale/az-AZ.ini rename to frontend/data/locale/az-AZ.ini diff --git a/UI/data/locale/ba-RU.ini b/frontend/data/locale/ba-RU.ini similarity index 100% rename from UI/data/locale/ba-RU.ini rename to frontend/data/locale/ba-RU.ini diff --git a/UI/data/locale/be-BY.ini b/frontend/data/locale/be-BY.ini similarity index 100% rename from UI/data/locale/be-BY.ini rename to frontend/data/locale/be-BY.ini diff --git a/UI/data/locale/bem-ZM.ini b/frontend/data/locale/bem-ZM.ini similarity index 100% rename from UI/data/locale/bem-ZM.ini rename to frontend/data/locale/bem-ZM.ini diff --git a/UI/data/locale/bg-BG.ini b/frontend/data/locale/bg-BG.ini similarity index 100% rename from UI/data/locale/bg-BG.ini rename to frontend/data/locale/bg-BG.ini diff --git a/UI/data/locale/bn-BD.ini b/frontend/data/locale/bn-BD.ini similarity index 100% rename from UI/data/locale/bn-BD.ini rename to frontend/data/locale/bn-BD.ini diff --git a/UI/data/locale/ca-ES.ini b/frontend/data/locale/ca-ES.ini similarity index 100% rename from UI/data/locale/ca-ES.ini rename to frontend/data/locale/ca-ES.ini diff --git a/UI/data/locale/cs-CZ.ini b/frontend/data/locale/cs-CZ.ini similarity index 100% rename from UI/data/locale/cs-CZ.ini rename to frontend/data/locale/cs-CZ.ini diff --git a/UI/data/locale/da-DK.ini b/frontend/data/locale/da-DK.ini similarity index 100% rename from UI/data/locale/da-DK.ini rename to frontend/data/locale/da-DK.ini diff --git a/UI/data/locale/de-DE.ini b/frontend/data/locale/de-DE.ini similarity index 100% rename from UI/data/locale/de-DE.ini rename to frontend/data/locale/de-DE.ini diff --git a/UI/data/locale/el-GR.ini b/frontend/data/locale/el-GR.ini similarity index 100% rename from UI/data/locale/el-GR.ini rename to frontend/data/locale/el-GR.ini diff --git a/UI/data/locale/en-GB.ini b/frontend/data/locale/en-GB.ini similarity index 100% rename from UI/data/locale/en-GB.ini rename to frontend/data/locale/en-GB.ini diff --git a/UI/data/locale/en-US.ini b/frontend/data/locale/en-US.ini similarity index 100% rename from UI/data/locale/en-US.ini rename to frontend/data/locale/en-US.ini diff --git a/UI/data/locale/eo-UY.ini b/frontend/data/locale/eo-UY.ini similarity index 100% rename from UI/data/locale/eo-UY.ini rename to frontend/data/locale/eo-UY.ini diff --git a/UI/data/locale/es-ES.ini b/frontend/data/locale/es-ES.ini similarity index 100% rename from UI/data/locale/es-ES.ini rename to frontend/data/locale/es-ES.ini diff --git a/UI/data/locale/et-EE.ini b/frontend/data/locale/et-EE.ini similarity index 100% rename from UI/data/locale/et-EE.ini rename to frontend/data/locale/et-EE.ini diff --git a/UI/data/locale/eu-ES.ini b/frontend/data/locale/eu-ES.ini similarity index 100% rename from UI/data/locale/eu-ES.ini rename to frontend/data/locale/eu-ES.ini diff --git a/UI/data/locale/fa-IR.ini b/frontend/data/locale/fa-IR.ini similarity index 100% rename from UI/data/locale/fa-IR.ini rename to frontend/data/locale/fa-IR.ini diff --git a/UI/data/locale/fi-FI.ini b/frontend/data/locale/fi-FI.ini similarity index 100% rename from UI/data/locale/fi-FI.ini rename to frontend/data/locale/fi-FI.ini diff --git a/UI/data/locale/fil-PH.ini b/frontend/data/locale/fil-PH.ini similarity index 100% rename from UI/data/locale/fil-PH.ini rename to frontend/data/locale/fil-PH.ini diff --git a/UI/data/locale/fr-FR.ini b/frontend/data/locale/fr-FR.ini similarity index 100% rename from UI/data/locale/fr-FR.ini rename to frontend/data/locale/fr-FR.ini diff --git a/UI/data/locale/gd-GB.ini b/frontend/data/locale/gd-GB.ini similarity index 100% rename from UI/data/locale/gd-GB.ini rename to frontend/data/locale/gd-GB.ini diff --git a/UI/data/locale/gl-ES.ini b/frontend/data/locale/gl-ES.ini similarity index 100% rename from UI/data/locale/gl-ES.ini rename to frontend/data/locale/gl-ES.ini diff --git a/UI/data/locale/he-IL.ini b/frontend/data/locale/he-IL.ini similarity index 100% rename from UI/data/locale/he-IL.ini rename to frontend/data/locale/he-IL.ini diff --git a/UI/data/locale/hi-IN.ini b/frontend/data/locale/hi-IN.ini similarity index 100% rename from UI/data/locale/hi-IN.ini rename to frontend/data/locale/hi-IN.ini diff --git a/UI/data/locale/hr-HR.ini b/frontend/data/locale/hr-HR.ini similarity index 100% rename from UI/data/locale/hr-HR.ini rename to frontend/data/locale/hr-HR.ini diff --git a/UI/data/locale/hu-HU.ini b/frontend/data/locale/hu-HU.ini similarity index 100% rename from UI/data/locale/hu-HU.ini rename to frontend/data/locale/hu-HU.ini diff --git a/UI/data/locale/hy-AM.ini b/frontend/data/locale/hy-AM.ini similarity index 100% rename from UI/data/locale/hy-AM.ini rename to frontend/data/locale/hy-AM.ini diff --git a/UI/data/locale/id-ID.ini b/frontend/data/locale/id-ID.ini similarity index 100% rename from UI/data/locale/id-ID.ini rename to frontend/data/locale/id-ID.ini diff --git a/UI/data/locale/is-IS.ini b/frontend/data/locale/is-IS.ini similarity index 100% rename from UI/data/locale/is-IS.ini rename to frontend/data/locale/is-IS.ini diff --git a/UI/data/locale/it-IT.ini b/frontend/data/locale/it-IT.ini similarity index 100% rename from UI/data/locale/it-IT.ini rename to frontend/data/locale/it-IT.ini diff --git a/UI/data/locale/ja-JP.ini b/frontend/data/locale/ja-JP.ini similarity index 100% rename from UI/data/locale/ja-JP.ini rename to frontend/data/locale/ja-JP.ini diff --git a/UI/data/locale/ka-GE.ini b/frontend/data/locale/ka-GE.ini similarity index 100% rename from UI/data/locale/ka-GE.ini rename to frontend/data/locale/ka-GE.ini diff --git a/UI/data/locale/kaa.ini b/frontend/data/locale/kaa.ini similarity index 100% rename from UI/data/locale/kaa.ini rename to frontend/data/locale/kaa.ini diff --git a/UI/data/locale/kab-KAB.ini b/frontend/data/locale/kab-KAB.ini similarity index 100% rename from UI/data/locale/kab-KAB.ini rename to frontend/data/locale/kab-KAB.ini diff --git a/UI/data/locale/kmr-TR.ini b/frontend/data/locale/kmr-TR.ini similarity index 100% rename from UI/data/locale/kmr-TR.ini rename to frontend/data/locale/kmr-TR.ini diff --git a/UI/data/locale/ko-KR.ini b/frontend/data/locale/ko-KR.ini similarity index 100% rename from UI/data/locale/ko-KR.ini rename to frontend/data/locale/ko-KR.ini diff --git a/UI/data/locale/lo-LA.ini b/frontend/data/locale/lo-LA.ini similarity index 100% rename from UI/data/locale/lo-LA.ini rename to frontend/data/locale/lo-LA.ini diff --git a/UI/data/locale/lt-LT.ini b/frontend/data/locale/lt-LT.ini similarity index 100% rename from UI/data/locale/lt-LT.ini rename to frontend/data/locale/lt-LT.ini diff --git a/UI/data/locale/lv-LV.ini b/frontend/data/locale/lv-LV.ini similarity index 100% rename from UI/data/locale/lv-LV.ini rename to frontend/data/locale/lv-LV.ini diff --git a/UI/data/locale/mn-MN.ini b/frontend/data/locale/mn-MN.ini similarity index 100% rename from UI/data/locale/mn-MN.ini rename to frontend/data/locale/mn-MN.ini diff --git a/UI/data/locale/ms-MY.ini b/frontend/data/locale/ms-MY.ini similarity index 100% rename from UI/data/locale/ms-MY.ini rename to frontend/data/locale/ms-MY.ini diff --git a/UI/data/locale/nb-NO.ini b/frontend/data/locale/nb-NO.ini similarity index 100% rename from UI/data/locale/nb-NO.ini rename to frontend/data/locale/nb-NO.ini diff --git a/UI/data/locale/nl-NL.ini b/frontend/data/locale/nl-NL.ini similarity index 100% rename from UI/data/locale/nl-NL.ini rename to frontend/data/locale/nl-NL.ini diff --git a/UI/data/locale/nn-NO.ini b/frontend/data/locale/nn-NO.ini similarity index 100% rename from UI/data/locale/nn-NO.ini rename to frontend/data/locale/nn-NO.ini diff --git a/UI/data/locale/oc-FR.ini b/frontend/data/locale/oc-FR.ini similarity index 100% rename from UI/data/locale/oc-FR.ini rename to frontend/data/locale/oc-FR.ini diff --git a/UI/data/locale/pa-IN.ini b/frontend/data/locale/pa-IN.ini similarity index 100% rename from UI/data/locale/pa-IN.ini rename to frontend/data/locale/pa-IN.ini diff --git a/UI/data/locale/pl-PL.ini b/frontend/data/locale/pl-PL.ini similarity index 100% rename from UI/data/locale/pl-PL.ini rename to frontend/data/locale/pl-PL.ini diff --git a/UI/data/locale/pt-BR.ini b/frontend/data/locale/pt-BR.ini similarity index 100% rename from UI/data/locale/pt-BR.ini rename to frontend/data/locale/pt-BR.ini diff --git a/UI/data/locale/pt-PT.ini b/frontend/data/locale/pt-PT.ini similarity index 100% rename from UI/data/locale/pt-PT.ini rename to frontend/data/locale/pt-PT.ini diff --git a/UI/data/locale/ro-RO.ini b/frontend/data/locale/ro-RO.ini similarity index 100% rename from UI/data/locale/ro-RO.ini rename to frontend/data/locale/ro-RO.ini diff --git a/UI/data/locale/ru-RU.ini b/frontend/data/locale/ru-RU.ini similarity index 100% rename from UI/data/locale/ru-RU.ini rename to frontend/data/locale/ru-RU.ini diff --git a/UI/data/locale/si-LK.ini b/frontend/data/locale/si-LK.ini similarity index 100% rename from UI/data/locale/si-LK.ini rename to frontend/data/locale/si-LK.ini diff --git a/UI/data/locale/sk-SK.ini b/frontend/data/locale/sk-SK.ini similarity index 100% rename from UI/data/locale/sk-SK.ini rename to frontend/data/locale/sk-SK.ini diff --git a/UI/data/locale/sl-SI.ini b/frontend/data/locale/sl-SI.ini similarity index 100% rename from UI/data/locale/sl-SI.ini rename to frontend/data/locale/sl-SI.ini diff --git a/UI/data/locale/sq-AL.ini b/frontend/data/locale/sq-AL.ini similarity index 100% rename from UI/data/locale/sq-AL.ini rename to frontend/data/locale/sq-AL.ini diff --git a/UI/data/locale/sr-CS.ini b/frontend/data/locale/sr-CS.ini similarity index 100% rename from UI/data/locale/sr-CS.ini rename to frontend/data/locale/sr-CS.ini diff --git a/UI/data/locale/sr-SP.ini b/frontend/data/locale/sr-SP.ini similarity index 100% rename from UI/data/locale/sr-SP.ini rename to frontend/data/locale/sr-SP.ini diff --git a/UI/data/locale/sv-SE.ini b/frontend/data/locale/sv-SE.ini similarity index 100% rename from UI/data/locale/sv-SE.ini rename to frontend/data/locale/sv-SE.ini diff --git a/UI/data/locale/szl-PL.ini b/frontend/data/locale/szl-PL.ini similarity index 100% rename from UI/data/locale/szl-PL.ini rename to frontend/data/locale/szl-PL.ini diff --git a/UI/data/locale/ta-IN.ini b/frontend/data/locale/ta-IN.ini similarity index 100% rename from UI/data/locale/ta-IN.ini rename to frontend/data/locale/ta-IN.ini diff --git a/UI/data/locale/te-IN.ini b/frontend/data/locale/te-IN.ini similarity index 100% rename from UI/data/locale/te-IN.ini rename to frontend/data/locale/te-IN.ini diff --git a/UI/data/locale/th-TH.ini b/frontend/data/locale/th-TH.ini similarity index 100% rename from UI/data/locale/th-TH.ini rename to frontend/data/locale/th-TH.ini diff --git a/UI/data/locale/tl-PH.ini b/frontend/data/locale/tl-PH.ini similarity index 100% rename from UI/data/locale/tl-PH.ini rename to frontend/data/locale/tl-PH.ini diff --git a/UI/data/locale/tr-TR.ini b/frontend/data/locale/tr-TR.ini similarity index 100% rename from UI/data/locale/tr-TR.ini rename to frontend/data/locale/tr-TR.ini diff --git a/UI/data/locale/tt-RU.ini b/frontend/data/locale/tt-RU.ini similarity index 100% rename from UI/data/locale/tt-RU.ini rename to frontend/data/locale/tt-RU.ini diff --git a/UI/data/locale/ug-CN.ini b/frontend/data/locale/ug-CN.ini similarity index 100% rename from UI/data/locale/ug-CN.ini rename to frontend/data/locale/ug-CN.ini diff --git a/UI/data/locale/uk-UA.ini b/frontend/data/locale/uk-UA.ini similarity index 100% rename from UI/data/locale/uk-UA.ini rename to frontend/data/locale/uk-UA.ini diff --git a/UI/data/locale/ur-PK.ini b/frontend/data/locale/ur-PK.ini similarity index 100% rename from UI/data/locale/ur-PK.ini rename to frontend/data/locale/ur-PK.ini diff --git a/UI/data/locale/vi-VN.ini b/frontend/data/locale/vi-VN.ini similarity index 100% rename from UI/data/locale/vi-VN.ini rename to frontend/data/locale/vi-VN.ini diff --git a/UI/data/locale/zh-CN.ini b/frontend/data/locale/zh-CN.ini similarity index 100% rename from UI/data/locale/zh-CN.ini rename to frontend/data/locale/zh-CN.ini diff --git a/UI/data/locale/zh-TW.ini b/frontend/data/locale/zh-TW.ini similarity index 100% rename from UI/data/locale/zh-TW.ini rename to frontend/data/locale/zh-TW.ini diff --git a/UI/data/themes/Acri/bot_hook.png b/frontend/data/themes/Acri/bot_hook.png similarity index 100% rename from UI/data/themes/Acri/bot_hook.png rename to frontend/data/themes/Acri/bot_hook.png diff --git a/UI/data/themes/Acri/bot_hook2.png b/frontend/data/themes/Acri/bot_hook2.png similarity index 100% rename from UI/data/themes/Acri/bot_hook2.png rename to frontend/data/themes/Acri/bot_hook2.png diff --git a/UI/data/themes/Acri/checkbox_checked.png b/frontend/data/themes/Acri/checkbox_checked.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked.png rename to frontend/data/themes/Acri/checkbox_checked.png diff --git a/UI/data/themes/Acri/checkbox_checked_disabled.png b/frontend/data/themes/Acri/checkbox_checked_disabled.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked_disabled.png rename to frontend/data/themes/Acri/checkbox_checked_disabled.png diff --git a/UI/data/themes/Acri/checkbox_checked_focus.png b/frontend/data/themes/Acri/checkbox_checked_focus.png similarity index 100% rename from UI/data/themes/Acri/checkbox_checked_focus.png rename to frontend/data/themes/Acri/checkbox_checked_focus.png diff --git a/UI/data/themes/Acri/checkbox_unchecked.png b/frontend/data/themes/Acri/checkbox_unchecked.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked.png rename to frontend/data/themes/Acri/checkbox_unchecked.png diff --git a/UI/data/themes/Acri/checkbox_unchecked_disabled.png b/frontend/data/themes/Acri/checkbox_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked_disabled.png rename to frontend/data/themes/Acri/checkbox_unchecked_disabled.png diff --git a/UI/data/themes/Acri/checkbox_unchecked_focus.png b/frontend/data/themes/Acri/checkbox_unchecked_focus.png similarity index 100% rename from UI/data/themes/Acri/checkbox_unchecked_focus.png rename to frontend/data/themes/Acri/checkbox_unchecked_focus.png diff --git a/UI/data/themes/Acri/radio_checked.png b/frontend/data/themes/Acri/radio_checked.png similarity index 100% rename from UI/data/themes/Acri/radio_checked.png rename to frontend/data/themes/Acri/radio_checked.png diff --git a/UI/data/themes/Acri/radio_checked_disabled.png b/frontend/data/themes/Acri/radio_checked_disabled.png similarity index 100% rename from UI/data/themes/Acri/radio_checked_disabled.png rename to frontend/data/themes/Acri/radio_checked_disabled.png diff --git a/UI/data/themes/Acri/radio_checked_focus.png b/frontend/data/themes/Acri/radio_checked_focus.png similarity index 100% rename from UI/data/themes/Acri/radio_checked_focus.png rename to frontend/data/themes/Acri/radio_checked_focus.png diff --git a/UI/data/themes/Acri/radio_unchecked.png b/frontend/data/themes/Acri/radio_unchecked.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked.png rename to frontend/data/themes/Acri/radio_unchecked.png diff --git a/UI/data/themes/Acri/radio_unchecked_disabled.png b/frontend/data/themes/Acri/radio_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked_disabled.png rename to frontend/data/themes/Acri/radio_unchecked_disabled.png diff --git a/UI/data/themes/Acri/radio_unchecked_focus.png b/frontend/data/themes/Acri/radio_unchecked_focus.png similarity index 100% rename from UI/data/themes/Acri/radio_unchecked_focus.png rename to frontend/data/themes/Acri/radio_unchecked_focus.png diff --git a/UI/data/themes/Acri/sizegrip.png b/frontend/data/themes/Acri/sizegrip.png similarity index 100% rename from UI/data/themes/Acri/sizegrip.png rename to frontend/data/themes/Acri/sizegrip.png diff --git a/UI/data/themes/Acri/top_hook.png b/frontend/data/themes/Acri/top_hook.png similarity index 100% rename from UI/data/themes/Acri/top_hook.png rename to frontend/data/themes/Acri/top_hook.png diff --git a/UI/data/themes/Dark/alert.svg b/frontend/data/themes/Dark/alert.svg similarity index 100% rename from UI/data/themes/Dark/alert.svg rename to frontend/data/themes/Dark/alert.svg diff --git a/UI/data/themes/Dark/close.svg b/frontend/data/themes/Dark/close.svg similarity index 100% rename from UI/data/themes/Dark/close.svg rename to frontend/data/themes/Dark/close.svg diff --git a/UI/data/themes/Dark/cogs.svg b/frontend/data/themes/Dark/cogs.svg similarity index 100% rename from UI/data/themes/Dark/cogs.svg rename to frontend/data/themes/Dark/cogs.svg diff --git a/UI/data/themes/Dark/collapse.svg b/frontend/data/themes/Dark/collapse.svg similarity index 100% rename from UI/data/themes/Dark/collapse.svg rename to frontend/data/themes/Dark/collapse.svg diff --git a/UI/data/themes/Dark/dots-vert.svg b/frontend/data/themes/Dark/dots-vert.svg similarity index 100% rename from UI/data/themes/Dark/dots-vert.svg rename to frontend/data/themes/Dark/dots-vert.svg diff --git a/UI/data/themes/Dark/dots.svg b/frontend/data/themes/Dark/dots.svg similarity index 100% rename from UI/data/themes/Dark/dots.svg rename to frontend/data/themes/Dark/dots.svg diff --git a/UI/data/themes/Dark/down.svg b/frontend/data/themes/Dark/down.svg similarity index 100% rename from UI/data/themes/Dark/down.svg rename to frontend/data/themes/Dark/down.svg diff --git a/UI/data/themes/Dark/entry-clear.svg b/frontend/data/themes/Dark/entry-clear.svg similarity index 100% rename from UI/data/themes/Dark/entry-clear.svg rename to frontend/data/themes/Dark/entry-clear.svg diff --git a/UI/data/themes/Dark/expand.svg b/frontend/data/themes/Dark/expand.svg similarity index 100% rename from UI/data/themes/Dark/expand.svg rename to frontend/data/themes/Dark/expand.svg diff --git a/UI/data/themes/Dark/filter.svg b/frontend/data/themes/Dark/filter.svg similarity index 100% rename from UI/data/themes/Dark/filter.svg rename to frontend/data/themes/Dark/filter.svg diff --git a/UI/data/themes/Dark/interact.svg b/frontend/data/themes/Dark/interact.svg similarity index 100% rename from UI/data/themes/Dark/interact.svg rename to frontend/data/themes/Dark/interact.svg diff --git a/UI/data/themes/Dark/left.svg b/frontend/data/themes/Dark/left.svg similarity index 100% rename from UI/data/themes/Dark/left.svg rename to frontend/data/themes/Dark/left.svg diff --git a/UI/data/themes/Dark/locked.svg b/frontend/data/themes/Dark/locked.svg similarity index 100% rename from UI/data/themes/Dark/locked.svg rename to frontend/data/themes/Dark/locked.svg diff --git a/UI/data/themes/Dark/media-pause.svg b/frontend/data/themes/Dark/media-pause.svg similarity index 100% rename from UI/data/themes/Dark/media-pause.svg rename to frontend/data/themes/Dark/media-pause.svg diff --git a/UI/data/themes/Dark/media/media_next.svg b/frontend/data/themes/Dark/media/media_next.svg similarity index 100% rename from UI/data/themes/Dark/media/media_next.svg rename to frontend/data/themes/Dark/media/media_next.svg diff --git a/UI/data/themes/Dark/media/media_pause.svg b/frontend/data/themes/Dark/media/media_pause.svg similarity index 100% rename from UI/data/themes/Dark/media/media_pause.svg rename to frontend/data/themes/Dark/media/media_pause.svg diff --git a/UI/data/themes/Dark/media/media_play.svg b/frontend/data/themes/Dark/media/media_play.svg similarity index 100% rename from UI/data/themes/Dark/media/media_play.svg rename to frontend/data/themes/Dark/media/media_play.svg diff --git a/UI/data/themes/Dark/media/media_previous.svg b/frontend/data/themes/Dark/media/media_previous.svg similarity index 100% rename from UI/data/themes/Dark/media/media_previous.svg rename to frontend/data/themes/Dark/media/media_previous.svg diff --git a/UI/data/themes/Dark/media/media_restart.svg b/frontend/data/themes/Dark/media/media_restart.svg similarity index 100% rename from UI/data/themes/Dark/media/media_restart.svg rename to frontend/data/themes/Dark/media/media_restart.svg diff --git a/UI/data/themes/Dark/media/media_stop.svg b/frontend/data/themes/Dark/media/media_stop.svg similarity index 100% rename from UI/data/themes/Dark/media/media_stop.svg rename to frontend/data/themes/Dark/media/media_stop.svg diff --git a/UI/data/themes/Dark/minus.svg b/frontend/data/themes/Dark/minus.svg similarity index 100% rename from UI/data/themes/Dark/minus.svg rename to frontend/data/themes/Dark/minus.svg diff --git a/UI/data/themes/Dark/mute.svg b/frontend/data/themes/Dark/mute.svg similarity index 100% rename from UI/data/themes/Dark/mute.svg rename to frontend/data/themes/Dark/mute.svg diff --git a/UI/data/themes/Dark/network-disconnected.svg b/frontend/data/themes/Dark/network-disconnected.svg similarity index 100% rename from UI/data/themes/Dark/network-disconnected.svg rename to frontend/data/themes/Dark/network-disconnected.svg diff --git a/UI/data/themes/Dark/network-inactive.svg b/frontend/data/themes/Dark/network-inactive.svg similarity index 100% rename from UI/data/themes/Dark/network-inactive.svg rename to frontend/data/themes/Dark/network-inactive.svg diff --git a/UI/data/themes/Dark/no_sources.svg b/frontend/data/themes/Dark/no_sources.svg similarity index 100% rename from UI/data/themes/Dark/no_sources.svg rename to frontend/data/themes/Dark/no_sources.svg diff --git a/UI/data/themes/Dark/plus.svg b/frontend/data/themes/Dark/plus.svg similarity index 100% rename from UI/data/themes/Dark/plus.svg rename to frontend/data/themes/Dark/plus.svg diff --git a/UI/data/themes/Dark/popout.svg b/frontend/data/themes/Dark/popout.svg similarity index 100% rename from UI/data/themes/Dark/popout.svg rename to frontend/data/themes/Dark/popout.svg diff --git a/UI/data/themes/Dark/recording-inactive.svg b/frontend/data/themes/Dark/recording-inactive.svg similarity index 100% rename from UI/data/themes/Dark/recording-inactive.svg rename to frontend/data/themes/Dark/recording-inactive.svg diff --git a/UI/data/themes/Dark/recording-pause-inactive.svg b/frontend/data/themes/Dark/recording-pause-inactive.svg similarity index 100% rename from UI/data/themes/Dark/recording-pause-inactive.svg rename to frontend/data/themes/Dark/recording-pause-inactive.svg diff --git a/UI/data/themes/Dark/refresh.svg b/frontend/data/themes/Dark/refresh.svg similarity index 100% rename from UI/data/themes/Dark/refresh.svg rename to frontend/data/themes/Dark/refresh.svg diff --git a/UI/data/themes/Dark/revert.svg b/frontend/data/themes/Dark/revert.svg similarity index 100% rename from UI/data/themes/Dark/revert.svg rename to frontend/data/themes/Dark/revert.svg diff --git a/UI/data/themes/Dark/right.svg b/frontend/data/themes/Dark/right.svg similarity index 100% rename from UI/data/themes/Dark/right.svg rename to frontend/data/themes/Dark/right.svg diff --git a/UI/data/themes/Dark/save.svg b/frontend/data/themes/Dark/save.svg similarity index 100% rename from UI/data/themes/Dark/save.svg rename to frontend/data/themes/Dark/save.svg diff --git a/UI/data/themes/Dark/settings/accessibility.svg b/frontend/data/themes/Dark/settings/accessibility.svg similarity index 100% rename from UI/data/themes/Dark/settings/accessibility.svg rename to frontend/data/themes/Dark/settings/accessibility.svg diff --git a/UI/data/themes/Dark/settings/advanced.svg b/frontend/data/themes/Dark/settings/advanced.svg similarity index 100% rename from UI/data/themes/Dark/settings/advanced.svg rename to frontend/data/themes/Dark/settings/advanced.svg diff --git a/UI/data/themes/Dark/settings/appearance.svg b/frontend/data/themes/Dark/settings/appearance.svg similarity index 100% rename from UI/data/themes/Dark/settings/appearance.svg rename to frontend/data/themes/Dark/settings/appearance.svg diff --git a/UI/data/themes/Dark/settings/audio.svg b/frontend/data/themes/Dark/settings/audio.svg similarity index 100% rename from UI/data/themes/Dark/settings/audio.svg rename to frontend/data/themes/Dark/settings/audio.svg diff --git a/UI/data/themes/Dark/settings/general.svg b/frontend/data/themes/Dark/settings/general.svg similarity index 100% rename from UI/data/themes/Dark/settings/general.svg rename to frontend/data/themes/Dark/settings/general.svg diff --git a/UI/data/themes/Dark/settings/hotkeys.svg b/frontend/data/themes/Dark/settings/hotkeys.svg similarity index 100% rename from UI/data/themes/Dark/settings/hotkeys.svg rename to frontend/data/themes/Dark/settings/hotkeys.svg diff --git a/UI/data/themes/Dark/settings/output.svg b/frontend/data/themes/Dark/settings/output.svg similarity index 100% rename from UI/data/themes/Dark/settings/output.svg rename to frontend/data/themes/Dark/settings/output.svg diff --git a/UI/data/themes/Dark/settings/stream.svg b/frontend/data/themes/Dark/settings/stream.svg similarity index 100% rename from UI/data/themes/Dark/settings/stream.svg rename to frontend/data/themes/Dark/settings/stream.svg diff --git a/UI/data/themes/Dark/settings/video.svg b/frontend/data/themes/Dark/settings/video.svg similarity index 100% rename from UI/data/themes/Dark/settings/video.svg rename to frontend/data/themes/Dark/settings/video.svg diff --git a/UI/data/themes/Dark/sources/brush.svg b/frontend/data/themes/Dark/sources/brush.svg similarity index 100% rename from UI/data/themes/Dark/sources/brush.svg rename to frontend/data/themes/Dark/sources/brush.svg diff --git a/UI/data/themes/Dark/sources/camera.svg b/frontend/data/themes/Dark/sources/camera.svg similarity index 100% rename from UI/data/themes/Dark/sources/camera.svg rename to frontend/data/themes/Dark/sources/camera.svg diff --git a/UI/data/themes/Dark/sources/default.svg b/frontend/data/themes/Dark/sources/default.svg similarity index 100% rename from UI/data/themes/Dark/sources/default.svg rename to frontend/data/themes/Dark/sources/default.svg diff --git a/UI/data/themes/Dark/sources/gamepad.svg b/frontend/data/themes/Dark/sources/gamepad.svg similarity index 100% rename from UI/data/themes/Dark/sources/gamepad.svg rename to frontend/data/themes/Dark/sources/gamepad.svg diff --git a/UI/data/themes/Dark/sources/globe.svg b/frontend/data/themes/Dark/sources/globe.svg similarity index 100% rename from UI/data/themes/Dark/sources/globe.svg rename to frontend/data/themes/Dark/sources/globe.svg diff --git a/UI/data/themes/Dark/sources/group.svg b/frontend/data/themes/Dark/sources/group.svg similarity index 100% rename from UI/data/themes/Dark/sources/group.svg rename to frontend/data/themes/Dark/sources/group.svg diff --git a/UI/data/themes/Dark/sources/image.svg b/frontend/data/themes/Dark/sources/image.svg similarity index 100% rename from UI/data/themes/Dark/sources/image.svg rename to frontend/data/themes/Dark/sources/image.svg diff --git a/UI/data/themes/Dark/sources/media.svg b/frontend/data/themes/Dark/sources/media.svg similarity index 100% rename from UI/data/themes/Dark/sources/media.svg rename to frontend/data/themes/Dark/sources/media.svg diff --git a/UI/data/themes/Dark/sources/microphone.svg b/frontend/data/themes/Dark/sources/microphone.svg similarity index 100% rename from UI/data/themes/Dark/sources/microphone.svg rename to frontend/data/themes/Dark/sources/microphone.svg diff --git a/UI/data/themes/Dark/sources/scene.svg b/frontend/data/themes/Dark/sources/scene.svg similarity index 100% rename from UI/data/themes/Dark/sources/scene.svg rename to frontend/data/themes/Dark/sources/scene.svg diff --git a/UI/data/themes/Dark/sources/slideshow.svg b/frontend/data/themes/Dark/sources/slideshow.svg similarity index 100% rename from UI/data/themes/Dark/sources/slideshow.svg rename to frontend/data/themes/Dark/sources/slideshow.svg diff --git a/UI/data/themes/Dark/sources/text.svg b/frontend/data/themes/Dark/sources/text.svg similarity index 100% rename from UI/data/themes/Dark/sources/text.svg rename to frontend/data/themes/Dark/sources/text.svg diff --git a/UI/data/themes/Dark/sources/window.svg b/frontend/data/themes/Dark/sources/window.svg similarity index 100% rename from UI/data/themes/Dark/sources/window.svg rename to frontend/data/themes/Dark/sources/window.svg diff --git a/UI/data/themes/Dark/sources/windowaudio.svg b/frontend/data/themes/Dark/sources/windowaudio.svg similarity index 100% rename from UI/data/themes/Dark/sources/windowaudio.svg rename to frontend/data/themes/Dark/sources/windowaudio.svg diff --git a/UI/data/themes/Dark/streaming-inactive.svg b/frontend/data/themes/Dark/streaming-inactive.svg similarity index 100% rename from UI/data/themes/Dark/streaming-inactive.svg rename to frontend/data/themes/Dark/streaming-inactive.svg diff --git a/UI/data/themes/Dark/trash.svg b/frontend/data/themes/Dark/trash.svg similarity index 100% rename from UI/data/themes/Dark/trash.svg rename to frontend/data/themes/Dark/trash.svg diff --git a/UI/data/themes/Dark/unassigned.svg b/frontend/data/themes/Dark/unassigned.svg similarity index 100% rename from UI/data/themes/Dark/unassigned.svg rename to frontend/data/themes/Dark/unassigned.svg diff --git a/UI/data/themes/Dark/up.svg b/frontend/data/themes/Dark/up.svg similarity index 100% rename from UI/data/themes/Dark/up.svg rename to frontend/data/themes/Dark/up.svg diff --git a/UI/data/themes/Dark/updown.svg b/frontend/data/themes/Dark/updown.svg similarity index 100% rename from UI/data/themes/Dark/updown.svg rename to frontend/data/themes/Dark/updown.svg diff --git a/UI/data/themes/Dark/visible.svg b/frontend/data/themes/Dark/visible.svg similarity index 100% rename from UI/data/themes/Dark/visible.svg rename to frontend/data/themes/Dark/visible.svg diff --git a/UI/data/themes/Light/alert.svg b/frontend/data/themes/Light/alert.svg similarity index 100% rename from UI/data/themes/Light/alert.svg rename to frontend/data/themes/Light/alert.svg diff --git a/UI/data/themes/Light/checkbox_checked.svg b/frontend/data/themes/Light/checkbox_checked.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked.svg rename to frontend/data/themes/Light/checkbox_checked.svg diff --git a/UI/data/themes/Light/checkbox_checked_disabled.svg b/frontend/data/themes/Light/checkbox_checked_disabled.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked_disabled.svg rename to frontend/data/themes/Light/checkbox_checked_disabled.svg diff --git a/UI/data/themes/Light/checkbox_checked_focus.svg b/frontend/data/themes/Light/checkbox_checked_focus.svg similarity index 100% rename from UI/data/themes/Light/checkbox_checked_focus.svg rename to frontend/data/themes/Light/checkbox_checked_focus.svg diff --git a/UI/data/themes/Light/checkbox_unchecked.svg b/frontend/data/themes/Light/checkbox_unchecked.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked.svg rename to frontend/data/themes/Light/checkbox_unchecked.svg diff --git a/UI/data/themes/Light/checkbox_unchecked_disabled.svg b/frontend/data/themes/Light/checkbox_unchecked_disabled.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked_disabled.svg rename to frontend/data/themes/Light/checkbox_unchecked_disabled.svg diff --git a/UI/data/themes/Light/checkbox_unchecked_focus.svg b/frontend/data/themes/Light/checkbox_unchecked_focus.svg similarity index 100% rename from UI/data/themes/Light/checkbox_unchecked_focus.svg rename to frontend/data/themes/Light/checkbox_unchecked_focus.svg diff --git a/UI/data/themes/Light/close.svg b/frontend/data/themes/Light/close.svg similarity index 100% rename from UI/data/themes/Light/close.svg rename to frontend/data/themes/Light/close.svg diff --git a/UI/data/themes/Light/cogs.svg b/frontend/data/themes/Light/cogs.svg similarity index 100% rename from UI/data/themes/Light/cogs.svg rename to frontend/data/themes/Light/cogs.svg diff --git a/UI/data/themes/Light/collapse.svg b/frontend/data/themes/Light/collapse.svg similarity index 100% rename from UI/data/themes/Light/collapse.svg rename to frontend/data/themes/Light/collapse.svg diff --git a/UI/data/themes/Light/dots-vert.svg b/frontend/data/themes/Light/dots-vert.svg similarity index 100% rename from UI/data/themes/Light/dots-vert.svg rename to frontend/data/themes/Light/dots-vert.svg diff --git a/UI/data/themes/Light/dots.svg b/frontend/data/themes/Light/dots.svg similarity index 100% rename from UI/data/themes/Light/dots.svg rename to frontend/data/themes/Light/dots.svg diff --git a/UI/data/themes/Light/down.svg b/frontend/data/themes/Light/down.svg similarity index 100% rename from UI/data/themes/Light/down.svg rename to frontend/data/themes/Light/down.svg diff --git a/UI/data/themes/Light/entry-clear.svg b/frontend/data/themes/Light/entry-clear.svg similarity index 100% rename from UI/data/themes/Light/entry-clear.svg rename to frontend/data/themes/Light/entry-clear.svg diff --git a/UI/data/themes/Light/expand.svg b/frontend/data/themes/Light/expand.svg similarity index 100% rename from UI/data/themes/Light/expand.svg rename to frontend/data/themes/Light/expand.svg diff --git a/UI/data/themes/Light/filter.svg b/frontend/data/themes/Light/filter.svg similarity index 100% rename from UI/data/themes/Light/filter.svg rename to frontend/data/themes/Light/filter.svg diff --git a/UI/data/themes/Light/interact.svg b/frontend/data/themes/Light/interact.svg similarity index 100% rename from UI/data/themes/Light/interact.svg rename to frontend/data/themes/Light/interact.svg diff --git a/UI/data/themes/Light/left.svg b/frontend/data/themes/Light/left.svg similarity index 100% rename from UI/data/themes/Light/left.svg rename to frontend/data/themes/Light/left.svg diff --git a/UI/data/themes/Light/locked.svg b/frontend/data/themes/Light/locked.svg similarity index 100% rename from UI/data/themes/Light/locked.svg rename to frontend/data/themes/Light/locked.svg diff --git a/UI/data/themes/Light/media-pause.svg b/frontend/data/themes/Light/media-pause.svg similarity index 100% rename from UI/data/themes/Light/media-pause.svg rename to frontend/data/themes/Light/media-pause.svg diff --git a/UI/data/themes/Light/media/media_next.svg b/frontend/data/themes/Light/media/media_next.svg similarity index 100% rename from UI/data/themes/Light/media/media_next.svg rename to frontend/data/themes/Light/media/media_next.svg diff --git a/UI/data/themes/Light/media/media_pause.svg b/frontend/data/themes/Light/media/media_pause.svg similarity index 100% rename from UI/data/themes/Light/media/media_pause.svg rename to frontend/data/themes/Light/media/media_pause.svg diff --git a/UI/data/themes/Light/media/media_play.svg b/frontend/data/themes/Light/media/media_play.svg similarity index 100% rename from UI/data/themes/Light/media/media_play.svg rename to frontend/data/themes/Light/media/media_play.svg diff --git a/UI/data/themes/Light/media/media_previous.svg b/frontend/data/themes/Light/media/media_previous.svg similarity index 100% rename from UI/data/themes/Light/media/media_previous.svg rename to frontend/data/themes/Light/media/media_previous.svg diff --git a/UI/data/themes/Light/media/media_restart.svg b/frontend/data/themes/Light/media/media_restart.svg similarity index 100% rename from UI/data/themes/Light/media/media_restart.svg rename to frontend/data/themes/Light/media/media_restart.svg diff --git a/UI/data/themes/Light/media/media_stop.svg b/frontend/data/themes/Light/media/media_stop.svg similarity index 100% rename from UI/data/themes/Light/media/media_stop.svg rename to frontend/data/themes/Light/media/media_stop.svg diff --git a/UI/data/themes/Light/minus.svg b/frontend/data/themes/Light/minus.svg similarity index 100% rename from UI/data/themes/Light/minus.svg rename to frontend/data/themes/Light/minus.svg diff --git a/UI/data/themes/Light/mute.svg b/frontend/data/themes/Light/mute.svg similarity index 100% rename from UI/data/themes/Light/mute.svg rename to frontend/data/themes/Light/mute.svg diff --git a/UI/data/themes/Light/no_sources.svg b/frontend/data/themes/Light/no_sources.svg similarity index 100% rename from UI/data/themes/Light/no_sources.svg rename to frontend/data/themes/Light/no_sources.svg diff --git a/UI/data/themes/Light/plus.svg b/frontend/data/themes/Light/plus.svg similarity index 100% rename from UI/data/themes/Light/plus.svg rename to frontend/data/themes/Light/plus.svg diff --git a/UI/data/themes/Light/popout.svg b/frontend/data/themes/Light/popout.svg similarity index 100% rename from UI/data/themes/Light/popout.svg rename to frontend/data/themes/Light/popout.svg diff --git a/UI/data/themes/Light/refresh.svg b/frontend/data/themes/Light/refresh.svg similarity index 100% rename from UI/data/themes/Light/refresh.svg rename to frontend/data/themes/Light/refresh.svg diff --git a/UI/data/themes/Light/revert.svg b/frontend/data/themes/Light/revert.svg similarity index 100% rename from UI/data/themes/Light/revert.svg rename to frontend/data/themes/Light/revert.svg diff --git a/UI/data/themes/Light/right.svg b/frontend/data/themes/Light/right.svg similarity index 100% rename from UI/data/themes/Light/right.svg rename to frontend/data/themes/Light/right.svg diff --git a/UI/data/themes/Light/save.svg b/frontend/data/themes/Light/save.svg similarity index 100% rename from UI/data/themes/Light/save.svg rename to frontend/data/themes/Light/save.svg diff --git a/UI/data/themes/Light/settings/accessibility.svg b/frontend/data/themes/Light/settings/accessibility.svg similarity index 100% rename from UI/data/themes/Light/settings/accessibility.svg rename to frontend/data/themes/Light/settings/accessibility.svg diff --git a/UI/data/themes/Light/settings/advanced.svg b/frontend/data/themes/Light/settings/advanced.svg similarity index 100% rename from UI/data/themes/Light/settings/advanced.svg rename to frontend/data/themes/Light/settings/advanced.svg diff --git a/UI/data/themes/Light/settings/appearance.svg b/frontend/data/themes/Light/settings/appearance.svg similarity index 100% rename from UI/data/themes/Light/settings/appearance.svg rename to frontend/data/themes/Light/settings/appearance.svg diff --git a/UI/data/themes/Light/settings/audio.svg b/frontend/data/themes/Light/settings/audio.svg similarity index 100% rename from UI/data/themes/Light/settings/audio.svg rename to frontend/data/themes/Light/settings/audio.svg diff --git a/UI/data/themes/Light/settings/general.svg b/frontend/data/themes/Light/settings/general.svg similarity index 100% rename from UI/data/themes/Light/settings/general.svg rename to frontend/data/themes/Light/settings/general.svg diff --git a/UI/data/themes/Light/settings/hotkeys.svg b/frontend/data/themes/Light/settings/hotkeys.svg similarity index 100% rename from UI/data/themes/Light/settings/hotkeys.svg rename to frontend/data/themes/Light/settings/hotkeys.svg diff --git a/UI/data/themes/Light/settings/output.svg b/frontend/data/themes/Light/settings/output.svg similarity index 100% rename from UI/data/themes/Light/settings/output.svg rename to frontend/data/themes/Light/settings/output.svg diff --git a/UI/data/themes/Light/settings/stream.svg b/frontend/data/themes/Light/settings/stream.svg similarity index 100% rename from UI/data/themes/Light/settings/stream.svg rename to frontend/data/themes/Light/settings/stream.svg diff --git a/UI/data/themes/Light/settings/video.svg b/frontend/data/themes/Light/settings/video.svg similarity index 100% rename from UI/data/themes/Light/settings/video.svg rename to frontend/data/themes/Light/settings/video.svg diff --git a/UI/data/themes/Light/sources/brush.svg b/frontend/data/themes/Light/sources/brush.svg similarity index 100% rename from UI/data/themes/Light/sources/brush.svg rename to frontend/data/themes/Light/sources/brush.svg diff --git a/UI/data/themes/Light/sources/camera.svg b/frontend/data/themes/Light/sources/camera.svg similarity index 100% rename from UI/data/themes/Light/sources/camera.svg rename to frontend/data/themes/Light/sources/camera.svg diff --git a/UI/data/themes/Light/sources/default.svg b/frontend/data/themes/Light/sources/default.svg similarity index 100% rename from UI/data/themes/Light/sources/default.svg rename to frontend/data/themes/Light/sources/default.svg diff --git a/UI/data/themes/Light/sources/gamepad.svg b/frontend/data/themes/Light/sources/gamepad.svg similarity index 100% rename from UI/data/themes/Light/sources/gamepad.svg rename to frontend/data/themes/Light/sources/gamepad.svg diff --git a/UI/data/themes/Light/sources/globe.svg b/frontend/data/themes/Light/sources/globe.svg similarity index 100% rename from UI/data/themes/Light/sources/globe.svg rename to frontend/data/themes/Light/sources/globe.svg diff --git a/UI/data/themes/Light/sources/group.svg b/frontend/data/themes/Light/sources/group.svg similarity index 100% rename from UI/data/themes/Light/sources/group.svg rename to frontend/data/themes/Light/sources/group.svg diff --git a/UI/data/themes/Light/sources/image.svg b/frontend/data/themes/Light/sources/image.svg similarity index 100% rename from UI/data/themes/Light/sources/image.svg rename to frontend/data/themes/Light/sources/image.svg diff --git a/UI/data/themes/Light/sources/media.svg b/frontend/data/themes/Light/sources/media.svg similarity index 100% rename from UI/data/themes/Light/sources/media.svg rename to frontend/data/themes/Light/sources/media.svg diff --git a/UI/data/themes/Light/sources/microphone.svg b/frontend/data/themes/Light/sources/microphone.svg similarity index 100% rename from UI/data/themes/Light/sources/microphone.svg rename to frontend/data/themes/Light/sources/microphone.svg diff --git a/UI/data/themes/Light/sources/scene.svg b/frontend/data/themes/Light/sources/scene.svg similarity index 100% rename from UI/data/themes/Light/sources/scene.svg rename to frontend/data/themes/Light/sources/scene.svg diff --git a/UI/data/themes/Light/sources/slideshow.svg b/frontend/data/themes/Light/sources/slideshow.svg similarity index 100% rename from UI/data/themes/Light/sources/slideshow.svg rename to frontend/data/themes/Light/sources/slideshow.svg diff --git a/UI/data/themes/Light/sources/text.svg b/frontend/data/themes/Light/sources/text.svg similarity index 100% rename from UI/data/themes/Light/sources/text.svg rename to frontend/data/themes/Light/sources/text.svg diff --git a/UI/data/themes/Light/sources/window.svg b/frontend/data/themes/Light/sources/window.svg similarity index 100% rename from UI/data/themes/Light/sources/window.svg rename to frontend/data/themes/Light/sources/window.svg diff --git a/UI/data/themes/Light/sources/windowaudio.svg b/frontend/data/themes/Light/sources/windowaudio.svg similarity index 100% rename from UI/data/themes/Light/sources/windowaudio.svg rename to frontend/data/themes/Light/sources/windowaudio.svg diff --git a/UI/data/themes/Light/trash.svg b/frontend/data/themes/Light/trash.svg similarity index 100% rename from UI/data/themes/Light/trash.svg rename to frontend/data/themes/Light/trash.svg diff --git a/UI/data/themes/Light/up.svg b/frontend/data/themes/Light/up.svg similarity index 100% rename from UI/data/themes/Light/up.svg rename to frontend/data/themes/Light/up.svg diff --git a/UI/data/themes/Light/updown.svg b/frontend/data/themes/Light/updown.svg similarity index 100% rename from UI/data/themes/Light/updown.svg rename to frontend/data/themes/Light/updown.svg diff --git a/UI/data/themes/Light/visible.svg b/frontend/data/themes/Light/visible.svg similarity index 100% rename from UI/data/themes/Light/visible.svg rename to frontend/data/themes/Light/visible.svg diff --git a/UI/data/themes/Rachni/checkbox_checked.png b/frontend/data/themes/Rachni/checkbox_checked.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked.png rename to frontend/data/themes/Rachni/checkbox_checked.png diff --git a/UI/data/themes/Rachni/checkbox_checked_disabled.png b/frontend/data/themes/Rachni/checkbox_checked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked_disabled.png rename to frontend/data/themes/Rachni/checkbox_checked_disabled.png diff --git a/UI/data/themes/Rachni/checkbox_checked_focus.png b/frontend/data/themes/Rachni/checkbox_checked_focus.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_checked_focus.png rename to frontend/data/themes/Rachni/checkbox_checked_focus.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked.png b/frontend/data/themes/Rachni/checkbox_unchecked.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked.png rename to frontend/data/themes/Rachni/checkbox_unchecked.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked_disabled.png b/frontend/data/themes/Rachni/checkbox_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked_disabled.png rename to frontend/data/themes/Rachni/checkbox_unchecked_disabled.png diff --git a/UI/data/themes/Rachni/checkbox_unchecked_focus.png b/frontend/data/themes/Rachni/checkbox_unchecked_focus.png similarity index 100% rename from UI/data/themes/Rachni/checkbox_unchecked_focus.png rename to frontend/data/themes/Rachni/checkbox_unchecked_focus.png diff --git a/UI/data/themes/Rachni/down_arrow.png b/frontend/data/themes/Rachni/down_arrow.png similarity index 100% rename from UI/data/themes/Rachni/down_arrow.png rename to frontend/data/themes/Rachni/down_arrow.png diff --git a/UI/data/themes/Rachni/down_arrow_disabled.png b/frontend/data/themes/Rachni/down_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/down_arrow_disabled.png rename to frontend/data/themes/Rachni/down_arrow_disabled.png diff --git a/UI/data/themes/Rachni/left_arrow.png b/frontend/data/themes/Rachni/left_arrow.png similarity index 100% rename from UI/data/themes/Rachni/left_arrow.png rename to frontend/data/themes/Rachni/left_arrow.png diff --git a/UI/data/themes/Rachni/left_arrow_disabled.png b/frontend/data/themes/Rachni/left_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/left_arrow_disabled.png rename to frontend/data/themes/Rachni/left_arrow_disabled.png diff --git a/UI/data/themes/Rachni/radio_checked.png b/frontend/data/themes/Rachni/radio_checked.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked.png rename to frontend/data/themes/Rachni/radio_checked.png diff --git a/UI/data/themes/Rachni/radio_checked_disabled.png b/frontend/data/themes/Rachni/radio_checked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked_disabled.png rename to frontend/data/themes/Rachni/radio_checked_disabled.png diff --git a/UI/data/themes/Rachni/radio_checked_focus.png b/frontend/data/themes/Rachni/radio_checked_focus.png similarity index 100% rename from UI/data/themes/Rachni/radio_checked_focus.png rename to frontend/data/themes/Rachni/radio_checked_focus.png diff --git a/UI/data/themes/Rachni/radio_unchecked.png b/frontend/data/themes/Rachni/radio_unchecked.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked.png rename to frontend/data/themes/Rachni/radio_unchecked.png diff --git a/UI/data/themes/Rachni/radio_unchecked_disabled.png b/frontend/data/themes/Rachni/radio_unchecked_disabled.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked_disabled.png rename to frontend/data/themes/Rachni/radio_unchecked_disabled.png diff --git a/UI/data/themes/Rachni/radio_unchecked_focus.png b/frontend/data/themes/Rachni/radio_unchecked_focus.png similarity index 100% rename from UI/data/themes/Rachni/radio_unchecked_focus.png rename to frontend/data/themes/Rachni/radio_unchecked_focus.png diff --git a/UI/data/themes/Rachni/right_arrow.png b/frontend/data/themes/Rachni/right_arrow.png similarity index 100% rename from UI/data/themes/Rachni/right_arrow.png rename to frontend/data/themes/Rachni/right_arrow.png diff --git a/UI/data/themes/Rachni/right_arrow_disabled.png b/frontend/data/themes/Rachni/right_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/right_arrow_disabled.png rename to frontend/data/themes/Rachni/right_arrow_disabled.png diff --git a/UI/data/themes/Rachni/sizegrip.png b/frontend/data/themes/Rachni/sizegrip.png similarity index 100% rename from UI/data/themes/Rachni/sizegrip.png rename to frontend/data/themes/Rachni/sizegrip.png diff --git a/UI/data/themes/Rachni/up_arrow.png b/frontend/data/themes/Rachni/up_arrow.png similarity index 100% rename from UI/data/themes/Rachni/up_arrow.png rename to frontend/data/themes/Rachni/up_arrow.png diff --git a/UI/data/themes/Rachni/up_arrow_disabled.png b/frontend/data/themes/Rachni/up_arrow_disabled.png similarity index 100% rename from UI/data/themes/Rachni/up_arrow_disabled.png rename to frontend/data/themes/Rachni/up_arrow_disabled.png diff --git a/UI/data/themes/System.obt b/frontend/data/themes/System.obt similarity index 100% rename from UI/data/themes/System.obt rename to frontend/data/themes/System.obt diff --git a/UI/data/themes/Yami.obt b/frontend/data/themes/Yami.obt similarity index 100% rename from UI/data/themes/Yami.obt rename to frontend/data/themes/Yami.obt diff --git a/UI/data/themes/Yami/checkbox_checked.svg b/frontend/data/themes/Yami/checkbox_checked.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked.svg rename to frontend/data/themes/Yami/checkbox_checked.svg diff --git a/UI/data/themes/Yami/checkbox_checked_disabled.svg b/frontend/data/themes/Yami/checkbox_checked_disabled.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked_disabled.svg rename to frontend/data/themes/Yami/checkbox_checked_disabled.svg diff --git a/UI/data/themes/Yami/checkbox_checked_focus.svg b/frontend/data/themes/Yami/checkbox_checked_focus.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_checked_focus.svg rename to frontend/data/themes/Yami/checkbox_checked_focus.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked.svg b/frontend/data/themes/Yami/checkbox_unchecked.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked.svg rename to frontend/data/themes/Yami/checkbox_unchecked.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked_disabled.svg b/frontend/data/themes/Yami/checkbox_unchecked_disabled.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked_disabled.svg rename to frontend/data/themes/Yami/checkbox_unchecked_disabled.svg diff --git a/UI/data/themes/Yami/checkbox_unchecked_focus.svg b/frontend/data/themes/Yami/checkbox_unchecked_focus.svg similarity index 100% rename from UI/data/themes/Yami/checkbox_unchecked_focus.svg rename to frontend/data/themes/Yami/checkbox_unchecked_focus.svg diff --git a/UI/data/themes/Yami_Acri.ovt b/frontend/data/themes/Yami_Acri.ovt similarity index 100% rename from UI/data/themes/Yami_Acri.ovt rename to frontend/data/themes/Yami_Acri.ovt diff --git a/UI/data/themes/Yami_Classic.ovt b/frontend/data/themes/Yami_Classic.ovt similarity index 100% rename from UI/data/themes/Yami_Classic.ovt rename to frontend/data/themes/Yami_Classic.ovt diff --git a/UI/data/themes/Yami_Default.ovt b/frontend/data/themes/Yami_Default.ovt similarity index 100% rename from UI/data/themes/Yami_Default.ovt rename to frontend/data/themes/Yami_Default.ovt diff --git a/UI/data/themes/Yami_Grey.ovt b/frontend/data/themes/Yami_Grey.ovt similarity index 100% rename from UI/data/themes/Yami_Grey.ovt rename to frontend/data/themes/Yami_Grey.ovt diff --git a/UI/data/themes/Yami_Light.ovt b/frontend/data/themes/Yami_Light.ovt similarity index 100% rename from UI/data/themes/Yami_Light.ovt rename to frontend/data/themes/Yami_Light.ovt diff --git a/UI/data/themes/Yami_Rachni.ovt b/frontend/data/themes/Yami_Rachni.ovt similarity index 100% rename from UI/data/themes/Yami_Rachni.ovt rename to frontend/data/themes/Yami_Rachni.ovt diff --git a/UI/window-namedialog.cpp b/frontend/dialogs/NameDialog.cpp similarity index 95% rename from UI/window-namedialog.cpp rename to frontend/dialogs/NameDialog.cpp index 561bd15b9..5458fafb6 100644 --- a/UI/window-namedialog.cpp +++ b/frontend/dialogs/NameDialog.cpp @@ -15,12 +15,18 @@ along with this program. If not, see . ******************************************************************************/ -#include "moc_window-namedialog.cpp" -#include "obs-app.hpp" +#include "NameDialog.hpp" -#include +#include + +#include +#include +#include +#include #include +#include "moc_NameDialog.cpp" + NameDialog::NameDialog(QWidget *parent) : QDialog(parent) { installEventFilter(CreateShortcutFilter()); diff --git a/UI/window-namedialog.hpp b/frontend/dialogs/NameDialog.hpp similarity index 93% rename from UI/window-namedialog.hpp rename to frontend/dialogs/NameDialog.hpp index e0f7006ba..a61f3dfd3 100644 --- a/UI/window-namedialog.hpp +++ b/frontend/dialogs/NameDialog.hpp @@ -18,12 +18,11 @@ #pragma once #include -#include -#include -#include -#include -#include -#include + +class QCheckBox; +class QLabel; +class QLineEdit; +class QString; class NameDialog : public QDialog { Q_OBJECT diff --git a/frontend/dialogs/OAuthLogin.cpp b/frontend/dialogs/OAuthLogin.cpp new file mode 100644 index 000000000..64af6869b --- /dev/null +++ b/frontend/dialogs/OAuthLogin.cpp @@ -0,0 +1,106 @@ +#include "OAuthLogin.hpp" + +#include + +#ifdef BROWSER_AVAILABLE +#include +#endif +#include + +#include "moc_OAuthLogin.cpp" + +#ifdef BROWSER_AVAILABLE +extern QCef *cef; +extern QCefCookieManager *panel_cookies; +#endif + +OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) +{ +#ifdef BROWSER_AVAILABLE + if (!cef) { + return; + } + + setWindowTitle("Auth"); + setMinimumSize(400, 400); + resize(700, 700); + + Qt::WindowFlags flags = windowFlags(); + Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; + setWindowFlags(flags & (~helpFlag)); + + OBSBasic::InitBrowserPanelSafeBlock(); + + cefWidget = cef->create_widget(nullptr, url, panel_cookies); + if (!cefWidget) { + fail = true; + return; + } + + connect(cefWidget, &QCefWidget::titleChanged, this, &OAuthLogin::setWindowTitle); + connect(cefWidget, &QCefWidget::urlChanged, this, &OAuthLogin::urlChanged); + + QPushButton *close = new QPushButton(QTStr("Cancel")); + connect(close, &QAbstractButton::clicked, this, &QDialog::reject); + + QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->addStretch(); + bottomLayout->addWidget(close); + bottomLayout->addStretch(); + + QVBoxLayout *topLayout = new QVBoxLayout(this); + topLayout->addWidget(cefWidget); + topLayout->addLayout(bottomLayout); +#else + UNUSED_PARAMETER(url); +#endif +} + +OAuthLogin::~OAuthLogin() {} + +int OAuthLogin::exec() +{ +#ifdef BROWSER_AVAILABLE + if (cefWidget) { + return QDialog::exec(); + } +#endif + return QDialog::Rejected; +} + +void OAuthLogin::reject() +{ +#ifdef BROWSER_AVAILABLE + delete cefWidget; +#endif + QDialog::reject(); +} + +void OAuthLogin::accept() +{ +#ifdef BROWSER_AVAILABLE + delete cefWidget; +#endif + QDialog::accept(); +} + +void OAuthLogin::urlChanged(const QString &url) +{ + std::string uri = get_token ? "access_token=" : "code="; + int code_idx = url.indexOf(uri.c_str()); + if (code_idx == -1) + return; + + if (!url.startsWith(OAUTH_BASE_URL)) + return; + + code_idx += (int)uri.size(); + + int next_idx = url.indexOf("&", code_idx); + if (next_idx != -1) + code = url.mid(code_idx, next_idx - code_idx); + else + code = url.right(url.size() - code_idx); + + accept(); +} diff --git a/frontend/dialogs/OAuthLogin.hpp b/frontend/dialogs/OAuthLogin.hpp new file mode 100644 index 000000000..9e6e94b49 --- /dev/null +++ b/frontend/dialogs/OAuthLogin.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +class QCefWidget; + +class OAuthLogin : public QDialog { + Q_OBJECT + + QCefWidget *cefWidget = nullptr; + QString code; + bool get_token = false; + bool fail = false; + +public: + OAuthLogin(QWidget *parent, const std::string &url, bool token); + ~OAuthLogin(); + + inline QString GetCode() const { return code; } + inline bool LoadFail() const { return fail; } + + virtual int exec() override; + virtual void reject() override; + virtual void accept() override; + +public slots: + void urlChanged(const QString &url); +}; diff --git a/UI/window-basic-about.cpp b/frontend/dialogs/OBSAbout.cpp similarity index 96% rename from UI/window-basic-about.cpp rename to frontend/dialogs/OBSAbout.cpp index 3509e279b..e9103dbab 100644 --- a/UI/window-basic-about.cpp +++ b/frontend/dialogs/OBSAbout.cpp @@ -1,14 +1,18 @@ -#include "moc_window-basic-about.cpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" +#include "OBSAbout.hpp" + +#include +#include + #include -#include -#include -#include + #include +#include "moc_OBSAbout.cpp" + using namespace json11; +extern bool steam; + OBSAbout::OBSAbout(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAbout) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/UI/window-basic-about.hpp b/frontend/dialogs/OBSAbout.hpp similarity index 93% rename from UI/window-basic-about.hpp rename to frontend/dialogs/OBSAbout.hpp index bb507fe2c..e3ec87c36 100644 --- a/UI/window-basic-about.hpp +++ b/frontend/dialogs/OBSAbout.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include "ui_OBSAbout.h" +#include + class OBSAbout : public QDialog { Q_OBJECT diff --git a/UI/window-basic-adv-audio.cpp b/frontend/dialogs/OBSBasicAdvAudio.cpp similarity index 95% rename from UI/window-basic-adv-audio.cpp rename to frontend/dialogs/OBSBasicAdvAudio.cpp index 001b804d7..8992e9822 100644 --- a/UI/window-basic-adv-audio.cpp +++ b/frontend/dialogs/OBSBasicAdvAudio.cpp @@ -1,13 +1,11 @@ -#include "window-basic-adv-audio.hpp" -#include "window-basic-main.hpp" -#include "item-widget-helpers.hpp" -#include "adv-audio-control.hpp" -#include "obs-app.hpp" -#include - +#include "OBSBasicAdvAudio.hpp" #include "ui_OBSAdvAudio.h" -Q_DECLARE_METATYPE(OBSSource); +#include +#include +#include + +#include "moc_OBSBasicAdvAudio.cpp" OBSBasicAdvAudio::OBSBasicAdvAudio(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAdvAudio), showInactive(false) { diff --git a/UI/window-basic-adv-audio.hpp b/frontend/dialogs/OBSBasicAdvAudio.hpp similarity index 96% rename from UI/window-basic-adv-audio.hpp rename to frontend/dialogs/OBSBasicAdvAudio.hpp index 28b6fa05b..98cb61b0a 100644 --- a/UI/window-basic-adv-audio.hpp +++ b/frontend/dialogs/OBSBasicAdvAudio.hpp @@ -1,9 +1,8 @@ #pragma once #include + #include -#include -#include class OBSAdvAudioCtrl; class Ui_OBSAdvAudio; diff --git a/UI/window-basic-filters.cpp b/frontend/dialogs/OBSBasicFilters.cpp similarity index 98% rename from UI/window-basic-filters.cpp rename to frontend/dialogs/OBSBasicFilters.cpp index c92036c89..7567757ee 100644 --- a/UI/window-basic-filters.cpp +++ b/frontend/dialogs/OBSBasicFilters.cpp @@ -15,35 +15,28 @@ along with this program. If not, see . ******************************************************************************/ -#include "properties-view.hpp" -#include "window-namedialog.hpp" -#include "window-basic-main.hpp" -#include "window-basic-filters.hpp" -#include "display-helpers.hpp" -#include "visibility-item-widget.hpp" -#include "item-widget-helpers.hpp" -#include "obs-app.hpp" -#include "undo-stack-obs.hpp" +#include "OBSBasicFilters.hpp" +#include +#include +#include +#include +#include +#include + +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif -using namespace std; +#include "moc_OBSBasicFilters.cpp" -Q_DECLARE_METATYPE(OBSSource); +using namespace std; OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_) : QDialog(parent), diff --git a/UI/window-basic-filters.hpp b/frontend/dialogs/OBSBasicFilters.hpp similarity index 97% rename from UI/window-basic-filters.hpp rename to frontend/dialogs/OBSBasicFilters.hpp index c1e2e6c98..a02d51914 100644 --- a/UI/window-basic-filters.hpp +++ b/frontend/dialogs/OBSBasicFilters.hpp @@ -17,16 +17,12 @@ #pragma once +#include "ui_OBSBasicFilters.h" + #include -#include -#include -#include -#include class OBSBasic; -class QMenu; - -#include "ui_OBSBasicFilters.h" +class OBSPropertiesView; class OBSBasicFilters : public QDialog { Q_OBJECT diff --git a/UI/window-basic-interaction.cpp b/frontend/dialogs/OBSBasicInteraction.cpp similarity index 97% rename from UI/window-basic-interaction.cpp rename to frontend/dialogs/OBSBasicInteraction.cpp index 93d572c5b..e4c2b705e 100644 --- a/UI/window-basic-interaction.cpp +++ b/frontend/dialogs/OBSBasicInteraction.cpp @@ -15,22 +15,24 @@ along with this program. If not, see . ******************************************************************************/ -#include "obs-app.hpp" -#include "moc_window-basic-interaction.cpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" +#include "OBSBasicInteraction.hpp" + +#include +#include +#include +#include #include -#include -#include -#include -#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif +#include "moc_OBSBasicInteraction.cpp" + using namespace std; OBSBasicInteraction::OBSBasicInteraction(QWidget *parent, OBSSource source_) diff --git a/UI/window-basic-interaction.hpp b/frontend/dialogs/OBSBasicInteraction.hpp similarity index 84% rename from UI/window-basic-interaction.hpp rename to frontend/dialogs/OBSBasicInteraction.hpp index 6bfaaab4a..01e288b6b 100644 --- a/UI/window-basic-interaction.hpp +++ b/frontend/dialogs/OBSBasicInteraction.hpp @@ -17,17 +17,13 @@ #pragma once -#include -#include -#include - -#include -#include - -class OBSBasic; - #include "ui_OBSBasicInteraction.h" +#include + +#include + +class OBSBasic; class OBSEventFilter; class OBSBasicInteraction : public QDialog { @@ -66,17 +62,3 @@ protected: virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; }; - -typedef std::function EventFilterFunc; - -class OBSEventFilter : public QObject { - Q_OBJECT -public: - OBSEventFilter(EventFilterFunc filter_) : filter(filter_) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) { return filter(obj, event); } - -public: - EventFilterFunc filter; -}; diff --git a/UI/window-basic-properties.cpp b/frontend/dialogs/OBSBasicProperties.cpp similarity index 97% rename from UI/window-basic-properties.cpp rename to frontend/dialogs/OBSBasicProperties.cpp index 342b02dd4..ee656f595 100644 --- a/UI/window-basic-properties.cpp +++ b/frontend/dialogs/OBSBasicProperties.cpp @@ -15,27 +15,24 @@ along with this program. If not, see . ******************************************************************************/ -#include "obs-app.hpp" -#include "moc_window-basic-properties.cpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" +#include "OBSBasicProperties.hpp" + +#include +#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #endif +#include "moc_OBSBasicProperties.cpp" + using namespace std; static void CreateTransitionScene(OBSSource scene, const char *text, uint32_t color); diff --git a/UI/window-basic-properties.hpp b/frontend/dialogs/OBSBasicProperties.hpp similarity index 95% rename from UI/window-basic-properties.hpp rename to frontend/dialogs/OBSBasicProperties.hpp index 3f9557c2c..91738115c 100644 --- a/UI/window-basic-properties.hpp +++ b/frontend/dialogs/OBSBasicProperties.hpp @@ -17,18 +17,13 @@ #pragma once -#include -#include -#include -#include -#include "qt-display.hpp" -#include - -class OBSPropertiesView; -class OBSBasic; - #include "ui_OBSBasicProperties.h" +#include + +class OBSBasic; +class OBSPropertiesView; + class OBSBasicProperties : public QDialog { Q_OBJECT diff --git a/UI/window-basic-source-select.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp similarity index 97% rename from UI/window-basic-source-select.cpp rename to frontend/dialogs/OBSBasicSourceSelect.cpp index 430a3f140..7f7cc3a3c 100644 --- a/UI/window-basic-source-select.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -15,11 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "OBSBasicSourceSelect.hpp" + #include -#include "window-basic-main.hpp" -#include "moc_window-basic-source-select.cpp" -#include "obs-app.hpp" + +#include "moc_OBSBasicSourceSelect.cpp" struct AddSourceData { /* Input data */ @@ -332,13 +332,6 @@ static inline const char *GetSourceDisplayName(const char *id) return obs_source_get_display_name(v_id); } -Q_DECLARE_METATYPE(OBSScene); - -template static inline T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_, undo_stack &undo_s) : QDialog(parent), ui(new Ui::OBSBasicSourceSelect), diff --git a/UI/window-basic-source-select.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp similarity index 94% rename from UI/window-basic-source-select.hpp rename to frontend/dialogs/OBSBasicSourceSelect.hpp index 3baadf7b3..077d8d298 100644 --- a/UI/window-basic-source-select.hpp +++ b/frontend/dialogs/OBSBasicSourceSelect.hpp @@ -17,14 +17,14 @@ #pragma once -#include -#include - #include "ui_OBSBasicSourceSelect.h" -#include "undo-stack-obs.hpp" -#include "window-basic-main.hpp" -class OBSBasic; +#include +#include + +#include + +#include class OBSBasicSourceSelect : public QDialog { Q_OBJECT diff --git a/UI/window-basic-transform.cpp b/frontend/dialogs/OBSBasicTransform.cpp similarity index 97% rename from UI/window-basic-transform.cpp rename to frontend/dialogs/OBSBasicTransform.cpp index 2e84f3519..7889f84bc 100644 --- a/UI/window-basic-transform.cpp +++ b/frontend/dialogs/OBSBasicTransform.cpp @@ -1,9 +1,8 @@ -#include -#include "window-basic-transform.hpp" -#include "window-basic-main.hpp" +#include "OBSBasicTransform.hpp" -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSceneItem); +#include + +#include "moc_OBSBasicTransform.cpp" static bool find_sel(obs_scene_t *, obs_sceneitem_t *item, void *param) { @@ -352,11 +351,6 @@ void OBSBasicTransform::OnCropChanged() ignoreTransformSignal = false; } -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - void OBSBasicTransform::OnSceneChanged(QListWidgetItem *current, QListWidgetItem *) { if (!current) diff --git a/UI/window-basic-transform.hpp b/frontend/dialogs/OBSBasicTransform.hpp similarity index 98% rename from UI/window-basic-transform.hpp rename to frontend/dialogs/OBSBasicTransform.hpp index 46905724f..d1559e18b 100644 --- a/UI/window-basic-transform.hpp +++ b/frontend/dialogs/OBSBasicTransform.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include - #include "ui_OBSBasicTransform.h" +#include + +#include + class OBSBasic; class QListWidgetItem; diff --git a/UI/window-basic-vcam-config.cpp b/frontend/dialogs/OBSBasicVCamConfig.cpp similarity index 95% rename from UI/window-basic-vcam-config.cpp rename to frontend/dialogs/OBSBasicVCamConfig.cpp index 7db63513f..3f7107499 100644 --- a/UI/window-basic-vcam-config.cpp +++ b/frontend/dialogs/OBSBasicVCamConfig.cpp @@ -1,11 +1,8 @@ -#include "moc_window-basic-vcam-config.cpp" -#include "window-basic-main.hpp" +#include "OBSBasicVCamConfig.hpp" -#include -#include -#include +#include -#include +#include "moc_OBSBasicVCamConfig.cpp" OBSBasicVCamConfig::OBSBasicVCamConfig(const VCamConfig &_config, bool _vcamActive, QWidget *parent) : config(_config), diff --git a/UI/window-basic-vcam-config.hpp b/frontend/dialogs/OBSBasicVCamConfig.hpp similarity index 85% rename from UI/window-basic-vcam-config.hpp rename to frontend/dialogs/OBSBasicVCamConfig.hpp index 7ca154c15..bd07700e7 100644 --- a/UI/window-basic-vcam-config.hpp +++ b/frontend/dialogs/OBSBasicVCamConfig.hpp @@ -1,14 +1,10 @@ #pragma once -#include -#include -#include - -#include "window-basic-vcam.hpp" - #include "ui_OBSBasicVCamConfig.h" -struct VCamConfig; +#include + +#include class OBSBasicVCamConfig : public QDialog { Q_OBJECT diff --git a/frontend/dialogs/OBSExtraBrowsers.cpp b/frontend/dialogs/OBSExtraBrowsers.cpp new file mode 100644 index 000000000..ab5789e79 --- /dev/null +++ b/frontend/dialogs/OBSExtraBrowsers.cpp @@ -0,0 +1,36 @@ +#include "OBSExtraBrowsers.hpp" +#include "ui_OBSExtraBrowsers.h" + +#include + +#include "moc_OBSExtraBrowsers.cpp" + +OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + model = new ExtraBrowsersModel(ui->table); + + ui->table->setModel(model); + ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model)); + ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model)); + ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed); + ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); +} + +OBSExtraBrowsers::~OBSExtraBrowsers() {} + +void OBSExtraBrowsers::closeEvent(QCloseEvent *event) +{ + QDialog::closeEvent(event); + model->Apply(); +} + +void OBSExtraBrowsers::on_apply_clicked() +{ + model->Apply(); +} diff --git a/frontend/dialogs/OBSExtraBrowsers.hpp b/frontend/dialogs/OBSExtraBrowsers.hpp new file mode 100644 index 000000000..c71a12cae --- /dev/null +++ b/frontend/dialogs/OBSExtraBrowsers.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +class Ui_OBSExtraBrowsers; + +class OBSExtraBrowsers : public QDialog { + Q_OBJECT + + std::unique_ptr ui; + ExtraBrowsersModel *model; + +public: + OBSExtraBrowsers(QWidget *parent); + ~OBSExtraBrowsers(); + + void closeEvent(QCloseEvent *event) override; + +public slots: + void on_apply_clicked(); +}; diff --git a/UI/window-log-reply.cpp b/frontend/dialogs/OBSLogReply.cpp similarity index 95% rename from UI/window-log-reply.cpp rename to frontend/dialogs/OBSLogReply.cpp index 71c379dfe..1000372a5 100644 --- a/UI/window-log-reply.cpp +++ b/frontend/dialogs/OBSLogReply.cpp @@ -15,12 +15,15 @@ along with this program. If not, see . ******************************************************************************/ +#include "OBSLogReply.hpp" + +#include + #include -#include -#include #include -#include "moc_window-log-reply.cpp" -#include "obs-app.hpp" +#include + +#include "moc_OBSLogReply.cpp" OBSLogReply::OBSLogReply(QWidget *parent, const QString &url, const bool crash) : QDialog(parent), diff --git a/UI/window-log-reply.hpp b/frontend/dialogs/OBSLogReply.hpp similarity index 98% rename from UI/window-log-reply.hpp rename to frontend/dialogs/OBSLogReply.hpp index 598a7f172..260b827f7 100644 --- a/UI/window-log-reply.hpp +++ b/frontend/dialogs/OBSLogReply.hpp @@ -17,9 +17,10 @@ #pragma once -#include #include "ui_OBSLogReply.h" +#include + class OBSLogReply : public QDialog { Q_OBJECT diff --git a/UI/log-viewer.cpp b/frontend/dialogs/OBSLogViewer.cpp similarity index 94% rename from UI/log-viewer.cpp rename to frontend/dialogs/OBSLogViewer.cpp index 9ef67a69c..592111c59 100644 --- a/UI/log-viewer.cpp +++ b/frontend/dialogs/OBSLogViewer.cpp @@ -1,16 +1,14 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSLogViewer.hpp" + +#include + #include -#include "moc_log-viewer.cpp" +#include +#include +#include + +#include "moc_OBSLogViewer.cpp" OBSLogViewer::OBSLogViewer(QWidget *parent) : QDialog(parent), ui(new Ui::OBSLogViewer) { diff --git a/UI/log-viewer.hpp b/frontend/dialogs/OBSLogViewer.hpp similarity index 88% rename from UI/log-viewer.hpp rename to frontend/dialogs/OBSLogViewer.hpp index d9e329bba..96a817100 100644 --- a/UI/log-viewer.hpp +++ b/frontend/dialogs/OBSLogViewer.hpp @@ -1,11 +1,9 @@ #pragma once -#include -#include -#include "obs-app.hpp" - #include "ui_OBSLogViewer.h" +#include + class OBSLogViewer : public QDialog { Q_OBJECT diff --git a/frontend/dialogs/OBSMissingFiles.cpp b/frontend/dialogs/OBSMissingFiles.cpp new file mode 100644 index 000000000..ca5d7a101 --- /dev/null +++ b/frontend/dialogs/OBSMissingFiles.cpp @@ -0,0 +1,154 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSMissingFiles.hpp" + +#include +#include +#include + +#include + +#include "moc_OBSMissingFiles.cpp" + +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesColumn { Source, OriginalPath, NewPath, State, Count }; + +OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) + : QDialog(parent), + filesModel(new MissingFilesModel), + ui(new Ui::OBSMissingFiles) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(filesModel); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath, + new MissingFilesPathItemDelegate(false, "")); + ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath, + new MissingFilesPathItemDelegate(true, "")); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); + ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32))); + + for (size_t i = 0; i < obs_missing_files_count(files); i++) { + obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i); + + const char *oldPath = obs_missing_file_get_path(f); + const char *name = obs_missing_file_get_source_name(f); + + addMissingFile(oldPath, name); + } + + QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files))); + + ui->found->setText(found); + + fileStore = files; + + connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles); + connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders); + connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close); + connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged); + + QModelIndex index = filesModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +OBSMissingFiles::~OBSMissingFiles() +{ + obs_missing_files_destroy(fileStore); +} + +void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName) +{ + QStringList list; + + list.append(originalPath); + list.append(sourceName); + + QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source); + + filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole); +} + +void OBSMissingFiles::saveFiles() +{ + for (int i = 0; i < filesModel->files.length(); i++) { + MissingFilesState state = filesModel->files[i].state; + if (state != MissingFilesState::Missing) { + obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i); + + QString path = filesModel->files[i].newPath; + + if (state == MissingFilesState::Cleared) { + obs_missing_file_issue_callback(f, ""); + } else { + char *p = bstrdup(path.toStdString().c_str()); + obs_missing_file_issue_callback(f, p); + bfree(p); + } + } + } + + QDialog::accept(); +} + +void OBSMissingFiles::browseFolders() +{ + QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir != "") { + dir += "/"; + filesModel->fileCheckLoop(filesModel->files, dir, true); + } +} + +void OBSMissingFiles::dataChanged() +{ + QString found = + QTStr("MissingFiles.NumFound") + .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore))); + + ui->found->setText(found); + + ui->tableView->resizeColumnToContents(MissingFilesColumn::State); + ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); +} + +QIcon OBSMissingFiles::GetWarningIcon() +{ + return filesModel->warningIcon; +} + +void OBSMissingFiles::SetWarningIcon(const QIcon &icon) +{ + ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); + filesModel->warningIcon = icon; +} diff --git a/frontend/dialogs/OBSMissingFiles.hpp b/frontend/dialogs/OBSMissingFiles.hpp new file mode 100644 index 000000000..2d5bb41e6 --- /dev/null +++ b/frontend/dialogs/OBSMissingFiles.hpp @@ -0,0 +1,53 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "ui_OBSMissingFiles.h" + +#include + +#include +#include + +class MissingFilesModel; + +class OBSMissingFiles : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) + + QPointer filesModel; + std::unique_ptr ui; + +public: + explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); + virtual ~OBSMissingFiles() override; + + void addMissingFile(const char *originalPath, const char *sourceName); + + QIcon GetWarningIcon(); + void SetWarningIcon(const QIcon &icon); + +private: + void saveFiles(); + void browseFolders(); + + obs_missing_files_t *fileStore; + +public slots: + void dataChanged(); +}; diff --git a/UI/window-permissions.cpp b/frontend/dialogs/OBSPermissions.cpp similarity index 97% rename from UI/window-permissions.cpp rename to frontend/dialogs/OBSPermissions.cpp index 046a011f9..9c62970d7 100644 --- a/UI/window-permissions.cpp +++ b/frontend/dialogs/OBSPermissions.cpp @@ -15,9 +15,11 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include "moc_window-permissions.cpp" -#include "obs-app.hpp" +#include "OBSPermissions.hpp" + +#include + +#include "moc_OBSPermissions.cpp" OBSPermissions::OBSPermissions(QWidget *parent, MacPermissionStatus capture, MacPermissionStatus video, MacPermissionStatus audio, MacPermissionStatus accessibility) diff --git a/UI/window-permissions.hpp b/frontend/dialogs/OBSPermissions.hpp similarity index 96% rename from UI/window-permissions.hpp rename to frontend/dialogs/OBSPermissions.hpp index 67a74cbb1..5fab46b55 100644 --- a/UI/window-permissions.hpp +++ b/frontend/dialogs/OBSPermissions.hpp @@ -18,7 +18,10 @@ #pragma once #include "ui_OBSPermissions.h" -#include "platform.hpp" + +#include + +#include #define MACOS_PERMISSIONS_DIALOG_VERSION 1 diff --git a/frontend/dialogs/OBSRemux.cpp b/frontend/dialogs/OBSRemux.cpp new file mode 100644 index 000000000..84b28372e --- /dev/null +++ b/frontend/dialogs/OBSRemux.cpp @@ -0,0 +1,310 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSRemux.hpp" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "moc_OBSRemux.cpp" + +OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_) + : QDialog(parent), + queueModel(new RemuxQueueModel), + worker(new RemuxWorker()), + ui(new Ui::OBSRemux), + recPath(path), + autoRemux(autoRemux_) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + + if (autoRemux) { + resize(280, 40); + ui->tableView->hide(); + ui->buttonBox->hide(); + ui->label->hide(); + } + + ui->progressBar->setMinimum(0); + ui->progressBar->setMaximum(1000); + ui->progressBar->setValue(0); + + ui->tableView->setModel(queueModel); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath, + new RemuxEntryPathItemDelegate(false, recPath)); + ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath, + new RemuxEntryPathItemDelegate(true, recPath)); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State, + QHeaderView::ResizeMode::Fixed); + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + ui->tableView->setTextElideMode(Qt::ElideMiddle); + ui->tableView->setWordWrap(false); + + installEventFilter(CreateShortcutFilter()); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll")); + ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux); + connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished); + connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, + &OBSRemux::clearAll); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close); + + worker->moveToThread(&remuxer); + remuxer.start(); + + connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress); + connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater); + connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished); + connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux); + + connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged); + connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged); + + QModelIndex index = queueModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +bool OBSRemux::stopRemux() +{ + if (!worker->isWorking) + return true; + + // By locking the worker thread's mutex, we ensure that its + // update poll will be blocked as long as we're in here with + // the popup open. + QMutexLocker lock(&worker->updateMutex); + + bool exit = false; + + if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + exit = true; + } + + if (exit) { + // Inform the worker it should no longer be + // working. It will interrupt accordingly in + // its next update callback. + worker->isWorking = false; + } + + return exit; +} + +OBSRemux::~OBSRemux() +{ + stopRemux(); + remuxer.quit(); + remuxer.wait(); +} + +void OBSRemux::rowCountChanged(const QModelIndex &, int, int) +{ + // See if there are still any rows ready to remux. Change + // the state of the "go" button accordingly. + // There must be more than one row, since there will always be + // at least one row for the empty insertion point. + if (queueModel->rowCount() > 1) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + } else { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + } +} + +void OBSRemux::dropEvent(QDropEvent *ev) +{ + QStringList urlList; + + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + + if (fileInfo.isDir()) { + QStringList directoryFilter; + directoryFilter << "*.flv" + << "*.mp4" + << "*.mov" + << "*.mkv" + << "*.ts" + << "*.m3u8"; + + QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files, + QDirIterator::Subdirectories); + + while (dirIter.hasNext()) { + urlList.append(dirIter.next()); + } + } else { + urlList.append(fileInfo.canonicalFilePath()); + } + } + + if (urlList.empty()) { + QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"), + QMessageBox::Ok); + } else if (!autoRemux) { + QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath); + queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole); + } +} + +void OBSRemux::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls() && !worker->isWorking) + ev->accept(); +} + +void OBSRemux::beginRemux() +{ + if (worker->isWorking) { + stopRemux(); + return; + } + + bool proceedWithRemux = true; + QFileInfoList overwriteFiles = queueModel->checkForOverwrites(); + + if (!overwriteFiles.empty()) { + QString message = QTStr("Remux.FileExists"); + message += "\n\n"; + + for (QFileInfo fileInfo : overwriteFiles) + message += fileInfo.canonicalFilePath() + "\n"; + + if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes) + proceedWithRemux = false; + } + + if (!proceedWithRemux) + return; + + // Set all jobs to "pending" first. + queueModel->beginProcessing(); + + ui->progressBar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop")); + setAcceptDrops(false); + + remuxNextEntry(); +} + +void OBSRemux::AutoRemux(QString inFile, QString outFile) +{ + if (inFile != "" && outFile != "" && autoRemux) { + ui->progressBar->setVisible(true); + emit remux(inFile, outFile); + autoRemuxFile = outFile; + } +} + +void OBSRemux::remuxNextEntry() +{ + worker->lastProgress = 0.f; + + QString inputPath, outputPath; + if (queueModel->beginNextEntry(inputPath, outputPath)) { + emit remux(inputPath, outputPath); + } else { + queueModel->autoRemux = autoRemux; + queueModel->endProcessing(); + + if (!autoRemux) { + OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"), + queueModel->checkForErrors() ? QTStr("Remux.FinishedError") + : QTStr("Remux.Finished")); + } + + ui->progressBar->setVisible(autoRemux); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux")); + ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true); + ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished()); + setAcceptDrops(true); + } +} + +void OBSRemux::closeEvent(QCloseEvent *event) +{ + if (!stopRemux()) + event->ignore(); + else + QDialog::closeEvent(event); +} + +void OBSRemux::reject() +{ + if (!stopRemux()) + return; + + QDialog::reject(); +} + +void OBSRemux::updateProgress(float percent) +{ + ui->progressBar->setValue(percent * 10); +} + +void OBSRemux::remuxFinished(bool success) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + + queueModel->finishEntry(success); + + if (autoRemux && autoRemuxFile != "") { + QTimer::singleShot(3000, this, &OBSRemux::close); + + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile)); + } + + remuxNextEntry(); +} + +void OBSRemux::clearFinished() +{ + queueModel->clearFinished(); +} + +void OBSRemux::clearAll() +{ + queueModel->clearAll(); +} diff --git a/frontend/dialogs/OBSRemux.hpp b/frontend/dialogs/OBSRemux.hpp new file mode 100644 index 000000000..87d68587e --- /dev/null +++ b/frontend/dialogs/OBSRemux.hpp @@ -0,0 +1,72 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "ui_OBSRemux.h" + +#include +#include + +class RemuxQueueModel; +class RemuxWorker; + +class OBSRemux : public QDialog { + Q_OBJECT + + QPointer queueModel; + QThread remuxer; + QPointer worker; + + std::unique_ptr ui; + + const char *recPath; + + virtual void closeEvent(QCloseEvent *event) override; + virtual void reject() override; + + bool autoRemux; + QString autoRemuxFile; + +public: + explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false); + virtual ~OBSRemux() override; + + using job_t = std::shared_ptr; + + void AutoRemux(QString inFile, QString outFile); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + + void remuxNextEntry(); + +private slots: + void rowCountChanged(const QModelIndex &parent, int first, int last); + +public slots: + void updateProgress(float percent); + void remuxFinished(bool success); + void beginRemux(); + bool stopRemux(); + void clearFinished(); + void clearAll(); + +signals: + void remux(const QString &source, const QString &target); +}; diff --git a/UI/update/update-window.cpp b/frontend/dialogs/OBSUpdate.cpp similarity index 89% rename from UI/update/update-window.cpp rename to frontend/dialogs/OBSUpdate.cpp index 777eb7878..e25c425b6 100644 --- a/UI/update/update-window.cpp +++ b/frontend/dialogs/OBSUpdate.cpp @@ -1,5 +1,8 @@ -#include "update-window.hpp" -#include "obs-app.hpp" +#include "OBSUpdate.hpp" + +#include + +#include "ui_OBSUpdate.h" OBSUpdate::OBSUpdate(QWidget *parent, bool manualUpdate, const QString &text) : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint), diff --git a/UI/update/update-window.hpp b/frontend/dialogs/OBSUpdate.hpp similarity index 89% rename from UI/update/update-window.hpp rename to frontend/dialogs/OBSUpdate.hpp index 8c269762b..5f45674fd 100644 --- a/UI/update/update-window.hpp +++ b/frontend/dialogs/OBSUpdate.hpp @@ -1,9 +1,8 @@ #pragma once #include -#include -#include "ui_OBSUpdate.h" +class Ui_OBSUpdate; class OBSUpdate : public QDialog { Q_OBJECT diff --git a/UI/window-whats-new.cpp b/frontend/dialogs/OBSWhatsNew.cpp similarity index 90% rename from UI/window-whats-new.cpp rename to frontend/dialogs/OBSWhatsNew.cpp index a39fc0b51..22ba4e29b 100644 --- a/UI/window-whats-new.cpp +++ b/frontend/dialogs/OBSWhatsNew.cpp @@ -1,17 +1,17 @@ -#include "moc_window-whats-new.cpp" +#include "OBSWhatsNew.hpp" -#include -#include -#include - -#include "window-basic-main.hpp" +#include #ifdef BROWSER_AVAILABLE #include extern QCef *cef; #endif -/* ------------------------------------------------------------------------- */ +#include +#include +#include + +#include "moc_OBSWhatsNew.cpp" OBSWhatsNew::OBSWhatsNew(QWidget *parent, const std::string &url) : QDialog(parent) { diff --git a/UI/window-whats-new.hpp b/frontend/dialogs/OBSWhatsNew.hpp similarity index 99% rename from UI/window-whats-new.hpp rename to frontend/dialogs/OBSWhatsNew.hpp index 1c39a850c..6ff1081aa 100644 --- a/UI/window-whats-new.hpp +++ b/frontend/dialogs/OBSWhatsNew.hpp @@ -2,6 +2,7 @@ #include #include + #include class QCefWidget; diff --git a/UI/window-youtube-actions.cpp b/frontend/dialogs/OBSYoutubeActions.cpp similarity index 99% rename from UI/window-youtube-actions.cpp rename to frontend/dialogs/OBSYoutubeActions.cpp index 9c8533fd0..27ceef450 100644 --- a/UI/window-youtube-actions.cpp +++ b/frontend/dialogs/OBSYoutubeActions.cpp @@ -1,16 +1,17 @@ -#include "window-basic-main.hpp" -#include "moc_window-youtube-actions.cpp" +#include "OBSYoutubeActions.hpp" -#include "obs-app.hpp" -#include "youtube-api-wrappers.hpp" +#include +#include #include -#include -#include + #include #include -#include #include +#include +#include + +#include "moc_OBSYoutubeActions.cpp" const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'"; const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m"; diff --git a/UI/window-youtube-actions.hpp b/frontend/dialogs/OBSYoutubeActions.hpp similarity index 96% rename from UI/window-youtube-actions.hpp rename to frontend/dialogs/OBSYoutubeActions.hpp index 7543792a8..323c25d52 100644 --- a/UI/window-youtube-actions.hpp +++ b/frontend/dialogs/OBSYoutubeActions.hpp @@ -1,11 +1,10 @@ #pragma once -#include -#include -#include - #include "ui_OBSYoutubeActions.h" -#include "youtube-api-wrappers.hpp" + +#include + +#include class WorkerThread : public QThread { Q_OBJECT diff --git a/UI/window-dock-browser.cpp b/frontend/docks/BrowserDock.cpp similarity index 92% rename from UI/window-dock-browser.cpp rename to frontend/docks/BrowserDock.cpp index 30e1de1bc..622d94bd6 100644 --- a/UI/window-dock-browser.cpp +++ b/frontend/docks/BrowserDock.cpp @@ -1,4 +1,5 @@ -#include "window-dock-browser.hpp" +#include "BrowserDock.hpp" + #include void BrowserDock::closeEvent(QCloseEvent *event) diff --git a/UI/window-dock-browser.hpp b/frontend/docks/BrowserDock.hpp similarity index 92% rename from UI/window-dock-browser.hpp rename to frontend/docks/BrowserDock.hpp index 750ed42bd..a62f9c26a 100644 --- a/UI/window-dock-browser.hpp +++ b/frontend/docks/BrowserDock.hpp @@ -1,9 +1,9 @@ #pragma once -#include "window-dock.hpp" -#include +#include "OBSDock.hpp" #include + extern QCef *cef; extern QCefCookieManager *panel_cookies; diff --git a/UI/window-dock.cpp b/frontend/docks/OBSDock.cpp similarity index 92% rename from UI/window-dock.cpp rename to frontend/docks/OBSDock.cpp index 8755dd097..c0271b61f 100644 --- a/UI/window-dock.cpp +++ b/frontend/docks/OBSDock.cpp @@ -1,9 +1,11 @@ -#include "moc_window-dock.cpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" +#include "OBSDock.hpp" + +#include -#include #include +#include + +#include "moc_OBSDock.cpp" void OBSDock::closeEvent(QCloseEvent *event) { diff --git a/UI/window-dock.hpp b/frontend/docks/OBSDock.hpp similarity index 86% rename from UI/window-dock.hpp rename to frontend/docks/OBSDock.hpp index 6d843a419..5eddc3d4a 100644 --- a/UI/window-dock.hpp +++ b/frontend/docks/OBSDock.hpp @@ -2,6 +2,10 @@ #include +class QCloseEvent; +class QShowEvent; +class QString; + class OBSDock : public QDockWidget { Q_OBJECT diff --git a/UI/window-dock-youtube-app.cpp b/frontend/docks/YouTubeAppDock.cpp similarity index 98% rename from UI/window-dock-youtube-app.cpp rename to frontend/docks/YouTubeAppDock.cpp index 5a2ec34de..a5b283484 100644 --- a/UI/window-dock-youtube-app.cpp +++ b/frontend/docks/YouTubeAppDock.cpp @@ -1,15 +1,19 @@ +#include "YouTubeAppDock.hpp" + +#include +#include + +#include + #include - -#include "window-basic-main.hpp" -#include "youtube-api-wrappers.hpp" -#include "moc_window-dock-youtube-app.cpp" - -#include "ui-config.h" -#include "qt-wrappers.hpp" - #include + +#include "moc_YouTubeAppDock.cpp" + using json = nlohmann::json; +extern bool cef_js_avail; + #ifdef YOUTUBE_WEBAPP_PLACEHOLDER static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL = YOUTUBE_WEBAPP_PLACEHOLDER; #else diff --git a/UI/window-dock-youtube-app.hpp b/frontend/docks/YouTubeAppDock.hpp similarity index 94% rename from UI/window-dock-youtube-app.hpp rename to frontend/docks/YouTubeAppDock.hpp index ff6074de7..2e7b79ce4 100644 --- a/UI/window-dock-youtube-app.hpp +++ b/frontend/docks/YouTubeAppDock.hpp @@ -1,10 +1,12 @@ #pragma once -#include "window-dock-browser.hpp" -#include "youtube-api-wrappers.hpp" +#include "BrowserDock.hpp" + +#include class QAction; class QCefWidget; +class YoutubeApiWrappers; class YouTubeAppDock : public BrowserDock { Q_OBJECT diff --git a/frontend/docks/YouTubeChatDock.cpp b/frontend/docks/YouTubeChatDock.cpp new file mode 100644 index 000000000..eae3a2b7c --- /dev/null +++ b/frontend/docks/YouTubeChatDock.cpp @@ -0,0 +1,35 @@ +#include "YouTubeChatDock.hpp" + +#include +#include +#include + +#include + +#include +#include + +#include "moc_YouTubeChatDock.cpp" + +#ifdef BROWSER_AVAILABLE +void YoutubeChatDock::YoutubeCookieCheck() +{ + QPointer this_ = this; + auto cb = [this_](bool currentlyLoggedIn) { + bool previouslyLoggedIn = this_->isLoggedIn; + this_->isLoggedIn = currentlyLoggedIn; + bool loginStateChanged = (currentlyLoggedIn && !previouslyLoggedIn) || + (!currentlyLoggedIn && previouslyLoggedIn); + if (loginStateChanged) { + OBSBasic *main = OBSBasic::Get(); + if (main->GetYouTubeAppDock() != nullptr) { + QMetaObject::invokeMethod(main->GetYouTubeAppDock(), "SettingsUpdated", + Qt::QueuedConnection, Q_ARG(bool, !currentlyLoggedIn)); + } + } + }; + if (panel_cookies) { + panel_cookies->CheckForCookie("https://www.youtube.com", "SID", cb); + } +} +#endif diff --git a/frontend/docks/YouTubeChatDock.hpp b/frontend/docks/YouTubeChatDock.hpp new file mode 100644 index 000000000..034be0e67 --- /dev/null +++ b/frontend/docks/YouTubeChatDock.hpp @@ -0,0 +1,23 @@ +#pragma once + +#ifdef BROWSER_AVAILABLE +#include "BrowserDock.hpp" + +class YoutubeChatDock : public BrowserDock { + Q_OBJECT + +private: + bool isLoggedIn; + +public: + YoutubeChatDock(const QString &title) : BrowserDock(title) {} + + inline void SetWidget(QCefWidget *widget_) + { + BrowserDock::SetWidget(widget_); + QWidget::connect(cefWidget.get(), &QCefWidget::urlChanged, this, &YoutubeChatDock::YoutubeCookieCheck); + } +private slots: + void YoutubeCookieCheck(); +}; +#endif diff --git a/UI/forms/AutoConfigFinishPage.ui b/frontend/forms/AutoConfigFinishPage.ui similarity index 100% rename from UI/forms/AutoConfigFinishPage.ui rename to frontend/forms/AutoConfigFinishPage.ui diff --git a/UI/forms/AutoConfigStartPage.ui b/frontend/forms/AutoConfigStartPage.ui similarity index 100% rename from UI/forms/AutoConfigStartPage.ui rename to frontend/forms/AutoConfigStartPage.ui diff --git a/UI/forms/AutoConfigStreamPage.ui b/frontend/forms/AutoConfigStreamPage.ui similarity index 99% rename from UI/forms/AutoConfigStreamPage.ui rename to frontend/forms/AutoConfigStreamPage.ui index 241292c6d..18c219ceb 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/frontend/forms/AutoConfigStreamPage.ui @@ -534,7 +534,7 @@ UrlPushButton QPushButton -
url-push-button.hpp
+
components/UrlPushButton.hpp
diff --git a/UI/forms/AutoConfigTestPage.ui b/frontend/forms/AutoConfigTestPage.ui similarity index 100% rename from UI/forms/AutoConfigTestPage.ui rename to frontend/forms/AutoConfigTestPage.ui diff --git a/UI/forms/AutoConfigVideoPage.ui b/frontend/forms/AutoConfigVideoPage.ui similarity index 100% rename from UI/forms/AutoConfigVideoPage.ui rename to frontend/forms/AutoConfigVideoPage.ui diff --git a/UI/forms/ColorSelect.ui b/frontend/forms/ColorSelect.ui similarity index 100% rename from UI/forms/ColorSelect.ui rename to frontend/forms/ColorSelect.ui diff --git a/UI/forms/OBSAbout.ui b/frontend/forms/OBSAbout.ui similarity index 99% rename from UI/forms/OBSAbout.ui rename to frontend/forms/OBSAbout.ui index 37ad2a278..415dc3f9e 100644 --- a/UI/forms/OBSAbout.ui +++ b/frontend/forms/OBSAbout.ui @@ -243,7 +243,7 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
diff --git a/UI/forms/OBSAdvAudio.ui b/frontend/forms/OBSAdvAudio.ui similarity index 100% rename from UI/forms/OBSAdvAudio.ui rename to frontend/forms/OBSAdvAudio.ui diff --git a/UI/forms/OBSBasic.ui b/frontend/forms/OBSBasic.ui similarity index 99% rename from UI/forms/OBSBasic.ui rename to frontend/forms/OBSBasic.ui index 53e457ad3..832038b09 100644 --- a/UI/forms/OBSBasic.ui +++ b/frontend/forms/OBSBasic.ui @@ -2218,18 +2218,18 @@ OBSBasicPreview QWidget -
window-basic-preview.hpp
+
widgets/OBSBasicPreview.hpp
1
OBSBasicStatusBar QStatusBar -
window-basic-status-bar.hpp
+
widgets/OBSBasicStatusBar.hpp
HScrollArea QScrollArea -
horizontal-scroll-area.hpp
+
components/HScrollArea.hpp
1
@@ -2241,28 +2241,28 @@ SourceTree QListView -
source-tree.hpp
+
components/SourceTree.hpp
SceneTree QListWidget -
scene-tree.hpp
+
components/SceneTree.hpp
OBSDock QDockWidget -
window-dock.hpp
+
docks/OBSDock.hpp
1
OBSPreviewScalingLabel QLabel -
preview-controls.hpp
+
components/OBSPreviewScalingLabel.hpp
OBSPreviewScalingComboBox QComboBox -
preview-controls.hpp
+
components/OBSPreviewScalingComboBox.hpp
diff --git a/UI/forms/OBSBasicControls.ui b/frontend/forms/OBSBasicControls.ui similarity index 99% rename from UI/forms/OBSBasicControls.ui rename to frontend/forms/OBSBasicControls.ui index ce1c929d5..51bbd0175 100644 --- a/UI/forms/OBSBasicControls.ui +++ b/frontend/forms/OBSBasicControls.ui @@ -393,7 +393,7 @@ NonCheckableButton QPushButton -
noncheckable-button.hpp
+
components/NonCheckableButton.hpp
diff --git a/UI/forms/OBSBasicFilters.ui b/frontend/forms/OBSBasicFilters.ui similarity index 99% rename from UI/forms/OBSBasicFilters.ui rename to frontend/forms/OBSBasicFilters.ui index ec964e268..c9cefaefc 100644 --- a/UI/forms/OBSBasicFilters.ui +++ b/frontend/forms/OBSBasicFilters.ui @@ -653,13 +653,13 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
FocusList QListWidget -
focus-list.hpp
+
components/FocusList.hpp
diff --git a/UI/forms/OBSBasicInteraction.ui b/frontend/forms/OBSBasicInteraction.ui similarity index 96% rename from UI/forms/OBSBasicInteraction.ui rename to frontend/forms/OBSBasicInteraction.ui index 65d313bc9..0ed573f50 100644 --- a/UI/forms/OBSBasicInteraction.ui +++ b/frontend/forms/OBSBasicInteraction.ui @@ -39,7 +39,7 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
diff --git a/UI/forms/OBSBasicProperties.ui b/frontend/forms/OBSBasicProperties.ui similarity index 98% rename from UI/forms/OBSBasicProperties.ui rename to frontend/forms/OBSBasicProperties.ui index 26145f772..66a25488e 100644 --- a/UI/forms/OBSBasicProperties.ui +++ b/frontend/forms/OBSBasicProperties.ui @@ -121,7 +121,7 @@ OBSQTDisplay QWidget -
qt-display.hpp
+
widgets/OBSQTDisplay.hpp
1
diff --git a/UI/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui similarity index 99% rename from UI/forms/OBSBasicSettings.ui rename to frontend/forms/OBSBasicSettings.ui index 267f4a3a4..50e69dd2e 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -8472,12 +8472,12 @@ UrlPushButton QPushButton -
url-push-button.hpp
+
components/UrlPushButton.hpp
OBSHotkeyEdit QLineEdit -
hotkey-edit.hpp
+
settings/OBSHotkeyEdit.hpp
diff --git a/UI/forms/OBSBasicSourceSelect.ui b/frontend/forms/OBSBasicSourceSelect.ui similarity index 100% rename from UI/forms/OBSBasicSourceSelect.ui rename to frontend/forms/OBSBasicSourceSelect.ui diff --git a/UI/forms/OBSBasicTransform.ui b/frontend/forms/OBSBasicTransform.ui similarity index 100% rename from UI/forms/OBSBasicTransform.ui rename to frontend/forms/OBSBasicTransform.ui diff --git a/UI/forms/OBSBasicVCamConfig.ui b/frontend/forms/OBSBasicVCamConfig.ui similarity index 100% rename from UI/forms/OBSBasicVCamConfig.ui rename to frontend/forms/OBSBasicVCamConfig.ui diff --git a/UI/forms/OBSExtraBrowsers.ui b/frontend/forms/OBSExtraBrowsers.ui similarity index 100% rename from UI/forms/OBSExtraBrowsers.ui rename to frontend/forms/OBSExtraBrowsers.ui diff --git a/UI/forms/OBSImporter.ui b/frontend/forms/OBSImporter.ui similarity index 100% rename from UI/forms/OBSImporter.ui rename to frontend/forms/OBSImporter.ui diff --git a/UI/forms/OBSLogReply.ui b/frontend/forms/OBSLogReply.ui similarity index 100% rename from UI/forms/OBSLogReply.ui rename to frontend/forms/OBSLogReply.ui diff --git a/UI/forms/OBSLogViewer.ui b/frontend/forms/OBSLogViewer.ui similarity index 100% rename from UI/forms/OBSLogViewer.ui rename to frontend/forms/OBSLogViewer.ui diff --git a/UI/forms/OBSMissingFiles.ui b/frontend/forms/OBSMissingFiles.ui similarity index 100% rename from UI/forms/OBSMissingFiles.ui rename to frontend/forms/OBSMissingFiles.ui diff --git a/UI/forms/OBSPermissions.ui b/frontend/forms/OBSPermissions.ui similarity index 100% rename from UI/forms/OBSPermissions.ui rename to frontend/forms/OBSPermissions.ui diff --git a/UI/forms/OBSRemux.ui b/frontend/forms/OBSRemux.ui similarity index 100% rename from UI/forms/OBSRemux.ui rename to frontend/forms/OBSRemux.ui diff --git a/UI/forms/OBSUpdate.ui b/frontend/forms/OBSUpdate.ui similarity index 100% rename from UI/forms/OBSUpdate.ui rename to frontend/forms/OBSUpdate.ui diff --git a/UI/forms/OBSYoutubeActions.ui b/frontend/forms/OBSYoutubeActions.ui similarity index 99% rename from UI/forms/OBSYoutubeActions.ui rename to frontend/forms/OBSYoutubeActions.ui index 3c7ddcc78..1f38008cd 100644 --- a/UI/forms/OBSYoutubeActions.ui +++ b/frontend/forms/OBSYoutubeActions.ui @@ -687,7 +687,7 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
diff --git a/UI/forms/StatusBarWidget.ui b/frontend/forms/StatusBarWidget.ui similarity index 100% rename from UI/forms/StatusBarWidget.ui rename to frontend/forms/StatusBarWidget.ui diff --git a/UI/forms/XML-Schema-Qt5.15.xsd b/frontend/forms/XML-Schema-Qt5.15.xsd similarity index 100% rename from UI/forms/XML-Schema-Qt5.15.xsd rename to frontend/forms/XML-Schema-Qt5.15.xsd diff --git a/UI/forms/fonts/OpenSans-Bold.ttf b/frontend/forms/fonts/OpenSans-Bold.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Bold.ttf rename to frontend/forms/fonts/OpenSans-Bold.ttf diff --git a/UI/forms/fonts/OpenSans-Italic.ttf b/frontend/forms/fonts/OpenSans-Italic.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Italic.ttf rename to frontend/forms/fonts/OpenSans-Italic.ttf diff --git a/UI/forms/fonts/OpenSans-Regular.ttf b/frontend/forms/fonts/OpenSans-Regular.ttf similarity index 100% rename from UI/forms/fonts/OpenSans-Regular.ttf rename to frontend/forms/fonts/OpenSans-Regular.ttf diff --git a/UI/forms/images/active.png b/frontend/forms/images/active.png similarity index 100% rename from UI/forms/images/active.png rename to frontend/forms/images/active.png diff --git a/UI/forms/images/active_mac.png b/frontend/forms/images/active_mac.png similarity index 100% rename from UI/forms/images/active_mac.png rename to frontend/forms/images/active_mac.png diff --git a/UI/forms/images/alert.svg b/frontend/forms/images/alert.svg similarity index 100% rename from UI/forms/images/alert.svg rename to frontend/forms/images/alert.svg diff --git a/UI/forms/images/cogs.svg b/frontend/forms/images/cogs.svg similarity index 100% rename from UI/forms/images/cogs.svg rename to frontend/forms/images/cogs.svg diff --git a/UI/forms/images/collapse.svg b/frontend/forms/images/collapse.svg similarity index 100% rename from UI/forms/images/collapse.svg rename to frontend/forms/images/collapse.svg diff --git a/UI/forms/images/dots-vert.svg b/frontend/forms/images/dots-vert.svg similarity index 100% rename from UI/forms/images/dots-vert.svg rename to frontend/forms/images/dots-vert.svg diff --git a/UI/forms/images/dots.svg b/frontend/forms/images/dots.svg similarity index 100% rename from UI/forms/images/dots.svg rename to frontend/forms/images/dots.svg diff --git a/UI/forms/images/down.svg b/frontend/forms/images/down.svg similarity index 100% rename from UI/forms/images/down.svg rename to frontend/forms/images/down.svg diff --git a/UI/forms/images/entry-clear.svg b/frontend/forms/images/entry-clear.svg similarity index 100% rename from UI/forms/images/entry-clear.svg rename to frontend/forms/images/entry-clear.svg diff --git a/UI/forms/images/expand.svg b/frontend/forms/images/expand.svg similarity index 100% rename from UI/forms/images/expand.svg rename to frontend/forms/images/expand.svg diff --git a/UI/forms/images/filter.svg b/frontend/forms/images/filter.svg similarity index 100% rename from UI/forms/images/filter.svg rename to frontend/forms/images/filter.svg diff --git a/UI/forms/images/help.svg b/frontend/forms/images/help.svg similarity index 100% rename from UI/forms/images/help.svg rename to frontend/forms/images/help.svg diff --git a/UI/forms/images/help_light.svg b/frontend/forms/images/help_light.svg similarity index 100% rename from UI/forms/images/help_light.svg rename to frontend/forms/images/help_light.svg diff --git a/UI/forms/images/interact.svg b/frontend/forms/images/interact.svg similarity index 100% rename from UI/forms/images/interact.svg rename to frontend/forms/images/interact.svg diff --git a/UI/forms/images/invisible.svg b/frontend/forms/images/invisible.svg similarity index 100% rename from UI/forms/images/invisible.svg rename to frontend/forms/images/invisible.svg diff --git a/UI/forms/images/locked.svg b/frontend/forms/images/locked.svg similarity index 100% rename from UI/forms/images/locked.svg rename to frontend/forms/images/locked.svg diff --git a/UI/forms/images/media-pause.svg b/frontend/forms/images/media-pause.svg similarity index 100% rename from UI/forms/images/media-pause.svg rename to frontend/forms/images/media-pause.svg diff --git a/UI/forms/images/media/media_next.svg b/frontend/forms/images/media/media_next.svg similarity index 100% rename from UI/forms/images/media/media_next.svg rename to frontend/forms/images/media/media_next.svg diff --git a/UI/forms/images/media/media_pause.svg b/frontend/forms/images/media/media_pause.svg similarity index 100% rename from UI/forms/images/media/media_pause.svg rename to frontend/forms/images/media/media_pause.svg diff --git a/UI/forms/images/media/media_play.svg b/frontend/forms/images/media/media_play.svg similarity index 100% rename from UI/forms/images/media/media_play.svg rename to frontend/forms/images/media/media_play.svg diff --git a/UI/forms/images/media/media_previous.svg b/frontend/forms/images/media/media_previous.svg similarity index 100% rename from UI/forms/images/media/media_previous.svg rename to frontend/forms/images/media/media_previous.svg diff --git a/UI/forms/images/media/media_restart.svg b/frontend/forms/images/media/media_restart.svg similarity index 100% rename from UI/forms/images/media/media_restart.svg rename to frontend/forms/images/media/media_restart.svg diff --git a/UI/forms/images/media/media_stop.svg b/frontend/forms/images/media/media_stop.svg similarity index 100% rename from UI/forms/images/media/media_stop.svg rename to frontend/forms/images/media/media_stop.svg diff --git a/UI/forms/images/minus.svg b/frontend/forms/images/minus.svg similarity index 100% rename from UI/forms/images/minus.svg rename to frontend/forms/images/minus.svg diff --git a/UI/forms/images/mute.svg b/frontend/forms/images/mute.svg similarity index 100% rename from UI/forms/images/mute.svg rename to frontend/forms/images/mute.svg diff --git a/UI/forms/images/network-bad.svg b/frontend/forms/images/network-bad.svg similarity index 100% rename from UI/forms/images/network-bad.svg rename to frontend/forms/images/network-bad.svg diff --git a/UI/forms/images/network-disconnected.svg b/frontend/forms/images/network-disconnected.svg similarity index 100% rename from UI/forms/images/network-disconnected.svg rename to frontend/forms/images/network-disconnected.svg diff --git a/UI/forms/images/network-excellent.svg b/frontend/forms/images/network-excellent.svg similarity index 100% rename from UI/forms/images/network-excellent.svg rename to frontend/forms/images/network-excellent.svg diff --git a/UI/forms/images/network-good.svg b/frontend/forms/images/network-good.svg similarity index 100% rename from UI/forms/images/network-good.svg rename to frontend/forms/images/network-good.svg diff --git a/UI/forms/images/network-inactive.svg b/frontend/forms/images/network-inactive.svg similarity index 100% rename from UI/forms/images/network-inactive.svg rename to frontend/forms/images/network-inactive.svg diff --git a/UI/forms/images/network-mediocre.svg b/frontend/forms/images/network-mediocre.svg similarity index 100% rename from UI/forms/images/network-mediocre.svg rename to frontend/forms/images/network-mediocre.svg diff --git a/UI/forms/images/no_sources.svg b/frontend/forms/images/no_sources.svg similarity index 100% rename from UI/forms/images/no_sources.svg rename to frontend/forms/images/no_sources.svg diff --git a/UI/forms/images/obs.png b/frontend/forms/images/obs.png similarity index 100% rename from UI/forms/images/obs.png rename to frontend/forms/images/obs.png diff --git a/UI/forms/images/obs_256x256.png b/frontend/forms/images/obs_256x256.png similarity index 100% rename from UI/forms/images/obs_256x256.png rename to frontend/forms/images/obs_256x256.png diff --git a/UI/forms/images/obs_macos.png b/frontend/forms/images/obs_macos.png similarity index 100% rename from UI/forms/images/obs_macos.png rename to frontend/forms/images/obs_macos.png diff --git a/UI/forms/images/obs_macos.svg b/frontend/forms/images/obs_macos.svg similarity index 100% rename from UI/forms/images/obs_macos.svg rename to frontend/forms/images/obs_macos.svg diff --git a/UI/forms/images/obs_paused.png b/frontend/forms/images/obs_paused.png similarity index 100% rename from UI/forms/images/obs_paused.png rename to frontend/forms/images/obs_paused.png diff --git a/UI/forms/images/obs_paused_macos.png b/frontend/forms/images/obs_paused_macos.png similarity index 100% rename from UI/forms/images/obs_paused_macos.png rename to frontend/forms/images/obs_paused_macos.png diff --git a/UI/forms/images/obs_paused_macos.svg b/frontend/forms/images/obs_paused_macos.svg similarity index 100% rename from UI/forms/images/obs_paused_macos.svg rename to frontend/forms/images/obs_paused_macos.svg diff --git a/UI/forms/images/paused.png b/frontend/forms/images/paused.png similarity index 100% rename from UI/forms/images/paused.png rename to frontend/forms/images/paused.png diff --git a/UI/forms/images/paused_mac.png b/frontend/forms/images/paused_mac.png similarity index 100% rename from UI/forms/images/paused_mac.png rename to frontend/forms/images/paused_mac.png diff --git a/UI/forms/images/plus.svg b/frontend/forms/images/plus.svg similarity index 100% rename from UI/forms/images/plus.svg rename to frontend/forms/images/plus.svg diff --git a/UI/forms/images/recording-active.svg b/frontend/forms/images/recording-active.svg similarity index 100% rename from UI/forms/images/recording-active.svg rename to frontend/forms/images/recording-active.svg diff --git a/UI/forms/images/recording-inactive.svg b/frontend/forms/images/recording-inactive.svg similarity index 100% rename from UI/forms/images/recording-inactive.svg rename to frontend/forms/images/recording-inactive.svg diff --git a/UI/forms/images/recording-pause-inactive.svg b/frontend/forms/images/recording-pause-inactive.svg similarity index 100% rename from UI/forms/images/recording-pause-inactive.svg rename to frontend/forms/images/recording-pause-inactive.svg diff --git a/UI/forms/images/recording-pause.svg b/frontend/forms/images/recording-pause.svg similarity index 100% rename from UI/forms/images/recording-pause.svg rename to frontend/forms/images/recording-pause.svg diff --git a/UI/forms/images/refresh.svg b/frontend/forms/images/refresh.svg similarity index 100% rename from UI/forms/images/refresh.svg rename to frontend/forms/images/refresh.svg diff --git a/UI/forms/images/revert.svg b/frontend/forms/images/revert.svg similarity index 100% rename from UI/forms/images/revert.svg rename to frontend/forms/images/revert.svg diff --git a/UI/forms/images/right.svg b/frontend/forms/images/right.svg similarity index 100% rename from UI/forms/images/right.svg rename to frontend/forms/images/right.svg diff --git a/UI/forms/images/save.svg b/frontend/forms/images/save.svg similarity index 100% rename from UI/forms/images/save.svg rename to frontend/forms/images/save.svg diff --git a/UI/forms/images/settings/accessibility.svg b/frontend/forms/images/settings/accessibility.svg similarity index 100% rename from UI/forms/images/settings/accessibility.svg rename to frontend/forms/images/settings/accessibility.svg diff --git a/UI/forms/images/settings/advanced.svg b/frontend/forms/images/settings/advanced.svg similarity index 100% rename from UI/forms/images/settings/advanced.svg rename to frontend/forms/images/settings/advanced.svg diff --git a/UI/forms/images/settings/appearance.svg b/frontend/forms/images/settings/appearance.svg similarity index 100% rename from UI/forms/images/settings/appearance.svg rename to frontend/forms/images/settings/appearance.svg diff --git a/UI/forms/images/settings/audio.svg b/frontend/forms/images/settings/audio.svg similarity index 100% rename from UI/forms/images/settings/audio.svg rename to frontend/forms/images/settings/audio.svg diff --git a/UI/forms/images/settings/general.svg b/frontend/forms/images/settings/general.svg similarity index 100% rename from UI/forms/images/settings/general.svg rename to frontend/forms/images/settings/general.svg diff --git a/UI/forms/images/settings/hotkeys.svg b/frontend/forms/images/settings/hotkeys.svg similarity index 100% rename from UI/forms/images/settings/hotkeys.svg rename to frontend/forms/images/settings/hotkeys.svg diff --git a/UI/forms/images/settings/output.svg b/frontend/forms/images/settings/output.svg similarity index 100% rename from UI/forms/images/settings/output.svg rename to frontend/forms/images/settings/output.svg diff --git a/UI/forms/images/settings/stream.svg b/frontend/forms/images/settings/stream.svg similarity index 100% rename from UI/forms/images/settings/stream.svg rename to frontend/forms/images/settings/stream.svg diff --git a/UI/forms/images/settings/video.svg b/frontend/forms/images/settings/video.svg similarity index 100% rename from UI/forms/images/settings/video.svg rename to frontend/forms/images/settings/video.svg diff --git a/UI/forms/images/sources/brush.svg b/frontend/forms/images/sources/brush.svg similarity index 100% rename from UI/forms/images/sources/brush.svg rename to frontend/forms/images/sources/brush.svg diff --git a/UI/forms/images/sources/camera.svg b/frontend/forms/images/sources/camera.svg similarity index 100% rename from UI/forms/images/sources/camera.svg rename to frontend/forms/images/sources/camera.svg diff --git a/UI/forms/images/sources/default.svg b/frontend/forms/images/sources/default.svg similarity index 100% rename from UI/forms/images/sources/default.svg rename to frontend/forms/images/sources/default.svg diff --git a/UI/forms/images/sources/gamepad.svg b/frontend/forms/images/sources/gamepad.svg similarity index 100% rename from UI/forms/images/sources/gamepad.svg rename to frontend/forms/images/sources/gamepad.svg diff --git a/UI/forms/images/sources/globe.svg b/frontend/forms/images/sources/globe.svg similarity index 100% rename from UI/forms/images/sources/globe.svg rename to frontend/forms/images/sources/globe.svg diff --git a/UI/forms/images/sources/group.svg b/frontend/forms/images/sources/group.svg similarity index 100% rename from UI/forms/images/sources/group.svg rename to frontend/forms/images/sources/group.svg diff --git a/UI/forms/images/sources/image.svg b/frontend/forms/images/sources/image.svg similarity index 100% rename from UI/forms/images/sources/image.svg rename to frontend/forms/images/sources/image.svg diff --git a/UI/forms/images/sources/media.svg b/frontend/forms/images/sources/media.svg similarity index 100% rename from UI/forms/images/sources/media.svg rename to frontend/forms/images/sources/media.svg diff --git a/UI/forms/images/sources/microphone.svg b/frontend/forms/images/sources/microphone.svg similarity index 100% rename from UI/forms/images/sources/microphone.svg rename to frontend/forms/images/sources/microphone.svg diff --git a/UI/forms/images/sources/scene.svg b/frontend/forms/images/sources/scene.svg similarity index 100% rename from UI/forms/images/sources/scene.svg rename to frontend/forms/images/sources/scene.svg diff --git a/UI/forms/images/sources/slideshow.svg b/frontend/forms/images/sources/slideshow.svg similarity index 100% rename from UI/forms/images/sources/slideshow.svg rename to frontend/forms/images/sources/slideshow.svg diff --git a/UI/forms/images/sources/text.svg b/frontend/forms/images/sources/text.svg similarity index 100% rename from UI/forms/images/sources/text.svg rename to frontend/forms/images/sources/text.svg diff --git a/UI/forms/images/sources/window.svg b/frontend/forms/images/sources/window.svg similarity index 100% rename from UI/forms/images/sources/window.svg rename to frontend/forms/images/sources/window.svg diff --git a/UI/forms/images/sources/windowaudio.svg b/frontend/forms/images/sources/windowaudio.svg similarity index 100% rename from UI/forms/images/sources/windowaudio.svg rename to frontend/forms/images/sources/windowaudio.svg diff --git a/UI/forms/images/streaming-active.svg b/frontend/forms/images/streaming-active.svg similarity index 100% rename from UI/forms/images/streaming-active.svg rename to frontend/forms/images/streaming-active.svg diff --git a/UI/forms/images/streaming-inactive.svg b/frontend/forms/images/streaming-inactive.svg similarity index 100% rename from UI/forms/images/streaming-inactive.svg rename to frontend/forms/images/streaming-inactive.svg diff --git a/UI/forms/images/trash.svg b/frontend/forms/images/trash.svg similarity index 100% rename from UI/forms/images/trash.svg rename to frontend/forms/images/trash.svg diff --git a/UI/forms/images/tray_active.png b/frontend/forms/images/tray_active.png similarity index 100% rename from UI/forms/images/tray_active.png rename to frontend/forms/images/tray_active.png diff --git a/UI/forms/images/tray_active_macos.png b/frontend/forms/images/tray_active_macos.png similarity index 100% rename from UI/forms/images/tray_active_macos.png rename to frontend/forms/images/tray_active_macos.png diff --git a/UI/forms/images/tray_active_macos.svg b/frontend/forms/images/tray_active_macos.svg similarity index 100% rename from UI/forms/images/tray_active_macos.svg rename to frontend/forms/images/tray_active_macos.svg diff --git a/UI/forms/images/unassigned.svg b/frontend/forms/images/unassigned.svg similarity index 100% rename from UI/forms/images/unassigned.svg rename to frontend/forms/images/unassigned.svg diff --git a/UI/forms/images/unlocked.svg b/frontend/forms/images/unlocked.svg similarity index 100% rename from UI/forms/images/unlocked.svg rename to frontend/forms/images/unlocked.svg diff --git a/UI/forms/images/up.svg b/frontend/forms/images/up.svg similarity index 100% rename from UI/forms/images/up.svg rename to frontend/forms/images/up.svg diff --git a/UI/forms/images/visible.svg b/frontend/forms/images/visible.svg similarity index 100% rename from UI/forms/images/visible.svg rename to frontend/forms/images/visible.svg diff --git a/UI/forms/images/warning.svg b/frontend/forms/images/warning.svg similarity index 100% rename from UI/forms/images/warning.svg rename to frontend/forms/images/warning.svg diff --git a/UI/forms/obs.qrc b/frontend/forms/obs.qrc similarity index 100% rename from UI/forms/obs.qrc rename to frontend/forms/obs.qrc diff --git a/UI/forms/source-toolbar/browser-source-toolbar.ui b/frontend/forms/source-toolbar/browser-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/browser-source-toolbar.ui rename to frontend/forms/source-toolbar/browser-source-toolbar.ui diff --git a/UI/forms/source-toolbar/color-source-toolbar.ui b/frontend/forms/source-toolbar/color-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/color-source-toolbar.ui rename to frontend/forms/source-toolbar/color-source-toolbar.ui diff --git a/UI/forms/source-toolbar/device-select-toolbar.ui b/frontend/forms/source-toolbar/device-select-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/device-select-toolbar.ui rename to frontend/forms/source-toolbar/device-select-toolbar.ui diff --git a/UI/forms/source-toolbar/game-capture-toolbar.ui b/frontend/forms/source-toolbar/game-capture-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/game-capture-toolbar.ui rename to frontend/forms/source-toolbar/game-capture-toolbar.ui diff --git a/UI/forms/source-toolbar/image-source-toolbar.ui b/frontend/forms/source-toolbar/image-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/image-source-toolbar.ui rename to frontend/forms/source-toolbar/image-source-toolbar.ui diff --git a/UI/forms/source-toolbar/media-controls.ui b/frontend/forms/source-toolbar/media-controls.ui similarity index 98% rename from UI/forms/source-toolbar/media-controls.ui rename to frontend/forms/source-toolbar/media-controls.ui index 1a2a47f2b..b6886e7e2 100644 --- a/UI/forms/source-toolbar/media-controls.ui +++ b/frontend/forms/source-toolbar/media-controls.ui @@ -285,12 +285,12 @@ ClickableLabel QLabel -
clickable-label.hpp
+
components/ClickableLabel.hpp
AbsoluteSlider QSlider -
absolute-slider.hpp
+
components/AbsoluteSlider.hpp
diff --git a/UI/forms/source-toolbar/text-source-toolbar.ui b/frontend/forms/source-toolbar/text-source-toolbar.ui similarity index 100% rename from UI/forms/source-toolbar/text-source-toolbar.ui rename to frontend/forms/source-toolbar/text-source-toolbar.ui diff --git a/frontend/importer/ImporterEntryPathItemDelegate.cpp b/frontend/importer/ImporterEntryPathItemDelegate.cpp new file mode 100644 index 000000000..d6b3b8f7c --- /dev/null +++ b/frontend/importer/ImporterEntryPathItemDelegate.cpp @@ -0,0 +1,160 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "ImporterEntryPathItemDelegate.hpp" +#include "ImporterModel.hpp" + +#include + +#include + +#include +#include +#include + +#include "moc_ImporterEntryPathItemDelegate.cpp" + +ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} + +QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + bool empty = index.model() + ->index(index.row(), ImporterColumn::Path) + .data(ImporterEntryRole::CheckEmpty) + .value(); + + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; +} + +void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + model->setData(index, list, ImporterEntryRole::NewPath); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + + bool isSet = false; + QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } + + if (isSet) + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} diff --git a/UI/window-importer.hpp b/frontend/importer/ImporterEntryPathItemDelegate.hpp similarity index 55% rename from UI/window-importer.hpp rename to frontend/importer/ImporterEntryPathItemDelegate.hpp index 8e77db645..61304c9cf 100644 --- a/UI/window-importer.hpp +++ b/frontend/importer/ImporterEntryPathItemDelegate.hpp @@ -17,65 +17,7 @@ #pragma once -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include #include -#include -#include "ui_OBSImporter.h" - -class ImporterModel; - -class OBSImporter : public QDialog { - Q_OBJECT - - QPointer optionsModel; - std::unique_ptr ui; - -public: - explicit OBSImporter(QWidget *parent = nullptr); - - void addImportOption(QString path, bool automatic); - -protected: - virtual void dropEvent(QDropEvent *ev) override; - virtual void dragEnterEvent(QDragEnterEvent *ev) override; - -public slots: - void browseImport(); - void importCollections(); - void dataChanged(); -}; - -class ImporterModel : public QAbstractTableModel { - Q_OBJECT - - friend class OBSImporter; - -public: - ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - -private: - struct ImporterEntry { - QString path; - QString program; - QString name; - - bool selected; - bool empty; - }; - - QList options; - - void checkInputPath(int row); -}; class ImporterEntryPathItemDelegate : public QStyledItemDelegate { Q_OBJECT diff --git a/frontend/importer/ImporterModel.cpp b/frontend/importer/ImporterModel.cpp new file mode 100644 index 000000000..0c6e22deb --- /dev/null +++ b/frontend/importer/ImporterModel.cpp @@ -0,0 +1,206 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "ImporterModel.hpp" + +#include +#include + +#include "moc_ImporterModel.cpp" + +int ImporterModel::rowCount(const QModelIndex &) const +{ + return options.length() + 1; +} + +int ImporterModel::columnCount(const QModelIndex &) const +{ + return ImporterColumn::Count; +} + +QVariant ImporterModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= options.length()) { + if (role == ImporterEntryRole::CheckEmpty) + result = true; + else + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case ImporterColumn::Path: + result = options[index.row()].path; + break; + case ImporterColumn::Program: + result = options[index.row()].program; + break; + case ImporterColumn::Name: + result = options[index.row()].name; + } + } else if (role == Qt::EditRole) { + if (index.column() == ImporterColumn::Name) { + result = options[index.row()].name; + } + } else if (role == Qt::CheckStateRole) { + switch (index.column()) { + case ImporterColumn::Selected: + if (options[index.row()].program != "") + result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; + else + result = Qt::Unchecked; + } + } else if (role == ImporterEntryRole::CheckEmpty) { + result = options[index.row()].empty; + } + + return result; +} + +Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { + flags |= Qt::ItemIsUserCheckable; + } else if (index.column() == ImporterColumn::Path || + (index.column() == ImporterColumn::Name && index.row() != options.length())) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void ImporterModel::checkInputPath(int row) +{ + ImporterEntry &entry = options[row]; + + if (entry.path.isEmpty()) { + entry.program = ""; + entry.empty = true; + entry.selected = false; + entry.name = ""; + } else { + entry.empty = false; + + std::string program = DetectProgram(entry.path.toStdString()); + entry.program = QTStr(program.c_str()); + + if (program.empty()) { + entry.selected = false; + } else { + std::string name = GetSCName(entry.path.toStdString(), program); + entry.name = name.c_str(); + } + } + + emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); +} + +bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == ImporterEntryRole::NewPath) { + QStringList list = value.toStringList(); + + if (list.size() == 0) { + if (index.row() < options.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + options.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (list.size() > 0 && index.row() < options.length()) { + options[index.row()].path = list[0]; + checkInputPath(index.row()); + + list.removeAt(0); + } + + if (list.size() > 0) { + int row = index.row(); + int lastRow = row + list.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : list) { + ImporterEntry entry; + entry.path = path; + + options.insert(row, entry); + + row++; + } + + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + } + } + } else if (index.row() == options.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + ImporterEntry entry; + entry.path = path; + entry.selected = role != ImporterEntryRole::AutoPath; + entry.empty = false; + + beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); + options.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + } + } else if (index.column() == ImporterColumn::Selected) { + bool select = value.toBool(); + + options[index.row()].selected = select; + } else if (index.column() == ImporterColumn::Path) { + QString path = value.toString(); + options[index.row()].path = path; + + checkInputPath(index.row()); + } else if (index.column() == ImporterColumn::Name) { + QString name = value.toString(); + options[index.row()].name = name; + } + + emit dataChanged(index, index); + + return true; +} + +QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case ImporterColumn::Path: + result = QTStr("Importer.Path"); + break; + case ImporterColumn::Program: + result = QTStr("Importer.Program"); + break; + case ImporterColumn::Name: + result = QTStr("Name"); + } + } + + return result; +} diff --git a/frontend/importer/ImporterModel.hpp b/frontend/importer/ImporterModel.hpp new file mode 100644 index 000000000..011ac83eb --- /dev/null +++ b/frontend/importer/ImporterModel.hpp @@ -0,0 +1,61 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +enum ImporterColumn { + Selected, + Name, + Path, + Program, + + Count +}; + +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + +class ImporterModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSImporter; + +public: + ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + +private: + struct ImporterEntry { + QString path; + QString program; + QString name; + + bool selected; + bool empty; + }; + + QList options; + + void checkInputPath(int row); +}; diff --git a/frontend/importer/OBSImporter.cpp b/frontend/importer/OBSImporter.cpp new file mode 100644 index 000000000..c33a095c6 --- /dev/null +++ b/frontend/importer/OBSImporter.cpp @@ -0,0 +1,232 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSImporter.hpp" +#include "ImporterEntryPathItemDelegate.hpp" +#include "ImporterModel.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "moc_OBSImporter.cpp" +OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(optionsModel); + ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); + + connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); + + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); + ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, + &OBSImporter::importCollections); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); + + ImportersInit(); + + bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); + + if (!autoSearchPrompt) { + QMessageBox::StandardButton button = OBSMessageBox::question( + parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); + + if (button == QMessageBox::Yes) { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); + } else { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); + } + + config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); + } + + bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + + OBSImporterFiles f; + if (autoSearch) + f = ImportersFindFiles(); + + for (size_t i = 0; i < f.size(); i++) { + QString path = f[i].c_str(); + path.replace("\\", "/"); + addImportOption(path, true); + } + + f.clear(); + + ui->tableView->resizeColumnsToContents(); + + QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +void OBSImporter::addImportOption(QString path, bool automatic) +{ + QStringList list; + + list.append(path); + + QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); + + optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); +} + +void OBSImporter::dropEvent(QDropEvent *ev) +{ + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + if (fileInfo.isDir()) { + + QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); + + while (dirIter.hasNext()) { + addImportOption(dirIter.next(), false); + } + } else { + addImportOption(fileInfo.canonicalFilePath(), false); + } + } +} + +void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls()) + ev->accept(); +} + +void OBSImporter::browseImport() +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + for (int i = 0; i < paths.count(); i++) { + addImportOption(paths[i], false); + } + } +} + +bool GetUnusedName(std::string &name) +{ + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { + return false; + } + + std::string newName; + int inc = 2; + do { + newName = name; + newName += " "; + newName += std::to_string(inc++); + } while (basic->GetSceneCollectionByName(newName)); + + name = newName; + return true; +} + +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + +void OBSImporter::importCollections() +{ + setEnabled(false); + + const std::filesystem::path sceneCollectionLocation = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); + + for (int i = 0; i < optionsModel->rowCount() - 1; i++) { + int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); + + if (selected == Qt::Unchecked) + continue; + + std::string pathStr = optionsModel->index(i, ImporterColumn::Path) + .data(Qt::DisplayRole) + .value() + .toStdString(); + std::string nameStr = optionsModel->index(i, ImporterColumn::Name) + .data(Qt::DisplayRole) + .value() + .toStdString(); + + json11::Json res; + ImportSC(pathStr, nameStr, res); + + if (res != json11::Json()) { + json11::Json::object out = res.object_items(); + std::string name = res["name"].string_value(); + std::string file; + + if (GetUnusedName(name)) { + json11::Json::object newOut = out; + newOut["name"] = name; + out = newOut; + } + + std::string fileName; + if (!GetFileSafeName(name.c_str(), fileName)) { + blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); + } + + std::string collectionFile; + collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); + collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); + } + + std::string out_str = json11::Json(out).dump(); + + bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), + false); + + blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), + success ? "SUCCESS" : "FAILURE"); + } + } + + close(); +} + +void OBSImporter::dataChanged() +{ + ui->tableView->resizeColumnToContents(ImporterColumn::Name); +} diff --git a/frontend/importer/OBSImporter.hpp b/frontend/importer/OBSImporter.hpp new file mode 100644 index 000000000..d9ab2bb6f --- /dev/null +++ b/frontend/importer/OBSImporter.hpp @@ -0,0 +1,46 @@ +/****************************************************************************** + Copyright (C) 2019-2020 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "ui_OBSImporter.h" + +#include +#include + +class ImporterModel; + +class OBSImporter : public QDialog { + Q_OBJECT + + QPointer optionsModel; + std::unique_ptr ui; + +public: + explicit OBSImporter(QWidget *parent = nullptr); + + void addImportOption(QString path, bool automatic); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + +public slots: + void browseImport(); + void importCollections(); + void dataChanged(); +}; diff --git a/UI/importers/classic.cpp b/frontend/importers/classic.cpp similarity index 100% rename from UI/importers/classic.cpp rename to frontend/importers/classic.cpp diff --git a/UI/importers/importers.cpp b/frontend/importers/importers.cpp similarity index 100% rename from UI/importers/importers.cpp rename to frontend/importers/importers.cpp diff --git a/UI/importers/importers.hpp b/frontend/importers/importers.hpp similarity index 100% rename from UI/importers/importers.hpp rename to frontend/importers/importers.hpp diff --git a/UI/importers/sl.cpp b/frontend/importers/sl.cpp similarity index 100% rename from UI/importers/sl.cpp rename to frontend/importers/sl.cpp diff --git a/UI/importers/studio.cpp b/frontend/importers/studio.cpp similarity index 100% rename from UI/importers/studio.cpp rename to frontend/importers/studio.cpp diff --git a/UI/importers/xsplit.cpp b/frontend/importers/xsplit.cpp similarity index 100% rename from UI/importers/xsplit.cpp rename to frontend/importers/xsplit.cpp diff --git a/UI/auth-base.cpp b/frontend/oauth/Auth.cpp similarity index 94% rename from UI/auth-base.cpp rename to frontend/oauth/Auth.cpp index 1f0999fe0..89b579f8e 100644 --- a/UI/auth-base.cpp +++ b/frontend/oauth/Auth.cpp @@ -1,8 +1,8 @@ -#include "moc_auth-base.cpp" -#include "window-basic-main.hpp" +#include "Auth.hpp" -#include -#include +#include + +#include "moc_Auth.cpp" struct AuthInfo { Auth::Def def; diff --git a/UI/auth-base.hpp b/frontend/oauth/Auth.hpp similarity index 96% rename from UI/auth-base.hpp rename to frontend/oauth/Auth.hpp index 9745f63fe..c983c5463 100644 --- a/UI/auth-base.hpp +++ b/frontend/oauth/Auth.hpp @@ -1,8 +1,6 @@ #pragma once #include -#include -#include class Auth : public QObject { Q_OBJECT diff --git a/UI/auth-listener.cpp b/frontend/oauth/AuthListener.cpp similarity index 95% rename from UI/auth-listener.cpp rename to frontend/oauth/AuthListener.cpp index e652da4d3..9298a18e5 100644 --- a/UI/auth-listener.cpp +++ b/frontend/oauth/AuthListener.cpp @@ -1,12 +1,14 @@ -#include "moc_auth-listener.cpp" +#include "AuthListener.hpp" + +#include -#include -#include -#include -#include #include -#include "obs-app.hpp" +#include +#include +#include + +#include "moc_AuthListener.cpp" #define LOGO_URL "https://obsproject.com/assets/images/new_icon_small-r.png" diff --git a/UI/auth-listener.hpp b/frontend/oauth/AuthListener.hpp similarity index 90% rename from UI/auth-listener.hpp rename to frontend/oauth/AuthListener.hpp index fa1db2dea..65bcf540e 100644 --- a/UI/auth-listener.hpp +++ b/frontend/oauth/AuthListener.hpp @@ -1,7 +1,8 @@ #pragma once #include -#include + +class QTcpServer; class AuthListener : public QObject { Q_OBJECT diff --git a/UI/auth-oauth.cpp b/frontend/oauth/OAuth.cpp similarity index 70% rename from UI/auth-oauth.cpp rename to frontend/oauth/OAuth.cpp index 04458c9c5..a7d6f60d6 100644 --- a/UI/auth-oauth.cpp +++ b/frontend/oauth/OAuth.cpp @@ -1,124 +1,17 @@ -#include "moc_auth-oauth.cpp" +#include "OAuth.hpp" -#include -#include -#include +#include +#include #include -#include - -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include +#include #include -#include "ui-config.h" +#include "moc_OAuth.cpp" using namespace json11; -#ifdef BROWSER_AVAILABLE -#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; -#endif - -/* ------------------------------------------------------------------------- */ - -OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) -{ -#ifdef BROWSER_AVAILABLE - if (!cef) { - return; - } - - setWindowTitle("Auth"); - setMinimumSize(400, 400); - resize(700, 700); - - Qt::WindowFlags flags = windowFlags(); - Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; - setWindowFlags(flags & (~helpFlag)); - - OBSBasic::InitBrowserPanelSafeBlock(); - - cefWidget = cef->create_widget(nullptr, url, panel_cookies); - if (!cefWidget) { - fail = true; - return; - } - - connect(cefWidget, &QCefWidget::titleChanged, this, &OAuthLogin::setWindowTitle); - connect(cefWidget, &QCefWidget::urlChanged, this, &OAuthLogin::urlChanged); - - QPushButton *close = new QPushButton(QTStr("Cancel")); - connect(close, &QAbstractButton::clicked, this, &QDialog::reject); - - QHBoxLayout *bottomLayout = new QHBoxLayout(); - bottomLayout->addStretch(); - bottomLayout->addWidget(close); - bottomLayout->addStretch(); - - QVBoxLayout *topLayout = new QVBoxLayout(this); - topLayout->addWidget(cefWidget); - topLayout->addLayout(bottomLayout); -#else - UNUSED_PARAMETER(url); -#endif -} - -OAuthLogin::~OAuthLogin() {} - -int OAuthLogin::exec() -{ -#ifdef BROWSER_AVAILABLE - if (cefWidget) { - return QDialog::exec(); - } -#endif - return QDialog::Rejected; -} - -void OAuthLogin::reject() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::reject(); -} - -void OAuthLogin::accept() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::accept(); -} - -void OAuthLogin::urlChanged(const QString &url) -{ - std::string uri = get_token ? "access_token=" : "code="; - int code_idx = url.indexOf(uri.c_str()); - if (code_idx == -1) - return; - - if (!url.startsWith(OAUTH_BASE_URL)) - return; - - code_idx += (int)uri.size(); - - int next_idx = url.indexOf("&", code_idx); - if (next_idx != -1) - code = url.mid(code_idx, next_idx - code_idx); - else - code = url.right(url.size() - code_idx); - - accept(); -} - -/* ------------------------------------------------------------------------- */ - struct OAuthInfo { Auth::Def def; OAuth::login_cb login; @@ -126,7 +19,6 @@ struct OAuthInfo { }; static std::vector loginCBs; - void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, delete_cookies_cb delete_cookies) { OAuthInfo info = {d, login, delete_cookies}; diff --git a/UI/auth-oauth.hpp b/frontend/oauth/OAuth.hpp similarity index 72% rename from UI/auth-oauth.hpp rename to frontend/oauth/OAuth.hpp index b6602e815..46bd720ca 100644 --- a/UI/auth-oauth.hpp +++ b/frontend/oauth/OAuth.hpp @@ -1,35 +1,6 @@ #pragma once -#include -#include -#include - -#include "auth-base.hpp" - -class QCefWidget; - -class OAuthLogin : public QDialog { - Q_OBJECT - - QCefWidget *cefWidget = nullptr; - QString code; - bool get_token = false; - bool fail = false; - -public: - OAuthLogin(QWidget *parent, const std::string &url, bool token); - ~OAuthLogin(); - - inline QString GetCode() const { return code; } - inline bool LoadFail() const { return fail; } - - virtual int exec() override; - virtual void reject() override; - virtual void accept() override; - -public slots: - void urlChanged(const QString &url); -}; +#include "Auth.hpp" class OAuth : public Auth { Q_OBJECT diff --git a/UI/auth-restream.cpp b/frontend/oauth/RestreamAuth.cpp similarity index 96% rename from UI/auth-restream.cpp rename to frontend/oauth/RestreamAuth.cpp index c832ee83d..e428e889d 100644 --- a/UI/auth-restream.cpp +++ b/frontend/oauth/RestreamAuth.cpp @@ -1,19 +1,17 @@ -#include "moc_auth-restream.cpp" +#include "RestreamAuth.hpp" + +#include +#include +#include +#include +#include -#include -#include -#include #include -#include -#include -#include +#include -#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" +#include + +#include "moc_RestreamAuth.cpp" using namespace json11; diff --git a/UI/auth-restream.hpp b/frontend/oauth/RestreamAuth.hpp similarity index 90% rename from UI/auth-restream.hpp rename to frontend/oauth/RestreamAuth.hpp index 16c77e813..6ea9128a1 100644 --- a/UI/auth-restream.hpp +++ b/frontend/oauth/RestreamAuth.hpp @@ -1,8 +1,6 @@ #pragma once -#include "auth-oauth.hpp" - -class BrowserDock; +#include "OAuth.hpp" class RestreamAuth : public OAuthStreamKey { Q_OBJECT diff --git a/UI/auth-twitch.cpp b/frontend/oauth/TwitchAuth.cpp similarity index 97% rename from UI/auth-twitch.cpp rename to frontend/oauth/TwitchAuth.cpp index 40f70b567..c6852e29d 100644 --- a/UI/auth-twitch.cpp +++ b/frontend/oauth/TwitchAuth.cpp @@ -1,22 +1,17 @@ -#include "moc_auth-twitch.cpp" +#include "TwitchAuth.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include -#include +#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" +#include -#include - -#include "ui-config.h" -#include "obf.h" +#include "moc_TwitchAuth.cpp" using namespace json11; diff --git a/UI/auth-twitch.hpp b/frontend/oauth/TwitchAuth.hpp similarity index 86% rename from UI/auth-twitch.hpp rename to frontend/oauth/TwitchAuth.hpp index 9fb3ba773..bbd8ca4b9 100644 --- a/UI/auth-twitch.hpp +++ b/frontend/oauth/TwitchAuth.hpp @@ -1,14 +1,10 @@ #pragma once -#include -#include -#include -#include +#include "OAuth.hpp" #include -#include "auth-oauth.hpp" -class BrowserDock; +#include class TwitchAuth : public OAuthStreamKey { Q_OBJECT diff --git a/UI/auth-youtube.cpp b/frontend/oauth/YoutubeAuth.cpp similarity index 86% rename from UI/auth-youtube.cpp rename to frontend/oauth/YoutubeAuth.cpp index 714003145..9d626bb7e 100644 --- a/UI/auth-youtube.cpp +++ b/frontend/oauth/YoutubeAuth.cpp @@ -1,36 +1,21 @@ -#include "moc_auth-youtube.cpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include - -#pragma comment(lib, "shell32") -#endif - -#include "auth-listener.hpp" -#include "obs-app.hpp" -#include "ui-config.h" -#include "youtube-api-wrappers.hpp" -#include "window-basic-main.hpp" -#include "obf.h" +#include "YoutubeAuth.hpp" #ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" +#include #endif +#include +#include +#include +#include -using namespace json11; +#include +#include + +#include +#include + +#include "moc_YoutubeAuth.cpp" -/* ------------------------------------------------------------------------- */ #define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" #define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" #define YOUTUBE_SCOPE_VERSION 1 @@ -318,26 +303,3 @@ std::shared_ptr YoutubeAuth::Login(QWidget *owner, const std::string &serv config_save_safe(config, "tmp", nullptr); return auth; } - -#ifdef BROWSER_AVAILABLE -void YoutubeChatDock::YoutubeCookieCheck() -{ - QPointer this_ = this; - auto cb = [this_](bool currentlyLoggedIn) { - bool previouslyLoggedIn = this_->isLoggedIn; - this_->isLoggedIn = currentlyLoggedIn; - bool loginStateChanged = (currentlyLoggedIn && !previouslyLoggedIn) || - (!currentlyLoggedIn && previouslyLoggedIn); - if (loginStateChanged) { - OBSBasic *main = OBSBasic::Get(); - if (main->GetYouTubeAppDock() != nullptr) { - QMetaObject::invokeMethod(main->GetYouTubeAppDock(), "SettingsUpdated", - Qt::QueuedConnection, Q_ARG(bool, !currentlyLoggedIn)); - } - } - }; - if (panel_cookies) { - panel_cookies->CheckForCookie("https://www.youtube.com", "SID", cb); - } -} -#endif diff --git a/UI/auth-youtube.hpp b/frontend/oauth/YoutubeAuth.hpp similarity index 61% rename from UI/auth-youtube.hpp rename to frontend/oauth/YoutubeAuth.hpp index cd560db7d..cecc5f2ac 100644 --- a/UI/auth-youtube.hpp +++ b/frontend/oauth/YoutubeAuth.hpp @@ -1,38 +1,15 @@ #pragma once -#include -#include -#include -#include - -#include "auth-oauth.hpp" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#include -class YoutubeChatDock : public BrowserDock { - Q_OBJECT - -private: - bool isLoggedIn; - -public: - YoutubeChatDock(const QString &title) : BrowserDock(title) {} - - inline void SetWidget(QCefWidget *widget_) - { - BrowserDock::SetWidget(widget_); - QWidget::connect(cefWidget.get(), &QCefWidget::urlChanged, this, &YoutubeChatDock::YoutubeCookieCheck); - } -private slots: - void YoutubeCookieCheck(); -}; -#endif +#include "OAuth.hpp" inline const std::vector youtubeServices = {{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; +#ifdef BROWSER_AVAILABLE +class YoutubeChatDock; +#endif + class YoutubeAuth : public OAuthStreamKey { Q_OBJECT diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp new file mode 100644 index 000000000..987dca5bc --- /dev/null +++ b/frontend/obs-main.cpp @@ -0,0 +1,1122 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#ifdef __APPLE__ +#include +#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include +#include + +#include +#include +#include +#ifdef _WIN32 +#include +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + +using namespace std; + +static log_handler_t def_log_handler; + +extern string currentLogFile; +extern string lastLogFile; +extern string lastCrashLogFile; + +bool portable_mode = false; +bool steam = false; +bool safe_mode = false; +bool disable_3p_plugins = false; +static bool unclean_shutdown = false; +static bool disable_shutdown_check = false; +static bool multi = false; +static bool log_verbose = false; +static bool unfiltered_log = false; +bool opt_start_streaming = false; +bool opt_start_recording = false; +bool opt_studio_mode = false; +bool opt_start_replaybuffer = false; +bool opt_start_virtualcam = false; +bool opt_minimize_tray = false; +bool opt_allow_opengl = false; +bool opt_always_on_top = false; +bool opt_disable_updater = false; +bool opt_disable_missing_files_check = false; +string opt_starting_collection; +string opt_starting_profile; +string opt_starting_scene; + +bool restart = false; +bool restart_safe = false; +static QStringList arguments; + +QPointer obsLogViewer; + +string CurrentTimeString() +{ + using namespace std::chrono; + + struct tm tstruct; + char buf[80]; + + auto tp = system_clock::now(); + auto now = system_clock::to_time_t(tp); + tstruct = *localtime(&now); + + size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); + if (ratio_less::value && written && (sizeof(buf) - written) > 5) { + auto tp_secs = time_point_cast(tp); + auto millis = duration_cast(tp - tp_secs).count(); + + snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); + } + + return buf; +} + +static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) +{ + static mutex logfile_mutex; + string msg; + msg += timeString; + msg += str; + + logfile_mutex.lock(); + logFile << msg << endl; + logfile_mutex.unlock(); + + if (!!obsLogViewer) + QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), + Q_ARG(QString, QString(msg.c_str()))); +} + +static inline void LogStringChunk(fstream &logFile, char *str, int log_level) +{ + char *nextLine = str; + string timeString = CurrentTimeString(); + timeString += ": "; + + while (*nextLine) { + char *nextLine = strchr(str, '\n'); + if (!nextLine) + break; + + if (nextLine != str && nextLine[-1] == '\r') { + nextLine[-1] = 0; + } else { + nextLine[0] = 0; + } + + LogString(logFile, timeString.c_str(), str, log_level); + nextLine++; + str = nextLine; + } + + LogString(logFile, timeString.c_str(), str, log_level); +} + +#define MAX_REPEATED_LINES 30 +#define MAX_CHAR_VARIATION (255 * 3) + +static inline int sum_chars(const char *str) +{ + int val = 0; + for (; *str != 0; str++) + val += *str; + + return val; +} + +static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) +{ + static mutex log_mutex; + static const char *last_msg_ptr = nullptr; + static int last_char_sum = 0; + static int rep_count = 0; + + int new_sum = sum_chars(output_str); + + lock_guard guard(log_mutex); + + if (unfiltered_log) { + return false; + } + + if (last_msg_ptr == msg) { + int diff = std::abs(new_sum - last_char_sum); + if (diff < MAX_CHAR_VARIATION) { + return (rep_count++ >= MAX_REPEATED_LINES); + } + } + + if (rep_count > MAX_REPEATED_LINES) { + logFile << CurrentTimeString() << ": Last log entry repeated for " + << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; + } + + last_msg_ptr = msg; + last_char_sum = new_sum; + rep_count = 0; + + return false; +} + +static void do_log(int log_level, const char *msg, va_list args, void *param) +{ + fstream &logFile = *static_cast(param); + char str[8192]; + +#ifndef _WIN32 + va_list args2; + va_copy(args2, args); +#endif + + vsnprintf(str, sizeof(str), msg, args); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (wNum > 1) { + static wstring wide_buf; + static mutex wide_mutex; + + lock_guard lock(wide_mutex); + wide_buf.reserve(wNum + 1); + wide_buf.resize(wNum - 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); + wide_buf.push_back('\n'); + + OutputDebugStringW(wide_buf.c_str()); + } + } +#endif + +#if !defined(_WIN32) && defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + + if (log_level <= LOG_INFO || log_verbose) { +#if !defined(_WIN32) && !defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + if (!too_many_repeated_entries(logFile, msg, str)) + LogStringChunk(logFile, str, log_level); + } + +#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) + if (log_level <= LOG_ERROR && IsDebuggerPresent()) + __debugbreak(); +#endif + +#ifndef _WIN32 + va_end(args2); +#endif +} + +static bool get_token(lexer *lex, string &str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + str.assign(token.text.array, token.text.len); + return true; +} + +static bool expect_token(lexer *lex, const char *str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + return strref_cmp(&token.text, str) == 0; +} + +static uint64_t convert_log_name(bool has_prefix, const char *name) +{ + BaseLexer lex; + string year, month, day, hour, minute, second; + + lexer_start(lex, name); + + if (has_prefix) { + string temp; + if (!get_token(lex, temp, BASETOKEN_ALPHA)) + return 0; + } + + if (!get_token(lex, year, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, month, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, day, BASETOKEN_DIGIT)) + return 0; + if (!get_token(lex, hour, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, minute, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, second, BASETOKEN_DIGIT)) + return 0; + + stringstream timestring; + timestring << year << month << day << hour << minute << second; + return std::stoull(timestring.str()); +} + +/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ +/* TODO: Remove after version 32.0. */ +#if defined(__FreeBSD__) +static void move_to_xdg(void) +{ + char old_path[512]; + char new_path[512]; + char *home = getenv("HOME"); + if (!home) + return; + + if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) + return; + + /* make base xdg path if it doesn't already exist */ + if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) + return; + if (os_mkdirs(new_path) == MKDIR_ERROR) + return; + + if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) + return; + + if (os_file_exists(old_path) && !os_file_exists(new_path)) { + rename(old_path, new_path); + } +} +#endif + +static void delete_oldest_file(bool has_prefix, const char *location) +{ + BPtr logDir(GetAppConfigPathPtr(location)); + string oldestLog; + uint64_t oldest_ts = (uint64_t)-1; + struct os_dirent *entry; + + unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); + + os_dir_t *dir = os_opendir(logDir); + if (dir) { + unsigned int count = 0; + + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts) { + if (ts < oldest_ts) { + oldestLog = entry->d_name; + oldest_ts = ts; + } + + count++; + } + } + + os_closedir(dir); + + if (count > maxLogs) { + stringstream delPath; + + delPath << logDir << "/" << oldestLog; + os_unlink(delPath.str().c_str()); + } + } +} + +static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) +{ + BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); + struct os_dirent *entry; + os_dir_t *dir = os_opendir(logDir); + uint64_t highest_ts = 0; + + if (dir) { + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts > highest_ts) { + last = entry->d_name; + highest_ts = ts; + } + } + + os_closedir(dir); + } +} + +static void create_log_file(fstream &logFile) +{ + stringstream dst; + + get_last_log(false, "obs-studio/logs", lastLogFile); +#ifdef _WIN32 + get_last_log(true, "obs-studio/crashes", lastCrashLogFile); +#endif + + currentLogFile = GenerateTimeDateFilename("txt"); + dst << "obs-studio/logs/" << currentLogFile.c_str(); + + BPtr path(GetAppConfigPathPtr(dst.str().c_str())); + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); +#else + logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); +#endif + + if (logFile.is_open()) { + delete_oldest_file(false, "obs-studio/logs"); + base_set_log_handler(do_log, &logFile); + } else { + blog(LOG_ERROR, "Failed to open log file"); + } +} + +static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { + profiler_name_store_free(store); +}; + +using ProfilerNameStore = std::unique_ptr; + +ProfilerNameStore CreateNameStore() +{ + return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; +} + +static auto SnapshotRelease = [](profiler_snapshot_t *snap) { + profile_snapshot_free(snap); +}; + +using ProfilerSnapshot = std::unique_ptr; + +ProfilerSnapshot GetSnapshot() +{ + return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; +} + +static void SaveProfilerData(const ProfilerSnapshot &snap) +{ + if (currentLogFile.empty()) + return; + + auto pos = currentLogFile.rfind('.'); + if (pos == currentLogFile.npos) + return; + +#define LITERAL_SIZE(x) x, (sizeof(x) - 1) + ostringstream dst; + dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); + dst.write(currentLogFile.c_str(), pos); + dst.write(LITERAL_SIZE(".csv.gz")); +#undef LITERAL_SIZE + + BPtr path = GetAppConfigPathPtr(dst.str().c_str()); + if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) + blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); +} + +static auto ProfilerFree = [](void *) { + profiler_stop(); + + auto snap = GetSnapshot(); + + profiler_print(snap.get()); + profiler_print_time_between_calls(snap.get()); + + SaveProfilerData(snap); + + profiler_free(); +}; + +QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) + return new VolumeAccessibleInterface(static_cast(object)); + + return nullptr; +} + +static const char *run_program_init = "run_program_init"; +static int run_program(fstream &logFile, int argc, char *argv[]) +{ + int ret = -1; + + auto profilerNameStore = CreateNameStore(); + + std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); + + profiler_start(); + profile_register_root(run_program_init, 0); + + ScopeProfiler prof{run_program_init}; + +#ifdef _WIN32 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + + QCoreApplication::addLibraryPath("."); + +#if __APPLE__ + InstallNSApplicationSubclass(); + InstallNSThreadLocks(); + + if (!isInBundle()) { + blog(LOG_ERROR, + "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); + return ret; + } +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) + /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and + * crashes loading saved geometry. Just turn off this theme and let users complain OBS + * looks ugly instead of crashing. */ + const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); + if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) + unsetenv("QT_QPA_PLATFORMTHEME"); +#endif + + /* NOTE: This disables an optimisation in Qt that attempts to determine if + * any "siblings" intersect with a widget when determining the approximate + * visible/unobscured area. However, by Qt's own admission this is slow + * and in the case of OBS it significantly slows down lists with many + * elements (e.g. Hotkeys) and it is actually faster to disable it. */ + qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); + + OBSApp program(argc, argv, profilerNameStore.get()); + try { + QAccessible::installFactory(accessibleFactory); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + + bool created_log = false; + + program.AppInit(); + delete_oldest_file(false, "obs-studio/profiler_data"); + + OBSTranslator translator; + program.installTranslator(&translator); + + /* --------------------------------------- */ + /* check and warn if already running */ + + bool cancel_launch = false; + bool already_running = false; + +#ifdef _WIN32 + RunOnceMutex rom = +#endif + CheckIfAlreadyRunning(already_running); + + if (!already_running) { + goto run; + } + + if (!multi) { + QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), + QTStr("AlreadyRunning.Text")); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); + QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); + mb.setDefaultButton(cancelButton); + + mb.exec(); + cancel_launch = mb.clickedButton() == cancelButton; + } + + if (cancel_launch) + return 0; + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { + blog(LOG_INFO, "User enabled --multi flag and is now " + "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); + /* Clear unclean_shutdown flag as multiple instances + * running from the same config will lead to a + * false-positive detection.*/ + unclean_shutdown = false; + } + + /* --------------------------------------- */ + run: + +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) + // Mounted by termina during chromeOS linux container startup + // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh + os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); + if (crosDir) { + QMessageBox::StandardButtons buttons(QMessageBox::Ok); + QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, + nullptr); + + mb.exec(); + return 0; + } +#endif + + if (!created_log) + create_log_file(logFile); + + if (unclean_shutdown) { + blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); + } + + if (unclean_shutdown && !safe_mode) { + QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); + QPushButton *launchSafeButton = + mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); + QPushButton *launchNormalButton = + mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); + mb.setDefaultButton(launchNormalButton); + mb.exec(); + + safe_mode = mb.clickedButton() == launchSafeButton; + if (safe_mode) { + blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); + } else { + blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); + } + } + + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { + switch (type) { +#ifdef _DEBUG + case QtDebugMsg: + blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); + break; + case QtInfoMsg: + blog(LOG_INFO, "%s", QT_TO_UTF8(message)); + break; +#else + case QtDebugMsg: + case QtInfoMsg: + break; +#endif + case QtWarningMsg: + blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); + break; + case QtCriticalMsg: + case QtFatalMsg: + blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); + break; + } + }); + +#ifdef __APPLE__ + MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); + MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); + MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); + MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); + + int permissionsDialogLastShown = + config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); + if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { + OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, + accessibility_permission); + check.exec(); + } +#endif + +#ifdef _WIN32 + if (IsRunningOnWine()) { + QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); + mb.setTextFormat(Qt::RichText); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); + QPushButton *closeButton = mb.addButton(QMessageBox::Close); + mb.setDefaultButton(closeButton); + + mb.exec(); + if (mb.clickedButton() == closeButton) + return 0; + } +#endif + + if (argc > 1) { + stringstream stor; + stor << argv[1]; + for (int i = 2; i < argc; ++i) { + stor << " " << argv[i]; + } + blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); + } + + if (!program.OBSInit()) + return 0; + + prof.Stop(); + + ret = program.exec(); + + } catch (const char *error) { + blog(LOG_ERROR, "%s", error); + OBSErrorBox(nullptr, "%s", error); + } + + if (restart || restart_safe) { + arguments = qApp->arguments(); + + if (restart_safe) { + arguments.append("--safe-mode"); + } else { + arguments.removeAll("--safe-mode"); + } + } + + return ret; +} + +#define MAX_CRASH_REPORT_SIZE (150 * 1024) + +#ifdef _WIN32 + +#define CRASH_MESSAGE \ + "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ + "to the clipboard? The crash log will still be saved to:\n\n%s" + +static void main_crash_handler(const char *format, va_list args, void * /* param */) +{ + char *text = new char[MAX_CRASH_REPORT_SIZE]; + + vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); + text[MAX_CRASH_REPORT_SIZE - 1] = 0; + + string crashFilePath = "obs-studio/crashes"; + + delete_oldest_file(true, crashFilePath.c_str()); + + string name = crashFilePath + "/"; + name += "Crash " + GenerateTimeDateFilename("txt"); + + BPtr path(GetAppConfigPathPtr(name.c_str())); + + fstream file; + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#else + file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#endif + file << text; + file.close(); + + string pathString(path.Get()); + +#ifdef _WIN32 + std::replace(pathString.begin(), pathString.end(), '/', '\\'); +#endif + + string absolutePath = canonical(filesystem::path(pathString)).u8string(); + + size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); + + unique_ptr message_buffer(new char[size + 1]); + + snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); + + string finalMessage = string(message_buffer.get(), message_buffer.get() + size); + + int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); + + if (ret == IDYES) { + size_t len = strlen(text); + + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(mem), text, len); + GlobalUnlock(mem); + + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + + exit(-1); +} + +static void load_debug_privilege(void) +{ + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { + return; + } + + if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); + } + + if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + blog(LOG_INFO, "Could not set privilege to " + "increase GPU priority"); + } + } + + CloseHandle(token); +} +#endif + +static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) +{ + return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); +} + +static void check_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + /* Safe Mode detection is disabled in Debug builds to keep developers + * somewhat sane. */ + return; +#else + if (disable_shutdown_check) + return; + + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + if (os_file_exists(sentinelPath)) { + unclean_shutdown = true; + return; + } + + os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); +#endif +} + +static void delete_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + return; +#else + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + os_unlink(sentinelPath); +#endif +} + +#ifdef _WIN32 +static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; +static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " + "Redistributables.\n\nYou will now be directed to the download page."; +static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; + +static bool vc_runtime_outdated() +{ + win_version_info ver; + if (!get_dll_ver(L"msvcp140.dll", &ver)) + return true; + /* Major is always 14 (hence 140.dll), so we only care about minor. */ + if (ver.minor >= 40) + return false; + + int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); + if (choice == IDOK) { + /* Open the URL in the default browser. */ + ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); + } + + return true; +} +#endif + +#if defined(__APPLE__) || defined(__linux__) +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + +int main(int argc, char *argv[]) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); + + struct sigaction sig_handler; + + sig_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sig_handler.sa_mask); + sig_handler.sa_flags = 0; + + sigaction(SIGINT, &sig_handler, NULL); + + /* Block SIGPIPE in all threads, this can happen if a thread calls write on + a closed pipe. */ + sigset_t sigpipe_mask; + sigemptyset(&sigpipe_mask); + sigaddset(&sigpipe_mask, SIGPIPE); + sigset_t saved_mask; + if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { + perror("pthread_sigmask"); + exit(1); + } +#endif + +#ifdef _WIN32 + // Abort as early as possible if MSVC runtime is outdated + if (vc_runtime_outdated()) + return 1; + // Try to keep this as early as possible + install_dll_blocklist_hook(); + + obs_init_win32_crash_handler(); + SetErrorMode(SEM_FAILCRITICALERRORS); + load_debug_privilege(); + base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } +#endif + + base_get_log_handler(&def_log_handler, nullptr); + +#if defined(__FreeBSD__) + move_to_xdg(); +#endif + + obs_set_cmdline_args(argc, argv); + + for (int i = 1; i < argc; i++) { + if (arg_is(argv[i], "--multi", "-m")) { + multi = true; + disable_shutdown_check = true; + +#if ALLOW_PORTABLE_MODE + } else if (arg_is(argv[i], "--portable", "-p")) { + portable_mode = true; + +#endif + } else if (arg_is(argv[i], "--verbose", nullptr)) { + log_verbose = true; + + } else if (arg_is(argv[i], "--safe-mode", nullptr)) { + safe_mode = true; + + } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { + disable_3p_plugins = true; + + } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { + /* This exists mostly to bypass the dialog during development. */ + disable_shutdown_check = true; + + } else if (arg_is(argv[i], "--always-on-top", nullptr)) { + opt_always_on_top = true; + + } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { + unfiltered_log = true; + + } else if (arg_is(argv[i], "--startstreaming", nullptr)) { + opt_start_streaming = true; + + } else if (arg_is(argv[i], "--startrecording", nullptr)) { + opt_start_recording = true; + + } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { + opt_start_replaybuffer = true; + + } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { + opt_start_virtualcam = true; + + } else if (arg_is(argv[i], "--collection", nullptr)) { + if (++i < argc) + opt_starting_collection = argv[i]; + + } else if (arg_is(argv[i], "--profile", nullptr)) { + if (++i < argc) + opt_starting_profile = argv[i]; + + } else if (arg_is(argv[i], "--scene", nullptr)) { + if (++i < argc) + opt_starting_scene = argv[i]; + + } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { + opt_minimize_tray = true; + + } else if (arg_is(argv[i], "--studio-mode", nullptr)) { + opt_studio_mode = true; + + } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { + opt_allow_opengl = true; + + } else if (arg_is(argv[i], "--disable-updater", nullptr)) { + opt_disable_updater = true; + + } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { + opt_disable_missing_files_check = true; + + } else if (arg_is(argv[i], "--steam", nullptr)) { + steam = true; + + } else if (arg_is(argv[i], "--help", "-h")) { + std::string help = + "--help, -h: Get list of available commands.\n\n" + "--startstreaming: Automatically start streaming.\n" + "--startrecording: Automatically start recording.\n" + "--startreplaybuffer: Start replay buffer.\n" + "--startvirtualcam: Start virtual camera (if available).\n\n" + "--collection : Use specific scene collection." + "\n" + "--profile : Use specific profile.\n" + "--scene : Start with specific scene.\n\n" + "--studio-mode: Enable studio mode.\n" + "--minimize-to-tray: Minimize to system tray.\n" +#if ALLOW_PORTABLE_MODE + "--portable, -p: Use portable mode.\n" +#endif + "--multi, -m: Don't warn when launching multiple instances.\n\n" + "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" + "--only-bundled-plugins: Only load included (first-party) plugins\n" + "--disable-shutdown-check: Disable unclean shutdown detection.\n" + "--verbose: Make log more verbose.\n" + "--always-on-top: Start in 'always on top' mode.\n\n" + "--unfiltered_log: Make log unfiltered.\n\n" + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" + "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + +#ifdef _WIN32 + MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); +#else + std::cout << help << "--version, -V: Get current version.\n"; +#endif + exit(0); + + } else if (arg_is(argv[i], "--version", "-V")) { + std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; + exit(0); + } + } + +#if ALLOW_PORTABLE_MODE + if (!portable_mode) { + portable_mode = os_file_exists(BASE_PATH "/portable_mode") || + os_file_exists(BASE_PATH "/obs_portable_mode") || + os_file_exists(BASE_PATH "/portable_mode.txt") || + os_file_exists(BASE_PATH "/obs_portable_mode.txt"); + } + + if (!opt_disable_updater) { + opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || + os_file_exists(BASE_PATH "/disable_updater.txt"); + } + + if (!opt_disable_missing_files_check) { + opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || + os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); + } +#endif + + check_safe_mode_sentinel(); + + fstream logFile; + + curl_global_init(CURL_GLOBAL_ALL); + int ret = run_program(logFile, argc, argv); + +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } + + log_blocked_dlls(); +#endif + + delete_safe_mode_sentinel(); + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); + base_set_log_handler(nullptr, nullptr); + + if (restart || restart_safe) { + auto executable = arguments.takeFirst(); + QProcess::startDetached(executable, arguments); + } + + return ret; +} diff --git a/UI/frontend-plugins/CMakeLists.txt b/frontend/plugins/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/CMakeLists.txt rename to frontend/plugins/CMakeLists.txt diff --git a/UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp b/frontend/plugins/aja-output-ui/AJAOutputUI.cpp similarity index 100% rename from UI/frontend-plugins/aja-output-ui/AJAOutputUI.cpp rename to frontend/plugins/aja-output-ui/AJAOutputUI.cpp diff --git a/UI/frontend-plugins/aja-output-ui/AJAOutputUI.h b/frontend/plugins/aja-output-ui/AJAOutputUI.h similarity index 100% rename from UI/frontend-plugins/aja-output-ui/AJAOutputUI.h rename to frontend/plugins/aja-output-ui/AJAOutputUI.h diff --git a/UI/frontend-plugins/aja-output-ui/CMakeLists.txt b/frontend/plugins/aja-output-ui/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/aja-output-ui/CMakeLists.txt rename to frontend/plugins/aja-output-ui/CMakeLists.txt diff --git a/UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp b/frontend/plugins/aja-output-ui/aja-ui-main.cpp similarity index 100% rename from UI/frontend-plugins/aja-output-ui/aja-ui-main.cpp rename to frontend/plugins/aja-output-ui/aja-ui-main.cpp diff --git a/UI/frontend-plugins/aja-output-ui/aja-ui-main.h b/frontend/plugins/aja-output-ui/aja-ui-main.h similarity index 100% rename from UI/frontend-plugins/aja-output-ui/aja-ui-main.h rename to frontend/plugins/aja-output-ui/aja-ui-main.h diff --git a/UI/frontend-plugins/aja-output-ui/cmake/windows/obs-module.rc.in b/frontend/plugins/aja-output-ui/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/aja-output-ui/cmake/windows/obs-module.rc.in rename to frontend/plugins/aja-output-ui/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/af-ZA.ini b/frontend/plugins/aja-output-ui/data/locale/af-ZA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/af-ZA.ini rename to frontend/plugins/aja-output-ui/data/locale/af-ZA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ar-SA.ini b/frontend/plugins/aja-output-ui/data/locale/ar-SA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ar-SA.ini rename to frontend/plugins/aja-output-ui/data/locale/ar-SA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/az-AZ.ini b/frontend/plugins/aja-output-ui/data/locale/az-AZ.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/az-AZ.ini rename to frontend/plugins/aja-output-ui/data/locale/az-AZ.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/be-BY.ini b/frontend/plugins/aja-output-ui/data/locale/be-BY.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/be-BY.ini rename to frontend/plugins/aja-output-ui/data/locale/be-BY.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/bg-BG.ini b/frontend/plugins/aja-output-ui/data/locale/bg-BG.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/bg-BG.ini rename to frontend/plugins/aja-output-ui/data/locale/bg-BG.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ca-ES.ini b/frontend/plugins/aja-output-ui/data/locale/ca-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ca-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/ca-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/cs-CZ.ini b/frontend/plugins/aja-output-ui/data/locale/cs-CZ.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/cs-CZ.ini rename to frontend/plugins/aja-output-ui/data/locale/cs-CZ.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/da-DK.ini b/frontend/plugins/aja-output-ui/data/locale/da-DK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/da-DK.ini rename to frontend/plugins/aja-output-ui/data/locale/da-DK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/de-DE.ini b/frontend/plugins/aja-output-ui/data/locale/de-DE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/de-DE.ini rename to frontend/plugins/aja-output-ui/data/locale/de-DE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/el-GR.ini b/frontend/plugins/aja-output-ui/data/locale/el-GR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/el-GR.ini rename to frontend/plugins/aja-output-ui/data/locale/el-GR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/en-GB.ini b/frontend/plugins/aja-output-ui/data/locale/en-GB.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/en-GB.ini rename to frontend/plugins/aja-output-ui/data/locale/en-GB.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini b/frontend/plugins/aja-output-ui/data/locale/en-US.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/en-US.ini rename to frontend/plugins/aja-output-ui/data/locale/en-US.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/es-ES.ini b/frontend/plugins/aja-output-ui/data/locale/es-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/es-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/es-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/et-EE.ini b/frontend/plugins/aja-output-ui/data/locale/et-EE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/et-EE.ini rename to frontend/plugins/aja-output-ui/data/locale/et-EE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/eu-ES.ini b/frontend/plugins/aja-output-ui/data/locale/eu-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/eu-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/eu-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fa-IR.ini b/frontend/plugins/aja-output-ui/data/locale/fa-IR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fa-IR.ini rename to frontend/plugins/aja-output-ui/data/locale/fa-IR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fi-FI.ini b/frontend/plugins/aja-output-ui/data/locale/fi-FI.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fi-FI.ini rename to frontend/plugins/aja-output-ui/data/locale/fi-FI.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fil-PH.ini b/frontend/plugins/aja-output-ui/data/locale/fil-PH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fil-PH.ini rename to frontend/plugins/aja-output-ui/data/locale/fil-PH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/fr-FR.ini b/frontend/plugins/aja-output-ui/data/locale/fr-FR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/fr-FR.ini rename to frontend/plugins/aja-output-ui/data/locale/fr-FR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/gl-ES.ini b/frontend/plugins/aja-output-ui/data/locale/gl-ES.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/gl-ES.ini rename to frontend/plugins/aja-output-ui/data/locale/gl-ES.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/he-IL.ini b/frontend/plugins/aja-output-ui/data/locale/he-IL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/he-IL.ini rename to frontend/plugins/aja-output-ui/data/locale/he-IL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hi-IN.ini b/frontend/plugins/aja-output-ui/data/locale/hi-IN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hi-IN.ini rename to frontend/plugins/aja-output-ui/data/locale/hi-IN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hr-HR.ini b/frontend/plugins/aja-output-ui/data/locale/hr-HR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hr-HR.ini rename to frontend/plugins/aja-output-ui/data/locale/hr-HR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hu-HU.ini b/frontend/plugins/aja-output-ui/data/locale/hu-HU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hu-HU.ini rename to frontend/plugins/aja-output-ui/data/locale/hu-HU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/hy-AM.ini b/frontend/plugins/aja-output-ui/data/locale/hy-AM.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/hy-AM.ini rename to frontend/plugins/aja-output-ui/data/locale/hy-AM.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/id-ID.ini b/frontend/plugins/aja-output-ui/data/locale/id-ID.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/id-ID.ini rename to frontend/plugins/aja-output-ui/data/locale/id-ID.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/it-IT.ini b/frontend/plugins/aja-output-ui/data/locale/it-IT.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/it-IT.ini rename to frontend/plugins/aja-output-ui/data/locale/it-IT.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ja-JP.ini b/frontend/plugins/aja-output-ui/data/locale/ja-JP.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ja-JP.ini rename to frontend/plugins/aja-output-ui/data/locale/ja-JP.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ka-GE.ini b/frontend/plugins/aja-output-ui/data/locale/ka-GE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ka-GE.ini rename to frontend/plugins/aja-output-ui/data/locale/ka-GE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/kaa.ini b/frontend/plugins/aja-output-ui/data/locale/kaa.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/kaa.ini rename to frontend/plugins/aja-output-ui/data/locale/kaa.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/kmr-TR.ini b/frontend/plugins/aja-output-ui/data/locale/kmr-TR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/kmr-TR.ini rename to frontend/plugins/aja-output-ui/data/locale/kmr-TR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ko-KR.ini b/frontend/plugins/aja-output-ui/data/locale/ko-KR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ko-KR.ini rename to frontend/plugins/aja-output-ui/data/locale/ko-KR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ms-MY.ini b/frontend/plugins/aja-output-ui/data/locale/ms-MY.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ms-MY.ini rename to frontend/plugins/aja-output-ui/data/locale/ms-MY.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/nb-NO.ini b/frontend/plugins/aja-output-ui/data/locale/nb-NO.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/nb-NO.ini rename to frontend/plugins/aja-output-ui/data/locale/nb-NO.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/nl-NL.ini b/frontend/plugins/aja-output-ui/data/locale/nl-NL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/nl-NL.ini rename to frontend/plugins/aja-output-ui/data/locale/nl-NL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pl-PL.ini b/frontend/plugins/aja-output-ui/data/locale/pl-PL.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pl-PL.ini rename to frontend/plugins/aja-output-ui/data/locale/pl-PL.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pt-BR.ini b/frontend/plugins/aja-output-ui/data/locale/pt-BR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pt-BR.ini rename to frontend/plugins/aja-output-ui/data/locale/pt-BR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/pt-PT.ini b/frontend/plugins/aja-output-ui/data/locale/pt-PT.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/pt-PT.ini rename to frontend/plugins/aja-output-ui/data/locale/pt-PT.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ro-RO.ini b/frontend/plugins/aja-output-ui/data/locale/ro-RO.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ro-RO.ini rename to frontend/plugins/aja-output-ui/data/locale/ro-RO.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ru-RU.ini b/frontend/plugins/aja-output-ui/data/locale/ru-RU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ru-RU.ini rename to frontend/plugins/aja-output-ui/data/locale/ru-RU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/si-LK.ini b/frontend/plugins/aja-output-ui/data/locale/si-LK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/si-LK.ini rename to frontend/plugins/aja-output-ui/data/locale/si-LK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sk-SK.ini b/frontend/plugins/aja-output-ui/data/locale/sk-SK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sk-SK.ini rename to frontend/plugins/aja-output-ui/data/locale/sk-SK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sl-SI.ini b/frontend/plugins/aja-output-ui/data/locale/sl-SI.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sl-SI.ini rename to frontend/plugins/aja-output-ui/data/locale/sl-SI.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/sv-SE.ini b/frontend/plugins/aja-output-ui/data/locale/sv-SE.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/sv-SE.ini rename to frontend/plugins/aja-output-ui/data/locale/sv-SE.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/th-TH.ini b/frontend/plugins/aja-output-ui/data/locale/th-TH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/th-TH.ini rename to frontend/plugins/aja-output-ui/data/locale/th-TH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tl-PH.ini b/frontend/plugins/aja-output-ui/data/locale/tl-PH.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tl-PH.ini rename to frontend/plugins/aja-output-ui/data/locale/tl-PH.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tr-TR.ini b/frontend/plugins/aja-output-ui/data/locale/tr-TR.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tr-TR.ini rename to frontend/plugins/aja-output-ui/data/locale/tr-TR.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/tt-RU.ini b/frontend/plugins/aja-output-ui/data/locale/tt-RU.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/tt-RU.ini rename to frontend/plugins/aja-output-ui/data/locale/tt-RU.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ug-CN.ini b/frontend/plugins/aja-output-ui/data/locale/ug-CN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ug-CN.ini rename to frontend/plugins/aja-output-ui/data/locale/ug-CN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/uk-UA.ini b/frontend/plugins/aja-output-ui/data/locale/uk-UA.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/uk-UA.ini rename to frontend/plugins/aja-output-ui/data/locale/uk-UA.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/ur-PK.ini b/frontend/plugins/aja-output-ui/data/locale/ur-PK.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/ur-PK.ini rename to frontend/plugins/aja-output-ui/data/locale/ur-PK.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/vi-VN.ini b/frontend/plugins/aja-output-ui/data/locale/vi-VN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/vi-VN.ini rename to frontend/plugins/aja-output-ui/data/locale/vi-VN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/zh-CN.ini b/frontend/plugins/aja-output-ui/data/locale/zh-CN.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/zh-CN.ini rename to frontend/plugins/aja-output-ui/data/locale/zh-CN.ini diff --git a/UI/frontend-plugins/aja-output-ui/data/locale/zh-TW.ini b/frontend/plugins/aja-output-ui/data/locale/zh-TW.ini similarity index 100% rename from UI/frontend-plugins/aja-output-ui/data/locale/zh-TW.ini rename to frontend/plugins/aja-output-ui/data/locale/zh-TW.ini diff --git a/UI/frontend-plugins/aja-output-ui/forms/output.ui b/frontend/plugins/aja-output-ui/forms/output.ui similarity index 100% rename from UI/frontend-plugins/aja-output-ui/forms/output.ui rename to frontend/plugins/aja-output-ui/forms/output.ui diff --git a/UI/frontend-plugins/decklink-captions/CMakeLists.txt b/frontend/plugins/decklink-captions/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/decklink-captions/CMakeLists.txt rename to frontend/plugins/decklink-captions/CMakeLists.txt diff --git a/UI/frontend-plugins/decklink-captions/cmake/windows/obs-module.rc.in b/frontend/plugins/decklink-captions/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/decklink-captions/cmake/windows/obs-module.rc.in rename to frontend/plugins/decklink-captions/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/decklink-captions/data/.keepme b/frontend/plugins/decklink-captions/data/.keepme similarity index 100% rename from UI/frontend-plugins/decklink-captions/data/.keepme rename to frontend/plugins/decklink-captions/data/.keepme diff --git a/UI/frontend-plugins/decklink-captions/decklink-captions.cpp b/frontend/plugins/decklink-captions/decklink-captions.cpp similarity index 100% rename from UI/frontend-plugins/decklink-captions/decklink-captions.cpp rename to frontend/plugins/decklink-captions/decklink-captions.cpp diff --git a/UI/frontend-plugins/decklink-captions/decklink-captions.h b/frontend/plugins/decklink-captions/decklink-captions.h similarity index 100% rename from UI/frontend-plugins/decklink-captions/decklink-captions.h rename to frontend/plugins/decklink-captions/decklink-captions.h diff --git a/UI/frontend-plugins/decklink-captions/forms/captions.ui b/frontend/plugins/decklink-captions/forms/captions.ui similarity index 100% rename from UI/frontend-plugins/decklink-captions/forms/captions.ui rename to frontend/plugins/decklink-captions/forms/captions.ui diff --git a/UI/frontend-plugins/decklink-output-ui/CMakeLists.txt b/frontend/plugins/decklink-output-ui/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/CMakeLists.txt rename to frontend/plugins/decklink-output-ui/CMakeLists.txt diff --git a/UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.cpp b/frontend/plugins/decklink-output-ui/DecklinkOutputUI.cpp similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.cpp rename to frontend/plugins/decklink-output-ui/DecklinkOutputUI.cpp diff --git a/UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.h b/frontend/plugins/decklink-output-ui/DecklinkOutputUI.h similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/DecklinkOutputUI.h rename to frontend/plugins/decklink-output-ui/DecklinkOutputUI.h diff --git a/UI/frontend-plugins/decklink-output-ui/cmake/windows/obs-module.rc.in b/frontend/plugins/decklink-output-ui/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/cmake/windows/obs-module.rc.in rename to frontend/plugins/decklink-output-ui/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/decklink-output-ui/data/.keepme b/frontend/plugins/decklink-output-ui/data/.keepme similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/data/.keepme rename to frontend/plugins/decklink-output-ui/data/.keepme diff --git a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp b/frontend/plugins/decklink-output-ui/decklink-ui-main.cpp similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp rename to frontend/plugins/decklink-output-ui/decklink-ui-main.cpp diff --git a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.h b/frontend/plugins/decklink-output-ui/decklink-ui-main.h similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/decklink-ui-main.h rename to frontend/plugins/decklink-output-ui/decklink-ui-main.h diff --git a/UI/frontend-plugins/decklink-output-ui/forms/output.ui b/frontend/plugins/decklink-output-ui/forms/output.ui similarity index 100% rename from UI/frontend-plugins/decklink-output-ui/forms/output.ui rename to frontend/plugins/decklink-output-ui/forms/output.ui diff --git a/UI/frontend-plugins/frontend-tools/CMakeLists.txt b/frontend/plugins/frontend-tools/CMakeLists.txt similarity index 100% rename from UI/frontend-plugins/frontend-tools/CMakeLists.txt rename to frontend/plugins/frontend-tools/CMakeLists.txt diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-nix.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher-nix.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-nix.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher-nix.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-osx.mm b/frontend/plugins/frontend-tools/auto-scene-switcher-osx.mm similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-osx.mm rename to frontend/plugins/frontend-tools/auto-scene-switcher-osx.mm diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher-win.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher-win.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher-win.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher-win.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher.cpp b/frontend/plugins/frontend-tools/auto-scene-switcher.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher.cpp rename to frontend/plugins/frontend-tools/auto-scene-switcher.cpp diff --git a/UI/frontend-plugins/frontend-tools/auto-scene-switcher.hpp b/frontend/plugins/frontend-tools/auto-scene-switcher.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/auto-scene-switcher.hpp rename to frontend/plugins/frontend-tools/auto-scene-switcher.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-handler.cpp b/frontend/plugins/frontend-tools/captions-handler.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-handler.cpp rename to frontend/plugins/frontend-tools/captions-handler.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-handler.hpp b/frontend/plugins/frontend-tools/captions-handler.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-handler.hpp rename to frontend/plugins/frontend-tools/captions-handler.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi-stream.cpp b/frontend/plugins/frontend-tools/captions-mssapi-stream.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi-stream.cpp rename to frontend/plugins/frontend-tools/captions-mssapi-stream.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi-stream.hpp b/frontend/plugins/frontend-tools/captions-mssapi-stream.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi-stream.hpp rename to frontend/plugins/frontend-tools/captions-mssapi-stream.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi.cpp b/frontend/plugins/frontend-tools/captions-mssapi.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi.cpp rename to frontend/plugins/frontend-tools/captions-mssapi.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions-mssapi.hpp b/frontend/plugins/frontend-tools/captions-mssapi.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions-mssapi.hpp rename to frontend/plugins/frontend-tools/captions-mssapi.hpp diff --git a/UI/frontend-plugins/frontend-tools/captions.cpp b/frontend/plugins/frontend-tools/captions.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions.cpp rename to frontend/plugins/frontend-tools/captions.cpp diff --git a/UI/frontend-plugins/frontend-tools/captions.hpp b/frontend/plugins/frontend-tools/captions.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/captions.hpp rename to frontend/plugins/frontend-tools/captions.hpp diff --git a/UI/frontend-plugins/frontend-tools/cmake/windows/obs-module.rc.in b/frontend/plugins/frontend-tools/cmake/windows/obs-module.rc.in similarity index 100% rename from UI/frontend-plugins/frontend-tools/cmake/windows/obs-module.rc.in rename to frontend/plugins/frontend-tools/cmake/windows/obs-module.rc.in diff --git a/UI/frontend-plugins/frontend-tools/data/locale/af-ZA.ini b/frontend/plugins/frontend-tools/data/locale/af-ZA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/af-ZA.ini rename to frontend/plugins/frontend-tools/data/locale/af-ZA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/an-ES.ini b/frontend/plugins/frontend-tools/data/locale/an-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/an-ES.ini rename to frontend/plugins/frontend-tools/data/locale/an-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ar-SA.ini b/frontend/plugins/frontend-tools/data/locale/ar-SA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ar-SA.ini rename to frontend/plugins/frontend-tools/data/locale/ar-SA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/az-AZ.ini b/frontend/plugins/frontend-tools/data/locale/az-AZ.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/az-AZ.ini rename to frontend/plugins/frontend-tools/data/locale/az-AZ.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ba-RU.ini b/frontend/plugins/frontend-tools/data/locale/ba-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ba-RU.ini rename to frontend/plugins/frontend-tools/data/locale/ba-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/be-BY.ini b/frontend/plugins/frontend-tools/data/locale/be-BY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/be-BY.ini rename to frontend/plugins/frontend-tools/data/locale/be-BY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/bg-BG.ini b/frontend/plugins/frontend-tools/data/locale/bg-BG.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/bg-BG.ini rename to frontend/plugins/frontend-tools/data/locale/bg-BG.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/bn-BD.ini b/frontend/plugins/frontend-tools/data/locale/bn-BD.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/bn-BD.ini rename to frontend/plugins/frontend-tools/data/locale/bn-BD.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ca-ES.ini b/frontend/plugins/frontend-tools/data/locale/ca-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ca-ES.ini rename to frontend/plugins/frontend-tools/data/locale/ca-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/cs-CZ.ini b/frontend/plugins/frontend-tools/data/locale/cs-CZ.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/cs-CZ.ini rename to frontend/plugins/frontend-tools/data/locale/cs-CZ.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/da-DK.ini b/frontend/plugins/frontend-tools/data/locale/da-DK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/da-DK.ini rename to frontend/plugins/frontend-tools/data/locale/da-DK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/de-DE.ini b/frontend/plugins/frontend-tools/data/locale/de-DE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/de-DE.ini rename to frontend/plugins/frontend-tools/data/locale/de-DE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini b/frontend/plugins/frontend-tools/data/locale/el-GR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/el-GR.ini rename to frontend/plugins/frontend-tools/data/locale/el-GR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-GB.ini b/frontend/plugins/frontend-tools/data/locale/en-GB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/en-GB.ini rename to frontend/plugins/frontend-tools/data/locale/en-GB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini b/frontend/plugins/frontend-tools/data/locale/en-US.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/en-US.ini rename to frontend/plugins/frontend-tools/data/locale/en-US.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/eo-UY.ini b/frontend/plugins/frontend-tools/data/locale/eo-UY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/eo-UY.ini rename to frontend/plugins/frontend-tools/data/locale/eo-UY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/es-ES.ini b/frontend/plugins/frontend-tools/data/locale/es-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/es-ES.ini rename to frontend/plugins/frontend-tools/data/locale/es-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/et-EE.ini b/frontend/plugins/frontend-tools/data/locale/et-EE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/et-EE.ini rename to frontend/plugins/frontend-tools/data/locale/et-EE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/eu-ES.ini b/frontend/plugins/frontend-tools/data/locale/eu-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/eu-ES.ini rename to frontend/plugins/frontend-tools/data/locale/eu-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fa-IR.ini b/frontend/plugins/frontend-tools/data/locale/fa-IR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fa-IR.ini rename to frontend/plugins/frontend-tools/data/locale/fa-IR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fi-FI.ini b/frontend/plugins/frontend-tools/data/locale/fi-FI.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fi-FI.ini rename to frontend/plugins/frontend-tools/data/locale/fi-FI.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fil-PH.ini b/frontend/plugins/frontend-tools/data/locale/fil-PH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fil-PH.ini rename to frontend/plugins/frontend-tools/data/locale/fil-PH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/fr-FR.ini b/frontend/plugins/frontend-tools/data/locale/fr-FR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/fr-FR.ini rename to frontend/plugins/frontend-tools/data/locale/fr-FR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini b/frontend/plugins/frontend-tools/data/locale/gd-GB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/gd-GB.ini rename to frontend/plugins/frontend-tools/data/locale/gd-GB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/gl-ES.ini b/frontend/plugins/frontend-tools/data/locale/gl-ES.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/gl-ES.ini rename to frontend/plugins/frontend-tools/data/locale/gl-ES.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/he-IL.ini b/frontend/plugins/frontend-tools/data/locale/he-IL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/he-IL.ini rename to frontend/plugins/frontend-tools/data/locale/he-IL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hi-IN.ini b/frontend/plugins/frontend-tools/data/locale/hi-IN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hi-IN.ini rename to frontend/plugins/frontend-tools/data/locale/hi-IN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hr-HR.ini b/frontend/plugins/frontend-tools/data/locale/hr-HR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hr-HR.ini rename to frontend/plugins/frontend-tools/data/locale/hr-HR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hu-HU.ini b/frontend/plugins/frontend-tools/data/locale/hu-HU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hu-HU.ini rename to frontend/plugins/frontend-tools/data/locale/hu-HU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/hy-AM.ini b/frontend/plugins/frontend-tools/data/locale/hy-AM.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/hy-AM.ini rename to frontend/plugins/frontend-tools/data/locale/hy-AM.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/id-ID.ini b/frontend/plugins/frontend-tools/data/locale/id-ID.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/id-ID.ini rename to frontend/plugins/frontend-tools/data/locale/id-ID.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/is-IS.ini b/frontend/plugins/frontend-tools/data/locale/is-IS.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/is-IS.ini rename to frontend/plugins/frontend-tools/data/locale/is-IS.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/it-IT.ini b/frontend/plugins/frontend-tools/data/locale/it-IT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/it-IT.ini rename to frontend/plugins/frontend-tools/data/locale/it-IT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ja-JP.ini b/frontend/plugins/frontend-tools/data/locale/ja-JP.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ja-JP.ini rename to frontend/plugins/frontend-tools/data/locale/ja-JP.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ka-GE.ini b/frontend/plugins/frontend-tools/data/locale/ka-GE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ka-GE.ini rename to frontend/plugins/frontend-tools/data/locale/ka-GE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kaa.ini b/frontend/plugins/frontend-tools/data/locale/kaa.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kaa.ini rename to frontend/plugins/frontend-tools/data/locale/kaa.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kab-KAB.ini b/frontend/plugins/frontend-tools/data/locale/kab-KAB.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kab-KAB.ini rename to frontend/plugins/frontend-tools/data/locale/kab-KAB.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/kmr-TR.ini b/frontend/plugins/frontend-tools/data/locale/kmr-TR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/kmr-TR.ini rename to frontend/plugins/frontend-tools/data/locale/kmr-TR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ko-KR.ini b/frontend/plugins/frontend-tools/data/locale/ko-KR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ko-KR.ini rename to frontend/plugins/frontend-tools/data/locale/ko-KR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/lo-LA.ini b/frontend/plugins/frontend-tools/data/locale/lo-LA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/lo-LA.ini rename to frontend/plugins/frontend-tools/data/locale/lo-LA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/lt-LT.ini b/frontend/plugins/frontend-tools/data/locale/lt-LT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/lt-LT.ini rename to frontend/plugins/frontend-tools/data/locale/lt-LT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/mn-MN.ini b/frontend/plugins/frontend-tools/data/locale/mn-MN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/mn-MN.ini rename to frontend/plugins/frontend-tools/data/locale/mn-MN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini b/frontend/plugins/frontend-tools/data/locale/ms-MY.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ms-MY.ini rename to frontend/plugins/frontend-tools/data/locale/ms-MY.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nb-NO.ini b/frontend/plugins/frontend-tools/data/locale/nb-NO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nb-NO.ini rename to frontend/plugins/frontend-tools/data/locale/nb-NO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nl-NL.ini b/frontend/plugins/frontend-tools/data/locale/nl-NL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nl-NL.ini rename to frontend/plugins/frontend-tools/data/locale/nl-NL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/nn-NO.ini b/frontend/plugins/frontend-tools/data/locale/nn-NO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/nn-NO.ini rename to frontend/plugins/frontend-tools/data/locale/nn-NO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/oc-FR.ini b/frontend/plugins/frontend-tools/data/locale/oc-FR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/oc-FR.ini rename to frontend/plugins/frontend-tools/data/locale/oc-FR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pl-PL.ini b/frontend/plugins/frontend-tools/data/locale/pl-PL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pl-PL.ini rename to frontend/plugins/frontend-tools/data/locale/pl-PL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pt-BR.ini b/frontend/plugins/frontend-tools/data/locale/pt-BR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pt-BR.ini rename to frontend/plugins/frontend-tools/data/locale/pt-BR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/pt-PT.ini b/frontend/plugins/frontend-tools/data/locale/pt-PT.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/pt-PT.ini rename to frontend/plugins/frontend-tools/data/locale/pt-PT.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ro-RO.ini b/frontend/plugins/frontend-tools/data/locale/ro-RO.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ro-RO.ini rename to frontend/plugins/frontend-tools/data/locale/ro-RO.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ru-RU.ini b/frontend/plugins/frontend-tools/data/locale/ru-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ru-RU.ini rename to frontend/plugins/frontend-tools/data/locale/ru-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/si-LK.ini b/frontend/plugins/frontend-tools/data/locale/si-LK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/si-LK.ini rename to frontend/plugins/frontend-tools/data/locale/si-LK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sk-SK.ini b/frontend/plugins/frontend-tools/data/locale/sk-SK.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sk-SK.ini rename to frontend/plugins/frontend-tools/data/locale/sk-SK.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sl-SI.ini b/frontend/plugins/frontend-tools/data/locale/sl-SI.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sl-SI.ini rename to frontend/plugins/frontend-tools/data/locale/sl-SI.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sq-AL.ini b/frontend/plugins/frontend-tools/data/locale/sq-AL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sq-AL.ini rename to frontend/plugins/frontend-tools/data/locale/sq-AL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sr-CS.ini b/frontend/plugins/frontend-tools/data/locale/sr-CS.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sr-CS.ini rename to frontend/plugins/frontend-tools/data/locale/sr-CS.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sr-SP.ini b/frontend/plugins/frontend-tools/data/locale/sr-SP.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sr-SP.ini rename to frontend/plugins/frontend-tools/data/locale/sr-SP.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/sv-SE.ini b/frontend/plugins/frontend-tools/data/locale/sv-SE.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/sv-SE.ini rename to frontend/plugins/frontend-tools/data/locale/sv-SE.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/szl-PL.ini b/frontend/plugins/frontend-tools/data/locale/szl-PL.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/szl-PL.ini rename to frontend/plugins/frontend-tools/data/locale/szl-PL.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ta-IN.ini b/frontend/plugins/frontend-tools/data/locale/ta-IN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ta-IN.ini rename to frontend/plugins/frontend-tools/data/locale/ta-IN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/th-TH.ini b/frontend/plugins/frontend-tools/data/locale/th-TH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/th-TH.ini rename to frontend/plugins/frontend-tools/data/locale/th-TH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tl-PH.ini b/frontend/plugins/frontend-tools/data/locale/tl-PH.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tl-PH.ini rename to frontend/plugins/frontend-tools/data/locale/tl-PH.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tr-TR.ini b/frontend/plugins/frontend-tools/data/locale/tr-TR.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tr-TR.ini rename to frontend/plugins/frontend-tools/data/locale/tr-TR.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/tt-RU.ini b/frontend/plugins/frontend-tools/data/locale/tt-RU.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/tt-RU.ini rename to frontend/plugins/frontend-tools/data/locale/tt-RU.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/ug-CN.ini b/frontend/plugins/frontend-tools/data/locale/ug-CN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/ug-CN.ini rename to frontend/plugins/frontend-tools/data/locale/ug-CN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/uk-UA.ini b/frontend/plugins/frontend-tools/data/locale/uk-UA.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/uk-UA.ini rename to frontend/plugins/frontend-tools/data/locale/uk-UA.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini b/frontend/plugins/frontend-tools/data/locale/vi-VN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/vi-VN.ini rename to frontend/plugins/frontend-tools/data/locale/vi-VN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini b/frontend/plugins/frontend-tools/data/locale/zh-CN.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/zh-CN.ini rename to frontend/plugins/frontend-tools/data/locale/zh-CN.ini diff --git a/UI/frontend-plugins/frontend-tools/data/locale/zh-TW.ini b/frontend/plugins/frontend-tools/data/locale/zh-TW.ini similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/locale/zh-TW.ini rename to frontend/plugins/frontend-tools/data/locale/zh-TW.ini diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source.lua b/frontend/plugins/frontend-tools/data/scripts/clock-source.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source.lua rename to frontend/plugins/frontend-tools/data/scripts/clock-source.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/dial.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/dial.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/dial.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/dial.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/hour.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/hour.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/hour.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/hour.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/minute.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/minute.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/minute.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/minute.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/clock-source/second.png b/frontend/plugins/frontend-tools/data/scripts/clock-source/second.png similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/clock-source/second.png rename to frontend/plugins/frontend-tools/data/scripts/clock-source/second.png diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua b/frontend/plugins/frontend-tools/data/scripts/countdown.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua rename to frontend/plugins/frontend-tools/data/scripts/countdown.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/instant-replay.lua b/frontend/plugins/frontend-tools/data/scripts/instant-replay.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/instant-replay.lua rename to frontend/plugins/frontend-tools/data/scripts/instant-replay.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/pause-scene.lua b/frontend/plugins/frontend-tools/data/scripts/pause-scene.lua similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/pause-scene.lua rename to frontend/plugins/frontend-tools/data/scripts/pause-scene.lua diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/url-text.py b/frontend/plugins/frontend-tools/data/scripts/url-text.py similarity index 100% rename from UI/frontend-plugins/frontend-tools/data/scripts/url-text.py rename to frontend/plugins/frontend-tools/data/scripts/url-text.py diff --git a/UI/frontend-plugins/frontend-tools/forms/auto-scene-switcher.ui b/frontend/plugins/frontend-tools/forms/auto-scene-switcher.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/auto-scene-switcher.ui rename to frontend/plugins/frontend-tools/forms/auto-scene-switcher.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/captions.ui b/frontend/plugins/frontend-tools/forms/captions.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/captions.ui rename to frontend/plugins/frontend-tools/forms/captions.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/output-timer.ui b/frontend/plugins/frontend-tools/forms/output-timer.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/output-timer.ui rename to frontend/plugins/frontend-tools/forms/output-timer.ui diff --git a/UI/frontend-plugins/frontend-tools/forms/scripts.ui b/frontend/plugins/frontend-tools/forms/scripts.ui similarity index 100% rename from UI/frontend-plugins/frontend-tools/forms/scripts.ui rename to frontend/plugins/frontend-tools/forms/scripts.ui diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in b/frontend/plugins/frontend-tools/frontend-tools-config.h.in similarity index 100% rename from UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in rename to frontend/plugins/frontend-tools/frontend-tools-config.h.in diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools.c b/frontend/plugins/frontend-tools/frontend-tools.c similarity index 100% rename from UI/frontend-plugins/frontend-tools/frontend-tools.c rename to frontend/plugins/frontend-tools/frontend-tools.c diff --git a/UI/frontend-plugins/frontend-tools/output-timer.cpp b/frontend/plugins/frontend-tools/output-timer.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/output-timer.cpp rename to frontend/plugins/frontend-tools/output-timer.cpp diff --git a/UI/frontend-plugins/frontend-tools/output-timer.hpp b/frontend/plugins/frontend-tools/output-timer.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/output-timer.hpp rename to frontend/plugins/frontend-tools/output-timer.hpp diff --git a/UI/frontend-plugins/frontend-tools/scripts.cpp b/frontend/plugins/frontend-tools/scripts.cpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/scripts.cpp rename to frontend/plugins/frontend-tools/scripts.cpp diff --git a/UI/frontend-plugins/frontend-tools/scripts.hpp b/frontend/plugins/frontend-tools/scripts.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/scripts.hpp rename to frontend/plugins/frontend-tools/scripts.hpp diff --git a/UI/frontend-plugins/frontend-tools/tool-helpers.hpp b/frontend/plugins/frontend-tools/tool-helpers.hpp similarity index 100% rename from UI/frontend-plugins/frontend-tools/tool-helpers.hpp rename to frontend/plugins/frontend-tools/tool-helpers.hpp diff --git a/UI/window-basic-settings.cpp b/frontend/settings/OBSBasicSettings.cpp similarity index 98% rename from UI/window-basic-settings.cpp rename to frontend/settings/OBSBasicSettings.cpp index d8a35501b..dc7bc73fd 100644 --- a/UI/window-basic-settings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -1,88 +1,60 @@ /****************************************************************************** - Copyright (C) 2023 by Lain Bailey - Philippe Groarke + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. +#include "OBSBasicSettings.hpp" +#include "OBSHotkeyLabel.hpp" +#include "OBSHotkeyWidget.hpp" - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +#include +#include +#include +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include +#include +#include +#include +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include +#include - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include "audio-encoders.hpp" -#include "hotkey-edit.hpp" -#include "source-label.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "properties-view.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-settings.cpp" -#include "window-basic-main-outputs.hpp" -#include "window-projector.hpp" +#include +#include -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif +#include -#include -#include -#include "ui-config.h" +#include "moc_OBSBasicSettings.cpp" using namespace std; -class SettingsEventFilter : public QObject { - QScopedPointer shortcutFilter; +extern const char *get_simple_output_encoder(const char *encoder); -public: - inline SettingsEventFilter() : shortcutFilter((OBSEventFilter *)CreateShortcutFilter()) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) override - { - int key; - - switch (event->type()) { - case QEvent::KeyPress: - case QEvent::KeyRelease: - key = static_cast(event)->key(); - if (key == Qt::Key_Escape) { - return false; - } - default: - break; - } - - return shortcutFilter->filter(obj, event); - } -}; +extern bool restart; +extern bool opt_allow_opengl; +extern bool cef_js_avail; static inline bool ResTooHigh(uint32_t cx, uint32_t cy) { diff --git a/UI/window-basic-settings.hpp b/frontend/settings/OBSBasicSettings.hpp similarity index 95% rename from UI/window-basic-settings.hpp rename to frontend/settings/OBSBasicSettings.hpp index 05d46fd41..08664e0d7 100644 --- a/UI/window-basic-settings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -18,57 +18,22 @@ #pragma once -#include -#include -#include -#include -#include - -#include - -#include "auth-base.hpp" -#include "ffmpeg-utils.hpp" -#include "obs-app-theming.hpp" - -class OBSBasic; -class QAbstractButton; -class QRadioButton; -class QComboBox; -class QCheckBox; -class QLabel; -class QButtonGroup; -class OBSPropertiesView; -class OBSHotkeyWidget; - #include "ui_OBSBasicSettings.h" +#include + +#include + #define VOLUME_METER_DECAY_FAST 23.53 #define VOLUME_METER_DECAY_MEDIUM 11.76 #define VOLUME_METER_DECAY_SLOW 8.57 -class SilentUpdateCheckBox : public QCheckBox { - Q_OBJECT - -public slots: - void setCheckedSilently(bool checked) - { - bool blocked = blockSignals(true); - setChecked(checked); - blockSignals(blocked); - } -}; - -class SilentUpdateSpinBox : public QSpinBox { - Q_OBJECT - -public slots: - void setValueSilently(int val) - { - bool blocked = blockSignals(true); - setValue(val); - blockSignals(blocked); - } -}; +class Auth; +class OBSBasic; +class OBSHotkeyWidget; +class OBSPropertiesView; +struct FFmpegFormat; +struct OBSTheme; std::string DeserializeConfigText(const char *value); @@ -171,6 +136,8 @@ private: void SaveEncoder(QComboBox *combo, const char *section, const char *value); bool ResFPSValid(obs_service_resolution *res_list, size_t res_count, int max_fps); + + // TODO: Remove, orphaned method void ClosestResFPS(obs_service_resolution *res_list, size_t res_count, int max_fps, int &new_cx, int &new_cy, int &new_fps); diff --git a/UI/window-basic-settings-a11y.cpp b/frontend/settings/OBSBasicSettings_A11y.cpp similarity index 98% rename from UI/window-basic-settings-a11y.cpp rename to frontend/settings/OBSBasicSettings_A11y.cpp index d50cbfded..15bf42435 100644 --- a/UI/window-basic-settings-a11y.cpp +++ b/frontend/settings/OBSBasicSettings_A11y.cpp @@ -1,8 +1,7 @@ -#include "window-basic-settings.hpp" -#include "window-basic-main.hpp" -#include "obs-frontend-api.h" -#include "obs-app.hpp" -#include +#include "OBSBasicSettings.hpp" + +#include + #include enum ColorPreset { diff --git a/UI/window-basic-settings-appearance.cpp b/frontend/settings/OBSBasicSettings_Appearance.cpp similarity index 88% rename from UI/window-basic-settings-appearance.cpp rename to frontend/settings/OBSBasicSettings_Appearance.cpp index cdfbc92f5..80e3fc914 100644 --- a/UI/window-basic-settings-appearance.cpp +++ b/frontend/settings/OBSBasicSettings_Appearance.cpp @@ -1,21 +1,9 @@ -#include "window-basic-settings.hpp" -#include "window-basic-main.hpp" -#include "obs-frontend-api.h" -#include "qt-wrappers.hpp" -#include "platform.hpp" -#include "obs-app.hpp" +#include "OBSBasicSettings.hpp" -#include -#include -#include -#include -#include -#include -#include +#include +#include -#include "util/profiler.hpp" - -using namespace std; +#include void OBSBasicSettings::InitAppearancePage() { diff --git a/UI/window-basic-settings-stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp similarity index 99% rename from UI/window-basic-settings-stream.cpp rename to frontend/settings/OBSBasicSettings_Stream.cpp index 885c49f8a..eff5d7de3 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/frontend/settings/OBSBasicSettings_Stream.cpp @@ -1,25 +1,17 @@ -#include -#include -#include -#include - -#include "window-basic-settings.hpp" -#include "obs-frontend-api.h" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "url-push-button.hpp" - -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" - -#include "ui-config.h" +#include "OBSBasicSettings.hpp" #ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" +#include #endif +#include +#ifdef YOUTUBE_ENABLED +#include +#endif +#include + +#include + +#include static const QUuid &CustomServerUUID() { @@ -32,6 +24,7 @@ struct QCefCookieManager; extern QCef *cef; extern QCefCookieManager *panel_cookies; +extern bool cef_js_avail; enum class ListOpt : int { ShowAll = 1, diff --git a/frontend/settings/OBSHotkeyEdit.cpp b/frontend/settings/OBSHotkeyEdit.cpp new file mode 100644 index 000000000..336f416c8 --- /dev/null +++ b/frontend/settings/OBSHotkeyEdit.cpp @@ -0,0 +1,217 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSHotkeyEdit.hpp" +#include "OBSBasicSettings.hpp" + +#include + +#include +#include + +#include + +#include "moc_OBSHotkeyEdit.cpp" + +void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + obs_key_combination_t new_key; + + switch (event->key()) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + new_key.key = OBS_KEY_NONE; + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + break; +#endif + + default: + new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const +{ + if (query == Qt::ImEnabled) { + return false; + } else { + return QLineEdit::inputMethodQuery(query); + } +} + +#ifdef __APPLE__ +void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) +{ + if (event->isAutoRepeat()) + return; + + if (event->key() != Qt::Key_CapsLock) + return; + + obs_key_combination_t new_key; + + // kVK_CapsLock == 57 + new_key.key = obs_key_from_virtual_key(57); + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} +#endif + +void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) +{ + obs_key_combination_t new_key; + + switch (event->button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return; + + case Qt::MiddleButton: + new_key.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + new_key.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4) + MAP_BUTTON(2, 5) + MAP_BUTTON(3, 6) + MAP_BUTTON(4, 7) + MAP_BUTTON(5, 8) + MAP_BUTTON(6, 9) + MAP_BUTTON(7, 10) + MAP_BUTTON(8, 11) + MAP_BUTTON(9, 12) + MAP_BUTTON(10, 13) + MAP_BUTTON(11, 14) + MAP_BUTTON(12, 15) + MAP_BUTTON(13, 16) + MAP_BUTTON(14, 17) + MAP_BUTTON(15, 18) + MAP_BUTTON(16, 19) + MAP_BUTTON(17, 20) + MAP_BUTTON(18, 21) + MAP_BUTTON(19, 22) + MAP_BUTTON(20, 23) + MAP_BUTTON(21, 24) + MAP_BUTTON(22, 25) + MAP_BUTTON(23, 26) + MAP_BUTTON(24, 27) +#undef MAP_BUTTON + } + + new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + HandleNewKey(new_key); +} + +void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) +{ + if (new_key == key || obs_key_combination_is_empty(new_key)) + return; + + key = new_key; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::RenderKey() +{ + DStr str; + obs_key_combination_to_str(key, str); + + setText(QT_UTF8(str)); +} + +void OBSHotkeyEdit::ResetKey() +{ + key = original; + + changed = false; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::ClearKey() +{ + key = {0, OBS_KEY_NONE}; + + changed = true; + emit KeyChanged(key); + + RenderKey(); +} + +void OBSHotkeyEdit::UpdateDuplicationState() +{ + if (!dupeIcon && !hasDuplicate) + return; + + if (!dupeIcon) + CreateDupeIcon(); + + if (dupeIcon->isVisible() != hasDuplicate) { + dupeIcon->setVisible(hasDuplicate); + update(); + } +} + +void OBSHotkeyEdit::InitSignalHandler() +{ + layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", + [](void *this_, calldata_t *) { + auto edit = static_cast(this_); + QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); + }, + this}; +} + +void OBSHotkeyEdit::CreateDupeIcon() +{ + dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); + dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); + QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); + dupeIcon->setVisible(false); +} + +void OBSHotkeyEdit::ReloadKeyLayout() +{ + RenderKey(); +} diff --git a/UI/hotkey-edit.hpp b/frontend/settings/OBSHotkeyEdit.hpp similarity index 56% rename from UI/hotkey-edit.hpp rename to frontend/settings/OBSHotkeyEdit.hpp index ea7e9b4ae..8514e906c 100644 --- a/UI/hotkey-edit.hpp +++ b/frontend/settings/OBSHotkeyEdit.hpp @@ -17,16 +17,13 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include - #include +#include + +class OBSBasicSettings; +class QWidget; + static inline bool operator!=(const obs_key_combination_t &c1, const obs_key_combination_t &c2) { return c1.modifiers != c2.modifiers || c1.key != c2.key; @@ -37,21 +34,6 @@ static inline bool operator==(const obs_key_combination_t &c1, const obs_key_com return !(c1 != c2); } -class OBSBasicSettings; -class OBSHotkeyWidget; - -class OBSHotkeyLabel : public QLabel { - Q_OBJECT - -public: - QPointer pairPartner; - QPointer widget; - void highlightPair(bool highlight); - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - void setToolTip(const QString &toolTip); -}; - class OBSHotkeyEdit : public QLineEdit { Q_OBJECT; @@ -118,73 +100,3 @@ signals: void KeyChanged(obs_key_combination_t); void SearchKey(obs_key_combination_t); }; - -class OBSHotkeyWidget : public QWidget { - Q_OBJECT; - -public: - OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, - const std::vector &combos = {}) - : QWidget(parent), - id(id), - name(name), - bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", - &OBSHotkeyWidget::BindingsChanged, this), - settings(settings) - { - auto layout = new QVBoxLayout; - layout->setSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); - setLayout(layout); - - SetKeyCombinations(combos); - } - - void SetKeyCombinations(const std::vector &); - - obs_hotkey_id id; - std::string name; - - bool changed = false; - bool Changed() const; - - QPointer label; - std::vector> edits; - - QString toolTip; - void setToolTip(const QString &toolTip_) - { - toolTip = toolTip_; - for (auto &edit : edits) - edit->setToolTip(toolTip_); - } - - void Apply(); - void GetCombinations(std::vector &) const; - void Save(); - void Save(std::vector &combinations); - - void enterEvent(QEnterEvent *event) override; - void leaveEvent(QEvent *event) override; - -private: - void AddEdit(obs_key_combination combo, int idx = -1); - void RemoveEdit(size_t idx, bool signal = true); - - static void BindingsChanged(void *data, calldata_t *param); - - std::vector> removeButtons; - std::vector> revertButtons; - OBSSignal bindingsChanged; - bool ignoreChangedBindings = false; - OBSBasicSettings *settings; - - QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } - -private slots: - void HandleChangedBindings(obs_hotkey_id id_); - -signals: - void KeyChanged(); - void SearchKey(obs_key_combination_t); -}; diff --git a/frontend/settings/OBSHotkeyLabel.cpp b/frontend/settings/OBSHotkeyLabel.cpp new file mode 100644 index 000000000..1cfa8e5b0 --- /dev/null +++ b/frontend/settings/OBSHotkeyLabel.cpp @@ -0,0 +1,69 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSHotkeyLabel.hpp" +#include "OBSHotkeyWidget.hpp" + +#include +#include + +#include "moc_OBSHotkeyLabel.cpp" + +static inline void updateStyle(QWidget *widget) +{ + auto style = widget->style(); + style->unpolish(widget); + style->polish(widget); + widget->update(); +} + +void OBSHotkeyLabel::highlightPair(bool highlight) +{ + if (!pairPartner) + return; + + pairPartner->setProperty("class", highlight ? "text-bright" : ""); + updateStyle(pairPartner); + setProperty("class", highlight ? "text-bright" : ""); + updateStyle(this); +} + +void OBSHotkeyLabel::enterEvent(QEnterEvent *event) +{ + + if (!pairPartner) + return; + + event->accept(); + highlightPair(true); +} + +void OBSHotkeyLabel::leaveEvent(QEvent *event) +{ + if (!pairPartner) + return; + + event->accept(); + highlightPair(false); +} + +void OBSHotkeyLabel::setToolTip(const QString &toolTip) +{ + QLabel::setToolTip(toolTip); + if (widget) + widget->setToolTip(toolTip); +} diff --git a/frontend/settings/OBSHotkeyLabel.hpp b/frontend/settings/OBSHotkeyLabel.hpp new file mode 100644 index 000000000..789453a18 --- /dev/null +++ b/frontend/settings/OBSHotkeyLabel.hpp @@ -0,0 +1,35 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +class OBSHotkeyWidget; + +class OBSHotkeyLabel : public QLabel { + Q_OBJECT + +public: + QPointer pairPartner; + QPointer widget; + void highlightPair(bool highlight); + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void setToolTip(const QString &toolTip); +}; diff --git a/UI/hotkey-edit.cpp b/frontend/settings/OBSHotkeyWidget.cpp similarity index 51% rename from UI/hotkey-edit.cpp rename to frontend/settings/OBSHotkeyWidget.cpp index 5dc5355e7..77d691c81 100644 --- a/UI/hotkey-edit.cpp +++ b/frontend/settings/OBSHotkeyWidget.cpp @@ -1,219 +1,28 @@ /****************************************************************************** - Copyright (C) 2014-2015 by Ruwen Hahn + Copyright (C) 2014-2015 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. +#include "OBSHotkeyWidget.hpp" +#include "OBSHotkeyLabel.hpp" - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +#include - You should have received a copy of the GNU General Public License - along with this program. If not, see . -******************************************************************************/ +#include -#include "window-basic-settings.hpp" -#include "moc_hotkey-edit.cpp" - -#include -#include -#include -#include -#include - -#include "obs-app.hpp" - -void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - obs_key_combination_t new_key; - - switch (event->key()) { - case Qt::Key_Shift: - case Qt::Key_Control: - case Qt::Key_Alt: - case Qt::Key_Meta: - new_key.key = OBS_KEY_NONE; - break; - -#ifdef __APPLE__ - case Qt::Key_CapsLock: - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - break; -#endif - - default: - new_key.key = obs_key_from_virtual_key(event->nativeVirtualKey()); - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -QVariant OBSHotkeyEdit::inputMethodQuery(Qt::InputMethodQuery query) const -{ - if (query == Qt::ImEnabled) { - return false; - } else { - return QLineEdit::inputMethodQuery(query); - } -} - -#ifdef __APPLE__ -void OBSHotkeyEdit::keyReleaseEvent(QKeyEvent *event) -{ - if (event->isAutoRepeat()) - return; - - if (event->key() != Qt::Key_CapsLock) - return; - - obs_key_combination_t new_key; - - // kVK_CapsLock == 57 - new_key.key = obs_key_from_virtual_key(57); - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} -#endif - -void OBSHotkeyEdit::mousePressEvent(QMouseEvent *event) -{ - obs_key_combination_t new_key; - - switch (event->button()) { - case Qt::NoButton: - case Qt::LeftButton: - case Qt::RightButton: - case Qt::AllButtons: - case Qt::MouseButtonMask: - return; - - case Qt::MiddleButton: - new_key.key = OBS_KEY_MOUSE3; - break; - -#define MAP_BUTTON(i, j) \ - case Qt::ExtraButton##i: \ - new_key.key = OBS_KEY_MOUSE##j; \ - break; - MAP_BUTTON(1, 4) - MAP_BUTTON(2, 5) - MAP_BUTTON(3, 6) - MAP_BUTTON(4, 7) - MAP_BUTTON(5, 8) - MAP_BUTTON(6, 9) - MAP_BUTTON(7, 10) - MAP_BUTTON(8, 11) - MAP_BUTTON(9, 12) - MAP_BUTTON(10, 13) - MAP_BUTTON(11, 14) - MAP_BUTTON(12, 15) - MAP_BUTTON(13, 16) - MAP_BUTTON(14, 17) - MAP_BUTTON(15, 18) - MAP_BUTTON(16, 19) - MAP_BUTTON(17, 20) - MAP_BUTTON(18, 21) - MAP_BUTTON(19, 22) - MAP_BUTTON(20, 23) - MAP_BUTTON(21, 24) - MAP_BUTTON(22, 25) - MAP_BUTTON(23, 26) - MAP_BUTTON(24, 27) -#undef MAP_BUTTON - } - - new_key.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); - - HandleNewKey(new_key); -} - -void OBSHotkeyEdit::HandleNewKey(obs_key_combination_t new_key) -{ - if (new_key == key || obs_key_combination_is_empty(new_key)) - return; - - key = new_key; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::RenderKey() -{ - DStr str; - obs_key_combination_to_str(key, str); - - setText(QT_UTF8(str)); -} - -void OBSHotkeyEdit::ResetKey() -{ - key = original; - - changed = false; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::ClearKey() -{ - key = {0, OBS_KEY_NONE}; - - changed = true; - emit KeyChanged(key); - - RenderKey(); -} - -void OBSHotkeyEdit::UpdateDuplicationState() -{ - if (!dupeIcon && !hasDuplicate) - return; - - if (!dupeIcon) - CreateDupeIcon(); - - if (dupeIcon->isVisible() != hasDuplicate) { - dupeIcon->setVisible(hasDuplicate); - update(); - } -} - -void OBSHotkeyEdit::InitSignalHandler() -{ - layoutChanged = {obs_get_signal_handler(), "hotkey_layout_change", - [](void *this_, calldata_t *) { - auto edit = static_cast(this_); - QMetaObject::invokeMethod(edit, "ReloadKeyLayout"); - }, - this}; -} - -void OBSHotkeyEdit::CreateDupeIcon() -{ - dupeIcon = addAction(settings->GetHotkeyConflictIcon(), ActionPosition::TrailingPosition); - dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning")); - QObject::connect(dupeIcon, &QAction::triggered, [=] { emit SearchKey(key); }); - dupeIcon->setVisible(false); -} - -void OBSHotkeyEdit::ReloadKeyLayout() -{ - RenderKey(); -} +#include "moc_OBSHotkeyWidget.cpp" void OBSHotkeyWidget::SetKeyCombinations(const std::vector &combos) { @@ -406,14 +215,6 @@ void OBSHotkeyWidget::HandleChangedBindings(obs_hotkey_id id_) SetKeyCombinations(bindings); } -static inline void updateStyle(QWidget *widget) -{ - auto style = widget->style(); - style->unpolish(widget); - style->polish(widget); - widget->update(); -} - void OBSHotkeyWidget::enterEvent(QEnterEvent *event) { if (!label) @@ -431,40 +232,3 @@ void OBSHotkeyWidget::leaveEvent(QEvent *event) event->accept(); label->highlightPair(false); } - -void OBSHotkeyLabel::highlightPair(bool highlight) -{ - if (!pairPartner) - return; - - pairPartner->setProperty("class", highlight ? "text-bright" : ""); - updateStyle(pairPartner); - setProperty("class", highlight ? "text-bright" : ""); - updateStyle(this); -} - -void OBSHotkeyLabel::enterEvent(QEnterEvent *event) -{ - - if (!pairPartner) - return; - - event->accept(); - highlightPair(true); -} - -void OBSHotkeyLabel::leaveEvent(QEvent *event) -{ - if (!pairPartner) - return; - - event->accept(); - highlightPair(false); -} - -void OBSHotkeyLabel::setToolTip(const QString &toolTip) -{ - QLabel::setToolTip(toolTip); - if (widget) - widget->setToolTip(toolTip); -} diff --git a/frontend/settings/OBSHotkeyWidget.hpp b/frontend/settings/OBSHotkeyWidget.hpp new file mode 100644 index 000000000..12f15f282 --- /dev/null +++ b/frontend/settings/OBSHotkeyWidget.hpp @@ -0,0 +1,98 @@ +/****************************************************************************** + Copyright (C) 2014-2015 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "OBSHotkeyEdit.hpp" + +#include +#include +#include +#include + +class OBSBasicSettings; +class OBSHotkeyLabel; + +class OBSHotkeyWidget : public QWidget { + Q_OBJECT; + +public: + OBSHotkeyWidget(QWidget *parent, obs_hotkey_id id, std::string name, OBSBasicSettings *settings, + const std::vector &combos = {}) + : QWidget(parent), + id(id), + name(name), + bindingsChanged(obs_get_signal_handler(), "hotkey_bindings_changed", + &OBSHotkeyWidget::BindingsChanged, this), + settings(settings) + { + auto layout = new QVBoxLayout; + layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + SetKeyCombinations(combos); + } + + void SetKeyCombinations(const std::vector &); + + obs_hotkey_id id; + std::string name; + + bool changed = false; + bool Changed() const; + + QPointer label; + std::vector> edits; + + QString toolTip; + void setToolTip(const QString &toolTip_) + { + toolTip = toolTip_; + for (auto &edit : edits) + edit->setToolTip(toolTip_); + } + + void Apply(); + void GetCombinations(std::vector &) const; + void Save(); + void Save(std::vector &combinations); + + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + void AddEdit(obs_key_combination combo, int idx = -1); + void RemoveEdit(size_t idx, bool signal = true); + + static void BindingsChanged(void *data, calldata_t *param); + + std::vector> removeButtons; + std::vector> revertButtons; + OBSSignal bindingsChanged; + bool ignoreChangedBindings = false; + OBSBasicSettings *settings; + + QVBoxLayout *layout() const { return dynamic_cast(QWidget::layout()); } + +private slots: + void HandleChangedBindings(obs_hotkey_id id_); + +signals: + void KeyChanged(); + void SearchKey(obs_key_combination_t); +}; diff --git a/UI/win-update/updater/CMakeLists.txt b/frontend/updater/CMakeLists.txt similarity index 95% rename from UI/win-update/updater/CMakeLists.txt rename to frontend/updater/CMakeLists.txt index 443509ff3..abaa84c76 100644 --- a/UI/win-update/updater/CMakeLists.txt +++ b/frontend/updater/CMakeLists.txt @@ -24,7 +24,7 @@ target_sources( target_compile_definitions(updater PRIVATE NOMINMAX "PSAPI_VERSION=2") -target_include_directories(updater PRIVATE "${CMAKE_SOURCE_DIR}/libobs" "${CMAKE_SOURCE_DIR}/UI/win-update") +target_include_directories(updater PRIVATE "${CMAKE_SOURCE_DIR}/libobs" "${CMAKE_SOURCE_DIR}/frontend/utility") target_link_libraries( updater diff --git a/UI/win-update/updater/hash.cpp b/frontend/updater/hash.cpp similarity index 100% rename from UI/win-update/updater/hash.cpp rename to frontend/updater/hash.cpp diff --git a/UI/win-update/updater/helpers.cpp b/frontend/updater/helpers.cpp similarity index 100% rename from UI/win-update/updater/helpers.cpp rename to frontend/updater/helpers.cpp diff --git a/UI/win-update/updater/helpers.hpp b/frontend/updater/helpers.hpp similarity index 100% rename from UI/win-update/updater/helpers.hpp rename to frontend/updater/helpers.hpp diff --git a/UI/win-update/updater/http.cpp b/frontend/updater/http.cpp similarity index 100% rename from UI/win-update/updater/http.cpp rename to frontend/updater/http.cpp diff --git a/UI/win-update/updater/init-hook-files.c b/frontend/updater/init-hook-files.c similarity index 100% rename from UI/win-update/updater/init-hook-files.c rename to frontend/updater/init-hook-files.c diff --git a/UI/win-update/updater/manifest.hpp b/frontend/updater/manifest.hpp similarity index 100% rename from UI/win-update/updater/manifest.hpp rename to frontend/updater/manifest.hpp diff --git a/UI/win-update/updater/patch.cpp b/frontend/updater/patch.cpp similarity index 100% rename from UI/win-update/updater/patch.cpp rename to frontend/updater/patch.cpp diff --git a/UI/win-update/updater/resource.h b/frontend/updater/resource.h similarity index 100% rename from UI/win-update/updater/resource.h rename to frontend/updater/resource.h diff --git a/UI/win-update/updater/updater.cpp b/frontend/updater/updater.cpp similarity index 100% rename from UI/win-update/updater/updater.cpp rename to frontend/updater/updater.cpp diff --git a/UI/win-update/updater/updater.hpp b/frontend/updater/updater.hpp similarity index 100% rename from UI/win-update/updater/updater.hpp rename to frontend/updater/updater.hpp diff --git a/UI/win-update/updater/updater.manifest b/frontend/updater/updater.manifest similarity index 100% rename from UI/win-update/updater/updater.manifest rename to frontend/updater/updater.manifest diff --git a/UI/win-update/updater/updater.rc b/frontend/updater/updater.rc similarity index 100% rename from UI/win-update/updater/updater.rc rename to frontend/updater/updater.rc diff --git a/frontend/utility/AdvancedOutput.cpp b/frontend/utility/AdvancedOutput.cpp new file mode 100644 index 000000000..0d927721d --- /dev/null +++ b/frontend/utility/AdvancedOutput.cpp @@ -0,0 +1,916 @@ +#include "AdvancedOutput.hpp" + +#include +#include +#include + +#include + +using namespace std; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} diff --git a/frontend/utility/AdvancedOutput.hpp b/frontend/utility/AdvancedOutput.hpp new file mode 100644 index 000000000..a06191420 --- /dev/null +++ b/frontend/utility/AdvancedOutput.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "BasicOutputHandler.hpp" + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; diff --git a/UI/update/win-update.cpp b/frontend/utility/AutoUpdateThread.cpp similarity index 96% rename from UI/update/win-update.cpp rename to frontend/utility/AutoUpdateThread.cpp index 73ef8e696..f72e56f77 100644 --- a/UI/update/win-update.cpp +++ b/frontend/utility/AutoUpdateThread.cpp @@ -1,30 +1,19 @@ -#include "../win-update/updater/manifest.hpp" -#include "update-helpers.hpp" -#include "shared-update.hpp" -#include "update-window.hpp" -#include "remote-text.hpp" -#include "win-update.hpp" -#include "obs-app.hpp" +#include "AutoUpdateThread.hpp" +#include "ui_OBSUpdate.h" + +#include +#include +#include +#include +#include #include -#include - -#include -#include #define WIN32_LEAN_AND_MEAN #include #include -#include -#include - -#ifdef BROWSER_AVAILABLE -#include -#endif - -using namespace std; -using namespace updater; +#include "moc_AutoUpdateThread.cpp" /* ------------------------------------------------------------------------ */ @@ -50,6 +39,11 @@ using namespace updater; /* ------------------------------------------------------------------------ */ +using namespace std; +using namespace updater; + +extern char *GetConfigPathPtr(const char *name); + static bool ParseUpdateManifest(const char *manifest_data, bool *updatesAvailable, string ¬es, string &updateVer, const string &branch) try { diff --git a/UI/update/win-update.hpp b/frontend/utility/AutoUpdateThread.hpp similarity index 97% rename from UI/update/win-update.hpp rename to frontend/utility/AutoUpdateThread.hpp index d345f204b..a0b3b0b4c 100644 --- a/UI/update/win-update.hpp +++ b/frontend/utility/AutoUpdateThread.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include class AutoUpdateThread : public QThread { Q_OBJECT diff --git a/frontend/utility/BaseLexer.hpp b/frontend/utility/BaseLexer.hpp new file mode 100644 index 000000000..c32156b2a --- /dev/null +++ b/frontend/utility/BaseLexer.hpp @@ -0,0 +1,29 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +struct BaseLexer { + lexer lex; + +public: + inline BaseLexer() { lexer_init(&lex); } + inline ~BaseLexer() { lexer_free(&lex); } + operator lexer *() { return &lex; } +}; diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp new file mode 100644 index 000000000..7488c4662 --- /dev/null +++ b/frontend/utility/BasicOutputHandler.cpp @@ -0,0 +1,584 @@ +#include "BasicOutputHandler.hpp" +#include "AdvancedOutput.hpp" +#include "SimpleOutput.hpp" + +#include +#include +#include +#include + +#include + +#include + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/UI/window-basic-main-outputs.hpp b/frontend/utility/BasicOutputHandler.hpp similarity index 66% rename from UI/window-basic-main-outputs.hpp rename to frontend/utility/BasicOutputHandler.hpp index f7178f19d..7d041304a 100644 --- a/UI/window-basic-main-outputs.hpp +++ b/frontend/utility/BasicOutputHandler.hpp @@ -1,10 +1,15 @@ #pragma once -#include -#include -#include +#include -#include "multitrack-video-output.hpp" +#include +#include + +#include + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" class OBSBasic; @@ -58,7 +63,7 @@ struct BasicOutputHandler { OBSSignal replayBufferStopping; OBSSignal replayBufferSaved; - inline BasicOutputHandler(OBSBasic *main_); + BasicOutputHandler(OBSBasic *main_); virtual ~BasicOutputHandler(){}; @@ -103,3 +108,39 @@ protected: BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main); BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main); + +void OBSStreamStarting(void *data, calldata_t *params); +void OBSStreamStopping(void *data, calldata_t *params); +void OBSStartStreaming(void *data, calldata_t *params); +void OBSStopStreaming(void *data, calldata_t *params); +void OBSStartRecording(void *data, calldata_t *params); +void OBSStopRecording(void *data, calldata_t *params); +void OBSRecordStopping(void *data, calldata_t *params); +void OBSRecordFileChanged(void *data, calldata_t *params); +void OBSStartReplayBuffer(void *data, calldata_t *params); +void OBSStopReplayBuffer(void *data, calldata_t *params); +void OBSReplayBufferStopping(void *data, calldata_t *params); +void OBSReplayBufferSaved(void *data, calldata_t *params); + +inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +const char *GetStreamOutputType(const obs_service_t *service); + +inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +void clear_archive_encoder(obs_output_t *output, const char *expected_name); diff --git a/frontend/utility/ExtraBrowsersDelegate.cpp b/frontend/utility/ExtraBrowsersDelegate.cpp new file mode 100644 index 000000000..e984797fb --- /dev/null +++ b/frontend/utility/ExtraBrowsersDelegate.cpp @@ -0,0 +1,132 @@ +#include "ExtraBrowsersDelegate.hpp" +#include "ExtraBrowsersModel.hpp" + +#include + +#include + +#include + +#include "moc_ExtraBrowsersDelegate.cpp" + +QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, + const QModelIndex &index) const +{ + QLineEdit *text = new EditWidget(parent, index); + text->installEventFilter(const_cast(this)); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + return text; +} + +void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = reinterpret_cast(editor); + text->blockSignals(true); + text->setText(index.data().toString()); + text->blockSignals(false); +} + +bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event) +{ + QLineEdit *edit = qobject_cast(object); + if (!edit) + return false; + + if (LineEditCanceled(event)) { + RevertText(edit); + } + if (LineEditChanged(event)) { + UpdateText(edit); + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab) { + model->TabSelection(true); + } else if (keyEvent->key() == Qt::Key_Backtab) { + model->TabSelection(false); + } + } + return true; + } + + return false; +} + +bool ExtraBrowsersDelegate::ValidName(const QString &name) const +{ + for (auto &item : model->items) { + if (name.compare(item.title, Qt::CaseInsensitive) == 0) { + return false; + } + } + return true; +} + +void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString oldText; + if (col == (int)Column::Title) { + oldText = newItem ? model->newTitle : model->items[row].title; + } else { + oldText = newItem ? model->newURL : model->items[row].url; + } + + edit->setText(oldText); +} + +bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_) +{ + EditWidget *edit = reinterpret_cast(edit_); + int row = edit->index.row(); + int col = edit->index.column(); + bool newItem = (row == model->items.size()); + + QString text = edit->text().trimmed(); + + if (!newItem && text.isEmpty()) { + return false; + } + + if (col == (int)Column::Title) { + QString oldText = newItem ? model->newTitle : model->items[row].title; + bool same = oldText.compare(text, Qt::CaseInsensitive) == 0; + + if (!same && !ValidName(text)) { + edit->setText(oldText); + return false; + } + } + + if (!newItem) { + /* if edited existing item, update it*/ + switch (col) { + case (int)Column::Title: + model->items[row].title = text; + break; + case (int)Column::Url: + model->items[row].url = text; + break; + } + } else { + /* if both new values filled out, create new one */ + switch (col) { + case (int)Column::Title: + model->newTitle = text; + break; + case (int)Column::Url: + model->newURL = text; + break; + } + + model->CheckToAdd(); + } + + emit commitData(edit); + return true; +} diff --git a/frontend/utility/ExtraBrowsersDelegate.hpp b/frontend/utility/ExtraBrowsersDelegate.hpp new file mode 100644 index 000000000..6abafc153 --- /dev/null +++ b/frontend/utility/ExtraBrowsersDelegate.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +class ExtraBrowsersModel; + +class ExtraBrowsersDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {} + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + + bool eventFilter(QObject *object, QEvent *event) override; + void RevertText(QLineEdit *edit); + bool UpdateText(QLineEdit *edit); + bool ValidName(const QString &text) const; + + ExtraBrowsersModel *model; +}; diff --git a/frontend/utility/ExtraBrowsersModel.cpp b/frontend/utility/ExtraBrowsersModel.cpp new file mode 100644 index 000000000..5409c3c19 --- /dev/null +++ b/frontend/utility/ExtraBrowsersModel.cpp @@ -0,0 +1,254 @@ +#include "ExtraBrowsersModel.hpp" + +#include +#include +#include + +#include + +#include + +#include "moc_ExtraBrowsersModel.cpp" + +void ExtraBrowsersModel::Reset() +{ + items.clear(); + + OBSBasic *main = OBSBasic::Get(); + + for (int i = 0; i < main->extraBrowserDocks.size(); i++) { + Item item; + item.prevIdx = i; + item.title = main->extraBrowserDockNames[i]; + item.url = main->extraBrowserDockTargets[i]; + items.push_back(item); + } +} + +int ExtraBrowsersModel::rowCount(const QModelIndex &) const +{ + int count = items.size() + 1; + return count; +} + +int ExtraBrowsersModel::columnCount(const QModelIndex &) const +{ + return (int)Column::Count; +} + +QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const +{ + int column = index.column(); + int idx = index.row(); + int count = items.size(); + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (!validRole) + return QVariant(); + + if (idx >= 0 && idx < count) { + switch (column) { + case (int)Column::Title: + return items[idx].title; + case (int)Column::Url: + return items[idx].url; + } + } else if (idx == count) { + switch (column) { + case (int)Column::Title: + return newTitle; + case (int)Column::Url: + return newURL; + } + } + + return QVariant(); +} + +QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole; + + if (validRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case (int)Column::Title: + return QTStr("ExtraBrowsers.DockName"); + case (int)Column::Url: + return QStringLiteral("URL"); + } + } + + return QVariant(); +} + +Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() != (int)Column::Delete) + flags |= Qt::ItemIsEditable; + + return flags; +} +void ExtraBrowsersModel::AddDeleteButton(int idx) +{ + QTableView *widget = reinterpret_cast(parent()); + + QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr); + + QPushButton *del = new DelButton(index); + del->setProperty("class", "icon-trash"); + del->setObjectName("extraPanelDelete"); + del->setMinimumSize(QSize(20, 20)); + connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem); + + widget->setIndexWidget(index, del); + widget->setRowHeight(idx, 20); + widget->setColumnWidth(idx, 20); +} + +void ExtraBrowsersModel::CheckToAdd() +{ + if (newTitle.isEmpty() || newURL.isEmpty()) + return; + + int idx = items.size() + 1; + beginInsertRows(QModelIndex(), idx, idx); + + Item item; + item.prevIdx = -1; + item.title = newTitle; + item.url = newURL; + items.push_back(item); + + newTitle = ""; + newURL = ""; + + endInsertRows(); + + AddDeleteButton(idx - 1); +} + +void ExtraBrowsersModel::UpdateItem(Item &item) +{ + int idx = item.prevIdx; + + OBSBasic *main = OBSBasic::Get(); + BrowserDock *dock = reinterpret_cast(main->extraBrowserDocks[idx].get()); + dock->setWindowTitle(item.title); + dock->setObjectName(item.title + OBJ_NAME_SUFFIX); + + if (main->extraBrowserDockNames[idx] != item.title) { + main->extraBrowserDockNames[idx] = item.title; + dock->toggleViewAction()->setText(item.title); + dock->setTitle(item.title); + } + + if (main->extraBrowserDockTargets[idx] != item.url) { + dock->cefWidget->setURL(QT_TO_UTF8(item.url)); + main->extraBrowserDockTargets[idx] = item.url; + } +} + +void ExtraBrowsersModel::DeleteItem() +{ + QTableView *widget = reinterpret_cast(parent()); + + DelButton *del = reinterpret_cast(sender()); + int row = del->index.row(); + + /* there's some sort of internal bug in Qt and deleting certain index + * widgets or "editors" that can cause a crash inside Qt if the widget + * is not manually removed, at least on 5.7 */ + widget->setIndexWidget(del->index, nullptr); + del->deleteLater(); + + /* --------- */ + + beginRemoveRows(QModelIndex(), row, row); + + int prevIdx = items[row].prevIdx; + items.removeAt(row); + + if (prevIdx != -1) { + int i = 0; + for (; i < deleted.size() && deleted[i] < prevIdx; i++) + ; + deleted.insert(i, prevIdx); + } + + endRemoveRows(); +} + +void ExtraBrowsersModel::Apply() +{ + OBSBasic *main = OBSBasic::Get(); + + for (Item &item : items) { + if (item.prevIdx != -1) { + UpdateItem(item); + } else { + QString uuid = QUuid::createUuid().toString(); + uuid.replace(QRegularExpression("[{}-]"), ""); + main->AddExtraBrowserDock(item.title, item.url, uuid, true); + } + } + + for (int i = deleted.size() - 1; i >= 0; i--) { + int idx = deleted[i]; + main->extraBrowserDockTargets.removeAt(idx); + main->extraBrowserDockNames.removeAt(idx); + main->extraBrowserDocks.removeAt(idx); + } + + if (main->extraBrowserDocks.empty()) + main->extraBrowserMenuDocksSeparator.clear(); + + deleted.clear(); + + Reset(); +} + +void ExtraBrowsersModel::TabSelection(bool forward) +{ + QListView *widget = reinterpret_cast(parent()); + QItemSelectionModel *selModel = widget->selectionModel(); + + QModelIndex sel = selModel->currentIndex(); + int row = sel.row(); + int col = sel.column(); + + switch (sel.column()) { + case (int)Column::Title: + if (!forward) { + if (row == 0) { + return; + } + + row -= 1; + } + + col += 1; + break; + + case (int)Column::Url: + if (forward) { + if (row == items.size()) { + return; + } + + row += 1; + } + + col -= 1; + } + + sel = createIndex(row, col, nullptr); + selModel->setCurrentIndex(sel, QItemSelectionModel::Clear); +} + +void ExtraBrowsersModel::Init() +{ + for (int i = 0; i < items.count(); i++) + AddDeleteButton(i); +} diff --git a/frontend/utility/ExtraBrowsersModel.hpp b/frontend/utility/ExtraBrowsersModel.hpp new file mode 100644 index 000000000..2424875e6 --- /dev/null +++ b/frontend/utility/ExtraBrowsersModel.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +enum class Column : int { + Title, + Url, + Delete, + + Count, +}; + +#define OBJ_NAME_SUFFIX "_extraBrowser" + +class ExtraBrowsersModel : public QAbstractTableModel { + Q_OBJECT + +public: + inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent) + { + Reset(); + QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + struct Item { + int prevIdx; + QString title; + QString url; + }; + + void TabSelection(bool forward); + + void AddDeleteButton(int idx); + void Reset(); + void CheckToAdd(); + void UpdateItem(Item &item); + void DeleteItem(); + void Apply(); + + QVector items; + QVector deleted; + + QString newTitle; + QString newURL; + +public slots: + void Init(); +}; diff --git a/UI/ffmpeg-utils.cpp b/frontend/utility/FFmpegCodec.cpp similarity index 75% rename from UI/ffmpeg-utils.cpp rename to frontend/utility/FFmpegCodec.cpp index 88e9b0eab..b93edb56b 100644 --- a/UI/ffmpeg-utils.cpp +++ b/frontend/utility/FFmpegCodec.cpp @@ -15,15 +15,12 @@ along with this program. If not, see . ******************************************************************************/ -#include "ffmpeg-utils.hpp" +#include "FFmpegCodec.hpp" +#include "FFmpegFormat.hpp" #include #include -extern "C" { -#include -} - using namespace std; vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility) @@ -47,51 +44,6 @@ vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_comp return codecs; } -static bool is_output_device(const AVClass *avclass) -{ - if (!avclass) - return false; - - switch (avclass->category) { - case AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT: - case AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT: - case AV_CLASS_CATEGORY_DEVICE_OUTPUT: - return true; - default: - return false; - } -} - -vector GetSupportedFormats() -{ - vector formats; - const AVOutputFormat *output_format; - - void *i = 0; - while ((output_format = av_muxer_iterate(&i)) != nullptr) { - if (is_output_device(output_format->priv_class)) - continue; - - formats.emplace_back(output_format); - } - - return formats; -} - -FFmpegCodec FFmpegFormat::GetDefaultEncoder(FFmpegCodecType codec_type) const -{ - const AVCodecID codec_id = codec_type == VIDEO ? video_codec : audio_codec; - if (codec_type == UNKNOWN || codec_id == AV_CODEC_ID_NONE) - return {}; - - if (auto codec = avcodec_find_encoder(codec_id)) - return {codec}; - - /* Fall back to using the format name as the encoder, - * this works for some formats such as FLV. */ - return FFmpegCodec{name, codec_id, codec_type}; -} - bool FFCodecAndFormatCompatible(const char *codec, const char *format) { if (!codec || !format) diff --git a/frontend/utility/FFmpegCodec.hpp b/frontend/utility/FFmpegCodec.hpp new file mode 100644 index 000000000..27893fd8e --- /dev/null +++ b/frontend/utility/FFmpegCodec.hpp @@ -0,0 +1,75 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "FFmpegShared.hpp" + +extern "C" { +#include +#include +} +#include + +struct FFmpegFormat; + +struct FFmpegCodec { + const char *name; + const char *long_name; + int id; + + FFmpegCodecType type; + + FFmpegCodec() = default; + + FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) + : name(name), + long_name(nullptr), + id(id), + type(type) + { + } + + FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) + { + switch (codec->type) { + case AVMEDIA_TYPE_AUDIO: + type = AUDIO; + break; + case AVMEDIA_TYPE_VIDEO: + type = VIDEO; + break; + default: + type = UNKNOWN; + } + } + + bool operator==(const FFmpegCodec &codec) const + { + if (id != codec.id) + return false; + + return strequal(name, codec.name); + } +}; +Q_DECLARE_METATYPE(FFmpegCodec) + +std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); + +bool FFCodecAndFormatCompatible(const char *codec, const char *format); +bool IsBuiltinCodec(const char *codec); +bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/frontend/utility/FFmpegFormat.cpp b/frontend/utility/FFmpegFormat.cpp new file mode 100644 index 000000000..8d8c4e606 --- /dev/null +++ b/frontend/utility/FFmpegFormat.cpp @@ -0,0 +1,66 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "FFmpegFormat.hpp" +#include "FFmpegCodec.hpp" + +using namespace std; + +static bool is_output_device(const AVClass *avclass) +{ + if (!avclass) + return false; + + switch (avclass->category) { + case AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT: + case AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT: + case AV_CLASS_CATEGORY_DEVICE_OUTPUT: + return true; + default: + return false; + } +} + +vector GetSupportedFormats() +{ + vector formats; + const AVOutputFormat *output_format; + + void *i = 0; + while ((output_format = av_muxer_iterate(&i)) != nullptr) { + if (is_output_device(output_format->priv_class)) + continue; + + formats.emplace_back(output_format); + } + + return formats; +} + +FFmpegCodec FFmpegFormat::GetDefaultEncoder(FFmpegCodecType codec_type) const +{ + const AVCodecID codec_id = codec_type == VIDEO ? video_codec : audio_codec; + if (codec_type == UNKNOWN || codec_id == AV_CODEC_ID_NONE) + return {}; + + if (auto codec = avcodec_find_encoder(codec_id)) + return {codec}; + + /* Fall back to using the format name as the encoder, + * this works for some formats such as FLV. */ + return FFmpegCodec{name, codec_id, codec_type}; +} diff --git a/UI/ffmpeg-utils.hpp b/frontend/utility/FFmpegFormat.hpp similarity index 59% rename from UI/ffmpeg-utils.hpp rename to frontend/utility/FFmpegFormat.hpp index 4a0ef5eba..6c44bcd59 100644 --- a/UI/ffmpeg-utils.hpp +++ b/frontend/utility/FFmpegFormat.hpp @@ -17,35 +17,13 @@ #pragma once -#include -#include -#include +#include "FFmpegShared.hpp" extern "C" { #include #include } - -enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; - -/* This needs to handle a few special cases due to how the format is used in the UI: - * - strequal(nullptr, "") must be true - * - strequal("", nullptr) must be true - * - strequal(nullptr, nullptr) must be true - */ -static bool strequal(const char *a, const char *b) -{ - if (!a && !b) - return true; - if (!a && *b == 0) - return true; - if (!b && *a == 0) - return true; - if (!a || !b) - return false; - - return strcmp(a, b) == 0; -} +#include struct FFmpegCodec; @@ -95,52 +73,7 @@ struct FFmpegFormat { return strequal(mime_type, format.mime_type); } }; + Q_DECLARE_METATYPE(FFmpegFormat) -struct FFmpegCodec { - const char *name; - const char *long_name; - int id; - - FFmpegCodecType type; - - FFmpegCodec() = default; - - FFmpegCodec(const char *name, int id, FFmpegCodecType type = UNKNOWN) - : name(name), - long_name(nullptr), - id(id), - type(type) - { - } - - FFmpegCodec(const AVCodec *codec) : name(codec->name), long_name(codec->long_name), id(codec->id) - { - switch (codec->type) { - case AVMEDIA_TYPE_AUDIO: - type = AUDIO; - break; - case AVMEDIA_TYPE_VIDEO: - type = VIDEO; - break; - default: - type = UNKNOWN; - } - } - - bool operator==(const FFmpegCodec &codec) const - { - if (id != codec.id) - return false; - - return strequal(name, codec.name); - } -}; -Q_DECLARE_METATYPE(FFmpegCodec) - std::vector GetSupportedFormats(); -std::vector GetFormatCodecs(const FFmpegFormat &format, bool ignore_compatibility); - -bool FFCodecAndFormatCompatible(const char *codec, const char *format); -bool IsBuiltinCodec(const char *codec); -bool ContainerSupportsCodec(const std::string &container, const std::string &codec); diff --git a/frontend/utility/FFmpegShared.hpp b/frontend/utility/FFmpegShared.hpp new file mode 100644 index 000000000..c0a3a573e --- /dev/null +++ b/frontend/utility/FFmpegShared.hpp @@ -0,0 +1,41 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +enum FFmpegCodecType { AUDIO, VIDEO, UNKNOWN }; + +/* This needs to handle a few special cases due to how the format is used in the UI: + * - strequal(nullptr, "") must be true + * - strequal("", nullptr) must be true + * - strequal(nullptr, nullptr) must be true + */ +static bool strequal(const char *a, const char *b) +{ + if (!a && !b) + return true; + if (!a && *b == 0) + return true; + if (!b && *a == 0) + return true; + if (!a || !b) + return false; + + return strcmp(a, b) == 0; +} diff --git a/UI/goliveapi-censoredjson.cpp b/frontend/utility/GoLiveAPI_CensoredJson.cpp similarity index 98% rename from UI/goliveapi-censoredjson.cpp rename to frontend/utility/GoLiveAPI_CensoredJson.cpp index bd59e5d1e..fc14bd372 100644 --- a/UI/goliveapi-censoredjson.cpp +++ b/frontend/utility/GoLiveAPI_CensoredJson.cpp @@ -1,7 +1,9 @@ -#include "goliveapi-censoredjson.hpp" -#include +#include "GoLiveAPI_CensoredJson.hpp" + #include +#include + void censorRecurse(obs_data_t *); void censorRecurseArray(obs_data_array_t *); diff --git a/UI/goliveapi-censoredjson.hpp b/frontend/utility/GoLiveAPI_CensoredJson.hpp similarity index 99% rename from UI/goliveapi-censoredjson.hpp rename to frontend/utility/GoLiveAPI_CensoredJson.hpp index 20f141c7f..004eace63 100644 --- a/UI/goliveapi-censoredjson.hpp +++ b/frontend/utility/GoLiveAPI_CensoredJson.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include #include diff --git a/UI/goliveapi-network.cpp b/frontend/utility/GoLiveAPI_Network.cpp similarity index 95% rename from UI/goliveapi-network.cpp rename to frontend/utility/GoLiveAPI_Network.cpp index dc557bbc1..b0688a006 100644 --- a/UI/goliveapi-network.cpp +++ b/frontend/utility/GoLiveAPI_Network.cpp @@ -1,17 +1,18 @@ -#include "goliveapi-network.hpp" -#include "goliveapi-censoredjson.hpp" +#include "GoLiveAPI_Network.hpp" +#include "GoLiveAPI_CensoredJson.hpp" + +#include +#include +#include #include -#include -#include -#include "multitrack-video-error.hpp" -#include -#include #include #include - #include +#include + +#include using json = nlohmann::json; diff --git a/UI/goliveapi-network.hpp b/frontend/utility/GoLiveAPI_Network.hpp similarity index 99% rename from UI/goliveapi-network.hpp rename to frontend/utility/GoLiveAPI_Network.hpp index 2a2daecf8..451526068 100644 --- a/UI/goliveapi-network.hpp +++ b/frontend/utility/GoLiveAPI_Network.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include - #include "models/multitrack-video.hpp" +#include + +#include + /** Returns either GO_LIVE_API_PRODUCTION_URL or a command line override. */ QString MultitrackVideoAutoConfigURL(obs_service_t *service); diff --git a/UI/goliveapi-postdata.cpp b/frontend/utility/GoLiveAPI_PostData.cpp similarity index 96% rename from UI/goliveapi-postdata.cpp rename to frontend/utility/GoLiveAPI_PostData.cpp index 9056764db..1aeb871a7 100644 --- a/UI/goliveapi-postdata.cpp +++ b/frontend/utility/GoLiveAPI_PostData.cpp @@ -1,11 +1,10 @@ -#include "goliveapi-postdata.hpp" +#include "GoLiveAPI_PostData.hpp" +#include "models/multitrack-video.hpp" + +#include #include -#include "system-info.hpp" - -#include "models/multitrack-video.hpp" - GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled) { diff --git a/UI/goliveapi-postdata.hpp b/frontend/utility/GoLiveAPI_PostData.hpp similarity index 99% rename from UI/goliveapi-postdata.hpp rename to frontend/utility/GoLiveAPI_PostData.hpp index 23f010883..42c9c921f 100644 --- a/UI/goliveapi-postdata.hpp +++ b/frontend/utility/GoLiveAPI_PostData.hpp @@ -1,9 +1,12 @@ #pragma once -#include -#include -#include #include "models/multitrack-video.hpp" +#include + +#include + +#include + GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled); diff --git a/UI/update/mac-update.cpp b/frontend/utility/MacUpdateThread.cpp similarity index 83% rename from UI/update/mac-update.cpp rename to frontend/utility/MacUpdateThread.cpp index 33fca9430..ffaa3881d 100644 --- a/UI/update/mac-update.cpp +++ b/frontend/utility/MacUpdateThread.cpp @@ -1,20 +1,15 @@ -#include "update-helpers.hpp" -#include "shared-update.hpp" -#include "moc_mac-update.cpp" -#include "obs-app.hpp" +#include "MacUpdateThread.hpp" -#include +#include +#include #include -#include -/* ------------------------------------------------------------------------ */ +#include "moc_MacUpdateThread.cpp" static const char *MAC_BRANCHES_URL = "https://obsproject.com/update_studio/branches.json"; static const char *MAC_DEFAULT_BRANCH = "stable"; -/* ------------------------------------------------------------------------ */ - bool GetBranch(std::string &selectedBranch) { const char *config_branch = config_get_string(App()->GetAppConfig(), "General", "UpdateBranch"); @@ -39,8 +34,6 @@ bool GetBranch(std::string &selectedBranch) return found; } -/* ------------------------------------------------------------------------ */ - void MacUpdateThread::infoMsg(const QString &title, const QString &text) { OBSMessageBox::information(App()->GetMainWindow(), title, text); diff --git a/frontend/utility/MacUpdateThread.hpp b/frontend/utility/MacUpdateThread.hpp new file mode 100644 index 000000000..c4bddbd10 --- /dev/null +++ b/frontend/utility/MacUpdateThread.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +class MacUpdateThread : public QThread { + Q_OBJECT + + bool manualUpdate; + + virtual void run() override; + + void info(const QString &title, const QString &text); + +signals: + void Result(const QString &branch, bool manual); + +private slots: + void infoMsg(const QString &title, const QString &text); + +public: + MacUpdateThread(bool manual) : manualUpdate(manual) {} +}; diff --git a/frontend/utility/MissingFilesModel.cpp b/frontend/utility/MissingFilesModel.cpp new file mode 100644 index 000000000..bda79680a --- /dev/null +++ b/frontend/utility/MissingFilesModel.cpp @@ -0,0 +1,270 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "MissingFilesModel.hpp" + +#include + +#include +#include + +#include "moc_MissingFilesModel.cpp" + +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +// TODO: Fix redefinition error of due to clash with enums defined in importer code. +enum MissingFilesColumn { Source, OriginalPath, NewPath, State, Count }; + +MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent) +{ + QStyle *style = QApplication::style(); + + warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); +} + +int MissingFilesModel::rowCount(const QModelIndex &) const +{ + return files.length(); +} + +int MissingFilesModel::columnCount(const QModelIndex &) const +{ + return MissingFilesColumn::Count; +} + +int MissingFilesModel::found() const +{ + int res = 0; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != Missing && files[i].state != Cleared) + res++; + } + + return res; +} + +QVariant MissingFilesModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= files.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + QFileInfo fi(files[index.row()].originalPath); + + switch (index.column()) { + case MissingFilesColumn::Source: + result = files[index.row()].source; + break; + case MissingFilesColumn::OriginalPath: + result = fi.fileName(); + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + case MissingFilesColumn::State: + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + } + break; + } + } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str()); + + if (source) { + result = main->GetSourceIcon(obs_source_get_id(source)); + } + } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) { + QFont font = QFont(); + font.setBold(true); + + result = font; + } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) { + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + + default: + break; + } + } else if (role == Qt::ToolTipRole) { + switch (index.column()) { + case MissingFilesColumn::OriginalPath: + result = files[index.row()].originalPath; + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + default: + break; + } + } + + return result; +} + +Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == MissingFilesColumn::OriginalPath) { + flags &= ~Qt::ItemIsEditable; + } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void MissingFilesModel::fileCheckLoop(QList files, QString path, bool skipPrompt) +{ + loop = false; + QUrl url = QUrl().fromLocalFile(path); + QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile); + + bool prompted = skipPrompt; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != MissingFilesState::Missing) + continue; + + QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); + QString filename = origFile.fileName(); + QString testFile = dir + filename; + + if (os_file_exists(testFile.toStdString().c_str())) { + if (!prompted) { + QMessageBox::StandardButton button = + QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"), + QTStr("MissingFiles.AutoSearchText")); + + if (button == QMessageBox::No) + break; + + prompted = true; + } + QModelIndex in = index(i, MissingFilesColumn::NewPath); + setData(in, testFile, 0); + } + } + loop = true; +} + +bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == MissingFilesRole::NewPathsToProcessRole) { + QStringList list = value.toStringList(); + + int row = index.row() + 1; + beginInsertRows(QModelIndex(), row, row); + + MissingFileEntry entry; + entry.originalPath = list[0].replace("\\", "/"); + entry.source = list[1]; + + files.insert(row, entry); + row++; + + endInsertRows(); + + success = true; + } else { + QString path = value.toString(); + if (index.column() == MissingFilesColumn::NewPath) { + files[index.row()].newPath = value.toString(); + QString fileName = QUrl(path).fileName(); + QString origFileName = QUrl(files[index.row()].originalPath).fileName(); + + if (path.isEmpty()) { + files[index.row()].state = MissingFilesState::Missing; + } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) { + files[index.row()].state = MissingFilesState::Cleared; + } else if (fileName.compare(origFileName) == 0) { + files[index.row()].state = MissingFilesState::Found; + + if (loop) + fileCheckLoop(files, path, false); + } else { + files[index.row()].state = MissingFilesState::Replaced; + + if (loop) + fileCheckLoop(files, path, false); + } + + emit dataChanged(index, index); + success = true; + } + } + + return success; +} + +QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case MissingFilesColumn::State: + result = QTStr("MissingFiles.State"); + break; + case MissingFilesColumn::Source: + result = QTStr("Basic.Main.Source"); + break; + case MissingFilesColumn::OriginalPath: + result = QTStr("MissingFiles.MissingFile"); + break; + case MissingFilesColumn::NewPath: + result = QTStr("MissingFiles.NewFile"); + break; + } + } + + return result; +} diff --git a/UI/window-missing-files.hpp b/frontend/utility/MissingFilesModel.hpp similarity index 55% rename from UI/window-missing-files.hpp rename to frontend/utility/MissingFilesModel.hpp index 2b24e0f8e..4b7057e84 100644 --- a/UI/window-missing-files.hpp +++ b/frontend/utility/MissingFilesModel.hpp @@ -17,42 +17,13 @@ #pragma once -#include -#include -#include "obs-app.hpp" -#include "ui_OBSMissingFiles.h" - -class MissingFilesModel; +#include +#include enum MissingFilesState { Missing, Found, Replaced, Cleared }; + Q_DECLARE_METATYPE(MissingFilesState); -class OBSMissingFiles : public QDialog { - Q_OBJECT - Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon DESIGNABLE true) - - QPointer filesModel; - std::unique_ptr ui; - -public: - explicit OBSMissingFiles(obs_missing_files_t *files, QWidget *parent = nullptr); - virtual ~OBSMissingFiles() override; - - void addMissingFile(const char *originalPath, const char *sourceName); - - QIcon GetWarningIcon(); - void SetWarningIcon(const QIcon &icon); - -private: - void saveFiles(); - void browseFolders(); - - obs_missing_files_t *fileStore; - -public slots: - void dataChanged(); -}; - class MissingFilesModel : public QAbstractTableModel { Q_OBJECT @@ -87,26 +58,3 @@ private: void fileCheckLoop(QList files, QString path, bool skipPrompt); }; - -class MissingFilesPathItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, - const QModelIndex &index) const override; - - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - bool isOutput; - QString defaultPath; - const char *PATH_LIST_PROP = "pathList"; - - void handleBrowse(QWidget *container); - void handleClear(QWidget *container); -}; diff --git a/frontend/utility/MissingFilesPathItemDelegate.cpp b/frontend/utility/MissingFilesPathItemDelegate.cpp new file mode 100644 index 000000000..f1e722511 --- /dev/null +++ b/frontend/utility/MissingFilesPathItemDelegate.cpp @@ -0,0 +1,164 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "MissingFilesPathItemDelegate.hpp" + +#include + +#include +#include +#include +#include + +#include "moc_MissingFilesPathItemDelegate.cpp" + +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &) const +{ + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in input cells + if (isOutput) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + + return container; +} + +void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + model->setData(index, list); + } else + model->setData(index, list, MissingFilesRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text(), 0); + } +} + +void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) +{ + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = + QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr); + +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } + + if (isSet) + emit commitData(container); +} + +void MissingFilesPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear")); + container->findChild()->clearFocus(); + ((QWidget *)container->parent())->setFocus(); + emit commitData(container); +} diff --git a/frontend/utility/MissingFilesPathItemDelegate.hpp b/frontend/utility/MissingFilesPathItemDelegate.hpp new file mode 100644 index 000000000..8b9606226 --- /dev/null +++ b/frontend/utility/MissingFilesPathItemDelegate.hpp @@ -0,0 +1,43 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class MissingFilesPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); +}; diff --git a/UI/multitrack-video-error.cpp b/frontend/utility/MultitrackVideoError.cpp similarity index 94% rename from UI/multitrack-video-error.cpp rename to frontend/utility/MultitrackVideoError.cpp index 8f4b478b4..5f7d95b1c 100644 --- a/UI/multitrack-video-error.cpp +++ b/frontend/utility/MultitrackVideoError.cpp @@ -1,8 +1,9 @@ -#include "multitrack-video-error.hpp" +#include "MultitrackVideoError.hpp" + +#include #include #include -#include "obs-app.hpp" MultitrackVideoError MultitrackVideoError::critical(QString error) { diff --git a/UI/multitrack-video-error.hpp b/frontend/utility/MultitrackVideoError.hpp similarity index 99% rename from UI/multitrack-video-error.hpp rename to frontend/utility/MultitrackVideoError.hpp index 7ce79fe72..b36f2f274 100644 --- a/UI/multitrack-video-error.hpp +++ b/frontend/utility/MultitrackVideoError.hpp @@ -1,4 +1,5 @@ #pragma once + #include class QWidget; diff --git a/UI/multitrack-video-output.cpp b/frontend/utility/MultitrackVideoOutput.cpp similarity index 97% rename from UI/multitrack-video-output.cpp rename to frontend/utility/MultitrackVideoOutput.cpp index 2a54505ee..1ee0fe170 100644 --- a/UI/multitrack-video-output.cpp +++ b/frontend/utility/MultitrackVideoOutput.cpp @@ -1,42 +1,21 @@ -#include "multitrack-video-output.hpp" +#include "MultitrackVideoError.hpp" +#include "MultitrackVideoOutput.hpp" +#include "models/multitrack-video.hpp" +#include "GoLiveAPI_Network.hpp" +#include "GoLiveAPI_PostData.hpp" + +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include +#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include #include -#include -#include -#include +#include +#include #include #include -#include -#include - -#include "system-info.hpp" -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" -#include "models/multitrack-video.hpp" +#include Qt::ConnectionType BlockingConnectionTypeFor(QObject *object) { diff --git a/UI/multitrack-video-output.hpp b/frontend/utility/MultitrackVideoOutput.hpp similarity index 97% rename from UI/multitrack-video-output.hpp rename to frontend/utility/MultitrackVideoOutput.hpp index 024cd8bca..851ef3562 100644 --- a/UI/multitrack-video-output.hpp +++ b/frontend/utility/MultitrackVideoOutput.hpp @@ -3,16 +3,16 @@ #include #include -#include +#include #include #include +#include #include -#include - #define NOMINMAX class QString; +class QWidget; void StreamStopHandler(void *arg, calldata_t *data); void StreamDeactivateHandler(void *arg, calldata_t *data); diff --git a/frontend/utility/OBSEventFilter.hpp b/frontend/utility/OBSEventFilter.hpp new file mode 100644 index 000000000..fe83ea674 --- /dev/null +++ b/frontend/utility/OBSEventFilter.hpp @@ -0,0 +1,35 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +typedef std::function EventFilterFunc; + +class OBSEventFilter : public QObject { + Q_OBJECT +public: + OBSEventFilter(EventFilterFunc filter_) : filter(filter_) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) { return filter(obj, event); } + +public: + EventFilterFunc filter; +}; diff --git a/UI/obs-proxy-style.cpp b/frontend/utility/OBSProxyStyle.cpp similarity index 97% rename from UI/obs-proxy-style.cpp rename to frontend/utility/OBSProxyStyle.cpp index 4daf7f47a..e598e1938 100644 --- a/UI/obs-proxy-style.cpp +++ b/frontend/utility/OBSProxyStyle.cpp @@ -1,5 +1,6 @@ -#include "obs-proxy-style.hpp" -#include +#include "OBSProxyStyle.hpp" + +#include static inline uint qt_intensity(uint r, uint g, uint b) { diff --git a/UI/obs-proxy-style.hpp b/frontend/utility/OBSProxyStyle.hpp similarity index 100% rename from UI/obs-proxy-style.hpp rename to frontend/utility/OBSProxyStyle.hpp diff --git a/frontend/utility/OBSSparkle.hpp b/frontend/utility/OBSSparkle.hpp new file mode 100644 index 000000000..3f4cffc3c --- /dev/null +++ b/frontend/utility/OBSSparkle.hpp @@ -0,0 +1,24 @@ +#pragma once + +#import + +class QAction; +#ifdef __OBJC__ +@class OBSUpdateDelegate; +#endif + +class OBSSparkle : public QObject { + Q_OBJECT + +public: + OBSSparkle(const char *branch, QAction *checkForUpdatesAction); + void setBranch(const char *branch); + void checkForUpdates(bool manualCheck); + +private: +#ifdef __OBJC__ + OBSUpdateDelegate *updaterDelegate; +#else + void *updaterDelegate; +#endif +}; diff --git a/frontend/utility/OBSSparkle.mm b/frontend/utility/OBSSparkle.mm new file mode 100644 index 000000000..bfb1911aa --- /dev/null +++ b/frontend/utility/OBSSparkle.mm @@ -0,0 +1,30 @@ +#import "OBSSparkle.hpp" +#import "OBSUpdateDelegate.h" + +OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) +{ + @autoreleasepool { + updaterDelegate = [[OBSUpdateDelegate alloc] init]; + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; + updaterDelegate.updaterController = + [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate + userDriverDelegate:nil]; + [updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction]; + } +} + +void OBSSparkle::setBranch(const char *branch) +{ + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; +} + +void OBSSparkle::checkForUpdates(bool manualCheck) +{ + @autoreleasepool { + if (manualCheck) { + [updaterDelegate.updaterController checkForUpdates:nil]; + } else { + [updaterDelegate.updaterController.updater checkForUpdatesInBackground]; + } + } +} diff --git a/frontend/utility/OBSTheme.hpp b/frontend/utility/OBSTheme.hpp new file mode 100644 index 000000000..dbb3b0a6c --- /dev/null +++ b/frontend/utility/OBSTheme.hpp @@ -0,0 +1,44 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +#include + +struct OBSTheme { + /* internal name, must be unique */ + QString id; + QString name; + QString author; + QString extends; + + /* First ancestor base theme */ + QString parent; + /* Dependencies from root to direct ancestor */ + QStringList dependencies; + /* File path */ + std::filesystem::path location; + std::filesystem::path filename; /* Filename without extension */ + + bool isDark; + bool isVisible; /* Whether it should be shown to the user */ + bool isBaseTheme; /* Whether it is a "style" or variant */ + bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ +}; diff --git a/UI/obs-app-theming.hpp b/frontend/utility/OBSThemeVariable.hpp similarity index 71% rename from UI/obs-app-theming.hpp rename to frontend/utility/OBSThemeVariable.hpp index cbaed7f24..f04f90cc5 100644 --- a/UI/obs-app-theming.hpp +++ b/frontend/utility/OBSThemeVariable.hpp @@ -17,33 +17,9 @@ #pragma once +#include #include -#include - -struct OBSThemeVariable; - -struct OBSTheme { - /* internal name, must be unique */ - QString id; - QString name; - QString author; - QString extends; - - /* First ancestor base theme */ - QString parent; - /* Dependencies from root to direct ancestor */ - QStringList dependencies; - /* File path */ - std::filesystem::path location; - std::filesystem::path filename; /* Filename without extension */ - - bool isDark; - bool isVisible; /* Whether it should be shown to the user */ - bool isBaseTheme; /* Whether it is a "style" or variant */ - bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ -}; - struct OBSThemeVariable { enum VariableType { Color, /* RGB color value*/ diff --git a/frontend/utility/OBSTranslator.cpp b/frontend/utility/OBSTranslator.cpp new file mode 100644 index 000000000..f61354cce --- /dev/null +++ b/frontend/utility/OBSTranslator.cpp @@ -0,0 +1,35 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSTranslator.hpp" + +#include + +#include + +#include "moc_OBSTranslator.cpp" + +QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const +{ + const char *out = nullptr; + QString str(sourceText); + str.replace(" ", ""); + if (!App()->TranslateString(QT_TO_UTF8(str), &out)) + return QString(sourceText); + + return QT_UTF8(out); +} diff --git a/frontend/utility/OBSTranslator.hpp b/frontend/utility/OBSTranslator.hpp new file mode 100644 index 000000000..a962306f3 --- /dev/null +++ b/frontend/utility/OBSTranslator.hpp @@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +class OBSTranslator : public QTranslator { + Q_OBJECT + +public: + virtual bool isEmpty() const override { return false; } + + virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, + int n) const override; +}; diff --git a/frontend/utility/OBSUpdateDelegate.h b/frontend/utility/OBSUpdateDelegate.h new file mode 100644 index 000000000..1e6f68306 --- /dev/null +++ b/frontend/utility/OBSUpdateDelegate.h @@ -0,0 +1,37 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#pragma once + +#import +#import + +#import + +@interface OBSUpdateDelegate : NSObject { +} +@property (copy) NSString *_Nonnull branch; +@property (nonatomic) SPUStandardUpdaterController *_Nonnull updaterController; + +- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater; +- (void)observeCanCheckForUpdatesWithAction:(nonnull QAction *)action; +- (void)observeValueForKeyPath:(NSString *_Nullable)keyPath + ofObject:(id _Nullable)object + change:(NSDictionary *_Nullable)change + context:(void *_Nullable)context; + +@end diff --git a/frontend/utility/OBSUpdateDelegate.mm b/frontend/utility/OBSUpdateDelegate.mm new file mode 100644 index 000000000..734091e8b --- /dev/null +++ b/frontend/utility/OBSUpdateDelegate.mm @@ -0,0 +1,40 @@ +#import "OBSUpdateDelegate.h" + +@implementation OBSUpdateDelegate { +} + +@synthesize branch; + +- (nonnull NSSet *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater +{ + return [NSSet setWithObject:branch]; +} + +- (void)observeCanCheckForUpdatesWithAction:(nonnull QAction *)action; +{ + [_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) + options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) + context:(void *) action]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) { + QAction *menuAction = (QAction *) context; + menuAction->setEnabled(_updaterController.updater.canCheckForUpdates); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)dealloc +{ + @autoreleasepool { + [_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))]; + } +} + +@end diff --git a/frontend/utility/QuickTransition.cpp b/frontend/utility/QuickTransition.cpp new file mode 100644 index 000000000..f0179a6cd --- /dev/null +++ b/frontend/utility/QuickTransition.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "QuickTransition.hpp" + +#include + +#include + +static inline QString MakeQuickTransitionText(QuickTransition *qt) +{ + QString name; + + if (!qt->fadeToBlack) + name = QT_UTF8(obs_source_get_name(qt->source)); + else + name = QTStr("FadeToBlack"); + + if (!obs_transition_fixed(qt->source)) + name += QString(" (%1ms)").arg(QString::number(qt->duration)); + return name; +} + +void QuickTransition::SourceRenamed(void *param, calldata_t *) +{ + QuickTransition *qt = reinterpret_cast(param); + + QString hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); + + obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); +} diff --git a/frontend/utility/QuickTransition.hpp b/frontend/utility/QuickTransition.hpp new file mode 100644 index 000000000..a17f107f2 --- /dev/null +++ b/frontend/utility/QuickTransition.hpp @@ -0,0 +1,50 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#include + +using namespace std; + +class QPushButton; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; diff --git a/UI/remote-text.cpp b/frontend/utility/RemoteTextThread.cpp similarity index 98% rename from UI/remote-text.cpp rename to frontend/utility/RemoteTextThread.cpp index 4b81e32bb..9541fd439 100644 --- a/UI/remote-text.cpp +++ b/frontend/utility/RemoteTextThread.cpp @@ -15,16 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include +#include "RemoteTextThread.hpp" + +#include + #include -#include "obs-app.hpp" -#include "moc_remote-text.cpp" +#include + +#include "moc_RemoteTextThread.cpp" using namespace std; static auto curl_deleter = [](CURL *curl) { curl_easy_cleanup(curl); }; + using Curl = unique_ptr; static size_t string_write(char *ptr, size_t size, size_t nmemb, string &str) diff --git a/UI/remote-text.hpp b/frontend/utility/RemoteTextThread.hpp similarity index 98% rename from UI/remote-text.hpp rename to frontend/utility/RemoteTextThread.hpp index f6f8efbc1..af24a51bc 100644 --- a/UI/remote-text.hpp +++ b/frontend/utility/RemoteTextThread.hpp @@ -18,8 +18,6 @@ #pragma once #include -#include -#include class RemoteTextThread : public QThread { Q_OBJECT diff --git a/frontend/utility/RemuxEntryPathItemDelegate.cpp b/frontend/utility/RemuxEntryPathItemDelegate.cpp new file mode 100644 index 000000000..0349d1fab --- /dev/null +++ b/frontend/utility/RemuxEntryPathItemDelegate.cpp @@ -0,0 +1,211 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "RemuxEntryPathItemDelegate.hpp" +#include "RemuxQueueModel.hpp" + +#include + +#include + +#include +#include +#include + +#include "moc_RemuxEntryPathItemDelegate.cpp" + +RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), + isOutput(isOutput), + defaultPath(defaultPath) +{ +} + +QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) { + // Never allow modification of rows that are + // in progress. + return Q_NULLPTR; + } else if (isOutput && state != RemuxEntryState::Ready) { + // Do not allow modification of output rows + // that aren't associated with a valid input. + return Q_NULLPTR; + } else if (!isOutput && state == RemuxEntryState::Complete) { + // Don't allow modification of rows that are + // already complete. + return Q_NULLPTR; + } else { + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse(container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear(container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in output cells + // or the insertion point's input cell. + if (!isOutput && state != RemuxEntryState::Empty) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + return container; + } +} + +void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void RemuxEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + if (list.size() > 0) + model->setData(index, list); + } else + model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + RemuxEntryState state = index.model() + ->index(index.row(), RemuxEntryColumn::State) + .data(RemuxEntryRole::EntryStateRole) + .value(); + + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + if (isOutput) { + if (state != Ready) { + QColor background = + localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window); + + localOption.backgroundBrush = QBrush(background); + } + } + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter); +} + +void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty()) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern); + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, QStringList() << newPath); + isSet = true; + } + } else { + QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath, + QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } +#ifdef __APPLE__ + // TODO: Revisit when QTBUG-42661 is fixed + container->window()->raise(); +#endif + } + + if (isSet) + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, QStringList()); + + emit commitData(container); +} + +void RemuxEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} diff --git a/frontend/utility/RemuxEntryPathItemDelegate.hpp b/frontend/utility/RemuxEntryPathItemDelegate.hpp new file mode 100644 index 000000000..dcbd7086f --- /dev/null +++ b/frontend/utility/RemuxEntryPathItemDelegate.hpp @@ -0,0 +1,46 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class RemuxEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/frontend/utility/RemuxQueueModel.cpp b/frontend/utility/RemuxQueueModel.cpp new file mode 100644 index 000000000..cae17b138 --- /dev/null +++ b/frontend/utility/RemuxQueueModel.cpp @@ -0,0 +1,380 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "RemuxQueueModel.hpp" + +#include + +#include +#include + +#include "moc_RemuxQueueModel.cpp" + +int RemuxQueueModel::rowCount(const QModelIndex &) const +{ + return queue.length() + (isProcessing ? 0 : 1); +} + +int RemuxQueueModel::columnCount(const QModelIndex &) const +{ + return RemuxEntryColumn::Count; +} + +QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= queue.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + result = queue[index.row()].sourcePath; + break; + case RemuxEntryColumn::OutputPath: + result = queue[index.row()].targetPath; + break; + } + } else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) { + result = getIcon(queue[index.row()].state); + } else if (role == RemuxEntryRole::EntryStateRole) { + result = queue[index.row()].state; + } + + return result; +} + +QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case RemuxEntryColumn::State: + result = QString(); + break; + case RemuxEntryColumn::InputPath: + result = QTStr("Remux.SourceFile"); + break; + case RemuxEntryColumn::OutputPath: + result = QTStr("Remux.TargetFile"); + break; + } + } + + return result; +} + +Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == RemuxEntryColumn::InputPath) { + flags |= Qt::ItemIsEditable; + } else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool success = false; + + if (role == RemuxEntryRole::NewPathsToProcessRole) { + QStringList pathList = value.toStringList(); + + if (pathList.size() == 0) { + if (index.row() < queue.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (pathList.size() >= 1 && index.row() < queue.length()) { + queue[index.row()].sourcePath = pathList[0]; + checkInputPath(index.row()); + + pathList.removeAt(0); + + success = true; + } + + if (pathList.size() > 0) { + int row = index.row(); + int lastRow = row + pathList.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : pathList) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + queue.insert(row, entry); + row++; + } + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + + success = true; + } + } + } else if (index.row() == queue.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + RemuxQueueEntry entry; + entry.sourcePath = path; + entry.state = RemuxEntryState::Empty; + + beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1); + queue.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + success = true; + } + } else { + QString path = value.toString(); + + if (path.isEmpty()) { + if (index.column() == RemuxEntryColumn::InputPath) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + queue.removeAt(index.row()); + endRemoveRows(); + } + } else { + switch (index.column()) { + case RemuxEntryColumn::InputPath: + queue[index.row()].sourcePath = value.toString(); + checkInputPath(index.row()); + success = true; + break; + case RemuxEntryColumn::OutputPath: + queue[index.row()].targetPath = value.toString(); + emit dataChanged(index, index); + success = true; + break; + } + } + } + + return success; +} + +QVariant RemuxQueueModel::getIcon(RemuxEntryState state) +{ + QVariant icon; + QStyle *style = QApplication::style(); + + switch (state) { + case RemuxEntryState::Complete: + icon = style->standardIcon(QStyle::SP_DialogApplyButton); + break; + + case RemuxEntryState::InProgress: + icon = style->standardIcon(QStyle::SP_ArrowRight); + break; + + case RemuxEntryState::Error: + icon = style->standardIcon(QStyle::SP_DialogCancelButton); + break; + + case RemuxEntryState::InvalidPath: + icon = style->standardIcon(QStyle::SP_MessageBoxWarning); + break; + + default: + break; + } + + return icon; +} + +void RemuxQueueModel::checkInputPath(int row) +{ + RemuxQueueEntry &entry = queue[row]; + + if (entry.sourcePath.isEmpty()) { + entry.state = RemuxEntryState::Empty; + } else { + entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath); + QFileInfo fileInfo(entry.sourcePath); + if (fileInfo.exists()) + entry.state = RemuxEntryState::Ready; + else + entry.state = RemuxEntryState::InvalidPath; + + QString newExt = ".mp4"; + QString suffix = fileInfo.suffix(); + + if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) { + newExt = ".remuxed." + suffix; + } + + if (entry.state == RemuxEntryState::Ready) + entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() + + fileInfo.completeBaseName() + newExt); + } + + if (entry.state == RemuxEntryState::Ready && isProcessing) + entry.state = RemuxEntryState::Pending; + + emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count)); +} + +QFileInfoList RemuxQueueModel::checkForOverwrites() const +{ + QFileInfoList list; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Ready) { + QFileInfo fileInfo(entry.targetPath); + if (fileInfo.exists()) { + list.append(fileInfo); + } + } + } + + return list; +} + +bool RemuxQueueModel::checkForErrors() const +{ + bool hasErrors = false; + + for (const RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Error) { + hasErrors = true; + break; + } + } + + return hasErrors; +} + +void RemuxQueueModel::clearAll() +{ + beginRemoveRows(QModelIndex(), 0, queue.size() - 1); + queue.clear(); + endRemoveRows(); +} + +void RemuxQueueModel::clearFinished() +{ + int index = 0; + + for (index = 0; index < queue.size(); index++) { + const RemuxQueueEntry &entry = queue[index]; + if (entry.state == RemuxEntryState::Complete) { + beginRemoveRows(QModelIndex(), index, index); + queue.removeAt(index); + endRemoveRows(); + index--; + } + } +} + +bool RemuxQueueModel::canClearFinished() const +{ + bool canClearFinished = false; + for (const RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Complete) { + canClearFinished = true; + break; + } + + return canClearFinished; +} + +void RemuxQueueModel::beginProcessing() +{ + for (RemuxQueueEntry &entry : queue) + if (entry.state == RemuxEntryState::Ready) + entry.state = RemuxEntryState::Pending; + + // Signal that the insertion point no longer exists. + beginRemoveRows(QModelIndex(), queue.length(), queue.length()); + endRemoveRows(); + + isProcessing = true; + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +void RemuxQueueModel::endProcessing() +{ + for (RemuxQueueEntry &entry : queue) { + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::Ready; + } + } + + // Signal that the insertion point exists again. + isProcessing = false; + if (!autoRemux) { + beginInsertRows(QModelIndex(), queue.length(), queue.length()); + endInsertRows(); + } + + emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State)); +} + +bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath) +{ + bool anyStarted = false; + + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::Pending) { + entry.state = RemuxEntryState::InProgress; + + inputPath = entry.sourcePath; + outputPath = entry.targetPath; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + anyStarted = true; + break; + } + } + + return anyStarted; +} + +void RemuxQueueModel::finishEntry(bool success) +{ + for (int row = 0; row < queue.length(); row++) { + RemuxQueueEntry &entry = queue[row]; + if (entry.state == RemuxEntryState::InProgress) { + if (success) + entry.state = RemuxEntryState::Complete; + else + entry.state = RemuxEntryState::Error; + + QModelIndex index = this->index(row, RemuxEntryColumn::State); + emit dataChanged(index, index); + + break; + } + } +} diff --git a/frontend/utility/RemuxQueueModel.hpp b/frontend/utility/RemuxQueueModel.hpp new file mode 100644 index 000000000..954b5fd5e --- /dev/null +++ b/frontend/utility/RemuxQueueModel.hpp @@ -0,0 +1,78 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error }; + +Q_DECLARE_METATYPE(RemuxEntryState); + +enum RemuxEntryColumn { + State, + InputPath, + OutputPath, + + Count +}; + +enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +class RemuxQueueModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSRemux; + +public: + RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + QFileInfoList checkForOverwrites() const; + bool checkForErrors() const; + void beginProcessing(); + void endProcessing(); + bool beginNextEntry(QString &inputPath, QString &outputPath); + void finishEntry(bool success); + bool canClearFinished() const; + void clearFinished(); + void clearAll(); + + bool autoRemux = false; + +private: + struct RemuxQueueEntry { + RemuxEntryState state; + + QString sourcePath; + QString targetPath; + }; + + QList queue; + bool isProcessing; + + static QVariant getIcon(RemuxEntryState state); + + void checkInputPath(int row); +}; diff --git a/frontend/utility/RemuxWorker.cpp b/frontend/utility/RemuxWorker.cpp new file mode 100644 index 000000000..e9d54093b --- /dev/null +++ b/frontend/utility/RemuxWorker.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "RemuxWorker.hpp" + +#include +#include + +void RemuxWorker::UpdateProgress(float percent) +{ + if (abs(lastProgress - percent) < 0.1f) + return; + + emit updateProgress(percent); + lastProgress = percent; +} + +void RemuxWorker::remux(const QString &source, const QString &target) +{ + isWorking = true; + + auto callback = [](void *data, float percent) { + RemuxWorker *rw = static_cast(data); + + QMutexLocker lock(&rw->updateMutex); + + rw->UpdateProgress(percent); + + return rw->isWorking; + }; + + bool stopped = false; + bool success = false; + + media_remux_job_t mr_job = nullptr; + if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) { + + success = media_remux_job_process(mr_job, callback, this); + + media_remux_job_destroy(mr_job); + + stopped = !isWorking; + } + + isWorking = false; + + emit remuxFinished(!stopped && success); +} diff --git a/frontend/utility/RemuxWorker.hpp b/frontend/utility/RemuxWorker.hpp new file mode 100644 index 000000000..9816e3501 --- /dev/null +++ b/frontend/utility/RemuxWorker.hpp @@ -0,0 +1,44 @@ +/****************************************************************************** + Copyright (C) 2014 by Ruwen Hahn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +class RemuxWorker : public QObject { + Q_OBJECT + + QMutex updateMutex; + + bool isWorking; + + float lastProgress; + void UpdateProgress(float percent); + + explicit RemuxWorker() : isWorking(false) {} + virtual ~RemuxWorker(){}; + +private slots: + void remux(const QString &source, const QString &target); + +signals: + void updateProgress(float percent); + void remuxFinished(bool success); + + friend class OBSRemux; +}; diff --git a/frontend/utility/SceneRenameDelegate.cpp b/frontend/utility/SceneRenameDelegate.cpp new file mode 100644 index 000000000..e505c5205 --- /dev/null +++ b/frontend/utility/SceneRenameDelegate.cpp @@ -0,0 +1,55 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "SceneRenameDelegate.hpp" + +#include +#include + +#include "moc_SceneRenameDelegate.cpp" + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} diff --git a/frontend/utility/SceneRenameDelegate.hpp b/frontend/utility/SceneRenameDelegate.hpp new file mode 100644 index 000000000..d5212f32e --- /dev/null +++ b/frontend/utility/SceneRenameDelegate.hpp @@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/UI/window-basic-main-screenshot.cpp b/frontend/utility/ScreenshotObj.cpp similarity index 90% rename from UI/window-basic-main-screenshot.cpp rename to frontend/utility/ScreenshotObj.cpp index a0b3622d1..5b7bc86ca 100644 --- a/UI/window-basic-main-screenshot.cpp +++ b/frontend/utility/ScreenshotObj.cpp @@ -15,8 +15,9 @@ along with this program. If not, see . ******************************************************************************/ -#include "window-basic-main.hpp" -#include "screenshot-obj.hpp" +#include "ScreenshotObj.hpp" + +#include #include @@ -27,9 +28,9 @@ #pragma comment(lib, "windowscodecs.lib") #endif -static void ScreenshotTick(void *param, float); +#include "moc_ScreenshotObj.cpp" -/* ========================================================================= */ +static void ScreenshotTick(void *param, float); ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source)) { @@ -278,8 +279,6 @@ void ScreenshotObj::MuxAndFinish() deleteLater(); } -/* ========================================================================= */ - #define STAGE_SCREENSHOT 0 #define STAGE_DOWNLOAD 1 #define STAGE_COPY_AND_SAVE 2 @@ -313,35 +312,3 @@ static void ScreenshotTick(void *param, float) data->stage++; } - -void OBSBasic::Screenshot(OBSSource source) -{ - if (!!screenshotData) { - blog(LOG_WARNING, "Cannot take new screenshot, " - "screenshot currently in progress"); - return; - } - - screenshotData = new ScreenshotObj(source); -} - -void OBSBasic::ScreenshotSelectedSource() -{ - OBSSceneItem item = GetCurrentSceneItem(); - if (item) { - Screenshot(obs_sceneitem_get_source(item)); - } else { - blog(LOG_INFO, "Could not take a source screenshot: " - "no source selected"); - } -} - -void OBSBasic::ScreenshotProgram() -{ - Screenshot(GetProgramSource()); -} - -void OBSBasic::ScreenshotScene() -{ - Screenshot(GetCurrentSceneSource()); -} diff --git a/UI/screenshot-obj.hpp b/frontend/utility/ScreenshotObj.hpp similarity index 98% rename from UI/screenshot-obj.hpp rename to frontend/utility/ScreenshotObj.hpp index 037b7f84d..01a4fefd8 100644 --- a/UI/screenshot-obj.hpp +++ b/frontend/utility/ScreenshotObj.hpp @@ -17,11 +17,13 @@ #pragma once -#include -#include -#include #include +#include +#include + +#include + class ScreenshotObj : public QObject { Q_OBJECT diff --git a/frontend/utility/SettingsEventFilter.hpp b/frontend/utility/SettingsEventFilter.hpp new file mode 100644 index 000000000..d8ffa8061 --- /dev/null +++ b/frontend/utility/SettingsEventFilter.hpp @@ -0,0 +1,51 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +#include +#include + +class SettingsEventFilter : public QObject { + QScopedPointer shortcutFilter; + +public: + inline SettingsEventFilter() : shortcutFilter((OBSEventFilter *)CreateShortcutFilter()) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) override + { + int key; + + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + key = static_cast(event)->key(); + if (key == Qt::Key_Escape) { + return false; + } + default: + break; + } + + return shortcutFilter->filter(obj, event); + } +}; diff --git a/frontend/utility/SimpleOutput.cpp b/frontend/utility/SimpleOutput.cpp new file mode 100644 index 000000000..4e783dd7a --- /dev/null +++ b/frontend/utility/SimpleOutput.cpp @@ -0,0 +1,904 @@ +#include "SimpleOutput.hpp" + +#include +#include +#include + +#include + +using namespace std; + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +extern bool EncoderAvailable(const char *encoder); + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} diff --git a/frontend/utility/SimpleOutput.hpp b/frontend/utility/SimpleOutput.hpp new file mode 100644 index 000000000..46d6fc381 --- /dev/null +++ b/frontend/utility/SimpleOutput.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "BasicOutputHandler.hpp" + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + std::string videoEncoder; + std::string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; diff --git a/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp new file mode 100644 index 000000000..f0535f4d2 --- /dev/null +++ b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; diff --git a/frontend/utility/SurfaceEventFilter.hpp b/frontend/utility/SurfaceEventFilter.hpp new file mode 100644 index 000000000..de6f12600 --- /dev/null +++ b/frontend/utility/SurfaceEventFilter.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include +#include + +class SurfaceEventFilter : public QObject { + OBSQTDisplay *display; + +public: + SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) override + { + bool result = QObject::eventFilter(obj, event); + QPlatformSurfaceEvent *surfaceEvent; + + switch (event->type()) { + case QEvent::PlatformSurface: + surfaceEvent = static_cast(event); + + switch (surfaceEvent->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + display->DestroyDisplay(); + break; + default: + break; + } + break; + default: + break; + } + + return result; + } +}; diff --git a/UI/window-basic-vcam.hpp b/frontend/utility/VCamConfig.hpp similarity index 95% rename from UI/window-basic-vcam.hpp rename to frontend/utility/VCamConfig.hpp index a3a5b6a2f..a3fa42953 100644 --- a/UI/window-basic-vcam.hpp +++ b/frontend/utility/VCamConfig.hpp @@ -1,7 +1,5 @@ #pragma once -#include - constexpr const char *VIRTUAL_CAM_ID = "virtualcam_output"; enum VCamOutputType { diff --git a/frontend/utility/VolumeMeterTimer.cpp b/frontend/utility/VolumeMeterTimer.cpp new file mode 100644 index 000000000..388b9640f --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.cpp @@ -0,0 +1,28 @@ +#include "VolumeMeterTimer.hpp" + +#include + +#include "moc_VolumeMeterTimer.cpp" + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} diff --git a/frontend/utility/VolumeMeterTimer.hpp b/frontend/utility/VolumeMeterTimer.hpp new file mode 100644 index 000000000..13590cb8f --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class VolumeMeter; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; diff --git a/frontend/utility/WhatsNewBrowserInitThread.cpp b/frontend/utility/WhatsNewBrowserInitThread.cpp new file mode 100644 index 000000000..200cf285f --- /dev/null +++ b/frontend/utility/WhatsNewBrowserInitThread.cpp @@ -0,0 +1,20 @@ +#include "WhatsNewBrowserInitThread.hpp" + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "moc_WhatsNewBrowserInitThread.cpp" + +#ifdef BROWSER_AVAILABLE +struct QCef; +extern QCef *cef; +#endif + +void WhatsNewBrowserInitThread::run() +{ +#ifdef BROWSER_AVAILABLE + cef->wait_for_browser_init(); +#endif + emit Result(url); +} diff --git a/frontend/utility/WhatsNewBrowserInitThread.hpp b/frontend/utility/WhatsNewBrowserInitThread.hpp new file mode 100644 index 000000000..6fd28acb7 --- /dev/null +++ b/frontend/utility/WhatsNewBrowserInitThread.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +class WhatsNewBrowserInitThread : public QThread { + Q_OBJECT + + QString url; + + virtual void run() override; + +signals: + void Result(const QString &url); + +public: + inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} +}; diff --git a/UI/update/shared-update.cpp b/frontend/utility/WhatsNewInfoThread.cpp similarity index 92% rename from UI/update/shared-update.cpp rename to frontend/utility/WhatsNewInfoThread.cpp index 0aedd8d95..24ebd010f 100644 --- a/UI/update/shared-update.cpp +++ b/frontend/utility/WhatsNewInfoThread.cpp @@ -1,27 +1,17 @@ -#include "moc_shared-update.cpp" -#include "crypto-helpers.hpp" -#include "update-helpers.hpp" -#include "obs-app.hpp" -#include "remote-text.hpp" -#include "platform.hpp" +#include "WhatsNewInfoThread.hpp" -#include -#include - -#include -#include -#include +#include +#include +#include +#include +#include #include -#include -#include +#include -#ifdef BROWSER_AVAILABLE -#include +#include -struct QCef; -extern QCef *cef; -#endif +#include "moc_WhatsNewInfoThread.cpp" #ifndef MAC_WHATSNEW_URL #define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" @@ -283,13 +273,3 @@ try { } catch (std::string &text) { blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); } - -/* ------------------------------------------------------------------------ */ - -void WhatsNewBrowserInitThread::run() -{ -#ifdef BROWSER_AVAILABLE - cef->wait_for_browser_init(); -#endif - emit Result(url); -} diff --git a/UI/update/shared-update.hpp b/frontend/utility/WhatsNewInfoThread.hpp similarity index 56% rename from UI/update/shared-update.hpp rename to frontend/utility/WhatsNewInfoThread.hpp index c33b8823d..d384eeb58 100644 --- a/UI/update/shared-update.hpp +++ b/frontend/utility/WhatsNewInfoThread.hpp @@ -1,10 +1,6 @@ #pragma once #include -#include - -#include -#include bool FetchAndVerifyFile(const char *name, const char *file, const char *url, std::string *out, const std::vector &extraHeaders = std::vector()); @@ -20,17 +16,3 @@ signals: public: inline WhatsNewInfoThread() {} }; - -class WhatsNewBrowserInitThread : public QThread { - Q_OBJECT - - QString url; - - virtual void run() override; - -signals: - void Result(const QString &url); - -public: - inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} -}; diff --git a/UI/youtube-api-wrappers.cpp b/frontend/utility/YoutubeApiWrappers.cpp similarity index 98% rename from UI/youtube-api-wrappers.cpp rename to frontend/utility/YoutubeApiWrappers.cpp index 1ed397f04..fba625495 100644 --- a/UI/youtube-api-wrappers.cpp +++ b/frontend/utility/YoutubeApiWrappers.cpp @@ -1,19 +1,16 @@ -#include "moc_youtube-api-wrappers.cpp" +#include "YoutubeApiWrappers.hpp" -#include -#include -#include +#include +#include +#include -#include -#include #include +#include -#include "auth-youtube.hpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" +#include +#include + +#include "moc_YoutubeApiWrappers.cpp" using namespace json11; diff --git a/UI/youtube-api-wrappers.hpp b/frontend/utility/YoutubeApiWrappers.hpp similarity index 98% rename from UI/youtube-api-wrappers.hpp rename to frontend/utility/YoutubeApiWrappers.hpp index 5216d2eb2..9cabb7091 100644 --- a/UI/youtube-api-wrappers.hpp +++ b/frontend/utility/YoutubeApiWrappers.hpp @@ -1,8 +1,9 @@ #pragma once -#include "auth-youtube.hpp" +#include #include + #include struct ChannelDescription { diff --git a/UI/audio-encoders.cpp b/frontend/utility/audio-encoders.cpp similarity index 98% rename from UI/audio-encoders.cpp rename to frontend/utility/audio-encoders.cpp index 692f25719..357ec45ca 100644 --- a/UI/audio-encoders.cpp +++ b/frontend/utility/audio-encoders.cpp @@ -1,15 +1,10 @@ -#include -#include -#include -#include +#include "audio-encoders.hpp" + +#include +#include + #include #include -#include -#include - -#include "audio-encoders.hpp" -#include "obs-app.hpp" -#include "window-main.hpp" using namespace std; diff --git a/UI/audio-encoders.hpp b/frontend/utility/audio-encoders.hpp similarity index 96% rename from UI/audio-encoders.hpp rename to frontend/utility/audio-encoders.hpp index b7fd0ab10..4a5f1b15c 100644 --- a/UI/audio-encoders.hpp +++ b/frontend/utility/audio-encoders.hpp @@ -1,8 +1,7 @@ #pragma once -#include - #include +#include #include const std::map &GetSimpleAACEncoderBitrateMap(); diff --git a/UI/update/crypto-helpers-mac.mm b/frontend/utility/crypto-helpers-mac.mm similarity index 100% rename from UI/update/crypto-helpers-mac.mm rename to frontend/utility/crypto-helpers-mac.mm diff --git a/UI/update/crypto-helpers-mbedtls.cpp b/frontend/utility/crypto-helpers-mbedtls.cpp similarity index 93% rename from UI/update/crypto-helpers-mbedtls.cpp rename to frontend/utility/crypto-helpers-mbedtls.cpp index f17c98764..caaef80dc 100644 --- a/UI/update/crypto-helpers-mbedtls.cpp +++ b/frontend/utility/crypto-helpers-mbedtls.cpp @@ -1,7 +1,7 @@ #include "crypto-helpers.hpp" -#include "mbedtls/md.h" -#include "mbedtls/pk.h" +#include +#include bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, const uint8_t *buf, const size_t len, const uint8_t *sig, const size_t sigLen) diff --git a/UI/update/crypto-helpers.hpp b/frontend/utility/crypto-helpers.hpp similarity index 100% rename from UI/update/crypto-helpers.hpp rename to frontend/utility/crypto-helpers.hpp index 8fe9c594e..890ec40c8 100644 --- a/UI/update/crypto-helpers.hpp +++ b/frontend/utility/crypto-helpers.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, const uint8_t *buf, const size_t len, const uint8_t *sig, const size_t sigLen); diff --git a/UI/display-helpers.hpp b/frontend/utility/display-helpers.hpp similarity index 98% rename from UI/display-helpers.hpp rename to frontend/utility/display-helpers.hpp index 586cbfeb6..a3b5d4457 100644 --- a/UI/display-helpers.hpp +++ b/frontend/utility/display-helpers.hpp @@ -17,8 +17,12 @@ #pragma once -#include #include +#include +#include + +#include +#include static inline void GetScaleAndCenterPos(int baseCX, int baseCY, int windowCX, int windowCY, int &x, int &y, float &scale) diff --git a/UI/item-widget-helpers.cpp b/frontend/utility/item-widget-helpers.cpp similarity index 97% rename from UI/item-widget-helpers.cpp rename to frontend/utility/item-widget-helpers.cpp index cd8ee8dc6..f6477e7e0 100644 --- a/UI/item-widget-helpers.cpp +++ b/frontend/utility/item-widget-helpers.cpp @@ -15,6 +15,8 @@ along with this program. If not, see . ******************************************************************************/ +#include "item-widget-helpers.hpp" + #include QListWidgetItem *TakeListItem(QListWidget *widget, int row) diff --git a/UI/item-widget-helpers.hpp b/frontend/utility/item-widget-helpers.hpp similarity index 98% rename from UI/item-widget-helpers.hpp rename to frontend/utility/item-widget-helpers.hpp index 599b83125..ae18ec7a8 100644 --- a/UI/item-widget-helpers.hpp +++ b/frontend/utility/item-widget-helpers.hpp @@ -22,6 +22,8 @@ * such as references to sources/etc from getting stuck in the Qt event queue * with no way of controlling when they'll be released. */ +#include + class QListWidget; class QListWidgetItem; diff --git a/UI/update/models/branches.hpp b/frontend/utility/models/branches.hpp similarity index 100% rename from UI/update/models/branches.hpp rename to frontend/utility/models/branches.hpp diff --git a/UI/models/multitrack-video.hpp b/frontend/utility/models/multitrack-video.hpp similarity index 100% rename from UI/models/multitrack-video.hpp rename to frontend/utility/models/multitrack-video.hpp diff --git a/UI/update/models/whatsnew.hpp b/frontend/utility/models/whatsnew.hpp similarity index 100% rename from UI/update/models/whatsnew.hpp rename to frontend/utility/models/whatsnew.hpp diff --git a/UI/obf.c b/frontend/utility/obf.c similarity index 99% rename from UI/obf.c rename to frontend/utility/obf.c index feba1886e..f144e76fc 100644 --- a/UI/obf.c +++ b/frontend/utility/obf.c @@ -1,4 +1,5 @@ #include "obf.h" + #include #define LOWER_HALFBYTE(x) ((x) & 0xF) diff --git a/UI/obf.h b/frontend/utility/obf.h similarity index 100% rename from UI/obf.h rename to frontend/utility/obf.h diff --git a/UI/platform-osx.mm b/frontend/utility/platform-osx.mm similarity index 97% rename from UI/platform-osx.mm rename to frontend/utility/platform-osx.mm index 75f4adfba..c9648b00f 100644 --- a/UI/platform-osx.mm +++ b/frontend/utility/platform-osx.mm @@ -15,20 +15,15 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include "platform.hpp" -#include "obs-app.hpp" +#import "platform.hpp" -#include +#import + +#import -#import -#import #import -#import +#import +#import using namespace std; diff --git a/UI/platform-windows.cpp b/frontend/utility/platform-windows.cpp similarity index 98% rename from UI/platform-windows.cpp rename to frontend/utility/platform-windows.cpp index d5d76ccc4..c6256b28c 100644 --- a/UI/platform-windows.cpp +++ b/frontend/utility/platform-windows.cpp @@ -15,30 +15,28 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include "obs-config.h" -#include "obs-app.hpp" #include "platform.hpp" -#include -#include -#include +#include -#define WIN32_LEAN_AND_MEAN -#include +#include +#include +#include + +#include +#include +#include #include #include -#include -#include -#include - -#include -#include -#include +#include +#define WIN32_LEAN_AND_MEAN +#include using namespace std; +extern bool portable_mode; +extern int GetConfigPath(char *path, size_t size, const char *name); + static inline bool check_path(const char *data, const char *path, string &output) { ostringstream str; diff --git a/UI/platform-x11.cpp b/frontend/utility/platform-x11.cpp similarity index 94% rename from UI/platform-x11.cpp rename to frontend/utility/platform-x11.cpp index 5520e019b..86bd05142 100644 --- a/UI/platform-x11.cpp +++ b/frontend/utility/platform-x11.cpp @@ -16,44 +16,53 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include "obs-app.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include - #include "platform.hpp" -#ifdef __linux__ -#include -#include -#include -#include -#include +#include + +#include +#include +#include + +#include +#include + +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#include +#endif +#include +#include +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#include + +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#include +#include #endif #if defined(__FreeBSD__) || defined(__DragonFly__) #include -#include -#include -#include -#include -#include - -#include -#include -#include #endif +#ifdef __linux__ +#include +#endif +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#ifdef __linux__ +#include +#endif +#if defined(__FreeBSD__) || defined(__DragonFly__) +#include +#endif +#include +using std::ostringstream; using std::string; using std::vector; -using std::ostringstream; #ifdef __linux__ void CheckIfAlreadyRunning(bool &already_running) diff --git a/UI/platform.hpp b/frontend/utility/platform.hpp similarity index 100% rename from UI/platform.hpp rename to frontend/utility/platform.hpp diff --git a/UI/system-info-macos.mm b/frontend/utility/system-info-macos.mm similarity index 100% rename from UI/system-info-macos.mm rename to frontend/utility/system-info-macos.mm diff --git a/UI/system-info-posix.cpp b/frontend/utility/system-info-posix.cpp similarity index 100% rename from UI/system-info-posix.cpp rename to frontend/utility/system-info-posix.cpp diff --git a/UI/system-info-windows.cpp b/frontend/utility/system-info-windows.cpp similarity index 99% rename from UI/system-info-windows.cpp rename to frontend/utility/system-info-windows.cpp index 084cdffe6..d428c06dd 100644 --- a/UI/system-info-windows.cpp +++ b/frontend/utility/system-info-windows.cpp @@ -1,15 +1,16 @@ #include "system-info.hpp" -#include -#include -#include - #include #include #include #include #include +#include + +#include +#include + static std::optional> system_gpu_data() { ComPtr factory; diff --git a/UI/system-info.hpp b/frontend/utility/system-info.hpp similarity index 100% rename from UI/system-info.hpp rename to frontend/utility/system-info.hpp diff --git a/UI/undo-stack-obs.cpp b/frontend/utility/undo_stack.cpp similarity index 97% rename from UI/undo-stack-obs.cpp rename to frontend/utility/undo_stack.cpp index 163a1180d..8ce5d6cc3 100644 --- a/UI/undo-stack-obs.cpp +++ b/frontend/utility/undo_stack.cpp @@ -1,6 +1,8 @@ -#include "moc_undo-stack-obs.cpp" +#include "undo_stack.hpp" -#include +#include + +#include "moc_undo_stack.cpp" #define MAX_STACK_SIZE 5000 diff --git a/UI/undo-stack-obs.hpp b/frontend/utility/undo_stack.hpp similarity index 96% rename from UI/undo-stack-obs.hpp rename to frontend/utility/undo_stack.hpp index 050471279..6486d9922 100644 --- a/UI/undo-stack-obs.hpp +++ b/frontend/utility/undo_stack.hpp @@ -1,15 +1,13 @@ #pragma once +#include "ui_OBSBasic.h" + #include #include #include #include -#include #include -#include - -#include "ui_OBSBasic.h" class undo_stack : public QObject { Q_OBJECT diff --git a/UI/update/update-helpers.cpp b/frontend/utility/update-helpers.cpp similarity index 100% rename from UI/update/update-helpers.cpp rename to frontend/utility/update-helpers.cpp diff --git a/UI/update/update-helpers.hpp b/frontend/utility/update-helpers.hpp similarity index 81% rename from UI/update/update-helpers.hpp rename to frontend/utility/update-helpers.hpp index 14d6e2c97..f07ee4f9a 100644 --- a/UI/update/update-helpers.hpp +++ b/frontend/utility/update-helpers.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include std::string strprintf(const char *format, ...); diff --git a/UI/win-dll-blocklist.c b/frontend/utility/win-dll-blocklist.c similarity index 100% rename from UI/win-dll-blocklist.c rename to frontend/utility/win-dll-blocklist.c diff --git a/frontend/widgets/ColorSelect.cpp b/frontend/widgets/ColorSelect.cpp new file mode 100644 index 000000000..297b578b2 --- /dev/null +++ b/frontend/widgets/ColorSelect.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "ColorSelect.hpp" + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} diff --git a/frontend/widgets/ColorSelect.hpp b/frontend/widgets/ColorSelect.hpp new file mode 100644 index 000000000..eab213409 --- /dev/null +++ b/frontend/widgets/ColorSelect.hpp @@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "ui_ColorSelect.h" + +#include + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp new file mode 100644 index 000000000..b840930ef --- /dev/null +++ b/frontend/widgets/OBSBasic.cpp @@ -0,0 +1,1992 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "ui-config.h" +#include "ColorSelect.hpp" +#include "OBSBasicControls.hpp" +#include "OBSBasicStats.hpp" +#include "VolControl.hpp" + +#ifdef YOUTUBE_ENABLED +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include +#endif +#include + +#include +#ifdef BROWSER_AVAILABLE +#include +#endif +#ifdef ENABLE_WAYLAND +#include +#endif +#include + +#include +#include +#include + +#ifdef _WIN32 +#include +#endif +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include "Windows.h" +#endif + +#include "moc_OBSBasic.cpp" + +using namespace std; + +extern bool portable_mode; +extern bool disable_3p_plugins; +extern bool opt_studio_mode; +extern bool opt_always_on_top; +extern bool opt_minimize_tray; +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +extern bool safe_mode; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; + +extern volatile long insideEventLoop; +extern bool restart; + +extern bool EncoderAvailable(const char *encoder); + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +struct QCef; + +extern QCef *cef; +extern bool cef_js_avail; + +extern void DestroyPanelCookieManager(); +extern void CheckExistingCookieId(); + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern void setupDockAction(QDockWidget *dock); + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->preview->addAction(nudge); + connect(nudge, &QAction::triggered, [this, distance, direction]() { Nudge(distance, direction); }); + }; + + addNudge(Qt::Key_Up, MoveDir::Up, 1); + addNudge(Qt::Key_Down, MoveDir::Down, 1); + addNudge(Qt::Key_Left, MoveDir::Left, 1); + addNudge(Qt::Key_Right, MoveDir::Right, 1); + addNudge(Qt::SHIFT | Qt::Key_Up, MoveDir::Up, 10); + addNudge(Qt::SHIFT | Qt::Key_Down, MoveDir::Down, 10); + addNudge(Qt::SHIFT | Qt::Key_Left, MoveDir::Left, 10); + addNudge(Qt::SHIFT | Qt::Key_Right, MoveDir::Right, 10); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main.hpp b/frontend/widgets/OBSBasic.hpp similarity index 80% rename from UI/window-basic-main.hpp rename to frontend/widgets/OBSBasic.hpp index 81bb7b478..e8169c50e 100644 --- a/UI/window-basic-main.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -17,49 +17,51 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "window-main.hpp" -#include "window-basic-interaction.hpp" -#include "window-basic-vcam.hpp" -#include "window-basic-properties.hpp" -#include "window-basic-transform.hpp" -#include "window-basic-adv-audio.hpp" -#include "window-basic-filters.hpp" -#include "window-missing-files.hpp" -#include "window-projector.hpp" -#include "window-basic-about.hpp" -#ifdef YOUTUBE_ENABLED -#include "window-dock-youtube-app.hpp" -#endif -#include "auth-base.hpp" -#include "log-viewer.hpp" -#include "undo-stack-obs.hpp" +#include "ui_OBSBasic.h" +#include "OBSMainWindow.hpp" + +#include +#include +#include +#include +#include +#include #include +#include +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); + +#include #include #include #include -#include +#include -class QMessageBox; -class QListWidgetItem; +#include + +extern volatile bool recording_paused; + +class ColorSelect; +class OBSAbout; +class OBSBasicAdvAudio; +class OBSBasicFilters; +class OBSBasicInteraction; +class OBSBasicProperties; +class OBSBasicTransform; +class OBSLogViewer; +class OBSMissingFiles; +class OBSProjector; class VolControl; -class OBSBasicStats; -class OBSBasicVCamConfig; - -#include "ui_OBSBasic.h" -#include "ui_ColorSelect.h" +#ifdef YOUTUBE_ENABLED +class YouTubeAppDock; +#endif +class QMessageBox; +class QWidgetAction; +struct QuickTransition; #define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") #define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") @@ -83,7 +85,7 @@ class OBSBasicVCamConfig; #define PREVIEW_EDGE_SIZE 10 -struct BasicOutputHandler; +enum class ProjectorType; enum class QtDataRole { OBSRef = Qt::UserRole, @@ -108,30 +110,6 @@ struct SourceCopyInfo { obs_blending_type blend_mode; }; -struct QuickTransition { - QPushButton *button = nullptr; - OBSSource source; - obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; - int duration = 0; - int id = 0; - bool fadeToBlack = false; - - inline QuickTransition() {} - inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) - : source(source_), - duration(duration_), - id(id_), - fadeToBlack(fadeToBlack_), - renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", - SourceRenamed, this)) - { - } - -private: - static void SourceRenamed(void *param, calldata_t *data); - std::shared_ptr renamedSignal; -}; - struct OBSProfile { std::string name; std::string directoryName; @@ -165,14 +143,60 @@ using OBSPromptCallback = std::function; using OBSProfileCache = std::map; using OBSSceneCollectionCache = std::map; -class ColorSelect : public QWidget { +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} -public: - explicit ColorSelect(QWidget *parent = 0); +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} -private: - std::unique_ptr ui; -}; +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif class OBSBasic : public OBSMainWindow { Q_OBJECT @@ -198,19 +222,12 @@ class OBSBasic : public OBSMainWindow { friend class OBSBasicPreview; friend class OBSBasicStatusBar; friend class OBSBasicSourceSelect; - friend class OBSBasicTransform; friend class OBSBasicSettings; friend class Auth; friend class AutoConfig; friend class AutoConfigStreamPage; - friend class RecordButton; - friend class ControlsSplitButton; friend class ExtraBrowsersModel; - friend class ExtraBrowsersDelegate; - friend class DeviceCaptureToolbar; - friend class OBSBasicSourceSelect; friend class OBSYoutubeActions; - friend class OBSPermissions; friend struct BasicOutputHandler; friend struct OBSStudioAPI; friend class ScreenshotObj; @@ -233,337 +250,102 @@ class OBSBasic : public OBSMainWindow { Vertical, Horizontal, }; - + /* ------------------------------------- + * MARK: - General + * ------------------------------------- + */ private: obs_frontend_callbacks *api = nullptr; - - std::shared_ptr auth; - - std::vector volumes; - std::vector signalHandlers; - QList> oldExtraDocks; - QStringList oldExtraDockNames; - - OBSDataAutoRelease collectionModuleData; - std::vector safeModeTransitions; - bool loaded = false; - long disableSaving = 1; - bool projectChanged = false; - bool previewEnabled = true; - ContextBarSize contextBarSize = ContextBarSize_Normal; - - std::deque clipboard; - OBSWeakSourceAutoRelease copyFiltersSource; - bool copyVisible = true; - obs_transform_info copiedTransformInfo; - obs_sceneitem_crop copiedCropInfo; - bool hasCopiedTransform = false; - OBSWeakSourceAutoRelease copySourceTransition; - int copySourceTransitionDuration; - bool closing = false; - bool clearingFailed = false; + // TODO: Remove, orphaned variable + bool copyVisible = true; + // TODO: Unused thread pointer, remove. QScopedPointer devicePropertiesThread; - QScopedPointer whatsNewInitThread; - QScopedPointer updateCheckThread; - QScopedPointer introCheckThread; + QScopedPointer logUploadThread; - QPointer interaction; - QPointer properties; - QPointer transformWindow; - QPointer advAudioWindow; - QPointer filters; - QPointer statsDock; -#ifdef YOUTUBE_ENABLED - QPointer youtubeAppDock; - uint64_t lastYouTubeAppDockCreationTime = 0; -#endif - QPointer about; - QPointer missDialog; - QPointer logView; - - QPointer cpuUsageTimer; - QPointer diskFullTimer; - - QPointer nudge_timer; - bool recent_nudge = false; - - os_cpu_usage_info_t *cpuUsageInfo = nullptr; - - OBSService service; - std::unique_ptr outputHandler; - std::shared_future setupStreamingGuard; - bool streamingStopping = false; - bool recordingStopping = false; - bool replayBufferStopping = false; - - gs_vertbuffer_t *box = nullptr; - gs_vertbuffer_t *boxLeft = nullptr; - gs_vertbuffer_t *boxTop = nullptr; - gs_vertbuffer_t *boxRight = nullptr; - gs_vertbuffer_t *boxBottom = nullptr; - gs_vertbuffer_t *circle = nullptr; - - gs_vertbuffer_t *actionSafeMargin = nullptr; - gs_vertbuffer_t *graphicsSafeMargin = nullptr; - gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; - gs_vertbuffer_t *leftLine = nullptr; - gs_vertbuffer_t *topLine = nullptr; - gs_vertbuffer_t *rightLine = nullptr; - - int previewX = 0, previewY = 0; - int previewCX = 0, previewCY = 0; - float previewScale = 0.0f; - ConfigFile activeConfiguration; - std::vector savedProjectorsArray; - std::vector projectors; - - QPointer stats; - QPointer remux; - QPointer extraBrowsers; - QPointer importer; - - QPointer transitionButton; - - bool vcamEnabled = false; - VCamConfig vcamConfig; - - QScopedPointer trayIcon; - QPointer sysTrayStream; - QPointer sysTrayRecord; - QPointer sysTrayReplayBuffer; - QPointer sysTrayVirtualCam; - QPointer showHide; - QPointer exit; - QPointer trayMenu; - QPointer previewProjector; - QPointer studioProgramProjector; - QPointer previewProjectorSource; - QPointer previewProjectorMain; - QPointer sceneProjectorMenu; - QPointer sourceProjector; - QPointer scaleFilteringMenu; - QPointer blendingMethodMenu; - QPointer blendingModeMenu; - QPointer colorMenu; - QPointer colorWidgetAction; - QPointer colorSelect; - QPointer deinterlaceMenu; - QPointer perSceneTransitionMenu; - QPointer shortcutFilter; - QPointer renameScene; - QPointer renameSource; - - QPointer programWidget; - QPointer programLayout; - QPointer programLabel; - QScopedPointer patronJsonThread; std::string patronJson; - std::atomic currentScene = nullptr; - std::optional> lastOutputResolution; - std::optional> migrationBaseResolution; - bool usingAbsoluteCoordinates = false; - - void DisableRelativeCoordinates(bool disable); + std::unique_ptr ui; void OnEvent(enum obs_frontend_event event); - void UpdateMultiviewProjectorMenu(); - - void DrawBackdrop(float cx, float cy); - - void SetupEncoders(); - - void CreateFirstRunSources(); - void CreateDefaultScene(bool firstStart); - - void UpdateVolumeControlsDecayRate(); - void UpdateVolumeControlsPeakMeterType(); - void ClearVolumeControls(); - - void UploadLog(const char *subdir, const char *file, const bool crash); - - void Save(const char *file); - void LoadData(obs_data_t *data, const char *file, bool remigrate = false); - void Load(const char *file, bool remigrate = false); - - void InitHotkeys(); - void CreateHotkeys(); - void ClearHotkeys(); - - bool InitService(); - bool InitBasicConfigDefaults(); void InitBasicConfigDefaults2(); bool InitBasicConfig(); void InitOBSCallbacks(); - void InitPrimitives(); - void OnFirstLoad(); - OBSSceneItem GetSceneItem(QListWidgetItem *item); - OBSSceneItem GetCurrentSceneItem(); - - bool QueryRemoveSource(obs_source_t *source); - - void TimedCheckForUpdates(); - void CheckForUpdates(bool manualUpdate); - void GetFPSCommon(uint32_t &num, uint32_t &den) const; void GetFPSInteger(uint32_t &num, uint32_t &den) const; void GetFPSFraction(uint32_t &num, uint32_t &den) const; void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; void GetConfigFPS(uint32_t &num, uint32_t &den) const; - void UpdatePreviewScalingMenu(); + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - void LoadSceneListOrder(obs_data_array_t *array); - obs_data_array_t *SaveSceneListOrder(); - void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + // TODO: Remove, orphaned instance method + void NewProject(); + // TODO: Remove, orphaned instance method + void LoadProject(); - void TempFileOutput(const char *path, int vBitrate, int aBitrate); - void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); +public slots: + void UpdatePatronJson(const QString &text, const QString &error); + void UpdateEditMenu(); - void CloseDialogs(); - void ClearSceneData(); - void ClearProjectors(); +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; - void Nudge(int dist, MoveDir dir); + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); - OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + virtual void OBSInit() override; - void GetAudioSourceFilters(); - void GetAudioSourceProperties(); - void VolControlContextMenu(); - void ToggleVolControlLayout(); - void ToggleMixerLayout(bool vertical); + virtual config_t *Config() const override; - void LogScenes(); - void SaveProjectNow(); + int ResetVideo(); + bool ResetAudio(); - int GetTopSelectedSourceItem(); + void UpdateTitleBar(); - QModelIndexList GetAllSelectedSourceItems(); + static OBSBasic *Get(); - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, - togglePreviewHotkeys, contextBarHotkeys; - obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + void SetDisplayAffinity(QWindow *window); - void InitDefaultTransitions(); - void InitTransition(obs_source_t *transition); - obs_source_t *FindTransition(const char *name); - OBSSource GetCurrentTransition(); - obs_data_array_t *SaveTransitions(); - void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + inline bool Closing() { return closing; } - obs_source_t *fadeTransition; - obs_source_t *cutTransition; +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; - void CreateProgramDisplay(); - void CreateProgramOptions(); - void AddQuickTransitionId(int id); - void AddQuickTransition(); - void AddQuickTransitionHotkey(QuickTransition *qt); - void RemoveQuickTransitionHotkey(QuickTransition *qt); - void LoadQuickTransitions(obs_data_array_t *array); - obs_data_array_t *SaveQuickTransitions(); - void ClearQuickTransitionWidgets(); - void RefreshQuickTransitions(); - void DisableQuickTransitionWidgets(); - void EnableTransitionWidgets(bool enable); - void CreateDefaultQuickTransitions(); + /* ------------------------------------- + * MARK: - OAuth + * ------------------------------------- + */ +private: + std::shared_ptr auth; - void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); - QMenu *CreatePerSceneTransitionMenu(); - QMenu *CreateVisibilityTransitionMenu(bool visible); +public: + inline Auth *GetAuth() { return auth.get(); } - QuickTransition *GetQuickTransition(int id); - int GetQuickTransitionIdx(int id); - QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); - void ClearQuickTransitions(); - void QuickTransitionClicked(); - void QuickTransitionChange(); - void QuickTransitionChangeDuration(int value); - void QuickTransitionRemoveClicked(); - - void SetPreviewProgramMode(bool enabled); - void ResizeProgram(uint32_t cx, uint32_t cy); - void SetCurrentScene(obs_scene_t *scene, bool force = false); - static void RenderProgram(void *data, uint32_t cx, uint32_t cy); - - std::vector quickTransitions; - QPointer programOptions; - QPointer program; - OBSWeakSource lastScene; - OBSWeakSource swapScene; - OBSWeakSource programScene; - OBSWeakSource lastProgramScene; - bool editPropertiesMode = false; - bool sceneDuplicationMode = true; - bool swapScenesMode = true; - volatile bool previewProgramMode = false; - obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; - obs_hotkey_id transitionHotkey = 0; - obs_hotkey_id statsHotkey = 0; - obs_hotkey_id screenshotHotkey = 0; - obs_hotkey_id sourceScreenshotHotkey = 0; - int quickTransitionIdCounter = 1; - bool overridingTransition = false; - - int programX = 0, programY = 0; - int programCX = 0, programCY = 0; - float programScale = 0.0f; - - int disableOutputsRef = 0; - - inline void OnActivate(bool force = false); - inline void OnDeactivate(); - - void AddDropSource(const char *file, DropType image); - void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - - bool sysTrayMinimizeToTray(); - - void EnumDialogs(); - - QList visDialogs; - QList modalDialogs; - QList visMsgBoxes; - - QList visDlgPositions; - - QByteArray startingDockLayout; - - obs_data_array_t *SaveProjectors(); - void LoadSavedProjectors(obs_data_array_t *savedProjectors); - - void MacBranchesFetched(const QString &branch, bool manualUpdate); - void ReceivedIntroJson(const QString &text); - void ShowWhatsNew(const QString &url); - - void UpdatePreviewProgramIndicators(); - - QStringList extraDockNames; - QList> extraDocks; - - QStringList extraCustomDockNames; - QList> extraCustomDocks; + /* ------------------------------------- + * MARK: - OBSBasic_Browser + * ------------------------------------- + */ +private: + QPointer extraBrowsers; #ifdef BROWSER_AVAILABLE QPointer extraBrowserMenuDocksSeparator; @@ -579,6 +361,141 @@ private: void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); #endif +public: + static void InitBrowserPanelSafeBlock(); + + /* ------------------------------------- + * MARK: - OBSBasic_Clipboard + * ------------------------------------- + */ +private: + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + int copySourceTransitionDuration; + OBSWeakSourceAutoRelease copySourceTransition; + +private slots: + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + +public: + OBSWeakSource copyFilter; + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + /* ------------------------------------- + * MARK: - OBSBasic_ContextToolbar + * ------------------------------------- + */ +private: + ContextBarSize contextBarSize = ContextBarSize_Normal; + + void SourceToolBarActionsSetEnabled(); + + void copyActionsDynamicProperties(); + +private slots: + void on_toggleContextBar_toggled(bool visible); + +public slots: + void ShowContextBar(); + void HideContextBar(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + + /* ------------------------------------- + * MARK: - OBSBasic_Docks + * ------------------------------------- + */ +private: + QList> oldExtraDocks; + QStringList oldExtraDockNames; + QPointer statsDock; + QByteArray startingDockLayout; + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + + QPointer controlsDock; + +public: + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + +private slots: + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_sideDocks_toggled(bool side); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* ------------------------------------- + * MARK: - OBSBasic_Dropfiles + * ------------------------------------- + */ +private: + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + /* ------------------------------------- + * MARK: - OBSBasic_Hotkeys + * ------------------------------------- + */ +private: + QPointer shortcutFilter; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + +private slots: + void ProcessHotkey(obs_hotkey_id id, bool pressed); + void ResetStatsHotkey(); + + /* ------------------------------------- + * MARK: - OBSBasic_Icons + * ------------------------------------- + */ +private: QIcon imageIcon; QIcon colorIcon; QIcon slideshowIcon; @@ -611,216 +528,7 @@ private: QIcon GetDefaultIcon() const; QIcon GetAudioProcessOutputIcon() const; - QSlider *tBar; - bool tBarActive = false; - - OBSSource GetOverrideTransition(OBSSource source); - int GetOverrideTransitionDuration(OBSSource source); - - void UpdateProjectorHideCursor(); - void UpdateProjectorAlwaysOnTop(bool top); - void ResetProjectors(); - - QPointer screenshotData; - - void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, - bool autostart, bool autostop, bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); - - void UpdatePreviewSafeAreas(); - bool drawSafeAreas = false; - - void CenterSelectedSceneItems(const CenterType ¢erType); - void ShowMissingFilesDialog(obs_missing_files_t *files); - - QColor selectionColor; - QColor cropColor; - QColor hoverColor; - - QColor GetCropColor() const; - QColor GetHoverColor() const; - - void UpdatePreviewSpacingHelpers(); - bool drawSpacingHelpers = true; - - float GetDevicePixelRatio(); - void SourceToolBarActionsSetEnabled(); - - std::string lastScreenshot; - std::string lastReplay; - - void UpdatePreviewOverflowSettings(); - void UpdatePreviewScrollbars(); - - bool streamingStarting = false; - - bool recordingStarted = false; - bool isRecordingPausable = false; - bool recordingPaused = false; - - bool restartingVCam = false; - -public slots: - void DeferSaveBegin(); - void DeferSaveEnd(); - - void DisplayStreamStartError(); - - void SetupBroadcast(); - - void StartStreaming(); - void StopStreaming(); - void ForceStopStreaming(); - - void StreamDelayStarting(int sec); - void StreamDelayStopping(int sec); - - void StreamingStart(); - void StreamStopping(); - void StreamingStop(int errorcode, QString last_error); - - void StartRecording(); - void StopRecording(); - - void RecordingStart(); - void RecordStopping(); - void RecordingStop(int code, QString last_error); - void RecordingFileChanged(QString lastRecordingPath); - - void ShowReplayBufferPauseWarning(); - void StartReplayBuffer(); - void StopReplayBuffer(); - - void ReplayBufferStart(); - void ReplayBufferSave(); - void ReplayBufferSaved(); - void ReplayBufferStopping(); - void ReplayBufferStop(int code); - - void StartVirtualCam(); - void StopVirtualCam(); - - void OnVirtualCamStart(); - void OnVirtualCamStop(int code); - - void SaveProjectDeferred(); - void SaveProject(); - - void SetTransition(OBSSource transition); - void OverrideTransition(OBSSource transition); - void TransitionToScene(OBSScene scene, bool force = false); - void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, - bool black = false, bool manual = false); - void SetCurrentScene(OBSSource scene, bool force = false); - - void UpdatePatronJson(const QString &text, const QString &error); - - void ShowContextBar(); - void HideContextBar(); - void PauseRecording(); - void UnpauseRecording(); - - void UpdateEditMenu(); - private slots: - - void on_actionMainUndo_triggered(); - void on_actionMainRedo_triggered(); - - void AddSceneItem(OBSSceneItem item); - void AddScene(OBSSource source); - void RemoveScene(OBSSource source); - void RenameSources(OBSSource source, QString newName, QString prevName); - - void ActivateAudioSource(OBSSource source); - void DeactivateAudioSource(OBSSource source); - - void DuplicateSelectedScene(); - void RemoveSelectedScene(); - - void ToggleAlwaysOnTop(); - - void ReorderSources(OBSScene scene); - void RefreshSources(OBSScene scene); - - void ProcessHotkey(obs_hotkey_id id, bool pressed); - - void AddTransition(const char *id); - void RenameTransition(OBSSource transition); - void TransitionClicked(); - void TransitionStopped(); - void TransitionFullyStopped(); - void TriggerQuickTransition(int id); - - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - - void IconActivated(QSystemTrayIcon::ActivationReason reason); - void SetShowing(bool showing); - - void ToggleShowHide(); - - void HideAudioControl(); - void UnhideAllAudioControls(); - void ToggleHideMixer(); - - void MixerRenameSource(); - - void on_vMixerScrollArea_customContextMenuRequested(); - void on_hMixerScrollArea_customContextMenuRequested(); - - void on_actionCopySource_triggered(); - void on_actionPasteRef_triggered(); - void on_actionPasteDup_triggered(); - - void on_actionCopyFilters_triggered(); - void on_actionPasteFilters_triggered(); - void AudioMixerCopyFilters(); - void AudioMixerPasteFilters(); - void SourcePasteFilters(OBSSource source, OBSSource dstSource); - - void on_previewXScrollBar_valueChanged(int value); - void on_previewYScrollBar_valueChanged(int value); - - void PreviewScalingModeChanged(int value); - - void ColorChange(); - - SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); - - void on_actionShowAbout_triggered(); - - void EnablePreview(); - void DisablePreview(); - - void EnablePreviewProgram(); - void DisablePreviewProgram(); - - void SceneCopyFilters(); - void ScenePasteFilters(); - - void CheckDiskSpaceRemaining(); - void OpenSavedProjector(SavedProjectorInfo *info); - - void ResetStatsHotkey(); - void SetImageIcon(const QIcon &icon); void SetColorIcon(const QIcon &icon); void SetSlideshowIcon(const QIcon &icon); @@ -838,202 +546,64 @@ private slots: void SetDefaultIcon(const QIcon &icon); void SetAudioProcessOutputIcon(const QIcon &icon); - void TBarChanged(int value); - void TBarReleased(); - - void LockVolumeControl(bool lock); - - void UpdateVirtualCamConfig(const VCamConfig &config); - void RestartVirtualCam(const VCamConfig &config); - void RestartingVirtualCam(); - -private: - /* OBS Callbacks */ - static void SceneReordered(void *data, calldata_t *params); - static void SceneRefreshed(void *data, calldata_t *params); - static void SceneItemAdded(void *data, calldata_t *params); - static void SourceCreated(void *data, calldata_t *params); - static void SourceRemoved(void *data, calldata_t *params); - static void SourceActivated(void *data, calldata_t *params); - static void SourceDeactivated(void *data, calldata_t *params); - static void SourceAudioActivated(void *data, calldata_t *params); - static void SourceAudioDeactivated(void *data, calldata_t *params); - static void SourceRenamed(void *data, calldata_t *params); - static void RenderMain(void *data, uint32_t cx, uint32_t cy); - - void ResizePreview(uint32_t cx, uint32_t cy); - - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - void copyActionsDynamicProperties(); - - static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - - void AutoRemux(QString input, bool no_show = false); - - void UpdateIsRecordingPausable(); - - bool IsFFmpegOutputToURL() const; - bool OutputPathValid(); - void OutputPathInvalidMessage(); - - bool LowDiskSpace(); - void DiskSpaceMessage(); - - OBSSource prevFTBSource = nullptr; - - float dpi = 1.0; - public: - OBSSource GetProgramSource(); - OBSScene GetCurrentScene(); - - void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); - - inline OBSSource GetCurrentSceneSource() - { - OBSScene curScene = GetCurrentScene(); - return OBSSource(obs_scene_get_source(curScene)); - } - - obs_service_t *GetService(); - void SetService(obs_service_t *service); - - int GetTransitionDuration(); - int GetTbarPosition(); - - inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } - - inline bool VCamEnabled() const { return vcamEnabled; } - - bool Active() const; - - void ResetUI(); - int ResetVideo(); - bool ResetAudio(); - - void ResetOutputs(); - - void RefreshVolumeColors(); - - void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - - void NewProject(); - void LoadProject(); - - inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) - { - x = previewX; - y = previewY; - cx = previewCX; - cy = previewCY; - } - - inline bool SavingDisabled() const { return disableSaving; } - - inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } - - void SaveService(); - bool LoadService(); - - inline Auth *GetAuth() { return auth.get(); } - - inline void EnableOutputs(bool enable) - { - if (enable) { - if (--disableOutputsRef < 0) - disableOutputsRef = 0; - } else { - disableOutputsRef++; - } - } - - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, - obs_sceneitem_t *item); - void CreateSourcePopupMenu(int idx, bool preview); - - void UpdateTitleBar(); - - void SystemTrayInit(); - void SystemTray(bool firstStarted); - - void OpenSavedProjectors(); - - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CreateEditTransformWindow(obs_sceneitem_t *item); - - QAction *AddDockWidget(QDockWidget *dock); - void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); - void RemoveDockWidget(const QString &name); - bool IsDockObjectNameUsed(const QString &name); - void AddCustomDockWidget(QDockWidget *dock); - - static OBSBasic *Get(); - - const char *GetCurrentOutputPath(); - - void DeleteProjector(OBSProjector *projector); - - static QList GetProjectorMenuMonitorsFormatted(); - template - static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) - { - auto projectors = GetProjectorMenuMonitorsFormatted(); - for (int i = 0; i < projectors.size(); i++) { - QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); - action->setProperty("monitor", i); - } - } - QIcon GetSourceIcon(const char *id) const; QIcon GetGroupIcon() const; QIcon GetSceneIcon() const; - OBSWeakSource copyFilter; + /* ------------------------------------- + * MARK: - OBSBasic_MainControls + * ------------------------------------- + */ +private: + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer about; + QPointer logView; + QPointer stats; + QPointer remux; + QPointer importer; + QPointer showHide; + QPointer exit; - void ShowStatusBarMessage(const QString &message); + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer deinterlaceMenu; - static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); - void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + QPointer colorWidgetAction; + QPointer colorSelect; - static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) - { - obs_scene_t *scene = obs_scene_from_source(scene_source); - return BackupScene(scene, sources); - } + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; - void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, - obs_data_array_t *redo_array); + QList visDlgPositions; - void SetDisplayAffinity(QWindow *window); - - QColor GetSelectionColor() const; - inline bool Closing() { return closing; } - -protected: - virtual void closeEvent(QCloseEvent *event) override; - virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; - virtual void changeEvent(QEvent *event) override; + void UploadLog(const char *subdir, const char *file, const bool crash); + void CloseDialogs(); + void EnumDialogs(); private slots: - void on_actionFullscreenInterface_triggered(); + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + void ToggleAlwaysOnTop(); - void on_actionShow_Recordings_triggered(); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void on_actionShowAbout_triggered(); + + void on_actionFullscreenInterface_triggered(); void on_actionRemux_triggered(); void on_action_Settings_triggered(); void on_actionShowMacPermissions_triggered(); - void on_actionShowMissingFiles_triggered(); void on_actionAdvAudioProperties_triggered(); - void on_actionMixerToolbarAdvAudio_triggered(); - void on_actionMixerToolbarMenu_triggered(); void on_actionShowLogs_triggered(); void on_actionUploadCurrentLog_triggered(); void on_actionUploadLastLog_triggered(); @@ -1046,45 +616,220 @@ private slots: void on_actionShowCrashLogs_triggered(); void on_actionUploadLastCrashLog_triggered(); - void on_actionEditTransform_triggered(); - void on_actionCopyTransform_triggered(); - void on_actionPasteTransform_triggered(); - void on_actionRotate90CW_triggered(); - void on_actionRotate90CCW_triggered(); - void on_actionRotate180_triggered(); - void on_actionFlipHorizontal_triggered(); - void on_actionFlipVertical_triggered(); - void on_actionFitToScreen_triggered(); - void on_actionStretchToScreen_triggered(); - void on_actionCenterToScreen_triggered(); - void on_actionVerticalCenter_triggered(); - void on_actionHorizontalCenter_triggered(); - void on_actionSceneFilters_triggered(); - void on_OBSBasic_customContextMenuRequested(const QPoint &pos); - void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); - void on_scenes_customContextMenuRequested(const QPoint &pos); - void GridActionClicked(); - void on_actionSceneListMode_triggered(); - void on_actionSceneGridMode_triggered(); - void on_actionAddScene_triggered(); - void on_actionRemoveScene_triggered(); - void on_actionSceneUp_triggered(); - void on_actionSceneDown_triggered(); - void on_sources_customContextMenuRequested(const QPoint &pos); - void on_scenes_itemDoubleClicked(QListWidgetItem *item); - void on_actionAddSource_triggered(); - void on_actionRemoveSource_triggered(); - void on_actionInteract_triggered(); - void on_actionSourceProperties_triggered(); - void on_actionSourceUp_triggered(); - void on_actionSourceDown_triggered(); + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); - void on_actionMoveUp_triggered(); - void on_actionMoveDown_triggered(); - void on_actionMoveToTop_triggered(); - void on_actionMoveToBottom_triggered(); + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + +public: + void ResetUI(); + + void CreateInteractionWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + void CreatePropertiesWindow(obs_source_t *source); + + /* ------------------------------------- + * MARK: - OBSBasic_OutputHandler + * ------------------------------------- + */ +private: + std::unique_ptr outputHandler; + std::optional> lastOutputResolution; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false) + { + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon( + QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } + } + + inline void OnDeactivate() + { + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } + } + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + // TODO: Unimplemented, remove. + void SetupEncoders(); + // TODO: Unimplemented, remove. + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + // TODO: Unimplemented, remove. + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + +public: + bool Active() const; + void ResetOutputs(); + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + const char *GetCurrentOutputPath(); + +private slots: + void ResizeOutputSizeOfSource(); + + /* ------------------------------------- + * MARK: - OBSBasic_Preview + * ------------------------------------- + */ +private: + bool previewEnabled = true; + QPointer nudge_timer; + bool recent_nudge = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + bool drawSafeAreas = false; + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + bool drawSpacingHelpers = true; + + float dpi = 1.0; + + void DrawBackdrop(float cx, float cy); + void InitPrimitives(); + void UpdatePreviewScalingMenu(); + + void Nudge(int dist, MoveDir dir); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + void UpdatePreviewSafeAreas(); + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + + float GetDevicePixelRatio(); + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + /* OBS Callbacks */ + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + +private slots: + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + void EnablePreview(); + void DisablePreview(); void on_actionLockPreview_triggered(); @@ -1093,161 +838,24 @@ private slots: void on_actionScaleCanvas_triggered(); void on_actionScaleOutput_triggered(); - void Screenshot(OBSSource source_ = nullptr); - void ScreenshotSelectedSource(); - void ScreenshotProgram(); - void ScreenshotScene(); - - void on_actionHelpPortal_triggered(); - void on_actionWebsite_triggered(); - void on_actionDiscord_triggered(); - void on_actionReleaseNotes_triggered(); - void on_preview_customContextMenuRequested(); - void ProgramViewContextMenuRequested(); void on_previewDisabledWidget_customContextMenuRequested(); - void on_actionShowSettingsFolder_triggered(); - void on_actionShowProfileFolder_triggered(); - - void on_actionAlwaysOnTop_triggered(); - - void on_toggleListboxToolbars_toggled(bool visible); - void on_toggleContextBar_toggled(bool visible); - void on_toggleStatusBar_toggled(bool visible); - void on_toggleSourceIcons_toggled(bool visible); - - void on_transitions_currentIndexChanged(int index); - void on_transitionAdd_clicked(); - void on_transitionRemove_clicked(); - void on_transitionProps_clicked(); - void on_transitionDuration_valueChanged(); - - void ShowTransitionProperties(); - void HideTransitionProperties(); - - // Source Context Buttons - void on_sourcePropertiesButton_clicked(); - void on_sourceFiltersButton_clicked(); - void on_sourceInteractButton_clicked(); - - void on_autoConfigure_triggered(); - void on_stats_triggered(); - - void on_resetUI_triggered(); - void on_resetDocks_triggered(bool force = false); - void on_lockDocks_toggled(bool lock); - void on_multiviewProjectorWindowed_triggered(); - void on_sideDocks_toggled(bool side); - - void logUploadFinished(const QString &text, const QString &error); - void crashUploadFinished(const QString &text, const QString &error); - void openLogDialog(const QString &text, const bool crash); - - void updateCheckFinished(); - - void MoveSceneToTop(); - void MoveSceneToBottom(); - - void EditSceneName(); - void EditSceneItemName(); - - void SceneNameEdited(QWidget *editor); - - void OpenSceneFilters(); - void OpenFilters(OBSSource source = nullptr); - void OpenProperties(OBSSource source = nullptr); - void OpenInteraction(OBSSource source = nullptr); - void OpenEditTransform(OBSSceneItem item = nullptr); - void EnablePreviewDisplay(bool enable); void TogglePreview(); - void OpenStudioProgramProjector(); - void OpenPreviewProjector(); - void OpenSourceProjector(); - void OpenMultiviewProjector(); - void OpenSceneProjector(); +public: + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } - void OpenStudioProgramWindow(); - void OpenPreviewWindow(); - void OpenSourceWindow(); - void OpenSceneWindow(); - - void StackedMixerAreaContextMenuRequested(); - - void ResizeOutputSizeOfSource(); - - void RepairOldExtraDockName(); - void RepairCustomExtraDockName(); - - /* Stream action (start/stop) slot */ - void StreamActionTriggered(); - - /* Record action (start/stop) slot */ - void RecordActionTriggered(); - - /* Record pause (pause/unpause) slot */ - void RecordPauseToggled(); - - /* Replay Buffer action (start/stop) slot */ - void ReplayBufferActionTriggered(); - - /* Virtual Cam action (start/stop) slots */ - void VirtualCamActionTriggered(); - - void OpenVirtualCamConfig(); - - /* Studio Mode toggle slot */ - void TogglePreviewProgramMode(); - -public slots: - void on_actionResetTransform_triggered(); - - bool StreamingActive(); - bool RecordingActive(); - bool ReplayBufferActive(); - bool VirtualCamActive(); - - void ClearContextBar(); - void UpdateContextBar(bool force = false); - void UpdateContextBarDeferred(bool force = false); - void UpdateContextBarVisibility(); + QColor GetSelectionColor() const; signals: - /* Streaming signals */ - void StreamingPreparing(); - void StreamingStarting(bool broadcastAutoStart); - void StreamingStarted(bool withDelay = false); - void StreamingStopping(); - void StreamingStopped(bool withDelay = false); - - /* Broadcast Flow signals */ - void BroadcastFlowEnabled(bool enabled); - void BroadcastStreamReady(bool ready); - void BroadcastStreamActive(); - void BroadcastStreamStarted(bool autoStop); - - /* Recording signals */ - void RecordingStarted(bool pausable = false); - void RecordingPaused(); - void RecordingUnpaused(); - void RecordingStopping(); - void RecordingStopped(); - - /* Replay Buffer signals */ - void ReplayBufEnabled(bool enabled); - void ReplayBufStarted(); - void ReplayBufStopping(); - void ReplayBufStopped(); - - /* Virtual Camera signals */ - void VirtualCamEnabled(); - void VirtualCamStarted(); - void VirtualCamStopped(); - - /* Studio Mode signal */ - void PreviewProgramModeChanged(bool enabled); void CanvasResized(uint32_t width, uint32_t height); void OutputResized(uint32_t width, uint32_t height); @@ -1255,35 +863,10 @@ signals: void PreviewXScrollBarMoved(int value); void PreviewYScrollBarMoved(int value); -private: - std::unique_ptr ui; - - QPointer controlsDock; - -public: - /* `undo_s` needs to be declared after `ui` to prevent an uninitialized - * warning for `ui` while initializing `undo_s`. */ - undo_stack undo_s; - - explicit OBSBasic(QWidget *parent = 0); - virtual ~OBSBasic(); - - virtual void OBSInit() override; - - virtual config_t *Config() const override; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const override; - - static void InitBrowserPanelSafeBlock(); -#ifdef YOUTUBE_ENABLED - void NewYouTubeAppDock(); - void DeleteYouTubeAppDock(); - YouTubeAppDock *GetYouTubeAppDock(); -#endif - // MARK: - Generic UI Helper Functions - OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); - - // MARK: - OBS Profile Management + /* ------------------------------------- + * MARK: - OBSBasic_Profiles + * ------------------------------------- + */ private: OBSProfileCache profiles{}; @@ -1327,10 +910,162 @@ public slots: bool CreateDuplicateProfile(const QString &name); void DeleteProfile(const QString &profileName); - // MARK: - OBS Scene Collection Management + /* ------------------------------------- + * MARK: - OBSBasic_Projectors + * ------------------------------------- + */ private: + std::vector savedProjectorsArray; + std::vector projectors; + QPointer previewProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + + void UpdateMultiviewProjectorMenu(); + void ClearProjectors(); + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + +private slots: + void OpenSavedProjector(SavedProjectorInfo *info); + void on_multiviewProjectorWindowed_triggered(); + + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + +public: + void OpenSavedProjectors(); + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + /* ------------------------------------- + * MARK: - OBSBasic_Recording + * ------------------------------------- + */ +private: + QPointer diskFullTimer; + bool recordingStopping = false; + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + void AutoRemux(QString input, bool no_show = false); + void UpdateIsRecordingPausable(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + +private slots: + void on_actionShow_Recordings_triggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + +public slots: + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void PauseRecording(); + void UnpauseRecording(); + + void CheckDiskSpaceRemaining(); + + bool RecordingActive(); + +signals: + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_ReplayBuffer + * ------------------------------------- + */ +private: + bool replayBufferStopping = false; + std::string lastReplay; + +public slots: + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + bool ReplayBufferActive(); + +private slots: + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + +signals: + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_SceneCollections + * ------------------------------------- + */ +private: + OBSDataAutoRelease collectionModuleData; + long disableSaving = 1; + bool projectChanged = false; + bool clearingFailed = false; + + QPointer missDialog; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + OBSSceneCollectionCache collections{}; + void DisableRelativeCoordinates(bool disable); + void CreateDefaultScene(bool firstStart); + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void ClearSceneData(); + void LogScenes(); + void SaveProjectNow(); + void ShowMissingFilesDialog(obs_missing_files_t *files); + void SetupNewSceneCollection(const std::string &collectionName); void SetupDuplicateSceneCollection(const std::string &collectionName); void SetupRenameSceneCollection(const std::string &collectionName); @@ -1347,15 +1082,18 @@ private: void RefreshSceneCollections(bool refreshCache = false); void ActivateSceneCollection(const OBSSceneCollection &collection); -public: - inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); - const OBSSceneCollection &GetCurrentSceneCollection() const; + void SaveProjectDeferred(); + void SaveProject(); - std::optional GetSceneCollectionByName(const std::string &collectionName) const; - std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + bool CreateNewSceneCollection(const QString &name); private slots: + void on_actionShowMissingFiles_triggered(); + void on_actionNewSceneCollection_triggered(); void on_actionDupSceneCollection_triggered(); void on_actionRenameSceneCollection_triggered(); @@ -1364,19 +1102,573 @@ private slots: void on_actionExportSceneCollection_triggered(); void on_actionRemigrateSceneCollection_triggered(); -public slots: - bool CreateNewSceneCollection(const QString &name); -}; +public: + inline bool SavingDisabled() const { return disableSaving; } -extern bool cef_js_avail; + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; -class SceneRenameDelegate : public QStyledItemDelegate { - Q_OBJECT + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + + /* ------------------------------------- + * MARK: - OBSBasic_SceneItems + * ------------------------------------- + */ +private: + QPointer sourceProjector; + QPointer renameSource; + + void CreateFirstRunSources(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + int GetTopSelectedSourceItem(); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + + QModelIndexList GetAllSelectedSourceItems(); + + // TODO: Move back to transitions + QMenu *CreateVisibilityTransitionMenu(bool visible); + void CenterSelectedSceneItems(const CenterType ¢erType); + + /* OBS Callbacks */ + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + +private slots: + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void MixerRenameSource(); + + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + + void on_actionEditTransform_triggered(); + + void on_sources_customContextMenuRequested(const QPoint &pos); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_toggleSourceIcons_toggled(bool visible); + + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); public: - SceneRenameDelegate(QObject *parent); - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); -protected: - virtual bool eventFilter(QObject *editor, QEvent *event) override; + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + /* ------------------------------------- + * MARK: - OBSBasic_Scenes + * ------------------------------------- + */ +private: + QPointer sceneProjectorMenu; + QPointer renameScene; + std::atomic currentScene = nullptr; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + +public slots: + void on_actionResetTransform_triggered(); + +private slots: + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionSceneFilters_triggered(); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + void OpenSceneFilters(); + +public: + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + OBSScene GetCurrentScene(); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + /* ------------------------------------- + * MARK: - OBSBasic_Screenshots + * ------------------------------------- + */ +private: + QPointer screenshotData; + std::string lastScreenshot; + +private slots: + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + /* ------------------------------------- + * MARK: - OBSBasic_Service + * ------------------------------------- + */ +private: + OBSService service; + + bool InitService(); + +public: + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + void SaveService(); + bool LoadService(); + + /* ------------------------------------- + * MARK: - OBSBasic_StatusBar + * ------------------------------------- + */ +private: + QPointer cpuUsageTimer; + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + +public: + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + void ShowStatusBarMessage(const QString &message); + + /* ------------------------------------- + * MARK: - OBSBasic_Streaming + * ------------------------------------- + */ +private: + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool streamingStarting = false; + +public slots: + void DisplayStreamStartError(); + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + bool StreamingActive(); + +private slots: + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* ------------------------------------- + * MARK: - OBSBasic_StudioMode + * ------------------------------------- + */ +private: + QPointer studioProgramProjector; + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + QPointer programOptions; + QPointer program; + OBSWeakSource lastProgramScene; + + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + + OBSWeakSource programScene; + volatile bool previewProgramMode = false; + + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + void UpdatePreviewProgramIndicators(); + +private slots: + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void ProgramViewContextMenuRequested(); + + void OpenStudioProgramProjector(); + void OpenStudioProgramWindow(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public: + OBSSource GetProgramSource(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + +signals: + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + + /* ------------------------------------- + * MARK: - OBSBasic_SysTray + * ------------------------------------- + */ +private: + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer trayMenu; + + bool sysTrayMinimizeToTray(); + +private slots: + void IconActivated(QSystemTrayIcon::ActivationReason reason); + +public: + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + /* ------------------------------------- + * MARK: - OBSBasic_Transitions + * ------------------------------------- + */ +private: + std::vector safeModeTransitions; + QPointer transitionButton; + QPointer perSceneTransitionMenu; + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + std::vector quickTransitions; + bool swapScenesMode = true; + + obs_hotkey_id transitionHotkey = 0; + + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource prevFTBSource = nullptr; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + // TODO: Remove orphaned method. + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + QMenu *CreatePerSceneTransitionMenu(); + + void SetCurrentScene(obs_scene_t *scene, bool force = false); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + +public slots: + void SetCurrentScene(OBSSource scene, bool force = false); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + +private slots: + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void TBarChanged(int value); + void TBarReleased(); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + +public: + int GetTransitionDuration(); + int GetTbarPosition(); + + /* ------------------------------------- + * MARK: - OBSBasic_Updater + * ------------------------------------- + */ +private: + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + /* ------------------------------------- + * MARK: - OBSBasic_VirtualCam + * ------------------------------------- + */ +private: + bool vcamEnabled = false; + VCamConfig vcamConfig; + bool restartingVCam = false; + +public slots: + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + bool VirtualCamActive(); + +private slots: + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + +public: + inline bool VCamEnabled() const { return vcamEnabled; } + +signals: + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* ------------------------------------- + * MARK: - OBSBasic_VolControl + * ------------------------------------- + */ +private: + std::vector volumes; + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + +private slots: + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void LockVolumeControl(bool lock); + + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + + void StackedMixerAreaContextMenuRequested(); + +public: + void RefreshVolumeColors(); + + /* ------------------------------------- + * MARK: - OBSBasic_YouTube + * ------------------------------------- + */ + +private: + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; + +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; + + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + +public: +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + +public slots: + void SetupBroadcast(); + +signals: + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); }; diff --git a/UI/basic-controls.cpp b/frontend/widgets/OBSBasicControls.cpp similarity index 99% rename from UI/basic-controls.cpp rename to frontend/widgets/OBSBasicControls.cpp index fe9483136..7fcc1bc06 100644 --- a/UI/basic-controls.cpp +++ b/frontend/widgets/OBSBasicControls.cpp @@ -1,6 +1,7 @@ -#include "moc_basic-controls.cpp" +#include "OBSBasicControls.hpp" +#include "OBSBasic.hpp" -#include "window-basic-main.hpp" +#include "moc_OBSBasicControls.cpp" OBSBasicControls::OBSBasicControls(OBSBasic *main) : QFrame(nullptr), ui(new Ui::OBSBasicControls) { diff --git a/UI/basic-controls.hpp b/frontend/widgets/OBSBasicControls.hpp similarity index 100% rename from UI/basic-controls.hpp rename to frontend/widgets/OBSBasicControls.hpp index 6aa677802..a79c43b47 100644 --- a/UI/basic-controls.hpp +++ b/frontend/widgets/OBSBasicControls.hpp @@ -1,14 +1,14 @@ #pragma once -#include +#include "ui_OBSBasicControls.h" #include #include #include -class OBSBasic; +#include -#include "ui_OBSBasicControls.h" +class OBSBasic; class OBSBasicControls : public QFrame { Q_OBJECT diff --git a/UI/window-basic-preview.cpp b/frontend/widgets/OBSBasicPreview.cpp similarity index 99% rename from UI/window-basic-preview.cpp rename to frontend/widgets/OBSBasicPreview.cpp index 361e318c2..2e4464ee6 100644 --- a/UI/window-basic-preview.cpp +++ b/frontend/widgets/OBSBasicPreview.cpp @@ -1,16 +1,9 @@ -#include -#include +#include "OBSBasicPreview.hpp" -#include -#include -#include -#include -#include -#include "moc_window-basic-preview.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "platform.hpp" -#include "display-helpers.hpp" +#include +#include + +#include "moc_OBSBasicPreview.cpp" #define HANDLE_RADIUS 4.0f #define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f) diff --git a/UI/window-basic-preview.hpp b/frontend/widgets/OBSBasicPreview.hpp similarity index 96% rename from UI/window-basic-preview.hpp rename to frontend/widgets/OBSBasicPreview.hpp index e880a8dac..67d7db296 100644 --- a/UI/window-basic-preview.hpp +++ b/frontend/widgets/OBSBasicPreview.hpp @@ -1,17 +1,10 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include "qt-display.hpp" -#include "obs-app.hpp" -#include "preview-controls.hpp" +#include "OBSQTDisplay.hpp" -class OBSBasic; -class QMouseEvent; +#include + +#include #define ITEM_LEFT (1 << 0) #define ITEM_RIGHT (1 << 1) diff --git a/UI/window-basic-stats.cpp b/frontend/widgets/OBSBasicStats.cpp similarity index 98% rename from UI/window-basic-stats.cpp rename to frontend/widgets/OBSBasicStats.cpp index bb8ab0bd8..f4a224f37 100644 --- a/UI/window-basic-stats.cpp +++ b/frontend/widgets/OBSBasicStats.cpp @@ -1,19 +1,16 @@ -#include "obs-frontend-api/obs-frontend-api.h" +#include "OBSBasicStats.hpp" -#include "moc_window-basic-stats.cpp" -#include "window-basic-main.hpp" -#include "platform.hpp" -#include "obs-app.hpp" +#include #include + +#include +#include #include #include #include -#include -#include -#include -#include +#include "moc_OBSBasicStats.cpp" #define TIMER_INTERVAL 2000 #define REC_TIME_LEFT_INTERVAL 30000 diff --git a/UI/window-basic-stats.hpp b/frontend/widgets/OBSBasicStats.hpp similarity index 95% rename from UI/window-basic-stats.hpp rename to frontend/widgets/OBSBasicStats.hpp index c1d434bf6..350b5c866 100644 --- a/UI/window-basic-stats.hpp +++ b/frontend/widgets/OBSBasicStats.hpp @@ -1,16 +1,15 @@ #pragma once +#include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include + +class QLabel; class QGridLayout; -class QCloseEvent; class OBSBasicStats : public QFrame { Q_OBJECT diff --git a/UI/window-basic-status-bar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp similarity index 97% rename from UI/window-basic-status-bar.cpp rename to frontend/widgets/OBSBasicStatusBar.cpp index 5bcae6f33..e6d3fce66 100644 --- a/UI/window-basic-status-bar.cpp +++ b/frontend/widgets/OBSBasicStatusBar.cpp @@ -1,14 +1,10 @@ -#include -#include -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "moc_window-basic-status-bar.cpp" -#include "window-basic-main-outputs.hpp" -#include "qt-wrappers.hpp" -#include "platform.hpp" - +#include "OBSBasicStatusBar.hpp" #include "ui_StatusBarWidget.h" +#include + +#include "moc_OBSBasicStatusBar.cpp" + static constexpr int bitrateUpdateSeconds = 2; static constexpr int congestionUpdateSeconds = 4; static constexpr float excellentThreshold = 0.0f; @@ -16,13 +12,6 @@ static constexpr float goodThreshold = 0.3333f; static constexpr float mediocreThreshold = 0.6667f; static constexpr float badThreshold = 1.0f; -StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) -{ - ui->setupUi(this); -} - -StatusBarWidget::~StatusBarWidget() {} - OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) : QStatusBar(parent), excellentPixmap(QIcon(":/res/images/network-excellent.svg").pixmap(QSize(16, 16))), diff --git a/UI/window-basic-status-bar.hpp b/frontend/widgets/OBSBasicStatusBar.hpp similarity index 88% rename from UI/window-basic-status-bar.hpp rename to frontend/widgets/OBSBasicStatusBar.hpp index 38be9e520..6ccc90fcc 100644 --- a/UI/window-basic-status-bar.hpp +++ b/frontend/widgets/OBSBasicStatusBar.hpp @@ -1,25 +1,13 @@ #pragma once -#include +#include "StatusBarWidget.hpp" + +#include + #include -#include -#include -#include +#include -class Ui_StatusBarWidget; - -class StatusBarWidget : public QWidget { - Q_OBJECT - - friend class OBSBasicStatusBar; - -private: - std::unique_ptr ui; - -public: - StatusBarWidget(QWidget *parent = nullptr); - ~StatusBarWidget(); -}; +class QTimer; class OBSBasicStatusBar : public QStatusBar { Q_OBJECT diff --git a/UI/window-basic-main-browser.cpp b/frontend/widgets/OBSBasic_Browser.cpp similarity index 50% rename from UI/window-basic-main-browser.cpp rename to frontend/widgets/OBSBasic_Browser.cpp index e822bb134..cfcf6b42d 100644 --- a/UI/window-basic-main-browser.cpp +++ b/frontend/widgets/OBSBasic_Browser.cpp @@ -15,23 +15,150 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include "window-basic-main.hpp" - -#include +#include "OBSBasic.hpp" #ifdef BROWSER_AVAILABLE -#include +#include +#include + +#include +#include + +#include + +using namespace json11; #endif +#include + struct QCef; struct QCefCookieManager; -extern QCef *cef; -extern QCefCookieManager *panel_cookies; +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +#ifdef BROWSER_AVAILABLE +void OBSBasic::ClearExtraBrowserDocks() +{ + extraBrowserDockTargets.clear(); + extraBrowserDockNames.clear(); + extraBrowserDocks.clear(); +} + +void OBSBasic::LoadExtraBrowserDocks() +{ + const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks"); + + std::string err; + Json json = Json::parse(jsonStr, err); + if (!err.empty()) + return; + + Json::array array = json.array_items(); + if (!array.empty()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + for (Json &item : array) { + std::string title = item["title"].string_value(); + std::string url = item["url"].string_value(); + std::string uuid = item["uuid"].string_value(); + + AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false); + } +} + +void OBSBasic::SaveExtraBrowserDocks() +{ + Json::array array; + for (int i = 0; i < extraBrowserDocks.size(); i++) { + QDockWidget *dock = extraBrowserDocks[i].get(); + QString title = extraBrowserDockNames[i]; + QString url = extraBrowserDockTargets[i]; + QString uuid = dock->property("uuid").toString(); + Json::object obj{ + {"title", QT_TO_UTF8(title)}, + {"url", QT_TO_UTF8(url)}, + {"uuid", QT_TO_UTF8(uuid)}, + }; + array.push_back(obj); + } + + std::string output = Json(array).dump(); + config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str()); +} + +void OBSBasic::ManageExtraBrowserDocks() +{ + if (!extraBrowsers.isNull()) { + extraBrowsers->show(); + extraBrowsers->raise(); + return; + } + + extraBrowsers = new OBSExtraBrowsers(this); + extraBrowsers->show(); +} + +void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate) +{ + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + BrowserDock *dock = new BrowserDock(title); + QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid); + bId.replace(QRegularExpression("[{}-]"), ""); + dock->setProperty("uuid", bId); + dock->setObjectName(title + OBJ_NAME_SUFFIX); + dock->resize(460, 600); + dock->setMinimumSize(80, 80); + dock->setWindowTitle(title); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + dock->SetWidget(browser); + + /* Add support for Twitch Dashboard panels */ + if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) { + QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/"); + QRegularExpressionMatch match = re.match(url); + QString username = match.captured(1); + if (username.length() > 0) { + std::string script; + script = "Object.defineProperty(document, 'referrer', { get: () => '"; + script += "https://twitch.tv/"; + script += QT_TO_UTF8(username); + script += "/dashboard/live"; + script += "'});"; + browser->setStartupScript(script); + } + } + + AddDockWidget(dock, Qt::RightDockWidgetArea, true); + extraBrowserDocks.push_back(std::shared_ptr(dock)); + extraBrowserDockNames.push_back(title); + extraBrowserDockTargets.push_back(url); + + if (firstCreate) { + dock->setFloating(true); + + QPoint curPos = pos(); + QSize wSizeD2 = size() / 2; + QSize dSizeD2 = dock->size() / 2; + + curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width()); + curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height()); + + dock->move(curPos); + dock->setVisible(true); + } +} +#endif static std::string GenId() { diff --git a/frontend/widgets/OBSBasic_Clipboard.cpp b/frontend/widgets/OBSBasic_Clipboard.cpp new file mode 100644 index 000000000..655591fb7 --- /dev/null +++ b/frontend/widgets/OBSBasic_Clipboard.cpp @@ -0,0 +1,249 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include +#include + +extern void undo_redo(const std::string &data); + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} diff --git a/frontend/widgets/OBSBasic_ContextToolbar.cpp b/frontend/widgets/OBSBasic_ContextToolbar.cpp new file mode 100644 index 000000000..ccb3474a3 --- /dev/null +++ b/frontend/widgets/OBSBasic_ContextToolbar.cpp @@ -0,0 +1,295 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} diff --git a/frontend/widgets/OBSBasic_Docks.cpp b/frontend/widgets/OBSBasic_Docks.cpp new file mode 100644 index 000000000..c2bdaf7ec --- /dev/null +++ b/frontend/widgets/OBSBasic_Docks.cpp @@ -0,0 +1,351 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} diff --git a/UI/window-basic-main-dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp similarity index 90% rename from UI/window-basic-main-dropfiles.cpp rename to frontend/widgets/OBSBasic_Dropfiles.cpp index 0e4713816..3f251951a 100644 --- a/UI/window-basic-main-dropfiles.cpp +++ b/frontend/widgets/OBSBasic_Dropfiles.cpp @@ -1,17 +1,32 @@ -#include -#include -#include -#include +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + #include #include -#include #ifdef _WIN32 #include #endif -#include -#include - -#include "window-basic-main.hpp" +#include using namespace std; diff --git a/frontend/widgets/OBSBasic_Hotkeys.cpp b/frontend/widgets/OBSBasic_Hotkeys.cpp new file mode 100644 index 000000000..3be00aeee --- /dev/null +++ b/frontend/widgets/OBSBasic_Hotkeys.cpp @@ -0,0 +1,310 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} diff --git a/UI/window-basic-main-icons.cpp b/frontend/widgets/OBSBasic_Icons.cpp similarity index 76% rename from UI/window-basic-main-icons.cpp rename to frontend/widgets/OBSBasic_Icons.cpp index eabda2a73..cf59dea7d 100644 --- a/UI/window-basic-main-icons.cpp +++ b/frontend/widgets/OBSBasic_Icons.cpp @@ -1,4 +1,23 @@ -#include +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" QIcon OBSBasic::GetSourceIcon(const char *id) const { diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp new file mode 100644 index 000000000..6b3c7958c --- /dev/null +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -0,0 +1,682 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "OBSBasicStats.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#endif +#include +#include +#ifdef _WIN32 +#include +#endif +#include +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include +#endif +#include + +#include + +#include + +#ifdef _WIN32 +#include +#endif + +extern bool restart; +extern bool restart_safe; +extern volatile long insideEventLoop; +extern bool safe_mode; + +struct QCef; +struct QCefCookieManager; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +using namespace std; + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} diff --git a/frontend/widgets/OBSBasic_OutputHandler.cpp b/frontend/widgets/OBSBasic_OutputHandler.cpp new file mode 100644 index 000000000..aa1778f53 --- /dev/null +++ b/frontend/widgets/OBSBasic_OutputHandler.cpp @@ -0,0 +1,139 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +#include + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp new file mode 100644 index 000000000..f9f334ac6 --- /dev/null +++ b/frontend/widgets/OBSBasic_Preview.cpp @@ -0,0 +1,661 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include + +#include + +#include + +#include + +extern void undo_redo(const std::string &data); + +using namespace std; + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + obs_enter_graphics(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + box = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::DrawBackdrop(float cx, float cy) +{ + if (!box) + return; + + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop"); + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 colorVal; + vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(color, &colorVal); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_scale3f(float(cx), float(cy), 1.0f); + + gs_load_vertexbuffer(box); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_matrix_pop(); + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_load_vertexbuffer(nullptr); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_locked(item)) + return true; + + struct vec2 &offset = *reinterpret_cast(param); + struct vec2 pos; + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset); + } + + return true; + } + + obs_sceneitem_get_pos(item, &pos); + vec2_add(&pos, &pos, &offset); + obs_sceneitem_set_pos(item, &pos); + return true; +} + +void OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->Locked()) + return; + + struct vec2 offset; + vec2_set(&offset, 0.0f, 0.0f); + + switch (dir) { + case MoveDir::Up: + offset.y = (float)-dist; + break; + case MoveDir::Down: + offset.y = (float)dist; + break; + case MoveDir::Left: + offset.x = (float)-dist; + break; + case MoveDir::Right: + offset.x = (float)dist; + break; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} diff --git a/UI/window-basic-main-profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp similarity index 98% rename from UI/window-basic-main-profiles.cpp rename to frontend/widgets/OBSBasic_Profiles.cpp index 9b346e8d4..2e33dbcd1 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/frontend/widgets/OBSBasic_Profiles.cpp @@ -15,21 +15,17 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#ifdef YOUTUBE_ENABLED +#include +#endif +#include + #include -#include "window-basic-main.hpp" -#include "window-basic-auto-config.hpp" -#include "window-namedialog.hpp" + +#include +#include // MARK: Constant Expressions @@ -38,6 +34,8 @@ constexpr std::string_view OBSProfileSettingsFile = "basic.ini"; // MARK: Forward Declarations +extern bool restart; + extern void DestroyPanelCookieManager(); extern void DuplicateCurrentCookieProfile(ConfigFile &config); extern void CheckExistingCookieId(); diff --git a/frontend/widgets/OBSBasic_Projectors.cpp b/frontend/widgets/OBSBasic_Projectors.cpp new file mode 100644 index 000000000..4ffe09bac --- /dev/null +++ b/frontend/widgets/OBSBasic_Projectors.cpp @@ -0,0 +1,274 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QRect screenGeometry = screen->geometry(); + qreal ratio = screen->devicePixelRatio(); + QString name = ""; +#if defined(__APPLE__) || defined(_WIN32) + name = screen->name(); +#else + name = screen->model().simplified(); + + if (name.length() > 1 && name.endsWith("-")) + name.chop(1); +#endif + name = name.simplified(); + + if (name.length() == 0) { + name = QString("%1 %2").arg(QTStr("Display")).arg(QString::number(i + 1)); + } + QString str = QString("%1: %2x%3 @ %4,%5") + .arg(name, QString::number(screenGeometry.width() * ratio), + QString::number(screenGeometry.height() * ratio), + QString::number(screenGeometry.x()), QString::number(screenGeometry.y())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::DeleteProjector(OBSProjector *projector) +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i] == projector) { + projectors[i]->deleteLater(); + projectors.erase(projectors.begin() + i); + break; + } + } +} + +OBSProjector *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "BasicWindow", "CloseExistingProjectors"); + + if (closeProjectors && monitor > -1) { + for (size_t i = projectors.size(); i > 0; i--) { + size_t idx = i - 1; + if (projectors[idx]->GetMonitor() == monitor) + DeleteProjector(projectors[idx]); + } + } + + OBSProjector *projector = new OBSProjector(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} diff --git a/frontend/widgets/OBSBasic_Recording.cpp b/frontend/widgets/OBSBasic_Recording.cpp new file mode 100644 index 000000000..83f030488 --- /dev/null +++ b/frontend/widgets/OBSBasic_Recording.cpp @@ -0,0 +1,408 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include + +#include + +#include +#include + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" + +extern volatile bool replaybuf_active; + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} diff --git a/frontend/widgets/OBSBasic_ReplayBuffer.cpp b/frontend/widgets/OBSBasic_ReplayBuffer.cpp new file mode 100644 index 000000000..8d95f2210 --- /dev/null +++ b/frontend/widgets/OBSBasic_ReplayBuffer.cpp @@ -0,0 +1,218 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +#include + +#include + +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp new file mode 100644 index 000000000..07c16cf6b --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -0,0 +1,1562 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +extern bool safe_mode; +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_virtualcam; +extern bool opt_start_replaybuffer; +extern std::string opt_starting_scene; + +// MARK: Constant Expressions + +constexpr std::string_view OBSSceneCollectionPath = "/obs-studio/basic/scenes/"; + +// MARK: - Anonymous Namespace +namespace { +QList sortedSceneCollections{}; + +void updateSortedSceneCollections(const OBSSceneCollectionCache &collections) +{ + const QLocale locale = QLocale::system(); + QList newList{}; + + for (auto [collectionName, _] : collections) { + QString entry = QString::fromStdString(collectionName); + newList.append(entry); + } + + std::sort(newList.begin(), newList.end(), [&locale](const QString &lhs, const QString &rhs) -> bool { + int result = QString::localeAwareCompare(locale.toLower(lhs), locale.toLower(rhs)); + + return (result < 0); + }); + + sortedSceneCollections.swap(newList); +} + +void cleanBackupCollision(const OBSSceneCollection &collection) +{ + std::filesystem::path backupFilePath = collection.collectionFile; + backupFilePath.replace_extension(".json.bak"); + + if (std::filesystem::exists(backupFilePath)) { + try { + std::filesystem::remove(backupFilePath); + } catch (std::filesystem::filesystem_error &) { + throw std::logic_error("Failed to remove pre-existing scene collection backup file: " + + backupFilePath.u8string()); + } + } +} +} // namespace + +// MARK: - Main Scene Collection Management Functions + +void OBSBasic::SetupNewSceneCollection(const std::string &collectionName) +{ + const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + + cleanBackupCollision(newCollection); + ActivateSceneCollection(newCollection); + + blog(LOG_INFO, "Created scene collection '%s' (clean, %s)", newCollection.name.c_str(), + newCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::SetupDuplicateSceneCollection(const std::string &collectionName) +{ + const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + SaveProjectNow(); + + const auto copyOptions = std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error("Failed to copy file for cloned scene collection: " + newCollection.name); + } + + OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str()); + + obs_data_set_string(collection, "name", newCollection.name.c_str()); + + OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources"); + + if (sources) { + obs_data_erase(collection, "sources"); + + obs_data_array_enum( + sources, + [](obs_data_t *data, void *) -> void { + const char *uuid = os_generate_uuid(); + + obs_data_set_string(data, "uuid", uuid); + + bfree((void *)uuid); + }, + nullptr); + + obs_data_set_array(collection, "sources", sources); + } + + obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr); + + cleanBackupCollision(newCollection); + ActivateSceneCollection(newCollection); + + blog(LOG_INFO, "Created scene collection '%s' (duplicate, %s)", newCollection.name.c_str(), + newCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::SetupRenameSceneCollection(const std::string &collectionName) +{ + const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName); + const OBSSceneCollection currentCollection = GetCurrentSceneCollection(); + + SaveProjectNow(); + + const auto copyOptions = std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error("Failed to copy file for scene collection: " + currentCollection.name); + } + + collections.erase(currentCollection.name); + + OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str()); + + obs_data_set_string(collection, "name", newCollection.name.c_str()); + + obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr); + + cleanBackupCollision(newCollection); + ActivateSceneCollection(newCollection); + RemoveSceneCollection(currentCollection); + + blog(LOG_INFO, "Renamed scene collection '%s' to '%s' (%s)", currentCollection.name.c_str(), + newCollection.name.c_str(), newCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_RENAMED); +} + +// MARK: - Scene Collection File Management Functions + +const OBSSceneCollection &OBSBasic::CreateSceneCollection(const std::string &collectionName) +{ + if (const auto &foundCollection = GetSceneCollectionByName(collectionName)) { + throw std::invalid_argument("Scene collection already exists: " + collectionName); + } + + std::string fileName; + if (!GetFileSafeName(collectionName.c_str(), fileName)) { + throw std::invalid_argument("Failed to create safe directory for new scene collection: " + + collectionName); + } + + std::string collectionFile; + collectionFile.reserve(App()->userScenesLocation.u8string().size() + OBSSceneCollectionPath.size() + + fileName.size()); + collectionFile.append(App()->userScenesLocation.u8string()).append(OBSSceneCollectionPath).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + throw std::invalid_argument("Failed to get closest file name for new scene collection: " + fileName); + } + + const std::filesystem::path collectionFilePath = std::filesystem::u8path(collectionFile); + + auto [iterator, success] = collections.try_emplace( + collectionName, + OBSSceneCollection{collectionName, collectionFilePath.filename().u8string(), collectionFilePath}); + + return iterator->second; +} + +void OBSBasic::RemoveSceneCollection(OBSSceneCollection collection) +{ + try { + std::filesystem::remove(collection.collectionFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error("Failed to remove scene collection file: " + collection.fileName); + } + + blog(LOG_INFO, "Removed scene collection '%s' (%s)", collection.name.c_str(), collection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +// MARK: - Scene Collection UI Handling Functions + +bool OBSBasic::CreateNewSceneCollection(const QString &name) +{ + try { + SetupNewSceneCollection(name.toStdString()); + return true; + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } +} + +bool OBSBasic::CreateDuplicateSceneCollection(const QString &name) +{ + try { + SetupDuplicateSceneCollection(name.toStdString()); + return true; + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } +} + +void OBSBasic::DeleteSceneCollection(const QString &name) +{ + const std::string_view currentCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + + if (currentCollectionName == name.toStdString()) { + on_actionRemoveSceneCollection_triggered(); + return; + } + + OBSSceneCollection currentCollection = GetCurrentSceneCollection(); + + RemoveSceneCollection(currentCollection); + + collections.erase(name.toStdString()); + + RefreshSceneCollections(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); +} + +void OBSBasic::ChangeSceneCollection() +{ + QAction *action = reinterpret_cast(sender()); + + if (!action) { + return; + } + + const std::string_view currentCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const QVariant qCollectionName = action->property("collection_name"); + const std::string selectedCollectionName{qCollectionName.toString().toStdString()}; + + if (currentCollectionName == selectedCollectionName) { + action->setChecked(true); + return; + } + + const std::optional foundCollection = GetSceneCollectionByName(selectedCollectionName); + + if (!foundCollection) { + const std::string errorMessage{"Selected scene collection not found: "}; + + throw std::invalid_argument(errorMessage + currentCollectionName.data()); + } + + const OBSSceneCollection &selectedCollection = foundCollection.value(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + + ActivateSceneCollection(selectedCollection); + + blog(LOG_INFO, "Switched to scene collection '%s' (%s)", selectedCollection.name.c_str(), + selectedCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::RefreshSceneCollections(bool refreshCache) +{ + std::string_view currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + + QList menuActions = ui->sceneCollectionMenu->actions(); + + for (auto &action : menuActions) { + QVariant variant = action->property("file_name"); + if (variant.typeName() != nullptr) { + delete action; + } + } + + if (refreshCache) { + RefreshSceneCollectionCache(); + } + + updateSortedSceneCollections(collections); + + size_t numAddedCollections = 0; + for (auto &name : sortedSceneCollections) { + const std::string collectionName = name.toStdString(); + try { + const OBSSceneCollection &collection = collections.at(collectionName); + const QString qCollectionName = QString().fromStdString(collectionName); + + QAction *action = new QAction(qCollectionName, this); + action->setProperty("collection_name", qCollectionName); + action->setProperty("file_name", QString().fromStdString(collection.fileName)); + connect(action, &QAction::triggered, this, &OBSBasic::ChangeSceneCollection); + action->setCheckable(true); + action->setChecked(collectionName == currentCollectionName); + + ui->sceneCollectionMenu->addAction(action); + + numAddedCollections += 1; + } catch (const std::out_of_range &error) { + blog(LOG_ERROR, "No scene collection with name %s found in scene collection cache.\n%s", + collectionName.c_str(), error.what()); + } + } + + ui->actionRemoveSceneCollection->setEnabled(numAddedCollections > 1); + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + main->ui->actionPasteFilters->setEnabled(false); + main->ui->actionPasteRef->setEnabled(false); + main->ui->actionPasteDup->setEnabled(false); +} + +// MARK: - Scene Collection Cache Functions + +void OBSBasic::RefreshSceneCollectionCache() +{ + OBSSceneCollectionCache foundCollections{}; + + const std::filesystem::path collectionsPath = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath.substr(1)); + + if (!std::filesystem::exists(collectionsPath)) { + blog(LOG_WARNING, "Failed to get scene collections config path"); + return; + } + + for (const auto &entry : std::filesystem::directory_iterator(collectionsPath)) { + if (entry.is_directory()) { + continue; + } + + if (entry.path().extension().u8string() != ".json") { + continue; + } + + OBSDataAutoRelease collectionData = + obs_data_create_from_json_file_safe(entry.path().u8string().c_str(), "bak"); + + std::string candidateName; + const char *collectionName = obs_data_get_string(collectionData, "name"); + + if (!collectionName) { + candidateName = entry.path().filename().u8string(); + } else { + candidateName = collectionName; + } + + foundCollections.try_emplace(candidateName, + OBSSceneCollection{candidateName, entry.path().filename().u8string(), + entry.path()}); + } + + collections.swap(foundCollections); +} + +const OBSSceneCollection &OBSBasic::GetCurrentSceneCollection() const +{ + std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + + if (currentCollectionName.empty()) { + throw std::invalid_argument("No valid scene collection name in configuration Basic->SceneCollection"); + } + + const auto &foundCollection = collections.find(currentCollectionName); + + if (foundCollection != collections.end()) { + return foundCollection->second; + } else { + throw std::invalid_argument("Scene collection not found in collection list: " + currentCollectionName); + } +} + +std::optional OBSBasic::GetSceneCollectionByName(const std::string &collectionName) const +{ + auto foundCollection = collections.find(collectionName); + + if (foundCollection == collections.end()) { + return {}; + } else { + return foundCollection->second; + } +} + +std::optional OBSBasic::GetSceneCollectionByFileName(const std::string &fileName) const +{ + for (auto &[iterator, collection] : collections) { + if (collection.fileName == fileName) { + return collection; + } + } + + return {}; +} + +// MARK: - Qt Slot Functions + +void OBSBasic::on_actionNewSceneCollection_triggered() +{ + const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { + if (GetSceneCollectionByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"), + Str("Basic.Main.AddSceneCollection.Text")}; + + OBSPromptResult result = PromptForName(request, sceneCollectionCallback); + + if (!result.success) { + return; + } + + try { + SetupNewSceneCollection(result.promptValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionDupSceneCollection_triggered() +{ + const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { + if (GetSceneCollectionByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"), + Str("Basic.Main.AddSceneCollection.Text")}; + + OBSPromptResult result = PromptForName(request, sceneCollectionCallback); + + if (!result.success) { + return; + } + + try { + SetupDuplicateSceneCollection(result.promptValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionRenameSceneCollection_triggered() +{ + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) { + if (GetSceneCollectionByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("Basic.Main.RenameSceneCollection.Title"), + Str("Basic.Main.AddSceneCollection.Text"), currentCollection.name}; + + OBSPromptResult result = PromptForName(request, sceneCollectionCallback); + + if (!result.success) { + return; + } + + try { + SetupRenameSceneCollection(result.promptValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionRemoveSceneCollection_triggered(bool skipConfirmation) +{ + if (collections.size() < 2) { + return; + } + + OBSSceneCollection currentCollection; + + try { + currentCollection = GetCurrentSceneCollection(); + + if (!skipConfirmation) { + const QString confirmationText = + QTStr("ConfirmRemove.Text").arg(QString::fromStdString(currentCollection.name)); + const QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmRemove.Title"), confirmationText); + + if (button == QMessageBox::No) { + return; + } + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + + collections.erase(currentCollection.name); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } + + const OBSSceneCollection &newCollection = collections.begin()->second; + + ActivateSceneCollection(newCollection); + RemoveSceneCollection(currentCollection); + + blog(LOG_INFO, "Switched to scene collection '%s' (%s)", newCollection.name.c_str(), + newCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::on_actionImportSceneCollection_triggered() +{ + OBSImporter imp(this); + imp.exec(); + + RefreshSceneCollections(true); +} + +void OBSBasic::on_actionExportSceneCollection_triggered() +{ + SaveProjectNow(); + + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + const QString home = QDir::homePath(); + + const QString destinationFileName = SaveFile(this, QTStr("Basic.MainMenu.SceneCollection.Export"), + home + "/" + currentCollection.fileName.c_str(), + "JSON Files (*.json)"); + + if (!destinationFileName.isEmpty() && !destinationFileName.isNull()) { + const std::filesystem::path sourceFile = currentCollection.collectionFile; + const std::filesystem::path destinationFile = + std::filesystem::u8path(destinationFileName.toStdString()); + + OBSDataAutoRelease collection = obs_data_create_from_json_file(sourceFile.u8string().c_str()); + + OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources"); + if (!sources) { + blog(LOG_WARNING, "No sources in exported scene collection"); + return; + } + + obs_data_erase(collection, "sources"); + + using OBSDataVector = std::vector; + + OBSDataVector sourceItems; + obs_data_array_enum( + sources, + [](obs_data_t *data, void *vector) -> void { + OBSDataVector &sourceItems{*static_cast(vector)}; + sourceItems.push_back(data); + }, + &sourceItems); + + std::sort(sourceItems.begin(), sourceItems.end(), [](const OBSData &a, const OBSData &b) { + return astrcmpi(obs_data_get_string(a, "name"), obs_data_get_string(b, "name")) < 0; + }); + + OBSDataArrayAutoRelease newSources = obs_data_array_create(); + for (auto &item : sourceItems) { + obs_data_array_push_back(newSources, item); + } + + obs_data_set_array(collection, "sources", newSources); + obs_data_save_json_pretty_safe(collection, destinationFile.u8string().c_str(), "tmp", "bak"); + } +} + +void OBSBasic::on_actionRemigrateSceneCollection_triggered() +{ + if (Active()) { + OBSMessageBox::warning(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), + QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.Active")); + return; + } + + OBSDataAutoRelease priv = obs_get_private_data(); + + if (!usingAbsoluteCoordinates && !migrationBaseResolution) { + OBSMessageBox::warning( + this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), + QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.UnknownBaseResolution")); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + if (!usingAbsoluteCoordinates && migrationBaseResolution->first == ovi.base_width && + migrationBaseResolution->second == ovi.base_height) { + OBSMessageBox::warning( + this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), + QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.BaseResolutionMatches")); + return; + } + + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + QString name = QString::fromStdString(currentCollection.name); + QString message = + QTStr("Basic.Main.RemigrateSceneCollection.Text").arg(name).arg(ovi.base_width).arg(ovi.base_height); + + auto answer = OBSMessageBox::question(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), message); + + if (answer == QMessageBox::No) + return; + + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + if (!usingAbsoluteCoordinates) { + /* Temporarily change resolution to migration resolution */ + ovi.base_width = migrationBaseResolution->first; + ovi.base_height = migrationBaseResolution->second; + + if (obs_reset_video(&ovi) != OBS_VIDEO_SUCCESS) { + OBSMessageBox::critical( + this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), + QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.FailedVideoReset")); + return; + } + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + + /* Save and immediately reload to (re-)run migrations. */ + SaveProjectNow(); + /* Reset video if we potentially changed to a temporary resolution */ + if (!usingAbsoluteCoordinates) { + ResetVideo(); + } + + ActivateSceneCollection(currentCollection); +} + +// MARK: - Scene Collection Management Helper Functions + +void OBSBasic::ActivateSceneCollection(const OBSSceneCollection &collection) +{ + const std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + + if (auto foundCollection = GetSceneCollectionByName(currentCollectionName)) { + if (collection.name != foundCollection.value().name) { + SaveProjectNow(); + } + } + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", collection.name.c_str()); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", collection.fileName.c_str()); + + Load(collection.collectionFile.u8string().c_str()); + + RefreshSceneCollections(); + + UpdateTitleBar(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); +} + +// MARK: - OBSBasic Scene Collection Functions + +using namespace std; + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val) +{ + const char *name = obs_source_get_name(filter); + const char *id = obs_source_get_id(filter); + int val = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool LogSceneItem(obs_scene_t *, obs_sceneitem_t *item, void *v_val) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + const char *name = obs_source_get_name(source); + const char *id = obs_source_get_id(source); + int indent_count = (int)(intptr_t)v_val; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " %s- monitoring: %s", indent.c_str(), type); + } + int child_indent = 1 + indent_count; + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)child_indent); + + obs_source_t *show_tn = obs_sceneitem_get_transition(item, true); + obs_source_t *hide_tn = obs_sceneitem_get_transition(item, false); + if (show_tn) + blog(LOG_INFO, " %s- show: '%s' (%s)", indent.c_str(), obs_source_get_name(show_tn), + obs_source_get_id(show_tn)); + if (hide_tn) + blog(LOG_INFO, " %s- hide: '%s' (%s)", indent.c_str(), obs_source_get_name(hide_tn), + obs_source_get_id(hide_tn)); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, LogSceneItem, (void *)(intptr_t)child_indent); + return true; +} + +void OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp new file mode 100644 index 000000000..f28e3d07c --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -0,0 +1,1407 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "ColorSelect.hpp" +#include "OBSProjector.hpp" +#include "VolControl.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include + +using namespace std; + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) +{ + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); + vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); + + auto GetMinPos = [&](float x, float y) { + vec3 pos; + vec3_set(&pos, x, y, 0.0f); + vec3_transform(&pos, &pos, &boxTransform); + vec3_min(&tl, &tl, &pos); + vec3_max(&br, &br, &pos); + }; + + GetMinPos(0.0f, 0.0f); + GetMinPos(1.0f, 0.0f); + GetMinPos(0.0f, 1.0f); + GetMinPos(1.0f, 1.0f); +} + +static vec3 GetItemTL(obs_sceneitem_t *item) +{ + vec3 tl, br; + GetItemBox(item, tl, br); + return tl; +} + +static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) +{ + vec3 newTL; + vec2 pos; + + obs_sceneitem_get_pos(item, &pos); + newTL = GetItemTL(item); + pos.x += tl.x - newTL.x; + pos.y += tl.y - newTL.y; + obs_sceneitem_set_pos(item, &pos); +} + +static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + float rot = *reinterpret_cast(param); + + vec3 tl = GetItemTL(item); + + rot += obs_sceneitem_get_rot(item); + if (rot >= 360.0f) + rot -= 360.0f; + else if (rot <= -360.0f) + rot += 360.0f; + obs_sceneitem_set_rot(item, rot); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + vec2 &mul = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + vec3 tl = GetItemTL(item); + + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + vec2_mul(&scale, &scale, &mul); + obs_sceneitem_set_scale(item, &scale); + + obs_sceneitem_force_update_transform(item); + + SetItemTL(item, tl); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_bounds_type boundsType = *reinterpret_cast(param); + + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_video_info ovi; + obs_get_video_info(&ovi); + + obs_transform_info itemInfo; + vec2_set(&itemInfo.pos, 0.0f, 0.0f); + vec2_set(&itemInfo.scale, 1.0f, 1.0f); + itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + itemInfo.rot = 0.0f; + + vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); + itemInfo.bounds_type = boundsType; + itemInfo.bounds_alignment = OBS_ALIGN_CENTER; + itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); + + obs_sceneitem_set_info2(item, &itemInfo); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + if (!items.size()) + return; + + // Get center x, y coordinates of items + vec3 center; + + float top = M_INFINITE; + float left = M_INFINITE; + float right = 0.0f; + float bottom = 0.0f; + + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + left = std::min(tl.x, left); + top = std::min(tl.y, top); + right = std::max(br.x, right); + bottom = std::max(br.y, bottom); + } + + center.x = (right + left) / 2.0f; + center.y = (top + bottom) / 2.0f; + center.z = 0.0f; + + // Get coordinates of screen center + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); + + vec3_mulf(&screenCenter, &screenCenter, 0.5f); + + // Calculate difference between screen center and item center + vec3 offset; + vec3_sub(&offset, &screenCenter, ¢er); + + // Shift items by offset + for (auto &item : items) { + vec3 tl, br; + + GetItemBox(item, tl, br); + + vec3_add(&tl, &tl, &offset); + + vec3 itemTL = GetItemTL(item); + + if (centerType == CenterType::Vertical) + tl.x = itemTL.x; + else if (centerType == CenterType::Horizontal) + tl.y = itemTL.y; + + SetItemTL(item, tl); + } +} + +void OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} diff --git a/frontend/widgets/OBSBasic_Scenes.cpp b/frontend/widgets/OBSBasic_Scenes.cpp new file mode 100644 index 000000000..ffc029ce4 --- /dev/null +++ b/frontend/widgets/OBSBasic_Scenes.cpp @@ -0,0 +1,973 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" + +#include + +#include + +#include +#include + +#include + +using namespace std; + +namespace { + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +extern void undo_redo(const std::string &data); + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +static bool select_one(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) +{ + obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + + obs_sceneitem_select(item, (selectedItem == item)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} diff --git a/frontend/widgets/OBSBasic_Screenshots.cpp b/frontend/widgets/OBSBasic_Screenshots.cpp new file mode 100644 index 000000000..aee070f97 --- /dev/null +++ b/frontend/widgets/OBSBasic_Screenshots.cpp @@ -0,0 +1,54 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +#include + +void OBSBasic::Screenshot(OBSSource source) +{ + if (!!screenshotData) { + blog(LOG_WARNING, "Cannot take new screenshot, " + "screenshot currently in progress"); + return; + } + + screenshotData = new ScreenshotObj(source); +} + +void OBSBasic::ScreenshotSelectedSource() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (item) { + Screenshot(obs_sceneitem_get_source(item)); + } else { + blog(LOG_INFO, "Could not take a source screenshot: " + "no source selected"); + } +} + +void OBSBasic::ScreenshotProgram() +{ + Screenshot(GetProgramSource()); +} + +void OBSBasic::ScreenshotScene() +{ + Screenshot(GetCurrentSceneSource()); +} diff --git a/frontend/widgets/OBSBasic_Service.cpp b/frontend/widgets/OBSBasic_Service.cpp new file mode 100644 index 000000000..ee201bc4d --- /dev/null +++ b/frontend/widgets/OBSBasic_Service.cpp @@ -0,0 +1,122 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} diff --git a/frontend/widgets/OBSBasic_StatusBar.cpp b/frontend/widgets/OBSBasic_StatusBar.cpp new file mode 100644 index 000000000..a13229809 --- /dev/null +++ b/frontend/widgets/OBSBasic_StatusBar.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} diff --git a/frontend/widgets/OBSBasic_Streaming.cpp b/frontend/widgets/OBSBasic_Streaming.cpp new file mode 100644 index 000000000..db5a30046 --- /dev/null +++ b/frontend/widgets/OBSBasic_Streaming.cpp @@ -0,0 +1,444 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#ifdef YOUTUBE_ENABLED +#include +#include +#endif + +#include + +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Error"); + break; + + case OBS_OUTPUT_DISCONNECTED: + /* doesn't happen if output is set to reconnect. note that + * reconnects are handled in the output, not in the UI */ + use_last_error = true; + errorDescription = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} diff --git a/frontend/widgets/OBSBasic_StudioMode.cpp b/frontend/widgets/OBSBasic_StudioMode.cpp new file mode 100644 index 000000000..f15c2e4c9 --- /dev/null +++ b/frontend/widgets/OBSBasic_StudioMode.cpp @@ -0,0 +1,395 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" +#include "OBSProjector.hpp" + +#include +#include + +#include +#include + +#include + +void OBSBasic::CreateProgramDisplay() +{ + program = new OBSQTDisplay(); + + program->setContextMenuPolicy(Qt::CustomContextMenu); + connect(program.data(), &QWidget::customContextMenuRequested, this, &OBSBasic::ProgramViewContextMenuRequested); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizeProgram(ovi.base_width, ovi.base_height); + }; + + connect(program.data(), &OBSQTDisplay::DisplayResized, displayResize); + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderProgram, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizeProgram(ovi.base_width, ovi.base_height); + }; + + connect(program.data(), &OBSQTDisplay::DisplayCreated, addDisplay); + + program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +#define T_BAR_PRECISION 1024 +#define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) +#define T_BAR_CLAMP (T_BAR_PRECISION / 10) + +void OBSBasic::CreateProgramOptions() +{ + programOptions = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(); + layout->setSpacing(4); + + QPushButton *configTransitions = new QPushButton(); + configTransitions->setProperty("class", "icon-dots-vert"); + + QHBoxLayout *mainButtonLayout = new QHBoxLayout(); + mainButtonLayout->setSpacing(2); + + transitionButton = new QPushButton(QTStr("Transition")); + transitionButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + QHBoxLayout *quickTransitions = new QHBoxLayout(); + quickTransitions->setSpacing(2); + + QPushButton *addQuickTransition = new QPushButton(); + addQuickTransition->setProperty("class", "icon-plus"); + + QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions")); + quickTransitionsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + quickTransitions->addWidget(quickTransitionsLabel); + quickTransitions->addWidget(addQuickTransition); + + mainButtonLayout->addWidget(transitionButton); + mainButtonLayout->addWidget(configTransitions); + + tBar = new SliderIgnoreClick(Qt::Horizontal); + tBar->setMinimum(0); + tBar->setMaximum(T_BAR_PRECISION - 1); + + tBar->setProperty("class", "slider-tbar"); + + connect(tBar, &QSlider::valueChanged, this, &OBSBasic::TBarChanged); + connect(tBar, &QSlider::sliderReleased, this, &OBSBasic::TBarReleased); + + layout->addStretch(0); + layout->addLayout(mainButtonLayout); + layout->addLayout(quickTransitions); + layout->addWidget(tBar); + layout->addStretch(0); + + programOptions->setLayout(layout); + + auto onAdd = [this]() { + QScopedPointer menu(CreateTransitionMenu(this, nullptr)); + menu->exec(QCursor::pos()); + }; + + auto onConfig = [this]() { + QMenu menu(this); + QAction *action; + + auto toggleEditProperties = [this]() { + editPropertiesMode = !editPropertiesMode; + + OBSSource actualScene = OBSGetStrongRef(programScene); + if (actualScene) + TransitionToScene(actualScene, true); + }; + + auto toggleSwapScenesMode = [this]() { + swapScenesMode = !swapScenesMode; + }; + + auto toggleSceneDuplication = [this]() { + sceneDuplicationMode = !sceneDuplicationMode; + + OBSSource actualScene = OBSGetStrongRef(programScene); + if (actualScene) + TransitionToScene(actualScene, true); + }; + + auto showToolTip = [&]() { + QAction *act = menu.activeAction(); + QToolTip::showText(QCursor::pos(), act->toolTip(), &menu, menu.actionGeometry(act)); + }; + + action = menu.addAction(QTStr("QuickTransitions.DuplicateScene")); + action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT")); + action->setCheckable(true); + action->setChecked(sceneDuplicationMode); + connect(action, &QAction::triggered, toggleSceneDuplication); + connect(action, &QAction::hovered, showToolTip); + + action = menu.addAction(QTStr("QuickTransitions.EditProperties")); + action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT")); + action->setCheckable(true); + action->setChecked(editPropertiesMode); + action->setEnabled(sceneDuplicationMode); + connect(action, &QAction::triggered, toggleEditProperties); + connect(action, &QAction::hovered, showToolTip); + + action = menu.addAction(QTStr("QuickTransitions.SwapScenes")); + action->setToolTip(QTStr("QuickTransitions.SwapScenesTT")); + action->setCheckable(true); + action->setChecked(swapScenesMode); + connect(action, &QAction::triggered, toggleSwapScenesMode); + connect(action, &QAction::hovered, showToolTip); + + menu.exec(QCursor::pos()); + }; + + connect(transitionButton.data(), &QAbstractButton::clicked, this, &OBSBasic::TransitionClicked); + connect(addQuickTransition, &QAbstractButton::clicked, onAdd); + connect(configTransitions, &QAbstractButton::clicked, onConfig); +} + +void OBSBasic::TogglePreviewProgramMode() +{ + SetPreviewProgramMode(!IsPreviewProgramMode()); +} + +void OBSBasic::SetPreviewProgramMode(bool enabled) +{ + if (IsPreviewProgramMode() == enabled) + return; + + os_atomic_set_bool(&previewProgramMode, enabled); + emit PreviewProgramModeChanged(enabled); + + if (IsPreviewProgramMode()) { + if (!previewEnabled) + EnablePreviewDisplay(true); + + CreateProgramDisplay(); + CreateProgramOptions(); + + OBSScene curScene = GetCurrentScene(); + + OBSSceneAutoRelease dup; + if (sceneDuplicationMode) { + dup = obs_scene_duplicate(curScene, obs_source_get_name(obs_scene_get_source(curScene)), + editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY + : OBS_SCENE_DUP_PRIVATE_REFS); + } else { + dup = std::move(OBSScene(curScene)); + } + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *dup_source = obs_scene_get_source(dup); + obs_transition_set(transition, dup_source); + + if (curScene) { + obs_source_t *source = obs_scene_get_source(curScene); + obs_source_inc_showing(source); + lastScene = OBSGetWeakRef(source); + programScene = OBSGetWeakRef(source); + } + + RefreshQuickTransitions(); + + programLabel = new QLabel(QTStr("StudioMode.ProgramSceneLabel"), this); + programLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + programLabel->setProperty("class", "label-preview-title"); + + programWidget = new QWidget(); + programLayout = new QVBoxLayout(); + + programLayout->setContentsMargins(0, 0, 0, 0); + programLayout->setSpacing(0); + + programLayout->addWidget(programLabel); + programLayout->addWidget(program); + + programWidget->setLayout(programLayout); + + ui->previewLayout->addWidget(programOptions); + ui->previewLayout->addWidget(programWidget); + ui->previewLayout->setAlignment(programOptions, Qt::AlignCenter); + + OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED); + + blog(LOG_INFO, "Switched to Preview/Program mode"); + blog(LOG_INFO, "-----------------------------" + "-------------------"); + } else { + OBSSource actualProgramScene = OBSGetStrongRef(programScene); + if (!actualProgramScene) + actualProgramScene = GetCurrentSceneSource(); + else + SetCurrentScene(actualProgramScene, true); + TransitionToScene(actualProgramScene, true); + + delete programOptions; + delete program; + delete programLabel; + delete programWidget; + + if (lastScene) { + OBSSource actualLastScene = OBSGetStrongRef(lastScene); + if (actualLastScene) + obs_source_dec_showing(actualLastScene); + lastScene = nullptr; + } + + programScene = nullptr; + swapScene = nullptr; + prevFTBSource = nullptr; + + for (QuickTransition &qt : quickTransitions) + qt.button = nullptr; + + if (!previewEnabled) + EnablePreviewDisplay(false); + + ui->transitions->setEnabled(true); + tBarActive = false; + + OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED); + + blog(LOG_INFO, "Switched to regular Preview mode"); + blog(LOG_INFO, "-----------------------------" + "-------------------"); + } + + ResetUI(); + UpdateTitleBar(); +} + +void OBSBasic::RenderProgram(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderProgram"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->programCX = int(window->programScale * float(ovi.base_width)); + window->programCY = int(window->programScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->programX, window->programY, window->programCX, window->programCY); + + obs_render_main_texture_src_color_only(); + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + + /* resize program panel to fix to the top section of the window */ + targetSize = GetPixelSize(program); + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, programX, programY, programScale); + + programX += float(PREVIEW_EDGE_SIZE); + programY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::UpdatePreviewProgramIndicators() +{ + bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") + : false; + + ui->previewLabel->setVisible(labels); + + if (programLabel) + programLabel->setVisible(labels); + + if (!labels) + return; + + QString preview = + QTStr("StudioMode.PreviewSceneName").arg(QT_UTF8(obs_source_get_name(GetCurrentSceneSource()))); + + QString program = QTStr("StudioMode.ProgramSceneName").arg(QT_UTF8(obs_source_get_name(GetProgramSource()))); + + if (ui->previewLabel->text() != preview) + ui->previewLabel->setText(preview); + + if (programLabel && programLabel->text() != program) + programLabel->setText(program); +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} diff --git a/frontend/widgets/OBSBasic_SysTray.cpp b/frontend/widgets/OBSBasic_SysTray.cpp new file mode 100644 index 000000000..ef74a2a4c --- /dev/null +++ b/frontend/widgets/OBSBasic_SysTray.cpp @@ -0,0 +1,147 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +extern bool opt_minimize_tray; + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} diff --git a/UI/window-basic-main-transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp similarity index 78% rename from UI/window-basic-main-transitions.cpp rename to frontend/widgets/OBSBasic_Transitions.cpp index a17137b2b..07bd796b0 100644 --- a/UI/window-basic-main-transitions.cpp +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -15,27 +15,21 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include +#include "OBSBasic.hpp" + +#include +#include +#include +#include + #include #include -#include "window-basic-main.hpp" -#include "window-basic-main-outputs.hpp" -#include "window-basic-vcam-config.hpp" -#include "display-helpers.hpp" -#include "window-namedialog.hpp" -#include "menu-button.hpp" -#include "obs-hotkey.h" +#include +#include -using namespace std; - -Q_DECLARE_METATYPE(OBSScene); -Q_DECLARE_METATYPE(OBSSource); -Q_DECLARE_METATYPE(QuickTransition); +extern bool disable_3p_plugins; +extern bool safe_mode; static inline QString MakeQuickTransitionText(QuickTransition *qt) { @@ -99,15 +93,6 @@ void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt) (void *)(uintptr_t)qt->id); } -void QuickTransition::SourceRenamed(void *param, calldata_t *) -{ - QuickTransition *qt = reinterpret_cast(param); - - QString hotkeyName = QTStr("QuickTransitions.HotkeyName").arg(MakeQuickTransitionText(qt)); - - obs_hotkey_set_description(qt->hotkey, QT_TO_UTF8(hotkeyName)); -} - void OBSBasic::TriggerQuickTransition(int id) { QuickTransition *qt = GetQuickTransition(id); @@ -613,11 +598,6 @@ void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force) SetCurrentScene(source, force); } -template static T GetOBSRef(QListWidgetItem *item) -{ - return item->data(static_cast(QtDataRole::OBSRef)).value(); -} - void OBSBasic::SetCurrentScene(OBSSource scene, bool force) { if (!IsPreviewProgramMode()) { @@ -664,35 +644,6 @@ void OBSBasic::SetCurrentScene(OBSSource scene, bool force) } } -void OBSBasic::CreateProgramDisplay() -{ - program = new OBSQTDisplay(); - - program->setContextMenuPolicy(Qt::CustomContextMenu); - connect(program.data(), &QWidget::customContextMenuRequested, this, &OBSBasic::ProgramViewContextMenuRequested); - - auto displayResize = [this]() { - struct obs_video_info ovi; - - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayResized, displayResize); - - auto addDisplay = [this](OBSQTDisplay *window) { - obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderProgram, this); - - struct obs_video_info ovi; - if (obs_get_video_info(&ovi)) - ResizeProgram(ovi.base_width, ovi.base_height); - }; - - connect(program.data(), &OBSQTDisplay::DisplayCreated, addDisplay); - - program->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -} - void OBSBasic::TransitionClicked() { if (previewProgramMode) @@ -703,117 +654,6 @@ void OBSBasic::TransitionClicked() #define T_BAR_PRECISION_F ((float)T_BAR_PRECISION) #define T_BAR_CLAMP (T_BAR_PRECISION / 10) -void OBSBasic::CreateProgramOptions() -{ - programOptions = new QWidget(); - QVBoxLayout *layout = new QVBoxLayout(); - layout->setSpacing(4); - - QPushButton *configTransitions = new QPushButton(); - configTransitions->setProperty("class", "icon-dots-vert"); - - QHBoxLayout *mainButtonLayout = new QHBoxLayout(); - mainButtonLayout->setSpacing(2); - - transitionButton = new QPushButton(QTStr("Transition")); - transitionButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - QHBoxLayout *quickTransitions = new QHBoxLayout(); - quickTransitions->setSpacing(2); - - QPushButton *addQuickTransition = new QPushButton(); - addQuickTransition->setProperty("class", "icon-plus"); - - QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions")); - quickTransitionsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - quickTransitions->addWidget(quickTransitionsLabel); - quickTransitions->addWidget(addQuickTransition); - - mainButtonLayout->addWidget(transitionButton); - mainButtonLayout->addWidget(configTransitions); - - tBar = new SliderIgnoreClick(Qt::Horizontal); - tBar->setMinimum(0); - tBar->setMaximum(T_BAR_PRECISION - 1); - - tBar->setProperty("class", "slider-tbar"); - - connect(tBar, &QSlider::valueChanged, this, &OBSBasic::TBarChanged); - connect(tBar, &QSlider::sliderReleased, this, &OBSBasic::TBarReleased); - - layout->addStretch(0); - layout->addLayout(mainButtonLayout); - layout->addLayout(quickTransitions); - layout->addWidget(tBar); - layout->addStretch(0); - - programOptions->setLayout(layout); - - auto onAdd = [this]() { - QScopedPointer menu(CreateTransitionMenu(this, nullptr)); - menu->exec(QCursor::pos()); - }; - - auto onConfig = [this]() { - QMenu menu(this); - QAction *action; - - auto toggleEditProperties = [this]() { - editPropertiesMode = !editPropertiesMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto toggleSwapScenesMode = [this]() { - swapScenesMode = !swapScenesMode; - }; - - auto toggleSceneDuplication = [this]() { - sceneDuplicationMode = !sceneDuplicationMode; - - OBSSource actualScene = OBSGetStrongRef(programScene); - if (actualScene) - TransitionToScene(actualScene, true); - }; - - auto showToolTip = [&]() { - QAction *act = menu.activeAction(); - QToolTip::showText(QCursor::pos(), act->toolTip(), &menu, menu.actionGeometry(act)); - }; - - action = menu.addAction(QTStr("QuickTransitions.DuplicateScene")); - action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT")); - action->setCheckable(true); - action->setChecked(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleSceneDuplication); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.EditProperties")); - action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT")); - action->setCheckable(true); - action->setChecked(editPropertiesMode); - action->setEnabled(sceneDuplicationMode); - connect(action, &QAction::triggered, toggleEditProperties); - connect(action, &QAction::hovered, showToolTip); - - action = menu.addAction(QTStr("QuickTransitions.SwapScenes")); - action->setToolTip(QTStr("QuickTransitions.SwapScenesTT")); - action->setCheckable(true); - action->setChecked(swapScenesMode); - connect(action, &QAction::triggered, toggleSwapScenesMode); - connect(action, &QAction::hovered, showToolTip); - - menu.exec(QCursor::pos()); - }; - - connect(transitionButton.data(), &QAbstractButton::clicked, this, &OBSBasic::TransitionClicked); - connect(addQuickTransition, &QAbstractButton::clicked, onAdd); - connect(configTransitions, &QAbstractButton::clicked, onConfig); -} - void OBSBasic::TBarReleased() { int val = tBar->value(); @@ -894,11 +734,6 @@ int OBSBasic::GetTbarPosition() return tBar->value(); } -void OBSBasic::TogglePreviewProgramMode() -{ - SetPreviewProgramMode(!IsPreviewProgramMode()); -} - static inline void ResetQuickTransitionText(QuickTransition *qt) { qt->button->setText(MakeQuickTransitionText(qt)); @@ -1443,157 +1278,6 @@ void OBSBasic::EnableTransitionWidgets(bool enable) transitionButton->setEnabled(enable); } -void OBSBasic::SetPreviewProgramMode(bool enabled) -{ - if (IsPreviewProgramMode() == enabled) - return; - - os_atomic_set_bool(&previewProgramMode, enabled); - emit PreviewProgramModeChanged(enabled); - - if (IsPreviewProgramMode()) { - if (!previewEnabled) - EnablePreviewDisplay(true); - - CreateProgramDisplay(); - CreateProgramOptions(); - - OBSScene curScene = GetCurrentScene(); - - OBSSceneAutoRelease dup; - if (sceneDuplicationMode) { - dup = obs_scene_duplicate(curScene, obs_source_get_name(obs_scene_get_source(curScene)), - editPropertiesMode ? OBS_SCENE_DUP_PRIVATE_COPY - : OBS_SCENE_DUP_PRIVATE_REFS); - } else { - dup = std::move(OBSScene(curScene)); - } - - OBSSourceAutoRelease transition = obs_get_output_source(0); - obs_source_t *dup_source = obs_scene_get_source(dup); - obs_transition_set(transition, dup_source); - - if (curScene) { - obs_source_t *source = obs_scene_get_source(curScene); - obs_source_inc_showing(source); - lastScene = OBSGetWeakRef(source); - programScene = OBSGetWeakRef(source); - } - - RefreshQuickTransitions(); - - programLabel = new QLabel(QTStr("StudioMode.ProgramSceneLabel"), this); - programLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); - programLabel->setProperty("class", "label-preview-title"); - - programWidget = new QWidget(); - programLayout = new QVBoxLayout(); - - programLayout->setContentsMargins(0, 0, 0, 0); - programLayout->setSpacing(0); - - programLayout->addWidget(programLabel); - programLayout->addWidget(program); - - programWidget->setLayout(programLayout); - - ui->previewLayout->addWidget(programOptions); - ui->previewLayout->addWidget(programWidget); - ui->previewLayout->setAlignment(programOptions, Qt::AlignCenter); - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED); - - blog(LOG_INFO, "Switched to Preview/Program mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } else { - OBSSource actualProgramScene = OBSGetStrongRef(programScene); - if (!actualProgramScene) - actualProgramScene = GetCurrentSceneSource(); - else - SetCurrentScene(actualProgramScene, true); - TransitionToScene(actualProgramScene, true); - - delete programOptions; - delete program; - delete programLabel; - delete programWidget; - - if (lastScene) { - OBSSource actualLastScene = OBSGetStrongRef(lastScene); - if (actualLastScene) - obs_source_dec_showing(actualLastScene); - lastScene = nullptr; - } - - programScene = nullptr; - swapScene = nullptr; - prevFTBSource = nullptr; - - for (QuickTransition &qt : quickTransitions) - qt.button = nullptr; - - if (!previewEnabled) - EnablePreviewDisplay(false); - - ui->transitions->setEnabled(true); - tBarActive = false; - - OnEvent(OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED); - - blog(LOG_INFO, "Switched to regular Preview mode"); - blog(LOG_INFO, "-----------------------------" - "-------------------"); - } - - ResetUI(); - UpdateTitleBar(); -} - -void OBSBasic::RenderProgram(void *data, uint32_t, uint32_t) -{ - GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderProgram"); - - OBSBasic *window = static_cast(data); - obs_video_info ovi; - - obs_get_video_info(&ovi); - - window->programCX = int(window->programScale * float(ovi.base_width)); - window->programCY = int(window->programScale * float(ovi.base_height)); - - gs_viewport_push(); - gs_projection_push(); - - /* --------------------------------------- */ - - gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); - gs_set_viewport(window->programX, window->programY, window->programCX, window->programCY); - - obs_render_main_texture_src_color_only(); - gs_load_vertexbuffer(nullptr); - - /* --------------------------------------- */ - - gs_projection_pop(); - gs_viewport_pop(); - - GS_DEBUG_MARKER_END(); -} - -void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy) -{ - QSize targetSize; - - /* resize program panel to fix to the top section of the window */ - targetSize = GetPixelSize(program); - GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, - targetSize.height() - PREVIEW_EDGE_SIZE * 2, programX, programY, programScale); - - programX += float(PREVIEW_EDGE_SIZE); - programY += float(PREVIEW_EDGE_SIZE); -} - obs_data_array_t *OBSBasic::SaveTransitions() { obs_data_array_t *transitions = obs_data_array_create(); @@ -1673,27 +1357,7 @@ int OBSBasic::GetOverrideTransitionDuration(OBSSource source) return (int)obs_data_get_int(data, "transition_duration"); } -void OBSBasic::UpdatePreviewProgramIndicators() +int OBSBasic::GetTransitionDuration() { - bool labels = previewProgramMode ? config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioModeLabels") - : false; - - ui->previewLabel->setVisible(labels); - - if (programLabel) - programLabel->setVisible(labels); - - if (!labels) - return; - - QString preview = - QTStr("StudioMode.PreviewSceneName").arg(QT_UTF8(obs_source_get_name(GetCurrentSceneSource()))); - - QString program = QTStr("StudioMode.ProgramSceneName").arg(QT_UTF8(obs_source_get_name(GetProgramSource()))); - - if (ui->previewLabel->text() != preview) - ui->previewLabel->setText(preview); - - if (programLabel && programLabel->text() != program) - programLabel->setText(program); + return ui->transitionDuration->value(); } diff --git a/frontend/widgets/OBSBasic_Updater.cpp b/frontend/widgets/OBSBasic_Updater.cpp new file mode 100644 index 000000000..b13221128 --- /dev/null +++ b/frontend/widgets/OBSBasic_Updater.cpp @@ -0,0 +1,236 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +#ifdef _WIN32 +#include +#endif +#ifdef ENABLE_SPARKLE_UPDATER +#include +#include +#endif +#if defined(_WIN32) || defined(WHATSNEW_ENABLED) +#include +#include +#endif + +#ifdef BROWSER_AVAILABLE +#include +#endif +#include + +#ifdef _WIN32 +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ +#endif + +struct QCef; +struct QCefCookieManager; + +extern QCef *cef; +extern QCefCookieManager *panel_cookies; + +using namespace std; + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} diff --git a/frontend/widgets/OBSBasic_VirtualCam.cpp b/frontend/widgets/OBSBasic_VirtualCam.cpp new file mode 100644 index 000000000..3ccb535e6 --- /dev/null +++ b/frontend/widgets/OBSBasic_VirtualCam.cpp @@ -0,0 +1,172 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include +#include + +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} diff --git a/frontend/widgets/OBSBasic_VolControl.cpp b/frontend/widgets/OBSBasic_VolControl.cpp new file mode 100644 index 000000000..674d84829 --- /dev/null +++ b/frontend/widgets/OBSBasic_VolControl.cpp @@ -0,0 +1,317 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#include + +using namespace std; + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} diff --git a/frontend/widgets/OBSBasic_YouTube.cpp b/frontend/widgets/OBSBasic_YouTube.cpp new file mode 100644 index 000000000..bd708bb3f --- /dev/null +++ b/frontend/widgets/OBSBasic_YouTube.cpp @@ -0,0 +1,254 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#ifdef YOUTUBE_ENABLED +#include +#include +#include +#endif + +#include + +using namespace std; + +extern bool cef_js_avail; + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif diff --git a/UI/window-main.hpp b/frontend/widgets/OBSMainWindow.hpp similarity index 76% rename from UI/window-main.hpp rename to frontend/widgets/OBSMainWindow.hpp index cfecfc375..ad726ac34 100644 --- a/UI/window-main.hpp +++ b/frontend/widgets/OBSMainWindow.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include +#include + class OBSMainWindow : public QMainWindow { Q_OBJECT @@ -12,6 +12,4 @@ public: virtual config_t *Config() const = 0; virtual void OBSInit() = 0; - - virtual int GetProfilePath(char *path, size_t size, const char *file) const = 0; }; diff --git a/UI/window-projector.cpp b/frontend/widgets/OBSProjector.cpp similarity index 97% rename from UI/window-projector.cpp rename to frontend/widgets/OBSProjector.cpp index 60f873bc9..7d6beb0d8 100644 --- a/UI/window-projector.cpp +++ b/frontend/widgets/OBSProjector.cpp @@ -1,15 +1,17 @@ -#include -#include -#include -#include -#include +#include "OBSProjector.hpp" + +#include +#include +#include +#include +#include + #include -#include "moc_window-projector.cpp" -#include "obs-app.hpp" -#include "window-basic-main.hpp" -#include "display-helpers.hpp" -#include "platform.hpp" -#include "multiview.hpp" + +#include +#include + +#include "moc_OBSProjector.cpp" static QList multiviewProjectors; diff --git a/UI/window-projector.hpp b/frontend/widgets/OBSProjector.hpp similarity index 94% rename from UI/window-projector.hpp rename to frontend/widgets/OBSProjector.hpp index f95063261..417192ee0 100644 --- a/UI/window-projector.hpp +++ b/frontend/widgets/OBSProjector.hpp @@ -1,8 +1,8 @@ #pragma once -#include -#include "qt-display.hpp" -#include "multiview.hpp" +#include "OBSQTDisplay.hpp" + +class Multiview; enum class ProjectorType { Source, @@ -12,8 +12,6 @@ enum class ProjectorType { Multiview, }; -class QMouseEvent; - class OBSProjector : public OBSQTDisplay { Q_OBJECT diff --git a/UI/qt-display.cpp b/frontend/widgets/OBSQTDisplay.cpp similarity index 83% rename from UI/qt-display.cpp rename to frontend/widgets/OBSQTDisplay.cpp index f7c37ae9a..4f0d81fb3 100644 --- a/UI/qt-display.cpp +++ b/frontend/widgets/OBSQTDisplay.cpp @@ -1,57 +1,24 @@ -#include "moc_qt-display.cpp" -#include "display-helpers.hpp" -#include -#include -#include -#include +#include "OBSQTDisplay.hpp" -#include -#include +#include +#include + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#endif + +#include +#ifdef ENABLE_WAYLAND +#include +#include +#endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #endif -#if !defined(_WIN32) && !defined(__APPLE__) -#include -#endif - -#ifdef ENABLE_WAYLAND -#include -#endif - -class SurfaceEventFilter : public QObject { - OBSQTDisplay *display; - -public: - SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {} - -protected: - bool eventFilter(QObject *obj, QEvent *event) override - { - bool result = QObject::eventFilter(obj, event); - QPlatformSurfaceEvent *surfaceEvent; - - switch (event->type()) { - case QEvent::PlatformSurface: - surfaceEvent = static_cast(event); - - switch (surfaceEvent->surfaceEventType()) { - case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: - display->DestroyDisplay(); - break; - default: - break; - } - break; - default: - break; - } - - return result; - } -}; +#include "moc_OBSQTDisplay.cpp" static inline long long color_to_int(const QColor &color) { diff --git a/UI/qt-display.hpp b/frontend/widgets/OBSQTDisplay.hpp similarity index 99% rename from UI/qt-display.hpp rename to frontend/widgets/OBSQTDisplay.hpp index e83505a22..4c1152ba5 100644 --- a/UI/qt-display.hpp +++ b/frontend/widgets/OBSQTDisplay.hpp @@ -1,8 +1,9 @@ #pragma once -#include #include +#include + #define GREY_COLOR_BACKGROUND 0xFF4C4C4C class OBSQTDisplay : public QWidget { diff --git a/frontend/widgets/StatusBarWidget.cpp b/frontend/widgets/StatusBarWidget.cpp new file mode 100644 index 000000000..c5a30c8b7 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.cpp @@ -0,0 +1,10 @@ +#include "StatusBarWidget.hpp" +#include "ui_StatusBarWidget.h" +#include "moc_StatusBarWidget.cpp" + +StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) +{ + ui->setupUi(this); +} + +StatusBarWidget::~StatusBarWidget() {} diff --git a/frontend/widgets/StatusBarWidget.hpp b/frontend/widgets/StatusBarWidget.hpp new file mode 100644 index 000000000..67e803716 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class OBSBasicStatusBar; +class Ui_StatusBarWidget; + +class StatusBarWidget : public QWidget { + Q_OBJECT + + friend class OBSBasicStatusBar; + +private: + std::unique_ptr ui; + +public: + StatusBarWidget(QWidget *parent = nullptr); + ~StatusBarWidget(); +}; diff --git a/frontend/widgets/VolControl.cpp b/frontend/widgets/VolControl.cpp new file mode 100644 index 000000000..ea9ee5c6b --- /dev/null +++ b/frontend/widgets/VolControl.cpp @@ -0,0 +1,400 @@ +#include "VolControl.hpp" +#include "VolumeMeter.hpp" +#include "OBSBasic.hpp" + +#include +#include +#include + +#include + +#include "moc_VolControl.cpp" + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} diff --git a/frontend/widgets/VolControl.hpp b/frontend/widgets/VolControl.hpp new file mode 100644 index 000000000..fe4ba8a20 --- /dev/null +++ b/frontend/widgets/VolControl.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include + +class OBSSourceLabel; +class VolumeMeter; +class VolumeSlider; +class MuteCheckBox; +class QLabel; +class QPushButton; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; diff --git a/frontend/widgets/VolumeAccessibleInterface.cpp b/frontend/widgets/VolumeAccessibleInterface.cpp new file mode 100644 index 000000000..ce7e496ab --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.cpp @@ -0,0 +1,61 @@ +#include "VolumeAccessibleInterface.hpp" + +#include + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolumeAccessibleInterface.hpp b/frontend/widgets/VolumeAccessibleInterface.hpp new file mode 100644 index 000000000..2de609162 --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +class VolumeSlider; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/UI/volume-control.cpp b/frontend/widgets/VolumeMeter.cpp similarity index 65% rename from UI/volume-control.cpp rename to frontend/widgets/VolumeMeter.cpp index cd00c14d7..5379a9e8d 100644 --- a/UI/volume-control.cpp +++ b/frontend/widgets/VolumeMeter.cpp @@ -1,21 +1,13 @@ -#include "window-basic-main.hpp" -#include "moc_volume-control.cpp" -#include "obs-app.hpp" -#include "mute-checkbox.hpp" -#include "absolute-slider.hpp" -#include "source-label.hpp" +#include "VolumeMeter.hpp" -#include -#include -#include -#include -#include -#include +#include +#include + +#include +#include #include -using namespace std; - -#define FADER_PRECISION 4096.0 +#include "moc_VolumeMeter.cpp" // Size of the audio indicator in pixels #define INDICATOR_THICKNESS 3 @@ -25,385 +17,6 @@ using namespace std; std::weak_ptr VolumeMeter::updateTimer; -static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) -{ - if (muted) - return Qt::Checked; - else if (unassigned) - return Qt::PartiallyChecked; - else - return Qt::Unchecked; -} - -static inline bool IsSourceUnassigned(obs_source_t *source) -{ - uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); - obs_monitoring_type mt = obs_source_get_monitoring_type(source); - - return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; -} - -static void ShowUnassignedWarning(const char *name) -{ - auto msgBox = [=]() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); - msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - } - }; - - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); -} - -void VolControl::OBSVolumeChanged(void *data, float db) -{ - Q_UNUSED(db); - VolControl *volControl = static_cast(data); - - QMetaObject::invokeMethod(volControl, "VolumeChanged"); -} - -void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) -{ - VolControl *volControl = static_cast(data); - - volControl->volMeter->setLevels(magnitude, peak, inputPeak); -} - -void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) -{ - VolControl *volControl = static_cast(data); - bool muted = calldata_bool(calldata, "muted"); - - QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); -} - -void VolControl::VolumeChanged() -{ - slider->blockSignals(true); - slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); - slider->blockSignals(false); - - updateText(); -} - -void VolControl::VolumeMuted(bool muted) -{ - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) -{ - - VolControl *volControl = static_cast(data); - QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); -} - -void VolControl::MixersOrMonitoringChanged() -{ - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - - auto newState = GetCheckState(muted, unassigned); - if (mute->checkState() != newState) - mute->setCheckState(newState); - - volMeter->muted = muted || unassigned; -} - -void VolControl::SetMuted(bool) -{ - bool checked = mute->checkState() == Qt::Checked; - bool prev = obs_source_muted(source); - obs_source_set_muted(source, checked); - bool unassigned = IsSourceUnassigned(source); - - if (!checked && unassigned) { - mute->setCheckState(Qt::PartiallyChecked); - /* Show notice about the source no being assigned to any tracks */ - bool has_shown_warning = - config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); - if (!has_shown_warning) - ShowUnassignedWarning(obs_source_get_name(source)); - } - - auto undo_redo = [](const std::string &uuid, bool val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_muted(source, val); - }; - - QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); - - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); -} - -void VolControl::SliderChanged(int vol) -{ - float prev = obs_source_get_volume(source); - - obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); - updateText(); - - auto undo_redo = [](const std::string &uuid, float val) { - OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); - obs_source_set_volume(source, val); - }; - - float val = obs_source_get_volume(source); - const char *name = obs_source_get_name(source); - const char *uuid = obs_source_get_uuid(source); - OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), - std::bind(undo_redo, std::placeholders::_1, prev), - std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); -} - -void VolControl::updateText() -{ - QString text; - float db = obs_fader_get_db(obs_fader); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - volLabel->setText(text); - - bool muted = obs_source_muted(source); - const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; - - QString sourceName = obs_source_get_name(source); - QString accText = QTStr(accTextLookup).arg(sourceName); - - slider->setAccessibleName(accText); -} - -void VolControl::EmitConfigClicked() -{ - emit ConfigClicked(); -} - -void VolControl::SetMeterDecayRate(qreal q) -{ - volMeter->setPeakDecayRate(q); -} - -void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) -{ - volMeter->setPeakMeterType(peakMeterType); -} - -VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) - : source(std::move(source_)), - levelTotal(0.0f), - levelCount(0.0f), - obs_fader(obs_fader_create(OBS_FADER_LOG)), - obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), - vertical(vertical), - contextMenu(nullptr) -{ - nameLabel = new OBSSourceLabel(source); - volLabel = new QLabel(); - mute = new MuteCheckBox(); - - volLabel->setObjectName("volLabel"); - volLabel->setAlignment(Qt::AlignCenter); - -#ifdef __APPLE__ - mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); -#endif - - QString sourceName = obs_source_get_name(source); - setObjectName(sourceName); - - if (showConfig) { - config = new QPushButton(this); - config->setProperty("class", "icon-dots-vert"); - config->setAutoDefault(false); - - config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); - - connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); - } - - QVBoxLayout *mainLayout = new QVBoxLayout; - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - if (vertical) { - QHBoxLayout *nameLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QHBoxLayout *volLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QHBoxLayout *meterLayout = new QHBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, true); - slider = new VolumeSlider(obs_fader, Qt::Vertical); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - nameLayout->setAlignment(Qt::AlignCenter); - meterLayout->setAlignment(Qt::AlignCenter); - controlLayout->setAlignment(Qt::AlignCenter); - volLayout->setAlignment(Qt::AlignCenter); - - meterFrame->setObjectName("volMeterFrame"); - - nameLayout->setContentsMargins(0, 0, 0, 0); - nameLayout->setSpacing(0); - nameLayout->addWidget(nameLabel); - - controlLayout->setContentsMargins(0, 0, 0, 0); - controlLayout->setSpacing(0); - - // Add Headphone (audio monitoring) widget here - controlLayout->addWidget(mute); - - if (showConfig) { - controlLayout->addWidget(config); - } - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - meterLayout->addWidget(slider); - meterLayout->addWidget(volMeter); - - meterFrame->setLayout(meterLayout); - - volLayout->setContentsMargins(0, 0, 0, 0); - volLayout->setSpacing(0); - volLayout->addWidget(volLabel); - volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); - - mainLayout->addItem(nameLayout); - mainLayout->addItem(volLayout); - mainLayout->addWidget(meterFrame); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - - // Default size can cause clipping of long names in vertical layout. - QFont font = nameLabel->font(); - QFontInfo info(font); - nameLabel->setFont(font); - - setMaximumWidth(110); - } else { - QHBoxLayout *textLayout = new QHBoxLayout; - QHBoxLayout *controlLayout = new QHBoxLayout; - QFrame *meterFrame = new QFrame; - QVBoxLayout *meterLayout = new QVBoxLayout; - QVBoxLayout *buttonLayout = new QVBoxLayout; - - volMeter = new VolumeMeter(nullptr, obs_volmeter, false); - volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - slider = new VolumeSlider(obs_fader, Qt::Horizontal); - slider->setLayoutDirection(Qt::LeftToRight); - slider->setDisplayTicks(true); - - textLayout->setContentsMargins(0, 0, 0, 0); - textLayout->addWidget(nameLabel); - textLayout->addWidget(volLabel); - textLayout->setAlignment(nameLabel, Qt::AlignLeft); - textLayout->setAlignment(volLabel, Qt::AlignRight); - - meterFrame->setObjectName("volMeterFrame"); - meterFrame->setLayout(meterLayout); - - meterLayout->setContentsMargins(0, 0, 0, 0); - meterLayout->setSpacing(0); - - meterLayout->addWidget(volMeter); - meterLayout->addWidget(slider); - - buttonLayout->setContentsMargins(0, 0, 0, 0); - buttonLayout->setSpacing(0); - - if (showConfig) { - buttonLayout->addWidget(config); - } - buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); - buttonLayout->addWidget(mute); - - controlLayout->addItem(buttonLayout); - controlLayout->addWidget(meterFrame); - - mainLayout->addItem(textLayout); - mainLayout->addItem(controlLayout); - - volMeter->setFocusProxy(slider); - } - - setLayout(mainLayout); - - nameLabel->setText(sourceName); - - slider->setMinimum(0); - slider->setMaximum(int(FADER_PRECISION)); - - bool muted = obs_source_muted(source); - bool unassigned = IsSourceUnassigned(source); - mute->setCheckState(GetCheckState(muted, unassigned)); - volMeter->muted = muted || unassigned; - mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); - obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); - sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, - this); - - QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); - QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); - - obs_fader_attach_source(obs_fader, source); - obs_volmeter_attach_source(obs_volmeter, source); - - /* Call volume changed once to init the slider position and label */ - VolumeChanged(); -} - -void VolControl::EnableSlider(bool enable) -{ - slider->setEnabled(enable); -} - -VolControl::~VolControl() -{ - obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); - obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); - - sigs.clear(); - - if (contextMenu) - contextMenu->close(); -} - static inline QColor color_from_int(long long val) { QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); @@ -636,16 +249,6 @@ void VolumeMeter::setMeterFontScaling(qreal v) recalculateLayout = true; } -void VolControl::refreshColors() -{ - volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); - volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); - volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); - volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); - volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); - volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); -} - qreal VolumeMeter::getMinimumLevel() const { return minimumLevel; @@ -1349,159 +952,3 @@ void VolumeMeter::changeEvent(QEvent *e) QWidget::changeEvent(e); } - -void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) -{ - volumeMeters.push_back(meter); -} - -void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) -{ - volumeMeters.removeOne(meter); -} - -void VolumeMeterTimer::timerEvent(QTimerEvent *) -{ - for (VolumeMeter *meter : volumeMeters) { - if (meter->needLayoutChange()) { - // Tell paintEvent to update layout and paint everything - meter->update(); - } else { - // Tell paintEvent to paint only the bars - meter->update(meter->getBarRect()); - } - } -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) -{ - fad = fader; -} - -VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) - : AbsoluteSlider(orientation, parent) -{ - fad = fader; -} - -bool VolumeSlider::getDisplayTicks() const -{ - return displayTicks; -} - -void VolumeSlider::setDisplayTicks(bool display) -{ - displayTicks = display; -} - -void VolumeSlider::paintEvent(QPaintEvent *event) -{ - if (!getDisplayTicks()) { - QSlider::paintEvent(event); - return; - } - - QPainter painter(this); - QColor tickColor(91, 98, 115, 255); - - obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); - - QStyleOptionSlider opt; - initStyleOption(&opt); - - QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - if (orientation() == Qt::Horizontal) { - const int sliderWidth = groove.width() - handle.width(); - - float tickLength = groove.height() * 1.5; - tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); - - float yPos = groove.center().y() - (tickLength / 2) + 1; - - for (int db = -10; db >= -90; db -= 10) { - float tickValue = fader_db_to_def(db); - - float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); - painter.fillRect(xPos, yPos, 1, tickLength, tickColor); - } - } - - if (orientation() == Qt::Vertical) { - const int sliderHeight = groove.height() - handle.height(); - - float tickLength = groove.width() * 1.5; - tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); - - float xPos = groove.center().x() - (tickLength / 2) + 1; - - for (int db = -10; db >= -96; db -= 10) { - float tickValue = fader_db_to_def(db); - - float yPos = - groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); - painter.fillRect(xPos, yPos, tickLength, 1, tickColor); - } - } - - QSlider::paintEvent(event); -} - -VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} - -VolumeSlider *VolumeAccessibleInterface::slider() const -{ - return qobject_cast(object()); -} - -QString VolumeAccessibleInterface::text(QAccessible::Text t) const -{ - if (slider()->isVisible()) { - switch (t) { - case QAccessible::Text::Value: - return currentValue().toString(); - default: - break; - } - } - return QAccessibleWidget::text(t); -} - -QVariant VolumeAccessibleInterface::currentValue() const -{ - QString text; - float db = obs_fader_get_db(slider()->fad); - - if (db < -96.0f) - text = "-inf dB"; - else - text = QString::number(db, 'f', 1).append(" dB"); - - return text; -} - -void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) -{ - slider()->setValue(value.toInt()); -} - -QVariant VolumeAccessibleInterface::maximumValue() const -{ - return slider()->maximum(); -} - -QVariant VolumeAccessibleInterface::minimumValue() const -{ - return slider()->minimum(); -} - -QVariant VolumeAccessibleInterface::minimumStepSize() const -{ - return slider()->singleStep(); -} - -QAccessible::Role VolumeAccessibleInterface::role() const -{ - return QAccessible::Role::Slider; -} diff --git a/UI/volume-control.hpp b/frontend/widgets/VolumeMeter.hpp similarity index 75% rename from UI/volume-control.hpp rename to frontend/widgets/VolumeMeter.hpp index 51c77f247..a8cbc3d45 100644 --- a/UI/volume-control.hpp +++ b/frontend/widgets/VolumeMeter.hpp @@ -1,18 +1,13 @@ #pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include "absolute-slider.hpp" -class QPushButton; +#include +#include + +#define FADER_PRECISION 4096.0 + class VolumeMeterTimer; -class VolumeSlider; class VolumeMeter : public QWidget { Q_OBJECT @@ -225,117 +220,3 @@ protected: void paintEvent(QPaintEvent *event) override; void changeEvent(QEvent *e) override; }; - -class VolumeMeterTimer : public QTimer { - Q_OBJECT - -public: - inline VolumeMeterTimer() : QTimer() {} - - void AddVolControl(VolumeMeter *meter); - void RemoveVolControl(VolumeMeter *meter); - -protected: - void timerEvent(QTimerEvent *event) override; - QList volumeMeters; -}; - -class QLabel; -class VolumeSlider; -class MuteCheckBox; -class OBSSourceLabel; - -class VolControl : public QFrame { - Q_OBJECT - -private: - OBSSource source; - std::vector sigs; - OBSSourceLabel *nameLabel; - QLabel *volLabel; - VolumeMeter *volMeter; - VolumeSlider *slider; - MuteCheckBox *mute; - QPushButton *config = nullptr; - float levelTotal; - float levelCount; - OBSFader obs_fader; - OBSVolMeter obs_volmeter; - bool vertical; - QMenu *contextMenu; - - static void OBSVolumeChanged(void *param, float db); - static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], - const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); - static void OBSVolumeMuted(void *data, calldata_t *calldata); - static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); - - void EmitConfigClicked(); - -private slots: - void VolumeChanged(); - void VolumeMuted(bool muted); - void MixersOrMonitoringChanged(); - - void SetMuted(bool checked); - void SliderChanged(int vol); - void updateText(); - -signals: - void ConfigClicked(); - -public: - explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); - ~VolControl(); - - inline obs_source_t *GetSource() const { return source; } - - void SetMeterDecayRate(qreal q); - void setPeakMeterType(enum obs_peak_meter_type peakMeterType); - - void EnableSlider(bool enable); - inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } - - void refreshColors(); -}; - -class VolumeSlider : public AbsoluteSlider { - Q_OBJECT - -public: - obs_fader_t *fad; - - VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); - VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); - - bool getDisplayTicks() const; - void setDisplayTicks(bool display); - -private: - bool displayTicks = false; - QColor tickColor; - -protected: - virtual void paintEvent(QPaintEvent *event) override; -}; - -class VolumeAccessibleInterface : public QAccessibleWidget { - -public: - VolumeAccessibleInterface(QWidget *w); - - QVariant currentValue() const; - void setCurrentValue(const QVariant &value); - - QVariant maximumValue() const; - QVariant minimumValue() const; - - QVariant minimumStepSize() const; - -private: - VolumeSlider *slider() const; - -protected: - virtual QAccessible::Role role() const override; - virtual QString text(QAccessible::Text t) const override; -}; diff --git a/frontend/wizards/AutoConfig.cpp b/frontend/wizards/AutoConfig.cpp new file mode 100644 index 000000000..90bfe0f65 --- /dev/null +++ b/frontend/wizards/AutoConfig.cpp @@ -0,0 +1,381 @@ +#include "AutoConfig.hpp" +#include "AutoConfigStartPage.hpp" +#include "AutoConfigStreamPage.hpp" +#include "AutoConfigTestPage.hpp" +#include "AutoConfigVideoPage.hpp" +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigStreamPage.h" +#include "ui_AutoConfigVideoPage.h" + +#ifdef YOUTUBE_ENABLED +#include +#include +#endif +#include + +#include + +#include "moc_AutoConfig.cpp" + +constexpr std::string_view OBSServiceFileName = "service.json"; + +enum class ListOpt : int { + ShowAll = 1, + Custom, +}; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + if (!std::filesystem::exists(jsonFilePath)) { + return OBSData(); + } + + OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + return settings.Get(); +} + +static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) +{ + OBSData settings = OpenServiceSettings(type); + + service = obs_data_get_string(settings, "service"); + server = obs_data_get_string(settings, "server"); + key = obs_data_get_string(settings, "key"); +} + +AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) +{ + EnableThreadedMessageBoxes(true); + + calldata_t cd = {0}; + calldata_set_int(&cd, "seconds", 5); + + proc_handler_t *ph = obs_get_proc_handler(); + proc_handler_call(ph, "twitch_ingests_refresh", &cd); + proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); + calldata_free(&cd); + + OBSBasic *main = reinterpret_cast(parent); + main->EnableOutputs(false); + + installEventFilter(CreateShortcutFilter()); + + std::string serviceType; + GetServiceInfo(serviceType, serviceName, server, key); +#if defined(_WIN32) || defined(__APPLE__) + setWizardStyle(QWizard::ModernStyle); +#endif + streamPage = new AutoConfigStreamPage(); + + setPage(StartPage, new AutoConfigStartPage()); + setPage(VideoPage, new AutoConfigVideoPage()); + setPage(StreamPage, streamPage); + setPage(TestPage, new AutoConfigTestPage()); + setWindowTitle(QTStr("Basic.AutoConfig")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + baseResolutionCX = ovi.base_width; + baseResolutionCY = ovi.base_height; + + /* ----------------------------------------- */ + /* check to see if Twitch's "auto" available */ + + OBSDataAutoRelease twitchSettings = obs_data_create(); + + obs_data_set_string(twitchSettings, "service", "Twitch"); + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, twitchSettings); + + obs_property_t *p = obs_properties_get(props, "server"); + const char *first = obs_property_list_item_string(p, 0); + twitchAuto = strcmp(first, "auto") == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* check to see if Amazon IVS "auto" entries are available */ + + OBSDataAutoRelease amazonIVSSettings = obs_data_create(); + + obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); + + props = obs_get_service_properties("rtmp_common"); + obs_properties_apply_settings(props, amazonIVSSettings); + + p = obs_properties_get(props, "server"); + first = obs_property_list_item_string(p, 0); + amazonIVSAuto = strncmp(first, "auto", 4) == 0; + + obs_properties_destroy(props); + + /* ----------------------------------------- */ + /* load service/servers */ + + customServer = serviceType == "rtmp_custom"; + + QComboBox *serviceList = streamPage->ui->service; + + if (!serviceName.empty()) { + serviceList->blockSignals(true); + + int count = serviceList->count(); + bool found = false; + + for (int i = 0; i < count; i++) { + QString name = serviceList->itemText(i); + + if (name == serviceName.c_str()) { + serviceList->setCurrentIndex(i); + found = true; + break; + } + } + + if (!found) { + serviceList->insertItem(0, serviceName.c_str()); + serviceList->setCurrentIndex(0); + } + + serviceList->blockSignals(false); + } + + streamPage->UpdateServerList(); + streamPage->UpdateKeyLink(); + streamPage->UpdateMoreInfoLink(); + streamPage->lastService.clear(); + + if (!customServer) { + QComboBox *serverList = streamPage->ui->server; + int idx = serverList->findData(QString(server.c_str())); + if (idx == -1) + idx = 0; + + serverList->setCurrentIndex(idx); + } else { + streamPage->ui->customServer->setText(server.c_str()); + int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); + streamPage->ui->service->setCurrentIndex(idx); + } + + if (!key.empty()) + streamPage->ui->key->setText(key.c_str()); + + TestHardwareEncoding(); + + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") + : true; + streamPage->ui->bitrate->setValue(bitrate); + streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); + streamPage->ServiceChanged(); + + if (!hardwareEncodingAvailable) { + delete streamPage->ui->preferHardware; + streamPage->ui->preferHardware = nullptr; + } else { + /* Newer generations of NVENC have a high enough quality to + * bitrate ratio that if NVENC is available, it makes sense to + * just always prefer hardware encoding by default */ + bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; + streamPage->ui->preferHardware->setChecked(preferHardware); + } + + setOptions(QWizard::WizardOptions()); + setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); + setButtonText(QWizard::BackButton, QTStr("Back")); + setButtonText(QWizard::NextButton, QTStr("Next")); + setButtonText(QWizard::CancelButton, QTStr("Cancel")); +} + +AutoConfig::~AutoConfig() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + main->EnableOutputs(true); + EnableThreadedMessageBoxes(false); +} + +void AutoConfig::TestHardwareEncoding() +{ + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) { + if (strcmp(id, "ffmpeg_nvenc") == 0) + hardwareEncodingAvailable = nvencAvailable = true; + else if (strcmp(id, "obs_qsv11") == 0) + hardwareEncodingAvailable = qsvAvailable = true; + else if (strcmp(id, "h264_texture_amf") == 0) + hardwareEncodingAvailable = vceAvailable = true; +#ifdef __APPLE__ + else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 +#ifndef __aarch64__ + && os_get_emulation_status() == true +#endif + ) + if (__builtin_available(macOS 13.0, *)) + hardwareEncodingAvailable = appleAvailable = true; +#endif + } +} + +bool AutoConfig::CanTestServer(const char *server) +{ + if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) + return true; + + if (service == Service::Twitch) { + if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || + astrcmp_n(server, "US Central:", 11) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else { + return true; + } + + return false; +} + +void AutoConfig::done(int result) +{ + QWizard::done(result); + + if (result == QDialog::Accepted) { + if (type == Type::Streaming) + SaveStreamSettings(); + SaveSettings(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) { + OBSBasic *main = OBSBasic::Get(); + main->NewYouTubeAppDock(); + } +#endif + } +} + +inline const char *AutoConfig::GetEncoderId(Encoder enc) +{ + switch (enc) { + case Encoder::NVENC: + return SIMPLE_ENCODER_NVENC; + case Encoder::QSV: + return SIMPLE_ENCODER_QSV; + case Encoder::AMD: + return SIMPLE_ENCODER_AMD; + case Encoder::Apple: + return SIMPLE_ENCODER_APPLE_H264; + default: + return SIMPLE_ENCODER_X264; + } +}; + +void AutoConfig::SaveStreamSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* ---------------------------------- */ + /* save service */ + + const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + + obs_service_t *oldService = main->GetService(); + OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); + + OBSDataAutoRelease settings = obs_data_create(); + + if (!customServer) + obs_data_set_string(settings, "service", serviceName.c_str()); + obs_data_set_string(settings, "server", server.c_str()); +#ifdef YOUTUBE_ENABLED + if (!streamPage->auth || !IsYouTubeService(serviceName)) + obs_data_set_string(settings, "key", key.c_str()); +#else + obs_data_set_string(settings, "key", key.c_str()); +#endif + + OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); + + if (!newService) + return; + + main->SetService(newService); + main->SaveService(); + main->auth = streamPage->auth; + if (!!main->auth) { + main->auth->LoadUI(); + main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); + } else { + main->SetBroadcastFlowEnabled(false); + } + + /* ---------------------------------- */ + /* save stream settings */ + + config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); + config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); + config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); + + config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); + + if (multitrackVideo.targetBitrate.has_value()) + config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", + *multitrackVideo.targetBitrate); + else + config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); + + if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && + (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + } else if (multitrackVideo.bitrate.has_value()) { + config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); + config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", + *multitrackVideo.bitrate); + } +} + +void AutoConfig::SaveSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (recordingEncoder != Encoder::Stream) + config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); + + const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; + + config_set_string(main->Config(), "Output", "Mode", "Simple"); + config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); + config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); + config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); + config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); + config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); + + if (fpsType != FPSType::UseCurrent) { + config_set_uint(main->Config(), "Video", "FPSType", 0); + config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); + } + + main->ResetVideo(); + main->ResetOutputs(); + config_save_safe(main->Config(), "tmp", nullptr); +} diff --git a/frontend/wizards/AutoConfig.hpp b/frontend/wizards/AutoConfig.hpp new file mode 100644 index 000000000..92fa568f9 --- /dev/null +++ b/frontend/wizards/AutoConfig.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include + +class AutoConfigStreamPage; + +class AutoConfig : public QWizard { + Q_OBJECT + + friend class AutoConfigStartPage; + friend class AutoConfigVideoPage; + friend class AutoConfigStreamPage; + friend class AutoConfigTestPage; + + enum class Type { + Invalid, + Streaming, + Recording, + VirtualCam, + }; + + enum class Service { + Twitch, + YouTube, + AmazonIVS, + Other, + }; + + enum class Encoder { + x264, + NVENC, + QSV, + AMD, + Apple, + Stream, + }; + + enum class Quality { + Stream, + High, + }; + + enum class FPSType : int { + PreferHighFPS, + PreferHighRes, + UseCurrent, + fps30, + fps60, + }; + + struct StreamServer { + std::string name; + std::string address; + }; + + static inline const char *GetEncoderId(Encoder enc); + + AutoConfigStreamPage *streamPage = nullptr; + + Service service = Service::Other; + Quality recordingQuality = Quality::Stream; + Encoder recordingEncoder = Encoder::Stream; + Encoder streamingEncoder = Encoder::x264; + Type type = Type::Streaming; + FPSType fpsType = FPSType::PreferHighFPS; + int idealBitrate = 2500; + struct { + std::optional targetBitrate; + std::optional bitrate; + bool testSuccessful = false; + } multitrackVideo; + int baseResolutionCX = 1920; + int baseResolutionCY = 1080; + int idealResolutionCX = 1280; + int idealResolutionCY = 720; + int idealFPSNum = 60; + int idealFPSDen = 1; + std::string serviceName; + std::string serverName; + std::string server; + std::vector serviceConfigServers; + std::string key; + + bool hardwareEncodingAvailable = false; + bool nvencAvailable = false; + bool qsvAvailable = false; + bool vceAvailable = false; + bool appleAvailable = false; + + int startingBitrate = 2500; + bool customServer = false; + bool bandwidthTest = false; + bool testMultitrackVideo = false; + bool testRegions = true; + bool twitchAuto = false; + bool amazonIVSAuto = false; + bool regionUS = true; + bool regionEU = true; + bool regionAsia = true; + bool regionOther = true; + bool preferHighFPS = false; + bool preferHardware = false; + int specificFPSNum = 0; + int specificFPSDen = 0; + + void TestHardwareEncoding(); + bool CanTestServer(const char *server); + + virtual void done(int result) override; + + void SaveStreamSettings(); + void SaveSettings(); + +public: + AutoConfig(QWidget *parent); + ~AutoConfig(); + + enum Page { + StartPage, + VideoPage, + StreamPage, + TestPage, + }; +}; diff --git a/frontend/wizards/AutoConfigStartPage.cpp b/frontend/wizards/AutoConfigStartPage.cpp new file mode 100644 index 000000000..706123a6f --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.cpp @@ -0,0 +1,48 @@ +#include "AutoConfigStartPage.hpp" +#include "AutoConfig.hpp" +#include "ui_AutoConfigStartPage.h" + +#include + +#include "moc_AutoConfigStartPage.cpp" + +#define wiz reinterpret_cast(wizard()) + +AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.StartPage")); + setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); + + OBSBasic *main = OBSBasic::Get(); + if (main->VCamEnabled()) { + QRadioButton *prioritizeVCam = + new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); + QBoxLayout *box = reinterpret_cast(layout()); + box->insertWidget(2, prioritizeVCam); + + connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); + } +} + +AutoConfigStartPage::~AutoConfigStartPage() {} + +int AutoConfigStartPage::nextId() const +{ + return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; +} + +void AutoConfigStartPage::on_prioritizeStreaming_clicked() +{ + wiz->type = AutoConfig::Type::Streaming; +} + +void AutoConfigStartPage::on_prioritizeRecording_clicked() +{ + wiz->type = AutoConfig::Type::Recording; +} + +void AutoConfigStartPage::PrioritizeVCam() +{ + wiz->type = AutoConfig::Type::VirtualCam; +} diff --git a/frontend/wizards/AutoConfigStartPage.hpp b/frontend/wizards/AutoConfigStartPage.hpp new file mode 100644 index 000000000..73cff2838 --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +class Ui_AutoConfigStartPage; + +class AutoConfigStartPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigStartPage(QWidget *parent = nullptr); + ~AutoConfigStartPage(); + + virtual int nextId() const override; + +public slots: + void on_prioritizeStreaming_clicked(); + void on_prioritizeRecording_clicked(); + void PrioritizeVCam(); +}; diff --git a/UI/window-basic-auto-config.cpp b/frontend/wizards/AutoConfigStreamPage.cpp similarity index 55% rename from UI/window-basic-auto-config.cpp rename to frontend/wizards/AutoConfigStreamPage.cpp index 6b1667609..86d11c3cf 100644 --- a/UI/window-basic-auto-config.cpp +++ b/frontend/wizards/AutoConfigStreamPage.cpp @@ -1,245 +1,30 @@ -#include -#include - -#include -#include - -#include - -#include "moc_window-basic-auto-config.cpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" -#include "url-push-button.hpp" - -#include "goliveapi-postdata.hpp" -#include "goliveapi-network.hpp" -#include "multitrack-video-error.hpp" - -#include "ui_AutoConfigStartPage.h" -#include "ui_AutoConfigVideoPage.h" +#include "AutoConfigStreamPage.hpp" +#include "AutoConfig.hpp" #include "ui_AutoConfigStreamPage.h" -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" -#include "ui-config.h" +#include +#include +#include +#include #ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" +#include #endif +#include -struct QCef; -struct QCefCookieManager; +#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; - -#define wiz reinterpret_cast(wizard()) - -/* ------------------------------------------------------------------------- */ - -constexpr std::string_view OBSServiceFileName = "service.json"; - -static OBSData OpenServiceSettings(std::string &type) -{ - const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); - const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - - const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); - - if (!std::filesystem::exists(jsonFilePath)) { - return OBSData(); - } - - OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - - return settings.Get(); -} - -static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key) -{ - OBSData settings = OpenServiceSettings(type); - - service = obs_data_get_string(settings, "service"); - server = obs_data_get_string(settings, "server"); - key = obs_data_get_string(settings, "key"); -} - -/* ------------------------------------------------------------------------- */ - -AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) -{ - ui->setupUi(this); - setTitle(QTStr("Basic.AutoConfig.StartPage")); - setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); - - OBSBasic *main = OBSBasic::Get(); - if (main->VCamEnabled()) { - QRadioButton *prioritizeVCam = - new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this); - QBoxLayout *box = reinterpret_cast(layout()); - box->insertWidget(2, prioritizeVCam); - - connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam); - } -} - -AutoConfigStartPage::~AutoConfigStartPage() {} - -int AutoConfigStartPage::nextId() const -{ - return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage; -} - -void AutoConfigStartPage::on_prioritizeStreaming_clicked() -{ - wiz->type = AutoConfig::Type::Streaming; -} - -void AutoConfigStartPage::on_prioritizeRecording_clicked() -{ - wiz->type = AutoConfig::Type::Recording; -} - -void AutoConfigStartPage::PrioritizeVCam() -{ - wiz->type = AutoConfig::Type::VirtualCam; -} - -/* ------------------------------------------------------------------------- */ - -#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x -#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") -#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") -#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") -#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") -#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") - -AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) -{ - ui->setupUi(this); - - setTitle(QTStr("Basic.AutoConfig.VideoPage")); - setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; - - QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); - - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); - ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); - ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); - ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); - ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); - ui->fps->setCurrentIndex(0); - - QString cxStr = QString::number(ovi.base_width); - QString cyStr = QString::number(ovi.base_height); - - int encRes = int(ovi.base_width << 16) | int(ovi.base_height); - - // Auto config only supports testing down to 240p, don't allow current - // resolution if it's lower than that. - if (ovi.base_height >= 240) - ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); - - QList screens = QGuiApplication::screens(); - for (int i = 0; i < screens.size(); i++) { - QScreen *screen = screens[i]; - QSize as = screen->size(); - int as_width = as.width(); - int as_height = as.height(); - - // Calculate physical screen resolution based on the virtual screen resolution - // They might differ if scaling is enabled, e.g. for HiDPI screens - as_width = round(as_width * screen->devicePixelRatio()); - as_height = round(as_height * screen->devicePixelRatio()); - - encRes = as_width << 16 | as_height; - - QString str = - QTStr(RES_USE_DISPLAY) - .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); - - ui->canvasRes->addItem(str, encRes); - } - - auto addRes = [&](int cx, int cy) { - encRes = (cx << 16) | cy; - QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); - ui->canvasRes->addItem(str, encRes); - }; - - addRes(1920, 1080); - addRes(1280, 720); - - ui->canvasRes->setCurrentIndex(0); -} - -AutoConfigVideoPage::~AutoConfigVideoPage() {} - -int AutoConfigVideoPage::nextId() const -{ - return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; -} - -bool AutoConfigVideoPage::validatePage() -{ - int encRes = ui->canvasRes->currentData().toInt(); - wiz->baseResolutionCX = encRes >> 16; - wiz->baseResolutionCY = encRes & 0xFFFF; - wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - switch (wiz->fpsType) { - case AutoConfig::FPSType::PreferHighFPS: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = true; - break; - case AutoConfig::FPSType::PreferHighRes: - wiz->specificFPSNum = 0; - wiz->specificFPSDen = 0; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::UseCurrent: - wiz->specificFPSNum = ovi.fps_num; - wiz->specificFPSDen = ovi.fps_den; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps30: - wiz->specificFPSNum = 30; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - case AutoConfig::FPSType::fps60: - wiz->specificFPSNum = 60; - wiz->specificFPSDen = 1; - wiz->preferHighFPS = false; - break; - } - - return true; -} - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfigStreamPage.cpp" enum class ListOpt : int { ShowAll = 1, Custom, }; +struct QCef; +extern QCef *cef; + +#define wiz reinterpret_cast(wizard()) + AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) { ui->setupUi(this); @@ -910,331 +695,3 @@ void AutoConfigStreamPage::UpdateCompleted() } emit completeChanged(); } - -/* ------------------------------------------------------------------------- */ - -AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) -{ - EnableThreadedMessageBoxes(true); - - calldata_t cd = {0}; - calldata_set_int(&cd, "seconds", 5); - - proc_handler_t *ph = obs_get_proc_handler(); - proc_handler_call(ph, "twitch_ingests_refresh", &cd); - proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd); - calldata_free(&cd); - - OBSBasic *main = reinterpret_cast(parent); - main->EnableOutputs(false); - - installEventFilter(CreateShortcutFilter()); - - std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); -#if defined(_WIN32) || defined(__APPLE__) - setWizardStyle(QWizard::ModernStyle); -#endif - streamPage = new AutoConfigStreamPage(); - - setPage(StartPage, new AutoConfigStartPage()); - setPage(VideoPage, new AutoConfigVideoPage()); - setPage(StreamPage, streamPage); - setPage(TestPage, new AutoConfigTestPage()); - setWindowTitle(QTStr("Basic.AutoConfig")); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - obs_video_info ovi; - obs_get_video_info(&ovi); - - baseResolutionCX = ovi.base_width; - baseResolutionCY = ovi.base_height; - - /* ----------------------------------------- */ - /* check to see if Twitch's "auto" available */ - - OBSDataAutoRelease twitchSettings = obs_data_create(); - - obs_data_set_string(twitchSettings, "service", "Twitch"); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, twitchSettings); - - obs_property_t *p = obs_properties_get(props, "server"); - const char *first = obs_property_list_item_string(p, 0); - twitchAuto = strcmp(first, "auto") == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* check to see if Amazon IVS "auto" entries are available */ - - OBSDataAutoRelease amazonIVSSettings = obs_data_create(); - - obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS"); - - props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, amazonIVSSettings); - - p = obs_properties_get(props, "server"); - first = obs_property_list_item_string(p, 0); - amazonIVSAuto = strncmp(first, "auto", 4) == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* load service/servers */ - - customServer = serviceType == "rtmp_custom"; - - QComboBox *serviceList = streamPage->ui->service; - - if (!serviceName.empty()) { - serviceList->blockSignals(true); - - int count = serviceList->count(); - bool found = false; - - for (int i = 0; i < count; i++) { - QString name = serviceList->itemText(i); - - if (name == serviceName.c_str()) { - serviceList->setCurrentIndex(i); - found = true; - break; - } - } - - if (!found) { - serviceList->insertItem(0, serviceName.c_str()); - serviceList->setCurrentIndex(0); - } - - serviceList->blockSignals(false); - } - - streamPage->UpdateServerList(); - streamPage->UpdateKeyLink(); - streamPage->UpdateMoreInfoLink(); - streamPage->lastService.clear(); - - if (!customServer) { - QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); - if (idx == -1) - idx = 0; - - serverList->setCurrentIndex(idx); - } else { - streamPage->ui->customServer->setText(server.c_str()); - int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom)); - streamPage->ui->service->setCurrentIndex(idx); - } - - if (!key.empty()) - streamPage->ui->key->setText(key.c_str()); - - TestHardwareEncoding(); - - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); - bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo") - ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo") - : true; - streamPage->ui->bitrate->setValue(bitrate); - streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled); - streamPage->ServiceChanged(); - - if (!hardwareEncodingAvailable) { - delete streamPage->ui->preferHardware; - streamPage->ui->preferHardware = nullptr; - } else { - /* Newer generations of NVENC have a high enough quality to - * bitrate ratio that if NVENC is available, it makes sense to - * just always prefer hardware encoding by default */ - bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4; - streamPage->ui->preferHardware->setChecked(preferHardware); - } - - setOptions(QWizard::WizardOptions()); - setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings")); - setButtonText(QWizard::BackButton, QTStr("Back")); - setButtonText(QWizard::NextButton, QTStr("Next")); - setButtonText(QWizard::CancelButton, QTStr("Cancel")); -} - -AutoConfig::~AutoConfig() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - main->EnableOutputs(true); - EnableThreadedMessageBoxes(false); -} - -void AutoConfig::TestHardwareEncoding() -{ - size_t idx = 0; - const char *id; - while (obs_enum_encoder_types(idx++, &id)) { - if (strcmp(id, "ffmpeg_nvenc") == 0) - hardwareEncodingAvailable = nvencAvailable = true; - else if (strcmp(id, "obs_qsv11") == 0) - hardwareEncodingAvailable = qsvAvailable = true; - else if (strcmp(id, "h264_texture_amf") == 0) - hardwareEncodingAvailable = vceAvailable = true; -#ifdef __APPLE__ - else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0 -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - if (__builtin_available(macOS 13.0, *)) - hardwareEncodingAvailable = appleAvailable = true; -#endif - } -} - -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (service == Service::Twitch) { - if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionUS; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAsia; - } else if (regionOther) { - return true; - } - } else { - return true; - } - - return false; -} - -void AutoConfig::done(int result) -{ - QWizard::done(result); - - if (result == QDialog::Accepted) { - if (type == Type::Streaming) - SaveStreamSettings(); - SaveSettings(); - -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected()) { - OBSBasic *main = OBSBasic::Get(); - main->NewYouTubeAppDock(); - } -#endif - } -} - -inline const char *AutoConfig::GetEncoderId(Encoder enc) -{ - switch (enc) { - case Encoder::NVENC: - return SIMPLE_ENCODER_NVENC; - case Encoder::QSV: - return SIMPLE_ENCODER_QSV; - case Encoder::AMD: - return SIMPLE_ENCODER_AMD; - case Encoder::Apple: - return SIMPLE_ENCODER_APPLE_H264; - default: - return SIMPLE_ENCODER_X264; - } -}; - -void AutoConfig::SaveStreamSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - /* ---------------------------------- */ - /* save service */ - - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); - - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!streamPage->auth || !IsYouTubeService(serviceName)) - obs_data_set_string(settings, "key", key.c_str()); -#else - obs_data_set_string(settings, "key", key.c_str()); -#endif - - OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData); - - if (!newService) - return; - - main->SetService(newService); - main->SaveService(); - main->auth = streamPage->auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } - - /* ---------------------------------- */ - /* save stream settings */ - - config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate); - config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); - config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); - - config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful); - - if (multitrackVideo.targetBitrate.has_value()) - config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate", - *multitrackVideo.targetBitrate); - else - config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate"); - - if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() && - (static_cast(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - } else if (multitrackVideo.bitrate.has_value()) { - config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false); - config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate", - *multitrackVideo.bitrate); - } -} - -void AutoConfig::SaveSettings() -{ - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - - if (recordingEncoder != Encoder::Stream) - config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); - - const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; - - config_set_string(main->Config(), "Output", "Mode", "Simple"); - config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); - config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); - config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); - config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); - config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); - - if (fpsType != FPSType::UseCurrent) { - config_set_uint(main->Config(), "Video", "FPSType", 0); - config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); - } - - main->ResetVideo(); - main->ResetOutputs(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/frontend/wizards/AutoConfigStreamPage.hpp b/frontend/wizards/AutoConfigStreamPage.hpp new file mode 100644 index 000000000..89216c461 --- /dev/null +++ b/frontend/wizards/AutoConfigStreamPage.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +class Auth; +class Ui_AutoConfigStreamPage; + +class AutoConfigStreamPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + enum class Section : int { + Connect, + StreamKey, + }; + + std::shared_ptr auth; + + std::unique_ptr ui; + QString lastService; + bool ready = false; + + void LoadServices(bool showAll); + inline bool IsCustomService() const; + +public: + AutoConfigStreamPage(QWidget *parent = nullptr); + ~AutoConfigStreamPage(); + + virtual bool isComplete() const override; + virtual int nextId() const override; + virtual bool validatePage() override; + + void OnAuthConnected(); + void OnOAuthStreamKeyConnected(); + +public slots: + void on_show_clicked(); + void on_connectAccount_clicked(); + void on_disconnectAccount_clicked(); + void on_useStreamKey_clicked(); + void on_preferHardware_clicked(); + void ServiceChanged(); + void UpdateKeyLink(); + void UpdateMoreInfoLink(); + void UpdateServerList(); + void UpdateCompleted(); + + void reset_service_ui_fields(std::string &service); +}; diff --git a/UI/window-basic-auto-config-test.cpp b/frontend/wizards/AutoConfigTestPage.cpp similarity index 94% rename from UI/window-basic-auto-config-test.cpp rename to frontend/wizards/AutoConfigTestPage.cpp index b53f37aa2..1d5c91850 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/frontend/wizards/AutoConfigTestPage.cpp @@ -1,86 +1,16 @@ -#include +#include "AutoConfigTestPage.hpp" +#include "AutoConfig.hpp" +#include "TestMode.hpp" +#include "ui_AutoConfigTestPage.h" -#include +#include -#include -#include -#include -#include -#include #include #include -#include "window-basic-auto-config.hpp" -#include "window-basic-main.hpp" -#include "obs-app.hpp" +#include -#include "ui_AutoConfigTestPage.h" - -#define wiz reinterpret_cast(wizard()) - -using namespace std; - -/* ------------------------------------------------------------------------- */ - -class TestMode { - obs_video_info ovi; - OBSSource source[6]; - - static void render_rand(void *, uint32_t cx, uint32_t cy) - { - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - gs_eparam_t *randomvals[3] = {gs_effect_get_param_by_name(solid, "randomvals1"), - gs_effect_get_param_by_name(solid, "randomvals2"), - gs_effect_get_param_by_name(solid, "randomvals3")}; - - struct vec4 r; - - for (int i = 0; i < 3; i++) { - vec4_set(&r, rand_float(true) * 100.0f, rand_float(true) * 100.0f, - rand_float(true) * 50000.0f + 10000.0f, 0.0f); - gs_effect_set_vec4(randomvals[i], &r); - } - - while (gs_effect_loop(solid, "Random")) - gs_draw_sprite(nullptr, 0, cx, cy); - } - -public: - inline TestMode() - { - obs_get_video_info(&ovi); - obs_add_main_render_callback(render_rand, this); - - for (uint32_t i = 0; i < 6; i++) { - source[i] = obs_get_output_source(i); - obs_source_release(source[i]); - obs_set_output_source(i, nullptr); - } - } - - inline ~TestMode() - { - for (uint32_t i = 0; i < 6; i++) - obs_set_output_source(i, source[i]); - - obs_remove_main_render_callback(render_rand, this); - obs_reset_video(&ovi); - } - - inline void SetVideo(int cx, int cy, int fps_num, int fps_den) - { - obs_video_info newOVI = ovi; - - newOVI.output_width = (uint32_t)cx; - newOVI.output_height = (uint32_t)cy; - newOVI.fps_num = (uint32_t)fps_num; - newOVI.fps_den = (uint32_t)fps_den; - - obs_reset_video(&newOVI); - } -}; - -/* ------------------------------------------------------------------------- */ +#include "moc_AutoConfigTestPage.cpp" #define TEST_STR(x) "Basic.AutoConfig.TestPage." x #define SUBTITLE_TESTING TEST_STR("SubTitle.Testing") @@ -97,6 +27,10 @@ public: #define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder") #define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder") +#define wiz reinterpret_cast(wizard()) + +using namespace std; + void AutoConfigTestPage::StartBandwidthStage() { ui->progressLabel->setText(QTStr(TEST_BW)); @@ -152,14 +86,7 @@ static inline void string_depad_key(string &key) } } -const char *FindAudioEncoderFromCodec(const char *type); - -static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, - const char *prot_test2 = nullptr) -{ - return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && - (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; -} +extern const char *FindAudioEncoderFromCodec(const char *type); static bool return_first_id(void *data, const char *id) { diff --git a/frontend/wizards/AutoConfigTestPage.hpp b/frontend/wizards/AutoConfigTestPage.hpp new file mode 100644 index 000000000..5d2d414a6 --- /dev/null +++ b/frontend/wizards/AutoConfigTestPage.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include +#include +#include + +class QFormLayout; +class Ui_AutoConfigTestPage; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished, + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/frontend/wizards/AutoConfigVideoPage.cpp b/frontend/wizards/AutoConfigVideoPage.cpp new file mode 100644 index 000000000..69de8e2ec --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.cpp @@ -0,0 +1,130 @@ +#include "AutoConfigVideoPage.hpp" +#include "AutoConfig.hpp" +#include "ui_AutoConfigVideoPage.h" + +#include + +#include + +#include "moc_AutoConfigVideoPage.cpp" + +#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x +#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") +#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") +#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") +#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") +#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") + +#define wiz reinterpret_cast(wizard()) + +AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage) +{ + ui->setupUi(this); + + setTitle(QTStr("Basic.AutoConfig.VideoPage")); + setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den; + + QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g'); + + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS); + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes); + ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent); + ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); + ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); + ui->fps->setCurrentIndex(0); + + QString cxStr = QString::number(ovi.base_width); + QString cyStr = QString::number(ovi.base_height); + + int encRes = int(ovi.base_width << 16) | int(ovi.base_height); + + // Auto config only supports testing down to 240p, don't allow current + // resolution if it's lower than that. + if (ovi.base_height >= 240) + ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes); + + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QSize as = screen->size(); + int as_width = as.width(); + int as_height = as.height(); + + // Calculate physical screen resolution based on the virtual screen resolution + // They might differ if scaling is enabled, e.g. for HiDPI screens + as_width = round(as_width * screen->devicePixelRatio()); + as_height = round(as_height * screen->devicePixelRatio()); + + encRes = as_width << 16 | as_height; + + QString str = + QTStr(RES_USE_DISPLAY) + .arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height)); + + ui->canvasRes->addItem(str, encRes); + } + + auto addRes = [&](int cx, int cy) { + encRes = (cx << 16) | cy; + QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy)); + ui->canvasRes->addItem(str, encRes); + }; + + addRes(1920, 1080); + addRes(1280, 720); + + ui->canvasRes->setCurrentIndex(0); +} + +AutoConfigVideoPage::~AutoConfigVideoPage() {} + +int AutoConfigVideoPage::nextId() const +{ + return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage; +} + +bool AutoConfigVideoPage::validatePage() +{ + int encRes = ui->canvasRes->currentData().toInt(); + wiz->baseResolutionCX = encRes >> 16; + wiz->baseResolutionCY = encRes & 0xFFFF; + wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + switch (wiz->fpsType) { + case AutoConfig::FPSType::PreferHighFPS: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = true; + break; + case AutoConfig::FPSType::PreferHighRes: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::UseCurrent: + wiz->specificFPSNum = ovi.fps_num; + wiz->specificFPSDen = ovi.fps_den; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps30: + wiz->specificFPSNum = 30; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps60: + wiz->specificFPSNum = 60; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + } + + return true; +} diff --git a/frontend/wizards/AutoConfigVideoPage.hpp b/frontend/wizards/AutoConfigVideoPage.hpp new file mode 100644 index 000000000..b6a441d2c --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +class Ui_AutoConfigVideoPage; + +class AutoConfigVideoPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + std::unique_ptr ui; + +public: + AutoConfigVideoPage(QWidget *parent = nullptr); + ~AutoConfigVideoPage(); + + virtual int nextId() const override; + virtual bool validatePage() override; +}; diff --git a/frontend/wizards/TestMode.hpp b/frontend/wizards/TestMode.hpp new file mode 100644 index 000000000..fd9b80646 --- /dev/null +++ b/frontend/wizards/TestMode.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +class TestMode { + obs_video_info ovi; + OBSSource source[6]; + + static void render_rand(void *, uint32_t cx, uint32_t cy) + { + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *randomvals[3] = {gs_effect_get_param_by_name(solid, "randomvals1"), + gs_effect_get_param_by_name(solid, "randomvals2"), + gs_effect_get_param_by_name(solid, "randomvals3")}; + + struct vec4 r; + + for (int i = 0; i < 3; i++) { + vec4_set(&r, rand_float(true) * 100.0f, rand_float(true) * 100.0f, + rand_float(true) * 50000.0f + 10000.0f, 0.0f); + gs_effect_set_vec4(randomvals[i], &r); + } + + while (gs_effect_loop(solid, "Random")) + gs_draw_sprite(nullptr, 0, cx, cy); + } + +public: + inline TestMode() + { + obs_get_video_info(&ovi); + obs_add_main_render_callback(render_rand, this); + + for (uint32_t i = 0; i < 6; i++) { + source[i] = obs_get_output_source(i); + obs_source_release(source[i]); + obs_set_output_source(i, nullptr); + } + } + + inline ~TestMode() + { + for (uint32_t i = 0; i < 6; i++) + obs_set_output_source(i, source[i]); + + obs_remove_main_render_callback(render_rand, this); + obs_reset_video(&ovi); + } + + inline void SetVideo(int cx, int cy, int fps_num, int fps_den) + { + obs_video_info newOVI = ovi; + + newOVI.output_width = (uint32_t)cx; + newOVI.output_height = (uint32_t)cy; + newOVI.fps_num = (uint32_t)fps_num; + newOVI.fps_den = (uint32_t)fps_den; + + obs_reset_video(&newOVI); + } +};