UI: Add advanced scene collection importer

This replaces the previous Open File dialog for importing collections
with a window for importing many collections at once, based on the remux
window, along with support for importing from OBS Classic, XSplit
Broadcaster and from Streamlabs' fork. This also translates sources
between OSes that Studio supports.
master
VodBox 2020-02-02 20:44:07 +13:00
parent da326f63f5
commit 191165c721
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();
};