Merge pull request #1956 from VodBox/advanced-importer

UI: Add advanced scene collection importer
master
Jim 2020-02-16 09:32:09 -08:00 committed by GitHub
commit 7f373edbea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2945 additions and 57 deletions

View File

@ -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}

View File

@ -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"

View File

@ -605,6 +605,44 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_19">
<property name="title">
<string>Basic.Settings.General.Importers</string>
</property>
<layout class="QFormLayout" name="formLayout_36">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item row="0" column="0">
<spacer name="horizontalSpacer_26">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>170</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="automaticSearch">
<property name="text">
<string>Basic.Settings.General.AutomaticCollectionSearch</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_11">
<property name="title">

61
UI/forms/OBSImporter.ui Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OBSImporter</class>
<widget class="QDialog" name="OBSImporter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>850</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
<string>Importer</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Importer.HelpText</string>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Open|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QTableView" name="tableView">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>23</number>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>23</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderDefaultSectionSize">
<number>23</number>
</attribute>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

584
UI/importers/classic.cpp Normal file
View File

@ -0,0 +1,584 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "importers.hpp"
#include <QByteArray>
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<char> 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<char> 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;
}

104
UI/importers/importers.cpp Normal file
View File

@ -0,0 +1,104 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "importers.hpp"
#include <memory>
using namespace std;
using namespace json11;
vector<unique_ptr<Importer>> importers;
void ImportersInit()
{
importers.clear();
importers.push_back(make_unique<StudioImporter>());
importers.push_back(make_unique<ClassicImporter>());
importers.push_back(make_unique<SLImporter>());
importers.push_back(make_unique<XSplitImporter>());
}
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;
}

172
UI/importers/importers.hpp Normal file
View File

@ -0,0 +1,172 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include "obs.hpp"
#include "json11.hpp"
#include <util/platform.h>
#include <util/util.hpp>
#include <string>
#include <vector>
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<std::string> 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;
}

463
UI/importers/sl.cpp Normal file
View File

@ -0,0 +1,463 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#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<char> 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<char> 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<char> 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;
}

212
UI/importers/studio.cpp Normal file
View File

@ -0,0 +1,212 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#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<char> 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<char> 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<char> 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;
}

539
UI/importers/xsplit.cpp Normal file
View File

@ -0,0 +1,539 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "importers.hpp"
#include <ctype.h>
#include <QDomDocument>
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("&lt;", "<");
opt.replace("&gt;", ">");
opt.replace("&quot;", "\"");
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<char> 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<char> 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) == "<?xml") {
line = ReadLine(pos);
} else {
if (line.substr(0, 14) == "<configuration") {
check = true;
}
break;
}
}
return check;
}
OBSImporterFiles XSplitImporter::FindFiles()
{
OBSImporterFiles res;
#ifdef _WIN32
char dst[512];
int found = os_get_program_data_path(
dst, 512, "SplitMediaLabs\\XSplit\\Presentation2.0\\");
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] == '.')
continue;
if (name == "Placements.bpres") {
string str = dst + name;
res.push_back(str);
break;
}
}
os_closedir(dir);
#endif
return res;
}

View File

@ -23,6 +23,7 @@
#include <QStandardPaths>
#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()

View File

@ -238,6 +238,7 @@ private:
QPointer<QWidget> stats;
QPointer<QWidget> remux;
QPointer<QWidget> extraBrowsers;
QPointer<QWidget> importer;
QPointer<QMenu> startStreamMenu;

View File

@ -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",

612
UI/window-importer.cpp Normal file
View File

@ -0,0 +1,612 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "window-importer.hpp"
#include "obs-app.hpp"
#include <QPushButton>
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <QMimeData>
#include <QStyledItemDelegate>
#include <QDirIterator>
#include <QDropEvent>
#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<bool>();
QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum,
QSizePolicy::Policy::Expanding,
QSizePolicy::ControlType::PushButton);
QWidget *container = new QWidget(parent);
auto browseCallback = [this, container]() {
const_cast<ImporterEntryPathItemDelegate *>(this)->handleBrowse(
container);
};
auto clearCallback = [this, container]() {
const_cast<ImporterEntryPathItemDelegate *>(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<QLineEdit *>();
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<QLineEdit *>();
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<QLineEdit *>();
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<QLineEdit *>(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<int>();
if (selected == Qt::Unchecked)
continue;
QString path = optionsModel->index(i, ImporterColumn::Path)
.data(Qt::DisplayRole)
.value<QString>();
QString name = optionsModel->index(i, ImporterColumn::Name)
.data(Qt::DisplayRole)
.value<QString>();
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);
}

107
UI/window-importer.hpp Normal file
View File

@ -0,0 +1,107 @@
/******************************************************************************
Copyright (C) 2019-2020 by Dillon Pentz <dillon@vodbox.io>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include "obs-app.hpp"
#include "window-basic-main.hpp"
#include <QPointer>
#include <QStyledItemDelegate>
#include <QFileInfo>
#include "ui_OBSImporter.h"
class ImporterModel;
class OBSImporter : public QDialog {
Q_OBJECT
QPointer<ImporterModel> optionsModel;
std::unique_ptr<Ui::OBSImporter> 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<ImporterEntry> 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();
};