diff --git a/obs/CMakeLists.txt b/obs/CMakeLists.txt index 4206582d2..97a99fc3a 100644 --- a/obs/CMakeLists.txt +++ b/obs/CMakeLists.txt @@ -100,6 +100,7 @@ set(obs_SOURCES window-basic-properties.cpp window-basic-main-outputs.cpp window-basic-source-select.cpp + window-basic-main-scene-collections.cpp window-license-agreement.cpp window-basic-status-bar.cpp window-basic-adv-audio.cpp diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini index 4b4a3a6f6..07adf5708 100644 --- a/obs/data/locale/en-US.ini +++ b/obs/data/locale/en-US.ini @@ -42,6 +42,9 @@ Untitled="Untitled" New="New" Duplicate="Duplicate" +# title bar strings +TitleBar.Scenes="Scenes" + # "name already exists" dialog box NameExists.Title="Name already exists" NameExists.Text="The name is already in use." @@ -126,6 +129,13 @@ Basic.Main.AddSceneDlg.Text="Please enter the name of the scene" # add scene suggested name Basic.Main.DefaultSceneName.Text="Scene %1" +# add scene collection dialog +Basic.Main.AddSceneCollection.Title="Add Scene Collection" +Basic.Main.AddSceneCollection.Text="Please enter the name of the scene collection" + +# rename scene collection dialog +Basic.Main.RenameSceneCollection.Title="Rename Scene Collection" + # preview window disabled Basic.Main.PreviewDisabled="Preview is currently disabled" @@ -239,6 +249,9 @@ Basic.MainMenu.Edit.Order.MoveToTop="Move to &Top" Basic.MainMenu.Edit.Order.MoveToBottom="Move to &Bottom" Basic.MainMenu.Edit.AdvAudio="&Advanced Audio Properties" +# basic mode profile/scene collection menus +Basic.MainMenu.SceneCollection="&Scene Collection" + # basic mode help menu Basic.MainMenu.Help="&Help" Basic.MainMenu.Help.Website="Visit &Website" diff --git a/obs/forms/OBSBasic.ui b/obs/forms/OBSBasic.ui index d8232ba02..bd95e46f5 100644 --- a/obs/forms/OBSBasic.ui +++ b/obs/forms/OBSBasic.ui @@ -628,8 +628,19 @@ + + + Basic.MainMenu.SceneCollection + + + + + + + + @@ -932,6 +943,26 @@ Basic.MainMenu.Help.Website + + + New + + + + + Duplicate + + + + + Rename + + + + + Remove + + diff --git a/obs/obs-app.cpp b/obs/obs-app.cpp index 6d9de5d50..a25ad5821 100644 --- a/obs/obs-app.cpp +++ b/obs/obs-app.cpp @@ -278,6 +278,18 @@ static bool MakeUserDirs() return true; } +static bool MakeUserProfileDirs() +{ + char path[512]; + + if (GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + return true; +} + bool OBSApp::InitGlobalConfig() { char path[512]; @@ -406,6 +418,32 @@ OBSApp::OBSApp(int &argc, char **argv) : QApplication(argc, argv) {} +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)) + return; + + if (GetConfigPath(new_path, 512, "obs-studio/basic/scenes") <= 0) + return; + if (os_file_exists(new_path)) + return; + + if (os_mkdir(new_path) == MKDIR_ERROR) + return; + + strcat(path, "/scenes.json"); + strcat(new_path, "/"); + strcat(new_path, Str("Untitled")); + strcat(new_path, ".json"); + + os_rename(path, new_path); +} + void OBSApp::AppInit() { if (!InitApplicationBundle()) @@ -418,6 +456,15 @@ void OBSApp::AppInit() throw "Failed to load locale"; if (!InitTheme()) throw "Failed to load theme"; + config_set_default_string(globalConfig, "Basic", "SceneCollection", + Str("Untitled")); + config_set_default_string(globalConfig, "Basic", "SceneCollectionFile", + Str("Untitled")); + + move_basic_to_scene_collections(); + + if (!MakeUserProfileDirs()) + throw "Failed to create profile directories"; } const char *OBSApp::GetRenderModule() const diff --git a/obs/window-basic-main-scene-collections.cpp b/obs/window-basic-main-scene-collections.cpp new file mode 100644 index 000000000..f69168a13 --- /dev/null +++ b/obs/window-basic-main-scene-collections.cpp @@ -0,0 +1,365 @@ +/****************************************************************************** + Copyright (C) 2015 by Hugh 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 "item-widget-helpers.hpp" +#include "window-basic-main.hpp" +#include "window-namedialog.hpp" +#include "qt-wrappers.hpp" + +template static void EnumSceneCollections(Func &&cb) +{ + 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; + } + + if (os_glob(path, 0, &glob) != 0) { + blog(LOG_WARNING, "Failed to glob scene collections"); + return; + } + + for (size_t i = 0; i < glob->gl_pathc; i++) { + const char *filePath = glob->gl_pathv[i].path; + + if (glob->gl_pathv[i].directory) + continue; + + BPtr fileData = os_quick_read_utf8_file(filePath); + if (!fileData) + continue; + + obs_data_t *data = obs_data_create_from_json(fileData); + std::string name = obs_data_get_string(data, "name"); + + /* 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); + } + + obs_data_release(data); + + if (!cb(name.c_str(), filePath)) + break; + } + + os_globfree(glob); +} + +static bool SceneCollectionExists(const char *findName) +{ + bool found = false; + auto func = [&](const char *name, const char*) + { + if (strcmp(name, findName) == 0) { + found = true; + return false; + } + + return true; + }; + + EnumSceneCollections(func); + return found; +} + +static bool GetSceneCollectionName(QWidget *parent, std::string &name, + std::string &file, const char *oldName = nullptr) +{ + bool rename = oldName != nullptr; + const char *title; + const char *text; + char path[512]; + size_t len; + int ret; + + 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"); + } + + for (;;) { + bool success = NameDialog::AskForName(parent, title, text, + name, QT_UTF8(oldName)); + if (!success) { + return false; + } + if (name.empty()) { + QMessageBox::information(parent, + QTStr("NoNameEntered.Title"), + QTStr("NoNameEntered.Text")); + continue; + } + if (SceneCollectionExists(name.c_str())) { + QMessageBox::information(parent, + QTStr("NameExists.Title"), + QTStr("NameExists.Text")); + continue; + } + break; + } + + 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; + } + + len = file.size(); + 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, file.size() - len); + return true; +} + +void OBSBasic::AddSceneCollection(bool create_new) +{ + std::string name; + std::string file; + + if (!GetSceneCollectionName(this, name, file)) + return; + + SaveProject(); + + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection", + name.c_str()); + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile", + file.c_str()); + if (create_new) { + CreateDefaultScene(); + } + SaveProject(); + RefreshSceneCollections(); + + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Added scene collection '%s' (%s, %s.json)", + name.c_str(), create_new ? "clean" : "duplicate", + file.c_str()); + + UpdateTitleBar(); +} + +void OBSBasic::RefreshSceneCollections() +{ + QList menuActions = ui->sceneCollectionMenu->actions(); + int count = 0; + + for (int i = 0; i < menuActions.count(); i++) { + QVariant v = menuActions[i]->property("fileName"); + if (v.typeName() != nullptr) + delete menuActions[i]; + } + + 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); + + QAction *action = new QAction(QT_UTF8(name), this); + action->setProperty("fileName", QT_UTF8(path)); + connect(action, &QAction::triggered, + this, &OBSBasic::ChangeSceneCollection); + action->setCheckable(true); + + action->setChecked(strcmp(name, cur_name) == 0); + + ui->sceneCollectionMenu->addAction(action); + count++; + return true; + }; + + EnumSceneCollections(addCollection); + + ui->actionRemoveSceneCollection->setEnabled(count > 1); +} + +void OBSBasic::on_actionNewSceneCollection_triggered() +{ + AddSceneCollection(true); +} + +void OBSBasic::on_actionDupSceneCollection_triggered() +{ + AddSceneCollection(false); +} + +void OBSBasic::on_actionRenameSceneCollection_triggered() +{ + std::string name; + std::string file; + + std::string oldFile = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollectionFile"); + const char *oldName = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollection"); + + bool success = GetSceneCollectionName(this, name, file, oldName); + if (!success) + return; + + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection", + name.c_str()); + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile", + file.c_str()); + SaveProject(); + + 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; + } + + oldFile.insert(0, path); + oldFile += ".json"; + os_unlink(oldFile.c_str()); + + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Renamed scene collection to '%s' (%s.json)", + name.c_str(), file.c_str()); + + UpdateTitleBar(); + RefreshSceneCollections(); +} + +void OBSBasic::on_actionRemoveSceneCollection_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; + } + + return true; + }; + + EnumSceneCollections(cb); + + /* this should never be true due to menu item being grayed out */ + if (newPath.empty()) + return; + + QString text = QTStr("ConfirmRemove.Text"); + text.replace("$1", QT_UTF8(oldName.c_str())); + + QMessageBox::StandardButton button = QMessageBox::question(this, + QTStr("ConfirmRemove.Title"), text); + if (button == QMessageBox::No) + return; + + 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; + } + + oldFile.insert(0, path); + oldFile += ".json"; + os_unlink(oldFile.c_str()); + + Load(newPath.c_str()); + RefreshSceneCollections(); + + const char *newFile = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollectionFile"); + + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Removed scene collection '%s' (%s.json), " + "switched to '%s' (%s.json)", + oldName.c_str(), oldFile.c_str(), + newName.c_str(), newFile); + + UpdateTitleBar(); +} + +void OBSBasic::ChangeSceneCollection() +{ + QAction *action = reinterpret_cast(sender()); + std::string fileName; + + if (!action) + return; + + fileName = QT_TO_UTF8(action->property("fileName").value()); + if (fileName.empty()) + return; + + const char *oldName = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollection"); + if (action->text().compare(QT_UTF8(oldName)) == 0) { + action->setChecked(true); + return; + } + + SaveProject(); + + 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, "------------------------------------------------"); + blog(LOG_INFO, "Switched to scene collection '%s' (%s.json)", + newName, newFile); + + UpdateTitleBar(); +} diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index a2e5d8e32..60bc8b8bf 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -224,6 +224,9 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder) obs_source_t *currentScene = obs_get_output_source(0); const char *sceneName = obs_source_get_name(currentScene); + const char *sceneCollection = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollection"); + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData); SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData); SaveAudioDevice(AUX_AUDIO_1, 3, saveData); @@ -232,6 +235,7 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder) obs_data_set_string(saveData, "current_scene", sceneName); obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); obs_data_set_array(saveData, "sources", sourcesArray); obs_data_array_release(sourcesArray); obs_source_release(currentScene); @@ -429,8 +433,18 @@ void OBSBasic::Load(const char *file) obs_data_array_t *sources = obs_data_get_array(data, "sources"); const char *sceneName = obs_data_get_string(data, "current_scene"); + + const char *curSceneCollection = config_get_string( + App()->GlobalConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); obs_source_t *curScene; + if (!name || !*name) + name = curSceneCollection; + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); LoadAudioDevice(AUX_AUDIO_1, 3, data); @@ -448,6 +462,15 @@ void OBSBasic::Load(const char *file) obs_data_array_release(sources); obs_data_array_release(sceneOrder); + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollection", + name); + config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile", + file_base.c_str()); + obs_data_release(data); disableSaving--; @@ -739,11 +762,23 @@ void OBSBasic::ResetOutputs() void OBSBasic::OBSInit() { + const char *sceneCollection = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollectionFile"); char savePath[512]; - int ret = GetConfigPath(savePath, sizeof(savePath), - "obs-studio/basic/scenes.json"); + char fileName[512]; + int ret; + + if (!sceneCollection) + throw "Failed to get scene collection name"; + + ret = snprintf(fileName, 512, "obs-studio/basic/scenes/%s.json", + sceneCollection); if (ret <= 0) - throw "Failed to get scenes.json file path"; + 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"; /* make sure it's fully displayed before doing any initialization */ show(); @@ -808,6 +843,7 @@ void OBSBasic::OBSInit() } #endif + RefreshSceneCollections(); disableSaving--; } @@ -1037,9 +1073,21 @@ void OBSBasic::SaveProject() if (disableSaving) return; + const char *sceneCollection = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollectionFile"); char savePath[512]; - int ret = GetConfigPath(savePath, sizeof(savePath), - "obs-studio/basic/scenes.json"); + char fileName[512]; + int ret; + + if (!sceneCollection) + return; + + ret = snprintf(fileName, 512, "obs-studio/basic/scenes/%s.json", + sceneCollection); + if (ret <= 0) + return; + + ret = GetConfigPath(savePath, sizeof(savePath), fileName); if (ret <= 0) return; @@ -3386,7 +3434,11 @@ void OBSBasic::UpdateTitleBar() { stringstream name; + const char *sceneCollection = config_get_string(App()->GlobalConfig(), + "Basic", "SceneCollection"); + name << "OBS " << App()->GetVersionString(); + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; blog(LOG_INFO, "%s", name.str().c_str()); setWindowTitle(QT_UTF8(name.str().c_str())); diff --git a/obs/window-basic-main.hpp b/obs/window-basic-main.hpp index 8ee133bbb..bacc3eab0 100644 --- a/obs/window-basic-main.hpp +++ b/obs/window-basic-main.hpp @@ -170,6 +170,10 @@ private: void GetAudioSourceProperties(); void VolControlContextMenu(); + void AddSceneCollection(bool create_new); + void RefreshSceneCollections(); + void ChangeSceneCollection(); + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys; public slots: @@ -334,6 +338,11 @@ private slots: void on_previewDisabledLabel_customContextMenuRequested( const QPoint &pos); + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(); + void logUploadFinished(const QString &text, const QString &error); void updateFileFinished(const QString &text, const QString &error);