Merge pull request #1956 from VodBox/advanced-importer
UI: Add advanced scene collection importermaster
commit
7f373edbea
|
@ -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}
|
||||
|
|
|
@ -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"
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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("<", "<");
|
||||
opt.replace(">", ">");
|
||||
opt.replace(""", "\"");
|
||||
|
||||
QDomDocument doc;
|
||||
doc.setContent(opt);
|
||||
|
||||
QDomNode el = doc.documentElement();
|
||||
QDomNamedNodeMap o_attr = el.attributes();
|
||||
|
||||
QString name = o_attr.namedItem("wndname").nodeValue();
|
||||
QString exec =
|
||||
o_attr.namedItem("imagename").nodeValue();
|
||||
|
||||
QString res = name = "::" + exec;
|
||||
|
||||
source["id"] = "game_capture";
|
||||
settings["window"] = res.toStdString();
|
||||
settings["capture_mode"] = "window";
|
||||
} else if (type == 8) {
|
||||
QString plugin = attr.namedItem("item").nodeValue();
|
||||
|
||||
if (plugin.startsWith(
|
||||
"html:plugin:imageslideshowplg*")) {
|
||||
source["id"] = "slideshow";
|
||||
settings = parse_slideshow(plugin);
|
||||
} else if (plugin.startsWith("html:plugin:titleplg")) {
|
||||
source["id"] = "text_gdiplus";
|
||||
settings = parse_text(plugin);
|
||||
} else if (plugin.startsWith("http")) {
|
||||
source["id"] = "browser_source";
|
||||
int end = plugin.indexOf('*');
|
||||
settings["url"] =
|
||||
plugin.left(end).toStdString();
|
||||
}
|
||||
} else if (type == 11) {
|
||||
QString id = attr.namedItem("item").nodeValue();
|
||||
Json source =
|
||||
get_source_with_id(id.toStdString(), sources);
|
||||
name = source["name"].string_value();
|
||||
|
||||
goto skip;
|
||||
}
|
||||
|
||||
source["settings"] = settings;
|
||||
sources.push_back(source);
|
||||
|
||||
skip:
|
||||
struct obs_video_info ovi;
|
||||
obs_get_video_info(&ovi);
|
||||
|
||||
int width = ovi.base_width;
|
||||
int height = ovi.base_height;
|
||||
|
||||
double pos_left =
|
||||
attr.namedItem("pos_left").nodeValue().toDouble();
|
||||
double pos_right =
|
||||
attr.namedItem("pos_right").nodeValue().toDouble();
|
||||
double pos_top =
|
||||
attr.namedItem("pos_top").nodeValue().toDouble();
|
||||
double pos_bottom =
|
||||
attr.namedItem("pos_bottom").nodeValue().toDouble();
|
||||
|
||||
bool visible = attr.namedItem("visible").nodeValue() == "1";
|
||||
|
||||
Json out_item = Json::object{
|
||||
{"bounds_type", 2},
|
||||
{"pos", Json::object{{"x", pos_left * width},
|
||||
{"y", pos_top * height}}},
|
||||
{"bounds",
|
||||
Json::object{{"x", (pos_right - pos_left) * width},
|
||||
{"y", (pos_bottom - pos_top) * height}}},
|
||||
{"name", name},
|
||||
{"visible", visible}};
|
||||
|
||||
items.push_back(out_item);
|
||||
|
||||
item = item.nextSibling();
|
||||
}
|
||||
}
|
||||
|
||||
static Json::object parse_scenes(QDomElement &scenes)
|
||||
{
|
||||
Json::array sources = Json::array{};
|
||||
|
||||
QString first = "";
|
||||
|
||||
QDomNode in_scene = scenes.firstChild();
|
||||
while (!in_scene.isNull()) {
|
||||
QString type = in_scene.nodeName();
|
||||
|
||||
if (type == "placement") {
|
||||
QDomNamedNodeMap attr = in_scene.attributes();
|
||||
|
||||
QString name = attr.namedItem("name").nodeValue();
|
||||
QString id = attr.namedItem("id").nodeValue();
|
||||
|
||||
if (first == "")
|
||||
first = name;
|
||||
|
||||
Json out = Json::object{
|
||||
{"id", "scene"},
|
||||
{"name", name.toStdString().c_str()},
|
||||
{"src_id", id.toStdString().c_str()}};
|
||||
|
||||
sources.push_back(out);
|
||||
}
|
||||
in_scene = in_scene.nextSibling();
|
||||
}
|
||||
|
||||
in_scene = scenes.firstChild();
|
||||
for (size_t i = 0, l = sources.size(); i < l; i++) {
|
||||
Json::object source = sources[i].object_items();
|
||||
Json::array items = Json::array{};
|
||||
QDomNode firstChild = in_scene.firstChild();
|
||||
|
||||
parse_items(firstChild, items, sources);
|
||||
|
||||
Json settings = Json::object{{"items", items},
|
||||
{"id_counter", (int)items.size()}};
|
||||
|
||||
source["settings"] = settings;
|
||||
sources[i] = source;
|
||||
|
||||
in_scene = in_scene.nextSibling();
|
||||
}
|
||||
|
||||
return Json::object{{"sources", sources},
|
||||
{"current_scene", first.toStdString()},
|
||||
{"current_program_scene", first.toStdString()}};
|
||||
}
|
||||
|
||||
int XSplitImporter::ImportScenes(const string &path, string &name,
|
||||
json11::Json &res)
|
||||
{
|
||||
if (name == "")
|
||||
name = "XSplit Import";
|
||||
|
||||
BPtr<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;
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -238,6 +238,7 @@ private:
|
|||
QPointer<QWidget> stats;
|
||||
QPointer<QWidget> remux;
|
||||
QPointer<QWidget> extraBrowsers;
|
||||
QPointer<QWidget> importer;
|
||||
|
||||
QPointer<QMenu> startStreamMenu;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
};
|
Loading…
Reference in New Issue