diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index 742e1051553713..ee6dae7da87d47 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -16,8 +16,6 @@ template static T GetOBSRef(QListWidgetItem *item) return item->data(static_cast(QtDataRole::OBSRef)).value(); } -void EnumSceneCollections(function &&cb); - extern volatile bool streaming_active; extern volatile bool recording_active; extern volatile bool recording_paused; @@ -168,19 +166,17 @@ struct OBSStudioAPI : obs_frontend_callbacks { void obs_frontend_get_scene_collections( std::vector &strings) override { - auto addCollection = [&](const char *name, const char *) { - strings.emplace_back(name); - return true; - }; - - EnumSceneCollections(addCollection); + for (auto &[collectionName, collection] : + main->GetSceneCollectionCache()) { + strings.emplace_back(collectionName); + } } char *obs_frontend_get_current_scene_collection(void) override { - const char *cur_name = config_get_string( - App()->GlobalConfig(), "Basic", "SceneCollection"); - return bstrdup(cur_name); + const OBSSceneCollection ¤tCollection = + main->GetCurrentSceneCollection(); + return bstrdup(currentCollection.name.c_str()); } void obs_frontend_set_current_scene_collection( @@ -206,10 +202,9 @@ struct OBSStudioAPI : obs_frontend_callbacks { bool obs_frontend_add_scene_collection(const char *name) override { bool success = false; - QMetaObject::invokeMethod(main, "AddSceneCollection", + QMetaObject::invokeMethod(main, "NewSceneCollection", WaitConnection(), Q_RETURN_ARG(bool, success), - Q_ARG(bool, true), Q_ARG(QString, QT_UTF8(name))); return success; } diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index e3fc0dc9b92ddd..1ce205c7d7ec52 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -1219,27 +1219,48 @@ static void move_basic_to_profiles(void) static void move_basic_to_scene_collections(void) { char path[512]; - char new_path[512]; - if (GetConfigPath(path, 512, "obs-studio/basic") <= 0) - return; - if (!os_file_exists(path)) + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); - if (GetConfigPath(new_path, 512, "obs-studio/basic/scenes") <= 0) + if (!std::filesystem::exists(basicPath)) { return; - if (os_file_exists(new_path)) + } + + const std::filesystem::path sceneCollectionPath = + App()->userScenesLocation / + std::filesystem::u8path("obs-studio/basic/scenes"); + + if (std::filesystem::exists(sceneCollectionPath)) { return; + } - if (os_mkdir(new_path) == MKDIR_ERROR) + try { + std::filesystem::create_directories(sceneCollectionPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create scene collection directory for migration from basic scene collection\n%s", + error.what()); return; + } - strcat(path, "/scenes.json"); - strcat(new_path, "/"); - strcat(new_path, Str("Untitled")); - strcat(new_path, ".json"); + const std::filesystem::path sourceFile = + basicPath / std::filesystem::u8path("scenes.json"); + const std::filesystem::path destinationFile = + (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))) + .replace_extension(".json"); - os_rename(path, new_path); + try { + std::filesystem::rename(sourceFile, destinationFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to rename basic scene collection file: \n%s", + error.what()); + return; + } } void OBSApp::AppInit() @@ -2523,36 +2544,6 @@ bool GetClosestUnusedFileName(std::string &path, const char *extension) return true; } -bool GetUnusedSceneCollectionFile(std::string &name, std::string &file) -{ - char path[512]; - int ret; - - if (!GetFileSafeName(name.c_str(), file)) { - blog(LOG_WARNING, "Failed to create safe file name for '%s'", - name.c_str()); - return false; - } - - ret = GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get scene collection config path"); - return false; - } - - file.insert(0, path); - - if (!GetClosestUnusedFileName(file, "json")) { - blog(LOG_WARNING, "Failed to get closest file name for %s", - file.c_str()); - return false; - } - - file.erase(file.size() - 5, 5); - file.erase(0, strlen(path)); - return true; -} - bool WindowPositionValid(QRect rect) { for (QScreen *screen : QGuiApplication::screens()) { diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp index a1ce86cbb8ad0d..4de7c29d4ccb4a 100644 --- a/UI/window-basic-main-scene-collections.cpp +++ b/UI/window-basic-main-scene-collections.cpp @@ -15,6 +15,9 @@ along with this program. If not, see . ******************************************************************************/ +#include +#include + #include #include #include @@ -27,209 +30,319 @@ #include "window-importer.hpp" #include "window-namedialog.hpp" -using namespace std; +// MARK: Constant Expressions + +constexpr std::string_view OBSSceneCollectionPath = "/obs-studio/basic/scenes/"; -void EnumSceneCollections(std::function &&cb) +// MARK: - Main Scene Collection Management Functions + +void OBSBasic::SetupNewSceneCollection(const std::string &collectionName) { - char path[512]; - os_glob_t *glob; - - int ret = GetConfigPath(path, sizeof(path), - "obs-studio/basic/scenes/*.json"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get config path for scene " - "collections"); - return; - } + const OBSSceneCollection &newCollection = + CreateSceneCollection(collectionName); - if (os_glob(path, 0, &glob) != 0) { - blog(LOG_WARNING, "Failed to glob scene collections"); - return; + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + + 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); } - for (size_t i = 0; i < glob->gl_pathc; i++) { - const char *filePath = glob->gl_pathv[i].path; + OBSDataAutoRelease collection = obs_data_create_from_json_file( + newCollection.collectionFile.u8string().c_str()); - if (glob->gl_pathv[i].directory) - continue; + obs_data_set_string(collection, "name", newCollection.name.c_str()); - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(filePath, "bak"); - std::string name = obs_data_get_string(data, "name"); + OBSDataArrayAutoRelease sources = + obs_data_get_array(collection, "sources"); - /* if no name found, use the file name as the name - * (this only happens when switching to the new version) */ - if (name.empty()) { - name = strrchr(filePath, '/') + 1; - name.resize(name.size() - 5); - } + if (sources) { + obs_data_erase(collection, "sources"); + + obs_data_array_enum( + sources, + [](obs_data_t *data, void *) -> void { + const char *uuid = os_generate_uuid(); - if (!cb(name.c_str(), filePath)) - break; + obs_data_set_string(data, "uuid", uuid); + + bfree((void *)uuid); + }, + nullptr); + + obs_data_set_array(collection, "sources", sources); } - os_globfree(glob); + obs_data_save_json_safe(collection, + newCollection.collectionFile.u8string().c_str(), + "tmp", nullptr); + + ActivateSceneCollection(newCollection); + + blog(LOG_INFO, "Created scene collection '%s' (duplicate, %s)", + newCollection.name.c_str(), newCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); } -bool SceneCollectionExists(const char *findName) +void OBSBasic::SetupRenameSceneCollection(const std::string &collectionName) { - bool found = false; - auto func = [&](const char *name, const char *) { - if (strcmp(name, findName) == 0) { - found = true; - return false; - } + const OBSSceneCollection &newCollection = + CreateSceneCollection(collectionName); + const OBSSceneCollection currentCollection = + GetCurrentSceneCollection(); - return true; - }; + 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()); - EnumSceneCollections(func); - return found; + obs_data_save_json_safe(collection, + newCollection.collectionFile.u8string().c_str(), + "tmp", nullptr); + + 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); } -static bool GetSceneCollectionName(QWidget *parent, std::string &name, - std::string &file, - const char *oldName = nullptr) +// MARK: - Scene Collection File Management Functions + +const OBSSceneCollection & +OBSBasic::CreateSceneCollection(const std::string &collectionName) { - bool rename = oldName != nullptr; - const char *title; - const char *text; + if (const auto &foundCollection = + GetSceneCollectionByName(collectionName)) { + throw std::invalid_argument( + "Scene collection already exists: " + collectionName); + } - if (rename) { - title = Str("Basic.Main.RenameSceneCollection.Title"); - text = Str("Basic.Main.AddSceneCollection.Text"); - } else { - title = Str("Basic.Main.AddSceneCollection.Title"); - text = Str("Basic.Main.AddSceneCollection.Text"); + std::string fileName; + if (!GetFileSafeName(collectionName.c_str(), fileName)) { + throw std::invalid_argument( + "Failed to create safe directory for new scene collection: " + + collectionName); } - for (;;) { - bool success = NameDialog::AskForName(parent, title, text, name, - QT_UTF8(oldName)); - if (!success) { - return false; - } - if (name.empty()) { - OBSMessageBox::warning(parent, - QTStr("NoNameEntered.Title"), - QTStr("NoNameEntered.Text")); - continue; - } - if (SceneCollectionExists(name.c_str())) { - OBSMessageBox::warning(parent, - QTStr("NameExists.Title"), - QTStr("NameExists.Text")); - continue; - } - break; + 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); } - if (!GetUnusedSceneCollectionFile(name, file)) { - return false; + 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) +{ + std::filesystem::path collectionBackupFile{collection.collectionFile}; + collectionBackupFile.replace_extension("json.bak"); + + try { + std::filesystem::remove(collection.collectionFile); + std::filesystem::remove(collectionBackupFile); + } 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); } - return true; + 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::AddSceneCollection(bool create_new, const QString &qname) +bool OBSBasic::CreateDuplicateSceneCollection(const QString &name) { - std::string name; - std::string file; + 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; + } +} - if (qname.isEmpty()) { - if (!GetSceneCollectionName(this, name, file)) - return false; - } else { - name = QT_TO_UTF8(qname); - if (SceneCollectionExists(name.c_str())) - return false; +void OBSBasic::DeleteSceneCollection(const QString &name) +{ + const std::string_view currentCollectionName{config_get_string( + App()->GetUserConfig(), "Basic", "SceneCollection")}; - if (!GetUnusedSceneCollectionFile(name, file)) { - return false; - } + if (currentCollectionName == name.toStdString()) { + on_actionRemoveSceneCollection_triggered(); + return; } - auto new_collection = [this, create_new](const std::string &file, - const std::string &name) { - SaveProjectNow(); + OBSSceneCollection currentCollection = GetCurrentSceneCollection(); - config_set_string(App()->GlobalConfig(), "Basic", - "SceneCollection", name.c_str()); - config_set_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile", file.c_str()); + RemoveSceneCollection(currentCollection); - if (create_new) { - CreateDefaultScene(false); - } else { - obs_reset_source_uuids(); - } + collections.erase(name.toStdString()); - SaveProjectNow(); - RefreshSceneCollections(); - }; + RefreshSceneCollections(); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); +} - new_collection(file, name); +void OBSBasic::ChangeSceneCollection() +{ + QAction *action = reinterpret_cast(sender()); - blog(LOG_INFO, "Added scene collection '%s' (%s, %s.json)", - name.c_str(), create_new ? "clean" : "duplicate", file.c_str()); - blog(LOG_INFO, "------------------------------------------------"); + if (!action) { + return; + } - UpdateTitleBar(); + const std::string_view currentCollectionName{config_get_string( + App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::string selectedCollectionName{action->text().toStdString()}; - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + 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); - return true; + blog(LOG_INFO, "Switched to scene collection '%s' (%s)", + selectedCollection.name.c_str(), + selectedCollection.fileName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); } -void OBSBasic::RefreshSceneCollections() +void OBSBasic::RefreshSceneCollections(bool refreshCache) { + std::string_view currentCollectionName{config_get_string( + App()->GetUserConfig(), "Basic", "SceneCollection")}; + QList menuActions = ui->sceneCollectionMenu->actions(); - int count = 0; - for (int i = 0; i < menuActions.count(); i++) { - QVariant v = menuActions[i]->property("file_name"); - if (v.typeName() != nullptr) - delete menuActions[i]; + for (auto &action : menuActions) { + QVariant variant = action->property("file_name"); + if (variant.typeName() != nullptr) { + delete action; + } } - const char *cur_name = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); - - auto addCollection = [&](const char *name, const char *path) { - std::string file = strrchr(path, '/') + 1; - file.erase(file.size() - 5, 5); + if (refreshCache) { + RefreshSceneCollectionCache(); + } - QAction *action = new QAction(QT_UTF8(name), this); - action->setProperty("file_name", QT_UTF8(path)); + size_t numAddedCollections = 0; + for (auto &[collectionName, collection] : collections) { + QAction *action = new QAction( + QString().fromStdString(collectionName), this); + action->setProperty("file_name", QString().fromStdString( + collection.fileName)); connect(action, &QAction::triggered, this, &OBSBasic::ChangeSceneCollection); action->setCheckable(true); - - action->setChecked(strcmp(name, cur_name) == 0); + action->setChecked(collectionName == currentCollectionName); ui->sceneCollectionMenu->addAction(action); - count++; - return true; - }; - EnumSceneCollections(addCollection); - - /* force saving of first scene collection on first run, otherwise - * no scene collections will show up */ - if (!count) { - long prevDisableVal = disableSaving; - - disableSaving = 0; - SaveProjectNow(); - disableSaving = prevDisableVal; - - EnumSceneCollections(addCollection); + numAddedCollections += 1; } - ui->actionRemoveSceneCollection->setEnabled(count > 1); + ui->actionRemoveSceneCollection->setEnabled(numAddedCollections > 1); OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); @@ -238,125 +351,243 @@ void OBSBasic::RefreshSceneCollections() main->ui->actionPasteDup->setEnabled(false); } -void OBSBasic::on_actionNewSceneCollection_triggered() +// 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 { - AddSceneCollection(true); + 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); + } } -void OBSBasic::on_actionDupSceneCollection_triggered() +std::optional +OBSBasic::GetSceneCollectionByName(const std::string &collectionName) const { - AddSceneCollection(false); + auto foundCollection = collections.find(collectionName); + + if (foundCollection == collections.end()) { + return {}; + } else { + return foundCollection->second; + } } -void OBSBasic::on_actionRenameSceneCollection_triggered() +std::optional +OBSBasic::GetSceneCollectionByFileName(const std::string &fileName) const { - std::string name; - std::string file; - std::string oname; - - std::string oldFile = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile"); - const char *oldName = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); - oname = std::string(oldName); - - bool success = GetSceneCollectionName(this, name, file, oldName); - if (!success) - return; + for (auto &[iterator, collection] : collections) { + if (collection.fileName == fileName) { + return collection; + } + } - config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection", - name.c_str()); - config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile", - file.c_str()); - SaveProjectNow(); + return {}; +} + +// MARK: - Qt Slot Functions - char path[512]; - int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get scene collection config path"); +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; } - oldFile.insert(0, path); - oldFile += ".json"; - os_unlink(oldFile.c_str()); - oldFile += ".bak"; - os_unlink(oldFile.c_str()); + 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; + } - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Renamed scene collection to '%s' (%s.json)", - name.c_str(), file.c_str()); - blog(LOG_INFO, "------------------------------------------------"); + return true; + }; - UpdateTitleBar(); - RefreshSceneCollections(); + const OBSPromptRequest request{ + Str("Basic.Main.AddSceneCollection.Title"), + Str("Basic.Main.AddSceneCollection.Text")}; - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_RENAMED); + 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_actionRemoveSceneCollection_triggered() +void OBSBasic::on_actionRenameSceneCollection_triggered() { - std::string newName; - std::string newPath; - - std::string oldFile = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile"); - std::string oldName = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); - - auto cb = [&](const char *name, const char *filePath) { - if (strcmp(oldName.c_str(), name) != 0) { - newName = name; - newPath = filePath; - return false; - } + const OBSSceneCollection ¤tCollection = + GetCurrentSceneCollection(); - return true; - }; + const OBSPromptCallback sceneCollectionCallback = + [this](const OBSPromptResult &result) { + if (GetSceneCollectionByName(result.promptValue)) { + return false; + } - EnumSceneCollections(cb); + return true; + }; - /* this should never be true due to menu item being grayed out */ - if (newPath.empty()) - return; + const OBSPromptRequest request{ + Str("Basic.Main.RenameSceneCollection.Title"), + Str("Basic.Main.AddSceneCollection.Text"), + currentCollection.name}; - QString text = - QTStr("ConfirmRemove.Text").arg(QT_UTF8(oldName.c_str())); + OBSPromptResult result = + PromptForName(request, sceneCollectionCallback); - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmRemove.Title"), text); - if (button == QMessageBox::No) + 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()); + } +} - char path[512]; - int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get scene collection config path"); +void OBSBasic::on_actionRemoveSceneCollection_triggered(bool skipConfirmation) +{ + if (collections.size() < 2) { return; } - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + OBSSceneCollection currentCollection; - oldFile.insert(0, path); - /* os_rename() overwrites if necessary, only the .bak file will remain. */ - os_rename((oldFile + ".json").c_str(), (oldFile + ".json.bak").c_str()); + try { + currentCollection = GetCurrentSceneCollection(); - Load(newPath.c_str()); - RefreshSceneCollections(); + if (!skipConfirmation) { + const QString confirmationText = + QTStr("ConfirmRemove.Text") + .arg(QString::fromStdString( + currentCollection.name)); + const QMessageBox::StandardButton button = + OBSMessageBox::question( + this, QTStr("ConfirmRemove.Title"), + confirmationText); - const char *newFile = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile"); + if (button == QMessageBox::No) { + return; + } + } - blog(LOG_INFO, - "Removed scene collection '%s' (%s.json), " - "switched to '%s' (%s.json)", - oldName.c_str(), oldFile.c_str(), newName.c_str(), newFile); - blog(LOG_INFO, "------------------------------------------------"); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); - UpdateTitleBar(); + 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()); + } - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + const OBSSceneCollection &newCollection = collections.rbegin()->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() @@ -364,37 +595,32 @@ void OBSBasic::on_actionImportSceneCollection_triggered() OBSImporter imp(this); imp.exec(); - RefreshSceneCollections(); + RefreshSceneCollections(true); } void OBSBasic::on_actionExportSceneCollection_triggered() { SaveProjectNow(); - char path[512]; - - QString home = QDir::homePath(); + const OBSSceneCollection ¤tCollection = + GetCurrentSceneCollection(); - QString currentFile = QT_UTF8(config_get_string( - App()->GlobalConfig(), "Basic", "SceneCollectionFile")); + const QString home = QDir::homePath(); - int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get scene collection config path"); - return; - } - - QString exportFile = + const QString destinationFileName = SaveFile(this, QTStr("Basic.MainMenu.SceneCollection.Export"), - home + "/" + currentFile, "JSON Files (*.json)"); - - string file = QT_TO_UTF8(exportFile); + home + "/" + currentCollection.fileName.c_str(), + "JSON Files (*.json)"); - if (!exportFile.isEmpty() && !exportFile.isNull()) { - QString inputFile = path + currentFile + ".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(QT_TO_UTF8(inputFile)); + OBSDataAutoRelease collection = obs_data_create_from_json_file( + sourceFile.u8string().c_str()); OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources"); @@ -403,15 +629,17 @@ void OBSBasic::on_actionExportSceneCollection_triggered() "No sources in exported scene collection"); return; } + obs_data_erase(collection, "sources"); - // We're just using std::sort on a vector to make life easier. - vector sourceItems; + using OBSDataVector = std::vector; + + OBSDataVector sourceItems; obs_data_array_enum( sources, - [](obs_data_t *data, void *pVec) -> void { - auto &sourceItems = - *static_cast *>(pVec); + [](obs_data_t *data, void *vector) -> void { + OBSDataVector &sourceItems{ + *static_cast(vector)}; sourceItems.push_back(data); }, &sourceItems); @@ -425,12 +653,14 @@ void OBSBasic::on_actionExportSceneCollection_triggered() }); OBSDataArrayAutoRelease newSources = obs_data_array_create(); - for (auto &item : sourceItems) + 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, QT_TO_UTF8(exportFile), "tmp", "bak"); + collection, destinationFile.u8string().c_str(), "tmp", + "bak"); } } @@ -467,8 +697,10 @@ void OBSBasic::on_actionRemigrateSceneCollection_triggered() return; } - QString name = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); + const OBSSceneCollection ¤tCollection = + GetCurrentSceneCollection(); + + QString name = QString::fromStdString(currentCollection.name); QString message = QTStr("Basic.Main.RemigrateSceneCollection.Text") .arg(name) .arg(ovi.base_width) @@ -497,71 +729,43 @@ void OBSBasic::on_actionRemigrateSceneCollection_triggered() } } - char path[512]; - int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get scene collection config path"); - return; - } - - std::string fileName = path; - fileName += config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile"); - fileName += ".json"; - - if (api) - api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + 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) + if (!usingAbsoluteCoordinates) { ResetVideo(); + } - Load(fileName.c_str(), !usingAbsoluteCoordinates); - RefreshSceneCollections(); - - if (api) - api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + ActivateSceneCollection(currentCollection); } -void OBSBasic::ChangeSceneCollection() -{ - QAction *action = reinterpret_cast(sender()); - std::string fileName; - - if (!action) - return; - - fileName = QT_TO_UTF8(action->property("file_name").value()); - if (fileName.empty()) - return; +// MARK: - Scene Collection Management Helper Functions - const char *oldName = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); +void OBSBasic::ActivateSceneCollection(const OBSSceneCollection &collection) +{ + const std::string currentCollectionName{config_get_string( + App()->GetUserConfig(), "Basic", "SceneCollection")}; - if (action->text().compare(QT_UTF8(oldName)) == 0) { - action->setChecked(true); - return; + if (auto foundCollection = + GetSceneCollectionByName(currentCollectionName)) { + if (collection.name != foundCollection.value().name) { + SaveProjectNow(); + } } - OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", + collection.name.c_str()); + config_set_string(App()->GetUserConfig(), "Basic", + "SceneCollectionFile", collection.fileName.c_str()); - SaveProjectNow(); + Load(collection.collectionFile.u8string().c_str()); - Load(fileName.c_str()); RefreshSceneCollections(); - const char *newName = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollection"); - const char *newFile = config_get_string(App()->GlobalConfig(), "Basic", - "SceneCollectionFile"); - - blog(LOG_INFO, "Switched to scene collection '%s' (%s.json)", newName, - newFile); - blog(LOG_INFO, "------------------------------------------------"); - 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 index c29d7a9d1c2ad8..8c6e646fa2e726 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2332,29 +2332,13 @@ void OBSBasic::OBSInit() { ProfileScope("OBSBasic::OBSInit"); - const char *sceneCollection = config_get_string( - App()->GlobalConfig(), "Basic", "SceneCollectionFile"); - char savePath[1024]; - char fileName[1024]; - int ret; - - if (!sceneCollection) - throw "Failed to get scene collection name"; - - ret = snprintf(fileName, sizeof(fileName), - "obs-studio/basic/scenes/%s.json", sceneCollection); - if (ret <= 0) - throw "Failed to create scene collection file name"; - - ret = GetConfigPath(savePath, sizeof(savePath), fileName); - if (ret <= 0) - throw "Failed to get scene collection json file path"; - if (!InitBasicConfig()) throw "Failed to load basic.ini"; if (!ResetAudio()) throw "Failed to initialize audio"; + int ret = 0; + ret = ResetVideo(); switch (ret) { @@ -2401,6 +2385,12 @@ void OBSBasic::OBSInit() 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, "---------------------------------"); @@ -2482,7 +2472,19 @@ void OBSBasic::OBSInit() { ProfileScope("OBSBasic::Load"); disableSaving--; - Load(savePath); + + try { + const OBSSceneCollection ¤tCollection = + GetCurrentSceneCollection(); + ActivateSceneCollection(currentCollection); + } catch (const std::invalid_argument &) { + const std::string collectionName = + config_get_string(App()->GetUserConfig(), + "Basic", "SceneCollection"); + + SetupNewSceneCollection(collectionName); + } + disableSaving++; } @@ -2500,7 +2502,6 @@ void OBSBasic::OBSInit() Qt::QueuedConnection, Q_ARG(bool, true)); - RefreshSceneCollections(); disableSaving--; auto addDisplay = [this](OBSQTDisplay *window) { @@ -3411,26 +3412,14 @@ void OBSBasic::SaveProjectDeferred() projectChanged = false; - const char *sceneCollection = config_get_string( - App()->GlobalConfig(), "Basic", "SceneCollectionFile"); - - char savePath[1024]; - char fileName[1024]; - int ret; - - if (!sceneCollection) - return; - - ret = snprintf(fileName, sizeof(fileName), - "obs-studio/basic/scenes/%s.json", sceneCollection); - if (ret <= 0) - return; - - ret = GetConfigPath(savePath, sizeof(savePath), fileName); - if (ret <= 0) - return; + try { + const OBSSceneCollection ¤tCollection = + GetCurrentSceneCollection(); - Save(savePath); + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } } OBSSource OBSBasic::GetProgramSource() diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 4bda02c2008956..2131e16de93e91 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -165,6 +165,8 @@ struct OBSPromptRequest { using OBSPromptCallback = std::function; using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + class ColorSelect : public QWidget { public: @@ -471,8 +473,6 @@ class OBSBasic : public OBSMainWindow { void ToggleVolControlLayout(); void ToggleMixerLayout(bool vertical); - void RefreshSceneCollections(); - void ChangeSceneCollection(); void LogScenes(); void SaveProjectNow(); @@ -760,8 +760,6 @@ public slots: bool manual = false); void SetCurrentScene(OBSSource scene, bool force = false); - bool AddSceneCollection(bool create_new, - const QString &name = QString()); void UpdatePatronJson(const QString &text, const QString &error); void ShowContextBar(); @@ -1163,14 +1161,6 @@ private slots: void ProgramViewContextMenuRequested(); void on_previewDisabledWidget_customContextMenuRequested(); - void on_actionNewSceneCollection_triggered(); - void on_actionDupSceneCollection_triggered(); - void on_actionRenameSceneCollection_triggered(); - void on_actionRemoveSceneCollection_triggered(); - void on_actionImportSceneCollection_triggered(); - void on_actionExportSceneCollection_triggered(); - void on_actionRemigrateSceneCollection_triggered(); - void on_actionShowSettingsFolder_triggered(); void on_actionShowProfileFolder_triggered(); @@ -1398,6 +1388,54 @@ public slots: bool CreateNewProfile(const QString &name); bool CreateDuplicateProfile(const QString &name); void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection & + CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache & + GetSceneCollectionCache() const noexcept + { + return collections; + }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional + GetSceneCollectionByName(const std::string &collectionName) const; + std::optional + GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void + on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); }; extern bool cef_js_avail; diff --git a/UI/window-importer.cpp b/UI/window-importer.cpp index 2dc9314f32d79f..e3e50d5598ca5a 100644 --- a/UI/window-importer.cpp +++ b/UI/window-importer.cpp @@ -536,8 +536,11 @@ void OBSImporter::browseImport() bool GetUnusedName(std::string &name) { - if (!SceneCollectionExists(name.c_str())) + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { return false; + } std::string newName; int inc = 2; @@ -545,18 +548,21 @@ bool GetUnusedName(std::string &name) newName = name; newName += " "; newName += std::to_string(inc++); - } while (SceneCollectionExists(newName.c_str())); + } while (basic->GetSceneCollectionByName(newName)); name = newName; return true; } +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + void OBSImporter::importCollections() { setEnabled(false); - char dst[512]; - GetConfigPath(dst, 512, "obs-studio/basic/scenes/"); + 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) @@ -591,22 +597,35 @@ void OBSImporter::importCollections() out = newOut; } - GetUnusedSceneCollectionFile(name, file); + 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 save = dst; - save += "/"; - save += file; - save += ".json"; + 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(save.c_str(), - out_str.c_str(), - out_str.size(), - false); + 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(), file.c_str(), + name.c_str(), fileName.c_str(), success ? "SUCCESS" : "FAILURE"); } }