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(); +};