UI: Rewrite scene collection system to enable user-provided storage

This change enables loading scene collections from locations different
than OBS' own configuration directory.

It also rewrites profile management in the app to work off an in-memory
collection of profiles found on disk and does not require iterating
over directory contents for most profile interactions by the app.
This commit is contained in:
PatTheMav 2024-09-03 16:29:58 +02:00 committed by Ryan Foster
parent 607d37b423
commit 3e0592dc20
6 changed files with 711 additions and 475 deletions

View File

@ -16,8 +16,6 @@ template<typename T> static T GetOBSRef(QListWidgetItem *item)
return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
}
void EnumSceneCollections(function<bool(const char *, const char *)> &&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<std::string> &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 &currentCollection =
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;
}

View File

@ -1220,27 +1220,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;
}
if (GetConfigPath(new_path, 512, "obs-studio/basic/scenes") <= 0)
return;
if (os_file_exists(new_path))
return;
const std::filesystem::path basicPath = std::filesystem::u8path(path);
if (os_mkdir(new_path) == MKDIR_ERROR)
if (!std::filesystem::exists(basicPath)) {
return;
}
strcat(path, "/scenes.json");
strcat(new_path, "/");
strcat(new_path, Str("Untitled"));
strcat(new_path, ".json");
const std::filesystem::path sceneCollectionPath =
App()->userScenesLocation /
std::filesystem::u8path("obs-studio/basic/scenes");
os_rename(path, new_path);
if (std::filesystem::exists(sceneCollectionPath)) {
return;
}
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;
}
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");
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()
@ -2524,36 +2545,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()) {

File diff suppressed because it is too large Load Diff

View File

@ -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 &currentCollection =
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");
try {
const OBSSceneCollection &currentCollection =
GetCurrentSceneCollection();
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;
Save(savePath);
Save(currentCollection.collectionFile.u8string().c_str());
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
}
}
OBSSource OBSBasic::GetProgramSource()

View File

@ -165,6 +165,8 @@ struct OBSPromptRequest {
using OBSPromptCallback = std::function<bool(const OBSPromptResult &result)>;
using OBSProfileCache = std::map<std::string, OBSProfile>;
using OBSSceneCollectionCache = std::map<std::string, OBSSceneCollection>;
class ColorSelect : public QWidget {
public:
@ -471,8 +473,6 @@ private:
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<OBSSceneCollection>
GetSceneCollectionByName(const std::string &collectionName) const;
std::optional<OBSSceneCollection>
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;

View File

@ -536,8 +536,11 @@ void OBSImporter::browseImport()
bool GetUnusedName(std::string &name)
{
if (!SceneCollectionExists(name.c_str()))
OBSBasic *basic = reinterpret_cast<OBSBasic *>(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");
}
}