diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt
index c0af1d0c6..a660f3267 100644
--- a/UI/CMakeLists.txt
+++ b/UI/CMakeLists.txt
@@ -57,6 +57,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
set(CMAKE_AUTOMOC TRUE)
find_package(Qt5Svg ${FIND_MODE})
+find_package(Qt5Xml ${FIND_MODE})
find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat)
@@ -230,6 +231,7 @@ set(obs_SOURCES
window-basic-transform.cpp
window-basic-preview.cpp
window-basic-about.cpp
+ window-importer.cpp
window-namedialog.cpp
window-log-reply.cpp
window-projector.cpp
@@ -284,6 +286,7 @@ set(obs_HEADERS
window-basic-adv-audio.hpp
window-basic-transform.hpp
window-basic-preview.hpp
+ window-importer.hpp
window-namedialog.hpp
window-log-reply.hpp
window-projector.hpp
@@ -324,6 +327,19 @@ set(obs_HEADERS
qt-wrappers.hpp
clickable-label.hpp)
+set(obs_importers_HEADERS
+ importers/importers.hpp)
+
+set(obs_importers_SOURCES
+ importers/importers.cpp
+ importers/classic.cpp
+ importers/sl.cpp
+ importers/studio.cpp
+ importers/xsplit.cpp)
+
+source_group("importers\\Source Files" FILES ${obs_importers_SOURCES})
+source_group("importers\\Header Files" FILES ${obs_importers_HEADERS})
+
set(obs_UI
forms/NameDialog.ui
forms/AutoConfigStartPage.ui
@@ -341,6 +357,7 @@ set(obs_UI
forms/OBSExtraBrowsers.ui
forms/OBSUpdate.ui
forms/OBSRemux.ui
+ forms/OBSImporter.ui
forms/OBSAbout.ui)
set(obs_QRC
@@ -353,6 +370,8 @@ add_executable(obs WIN32
obs.manifest
${obs_SOURCES}
${obs_HEADERS}
+ ${obs_importers_SOURCES}
+ ${obs_importers_HEADERS}
${obs_UI_HEADERS}
${obs_QRC_SOURCES})
@@ -372,6 +391,7 @@ target_link_libraries(obs
libobs
Qt5::Widgets
Qt5::Svg
+ Qt5::Xml
obs-frontend-api
${FFMPEG_LIBRARIES}
${LIBCURL_LIBRARIES}
diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 20088c886..2eef5a36c 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -658,6 +658,8 @@ Basic.Settings.General.Preview="Preview"
Basic.Settings.General.OverflowHidden="Hide overflow"
Basic.Settings.General.OverflowAlwaysVisible="Overflow always visible"
Basic.Settings.General.OverflowSelectionHidden="Show overflow even when source is invisible"
+Basic.Settings.General.Importers="Importers"
+Basic.Settings.General.AutomaticCollectionSearch="Search known locations for scene collections when importing"
Basic.Settings.General.SwitchOnDoubleClick="Transition to scene when double-clicked"
Basic.Settings.General.StudioPortraitLayout="Enable portrait/vertical layout"
Basic.Settings.General.TogglePreviewProgramLabels="Show preview/program labels"
@@ -996,3 +998,19 @@ ResizeOutputSizeOfSource.Text="The base and output resolutions will be resized t
ResizeOutputSizeOfSource.Continue="Do you want to continue?"
PreviewTransition="Preview Transition"
+
+# Import Dialog
+Importer="Scene Collection Importer"
+Importer.SelectCollection="Select a Scene Collection"
+Importer.Collection="Scene Collection"
+Importer.HelpText="Add files to this window to import collections from OBS or other supported programs."
+Importer.Path="Collection Path"
+Importer.Program="Detected Application"
+Importer.AutomaticCollectionPrompt="Automatically Search for Scene Collections"
+Importer.AutomaticCollectionText="OBS can automatically find importable scene collections from supported third-party programs. Would you like OBS to automatically find collections for you?\n\nYou can change this later in Settings > General > Importers."
+
+# Importers
+OBSStudio="OBS Studio"
+OBSClassic="OBS Classic"
+Streamlabs="Streamlabs"
+XSplitBroadcaster="XSplit Broadcaster"
\ No newline at end of file
diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui
index e5cabc044..e50cb5be0 100644
--- a/UI/forms/OBSBasicSettings.ui
+++ b/UI/forms/OBSBasicSettings.ui
@@ -605,6 +605,44 @@
+ -
+
+
+ Basic.Settings.General.Importers
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 2
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 170
+ 5
+
+
+
+
+ -
+
+
+ Basic.Settings.General.AutomaticCollectionSearch
+
+
+
+
+
+
-
diff --git a/UI/forms/OBSImporter.ui b/UI/forms/OBSImporter.ui
new file mode 100644
index 000000000..7e9706876
--- /dev/null
+++ b/UI/forms/OBSImporter.ui
@@ -0,0 +1,61 @@
+
+
+ OBSImporter
+
+
+
+ 0
+ 0
+ 850
+ 400
+
+
+
+ Importer
+
+
+
-
+
+
+ Importer.HelpText
+
+
+
+ -
+
+
+ 6
+
+
-
+
+
+ QDialogButtonBox::Close|QDialogButtonBox::Open|QDialogButtonBox::Ok
+
+
+
+
+
+ -
+
+
+ QAbstractItemView::NoSelection
+
+
+ 23
+
+
+ 23
+
+
+ false
+
+
+ 23
+
+
+
+
+
+
+
+
diff --git a/UI/importers/classic.cpp b/UI/importers/classic.cpp
new file mode 100644
index 000000000..58d133693
--- /dev/null
+++ b/UI/importers/classic.cpp
@@ -0,0 +1,584 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "importers.hpp"
+
+#include
+
+using namespace std;
+using namespace json11;
+
+static bool source_name_exists(const Json::array &sources, const string &name)
+{
+ for (size_t i = 0; i < sources.size(); i++) {
+ Json source = sources[i];
+ if (name == source["name"].string_value())
+ return true;
+ }
+
+ return false;
+}
+
+#define translate_int(in_key, in, out_key, out, off) \
+ out[out_key] = in[in_key].int_value() + off;
+#define translate_string(in_key, in, out_key, out) out[out_key] = in[in_key];
+#define translate_double(in_key, in, out_key, out) \
+ translate_string(in_key, in, out_key, out);
+#define translate_bool(in_key, in, out_key, out) \
+ out[out_key] = in[in_key].int_value() == 1;
+
+static Json::object translate_scene_item(const Json &in, const Json &source)
+{
+ Json::object item = Json::object{};
+
+ translate_string("name", source, "name", item);
+
+ translate_int("crop.top", in, "crop_top", item, 0);
+ translate_int("crop.bottom", in, "crop_bottom", item, 0);
+ translate_int("crop.left", in, "crop_left", item, 0);
+ translate_int("crop.right", in, "crop_right", item, 0);
+
+ Json::object pos = Json::object{};
+ translate_int("x", in, "x", pos, 0);
+ translate_int("y", in, "y", pos, 0);
+
+ Json::object bounds = Json::object{};
+ translate_int("cx", in, "x", bounds, 0);
+ translate_int("cy", in, "y", bounds, 0);
+
+ item["pos"] = pos;
+ item["bounds"] = bounds;
+ item["bounds_type"] = 2;
+ item["visible"] = true;
+
+ return item;
+}
+
+static int red_blue_swap(int color)
+{
+ int r = color / 256 / 256;
+ int b = color % 256;
+
+ return color - (r * 65536) - b + (b * 65536) + r;
+}
+
+static void create_string_obj(const string &data, Json::array &arr);
+
+static Json::object translate_source(const Json &in, const Json &sources)
+{
+ string id = in["class"].string_value();
+ string name = in["name"].string_value();
+
+ Json::array source_arr = sources.array_items();
+
+ if (id == "GlobalSource") {
+ for (size_t i = 0; i < source_arr.size(); i++) {
+ Json source = source_arr[i];
+ if (name == source["name"].string_value()) {
+ Json::object obj = source.object_items();
+ obj["preexist"] = true;
+ return obj;
+ }
+ }
+ }
+
+ Json in_settings = in["data"];
+
+ Json::object settings = Json::object{};
+ Json::object out = Json::object{};
+
+ int i = 0;
+ string new_name = name;
+ while (source_name_exists(source_arr, new_name)) {
+ new_name = name + to_string(++i);
+ }
+ out["name"] = new_name;
+
+ if (id == "TextSource") {
+ out["id"] = "text_gdiplus";
+
+ int color = in_settings["color"].int_value() + 16777216;
+ color = red_blue_swap(color) + 4278190080;
+ settings["color"] = color;
+
+ color = in_settings["backgroundColor"].int_value();
+ color = red_blue_swap(color + 16777216) + 4278190080;
+ settings["bk_color"] = color;
+
+ color = in_settings["outlineColor"].int_value();
+ color = red_blue_swap(color + 16777216) + 4278190080;
+ settings["outline_color"] = color;
+
+ translate_string("text", in_settings, "text", settings);
+ translate_int("backgroundOpacity", in_settings, "bk_opacity",
+ settings, 0);
+ translate_bool("vertical", in_settings, "vertical", settings);
+ translate_int("textOpacity", in_settings, "opacity", settings,
+ 0);
+ translate_bool("useOutline", in_settings, "outline", settings);
+ translate_int("outlineOpacity", in_settings, "outline_opacity",
+ settings, 0);
+ translate_int("outlineSize", in_settings, "outline_size",
+ settings, 0);
+ translate_bool("useTextExtents", in_settings, "extents",
+ settings);
+ translate_int("extentWidth", in_settings, "extents_cx",
+ settings, 0);
+ translate_int("extentHeight", in_settings, "extents_cy",
+ settings, 0);
+ translate_bool("mode", in_settings, "read_from_file", settings);
+ translate_bool("wrap", in_settings, "extents_wrap", settings);
+
+ string str = in_settings["file"].string_value();
+ settings["file"] = StringReplace(str, "\\\\", "/");
+
+ int in_align = in_settings["align"].int_value();
+ string align = in_align == 0
+ ? "left"
+ : (in_align == 1 ? "center" : "right");
+
+ settings["align"] = align;
+
+ bool bold = in_settings["bold"].int_value() == 1;
+ bool italic = in_settings["italic"].int_value() == 1;
+ bool underline = in_settings["underline"].int_value() == 1;
+
+ int flags = bold ? OBS_FONT_BOLD : 0;
+ flags |= italic ? OBS_FONT_ITALIC : 0;
+ flags |= underline ? OBS_FONT_UNDERLINE : 0;
+
+ Json::object font = Json::object{};
+
+ font["flags"] = flags;
+
+ translate_int("fontSize", in_settings, "size", font, 0);
+ translate_string("font", in_settings, "face", font);
+
+ if (bold && italic) {
+ font["style"] = "Bold Italic";
+ } else if (bold) {
+ font["style"] = "Bold";
+ } else if (italic) {
+ font["style"] = "Italic";
+ } else {
+ font["style"] = "Regular";
+ }
+
+ settings["font"] = font;
+ } else if (id == "MonitorCaptureSource") {
+ out["id"] = "monitor_capture";
+
+ translate_int("monitor", in_settings, "monitor", settings, 0);
+ translate_bool("captureMouse", in_settings, "capture_cursor",
+ settings);
+ } else if (id == "BitmapImageSource") {
+ out["id"] = "image_source";
+
+ string str = in_settings["path"].string_value();
+ settings["file"] = StringReplace(str, "\\\\", "/");
+ } else if (id == "BitmapTransitionSource") {
+ out["id"] = "slideshow";
+
+ Json files = in_settings["bitmap"];
+
+ if (!files.is_array()) {
+ files = Json::array{in_settings["bitmap"]};
+ }
+
+ settings["files"] = files;
+ } else if (id == "WindowCaptureSource") {
+ out["id"] = "window_capture";
+
+ string win = in_settings["window"].string_value();
+ string winClass = in_settings["windowClass"].string_value();
+
+ win = StringReplace(win, "/", "\\\\");
+ win = StringReplace(win, ":", "#3A");
+ winClass = StringReplace(winClass, ":", "#3A");
+
+ settings["window"] = win + ":" + winClass + ":";
+ settings["priority"] = 0;
+ } else if (id == "CLRBrowserSource") {
+ out["id"] = "browser_source";
+
+ string browser_dec =
+ QByteArray::fromBase64(in_settings["sourceSettings"]
+ .string_value()
+ .c_str())
+ .toStdString();
+
+ string err;
+
+ Json browser = Json::parse(browser_dec, err);
+
+ if (err != "")
+ return Json::object{};
+
+ Json::object obj = browser.object_items();
+
+ translate_string("CSS", obj, "css", settings);
+ translate_int("Height", obj, "height", settings, 0);
+ translate_int("Width", obj, "width", settings, 0);
+ translate_string("Url", obj, "url", settings);
+ } else if (id == "DeviceCapture") {
+ out["id"] = "dshow_input";
+
+ string device_id = in_settings["deviceID"].string_value();
+ string device_name = in_settings["deviceName"].string_value();
+
+ settings["video_device_id"] = device_name + ":" + device_id;
+
+ int w = in_settings["resolutionWidth"].int_value();
+ int h = in_settings["resolutionHeight"].int_value();
+
+ settings["resolution"] = to_string(w) + "x" + to_string(h);
+ } else if (id == "GraphicsCapture") {
+ bool hotkey = in_settings["useHotkey"].int_value() == 1;
+
+ if (hotkey) {
+ settings["capture_mode"] = "hotkey";
+ } else {
+ settings["capture_mode"] = "window";
+ }
+
+ string winClass = in_settings["windowClass"].string_value();
+ string exec = in_settings["executable"].string_value();
+
+ string window = ":" + winClass + ":" + exec;
+
+ settings["window"] = ":" + winClass + ":" + exec;
+
+ translate_bool("captureMouse", in_settings, "capture_cursor",
+ settings);
+ }
+
+ out["settings"] = settings;
+
+ return out;
+}
+
+#undef translate_int
+#undef translate_string
+#undef translate_double
+#undef translate_bool
+
+static void translate_sc(const Json &in, Json &out)
+{
+ Json::object res = Json::object{};
+
+ Json::array out_sources = Json::array{};
+ Json::array global = in["globals"].array_items();
+
+ if (!in["globals"].is_null()) {
+ for (size_t i = 0; i < global.size(); i++) {
+ Json source = global[i];
+
+ Json out_source = translate_source(source, out_sources);
+ out_sources.push_back(out_source);
+ }
+ }
+
+ Json::array scenes = in["scenes"].array_items();
+ string first_name = "";
+
+ for (size_t i = 0; i < scenes.size(); i++) {
+ Json in_scene = scenes[i];
+
+ if (first_name == "")
+ first_name = in_scene["name"].string_value();
+
+ Json::object settings = Json::object{};
+ Json::array items = Json::array{};
+
+ Json::array sources = in_scene["sources"].array_items();
+
+ for (size_t x = sources.size(); x > 0; x--) {
+ Json source = sources[x - 1];
+
+ Json::object out_source =
+ translate_source(source, out_sources);
+ Json::object out_item =
+ translate_scene_item(source, out_source);
+
+ out_item["id"] = (int)x - 1;
+
+ items.push_back(out_item);
+
+ if (out_source.find("preexist") == out_source.end())
+ out_sources.push_back(out_source);
+ }
+
+ out_sources.push_back(Json::object{
+ {"id", "scene"},
+ {"name", in_scene["name"]},
+ {"settings",
+ Json::object{{"items", items},
+ {"id_counter", (int)items.size()}}}});
+ }
+
+ res["current_scene"] = first_name;
+ res["current_program_scene"] = first_name;
+ res["sources"] = out_sources;
+ res["name"] = in["name"];
+
+ out = res;
+}
+
+static void create_string(const string &name, Json::object &out,
+ const string &data)
+{
+ string str = StringReplace(data, "\\\\", "/");
+ out[name] = str;
+}
+
+static void create_string_obj(const string &data, Json::array &arr)
+{
+ Json::object obj = Json::object{};
+ create_string("value", obj, data);
+ arr.push_back(obj);
+}
+
+static void create_double(const string &name, Json::object &out,
+ const string &data)
+{
+ double d = atof(data.c_str());
+ out[name] = d;
+}
+
+static void create_int(const string &name, Json::object &out,
+ const string &data)
+{
+ int i = atoi(data.c_str());
+ out[name] = i;
+}
+
+static void create_data_item(Json::object &out, const string &line)
+{
+ size_t end_pos = line.find(':') - 1;
+
+ if (end_pos == string::npos)
+ return;
+
+ size_t start_pos = 0;
+ while (line[start_pos] == ' ')
+ start_pos++;
+
+ string name = line.substr(start_pos, end_pos - start_pos);
+ const char *c_name = name.c_str();
+
+ string first = line.substr(end_pos + 3);
+
+ if ((first[0] >= 'A' && first[0] <= 'Z') ||
+ (first[0] >= 'a' && first[0] <= 'z') || first[0] == '\\' ||
+ first[0] == '/') {
+ if (out.find(c_name) != out.end()) {
+ Json::array arr = out[c_name].array_items();
+ if (out[c_name].is_string()) {
+ Json::array new_arr = Json::array{};
+ string str = out[c_name].string_value();
+ create_string_obj(str, new_arr);
+ arr = new_arr;
+ }
+
+ create_string_obj(first, arr);
+ out[c_name] = arr;
+ } else {
+ create_string(c_name, out, first);
+ }
+ } else if (first[0] == '"') {
+ string str = first.substr(1, first.size() - 2);
+
+ if (out.find(c_name) != out.end()) {
+ Json::array arr = out[c_name].array_items();
+ if (out[c_name].is_string()) {
+ Json::array new_arr = Json::array{};
+ string str1 = out[c_name].string_value();
+ create_string_obj(str1, new_arr);
+ arr = new_arr;
+ }
+
+ create_string_obj(str, arr);
+ out[c_name] = arr;
+ } else {
+ create_string(c_name, out, str);
+ }
+ } else if (first.find('.') != string::npos) {
+ create_double(c_name, out, first);
+ } else {
+ create_int(c_name, out, first);
+ }
+}
+
+static Json::object create_object(Json::object &out, string &line, string &src);
+
+static Json::array create_sources(Json::object &out, string &line, string &src)
+{
+ Json::array res = Json::array{};
+
+ line = ReadLine(src);
+ size_t l_len = line.size();
+ while (line != "" && line[l_len - 1] != '}') {
+ size_t end_pos = line.find(':');
+
+ if (end_pos == string::npos)
+ return Json::array{};
+
+ size_t start_pos = 0;
+ while (line[start_pos] == ' ')
+ start_pos++;
+
+ string name = line.substr(start_pos, end_pos - start_pos - 1);
+
+ Json::object nul = Json::object();
+
+ Json::object source = create_object(nul, line, src);
+ source["name"] = name;
+ res.push_back(source);
+
+ line = ReadLine(src);
+ l_len = line.size();
+ }
+
+ if (!out.empty())
+ out["sources"] = res;
+
+ return res;
+}
+
+static Json::object create_object(Json::object &out, string &line, string &src)
+{
+ size_t end_pos = line.find(':');
+
+ if (end_pos == string::npos)
+ return Json::object{};
+
+ size_t start_pos = 0;
+ while (line[start_pos] == ' ')
+ start_pos++;
+
+ string name = line.substr(start_pos, end_pos - start_pos - 1);
+
+ Json::object res = Json::object{};
+
+ line = ReadLine(src);
+
+ size_t l_len = line.size() - 1;
+
+ while (line != "" && line[l_len] != '}') {
+ start_pos = 0;
+ while (line[start_pos] == ' ')
+ start_pos++;
+
+ if (line.substr(start_pos, 7) == "sources")
+ create_sources(res, line, src);
+ else if (line[l_len] == '{')
+ create_object(res, line, src);
+ else
+ create_data_item(res, line);
+
+ line = ReadLine(src);
+ l_len = line.size() - 1;
+ }
+
+ if (!out.empty())
+ out[name] = res;
+
+ return res;
+}
+
+string ClassicImporter::Name(const string &path)
+{
+ return GetFilenameFromPath(path);
+}
+
+int ClassicImporter::ImportScenes(const string &path, string &name, Json &res)
+{
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+ if (!file_data)
+ return IMPORTER_FILE_WONT_OPEN;
+
+ string sc_name = GetFilenameFromPath(path);
+
+ if (name == "")
+ name = sc_name;
+
+ Json::object data = Json::object{};
+ data["name"] = name;
+
+ string file = file_data.Get();
+ string line = ReadLine(file);
+
+ while (line != "" && line[0] != '\0') {
+ string key = line != "global sources : {" ? "scenes"
+ : "globals";
+
+ Json::array arr = create_sources(data, line, file);
+ data[key] = arr;
+
+ line = ReadLine(file);
+ }
+
+ Json sc = data;
+ translate_sc(sc, res);
+
+ return IMPORTER_SUCCESS;
+}
+
+bool ClassicImporter::Check(const string &path)
+{
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+
+ if (!file_data)
+ return false;
+
+ bool check = false;
+
+ if (strncmp(file_data, "scenes : {\r\n", 12) == 0)
+ check = true;
+
+ return check;
+}
+
+OBSImporterFiles ClassicImporter::FindFiles()
+{
+ OBSImporterFiles res;
+
+#ifdef _WIN32
+ char dst[512];
+ int found = os_get_config_path(dst, 512, "OBS\\sceneCollection\\");
+ if (found == -1)
+ return res;
+
+ os_dir_t *dir = os_opendir(dst);
+ struct os_dirent *ent;
+ while ((ent = os_readdir(dir)) != NULL) {
+ if (ent->directory || *ent->d_name == '.')
+ continue;
+
+ string name = ent->d_name;
+ size_t pos = name.find(".xconfig");
+ if (pos != -1 && pos == name.length() - 8) {
+ string path = dst + name;
+ res.push_back(path);
+ }
+ }
+
+ os_closedir(dir);
+#endif
+
+ return res;
+}
diff --git a/UI/importers/importers.cpp b/UI/importers/importers.cpp
new file mode 100644
index 000000000..f25546b33
--- /dev/null
+++ b/UI/importers/importers.cpp
@@ -0,0 +1,104 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "importers.hpp"
+#include
+
+using namespace std;
+using namespace json11;
+
+vector> importers;
+
+void ImportersInit()
+{
+ importers.clear();
+ importers.push_back(make_unique());
+ importers.push_back(make_unique());
+ importers.push_back(make_unique());
+ importers.push_back(make_unique());
+}
+
+int ImportSCFromProg(const string &path, string &name, const string &program,
+ Json &res)
+{
+ if (!os_file_exists(path.c_str())) {
+ return IMPORTER_FILE_NOT_FOUND;
+ }
+
+ for (size_t i = 0; i < importers.size(); i++) {
+ if (program == importers[i]->Prog()) {
+ return importers[i]->ImportScenes(path, name, res);
+ }
+ }
+
+ return IMPORTER_UNKNOWN_ERROR;
+}
+
+int ImportSC(const string &path, std::string &name, Json &res)
+{
+ if (!os_file_exists(path.c_str())) {
+ return IMPORTER_FILE_NOT_FOUND;
+ }
+
+ string prog = DetectProgram(path);
+
+ if (prog == "Null") {
+ return IMPORTER_FILE_NOT_RECOGNISED;
+ }
+
+ return ImportSCFromProg(path, name, prog, res);
+}
+
+string DetectProgram(const string &path)
+{
+ if (!os_file_exists(path.c_str())) {
+ return "Null";
+ }
+
+ for (size_t i = 0; i < importers.size(); i++) {
+ if (importers[i]->Check(path)) {
+ return importers[i]->Prog();
+ }
+ }
+
+ return "Null";
+}
+
+string GetSCName(const string &path, const string &prog)
+{
+ for (size_t i = 0; i < importers.size(); i++) {
+ if (importers[i]->Prog() == prog) {
+ return importers[i]->Name(path);
+ }
+ }
+
+ return "Null";
+}
+
+OBSImporterFiles ImportersFindFiles()
+{
+ OBSImporterFiles f;
+
+ for (size_t i = 0; i < importers.size(); i++) {
+ OBSImporterFiles f2 = importers[i]->FindFiles();
+ if (f2.size() != 0) {
+ f.insert(f.end(), f2.begin(), f2.end());
+ }
+ }
+
+ return f;
+}
diff --git a/UI/importers/importers.hpp b/UI/importers/importers.hpp
new file mode 100644
index 000000000..a085e4171
--- /dev/null
+++ b/UI/importers/importers.hpp
@@ -0,0 +1,172 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 .
+******************************************************************************/
+
+#pragma once
+
+#include "obs.hpp"
+#include "json11.hpp"
+#include
+#include
+#include
+#include
+
+enum obs_importer_responses {
+ IMPORTER_SUCCESS,
+ IMPORTER_FILE_NOT_FOUND,
+ IMPORTER_FILE_NOT_RECOGNISED,
+ IMPORTER_FILE_WONT_OPEN,
+ IMPORTER_ERROR_DURING_CONVERSION,
+ IMPORTER_UNKNOWN_ERROR,
+ IMPORTER_NOT_FOUND
+};
+
+typedef std::vector OBSImporterFiles;
+
+class Importer {
+public:
+ virtual std::string Prog() { return "Null"; };
+ virtual int ImportScenes(const std::string &path, std::string &name,
+ json11::Json &res) = 0;
+ virtual bool Check(const std::string &path) = 0;
+ virtual std::string Name(const std::string &path) = 0;
+ virtual OBSImporterFiles FindFiles()
+ {
+ OBSImporterFiles f;
+ return f;
+ };
+};
+
+class ClassicImporter : public Importer {
+public:
+ std::string Prog() { return "OBSClassic"; };
+ int ImportScenes(const std::string &path, std::string &name,
+ json11::Json &res);
+ bool Check(const std::string &path);
+ std::string Name(const std::string &path);
+ OBSImporterFiles FindFiles();
+};
+
+class StudioImporter : public Importer {
+public:
+ std::string Prog() { return "OBSStudio"; };
+ int ImportScenes(const std::string &path, std::string &name,
+ json11::Json &res);
+ bool Check(const std::string &path);
+ std::string Name(const std::string &path);
+};
+
+class SLImporter : public Importer {
+public:
+ std::string Prog() { return "Streamlabs"; };
+ int ImportScenes(const std::string &path, std::string &name,
+ json11::Json &res);
+ bool Check(const std::string &path);
+ std::string Name(const std::string &path);
+ OBSImporterFiles FindFiles();
+};
+
+class XSplitImporter : public Importer {
+public:
+ std::string Prog() { return "XSplitBroadcaster"; };
+ int ImportScenes(const std::string &path, std::string &name,
+ json11::Json &res);
+ bool Check(const std::string &path);
+ std::string Name(const std::string &path)
+ {
+ return "XSplit Import";
+ UNUSED_PARAMETER(path);
+ };
+ OBSImporterFiles FindFiles();
+};
+
+void ImportersInit();
+
+std::string DetectProgram(const std::string &path);
+std::string GetSCName(const std::string &path, const std::string &prog);
+
+int ImportSCFromProg(const std::string &path, std::string &name,
+ const std::string &program, json11::Json &res);
+int ImportSC(const std::string &path, std::string &name, json11::Json &res);
+
+OBSImporterFiles ImportersFindFiles();
+
+void TranslateOSStudio(json11::Json &data);
+
+static inline std::string GetFilenameFromPath(const std::string &path)
+{
+#ifdef _WIN32
+ size_t pos = path.find_last_of('\\');
+ if (pos == -1 || pos < path.find_last_of('/'))
+ pos = path.find_last_of('/');
+#else
+ size_t pos = path.find_last_of('/');
+#endif
+ size_t ext = path.find_last_of('.');
+
+ if (ext < pos) {
+ return path.substr(pos + 1);
+ } else {
+ return path.substr(pos + 1, ext - pos - 1);
+ }
+}
+
+static inline std::string GetFolderFromPath(const std::string &path)
+{
+#ifdef _WIN32
+ size_t pos = path.find_last_of('\\');
+ if (pos == -1 || pos < path.find_last_of('/'))
+ pos = path.find_last_of('/');
+#else
+ size_t pos = path.find_last_of('/');
+#endif
+ return path.substr(0, pos + 1);
+}
+
+static inline std::string StringReplace(const std::string &in,
+ const std::string &search,
+ const std::string &rep)
+{
+ std::string res = in;
+ size_t pos;
+
+ while ((pos = res.find(search)) != std::string::npos) {
+ res.replace(pos, search.length(), rep);
+ }
+
+ return res;
+}
+
+static inline std::string ReadLine(std::string &str)
+{
+ str = StringReplace(str, "\r\n", "\n");
+
+ size_t pos = str.find('\n');
+
+ if (pos == std::string::npos)
+ pos = str.find(EOF);
+
+ if (pos == std::string::npos)
+ pos = str.find('\0');
+
+ if (pos == std::string::npos)
+ return "";
+
+ std::string res = str.substr(0, pos);
+ str = str.substr(pos + 1);
+
+ return res;
+}
diff --git a/UI/importers/sl.cpp b/UI/importers/sl.cpp
new file mode 100644
index 000000000..2e33c9ae6
--- /dev/null
+++ b/UI/importers/sl.cpp
@@ -0,0 +1,463 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "importers.hpp"
+
+using namespace std;
+using namespace json11;
+
+static string translate_key(const string &sl_key)
+{
+ if (sl_key.substr(0, 6) == "Numpad" && sl_key.size() == 7) {
+ return "OBS_KEY_NUM" + sl_key.substr(6);
+ } else if (sl_key.substr(0, 3) == "Key") {
+ return "OBS_KEY_" + sl_key.substr(3);
+ } else if (sl_key.substr(0, 5) == "Digit") {
+ return "OBS_KEY_" + sl_key.substr(5);
+ } else if (sl_key[0] == 'F' && sl_key.size() < 4) {
+ return "OBS_KEY_" + sl_key;
+ }
+
+#define add_translation(str, out) \
+ if (sl_key == str) { \
+ return out; \
+ }
+
+ add_translation("Backquote", "OBS_KEY_ASCIITILDE");
+ add_translation("Backspace", "OBS_KEY_BACKSPACE");
+ add_translation("Tab", "OBS_KEY_TAB");
+ add_translation("Space", "OBS_KEY_SPACE");
+ add_translation("Period", "OBS_KEY_PERIOD");
+ add_translation("Slash", "OBS_KEY_SLASH");
+ add_translation("Backslash", "OBS_KEY_BACKSLASH");
+ add_translation("Minus", "OBS_KEY_MINUS");
+ add_translation("Comma", "OBS_KEY_COMMA");
+ add_translation("Plus", "OBS_KEY_PLUS");
+ add_translation("Quote", "OBS_KEY_APOSTROPHE");
+ add_translation("Semicolon", "OBS_KEY_SEMICOLON");
+ add_translation("NumpadSubtract", "OBS_KEY_NUMMINUS");
+ add_translation("NumpadAdd", "OBS_KEY_NUMPLUS");
+ add_translation("NumpadDecimal", "OBS_KEY_NUMPERIOD");
+ add_translation("NumpadDivide", "OBS_KEY_NUMSLASH");
+ add_translation("NumpadMultiply", "OBS_KEY_NUMASTERISK");
+ add_translation("Enter", "OBS_KEY_RETURN");
+ add_translation("CapsLock", "OBS_KEY_CAPSLOCK");
+ add_translation("NumLock", "OBS_KEY_NUMLOCK");
+ add_translation("ScrollLock", "OBS_KEY_SCROLLLOCK");
+ add_translation("Pause", "OBS_KEY_PAUSE");
+ add_translation("Insert", "OBS_KEY_INSERT");
+ add_translation("Home", "OBS_KEY_HOME");
+ add_translation("End", "OBS_KEY_END");
+ add_translation("Escape", "OBS_KEY_ESCAPE");
+ add_translation("Delete", "OBS_KEY_DELETE");
+ add_translation("ArrowUp", "OBS_KEY_UP");
+ add_translation("ArrowDown", "OBS_KEY_DOWN");
+ add_translation("ArrowLeft", "OBS_KEY_LEFT");
+ add_translation("ArrowRight", "OBS_KEY_RIGHT");
+ add_translation("PageUp", "OBS_KEY_PAGEUP");
+ add_translation("PageDown", "OBS_KEY_PAGEDOWN");
+ add_translation("BracketLeft", "OBS_KEY_BRACKETLEFT");
+ add_translation("BracketRight", "OBS_KEY_BRACKETRIGHT");
+#undef add_translation
+
+ return "";
+}
+
+static string translate_hotkey(const Json &hotkey, const string &source)
+{
+ string name = hotkey["actionName"].string_value();
+
+#define add_translation(in, str, out, source) \
+ if (in == str) { \
+ return out + source; \
+ }
+
+ add_translation(name, "TOGGLE_SOURCE_VISIBILITY_SHOW",
+ "libobs.show_scene_item.", source);
+ add_translation(name, "TOGGLE_SOURCE_VISIBILITY_HIDE",
+ "libobs.hide_scene_item.", source);
+
+ string empty = "";
+
+ add_translation(name, "SWITCH_TO_SCENE", "OBSBasic.SelectScene", empty);
+
+ add_translation(name, "TOGGLE_MUTE", "libobs.mute", empty);
+ add_translation(name, "TOGGLE_UNMUTE", "libobs.unmute", empty);
+ add_translation(name, "PUSH_TO_MUTE", "libobs.push-to-mute", empty);
+ add_translation(name, "PUSH_TO_TALK", "libobs.push-to-talk", empty);
+ add_translation(name, "GAME_CAPTURE_HOTKEY_START", "hotkey_start",
+ empty);
+ add_translation(name, "GAME_CAPTURE_HOTKEY_STOP", "hotkey_stop", empty);
+
+ return "";
+#undef add_translation
+}
+
+static string get_source_name_from_id(const Json &root, const string &id)
+{
+ Json::array source_arr = root["sources"]["items"].array_items();
+ Json::array scene_arr = root["scenes"]["items"].array_items();
+
+ source_arr.insert(source_arr.end(), scene_arr.begin(), scene_arr.end());
+
+ for (size_t i = 0; i < source_arr.size(); i++) {
+ Json item = source_arr[i];
+ string source_id = item["id"].string_value();
+
+ if (source_id == id) {
+ return item["name"].string_value();
+ }
+ }
+
+ return "";
+}
+
+static void get_hotkey_bindings(Json::object &out_hotkeys,
+ const Json &in_hotkeys, const string &name)
+{
+ Json::array hot_arr = in_hotkeys.array_items();
+ for (size_t i = 0; i < hot_arr.size(); i++) {
+ Json hotkey = hot_arr[i];
+ Json::array bindings = hotkey["bindings"].array_items();
+ Json::array out_hotkey = Json::array{};
+
+ string hotkey_name = translate_hotkey(hotkey, name);
+
+ for (size_t x = 0; x < bindings.size(); x++) {
+ Json binding = bindings[x];
+ Json modifiers = binding["modifiers"];
+
+ string key =
+ translate_key(binding["key"].string_value());
+
+ out_hotkey.push_back(
+ Json::object{{"control", modifiers["ctrl"]},
+ {"shift", modifiers["shift"]},
+ {"command", modifiers["meta"]},
+ {"alt", modifiers["alt"]},
+ {"key", key}});
+ }
+
+ out_hotkeys[hotkey_name] = out_hotkey;
+ }
+}
+
+static void get_scene_items(const Json &root, Json::object &scene,
+ const Json::array &in)
+{
+ int length = 0;
+
+ Json::object hotkeys = scene["hotkeys"].object_items();
+
+ Json::array out_items = Json::array{};
+ for (size_t i = 0; i < in.size(); i++) {
+ Json item = in[i];
+
+ Json in_crop = item["crop"];
+ string id = item["sourceId"].string_value();
+ string name = get_source_name_from_id(root, id);
+
+ Json::array hotkey_items =
+ item["hotkeys"]["items"].array_items();
+
+ get_hotkey_bindings(hotkeys, hotkey_items, name);
+
+ out_items.push_back(Json::object{
+ {"name", name},
+ {"id", length++},
+ {"pos",
+ Json::object{{"x", item["x"]}, {"y", item["y"]}}},
+ {"scale", Json::object{{"x", item["scaleX"]},
+ {"y", item["scaleY"]}}},
+ {"rot", item["rotation"]},
+ {"visible", item["visible"]},
+ {"crop_top", in_crop["top"]},
+ {"crop_bottom", in_crop["bottom"]},
+ {"crop_left", in_crop["left"]},
+ {"crop_right", in_crop["right"]}});
+ }
+
+ scene["hotkeys"] = hotkeys;
+ scene["settings"] =
+ Json::object{{"items", out_items}, {"id_counter", length}};
+}
+
+static int attempt_import(const Json &root, const string &name, Json &res)
+{
+ Json::array source_arr = root["sources"]["items"].array_items();
+ Json::array scenes_arr = root["scenes"]["items"].array_items();
+ Json::array t_arr = root["transitions"]["transitions"].array_items();
+
+ string t_id = root["transitions"]["defaultTransitionId"].string_value();
+
+ Json::array out_sources = Json::array{};
+ Json::array out_transitions = Json::array{};
+
+ for (size_t i = 0; i < source_arr.size(); i++) {
+ Json source = source_arr[i];
+
+ Json in_hotkeys = source["hotkeys"];
+ Json::array hotkey_items =
+ source["hotkeys"]["items"].array_items();
+ Json in_filters = source["filters"];
+ Json::array filter_items = in_filters["items"].array_items();
+
+ Json in_settings = source["settings"];
+ Json in_sync = source["syncOffset"];
+
+ int sync = (int)(in_sync["sec"].number_value() * 1000000000 +
+ in_sync["nsec"].number_value());
+
+ double vol = source["volume"].number_value();
+ bool muted = source["muted"].bool_value();
+ string name = source["name"].string_value();
+ int monitoring = (int)source["monitoringType"].int_value();
+
+ Json::object out_hotkeys = Json::object{};
+ get_hotkey_bindings(out_hotkeys, hotkey_items, "");
+
+ Json::array out_filters = Json::array{};
+ for (size_t x = 0; x < filter_items.size(); x++) {
+ Json::object filter = filter_items[x].object_items();
+ string type = filter["type"].string_value();
+ filter["id"] = type;
+
+ out_filters.push_back(filter);
+ }
+
+ out_sources.push_back(
+ Json::object{{"filters", out_filters},
+ {"hotkeys", out_hotkeys},
+ {"id", source["type"]},
+ {"settings", in_settings},
+ {"sync", sync},
+ {"volume", vol},
+ {"muted", muted},
+ {"name", name},
+ {"monitoring_type", monitoring}});
+ }
+
+ string scene_name = "";
+
+ for (size_t i = 0; i < scenes_arr.size(); i++) {
+ Json scene = scenes_arr[i];
+
+ if (scene_name == "")
+ scene_name = scene["name"].string_value();
+
+ Json in_hotkeys = scene["hotkeys"];
+ Json::array hotkey_items = in_hotkeys["items"].array_items();
+ Json in_filters = scene["filters"];
+ Json::array filter_items = in_filters["items"].array_items();
+
+ Json in_settings = scene["settings"];
+ Json in_sync = scene["syncOffset"];
+
+ int sync = (int)(in_sync["sec"].number_value() * 1000000000 +
+ in_sync["nsec"].number_value());
+
+ double vol = scene["volume"].number_value();
+ bool muted = scene["muted"].bool_value();
+ string name = scene["name"].string_value();
+ int monitoring = scene["monitoringType"].int_value();
+
+ Json::object out_hotkeys = Json::object{};
+ get_hotkey_bindings(out_hotkeys, hotkey_items, "");
+
+ Json::array out_filters = Json::array{};
+ for (size_t x = 0; x < filter_items.size(); x++) {
+ Json::object filter = filter_items[x].object_items();
+ string type = filter["type"].string_value();
+ filter["id"] = type;
+
+ out_filters.push_back(filter);
+ }
+
+ Json::object out =
+ Json::object{{"filters", out_filters},
+ {"hotkeys", out_hotkeys},
+ {"id", "scene"},
+ {"settings", in_settings},
+ {"sync", sync},
+ {"volume", vol},
+ {"muted", muted},
+ {"name", name},
+ {"monitoring_type", monitoring},
+ {"private_settings", Json::object{}}};
+
+ Json in_items = scene["sceneItems"];
+ Json::array items_arr = in_items["items"].array_items();
+
+ get_scene_items(root, out, items_arr);
+
+ out_sources.push_back(out);
+ }
+
+ string transition_name = "";
+
+ for (size_t i = 0; i < t_arr.size(); i++) {
+ Json transition = t_arr[i];
+
+ Json in_settings = transition["settings"];
+
+ int duration = transition["duration"].int_value();
+ string name = transition["name"].string_value();
+ string id = transition["id"].string_value();
+
+ if (id == t_id)
+ transition_name = name;
+
+ out_transitions.push_back(
+ Json::object{{"id", transition["type"]},
+ {"settings", in_settings},
+ {"name", name},
+ {"duration", duration}});
+ }
+
+ res = Json::object{{"sources", out_sources},
+ {"transitions", out_transitions},
+ {"current_scene", scene_name},
+ {"current_program_scene", scene_name},
+ {"current_transition", transition_name},
+ {"name", name == "" ? "Streamlabs Import" : name}};
+
+ return IMPORTER_SUCCESS;
+}
+
+string SLImporter::Name(const string &path)
+{
+ string name;
+
+ string folder = GetFolderFromPath(path);
+ string manifest_file = GetFilenameFromPath(path);
+ string manifest_path = folder + "manifest.json";
+
+ if (os_file_exists(manifest_path.c_str())) {
+ BPtr file_data =
+ os_quick_read_utf8_file(manifest_path.c_str());
+
+ string err;
+ Json data = Json::parse(file_data, err);
+
+ if (err == "") {
+ Json::array collections =
+ data["collections"].array_items();
+
+ bool name_set = false;
+
+ for (size_t i = 0, l = collections.size(); i < l; i++) {
+ Json c = collections[i];
+ string c_id = c["id"].string_value();
+ string c_name = c["name"].string_value();
+
+ if (c_id == manifest_file) {
+ name = c_name;
+ name_set = true;
+ break;
+ }
+ }
+
+ if (!name_set) {
+ name = "Unknown Streamlabs Import";
+ }
+ }
+ } else {
+ name = "Unknown Streamlabs Import";
+ }
+
+ return name;
+}
+
+int SLImporter::ImportScenes(const string &path, string &name, Json &res)
+{
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+
+ std::string err;
+ Json data = Json::parse(file_data, err);
+
+ if (err != "")
+ return IMPORTER_ERROR_DURING_CONVERSION;
+
+ string node_type = data["nodeType"].string_value();
+
+ int result = IMPORTER_ERROR_DURING_CONVERSION;
+
+ if (node_type == "RootNode") {
+ if (name == "") {
+ string auto_name = Name(path);
+ result = attempt_import(data, auto_name, res);
+ } else {
+ result = attempt_import(data, name, res);
+ }
+ }
+
+ TranslateOSStudio(res);
+
+ return result;
+}
+
+bool SLImporter::Check(const string &path)
+{
+ bool check = false;
+
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+
+ if (file_data) {
+ string err;
+ Json root = Json::parse(file_data, err);
+
+ if (!root.is_null()) {
+ string node_type = root["nodeType"].string_value();
+
+ if (node_type == "RootNode")
+ check = true;
+ }
+ }
+
+ return check;
+}
+
+OBSImporterFiles SLImporter::FindFiles()
+{
+ OBSImporterFiles res;
+#ifdef _WIN32
+ char dst[512];
+
+ int found = os_get_config_path(dst, 512,
+ "slobs-client\\SceneCollections\\");
+ if (found == -1)
+ return res;
+
+ os_dir_t *dir = os_opendir(dst);
+ struct os_dirent *ent;
+ while ((ent = os_readdir(dir)) != NULL) {
+ string name = ent->d_name;
+
+ if (ent->directory || name[0] == '.' || name == "manifest.json")
+ continue;
+
+ size_t pos = name.find_last_of(".json");
+ size_t end_pos = name.size() - 1;
+ if (pos != -1 && pos == end_pos) {
+ string str = dst + name;
+ res.push_back(str);
+ }
+ }
+ os_closedir(dir);
+#endif
+ return res;
+}
diff --git a/UI/importers/studio.cpp b/UI/importers/studio.cpp
new file mode 100644
index 000000000..798b515ae
--- /dev/null
+++ b/UI/importers/studio.cpp
@@ -0,0 +1,212 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "importers.hpp"
+
+using namespace std;
+using namespace json11;
+
+void TranslateOSStudio(Json &res)
+{
+ Json::object out = res.object_items();
+ Json::array sources = out["sources"].array_items();
+
+ for (size_t i = 0; i < sources.size(); i++) {
+ Json::object source = sources[i].object_items();
+ Json::object settings = source["settings"].object_items();
+
+ string id = source["id"].string_value();
+
+#define DirectTranslation(before, after) \
+ if (id == before) { \
+ source["id"] = after; \
+ }
+
+#define ClearTranslation(before, after) \
+ if (id == before) { \
+ source["id"] = after; \
+ source["settings"] = Json::object{}; \
+ }
+
+#ifdef __APPLE__
+ DirectTranslation("text_gdiplus", "text_ft2_source");
+
+ ClearTranslation("game_capture", "syphon-input");
+
+ ClearTranslation("wasapi_input_capture",
+ "coreaudio_input_capture");
+ ClearTranslation("wasapi_output_capture",
+ "coreaudio_output_capture");
+ ClearTranslation("pulse_input_capture",
+ "coreaudio_input_capture");
+ ClearTranslation("pulse_output_capture",
+ "coreaudio_output_capture");
+
+ ClearTranslation("jack_output_capture",
+ "coreaudio_output_capture");
+ ClearTranslation("alsa_input_capture",
+ "coreaudio_input_capture");
+
+ ClearTranslation("dshow_input", "av_capture_input");
+ ClearTranslation("v4l2_input", "av_capture_input");
+
+ ClearTranslation("xcomposite_input", "window_capture");
+
+ if (id == "monitor_capture") {
+ if (settings["show_cursor"].is_null() &&
+ !settings["capture_cursor"].is_null()) {
+ bool cursor =
+ settings["capture_cursor"].bool_value();
+
+ settings["show_cursor"] = cursor;
+ }
+ }
+
+ DirectTranslation("xshm_input", "monitor_capture");
+#elif defined(_WIN32)
+ DirectTranslation("text_ft2_source", "text_gdiplus");
+
+ ClearTranslation("syphon-input", "game_capture");
+
+ ClearTranslation("coreaudio_input_capture",
+ "wasapi_input_capture");
+ ClearTranslation("coreaudio_output_capture",
+ "wasapi_output_capture");
+ ClearTranslation("pulse_input_capture", "wasapi_input_capture");
+ ClearTranslation("pulse_output_capture",
+ "wasapi_output_capture");
+
+ ClearTranslation("jack_output_capture",
+ "wasapi_output_capture");
+ ClearTranslation("alsa_input_capture", "wasapi_input_capture");
+
+ ClearTranslation("av_capture_input", "dshow_input");
+ ClearTranslation("v4l2_input", "dshow_input");
+
+ ClearTranslation("xcomposite_input", "window_capture");
+
+ if (id == "monitor_capture" || id == "xshm_input") {
+ if (!settings["show_cursor"].is_null()) {
+ bool cursor =
+ settings["show_cursor"].bool_value();
+
+ settings["capture_cursor"] = cursor;
+ }
+
+ source["id"] = "monitor_capture";
+ }
+#else
+ DirectTranslation("text_gdiplus", "text_ft2_source");
+
+ ClearTranslation("coreaudio_input_capture",
+ "pulse_input_capture");
+ ClearTranslation("coreaudio_output_capture",
+ "pulse_output_capture");
+ ClearTranslation("wasapi_input_capture", "pulse_input_capture");
+ ClearTranslation("wasapi_output_capture",
+ "pulse_output_capture");
+
+ ClearTranslation("av_capture_input", "v4l2_input");
+ ClearTranslation("dshow_input", "v4l2_input");
+
+ ClearTranslation("window_capture", "xcomposite_input");
+
+ if (id == "monitor_capture") {
+ source["id"] = "xshm_input";
+
+ if (settings["show_cursor"].is_null() &&
+ !settings["capture_cursor"].is_null()) {
+ bool cursor =
+ settings["capture_cursor"].bool_value();
+
+ settings["show_cursor"] = cursor;
+ }
+ }
+#endif
+ source["settings"] = settings;
+ sources[i] = source;
+#undef DirectTranslation
+#undef ClearTranslation
+ }
+
+ res = out;
+}
+
+bool StudioImporter::Check(const string &path)
+{
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+ string err;
+ Json collection = Json::parse(file_data, err);
+
+ if (err != "")
+ return false;
+
+ if (collection.is_null())
+ return false;
+
+ if (collection["sources"].is_null())
+ return false;
+
+ if (collection["name"].is_null())
+ return false;
+
+ if (collection["current_scene"].is_null())
+ return false;
+
+ return true;
+}
+
+string StudioImporter::Name(const string &path)
+{
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+ string err;
+
+ Json d = Json::parse(file_data, err);
+
+ string name = d["name"].string_value();
+
+ return name;
+}
+
+int StudioImporter::ImportScenes(const string &path, string &name, Json &res)
+{
+ if (!os_file_exists(path.c_str()))
+ return IMPORTER_FILE_NOT_FOUND;
+
+ if (!Check(path.c_str()))
+ return IMPORTER_FILE_NOT_RECOGNISED;
+
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+ string err;
+ Json d = Json::parse(file_data, err);
+
+ if (err != "")
+ return IMPORTER_ERROR_DURING_CONVERSION;
+
+ TranslateOSStudio(d);
+
+ Json::object obj = d.object_items();
+
+ if (name != "")
+ obj["name"] = name;
+ else
+ obj["name"] = "OBS Studio Import";
+
+ res = obj;
+
+ return IMPORTER_SUCCESS;
+}
diff --git a/UI/importers/xsplit.cpp b/UI/importers/xsplit.cpp
new file mode 100644
index 000000000..34642bfd9
--- /dev/null
+++ b/UI/importers/xsplit.cpp
@@ -0,0 +1,539 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "importers.hpp"
+#include
+
+#include
+
+using namespace std;
+using namespace json11;
+
+static int hex_string_to_int(string str)
+{
+ int res = 0;
+
+ if (str[0] == '#')
+ str = str.substr(1);
+
+ for (size_t i = 0, l = str.size(); i < l; i++) {
+ res *= 16;
+
+ if (str[0] >= '0' && str[0] <= '9')
+ res += str[0] - '0';
+ else
+ res += str[0] - 'A' + 10;
+
+ str = str.substr(1);
+ }
+
+ return res;
+}
+
+static Json::object parse_text(QString &config)
+{
+ int start = config.indexOf("*{");
+ config = config.mid(start + 1);
+ config.replace("\\", "/");
+
+ string err;
+ Json data = Json::parse(config.toStdString(), err);
+
+ if (err != "")
+ return Json::object{};
+
+ string outline = data["outline"].string_value();
+ int out = 0;
+
+ if (outline == "thick")
+ out = 20;
+ else if (outline == "thicker")
+ out = 40;
+ else if (outline == "thinner")
+ out = 5;
+ else if (outline == "thin")
+ out = 10;
+
+ string valign = data["vertAlign"].string_value();
+ if (valign == "middle")
+ valign = "center";
+
+ Json font = Json::object{{"face", data["fontStyle"]}, {"size", 200}};
+
+ return Json::object{
+ {"text", data["text"]},
+ {"font", font},
+ {"outline", out > 0},
+ {"outline_size", out},
+ {"outline_color",
+ hex_string_to_int(data["outlineColor"].string_value())},
+ {"color", hex_string_to_int(data["color"].string_value())},
+ {"align", data["textAlign"]},
+ {"valign", valign},
+ {"alpha", data["opacity"]}};
+}
+
+static Json::array parse_playlist(QString &playlist)
+{
+ Json::array out = Json::array{};
+
+ while (true) {
+ int end = playlist.indexOf('*');
+ QString path = playlist.left(end);
+
+ out.push_back(Json::object{{"value", path.toStdString()}});
+
+ int next = playlist.indexOf('|');
+ if (next == -1)
+ break;
+
+ playlist = playlist.mid(next + 1);
+ }
+
+ return out;
+}
+
+static void parse_media_types(QDomNamedNodeMap &attr, Json::object &source,
+ Json::object &settings)
+{
+ QString playlist = attr.namedItem("FilePlaylist").nodeValue();
+
+ if (playlist != "") {
+ source["id"] = "vlc_source";
+ settings["playlist"] = parse_playlist(playlist);
+
+ QString end_op = attr.namedItem("OpWhenFinished").nodeValue();
+ if (end_op == "2")
+ settings["loop"] = true;
+ } else {
+ QString url = attr.namedItem("item").nodeValue();
+ int sep = url.indexOf("://");
+
+ if (sep != -1) {
+ QString prot = url.left(sep);
+ if (prot == "smlndi") {
+ source["id"] = "ndi_source";
+ } else {
+ source["id"] = "ffmpeg_source";
+ int info = url.indexOf("\\");
+ QString input;
+
+ if (info != -1) {
+ input = url.left(info);
+ } else {
+ input = url;
+ }
+
+ settings["input"] = input.toStdString();
+ settings["is_local_file"] = false;
+ }
+ } else {
+ source["id"] = "ffmpeg_source";
+ settings["local_file"] =
+ url.replace("\\", "/").toStdString();
+ settings["is_local_file"] = true;
+ }
+ }
+}
+
+static Json::object parse_slideshow(QString &config)
+{
+ int start = config.indexOf("images\":[");
+ if (start == -1)
+ return Json::object{};
+
+ config = config.mid(start + 8);
+ config.replace("\\\\", "/");
+
+ int end = config.indexOf(']');
+ if (end == -1)
+ return Json::object{};
+
+ string arr = config.left(end + 1).toStdString();
+ string err;
+ Json::array files = Json::parse(arr, err).array_items();
+
+ if (err != "")
+ return Json::object{};
+
+ Json::array files_out = Json::array{};
+
+ for (size_t i = 0; i < files.size(); i++) {
+ string file = files[i].string_value();
+ files_out.push_back(Json::object{{"value", file}});
+ }
+
+ QString options = config.mid(end + 1);
+ options[0] = '{';
+
+ Json opt = Json::parse(options.toStdString(), err);
+
+ if (err != "")
+ return Json::object{};
+
+ return Json::object{{"randomize", opt["random"]},
+ {"slide_time",
+ opt["delay"].number_value() * 1000 + 700},
+ {"files", files_out}};
+}
+
+static bool source_name_exists(const string &name, const Json::array &sources)
+{
+ for (size_t i = 0; i < sources.size(); i++) {
+ if (sources.at(i)["name"].string_value() == name)
+ return true;
+ }
+
+ return false;
+}
+
+static Json get_source_with_id(const string &src_id, const Json::array &sources)
+{
+ for (size_t i = 0; i < sources.size(); i++) {
+ if (sources.at(i)["src_id"].string_value() == src_id)
+ return sources.at(i);
+ }
+
+ return nullptr;
+}
+
+static void parse_items(QDomNode &item, Json::array &items,
+ Json::array &sources)
+{
+ while (!item.isNull()) {
+ QDomNamedNodeMap attr = item.attributes();
+ QString srcid = attr.namedItem("srcid").nodeValue();
+ double vol = attr.namedItem("volume").nodeValue().toDouble();
+ int type = attr.namedItem("type").nodeValue().toInt();
+
+ string name;
+ Json::object settings;
+ Json::object source;
+ string temp_name;
+ int x = 0;
+
+ Json exists = get_source_with_id(srcid.toStdString(), sources);
+ if (!exists.is_null()) {
+ name = exists["name"].string_value();
+ goto skip;
+ }
+
+ name = attr.namedItem("cname").nodeValue().toStdString();
+ if (name == "" || name[0] == '\0')
+ name = attr.namedItem("name").nodeValue().toStdString();
+
+ temp_name = name;
+ while (source_name_exists(temp_name, sources)) {
+ string new_name = name + " " + to_string(x++);
+ temp_name = new_name;
+ }
+
+ name = temp_name;
+
+ settings = Json::object{};
+ source = Json::object{{"name", name},
+ {"src_id", srcid.toStdString()},
+ {"volume", vol}};
+
+ /** type=1 means Media of some kind (Video Playlist, RTSP,
+ RTMP, NDI or Media File).
+ type=2 means either a DShow or WASAPI source.
+ type=4 means an Image source.
+ type=5 means either a Display or Window Capture.
+ type=7 means a Game Capture.
+ type=8 means rendered with a browser, which includes:
+ Web Page, Image Slideshow, Text.
+ type=11 means another Scene. **/
+
+ if (type == 1) {
+ parse_media_types(attr, source, settings);
+ } else if (type == 2) {
+ QString audio = attr.namedItem("itemaudio").nodeValue();
+
+ if (audio == "") {
+ source["id"] = "dshow_input";
+ } else {
+ source["id"] = "wasapi_input_capture";
+ int dev = audio.indexOf("\\wave:") + 6;
+
+ QString res =
+ "{0.0.1.00000000}." + audio.mid(dev);
+ res = res.toLower();
+
+ settings["device_id"] = res.toStdString();
+ }
+ } else if (type == 4) {
+ source["id"] = "image_source";
+
+ QString path = attr.namedItem("item").nodeValue();
+ path.replace("\\", "/");
+ settings["file"] = path.toStdString();
+ } else if (type == 5) {
+ QString opt = attr.namedItem("item").nodeValue();
+
+ QDomDocument options;
+ options.setContent(opt);
+
+ QDomNode el = options.documentElement();
+
+ QDomNamedNodeMap o_attr = el.attributes();
+ QString display =
+ o_attr.namedItem("desktop").nodeValue();
+
+ if (display != "") {
+ source["id"] = "monitor_capture";
+ int cursor = attr.namedItem("ScrCapShowMouse")
+ .nodeValue()
+ .toInt();
+ settings["capture_cursor"] = cursor == 1;
+ } else {
+ source["id"] = "window_capture";
+
+ QString exec =
+ o_attr.namedItem("module").nodeValue();
+ QString window =
+ o_attr.namedItem("window").nodeValue();
+ QString _class =
+ o_attr.namedItem("class").nodeValue();
+
+ int pos = exec.lastIndexOf('\\');
+
+ if (_class == "") {
+ _class = "class";
+ }
+
+ QString res = window + ":" + _class + ":" +
+ exec.mid(pos + 1);
+
+ settings["window"] = res.toStdString();
+ settings["priority"] = 2;
+ }
+ } else if (type == 7) {
+ QString opt = attr.namedItem("item").nodeValue();
+ opt.replace("<", "<");
+ opt.replace(">", ">");
+ opt.replace(""", "\"");
+
+ QDomDocument doc;
+ doc.setContent(opt);
+
+ QDomNode el = doc.documentElement();
+ QDomNamedNodeMap o_attr = el.attributes();
+
+ QString name = o_attr.namedItem("wndname").nodeValue();
+ QString exec =
+ o_attr.namedItem("imagename").nodeValue();
+
+ QString res = name = "::" + exec;
+
+ source["id"] = "game_capture";
+ settings["window"] = res.toStdString();
+ settings["capture_mode"] = "window";
+ } else if (type == 8) {
+ QString plugin = attr.namedItem("item").nodeValue();
+
+ if (plugin.startsWith(
+ "html:plugin:imageslideshowplg*")) {
+ source["id"] = "slideshow";
+ settings = parse_slideshow(plugin);
+ } else if (plugin.startsWith("html:plugin:titleplg")) {
+ source["id"] = "text_gdiplus";
+ settings = parse_text(plugin);
+ } else if (plugin.startsWith("http")) {
+ source["id"] = "browser_source";
+ int end = plugin.indexOf('*');
+ settings["url"] =
+ plugin.left(end).toStdString();
+ }
+ } else if (type == 11) {
+ QString id = attr.namedItem("item").nodeValue();
+ Json source =
+ get_source_with_id(id.toStdString(), sources);
+ name = source["name"].string_value();
+
+ goto skip;
+ }
+
+ source["settings"] = settings;
+ sources.push_back(source);
+
+ skip:
+ struct obs_video_info ovi;
+ obs_get_video_info(&ovi);
+
+ int width = ovi.base_width;
+ int height = ovi.base_height;
+
+ double pos_left =
+ attr.namedItem("pos_left").nodeValue().toDouble();
+ double pos_right =
+ attr.namedItem("pos_right").nodeValue().toDouble();
+ double pos_top =
+ attr.namedItem("pos_top").nodeValue().toDouble();
+ double pos_bottom =
+ attr.namedItem("pos_bottom").nodeValue().toDouble();
+
+ bool visible = attr.namedItem("visible").nodeValue() == "1";
+
+ Json out_item = Json::object{
+ {"bounds_type", 2},
+ {"pos", Json::object{{"x", pos_left * width},
+ {"y", pos_top * height}}},
+ {"bounds",
+ Json::object{{"x", (pos_right - pos_left) * width},
+ {"y", (pos_bottom - pos_top) * height}}},
+ {"name", name},
+ {"visible", visible}};
+
+ items.push_back(out_item);
+
+ item = item.nextSibling();
+ }
+}
+
+static Json::object parse_scenes(QDomElement &scenes)
+{
+ Json::array sources = Json::array{};
+
+ QString first = "";
+
+ QDomNode in_scene = scenes.firstChild();
+ while (!in_scene.isNull()) {
+ QString type = in_scene.nodeName();
+
+ if (type == "placement") {
+ QDomNamedNodeMap attr = in_scene.attributes();
+
+ QString name = attr.namedItem("name").nodeValue();
+ QString id = attr.namedItem("id").nodeValue();
+
+ if (first == "")
+ first = name;
+
+ Json out = Json::object{
+ {"id", "scene"},
+ {"name", name.toStdString().c_str()},
+ {"src_id", id.toStdString().c_str()}};
+
+ sources.push_back(out);
+ }
+ in_scene = in_scene.nextSibling();
+ }
+
+ in_scene = scenes.firstChild();
+ for (size_t i = 0, l = sources.size(); i < l; i++) {
+ Json::object source = sources[i].object_items();
+ Json::array items = Json::array{};
+ QDomNode firstChild = in_scene.firstChild();
+
+ parse_items(firstChild, items, sources);
+
+ Json settings = Json::object{{"items", items},
+ {"id_counter", (int)items.size()}};
+
+ source["settings"] = settings;
+ sources[i] = source;
+
+ in_scene = in_scene.nextSibling();
+ }
+
+ return Json::object{{"sources", sources},
+ {"current_scene", first.toStdString()},
+ {"current_program_scene", first.toStdString()}};
+}
+
+int XSplitImporter::ImportScenes(const string &path, string &name,
+ json11::Json &res)
+{
+ if (name == "")
+ name = "XSplit Import";
+
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+
+ if (!file_data)
+ return IMPORTER_FILE_WONT_OPEN;
+
+ QDomDocument doc;
+ doc.setContent(QString(file_data));
+
+ QDomElement docElem = doc.documentElement();
+
+ Json::object r = parse_scenes(docElem);
+ r["name"] = name;
+
+ res = r;
+
+ return IMPORTER_SUCCESS;
+}
+
+bool XSplitImporter::Check(const string &path)
+{
+ bool check = false;
+
+ BPtr file_data = os_quick_read_utf8_file(path.c_str());
+
+ if (!file_data)
+ return false;
+
+ string pos = file_data.Get();
+
+ string line = ReadLine(pos);
+ while (line != "") {
+ if (line.substr(0, 5) == "d_name;
+
+ if (ent->directory || name[0] == '.')
+ continue;
+
+ if (name == "Placements.bpres") {
+ string str = dst + name;
+ res.push_back(str);
+
+ break;
+ }
+ }
+ os_closedir(dir);
+#endif
+ return res;
+}
diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp
index e0ea3d125..9eb529d09 100644
--- a/UI/window-basic-main-scene-collections.cpp
+++ b/UI/window-basic-main-scene-collections.cpp
@@ -23,6 +23,7 @@
#include
#include "item-widget-helpers.hpp"
#include "window-basic-main.hpp"
+#include "window-importer.hpp"
#include "window-namedialog.hpp"
#include "qt-wrappers.hpp"
@@ -388,64 +389,11 @@ void OBSBasic::on_actionRemoveSceneCollection_triggered()
void OBSBasic::on_actionImportSceneCollection_triggered()
{
- char path[512];
+ OBSImporter *imp;
+ imp = new OBSImporter(this);
+ imp->exec();
- QString qhome = QDir::homePath();
-
- int ret = GetConfigPath(path, 512, "obs-studio/basic/scenes/");
- if (ret <= 0) {
- blog(LOG_WARNING, "Failed to get scene collection config path");
- return;
- }
-
- QString qfilePath = QFileDialog::getOpenFileName(
- this, QTStr("Basic.MainMenu.SceneCollection.Import"), qhome,
- "JSON Files (*.json)");
-
- QFileInfo finfo(qfilePath);
- QString qfilename = finfo.fileName();
- QString qpath = QT_UTF8(path);
- QFileInfo destinfo(QT_UTF8(path) + qfilename);
-
- if (!qfilePath.isEmpty() && !qfilePath.isNull()) {
- string absPath = QT_TO_UTF8(finfo.absoluteFilePath());
- OBSData scenedata =
- obs_data_create_from_json_file(absPath.c_str());
- obs_data_release(scenedata);
-
- string origName = obs_data_get_string(scenedata, "name");
- string name = origName;
- string file;
- int inc = 1;
-
- while (SceneCollectionExists(name.c_str())) {
- name = origName + " (" + to_string(++inc) + ")";
- }
-
- obs_data_set_string(scenedata, "name", name.c_str());
-
- if (!GetFileSafeName(name.c_str(), file)) {
- blog(LOG_WARNING,
- "Failed to create "
- "safe file name for '%s'",
- name.c_str());
- return;
- }
-
- string filePath = path + file;
-
- if (!GetClosestUnusedFileName(filePath, "json")) {
- blog(LOG_WARNING,
- "Failed to get "
- "closest file name for %s",
- file.c_str());
- return;
- }
-
- obs_data_save_json_safe(scenedata, filePath.c_str(), "tmp",
- "bak");
- RefreshSceneCollections();
- }
+ RefreshSceneCollections();
}
void OBSBasic::on_actionExportSceneCollection_triggered()
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index 4b2b1e846..5770bb2db 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -238,6 +238,7 @@ private:
QPointer stats;
QPointer remux;
QPointer extraBrowsers;
+ QPointer importer;
QPointer startStreamMenu;
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index d9af21a7a..bd6afcfd8 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -354,6 +354,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->overflowHide, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->overflowAlwaysVisible,CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->overflowSelectionHide,CHECK_CHANGED, GENERAL_CHANGED);
+ HookWidget(ui->automaticSearch, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->doubleClickSwitch, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->studioPortraitLayout, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->prevProgLabelToggle, CHECK_CHANGED, GENERAL_CHANGED);
@@ -1200,6 +1201,10 @@ void OBSBasicSettings::LoadGeneralSettings()
GetGlobalConfig(), "BasicWindow", "OverflowSelectionHidden");
ui->overflowSelectionHide->setChecked(overflowSelectionHide);
+ bool automaticSearch = config_get_bool(GetGlobalConfig(), "General",
+ "AutomaticCollectionSearch");
+ ui->automaticSearch->setChecked(automaticSearch);
+
bool doubleClickSwitch = config_get_bool(
GetGlobalConfig(), "BasicWindow", "TransitionOnDoubleClick");
ui->doubleClickSwitch->setChecked(doubleClickSwitch);
@@ -2857,6 +2862,10 @@ void OBSBasicSettings::SaveGeneralSettings()
config_set_bool(GetGlobalConfig(), "BasicWindow",
"TransitionOnDoubleClick",
ui->doubleClickSwitch->isChecked());
+ if (WidgetChanged(ui->automaticSearch))
+ config_set_bool(GetGlobalConfig(), "General",
+ "AutomaticCollectionSearch",
+ ui->automaticSearch->isChecked());
config_set_bool(GetGlobalConfig(), "BasicWindow",
"WarnBeforeStartingStream",
diff --git a/UI/window-importer.cpp b/UI/window-importer.cpp
new file mode 100644
index 000000000..e55027987
--- /dev/null
+++ b/UI/window-importer.cpp
@@ -0,0 +1,612 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 "window-importer.hpp"
+
+#include "obs-app.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "qt-wrappers.hpp"
+#include "importers/importers.hpp"
+
+enum ImporterColumn {
+ Selected,
+ Name,
+ Path,
+ Program,
+
+ Count
+};
+
+enum ImporterEntryRole {
+ EntryStateRole = Qt::UserRole,
+ NewPath,
+ AutoPath,
+ CheckEmpty
+};
+
+/**********************************************************
+ Delegate - Presents cells in the grid.
+**********************************************************/
+
+ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate()
+ : QStyledItemDelegate()
+{
+}
+
+QWidget *ImporterEntryPathItemDelegate::createEditor(
+ QWidget *parent, const QStyleOptionViewItem & /* option */,
+ const QModelIndex &index) const
+{
+ bool empty = index.model()
+ ->index(index.row(), ImporterColumn::Path)
+ .data(ImporterEntryRole::CheckEmpty)
+ .value();
+
+ QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum,
+ QSizePolicy::Policy::Expanding,
+ QSizePolicy::ControlType::PushButton);
+
+ QWidget *container = new QWidget(parent);
+
+ auto browseCallback = [this, container]() {
+ const_cast(this)->handleBrowse(
+ container);
+ };
+
+ auto clearCallback = [this, container]() {
+ const_cast(this)->handleClear(
+ container);
+ };
+
+ QHBoxLayout *layout = new QHBoxLayout();
+ layout->setMargin(0);
+ layout->setSpacing(0);
+
+ QLineEdit *text = new QLineEdit();
+ text->setObjectName(QStringLiteral("text"));
+ text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding,
+ QSizePolicy::Policy::Expanding,
+ QSizePolicy::ControlType::LineEdit));
+ layout->addWidget(text);
+
+ QToolButton *browseButton = new QToolButton();
+ browseButton->setText("...");
+ browseButton->setSizePolicy(buttonSizePolicy);
+ layout->addWidget(browseButton);
+
+ container->connect(browseButton, &QToolButton::clicked, browseCallback);
+
+ // The "clear" button is not shown in output cells
+ // or the insertion point's input cell.
+ if (!empty) {
+ QToolButton *clearButton = new QToolButton();
+ clearButton->setText("X");
+ clearButton->setSizePolicy(buttonSizePolicy);
+ layout->addWidget(clearButton);
+
+ container->connect(clearButton, &QToolButton::clicked,
+ clearCallback);
+ }
+
+ container->setLayout(layout);
+ container->setFocusProxy(text);
+ return container;
+}
+
+void ImporterEntryPathItemDelegate::setEditorData(
+ QWidget *editor, const QModelIndex &index) const
+{
+ QLineEdit *text = editor->findChild();
+ text->setText(index.data().toString());
+ QObject::connect(text, SIGNAL(textEdited(QString)), this,
+ SLOT(updateText()));
+ editor->setProperty(PATH_LIST_PROP, QVariant());
+}
+
+void ImporterEntryPathItemDelegate::setModelData(QWidget *editor,
+ QAbstractItemModel *model,
+ const QModelIndex &index) const
+{
+ // We use the PATH_LIST_PROP property to pass a list of
+ // path strings from the editor widget into the model's
+ // NewPathsToProcessRole. This is only used when paths
+ // are selected through the "browse" or "delete" buttons
+ // in the editor. If the user enters new text in the
+ // text box, we simply pass that text on to the model
+ // as normal text data in the default role.
+ QVariant pathListProp = editor->property(PATH_LIST_PROP);
+ if (pathListProp.isValid()) {
+ QStringList list =
+ editor->property(PATH_LIST_PROP).toStringList();
+ model->setData(index, list, ImporterEntryRole::NewPath);
+ } else {
+ QLineEdit *lineEdit = editor->findChild();
+ model->setData(index, lineEdit->text());
+ }
+}
+
+void ImporterEntryPathItemDelegate::paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QStyleOptionViewItem localOption = option;
+ initStyleOption(&localOption, index);
+
+ QApplication::style()->drawControl(QStyle::CE_ItemViewItem,
+ &localOption, painter);
+}
+
+void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container)
+{
+ QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
+
+ QLineEdit *text = container->findChild();
+
+ QString currentPath = text->text();
+
+ bool isSet = false;
+ QStringList paths = QFileDialog::getOpenFileNames(
+ container, QTStr("Importer.SelectCollection"), currentPath,
+ QTStr("Importer.Collection") + QString(" ") + Pattern);
+
+ if (!paths.empty()) {
+ container->setProperty(PATH_LIST_PROP, paths);
+ isSet = true;
+ }
+
+ if (isSet)
+ emit commitData(container);
+}
+
+void ImporterEntryPathItemDelegate::handleClear(QWidget *container)
+{
+ // An empty string list will indicate that the entry is being
+ // blanked and should be deleted.
+ container->setProperty(PATH_LIST_PROP, QStringList());
+
+ emit commitData(container);
+}
+
+void ImporterEntryPathItemDelegate::updateText()
+{
+ QLineEdit *lineEdit = dynamic_cast(sender());
+ QWidget *editor = lineEdit->parentWidget();
+ emit commitData(editor);
+}
+
+/**
+ Model
+**/
+
+int ImporterModel::rowCount(const QModelIndex &) const
+{
+ return options.length() + 1;
+}
+
+int ImporterModel::columnCount(const QModelIndex &) const
+{
+ return ImporterColumn::Count;
+}
+
+QVariant ImporterModel::data(const QModelIndex &index, int role) const
+{
+ QVariant result = QVariant();
+
+ if (index.row() >= options.length()) {
+ if (role == ImporterEntryRole::CheckEmpty)
+ result = true;
+ else
+ return QVariant();
+ } else if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case ImporterColumn::Path:
+ result = options[index.row()].path;
+ break;
+ case ImporterColumn::Program:
+ result = options[index.row()].program;
+ break;
+ case ImporterColumn::Name:
+ result = options[index.row()].name;
+ }
+ } else if (role == Qt::EditRole) {
+ if (index.column() == ImporterColumn::Name) {
+ result = options[index.row()].name;
+ }
+ } else if (role == Qt::CheckStateRole) {
+ switch (index.column()) {
+ case ImporterColumn::Selected:
+ if (options[index.row()].program != "")
+ result = options[index.row()].selected
+ ? Qt::Checked
+ : Qt::Unchecked;
+ else
+ result = Qt::Unchecked;
+ }
+ } else if (role == ImporterEntryRole::CheckEmpty) {
+ result = options[index.row()].empty;
+ }
+
+ return result;
+}
+
+Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags flags = QAbstractTableModel::flags(index);
+
+ if (index.column() == ImporterColumn::Selected &&
+ index.row() != options.length()) {
+ flags |= Qt::ItemIsUserCheckable;
+ } else if (index.column() == ImporterColumn::Path ||
+ (index.column() == ImporterColumn::Name &&
+ index.row() != options.length())) {
+ flags |= Qt::ItemIsEditable;
+ }
+
+ return flags;
+}
+
+void ImporterModel::checkInputPath(int row)
+{
+ ImporterEntry &entry = options[row];
+
+ if (entry.path.isEmpty()) {
+ entry.program = "";
+ entry.empty = true;
+ entry.selected = false;
+ entry.name = "";
+ } else {
+ entry.empty = false;
+
+ std::string program = DetectProgram(entry.path.toStdString());
+ entry.program = QTStr(program.c_str());
+
+ if (program == "") {
+ entry.selected = false;
+ } else {
+ std::string name =
+ GetSCName(entry.path.toStdString(), program);
+ entry.name = name.c_str();
+ }
+ }
+
+ emit dataChanged(index(row, 0), index(row, ImporterColumn::Count));
+}
+
+bool ImporterModel::setData(const QModelIndex &index, const QVariant &value,
+ int role)
+{
+ if (role == ImporterEntryRole::NewPath) {
+ QStringList list = value.toStringList();
+
+ if (list.size() == 0) {
+ if (index.row() < options.size()) {
+ beginRemoveRows(QModelIndex(), index.row(),
+ index.row());
+ options.removeAt(index.row());
+ endRemoveRows();
+ }
+ } else {
+ if (list.size() > 0 && index.row() < options.length()) {
+ options[index.row()].path = list[0];
+ checkInputPath(index.row());
+
+ list.removeAt(0);
+ }
+
+ if (list.size() > 0) {
+ int row = index.row();
+ int lastRow = row + list.size() - 1;
+ beginInsertRows(QModelIndex(), row, lastRow);
+
+ for (QString path : list) {
+ ImporterEntry entry;
+ entry.path = path;
+
+ options.insert(row, entry);
+
+ row++;
+ }
+
+ endInsertRows();
+
+ for (row = index.row(); row <= lastRow; row++) {
+ checkInputPath(row);
+ }
+ }
+ }
+ } else if (index.row() == options.length()) {
+ QString path = value.toString();
+
+ if (!path.isEmpty()) {
+ ImporterEntry entry;
+ entry.path = path;
+ entry.selected = role != ImporterEntryRole::AutoPath;
+ entry.empty = false;
+
+ beginInsertRows(QModelIndex(), options.length() + 1,
+ options.length() + 1);
+ options.append(entry);
+ endInsertRows();
+
+ checkInputPath(index.row());
+ }
+ } else if (index.column() == ImporterColumn::Selected) {
+ bool select = value.toBool();
+
+ options[index.row()].selected = select;
+ } else if (index.column() == ImporterColumn::Path) {
+ QString path = value.toString();
+ options[index.row()].path = path;
+
+ checkInputPath(index.row());
+ } else if (index.column() == ImporterColumn::Name) {
+ QString name = value.toString();
+ options[index.row()].name = name;
+ }
+
+ emit dataChanged(index, index);
+
+ return true;
+}
+
+QVariant ImporterModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ QVariant result = QVariant();
+
+ if (role == Qt::DisplayRole &&
+ orientation == Qt::Orientation::Horizontal) {
+ switch (section) {
+ case ImporterColumn::Path:
+ result = QTStr("Importer.Path");
+ break;
+ case ImporterColumn::Program:
+ result = QTStr("Importer.Program");
+ break;
+ case ImporterColumn::Name:
+ result = QTStr("Name");
+ }
+ }
+
+ return result;
+}
+
+/**
+ Window
+**/
+
+OBSImporter::OBSImporter(QWidget *parent)
+ : QDialog(parent),
+ optionsModel(new ImporterModel),
+ ui(new Ui::OBSImporter)
+{
+ setAcceptDrops(true);
+
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+ ui->setupUi(this);
+
+ ui->tableView->setModel(optionsModel);
+ ui->tableView->setItemDelegateForColumn(
+ ImporterColumn::Path, new ImporterEntryPathItemDelegate());
+ ui->tableView->horizontalHeader()->setSectionResizeMode(
+ QHeaderView::ResizeMode::ResizeToContents);
+ ui->tableView->horizontalHeader()->setSectionResizeMode(
+ ImporterColumn::Path, QHeaderView::ResizeMode::Stretch);
+
+ connect(optionsModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)),
+ this, SLOT(dataChanged()));
+
+ ui->tableView->setEditTriggers(
+ QAbstractItemView::EditTrigger::CurrentChanged);
+
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import"));
+ ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add"));
+
+ connect(ui->buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()),
+ this, SLOT(importCollections()));
+ connect(ui->buttonBox->button(QDialogButtonBox::Open),
+ SIGNAL(clicked()), this, SLOT(browseImport()));
+ connect(ui->buttonBox->button(QDialogButtonBox::Close),
+ SIGNAL(clicked()), this, SLOT(close()));
+
+ ImportersInit();
+
+ bool autoSearchPrompt = config_get_bool(App()->GlobalConfig(),
+ "General", "AutoSearchPrompt");
+
+ if (!autoSearchPrompt) {
+ QMessageBox::StandardButton button = OBSMessageBox::question(
+ parent, QTStr("Importer.AutomaticCollectionPrompt"),
+ QTStr("Importer.AutomaticCollectionText"));
+
+ if (button == QMessageBox::Yes) {
+ config_set_bool(App()->GlobalConfig(), "General",
+ "AutomaticCollectionSearch", true);
+ } else {
+ config_set_bool(App()->GlobalConfig(), "General",
+ "AutomaticCollectionSearch", false);
+ }
+
+ config_set_bool(App()->GlobalConfig(), "General",
+ "AutoSearchPrompt", true);
+ }
+
+ bool autoSearch = config_get_bool(App()->GlobalConfig(), "General",
+ "AutomaticCollectionSearch");
+
+ OBSImporterFiles f;
+ if (autoSearch)
+ f = ImportersFindFiles();
+
+ for (size_t i = 0; i < f.size(); i++) {
+ QString path = f[i].c_str();
+ path.replace("\\", "/");
+ addImportOption(path, true);
+ }
+
+ f.clear();
+
+ ui->tableView->resizeColumnsToContents();
+
+ QModelIndex index =
+ optionsModel->createIndex(optionsModel->rowCount() - 1, 2);
+ QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex",
+ Qt::QueuedConnection,
+ Q_ARG(const QModelIndex &, index));
+}
+
+void OBSImporter::addImportOption(QString path, bool automatic)
+{
+ QStringList list;
+
+ list.append(path);
+
+ QModelIndex insertIndex = optionsModel->index(
+ optionsModel->rowCount() - 1, ImporterColumn::Path);
+
+ optionsModel->setData(insertIndex, list,
+ automatic ? ImporterEntryRole::AutoPath
+ : ImporterEntryRole::NewPath);
+}
+
+void OBSImporter::dropEvent(QDropEvent *ev)
+{
+ for (QUrl url : ev->mimeData()->urls()) {
+ QFileInfo fileInfo(url.toLocalFile());
+ if (fileInfo.isDir()) {
+
+ QDirIterator dirIter(fileInfo.absoluteFilePath(),
+ QDir::Files);
+
+ while (dirIter.hasNext()) {
+ addImportOption(dirIter.next(), false);
+ }
+ } else {
+ addImportOption(fileInfo.canonicalFilePath(), false);
+ }
+ }
+}
+
+void OBSImporter::dragEnterEvent(QDragEnterEvent *ev)
+{
+ if (ev->mimeData()->hasUrls())
+ ev->accept();
+}
+
+static bool CheckConfigExists(const char *dir, QString name)
+{
+
+ QString dst = dir;
+ dst += "/";
+ dst += name;
+ dst += ".json";
+
+ dst.replace(" ", "_");
+
+ return os_file_exists(dst.toStdString().c_str());
+}
+
+void OBSImporter::browseImport()
+{
+ QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
+
+ QStringList paths = QFileDialog::getOpenFileNames(
+ this, QTStr("Importer.SelectCollection"), "",
+ QTStr("Importer.Collection") + QString(" ") + Pattern);
+
+ if (!paths.empty()) {
+ for (int i = 0; i < paths.count(); i++) {
+ addImportOption(paths[i], false);
+ }
+ }
+}
+
+void OBSImporter::importCollections()
+{
+ setEnabled(false);
+
+ char dst[512];
+ GetConfigPath(dst, 512, "obs-studio/basic/scenes/");
+
+ for (int i = 0; i < optionsModel->rowCount() - 1; i++) {
+ int selected = optionsModel->index(i, ImporterColumn::Selected)
+ .data(Qt::CheckStateRole)
+ .value();
+
+ if (selected == Qt::Unchecked)
+ continue;
+
+ QString path = optionsModel->index(i, ImporterColumn::Path)
+ .data(Qt::DisplayRole)
+ .value();
+ QString name = optionsModel->index(i, ImporterColumn::Name)
+ .data(Qt::DisplayRole)
+ .value();
+
+ std::string pathStr = path.toStdString();
+ std::string nameStr = name.toStdString();
+
+ json11::Json res;
+ ImportSC(pathStr, nameStr, res);
+
+ if (res != json11::Json()) {
+ json11::Json::object out = res.object_items();
+ QString file = res["name"].string_value().c_str();
+
+ bool safe = !CheckConfigExists(dst, file);
+ int x = 1;
+ while (!safe) {
+ file = name;
+ file += " (";
+ file += QString::number(x);
+ file += ")";
+
+ safe = !CheckConfigExists(dst, file);
+ x++;
+ }
+
+ out["name"] = file.toStdString();
+
+ std::string save = dst;
+ save += "/";
+ save += file.replace(" ", "_").toStdString();
+ save += ".json";
+
+ std::string out_str = json11::Json(out).dump();
+
+ os_quick_write_utf8_file(save.c_str(), out_str.c_str(),
+ out_str.size(), false);
+ }
+ }
+
+ close();
+}
+
+void OBSImporter::dataChanged()
+{
+ ui->tableView->resizeColumnToContents(ImporterColumn::Name);
+}
diff --git a/UI/window-importer.hpp b/UI/window-importer.hpp
new file mode 100644
index 000000000..63309975e
--- /dev/null
+++ b/UI/window-importer.hpp
@@ -0,0 +1,107 @@
+/******************************************************************************
+ Copyright (C) 2019-2020 by Dillon Pentz
+
+ 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 .
+******************************************************************************/
+
+#pragma once
+
+#include "obs-app.hpp"
+#include "window-basic-main.hpp"
+#include
+#include
+#include
+#include "ui_OBSImporter.h"
+
+class ImporterModel;
+
+class OBSImporter : public QDialog {
+ Q_OBJECT
+
+ QPointer optionsModel;
+ std::unique_ptr ui;
+
+public:
+ explicit OBSImporter(QWidget *parent = nullptr);
+
+ void addImportOption(QString path, bool automatic);
+
+protected:
+ virtual void dropEvent(QDropEvent *ev) override;
+ virtual void dragEnterEvent(QDragEnterEvent *ev) override;
+
+public slots:
+ void browseImport();
+ void importCollections();
+ void dataChanged();
+};
+
+class ImporterModel : public QAbstractTableModel {
+ Q_OBJECT
+
+ friend class OBSImporter;
+
+public:
+ ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {}
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ bool setData(const QModelIndex &index, const QVariant &value, int role);
+
+private:
+ struct ImporterEntry {
+ QString path;
+ QString program;
+ QString name;
+
+ bool selected;
+ bool empty;
+ };
+
+ QList options;
+
+ void checkInputPath(int row);
+};
+
+class ImporterEntryPathItemDelegate : public QStyledItemDelegate {
+ Q_OBJECT
+
+public:
+ ImporterEntryPathItemDelegate();
+
+ virtual QWidget *createEditor(QWidget *parent,
+ const QStyleOptionViewItem & /* option */,
+ const QModelIndex &index) const override;
+
+ virtual void setEditorData(QWidget *editor,
+ const QModelIndex &index) const override;
+ virtual void setModelData(QWidget *editor, QAbstractItemModel *model,
+ const QModelIndex &index) const override;
+ virtual void paint(QPainter *painter,
+ const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+
+private:
+ const char *PATH_LIST_PROP = "pathList";
+
+ void handleBrowse(QWidget *container);
+ void handleClear(QWidget *container);
+
+private slots:
+ void updateText();
+};