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:
parent
607d37b423
commit
3e0592dc20
@ -16,8 +16,6 @@ template<typename T> static T GetOBSRef(QListWidgetItem *item)
|
|||||||
return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
|
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 streaming_active;
|
||||||
extern volatile bool recording_active;
|
extern volatile bool recording_active;
|
||||||
extern volatile bool recording_paused;
|
extern volatile bool recording_paused;
|
||||||
@ -168,19 +166,17 @@ struct OBSStudioAPI : obs_frontend_callbacks {
|
|||||||
void obs_frontend_get_scene_collections(
|
void obs_frontend_get_scene_collections(
|
||||||
std::vector<std::string> &strings) override
|
std::vector<std::string> &strings) override
|
||||||
{
|
{
|
||||||
auto addCollection = [&](const char *name, const char *) {
|
for (auto &[collectionName, collection] :
|
||||||
strings.emplace_back(name);
|
main->GetSceneCollectionCache()) {
|
||||||
return true;
|
strings.emplace_back(collectionName);
|
||||||
};
|
}
|
||||||
|
|
||||||
EnumSceneCollections(addCollection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char *obs_frontend_get_current_scene_collection(void) override
|
char *obs_frontend_get_current_scene_collection(void) override
|
||||||
{
|
{
|
||||||
const char *cur_name = config_get_string(
|
const OBSSceneCollection ¤tCollection =
|
||||||
App()->GlobalConfig(), "Basic", "SceneCollection");
|
main->GetCurrentSceneCollection();
|
||||||
return bstrdup(cur_name);
|
return bstrdup(currentCollection.name.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void obs_frontend_set_current_scene_collection(
|
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 obs_frontend_add_scene_collection(const char *name) override
|
||||||
{
|
{
|
||||||
bool success = false;
|
bool success = false;
|
||||||
QMetaObject::invokeMethod(main, "AddSceneCollection",
|
QMetaObject::invokeMethod(main, "NewSceneCollection",
|
||||||
WaitConnection(),
|
WaitConnection(),
|
||||||
Q_RETURN_ARG(bool, success),
|
Q_RETURN_ARG(bool, success),
|
||||||
Q_ARG(bool, true),
|
|
||||||
Q_ARG(QString, QT_UTF8(name)));
|
Q_ARG(QString, QT_UTF8(name)));
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
@ -1220,27 +1220,48 @@ static void move_basic_to_profiles(void)
|
|||||||
static void move_basic_to_scene_collections(void)
|
static void move_basic_to_scene_collections(void)
|
||||||
{
|
{
|
||||||
char path[512];
|
char path[512];
|
||||||
char new_path[512];
|
|
||||||
|
|
||||||
if (GetConfigPath(path, 512, "obs-studio/basic") <= 0)
|
if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) {
|
||||||
return;
|
|
||||||
if (!os_file_exists(path))
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (GetConfigPath(new_path, 512, "obs-studio/basic/scenes") <= 0)
|
const std::filesystem::path basicPath = std::filesystem::u8path(path);
|
||||||
return;
|
|
||||||
if (os_file_exists(new_path))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (os_mkdir(new_path) == MKDIR_ERROR)
|
if (!std::filesystem::exists(basicPath)) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
strcat(path, "/scenes.json");
|
const std::filesystem::path sceneCollectionPath =
|
||||||
strcat(new_path, "/");
|
App()->userScenesLocation /
|
||||||
strcat(new_path, Str("Untitled"));
|
std::filesystem::u8path("obs-studio/basic/scenes");
|
||||||
strcat(new_path, ".json");
|
|
||||||
|
|
||||||
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()
|
void OBSApp::AppInit()
|
||||||
@ -2524,36 +2545,6 @@ bool GetClosestUnusedFileName(std::string &path, const char *extension)
|
|||||||
return true;
|
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)
|
bool WindowPositionValid(QRect rect)
|
||||||
{
|
{
|
||||||
for (QScreen *screen : QGuiApplication::screens()) {
|
for (QScreen *screen : QGuiApplication::screens()) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2332,29 +2332,13 @@ void OBSBasic::OBSInit()
|
|||||||
{
|
{
|
||||||
ProfileScope("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())
|
if (!InitBasicConfig())
|
||||||
throw "Failed to load basic.ini";
|
throw "Failed to load basic.ini";
|
||||||
if (!ResetAudio())
|
if (!ResetAudio())
|
||||||
throw "Failed to initialize audio";
|
throw "Failed to initialize audio";
|
||||||
|
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
ret = ResetVideo();
|
ret = ResetVideo();
|
||||||
|
|
||||||
switch (ret) {
|
switch (ret) {
|
||||||
@ -2401,6 +2385,12 @@ void OBSBasic::OBSInit()
|
|||||||
AddExtraModulePaths();
|
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, "---------------------------------");
|
blog(LOG_INFO, "---------------------------------");
|
||||||
obs_load_all_modules2(&mfi);
|
obs_load_all_modules2(&mfi);
|
||||||
blog(LOG_INFO, "---------------------------------");
|
blog(LOG_INFO, "---------------------------------");
|
||||||
@ -2482,7 +2472,19 @@ void OBSBasic::OBSInit()
|
|||||||
{
|
{
|
||||||
ProfileScope("OBSBasic::Load");
|
ProfileScope("OBSBasic::Load");
|
||||||
disableSaving--;
|
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++;
|
disableSaving++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2500,7 +2502,6 @@ void OBSBasic::OBSInit()
|
|||||||
Qt::QueuedConnection,
|
Qt::QueuedConnection,
|
||||||
Q_ARG(bool, true));
|
Q_ARG(bool, true));
|
||||||
|
|
||||||
RefreshSceneCollections();
|
|
||||||
disableSaving--;
|
disableSaving--;
|
||||||
|
|
||||||
auto addDisplay = [this](OBSQTDisplay *window) {
|
auto addDisplay = [this](OBSQTDisplay *window) {
|
||||||
@ -3411,26 +3412,14 @@ void OBSBasic::SaveProjectDeferred()
|
|||||||
|
|
||||||
projectChanged = false;
|
projectChanged = false;
|
||||||
|
|
||||||
const char *sceneCollection = config_get_string(
|
try {
|
||||||
App()->GlobalConfig(), "Basic", "SceneCollectionFile");
|
const OBSSceneCollection ¤tCollection =
|
||||||
|
GetCurrentSceneCollection();
|
||||||
|
|
||||||
char savePath[1024];
|
Save(currentCollection.collectionFile.u8string().c_str());
|
||||||
char fileName[1024];
|
} catch (const std::invalid_argument &error) {
|
||||||
int ret;
|
blog(LOG_ERROR, "%s", error.what());
|
||||||
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OBSSource OBSBasic::GetProgramSource()
|
OBSSource OBSBasic::GetProgramSource()
|
||||||
|
@ -165,6 +165,8 @@ struct OBSPromptRequest {
|
|||||||
using OBSPromptCallback = std::function<bool(const OBSPromptResult &result)>;
|
using OBSPromptCallback = std::function<bool(const OBSPromptResult &result)>;
|
||||||
|
|
||||||
using OBSProfileCache = std::map<std::string, OBSProfile>;
|
using OBSProfileCache = std::map<std::string, OBSProfile>;
|
||||||
|
using OBSSceneCollectionCache = std::map<std::string, OBSSceneCollection>;
|
||||||
|
|
||||||
class ColorSelect : public QWidget {
|
class ColorSelect : public QWidget {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -471,8 +473,6 @@ private:
|
|||||||
void ToggleVolControlLayout();
|
void ToggleVolControlLayout();
|
||||||
void ToggleMixerLayout(bool vertical);
|
void ToggleMixerLayout(bool vertical);
|
||||||
|
|
||||||
void RefreshSceneCollections();
|
|
||||||
void ChangeSceneCollection();
|
|
||||||
void LogScenes();
|
void LogScenes();
|
||||||
void SaveProjectNow();
|
void SaveProjectNow();
|
||||||
|
|
||||||
@ -760,8 +760,6 @@ public slots:
|
|||||||
bool manual = false);
|
bool manual = false);
|
||||||
void SetCurrentScene(OBSSource scene, bool force = 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 UpdatePatronJson(const QString &text, const QString &error);
|
||||||
|
|
||||||
void ShowContextBar();
|
void ShowContextBar();
|
||||||
@ -1163,14 +1161,6 @@ private slots:
|
|||||||
void ProgramViewContextMenuRequested();
|
void ProgramViewContextMenuRequested();
|
||||||
void on_previewDisabledWidget_customContextMenuRequested();
|
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_actionShowSettingsFolder_triggered();
|
||||||
void on_actionShowProfileFolder_triggered();
|
void on_actionShowProfileFolder_triggered();
|
||||||
|
|
||||||
@ -1398,6 +1388,54 @@ public slots:
|
|||||||
bool CreateNewProfile(const QString &name);
|
bool CreateNewProfile(const QString &name);
|
||||||
bool CreateDuplicateProfile(const QString &name);
|
bool CreateDuplicateProfile(const QString &name);
|
||||||
void DeleteProfile(const QString &profileName);
|
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;
|
extern bool cef_js_avail;
|
||||||
|
@ -536,8 +536,11 @@ void OBSImporter::browseImport()
|
|||||||
|
|
||||||
bool GetUnusedName(std::string &name)
|
bool GetUnusedName(std::string &name)
|
||||||
{
|
{
|
||||||
if (!SceneCollectionExists(name.c_str()))
|
OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
|
||||||
|
|
||||||
|
if (!basic->GetSceneCollectionByName(name)) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::string newName;
|
std::string newName;
|
||||||
int inc = 2;
|
int inc = 2;
|
||||||
@ -545,18 +548,21 @@ bool GetUnusedName(std::string &name)
|
|||||||
newName = name;
|
newName = name;
|
||||||
newName += " ";
|
newName += " ";
|
||||||
newName += std::to_string(inc++);
|
newName += std::to_string(inc++);
|
||||||
} while (SceneCollectionExists(newName.c_str()));
|
} while (basic->GetSceneCollectionByName(newName));
|
||||||
|
|
||||||
name = newName;
|
name = newName;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/";
|
||||||
|
|
||||||
void OBSImporter::importCollections()
|
void OBSImporter::importCollections()
|
||||||
{
|
{
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
|
|
||||||
char dst[512];
|
const std::filesystem::path sceneCollectionLocation =
|
||||||
GetConfigPath(dst, 512, "obs-studio/basic/scenes/");
|
App()->userScenesLocation /
|
||||||
|
std::filesystem::u8path(OBSSceneCollectionPath);
|
||||||
|
|
||||||
for (int i = 0; i < optionsModel->rowCount() - 1; i++) {
|
for (int i = 0; i < optionsModel->rowCount() - 1; i++) {
|
||||||
int selected = optionsModel->index(i, ImporterColumn::Selected)
|
int selected = optionsModel->index(i, ImporterColumn::Selected)
|
||||||
@ -591,22 +597,35 @@ void OBSImporter::importCollections()
|
|||||||
out = newOut;
|
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;
|
std::string collectionFile;
|
||||||
save += "/";
|
collectionFile.reserve(
|
||||||
save += file;
|
sceneCollectionLocation.u8string().size() +
|
||||||
save += ".json";
|
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();
|
std::string out_str = json11::Json(out).dump();
|
||||||
|
|
||||||
bool success = os_quick_write_utf8_file(save.c_str(),
|
bool success = os_quick_write_utf8_file(
|
||||||
out_str.c_str(),
|
collectionFile.c_str(), out_str.c_str(),
|
||||||
out_str.size(),
|
out_str.size(), false);
|
||||||
false);
|
|
||||||
|
|
||||||
blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s",
|
blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s",
|
||||||
name.c_str(), file.c_str(),
|
name.c_str(), fileName.c_str(),
|
||||||
success ? "SUCCESS" : "FAILURE");
|
success ? "SUCCESS" : "FAILURE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user