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 @@
+
+
@@ -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);