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>();
|
||||
}
|
||||
|
||||
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 ¤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;
|
||||
}
|
||||
|
@ -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
@ -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");
|
||||
try {
|
||||
const OBSSceneCollection ¤tCollection =
|
||||
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()
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user