Merge pull request #11622 from PatTheMav/frontend-refactor

UI: Reorganize and refactor entire frontend codebase
This commit is contained in:
Ryan Foster
2025-01-16 14:04:12 -05:00
committed by GitHub
980 changed files with 28570 additions and 26315 deletions

View File

@@ -39,7 +39,7 @@ runs:
uses: ./.github/actions/check-changes
id: checks
with:
checkGlob: 'UI/forms/**/*.ui'
checkGlob: 'frontend/forms/**/*.ui'
- name: Validate XML 💯
if: fromJSON(steps.checks.outputs.hasChangedFiles)
@@ -56,6 +56,6 @@ runs:
if (( ${#CHANGED_FILES[@]} )); then
if [[ '${{ inputs.failCondition }}' == never ]]; then set +e; fi
xmllint \
--schema ${{ github.workspace }}/UI/forms/XML-Schema-Qt5.15.xsd \
--schema ${{ github.workspace }}/frontend/forms/XML-Schema-Qt5.15.xsd \
--noout "${CHANGED_FILES[@]}"
fi

View File

@@ -201,7 +201,7 @@ build() {
rm -rf OBS.app
mkdir OBS.app
ditto UI/${config}/OBS.app OBS.app
ditto frontend/${config}/OBS.app OBS.app
}
}
popd

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
!/cmake
!/deps
!/docs
!/frontend
!/libobs*
!/plugins
!/shared

View File

@@ -29,6 +29,6 @@ add_subdirectory(plugins)
add_subdirectory(test/test-input)
add_subdirectory(UI)
add_subdirectory(frontend)
message_configuration()

View File

@@ -1,636 +0,0 @@
#include <obs-frontend-internal.hpp>
#include <qt-wrappers.hpp>
#include "obs-app.hpp"
#include "window-basic-main.hpp"
#include "window-basic-main-outputs.hpp"
#include <functional>
using namespace std;
Q_DECLARE_METATYPE(OBSScene);
Q_DECLARE_METATYPE(OBSSource);
template<typename T> static T GetOBSRef(QListWidgetItem *item)
{
return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
}
extern volatile bool streaming_active;
extern volatile bool recording_active;
extern volatile bool recording_paused;
extern volatile bool replaybuf_active;
extern volatile bool virtualcam_active;
/* ------------------------------------------------------------------------- */
template<typename T> struct OBSStudioCallback {
T callback;
void *private_data;
inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {}
};
template<typename T>
inline size_t GetCallbackIdx(vector<OBSStudioCallback<T>> &callbacks, T callback, void *private_data)
{
for (size_t i = 0; i < callbacks.size(); i++) {
OBSStudioCallback<T> curCB = callbacks[i];
if (curCB.callback == callback && curCB.private_data == private_data)
return i;
}
return (size_t)-1;
}
struct OBSStudioAPI : obs_frontend_callbacks {
OBSBasic *main;
vector<OBSStudioCallback<obs_frontend_event_cb>> callbacks;
vector<OBSStudioCallback<obs_frontend_save_cb>> saveCallbacks;
vector<OBSStudioCallback<obs_frontend_save_cb>> preloadCallbacks;
inline OBSStudioAPI(OBSBasic *main_) : main(main_) {}
void *obs_frontend_get_main_window(void) override { return (void *)main; }
void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); }
void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); }
void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override
{
for (int i = 0; i < main->ui->scenes->count(); i++) {
QListWidgetItem *item = main->ui->scenes->item(i);
OBSScene scene = GetOBSRef<OBSScene>(item);
obs_source_t *source = obs_scene_get_source(scene);
if (obs_source_get_ref(source) != nullptr)
da_push_back(sources->sources, &source);
}
}
obs_source_t *obs_frontend_get_current_scene(void) override
{
if (main->IsPreviewProgramMode()) {
return obs_weak_source_get_source(main->programScene);
} else {
OBSSource source = main->GetCurrentSceneSource();
return obs_source_get_ref(source);
}
}
void obs_frontend_set_current_scene(obs_source_t *scene) override
{
if (main->IsPreviewProgramMode()) {
QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(),
Q_ARG(OBSSource, OBSSource(scene)));
} else {
QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(),
Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false));
}
}
void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override
{
for (int i = 0; i < main->ui->transitions->count(); i++) {
OBSSource tr = main->ui->transitions->itemData(i).value<OBSSource>();
if (!tr)
continue;
if (obs_source_get_ref(tr) != nullptr)
da_push_back(sources->sources, &tr);
}
}
obs_source_t *obs_frontend_get_current_transition(void) override
{
OBSSource tr = main->GetCurrentTransition();
return obs_source_get_ref(tr);
}
void obs_frontend_set_current_transition(obs_source_t *transition) override
{
QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition)));
}
int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); }
void obs_frontend_set_transition_duration(int duration) override
{
QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration));
}
void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); }
void obs_frontend_set_tbar_position(int position) override
{
QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position));
}
int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); }
void obs_frontend_get_scene_collections(std::vector<std::string> &strings) override
{
for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) {
strings.emplace_back(collectionName);
}
}
char *obs_frontend_get_current_scene_collection(void) override
{
const OBSSceneCollection &currentCollection = main->GetCurrentSceneCollection();
return bstrdup(currentCollection.name.c_str());
}
void obs_frontend_set_current_scene_collection(const char *collection) override
{
QList<QAction *> menuActions = main->ui->sceneCollectionMenu->actions();
QString qstrCollection = QT_UTF8(collection);
for (int i = 0; i < menuActions.count(); i++) {
QAction *action = menuActions[i];
QVariant v = action->property("file_name");
if (v.typeName() != nullptr) {
if (action->text() == qstrCollection) {
action->trigger();
break;
}
}
}
}
bool obs_frontend_add_scene_collection(const char *name) override
{
bool success = false;
QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(),
Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name)));
return success;
}
void obs_frontend_get_profiles(std::vector<std::string> &strings) override
{
const OBSProfileCache &profiles = main->GetProfileCache();
for (auto &[profileName, profile] : profiles) {
strings.emplace_back(profileName);
}
}
char *obs_frontend_get_current_profile(void) override
{
const OBSProfile &profile = main->GetCurrentProfile();
return bstrdup(profile.name.c_str());
}
char *obs_frontend_get_current_profile_path(void) override
{
const OBSProfile &profile = main->GetCurrentProfile();
return bstrdup(profile.path.u8string().c_str());
}
void obs_frontend_set_current_profile(const char *profile) override
{
QList<QAction *> menuActions = main->ui->profileMenu->actions();
QString qstrProfile = QT_UTF8(profile);
for (int i = 0; i < menuActions.count(); i++) {
QAction *action = menuActions[i];
QVariant v = action->property("file_name");
if (v.typeName() != nullptr) {
if (action->text() == qstrProfile) {
action->trigger();
break;
}
}
}
}
void obs_frontend_create_profile(const char *name) override
{
QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name));
}
void obs_frontend_duplicate_profile(const char *name) override
{
QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name));
}
void obs_frontend_delete_profile(const char *profile) override
{
QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile));
}
void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); }
void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); }
bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); }
void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); }
void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); }
bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); }
void obs_frontend_recording_pause(bool pause) override
{
QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording");
}
bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); }
bool obs_frontend_recording_split_file(void) override
{
if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) {
proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput);
uint8_t stack[128];
calldata cd;
calldata_init_fixed(&cd, stack, sizeof(stack));
proc_handler_call(ph, "split_file", &cd);
bool result = calldata_bool(&cd, "split_file_enabled");
return result;
} else {
return false;
}
}
bool obs_frontend_recording_add_chapter(const char *name) override
{
if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused))
return false;
proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput);
calldata cd;
calldata_init(&cd);
calldata_set_string(&cd, "chapter_name", name);
bool result = proc_handler_call(ph, "add_chapter", &cd);
calldata_free(&cd);
return result;
}
void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); }
void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); }
void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); }
bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); }
void *obs_frontend_add_tools_menu_qaction(const char *name) override
{
main->ui->menuTools->setEnabled(true);
return (void *)main->ui->menuTools->addAction(QT_UTF8(name));
}
void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override
{
main->ui->menuTools->setEnabled(true);
auto func = [private_data, callback]() {
callback(private_data);
};
QAction *action = main->ui->menuTools->addAction(QT_UTF8(name));
QObject::connect(action, &QAction::triggered, func);
}
void *obs_frontend_add_dock(void *dock) override
{
QDockWidget *d = reinterpret_cast<QDockWidget *>(dock);
QString name = d->objectName();
if (name.isEmpty() || main->IsDockObjectNameUsed(name)) {
blog(LOG_WARNING, "The object name of the added dock is empty or already used,"
" a temporary one will be set to avoid conflicts");
char *uuid = os_generate_uuid();
name = QT_UTF8(uuid);
bfree(uuid);
name.append("_oldExtraDock");
d->setObjectName(name);
}
return (void *)main->AddDockWidget(d);
}
bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override
{
if (main->IsDockObjectNameUsed(QT_UTF8(id))) {
blog(LOG_WARNING,
"Dock id '%s' already used! "
"Duplicate library?",
id);
return false;
}
OBSDock *dock = new OBSDock(main);
dock->setWidget((QWidget *)widget);
dock->setWindowTitle(QT_UTF8(title));
dock->setObjectName(QT_UTF8(id));
main->AddDockWidget(dock, Qt::RightDockWidgetArea);
dock->setVisible(false);
dock->setFloating(true);
return true;
}
void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); }
bool obs_frontend_add_custom_qdock(const char *id, void *dock) override
{
if (main->IsDockObjectNameUsed(QT_UTF8(id))) {
blog(LOG_WARNING,
"Dock id '%s' already used! "
"Duplicate library?",
id);
return false;
}
QDockWidget *d = reinterpret_cast<QDockWidget *>(dock);
d->setObjectName(QT_UTF8(id));
main->AddCustomDockWidget(d);
return true;
}
void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(callbacks, callback, private_data);
if (idx == (size_t)-1)
callbacks.emplace_back(callback, private_data);
}
void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(callbacks, callback, private_data);
if (idx == (size_t)-1)
return;
callbacks.erase(callbacks.begin() + idx);
}
obs_output_t *obs_frontend_get_streaming_output(void) override
{
auto multitrackVideo = main->outputHandler->multitrackVideo.get();
auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr;
if (mtvOutput)
return mtvOutput;
OBSOutput output = main->outputHandler->streamOutput.Get();
return obs_output_get_ref(output);
}
obs_output_t *obs_frontend_get_recording_output(void) override
{
OBSOutput out = main->outputHandler->fileOutput.Get();
return obs_output_get_ref(out);
}
obs_output_t *obs_frontend_get_replay_buffer_output(void) override
{
OBSOutput out = main->outputHandler->replayBuffer.Get();
return obs_output_get_ref(out);
}
config_t *obs_frontend_get_profile_config(void) override { return main->activeConfiguration; }
config_t *obs_frontend_get_global_config(void) override
{
blog(LOG_WARNING,
"DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead.");
return App()->GetAppConfig();
}
config_t *obs_frontend_get_app_config(void) override { return App()->GetAppConfig(); }
config_t *obs_frontend_get_user_config(void) override { return App()->GetUserConfig(); }
void obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name) override
{
SavedProjectorInfo proj = {
ProjectorType::Preview,
monitor,
geometry ? geometry : "",
name ? name : "",
};
if (type) {
if (astrcmpi(type, "Source") == 0)
proj.type = ProjectorType::Source;
else if (astrcmpi(type, "Scene") == 0)
proj.type = ProjectorType::Scene;
else if (astrcmpi(type, "StudioProgram") == 0)
proj.type = ProjectorType::StudioProgram;
else if (astrcmpi(type, "Multiview") == 0)
proj.type = ProjectorType::Multiview;
}
QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(),
Q_ARG(SavedProjectorInfo *, &proj));
}
void obs_frontend_save(void) override { main->SaveProject(); }
void obs_frontend_defer_save_begin(void) override { QMetaObject::invokeMethod(main, "DeferSaveBegin"); }
void obs_frontend_defer_save_end(void) override { QMetaObject::invokeMethod(main, "DeferSaveEnd"); }
void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data);
if (idx == (size_t)-1)
saveCallbacks.emplace_back(callback, private_data);
}
void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data);
if (idx == (size_t)-1)
return;
saveCallbacks.erase(saveCallbacks.begin() + idx);
}
void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data);
if (idx == (size_t)-1)
preloadCallbacks.emplace_back(callback, private_data);
}
void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override
{
size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data);
if (idx == (size_t)-1)
return;
preloadCallbacks.erase(preloadCallbacks.begin() + idx);
}
void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override
{
App()->PushUITranslation(translate);
}
void obs_frontend_pop_ui_translation(void) override { App()->PopUITranslation(); }
void obs_frontend_set_streaming_service(obs_service_t *service) override { main->SetService(service); }
obs_service_t *obs_frontend_get_streaming_service(void) override { return main->GetService(); }
void obs_frontend_save_streaming_service(void) override { main->SaveService(); }
bool obs_frontend_preview_program_mode_active(void) override { return main->IsPreviewProgramMode(); }
void obs_frontend_set_preview_program_mode(bool enable) override { main->SetPreviewProgramMode(enable); }
void obs_frontend_preview_program_trigger_transition(void) override
{
QMetaObject::invokeMethod(main, "TransitionClicked");
}
bool obs_frontend_preview_enabled(void) override { return main->previewEnabled; }
void obs_frontend_set_preview_enabled(bool enable) override
{
if (main->previewEnabled != enable)
main->EnablePreviewDisplay(enable);
}
obs_source_t *obs_frontend_get_current_preview_scene(void) override
{
if (main->IsPreviewProgramMode()) {
OBSSource source = main->GetCurrentSceneSource();
return obs_source_get_ref(source);
}
return nullptr;
}
void obs_frontend_set_current_preview_scene(obs_source_t *scene) override
{
if (main->IsPreviewProgramMode()) {
QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)),
Q_ARG(bool, false));
}
}
void obs_frontend_take_screenshot(void) override { QMetaObject::invokeMethod(main, "Screenshot"); }
void obs_frontend_take_source_screenshot(obs_source_t *source) override
{
QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source)));
}
obs_output_t *obs_frontend_get_virtualcam_output(void) override
{
OBSOutput output = main->outputHandler->virtualCam.Get();
return obs_output_get_ref(output);
}
void obs_frontend_start_virtualcam(void) override { QMetaObject::invokeMethod(main, "StartVirtualCam"); }
void obs_frontend_stop_virtualcam(void) override { QMetaObject::invokeMethod(main, "StopVirtualCam"); }
bool obs_frontend_virtualcam_active(void) override { return os_atomic_load_bool(&virtualcam_active); }
void obs_frontend_reset_video(void) override { main->ResetVideo(); }
void obs_frontend_open_source_properties(obs_source_t *source) override
{
QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source)));
}
void obs_frontend_open_source_filters(obs_source_t *source) override
{
QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source)));
}
void obs_frontend_open_source_interaction(obs_source_t *source) override
{
QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source)));
}
void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override
{
QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item)));
}
char *obs_frontend_get_current_record_output_path(void) override
{
const char *recordOutputPath = main->GetCurrentOutputPath();
return bstrdup(recordOutputPath);
}
const char *obs_frontend_get_locale_string(const char *string) override { return Str(string); }
bool obs_frontend_is_theme_dark(void) override { return App()->IsThemeDark(); }
char *obs_frontend_get_last_recording(void) override
{
return bstrdup(main->outputHandler->lastRecordingPath.c_str());
}
char *obs_frontend_get_last_screenshot(void) override { return bstrdup(main->lastScreenshot.c_str()); }
char *obs_frontend_get_last_replay(void) override { return bstrdup(main->lastReplay.c_str()); }
void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo,
const char *undo_data, const char *redo_data, bool repeatable) override
{
main->undo_s.add_action(
name, [undo](const std::string &data) { undo(data.c_str()); },
[redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable);
}
void on_load(obs_data_t *settings) override
{
for (size_t i = saveCallbacks.size(); i > 0; i--) {
auto cb = saveCallbacks[i - 1];
cb.callback(settings, false, cb.private_data);
}
}
void on_preload(obs_data_t *settings) override
{
for (size_t i = preloadCallbacks.size(); i > 0; i--) {
auto cb = preloadCallbacks[i - 1];
cb.callback(settings, false, cb.private_data);
}
}
void on_save(obs_data_t *settings) override
{
for (size_t i = saveCallbacks.size(); i > 0; i--) {
auto cb = saveCallbacks[i - 1];
cb.callback(settings, true, cb.private_data);
}
}
void on_event(enum obs_frontend_event event) override
{
if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP &&
event != OBS_FRONTEND_EVENT_EXIT)
return;
for (size_t i = callbacks.size(); i > 0; i--) {
auto cb = callbacks[i - 1];
cb.callback(event, cb.private_data);
}
}
};
obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main)
{
obs_frontend_callbacks *api = new OBSStudioAPI(main);
obs_frontend_set_callbacks_internal(api);
return api;
}

View File

@@ -1,10 +0,0 @@
if(TARGET OBS::browser-panels)
target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE)
target_link_libraries(obs-studio PRIVATE OBS::browser-panels)
target_sources(
obs-studio
PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp
)
endif()

View File

@@ -1,10 +0,0 @@
target_sources(
obs-studio
PRIVATE
importers/classic.cpp
importers/importers.cpp
importers/importers.hpp
importers/sl.cpp
importers/studio.cpp
importers/xsplit.cpp
)

View File

@@ -1,78 +0,0 @@
if(NOT TARGET OBS::properties-view)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view")
endif()
if(NOT TARGET OBS::qt-plain-text-edit)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/plain-text-edit" "${CMAKE_BINARY_DIR}/shared/qt/plain-text-edit")
endif()
if(NOT TARGET OBS::qt-slider-ignorewheel)
add_subdirectory(
"${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel"
"${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel"
)
endif()
if(NOT TARGET OBS::qt-vertical-scroll-area)
add_subdirectory(
"${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area"
"${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area"
)
endif()
target_link_libraries(
obs-studio
PRIVATE OBS::properties-view OBS::qt-plain-text-edit OBS::qt-slider-ignorewheel OBS::qt-vertical-scroll-area
)
target_sources(
obs-studio
PRIVATE
absolute-slider.cpp
absolute-slider.hpp
adv-audio-control.cpp
adv-audio-control.hpp
audio-encoders.cpp
audio-encoders.hpp
balance-slider.hpp
basic-controls.cpp
basic-controls.hpp
clickable-label.hpp
context-bar-controls.cpp
context-bar-controls.hpp
focus-list.cpp
focus-list.hpp
horizontal-scroll-area.cpp
horizontal-scroll-area.hpp
hotkey-edit.cpp
hotkey-edit.hpp
item-widget-helpers.cpp
item-widget-helpers.hpp
log-viewer.cpp
log-viewer.hpp
media-controls.cpp
media-controls.hpp
menu-button.cpp
menu-button.hpp
mute-checkbox.hpp
noncheckable-button.hpp
preview-controls.cpp
preview-controls.hpp
remote-text.cpp
remote-text.hpp
scene-tree.cpp
scene-tree.hpp
screenshot-obj.hpp
source-label.cpp
source-label.hpp
source-tree.cpp
source-tree.hpp
undo-stack-obs.cpp
undo-stack-obs.hpp
url-push-button.cpp
url-push-button.hpp
visibility-item-widget.cpp
visibility-item-widget.hpp
volume-control.cpp
volume-control.hpp
)

View File

@@ -1,65 +0,0 @@
find_package(Qt6 REQUIRED Widgets Network Svg Xml)
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
find_package(Qt6 REQUIRED Gui DBus)
endif()
if(NOT TARGET OBS::qt-wrappers)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers")
endif()
target_link_libraries(
obs-studio
PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers
)
set_target_properties(
obs-studio
PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON
)
set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar)
set(
_qt_sources
forms/AutoConfigFinishPage.ui
forms/AutoConfigStartPage.ui
forms/AutoConfigStartPage.ui
forms/AutoConfigStreamPage.ui
forms/AutoConfigTestPage.ui
forms/AutoConfigVideoPage.ui
forms/ColorSelect.ui
forms/obs.qrc
forms/OBSAbout.ui
forms/OBSAdvAudio.ui
forms/OBSBasic.ui
forms/OBSBasicControls.ui
forms/OBSBasicFilters.ui
forms/OBSBasicInteraction.ui
forms/OBSBasicProperties.ui
forms/OBSBasicSettings.ui
forms/OBSBasicSourceSelect.ui
forms/OBSBasicTransform.ui
forms/OBSBasicVCamConfig.ui
forms/OBSExtraBrowsers.ui
forms/OBSImporter.ui
forms/OBSLogReply.ui
forms/OBSLogViewer.ui
forms/OBSMissingFiles.ui
forms/OBSRemux.ui
forms/OBSUpdate.ui
forms/OBSYoutubeActions.ui
forms/source-toolbar/browser-source-toolbar.ui
forms/source-toolbar/color-source-toolbar.ui
forms/source-toolbar/device-select-toolbar.ui
forms/source-toolbar/game-capture-toolbar.ui
forms/source-toolbar/image-source-toolbar.ui
forms/source-toolbar/media-controls.ui
forms/source-toolbar/text-source-toolbar.ui
)
target_sources(obs-studio PRIVATE ${_qt_sources})
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/forms" PREFIX "UI Files" FILES ${_qt_sources})
unset(_qt_sources)

View File

@@ -1,63 +0,0 @@
target_sources(
obs-studio
PRIVATE
window-basic-about.cpp
window-basic-about.hpp
window-basic-adv-audio.cpp
window-basic-adv-audio.hpp
window-basic-auto-config-test.cpp
window-basic-auto-config.cpp
window-basic-auto-config.hpp
window-basic-filters.cpp
window-basic-filters.hpp
window-basic-interaction.cpp
window-basic-interaction.hpp
window-basic-main-browser.cpp
window-basic-main-dropfiles.cpp
window-basic-main-icons.cpp
window-basic-main-outputs.cpp
window-basic-main-outputs.hpp
window-basic-main-profiles.cpp
window-basic-main-scene-collections.cpp
window-basic-main-screenshot.cpp
window-basic-main-transitions.cpp
window-basic-main.cpp
window-basic-main.hpp
window-basic-preview.cpp
window-basic-preview.hpp
window-basic-properties.cpp
window-basic-properties.hpp
window-basic-settings-a11y.cpp
window-basic-settings-appearance.cpp
window-basic-settings-stream.cpp
window-basic-settings.cpp
window-basic-settings.hpp
window-basic-source-select.cpp
window-basic-source-select.hpp
window-basic-stats.cpp
window-basic-stats.hpp
window-basic-status-bar.cpp
window-basic-status-bar.hpp
window-basic-transform.cpp
window-basic-transform.hpp
window-basic-vcam-config.cpp
window-basic-vcam-config.hpp
window-basic-vcam.hpp
window-dock.cpp
window-dock.hpp
window-importer.cpp
window-importer.hpp
window-log-reply.cpp
window-log-reply.hpp
window-main.hpp
window-missing-files.cpp
window-missing-files.hpp
window-namedialog.cpp
window-namedialog.hpp
window-projector.cpp
window-projector.hpp
window-remux.cpp
window-remux.hpp
window-whats-new.cpp
window-whats-new.hpp
)

View File

@@ -1,707 +0,0 @@
#include "window-basic-main.hpp"
#include "moc_context-bar-controls.cpp"
#include "obs-app.hpp"
#include <qt-wrappers.hpp>
#include <QStandardItemModel>
#include <QColorDialog>
#include <QFontDialog>
#include "ui_browser-source-toolbar.h"
#include "ui_device-select-toolbar.h"
#include "ui_game-capture-toolbar.h"
#include "ui_image-source-toolbar.h"
#include "ui_color-source-toolbar.h"
#include "ui_text-source-toolbar.h"
#ifdef _WIN32
#define get_os_module(win, mac, linux) obs_get_module(win)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win)
#elif __APPLE__
#define get_os_module(win, mac, linux) obs_get_module(mac)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac)
#else
#define get_os_module(win, mac, linux) obs_get_module(linux)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux)
#endif
/* ========================================================================= */
SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source)
: QWidget(parent),
weakSource(OBSGetWeakRef(source)),
props(obs_source_properties(source), obs_properties_destroy)
{
}
void SourceToolbar::SaveOldProperties(obs_source_t *source)
{
oldData = obs_data_create();
OBSDataAutoRelease oldSettings = obs_source_get_settings(source);
obs_data_apply(oldData, oldSettings);
obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source));
}
void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable)
{
if (!oldData) {
blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__);
return;
}
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
OBSSource currentSceneSource = main->GetCurrentSceneSource();
if (!currentSceneSource)
return;
std::string scene_uuid = obs_source_get_uuid(currentSceneSource);
auto undo_redo = [scene_uuid = std::move(scene_uuid), main](const std::string &data) {
OBSDataAutoRelease settings = obs_data_create_from_json(data.c_str());
OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(settings, "undo_suuid"));
obs_source_reset_settings(source, settings);
OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str());
main->SetCurrentScene(scene_source.Get(), true);
main->UpdateContextBar();
};
OBSDataAutoRelease new_settings = obs_data_create();
OBSDataAutoRelease curr_settings = obs_source_get_settings(source);
obs_data_apply(new_settings, curr_settings);
obs_data_set_string(new_settings, "undo_suuid", obs_source_get_uuid(source));
std::string undo_data(obs_data_get_json(oldData));
std::string redo_data(obs_data_get_json(new_settings));
if (undo_data.compare(redo_data) != 0)
main->undo_s.add_action(QTStr("Undo.Properties").arg(obs_source_get_name(source)), undo_redo, undo_redo,
undo_data, redo_data, repeatable);
oldData = nullptr;
}
/* ========================================================================= */
BrowserToolbar::BrowserToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_BrowserSourceToolbar)
{
ui->setupUi(this);
}
BrowserToolbar::~BrowserToolbar() {}
void BrowserToolbar::on_refresh_clicked()
{
OBSSource source = GetSource();
if (!source) {
return;
}
obs_property_t *p = obs_properties_get(props.get(), "refreshnocache");
obs_property_button_clicked(p, source.Get());
}
/* ========================================================================= */
ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_DeviceSelectToolbar)
{
ui->setupUi(this);
}
ComboSelectToolbar::~ComboSelectToolbar() {}
static int FillPropertyCombo(QComboBox *c, obs_property_t *p, const std::string &cur_id, bool is_int = false)
{
size_t count = obs_property_list_item_count(p);
int cur_idx = -1;
for (size_t i = 0; i < count; i++) {
const char *name = obs_property_list_item_name(p, i);
std::string id;
if (is_int) {
id = std::to_string(obs_property_list_item_int(p, i));
} else {
const char *val = obs_property_list_item_string(p, i);
id = val ? val : "";
}
if (cur_id == id)
cur_idx = (int)i;
c->addItem(name, id.c_str());
}
return cur_idx;
}
void UpdateSourceComboToolbarProperties(QComboBox *combo, OBSSource source, obs_properties_t *props,
const char *prop_name, bool is_int)
{
std::string cur_id;
OBSDataAutoRelease settings = obs_source_get_settings(source);
if (is_int) {
cur_id = std::to_string(obs_data_get_int(settings, prop_name));
} else {
cur_id = obs_data_get_string(settings, prop_name);
}
combo->blockSignals(true);
obs_property_t *p = obs_properties_get(props, prop_name);
int cur_idx = FillPropertyCombo(combo, p, cur_id, is_int);
if (cur_idx == -1 || obs_property_list_item_disabled(p, cur_idx)) {
if (cur_idx == -1) {
combo->insertItem(0, QTStr("Basic.Settings.Audio.UnknownAudioDevice"));
cur_idx = 0;
}
SetComboItemEnabled(combo, cur_idx, false);
}
combo->setCurrentIndex(cur_idx);
combo->blockSignals(false);
}
void ComboSelectToolbar::Init()
{
OBSSource source = GetSource();
if (!source) {
return;
}
UpdateSourceComboToolbarProperties(ui->device, source, props.get(), prop_name, is_int);
}
void UpdateSourceComboToolbarValue(QComboBox *combo, OBSSource source, int idx, const char *prop_name, bool is_int)
{
QString id = combo->itemData(idx).toString();
OBSDataAutoRelease settings = obs_data_create();
if (is_int) {
obs_data_set_int(settings, prop_name, id.toInt());
} else {
obs_data_set_string(settings, prop_name, QT_TO_UTF8(id));
}
obs_source_update(source, settings);
}
void ComboSelectToolbar::on_device_currentIndexChanged(int idx)
{
OBSSource source = GetSource();
if (idx == -1 || !source) {
return;
}
SaveOldProperties(source);
UpdateSourceComboToolbarValue(ui->device, source, idx, prop_name, is_int);
SetUndoProperties(source);
}
AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {}
void AudioCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio");
if (!mod)
return;
const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device");
ui->deviceLabel->setText(device_str);
prop_name = "device_id";
ComboSelectToolbar::Init();
}
WindowCaptureToolbar::WindowCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {}
void WindowCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture");
if (!mod)
return;
const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window");
ui->deviceLabel->setText(device_str);
#if !defined(_WIN32) && !defined(__APPLE__) //linux
prop_name = "capture_window";
#else
prop_name = "window";
#endif
#ifdef __APPLE__
is_int = true;
#endif
ComboSelectToolbar::Init();
}
ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source)
: ComboSelectToolbar(parent, source)
{
}
void ApplicationAudioCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = obs_get_module("win-wasapi");
const char *device_str = obs_module_get_locale_text(mod, "Window");
ui->deviceLabel->setText(device_str);
prop_name = "window";
ComboSelectToolbar::Init();
}
DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {}
void DisplayCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture");
if (!mod)
return;
const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen");
ui->deviceLabel->setText(device_str);
#ifdef _WIN32
prop_name = "monitor_id";
#elif __APPLE__
prop_name = "display_uuid";
#else
is_int = true;
prop_name = "screen";
#endif
ComboSelectToolbar::Init();
}
/* ========================================================================= */
DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source)
: QWidget(parent),
weakSource(OBSGetWeakRef(source)),
ui(new Ui_DeviceSelectToolbar)
{
ui->setupUi(this);
delete ui->deviceLabel;
delete ui->device;
ui->deviceLabel = nullptr;
ui->device = nullptr;
OBSDataAutoRelease settings = obs_source_get_settings(source);
active = obs_data_get_bool(settings, "active");
obs_module_t *mod = obs_get_module("win-dshow");
if (!mod)
return;
activateText = obs_module_get_locale_text(mod, "Activate");
deactivateText = obs_module_get_locale_text(mod, "Deactivate");
ui->activateButton->setText(active ? deactivateText : activateText);
}
DeviceCaptureToolbar::~DeviceCaptureToolbar() {}
void DeviceCaptureToolbar::on_activateButton_clicked()
{
OBSSource source = OBSGetStrongRef(weakSource);
if (!source) {
return;
}
OBSDataAutoRelease settings = obs_source_get_settings(source);
bool now_active = obs_data_get_bool(settings, "active");
bool desyncedSetting = now_active != active;
active = !active;
const char *text = active ? deactivateText : activateText;
ui->activateButton->setText(text);
if (desyncedSetting) {
return;
}
calldata_t cd = {};
calldata_set_bool(&cd, "active", active);
proc_handler_t *ph = obs_source_get_proc_handler(source);
proc_handler_call(ph, "activate", &cd);
calldata_free(&cd);
}
/* ========================================================================= */
GameCaptureToolbar::GameCaptureToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_GameCaptureToolbar)
{
obs_property_t *p;
int cur_idx;
ui->setupUi(this);
obs_module_t *mod = obs_get_module("win-capture");
if (!mod)
return;
ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode"));
ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window"));
OBSDataAutoRelease settings = obs_source_get_settings(source);
std::string cur_mode = obs_data_get_string(settings, "capture_mode");
std::string cur_window = obs_data_get_string(settings, "window");
ui->mode->blockSignals(true);
p = obs_properties_get(props.get(), "capture_mode");
cur_idx = FillPropertyCombo(ui->mode, p, cur_mode);
ui->mode->setCurrentIndex(cur_idx);
ui->mode->blockSignals(false);
ui->window->blockSignals(true);
p = obs_properties_get(props.get(), "window");
cur_idx = FillPropertyCombo(ui->window, p, cur_window);
ui->window->setCurrentIndex(cur_idx);
ui->window->blockSignals(false);
if (cur_idx != -1 && obs_property_list_item_disabled(p, cur_idx)) {
SetComboItemEnabled(ui->window, cur_idx, false);
}
UpdateWindowVisibility();
}
GameCaptureToolbar::~GameCaptureToolbar() {}
void GameCaptureToolbar::UpdateWindowVisibility()
{
QString mode = ui->mode->currentData().toString();
bool is_window = (mode == "window");
ui->windowLabel->setVisible(is_window);
ui->window->setVisible(is_window);
}
void GameCaptureToolbar::on_mode_currentIndexChanged(int idx)
{
OBSSource source = GetSource();
if (idx == -1 || !source) {
return;
}
QString id = ui->mode->itemData(idx).toString();
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id));
obs_source_update(source, settings);
SetUndoProperties(source);
UpdateWindowVisibility();
}
void GameCaptureToolbar::on_window_currentIndexChanged(int idx)
{
OBSSource source = GetSource();
if (idx == -1 || !source) {
return;
}
QString id = ui->window->itemData(idx).toString();
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "window", QT_TO_UTF8(id));
obs_source_update(source, settings);
SetUndoProperties(source);
}
/* ========================================================================= */
ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_ImageSourceToolbar)
{
ui->setupUi(this);
obs_module_t *mod = obs_get_module("image-source");
ui->pathLabel->setText(obs_module_get_locale_text(mod, "File"));
OBSDataAutoRelease settings = obs_source_get_settings(source);
std::string file = obs_data_get_string(settings, "file");
ui->path->setText(file.c_str());
}
ImageSourceToolbar::~ImageSourceToolbar() {}
void ImageSourceToolbar::on_browse_clicked()
{
OBSSource source = GetSource();
if (!source) {
return;
}
obs_property_t *p = obs_properties_get(props.get(), "file");
const char *desc = obs_property_description(p);
const char *filter = obs_property_path_filter(p);
const char *default_path = obs_property_path_default_path(p);
QString startDir = ui->path->text();
if (startDir.isEmpty())
startDir = default_path;
QString path = OpenFile(this, desc, startDir, filter);
if (path.isEmpty()) {
return;
}
ui->path->setText(path);
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "file", QT_TO_UTF8(path));
obs_source_update(source, settings);
SetUndoProperties(source);
}
/* ========================================================================= */
static inline QColor color_from_int(long long val)
{
return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff);
}
static inline long long color_to_int(QColor color)
{
auto shift = [&](unsigned val, int shift) {
return ((val & 0xff) << shift);
};
return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24);
}
ColorSourceToolbar::ColorSourceToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_ColorSourceToolbar)
{
ui->setupUi(this);
OBSDataAutoRelease settings = obs_source_get_settings(source);
unsigned int val = (unsigned int)obs_data_get_int(settings, "color");
color = color_from_int(val);
UpdateColor();
}
ColorSourceToolbar::~ColorSourceToolbar() {}
void ColorSourceToolbar::UpdateColor()
{
QPalette palette = QPalette(color);
ui->color->setFrameStyle(QFrame::Sunken | QFrame::Panel);
ui->color->setText(color.name(QColor::HexRgb));
ui->color->setPalette(palette);
ui->color->setStyleSheet(QString("background-color :%1; color: %2;")
.arg(palette.color(QPalette::Window).name(QColor::HexRgb))
.arg(palette.color(QPalette::WindowText).name(QColor::HexRgb)));
ui->color->setAutoFillBackground(true);
ui->color->setAlignment(Qt::AlignCenter);
}
void ColorSourceToolbar::on_choose_clicked()
{
OBSSource source = GetSource();
if (!source) {
return;
}
obs_property_t *p = obs_properties_get(props.get(), "color");
const char *desc = obs_property_description(p);
QColorDialog::ColorDialogOptions options;
options |= QColorDialog::ShowAlphaChannel;
#ifdef __linux__
// TODO: Revisit hang on Ubuntu with native dialog
options |= QColorDialog::DontUseNativeDialog;
#endif
QColor newColor = QColorDialog::getColor(color, this, desc, options);
if (!newColor.isValid()) {
return;
}
color = newColor;
UpdateColor();
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_int(settings, "color", color_to_int(color));
obs_source_update(source, settings);
SetUndoProperties(source);
}
/* ========================================================================= */
extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false);
TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source)
: SourceToolbar(parent, source),
ui(new Ui_TextSourceToolbar)
{
ui->setupUi(this);
OBSDataAutoRelease settings = obs_source_get_settings(source);
const char *id = obs_source_get_unversioned_id(source);
bool ft2 = strcmp(id, "text_ft2_source") == 0;
bool read_from_file = obs_data_get_bool(settings, ft2 ? "from_file" : "read_from_file");
OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font");
MakeQFont(font_obj, font);
// Use "color1" if it's a freetype source and "color" elsewise
unsigned int val = (unsigned int)obs_data_get_int(
settings, (strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0) ? "color1" : "color");
color = color_from_int(val);
const char *text = obs_data_get_string(settings, "text");
bool single_line = !read_from_file && (!text || (strchr(text, '\n') == nullptr));
ui->emptySpace->setVisible(!single_line);
ui->text->setVisible(single_line);
if (single_line)
ui->text->setText(text);
}
TextSourceToolbar::~TextSourceToolbar() {}
void TextSourceToolbar::on_selectFont_clicked()
{
OBSSource source = GetSource();
if (!source) {
return;
}
QFontDialog::FontDialogOptions options;
uint32_t flags;
bool success;
#ifndef _WIN32
options = QFontDialog::DontUseNativeDialog;
#endif
font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"),
options);
if (!success) {
return;
}
OBSDataAutoRelease font_obj = obs_data_create();
obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family()));
obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName()));
obs_data_set_int(font_obj, "size", font.pointSize());
flags = font.bold() ? OBS_FONT_BOLD : 0;
flags |= font.italic() ? OBS_FONT_ITALIC : 0;
flags |= font.underline() ? OBS_FONT_UNDERLINE : 0;
flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0;
obs_data_set_int(font_obj, "flags", flags);
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_obj(settings, "font", font_obj);
obs_source_update(source, settings);
SetUndoProperties(source);
}
void TextSourceToolbar::on_selectColor_clicked()
{
OBSSource source = GetSource();
if (!source) {
return;
}
bool freetype = strncmp(obs_source_get_id(source), "text_ft2_source", 15) == 0;
obs_property_t *p = obs_properties_get(props.get(), freetype ? "color1" : "color");
const char *desc = obs_property_description(p);
QColorDialog::ColorDialogOptions options;
options |= QColorDialog::ShowAlphaChannel;
#ifdef __linux__
// TODO: Revisit hang on Ubuntu with native dialog
options |= QColorDialog::DontUseNativeDialog;
#endif
QColor newColor = QColorDialog::getColor(color, this, desc, options);
if (!newColor.isValid()) {
return;
}
color = newColor;
SaveOldProperties(source);
OBSDataAutoRelease settings = obs_data_create();
if (freetype) {
obs_data_set_int(settings, "color1", color_to_int(color));
obs_data_set_int(settings, "color2", color_to_int(color));
} else {
obs_data_set_int(settings, "color", color_to_int(color));
}
obs_source_update(source, settings);
SetUndoProperties(source);
}
void TextSourceToolbar::on_text_textChanged()
{
OBSSource source = GetSource();
if (!source) {
return;
}
std::string newText = QT_TO_UTF8(ui->text->text());
OBSDataAutoRelease settings = obs_source_get_settings(source);
if (newText == obs_data_get_string(settings, "text")) {
return;
}
SaveOldProperties(source);
obs_data_set_string(settings, "text", newText.c_str());
obs_source_update(source, nullptr);
SetUndoProperties(source, true);
}

View File

@@ -1,178 +0,0 @@
#pragma once
#include <memory>
#include <obs.hpp>
#include <QWidget>
class Ui_BrowserSourceToolbar;
class Ui_DeviceSelectToolbar;
class Ui_GameCaptureToolbar;
class Ui_ImageSourceToolbar;
class Ui_ColorSourceToolbar;
class Ui_TextSourceToolbar;
class SourceToolbar : public QWidget {
Q_OBJECT
OBSWeakSource weakSource;
protected:
using properties_delete_t = decltype(&obs_properties_destroy);
using properties_t = std::unique_ptr<obs_properties_t, properties_delete_t>;
properties_t props;
OBSDataAutoRelease oldData;
void SaveOldProperties(obs_source_t *source);
void SetUndoProperties(obs_source_t *source, bool repeatable = false);
public:
SourceToolbar(QWidget *parent, OBSSource source);
OBSSource GetSource() { return OBSGetStrongRef(weakSource); }
public slots:
virtual void Update() {}
};
class BrowserToolbar : public SourceToolbar {
Q_OBJECT
std::unique_ptr<Ui_BrowserSourceToolbar> ui;
public:
BrowserToolbar(QWidget *parent, OBSSource source);
~BrowserToolbar();
public slots:
void on_refresh_clicked();
};
class ComboSelectToolbar : public SourceToolbar {
Q_OBJECT
protected:
std::unique_ptr<Ui_DeviceSelectToolbar> ui;
const char *prop_name;
bool is_int = false;
public:
ComboSelectToolbar(QWidget *parent, OBSSource source);
~ComboSelectToolbar();
virtual void Init();
public slots:
void on_device_currentIndexChanged(int idx);
};
class AudioCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
AudioCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};
class WindowCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
WindowCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};
class ApplicationAudioCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};
class DisplayCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
DisplayCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};
class DeviceCaptureToolbar : public QWidget {
Q_OBJECT
OBSWeakSource weakSource;
std::unique_ptr<Ui_DeviceSelectToolbar> ui;
const char *activateText;
const char *deactivateText;
bool active;
public:
DeviceCaptureToolbar(QWidget *parent, OBSSource source);
~DeviceCaptureToolbar();
public slots:
void on_activateButton_clicked();
};
class GameCaptureToolbar : public SourceToolbar {
Q_OBJECT
std::unique_ptr<Ui_GameCaptureToolbar> ui;
void UpdateWindowVisibility();
public:
GameCaptureToolbar(QWidget *parent, OBSSource source);
~GameCaptureToolbar();
public slots:
void on_mode_currentIndexChanged(int idx);
void on_window_currentIndexChanged(int idx);
};
class ImageSourceToolbar : public SourceToolbar {
Q_OBJECT
std::unique_ptr<Ui_ImageSourceToolbar> ui;
public:
ImageSourceToolbar(QWidget *parent, OBSSource source);
~ImageSourceToolbar();
public slots:
void on_browse_clicked();
};
class ColorSourceToolbar : public SourceToolbar {
Q_OBJECT
std::unique_ptr<Ui_ColorSourceToolbar> ui;
QColor color;
void UpdateColor();
public:
ColorSourceToolbar(QWidget *parent, OBSSource source);
~ColorSourceToolbar();
public slots:
void on_choose_clicked();
};
class TextSourceToolbar : public SourceToolbar {
Q_OBJECT
std::unique_ptr<Ui_TextSourceToolbar> ui;
QFont font;
QColor color;
public:
TextSourceToolbar(QWidget *parent, OBSSource source);
~TextSourceToolbar();
public slots:
void on_selectFont_clicked();
void on_selectColor_clicked();
void on_text_textChanged();
};

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<description>OBS Studio</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="asInvoker"
uiAccess="false"
/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -1,26 +0,0 @@
IDI_ICON1 ICON DISCARDABLE "../cmake/bundle/windows/obs-studio.ico"
1 VERSIONINFO
FILEVERSION ${UI_VERSION_MAJOR},${UI_VERSION_MINOR},${UI_VERSION_PATCH},0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "OBS"
VALUE "FileDescription", "OBS Studio"
VALUE "FileVersion", "${UI_VERSION}"
VALUE "InternalName", "obs"
VALUE "OriginalFilename", "obs"
VALUE "ProductName", "OBS Studio"
VALUE "ProductVersion", "${UI_VERSION}"
VALUE "Comments", "Free and open source software for video recording and live streaming"
VALUE "LegalCopyright", "(C) Lain Bailey"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 0x04B0
END
END

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,202 +0,0 @@
#pragma once
#include <QList>
#include <QVector>
#include <QPointer>
#include <QListView>
#include <QCheckBox>
#include <QStaticText>
#include <QSvgRenderer>
#include <QAbstractListModel>
#include <QStyledItemDelegate>
#include <obs.hpp>
#include <obs-frontend-api.h>
class QLabel;
class OBSSourceLabel;
class QCheckBox;
class QLineEdit;
class SourceTree;
class QSpacerItem;
class QHBoxLayout;
class VisibilityItemWidget;
class SourceTreeItem : public QFrame {
Q_OBJECT
friend class SourceTree;
friend class SourceTreeModel;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
virtual bool eventFilter(QObject *object, QEvent *event) override;
void Update(bool force);
enum class Type {
Unknown,
Item,
Group,
SubItem,
};
void DisconnectSignals();
void ReconnectSignals();
Type type = Type::Unknown;
public:
explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem);
bool IsEditing();
private:
QSpacerItem *spacer = nullptr;
QCheckBox *expand = nullptr;
QLabel *iconLabel = nullptr;
QCheckBox *vis = nullptr;
QCheckBox *lock = nullptr;
QHBoxLayout *boxLayout = nullptr;
OBSSourceLabel *label = nullptr;
QLineEdit *editor = nullptr;
std::string newName;
SourceTree *tree;
OBSSceneItem sceneitem;
std::vector<OBSSignal> sigs;
virtual void paintEvent(QPaintEvent *event) override;
void ExitEditModeInternal(bool save);
private slots:
void Clear();
void EnterEditMode();
void ExitEditMode(bool save);
void VisibilityChanged(bool visible);
void LockedChanged(bool locked);
void ExpandClicked(bool checked);
void Select();
void Deselect();
};
class SourceTreeModel : public QAbstractListModel {
Q_OBJECT
friend class SourceTree;
friend class SourceTreeItem;
SourceTree *st;
QVector<OBSSceneItem> items;
bool hasGroups = false;
static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr);
void Clear();
void SceneChanged();
void ReorderItems();
void Add(obs_sceneitem_t *item);
void Remove(obs_sceneitem_t *item);
OBSSceneItem Get(int idx);
QString GetNewGroupName();
void AddGroup();
void GroupSelectedItems(QModelIndexList &indices);
void UngroupSelectedGroups(QModelIndexList &indices);
void ExpandGroup(obs_sceneitem_t *item);
void CollapseGroup(obs_sceneitem_t *item);
void UpdateGroupState(bool update);
public:
explicit SourceTreeModel(SourceTree *st);
virtual int rowCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
virtual Qt::DropActions supportedDropActions() const override;
};
class SourceTree : public QListView {
Q_OBJECT
bool ignoreReorder = false;
friend class SourceTreeModel;
friend class SourceTreeItem;
bool textPrepared = false;
QStaticText textNoSources;
QSvgRenderer iconNoSources;
OBSData undoSceneData;
bool iconsVisible = true;
void UpdateNoSourcesMessage();
void ResetWidgets();
void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item);
void UpdateWidgets(bool force = false);
inline SourceTreeModel *GetStm() const { return reinterpret_cast<SourceTreeModel *>(model()); }
public:
inline SourceTreeItem *GetItemWidget(int idx)
{
QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0));
return reinterpret_cast<SourceTreeItem *>(widget);
}
explicit SourceTree(QWidget *parent = nullptr);
inline bool IgnoreReorder() const { return ignoreReorder; }
inline void Clear() { GetStm()->Clear(); }
inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); }
inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); }
inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); }
void SelectItem(obs_sceneitem_t *sceneitem, bool select);
bool MultipleBaseSelected() const;
bool GroupsSelected() const;
bool GroupedItemsSelected() const;
void UpdateIcons();
void SetIconsVisible(bool visible);
public slots:
inline void ReorderItems() { GetStm()->ReorderItems(); }
inline void RefreshItems() { GetStm()->SceneChanged(); }
void Remove(OBSSceneItem item, OBSScene scene);
void GroupSelectedItems();
void UngroupSelectedGroups();
void AddGroup();
bool Edit(int idx);
void NewGroupEdit(int idx);
protected:
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
virtual void dropEvent(QDropEvent *event) override;
virtual void paintEvent(QPaintEvent *event) override;
virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override;
};
class SourceTreeDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
SourceTreeDelegate(QObject *parent);
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

View File

@@ -1,51 +0,0 @@
#ifndef MAC_UPDATER_H
#define MAC_UPDATER_H
#include <string>
#include <QThread>
#include <QString>
#include <QObject>
class QAction;
class MacUpdateThread : public QThread {
Q_OBJECT
bool manualUpdate;
virtual void run() override;
void info(const QString &title, const QString &text);
signals:
void Result(const QString &branch, bool manual);
private slots:
void infoMsg(const QString &title, const QString &text);
public:
MacUpdateThread(bool manual) : manualUpdate(manual) {}
};
#ifdef __OBJC__
@class OBSUpdateDelegate;
#endif
class OBSSparkle : public QObject {
Q_OBJECT
public:
OBSSparkle(const char *branch, QAction *checkForUpdatesAction);
void setBranch(const char *branch);
void checkForUpdates(bool manualCheck);
private:
#ifdef __OBJC__
OBSUpdateDelegate *updaterDelegate;
#else
void *updaterDelegate;
#endif
};
#endif

View File

@@ -1,79 +0,0 @@
#include "mac-update.hpp"
#include <qaction.h>
#import <Cocoa/Cocoa.h>
#import <Sparkle/Sparkle.h>
@interface OBSUpdateDelegate : NSObject <SPUUpdaterDelegate> {
}
@property (copy) NSString *branch;
@property (nonatomic) SPUStandardUpdaterController *updaterController;
@end
@implementation OBSUpdateDelegate {
}
@synthesize branch;
- (nonnull NSSet<NSString *> *)allowedChannelsForUpdater:(nonnull SPUUpdater *)updater
{
return [NSSet setWithObject:branch];
}
- (void)observeCanCheckForUpdatesWithAction:(QAction *)action
{
[_updaterController.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))
options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew)
context:(void *) action];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context
{
if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) {
QAction *menuAction = (QAction *) context;
menuAction->setEnabled(_updaterController.updater.canCheckForUpdates);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
@autoreleasepool {
[_updaterController.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))];
}
}
@end
OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction)
{
@autoreleasepool {
updaterDelegate = [[OBSUpdateDelegate alloc] init];
updaterDelegate.branch = [NSString stringWithUTF8String:branch];
updaterDelegate.updaterController =
[[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updaterDelegate
userDriverDelegate:nil];
[updaterDelegate observeCanCheckForUpdatesWithAction:checkForUpdatesAction];
}
}
void OBSSparkle::setBranch(const char *branch)
{
updaterDelegate.branch = [NSString stringWithUTF8String:branch];
}
void OBSSparkle::checkForUpdates(bool manualCheck)
{
@autoreleasepool {
if (manualCheck) {
[updaterDelegate.updaterController checkForUpdates:nil];
} else {
[updaterDelegate.updaterController.updater checkForUpdatesInBackground];
}
}
}

View File

@@ -1,133 +0,0 @@
#include "moc_visibility-item-widget.cpp"
#include "obs-app.hpp"
#include "source-label.hpp"
#include <qt-wrappers.hpp>
#include <QListWidget>
#include <QLineEdit>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QLabel>
#include <QKeyEvent>
#include <QCheckBox>
VisibilityItemWidget::VisibilityItemWidget(obs_source_t *source_)
: source(source_),
enabledSignal(obs_source_get_signal_handler(source), "enable", OBSSourceEnabled, this)
{
bool enabled = obs_source_enabled(source);
vis = new QCheckBox();
vis->setProperty("class", "checkbox-icon indicator-visibility");
vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
vis->setChecked(enabled);
label = new OBSSourceLabel(source);
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
QHBoxLayout *itemLayout = new QHBoxLayout();
itemLayout->addWidget(vis);
itemLayout->addWidget(label);
itemLayout->setContentsMargins(0, 0, 0, 0);
setLayout(itemLayout);
connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); });
}
void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data)
{
VisibilityItemWidget *window = reinterpret_cast<VisibilityItemWidget *>(param);
bool enabled = calldata_bool(data, "enabled");
QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled));
}
void VisibilityItemWidget::SourceEnabled(bool enabled)
{
if (vis->isChecked() != enabled)
vis->setChecked(enabled);
}
void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_)
{
/* Do not update unless the state has actually changed */
if (active_ == active && selected_ == selected)
return;
QPalette pal = vis->palette();
pal.setColor(QPalette::WindowText, color);
vis->setPalette(pal);
label->setStyleSheet(QString("color: %1;").arg(color.name()));
active = active_;
selected = selected_;
}
VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyledItemDelegate::paint(painter, option, index);
QObject *parentObj = parent();
QListWidget *list = qobject_cast<QListWidget *>(parentObj);
if (!list)
return;
QListWidgetItem *item = list->item(index.row());
VisibilityItemWidget *widget = qobject_cast<VisibilityItemWidget *>(list->itemWidget(item));
if (!widget)
return;
bool selected = option.state.testFlag(QStyle::State_Selected);
bool active = option.state.testFlag(QStyle::State_Active);
QPalette palette = list->palette();
#if defined(_WIN32) || defined(__APPLE__)
QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Inactive;
#else
QPalette::ColorGroup group = QPalette::Active;
#endif
#ifdef _WIN32
QPalette::ColorRole highlightRole = QPalette::WindowText;
#else
QPalette::ColorRole highlightRole = QPalette::HighlightedText;
#endif
QPalette::ColorRole role;
if (selected && active)
role = highlightRole;
else
role = QPalette::WindowText;
widget->SetColor(palette.color(group, role), active, selected);
}
bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event)
{
QWidget *editor = qobject_cast<QWidget *>(object);
if (!editor)
return false;
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
return false;
}
}
return QStyledItemDelegate::eventFilter(object, event);
}
void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source)
{
VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source);
list->setItemWidget(item, baseWidget);
}

View File

@@ -1,288 +0,0 @@
#pragma once
#include <QWizard>
#include <QPointer>
#include <QFormLayout>
#include <QWizardPage>
#include <condition_variable>
#include <utility>
#include <thread>
#include <memory>
#include <vector>
#include <string>
#include <mutex>
#include <optional>
class Ui_AutoConfigStartPage;
class Ui_AutoConfigVideoPage;
class Ui_AutoConfigStreamPage;
class Ui_AutoConfigTestPage;
class AutoConfigStreamPage;
class Auth;
class AutoConfig : public QWizard {
Q_OBJECT
friend class AutoConfigStartPage;
friend class AutoConfigVideoPage;
friend class AutoConfigStreamPage;
friend class AutoConfigTestPage;
enum class Type {
Invalid,
Streaming,
Recording,
VirtualCam,
};
enum class Service {
Twitch,
YouTube,
AmazonIVS,
Other,
};
enum class Encoder {
x264,
NVENC,
QSV,
AMD,
Apple,
Stream,
};
enum class Quality {
Stream,
High,
};
enum class FPSType : int {
PreferHighFPS,
PreferHighRes,
UseCurrent,
fps30,
fps60,
};
struct StreamServer {
std::string name;
std::string address;
};
static inline const char *GetEncoderId(Encoder enc);
AutoConfigStreamPage *streamPage = nullptr;
Service service = Service::Other;
Quality recordingQuality = Quality::Stream;
Encoder recordingEncoder = Encoder::Stream;
Encoder streamingEncoder = Encoder::x264;
Type type = Type::Streaming;
FPSType fpsType = FPSType::PreferHighFPS;
int idealBitrate = 2500;
struct {
std::optional<int> targetBitrate;
std::optional<int> bitrate;
bool testSuccessful = false;
} multitrackVideo;
int baseResolutionCX = 1920;
int baseResolutionCY = 1080;
int idealResolutionCX = 1280;
int idealResolutionCY = 720;
int idealFPSNum = 60;
int idealFPSDen = 1;
std::string serviceName;
std::string serverName;
std::string server;
std::vector<StreamServer> serviceConfigServers;
std::string key;
bool hardwareEncodingAvailable = false;
bool nvencAvailable = false;
bool qsvAvailable = false;
bool vceAvailable = false;
bool appleAvailable = false;
int startingBitrate = 2500;
bool customServer = false;
bool bandwidthTest = false;
bool testMultitrackVideo = false;
bool testRegions = true;
bool twitchAuto = false;
bool amazonIVSAuto = false;
bool regionUS = true;
bool regionEU = true;
bool regionAsia = true;
bool regionOther = true;
bool preferHighFPS = false;
bool preferHardware = false;
int specificFPSNum = 0;
int specificFPSDen = 0;
void TestHardwareEncoding();
bool CanTestServer(const char *server);
virtual void done(int result) override;
void SaveStreamSettings();
void SaveSettings();
public:
AutoConfig(QWidget *parent);
~AutoConfig();
enum Page {
StartPage,
VideoPage,
StreamPage,
TestPage,
};
};
class AutoConfigStartPage : public QWizardPage {
Q_OBJECT
friend class AutoConfig;
std::unique_ptr<Ui_AutoConfigStartPage> ui;
public:
AutoConfigStartPage(QWidget *parent = nullptr);
~AutoConfigStartPage();
virtual int nextId() const override;
public slots:
void on_prioritizeStreaming_clicked();
void on_prioritizeRecording_clicked();
void PrioritizeVCam();
};
class AutoConfigVideoPage : public QWizardPage {
Q_OBJECT
friend class AutoConfig;
std::unique_ptr<Ui_AutoConfigVideoPage> ui;
public:
AutoConfigVideoPage(QWidget *parent = nullptr);
~AutoConfigVideoPage();
virtual int nextId() const override;
virtual bool validatePage() override;
};
class AutoConfigStreamPage : public QWizardPage {
Q_OBJECT
friend class AutoConfig;
enum class Section : int {
Connect,
StreamKey,
};
std::shared_ptr<Auth> auth;
std::unique_ptr<Ui_AutoConfigStreamPage> ui;
QString lastService;
bool ready = false;
void LoadServices(bool showAll);
inline bool IsCustomService() const;
public:
AutoConfigStreamPage(QWidget *parent = nullptr);
~AutoConfigStreamPage();
virtual bool isComplete() const override;
virtual int nextId() const override;
virtual bool validatePage() override;
void OnAuthConnected();
void OnOAuthStreamKeyConnected();
public slots:
void on_show_clicked();
void on_connectAccount_clicked();
void on_disconnectAccount_clicked();
void on_useStreamKey_clicked();
void on_preferHardware_clicked();
void ServiceChanged();
void UpdateKeyLink();
void UpdateMoreInfoLink();
void UpdateServerList();
void UpdateCompleted();
void reset_service_ui_fields(std::string &service);
};
class AutoConfigTestPage : public QWizardPage {
Q_OBJECT
friend class AutoConfig;
QPointer<QFormLayout> results;
std::unique_ptr<Ui_AutoConfigTestPage> ui;
std::thread testThread;
std::condition_variable cv;
std::mutex m;
bool cancel = false;
bool started = false;
enum class Stage {
Starting,
BandwidthTest,
StreamEncoder,
RecordingEncoder,
Finished,
};
Stage stage = Stage::Starting;
bool softwareTested = false;
void StartBandwidthStage();
void StartStreamEncoderStage();
void StartRecordingEncoderStage();
void FindIdealHardwareResolution();
bool TestSoftwareEncoding();
void TestBandwidthThread();
void TestStreamEncoderThread();
void TestRecordingEncoderThread();
void FinalizeResults();
struct ServerInfo {
std::string name;
std::string address;
int bitrate = 0;
int ms = -1;
inline ServerInfo() {}
inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {}
};
void GetServers(std::vector<ServerInfo> &servers);
public:
AutoConfigTestPage(QWidget *parent = nullptr);
~AutoConfigTestPage();
virtual void initializePage() override;
virtual void cleanupPage() override;
virtual bool isComplete() const override;
virtual int nextId() const override;
public slots:
void NextStage();
void UpdateMessage(QString message);
void Failure(QString message);
void Progress(int percentage);
};

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,720 +0,0 @@
/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
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 <filesystem>
#include <string>
#include <obs.hpp>
#include <util/util.hpp>
#include <QMessageBox>
#include <QVariant>
#include <QFileDialog>
#include <QStandardPaths>
#include <qt-wrappers.hpp>
#include "item-widget-helpers.hpp"
#include "window-basic-main.hpp"
#include "window-importer.hpp"
#include "window-namedialog.hpp"
// MARK: Constant Expressions
constexpr std::string_view OBSSceneCollectionPath = "/obs-studio/basic/scenes/";
// MARK: - Anonymous Namespace
namespace {
QList<QString> sortedSceneCollections{};
void updateSortedSceneCollections(const OBSSceneCollectionCache &collections)
{
const QLocale locale = QLocale::system();
QList<QString> newList{};
for (auto [collectionName, _] : collections) {
QString entry = QString::fromStdString(collectionName);
newList.append(entry);
}
std::sort(newList.begin(), newList.end(), [&locale](const QString &lhs, const QString &rhs) -> bool {
int result = QString::localeAwareCompare(locale.toLower(lhs), locale.toLower(rhs));
return (result < 0);
});
sortedSceneCollections.swap(newList);
}
void cleanBackupCollision(const OBSSceneCollection &collection)
{
std::filesystem::path backupFilePath = collection.collectionFile;
backupFilePath.replace_extension(".json.bak");
if (std::filesystem::exists(backupFilePath)) {
try {
std::filesystem::remove(backupFilePath);
} catch (std::filesystem::filesystem_error &) {
throw std::logic_error("Failed to remove pre-existing scene collection backup file: " +
backupFilePath.u8string());
}
}
}
} // namespace
// MARK: - Main Scene Collection Management Functions
void OBSBasic::SetupNewSceneCollection(const std::string &collectionName)
{
const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName);
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING);
cleanBackupCollision(newCollection);
ActivateSceneCollection(newCollection);
blog(LOG_INFO, "Created scene collection '%s' (clean, %s)", newCollection.name.c_str(),
newCollection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
}
void OBSBasic::SetupDuplicateSceneCollection(const std::string &collectionName)
{
const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName);
const OBSSceneCollection &currentCollection = GetCurrentSceneCollection();
SaveProjectNow();
const auto copyOptions = std::filesystem::copy_options::overwrite_existing;
try {
std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions);
} catch (const std::filesystem::filesystem_error &error) {
blog(LOG_DEBUG, "%s", error.what());
throw std::logic_error("Failed to copy file for cloned scene collection: " + newCollection.name);
}
OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str());
obs_data_set_string(collection, "name", newCollection.name.c_str());
OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources");
if (sources) {
obs_data_erase(collection, "sources");
obs_data_array_enum(
sources,
[](obs_data_t *data, void *) -> void {
const char *uuid = os_generate_uuid();
obs_data_set_string(data, "uuid", uuid);
bfree((void *)uuid);
},
nullptr);
obs_data_set_array(collection, "sources", sources);
}
obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr);
cleanBackupCollision(newCollection);
ActivateSceneCollection(newCollection);
blog(LOG_INFO, "Created scene collection '%s' (duplicate, %s)", newCollection.name.c_str(),
newCollection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
}
void OBSBasic::SetupRenameSceneCollection(const std::string &collectionName)
{
const OBSSceneCollection &newCollection = CreateSceneCollection(collectionName);
const OBSSceneCollection currentCollection = GetCurrentSceneCollection();
SaveProjectNow();
const auto copyOptions = std::filesystem::copy_options::overwrite_existing;
try {
std::filesystem::copy(currentCollection.collectionFile, newCollection.collectionFile, copyOptions);
} catch (const std::filesystem::filesystem_error &error) {
blog(LOG_DEBUG, "%s", error.what());
throw std::logic_error("Failed to copy file for scene collection: " + currentCollection.name);
}
collections.erase(currentCollection.name);
OBSDataAutoRelease collection = obs_data_create_from_json_file(newCollection.collectionFile.u8string().c_str());
obs_data_set_string(collection, "name", newCollection.name.c_str());
obs_data_save_json_safe(collection, newCollection.collectionFile.u8string().c_str(), "tmp", nullptr);
cleanBackupCollision(newCollection);
ActivateSceneCollection(newCollection);
RemoveSceneCollection(currentCollection);
blog(LOG_INFO, "Renamed scene collection '%s' to '%s' (%s)", currentCollection.name.c_str(),
newCollection.name.c_str(), newCollection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_RENAMED);
}
// MARK: - Scene Collection File Management Functions
const OBSSceneCollection &OBSBasic::CreateSceneCollection(const std::string &collectionName)
{
if (const auto &foundCollection = GetSceneCollectionByName(collectionName)) {
throw std::invalid_argument("Scene collection already exists: " + collectionName);
}
std::string fileName;
if (!GetFileSafeName(collectionName.c_str(), fileName)) {
throw std::invalid_argument("Failed to create safe directory for new scene collection: " +
collectionName);
}
std::string collectionFile;
collectionFile.reserve(App()->userScenesLocation.u8string().size() + OBSSceneCollectionPath.size() +
fileName.size());
collectionFile.append(App()->userScenesLocation.u8string()).append(OBSSceneCollectionPath).append(fileName);
if (!GetClosestUnusedFileName(collectionFile, "json")) {
throw std::invalid_argument("Failed to get closest file name for new scene collection: " + fileName);
}
const std::filesystem::path collectionFilePath = std::filesystem::u8path(collectionFile);
auto [iterator, success] = collections.try_emplace(
collectionName,
OBSSceneCollection{collectionName, collectionFilePath.filename().u8string(), collectionFilePath});
return iterator->second;
}
void OBSBasic::RemoveSceneCollection(OBSSceneCollection collection)
{
try {
std::filesystem::remove(collection.collectionFile);
} catch (const std::filesystem::filesystem_error &error) {
blog(LOG_DEBUG, "%s", error.what());
throw std::logic_error("Failed to remove scene collection file: " + collection.fileName);
}
blog(LOG_INFO, "Removed scene collection '%s' (%s)", collection.name.c_str(), collection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
}
// MARK: - Scene Collection UI Handling Functions
bool OBSBasic::CreateNewSceneCollection(const QString &name)
{
try {
SetupNewSceneCollection(name.toStdString());
return true;
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
return false;
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
return false;
}
}
bool OBSBasic::CreateDuplicateSceneCollection(const QString &name)
{
try {
SetupDuplicateSceneCollection(name.toStdString());
return true;
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
return false;
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
return false;
}
}
void OBSBasic::DeleteSceneCollection(const QString &name)
{
const std::string_view currentCollectionName{
config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")};
if (currentCollectionName == name.toStdString()) {
on_actionRemoveSceneCollection_triggered();
return;
}
OBSSceneCollection currentCollection = GetCurrentSceneCollection();
RemoveSceneCollection(currentCollection);
collections.erase(name.toStdString());
RefreshSceneCollections();
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
}
void OBSBasic::ChangeSceneCollection()
{
QAction *action = reinterpret_cast<QAction *>(sender());
if (!action) {
return;
}
const std::string_view currentCollectionName{
config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")};
const QVariant qCollectionName = action->property("collection_name");
const std::string selectedCollectionName{qCollectionName.toString().toStdString()};
if (currentCollectionName == selectedCollectionName) {
action->setChecked(true);
return;
}
const std::optional<OBSSceneCollection> foundCollection = GetSceneCollectionByName(selectedCollectionName);
if (!foundCollection) {
const std::string errorMessage{"Selected scene collection not found: "};
throw std::invalid_argument(errorMessage + currentCollectionName.data());
}
const OBSSceneCollection &selectedCollection = foundCollection.value();
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING);
ActivateSceneCollection(selectedCollection);
blog(LOG_INFO, "Switched to scene collection '%s' (%s)", selectedCollection.name.c_str(),
selectedCollection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
}
void OBSBasic::RefreshSceneCollections(bool refreshCache)
{
std::string_view currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")};
QList<QAction *> menuActions = ui->sceneCollectionMenu->actions();
for (auto &action : menuActions) {
QVariant variant = action->property("file_name");
if (variant.typeName() != nullptr) {
delete action;
}
}
if (refreshCache) {
RefreshSceneCollectionCache();
}
updateSortedSceneCollections(collections);
size_t numAddedCollections = 0;
for (auto &name : sortedSceneCollections) {
const std::string collectionName = name.toStdString();
try {
const OBSSceneCollection &collection = collections.at(collectionName);
const QString qCollectionName = QString().fromStdString(collectionName);
QAction *action = new QAction(qCollectionName, this);
action->setProperty("collection_name", qCollectionName);
action->setProperty("file_name", QString().fromStdString(collection.fileName));
connect(action, &QAction::triggered, this, &OBSBasic::ChangeSceneCollection);
action->setCheckable(true);
action->setChecked(collectionName == currentCollectionName);
ui->sceneCollectionMenu->addAction(action);
numAddedCollections += 1;
} catch (const std::out_of_range &error) {
blog(LOG_ERROR, "No scene collection with name %s found in scene collection cache.\n%s",
collectionName.c_str(), error.what());
}
}
ui->actionRemoveSceneCollection->setEnabled(numAddedCollections > 1);
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
main->ui->actionPasteFilters->setEnabled(false);
main->ui->actionPasteRef->setEnabled(false);
main->ui->actionPasteDup->setEnabled(false);
}
// MARK: - Scene Collection Cache Functions
void OBSBasic::RefreshSceneCollectionCache()
{
OBSSceneCollectionCache foundCollections{};
const std::filesystem::path collectionsPath =
App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath.substr(1));
if (!std::filesystem::exists(collectionsPath)) {
blog(LOG_WARNING, "Failed to get scene collections config path");
return;
}
for (const auto &entry : std::filesystem::directory_iterator(collectionsPath)) {
if (entry.is_directory()) {
continue;
}
if (entry.path().extension().u8string() != ".json") {
continue;
}
OBSDataAutoRelease collectionData =
obs_data_create_from_json_file_safe(entry.path().u8string().c_str(), "bak");
std::string candidateName;
const char *collectionName = obs_data_get_string(collectionData, "name");
if (!collectionName) {
candidateName = entry.path().filename().u8string();
} else {
candidateName = collectionName;
}
foundCollections.try_emplace(candidateName,
OBSSceneCollection{candidateName, entry.path().filename().u8string(),
entry.path()});
}
collections.swap(foundCollections);
}
const OBSSceneCollection &OBSBasic::GetCurrentSceneCollection() const
{
std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")};
if (currentCollectionName.empty()) {
throw std::invalid_argument("No valid scene collection name in configuration Basic->SceneCollection");
}
const auto &foundCollection = collections.find(currentCollectionName);
if (foundCollection != collections.end()) {
return foundCollection->second;
} else {
throw std::invalid_argument("Scene collection not found in collection list: " + currentCollectionName);
}
}
std::optional<OBSSceneCollection> OBSBasic::GetSceneCollectionByName(const std::string &collectionName) const
{
auto foundCollection = collections.find(collectionName);
if (foundCollection == collections.end()) {
return {};
} else {
return foundCollection->second;
}
}
std::optional<OBSSceneCollection> OBSBasic::GetSceneCollectionByFileName(const std::string &fileName) const
{
for (auto &[iterator, collection] : collections) {
if (collection.fileName == fileName) {
return collection;
}
}
return {};
}
// MARK: - Qt Slot Functions
void OBSBasic::on_actionNewSceneCollection_triggered()
{
const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) {
if (GetSceneCollectionByName(result.promptValue)) {
return false;
}
return true;
};
const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"),
Str("Basic.Main.AddSceneCollection.Text")};
OBSPromptResult result = PromptForName(request, sceneCollectionCallback);
if (!result.success) {
return;
}
try {
SetupNewSceneCollection(result.promptValue);
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
}
}
void OBSBasic::on_actionDupSceneCollection_triggered()
{
const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) {
if (GetSceneCollectionByName(result.promptValue)) {
return false;
}
return true;
};
const OBSPromptRequest request{Str("Basic.Main.AddSceneCollection.Title"),
Str("Basic.Main.AddSceneCollection.Text")};
OBSPromptResult result = PromptForName(request, sceneCollectionCallback);
if (!result.success) {
return;
}
try {
SetupDuplicateSceneCollection(result.promptValue);
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
}
}
void OBSBasic::on_actionRenameSceneCollection_triggered()
{
const OBSSceneCollection &currentCollection = GetCurrentSceneCollection();
const OBSPromptCallback sceneCollectionCallback = [this](const OBSPromptResult &result) {
if (GetSceneCollectionByName(result.promptValue)) {
return false;
}
return true;
};
const OBSPromptRequest request{Str("Basic.Main.RenameSceneCollection.Title"),
Str("Basic.Main.AddSceneCollection.Text"), currentCollection.name};
OBSPromptResult result = PromptForName(request, sceneCollectionCallback);
if (!result.success) {
return;
}
try {
SetupRenameSceneCollection(result.promptValue);
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
}
}
void OBSBasic::on_actionRemoveSceneCollection_triggered(bool skipConfirmation)
{
if (collections.size() < 2) {
return;
}
OBSSceneCollection currentCollection;
try {
currentCollection = GetCurrentSceneCollection();
if (!skipConfirmation) {
const QString confirmationText =
QTStr("ConfirmRemove.Text").arg(QString::fromStdString(currentCollection.name));
const QMessageBox::StandardButton button =
OBSMessageBox::question(this, QTStr("ConfirmRemove.Title"), confirmationText);
if (button == QMessageBox::No) {
return;
}
}
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING);
collections.erase(currentCollection.name);
} catch (const std::invalid_argument &error) {
blog(LOG_ERROR, "%s", error.what());
} catch (const std::logic_error &error) {
blog(LOG_ERROR, "%s", error.what());
}
const OBSSceneCollection &newCollection = collections.begin()->second;
ActivateSceneCollection(newCollection);
RemoveSceneCollection(currentCollection);
blog(LOG_INFO, "Switched to scene collection '%s' (%s)", newCollection.name.c_str(),
newCollection.fileName.c_str());
blog(LOG_INFO, "------------------------------------------------");
}
void OBSBasic::on_actionImportSceneCollection_triggered()
{
OBSImporter imp(this);
imp.exec();
RefreshSceneCollections(true);
}
void OBSBasic::on_actionExportSceneCollection_triggered()
{
SaveProjectNow();
const OBSSceneCollection &currentCollection = GetCurrentSceneCollection();
const QString home = QDir::homePath();
const QString destinationFileName = SaveFile(this, QTStr("Basic.MainMenu.SceneCollection.Export"),
home + "/" + currentCollection.fileName.c_str(),
"JSON Files (*.json)");
if (!destinationFileName.isEmpty() && !destinationFileName.isNull()) {
const std::filesystem::path sourceFile = currentCollection.collectionFile;
const std::filesystem::path destinationFile =
std::filesystem::u8path(destinationFileName.toStdString());
OBSDataAutoRelease collection = obs_data_create_from_json_file(sourceFile.u8string().c_str());
OBSDataArrayAutoRelease sources = obs_data_get_array(collection, "sources");
if (!sources) {
blog(LOG_WARNING, "No sources in exported scene collection");
return;
}
obs_data_erase(collection, "sources");
using OBSDataVector = std::vector<OBSData>;
OBSDataVector sourceItems;
obs_data_array_enum(
sources,
[](obs_data_t *data, void *vector) -> void {
OBSDataVector &sourceItems{*static_cast<OBSDataVector *>(vector)};
sourceItems.push_back(data);
},
&sourceItems);
std::sort(sourceItems.begin(), sourceItems.end(), [](const OBSData &a, const OBSData &b) {
return astrcmpi(obs_data_get_string(a, "name"), obs_data_get_string(b, "name")) < 0;
});
OBSDataArrayAutoRelease newSources = obs_data_array_create();
for (auto &item : sourceItems) {
obs_data_array_push_back(newSources, item);
}
obs_data_set_array(collection, "sources", newSources);
obs_data_save_json_pretty_safe(collection, destinationFile.u8string().c_str(), "tmp", "bak");
}
}
void OBSBasic::on_actionRemigrateSceneCollection_triggered()
{
if (Active()) {
OBSMessageBox::warning(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"),
QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.Active"));
return;
}
OBSDataAutoRelease priv = obs_get_private_data();
if (!usingAbsoluteCoordinates && !migrationBaseResolution) {
OBSMessageBox::warning(
this, QTStr("Basic.Main.RemigrateSceneCollection.Title"),
QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.UnknownBaseResolution"));
return;
}
obs_video_info ovi;
obs_get_video_info(&ovi);
if (!usingAbsoluteCoordinates && migrationBaseResolution->first == ovi.base_width &&
migrationBaseResolution->second == ovi.base_height) {
OBSMessageBox::warning(
this, QTStr("Basic.Main.RemigrateSceneCollection.Title"),
QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.BaseResolutionMatches"));
return;
}
const OBSSceneCollection &currentCollection = GetCurrentSceneCollection();
QString name = QString::fromStdString(currentCollection.name);
QString message =
QTStr("Basic.Main.RemigrateSceneCollection.Text").arg(name).arg(ovi.base_width).arg(ovi.base_height);
auto answer = OBSMessageBox::question(this, QTStr("Basic.Main.RemigrateSceneCollection.Title"), message);
if (answer == QMessageBox::No)
return;
lastOutputResolution = {ovi.base_width, ovi.base_height};
if (!usingAbsoluteCoordinates) {
/* Temporarily change resolution to migration resolution */
ovi.base_width = migrationBaseResolution->first;
ovi.base_height = migrationBaseResolution->second;
if (obs_reset_video(&ovi) != OBS_VIDEO_SUCCESS) {
OBSMessageBox::critical(
this, QTStr("Basic.Main.RemigrateSceneCollection.Title"),
QTStr("Basic.Main.RemigrateSceneCollection.CannotMigrate.FailedVideoReset"));
return;
}
}
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING);
/* Save and immediately reload to (re-)run migrations. */
SaveProjectNow();
/* Reset video if we potentially changed to a temporary resolution */
if (!usingAbsoluteCoordinates) {
ResetVideo();
}
ActivateSceneCollection(currentCollection);
}
// MARK: - Scene Collection Management Helper Functions
void OBSBasic::ActivateSceneCollection(const OBSSceneCollection &collection)
{
const std::string currentCollectionName{config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")};
if (auto foundCollection = GetSceneCollectionByName(currentCollectionName)) {
if (collection.name != foundCollection.value().name) {
SaveProjectNow();
}
}
config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", collection.name.c_str());
config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", collection.fileName.c_str());
Load(collection.collectionFile.u8string().c_str());
RefreshSceneCollections();
UpdateTitleBar();
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,562 +0,0 @@
#include "moc_window-extra-browsers.cpp"
#include "window-dock-browser.hpp"
#include "window-basic-main.hpp"
#include <qt-wrappers.hpp>
#include <QLineEdit>
#include <QHBoxLayout>
#include <QUuid>
#include <json11.hpp>
#include "ui_OBSExtraBrowsers.h"
using namespace json11;
#define OBJ_NAME_SUFFIX "_extraBrowser"
enum class Column : int {
Title,
Url,
Delete,
Count,
};
/* ------------------------------------------------------------------------- */
void ExtraBrowsersModel::Reset()
{
items.clear();
OBSBasic *main = OBSBasic::Get();
for (int i = 0; i < main->extraBrowserDocks.size(); i++) {
Item item;
item.prevIdx = i;
item.title = main->extraBrowserDockNames[i];
item.url = main->extraBrowserDockTargets[i];
items.push_back(item);
}
}
int ExtraBrowsersModel::rowCount(const QModelIndex &) const
{
int count = items.size() + 1;
return count;
}
int ExtraBrowsersModel::columnCount(const QModelIndex &) const
{
return (int)Column::Count;
}
QVariant ExtraBrowsersModel::data(const QModelIndex &index, int role) const
{
int column = index.column();
int idx = index.row();
int count = items.size();
bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole;
if (!validRole)
return QVariant();
if (idx >= 0 && idx < count) {
switch (column) {
case (int)Column::Title:
return items[idx].title;
case (int)Column::Url:
return items[idx].url;
}
} else if (idx == count) {
switch (column) {
case (int)Column::Title:
return newTitle;
case (int)Column::Url:
return newURL;
}
}
return QVariant();
}
QVariant ExtraBrowsersModel::headerData(int section, Qt::Orientation orientation, int role) const
{
bool validRole = role == Qt::DisplayRole || role == Qt::AccessibleTextRole;
if (validRole && orientation == Qt::Orientation::Horizontal) {
switch (section) {
case (int)Column::Title:
return QTStr("ExtraBrowsers.DockName");
case (int)Column::Url:
return QStringLiteral("URL");
}
}
return QVariant();
}
Qt::ItemFlags ExtraBrowsersModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() != (int)Column::Delete)
flags |= Qt::ItemIsEditable;
return flags;
}
class DelButton : public QPushButton {
public:
inline DelButton(QModelIndex index_) : QPushButton(), index(index_) {}
QPersistentModelIndex index;
};
class EditWidget : public QLineEdit {
public:
inline EditWidget(QWidget *parent, QModelIndex index_) : QLineEdit(parent), index(index_) {}
QPersistentModelIndex index;
};
void ExtraBrowsersModel::AddDeleteButton(int idx)
{
QTableView *widget = reinterpret_cast<QTableView *>(parent());
QModelIndex index = createIndex(idx, (int)Column::Delete, nullptr);
QPushButton *del = new DelButton(index);
del->setProperty("class", "icon-trash");
del->setObjectName("extraPanelDelete");
del->setMinimumSize(QSize(20, 20));
connect(del, &QPushButton::clicked, this, &ExtraBrowsersModel::DeleteItem);
widget->setIndexWidget(index, del);
widget->setRowHeight(idx, 20);
widget->setColumnWidth(idx, 20);
}
void ExtraBrowsersModel::CheckToAdd()
{
if (newTitle.isEmpty() || newURL.isEmpty())
return;
int idx = items.size() + 1;
beginInsertRows(QModelIndex(), idx, idx);
Item item;
item.prevIdx = -1;
item.title = newTitle;
item.url = newURL;
items.push_back(item);
newTitle = "";
newURL = "";
endInsertRows();
AddDeleteButton(idx - 1);
}
void ExtraBrowsersModel::UpdateItem(Item &item)
{
int idx = item.prevIdx;
OBSBasic *main = OBSBasic::Get();
BrowserDock *dock = reinterpret_cast<BrowserDock *>(main->extraBrowserDocks[idx].get());
dock->setWindowTitle(item.title);
dock->setObjectName(item.title + OBJ_NAME_SUFFIX);
if (main->extraBrowserDockNames[idx] != item.title) {
main->extraBrowserDockNames[idx] = item.title;
dock->toggleViewAction()->setText(item.title);
dock->setTitle(item.title);
}
if (main->extraBrowserDockTargets[idx] != item.url) {
dock->cefWidget->setURL(QT_TO_UTF8(item.url));
main->extraBrowserDockTargets[idx] = item.url;
}
}
void ExtraBrowsersModel::DeleteItem()
{
QTableView *widget = reinterpret_cast<QTableView *>(parent());
DelButton *del = reinterpret_cast<DelButton *>(sender());
int row = del->index.row();
/* there's some sort of internal bug in Qt and deleting certain index
* widgets or "editors" that can cause a crash inside Qt if the widget
* is not manually removed, at least on 5.7 */
widget->setIndexWidget(del->index, nullptr);
del->deleteLater();
/* --------- */
beginRemoveRows(QModelIndex(), row, row);
int prevIdx = items[row].prevIdx;
items.removeAt(row);
if (prevIdx != -1) {
int i = 0;
for (; i < deleted.size() && deleted[i] < prevIdx; i++)
;
deleted.insert(i, prevIdx);
}
endRemoveRows();
}
void ExtraBrowsersModel::Apply()
{
OBSBasic *main = OBSBasic::Get();
for (Item &item : items) {
if (item.prevIdx != -1) {
UpdateItem(item);
} else {
QString uuid = QUuid::createUuid().toString();
uuid.replace(QRegularExpression("[{}-]"), "");
main->AddExtraBrowserDock(item.title, item.url, uuid, true);
}
}
for (int i = deleted.size() - 1; i >= 0; i--) {
int idx = deleted[i];
main->extraBrowserDockTargets.removeAt(idx);
main->extraBrowserDockNames.removeAt(idx);
main->extraBrowserDocks.removeAt(idx);
}
if (main->extraBrowserDocks.empty())
main->extraBrowserMenuDocksSeparator.clear();
deleted.clear();
Reset();
}
void ExtraBrowsersModel::TabSelection(bool forward)
{
QListView *widget = reinterpret_cast<QListView *>(parent());
QItemSelectionModel *selModel = widget->selectionModel();
QModelIndex sel = selModel->currentIndex();
int row = sel.row();
int col = sel.column();
switch (sel.column()) {
case (int)Column::Title:
if (!forward) {
if (row == 0) {
return;
}
row -= 1;
}
col += 1;
break;
case (int)Column::Url:
if (forward) {
if (row == items.size()) {
return;
}
row += 1;
}
col -= 1;
}
sel = createIndex(row, col, nullptr);
selModel->setCurrentIndex(sel, QItemSelectionModel::Clear);
}
void ExtraBrowsersModel::Init()
{
for (int i = 0; i < items.count(); i++)
AddDeleteButton(i);
}
/* ------------------------------------------------------------------------- */
QWidget *ExtraBrowsersDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &,
const QModelIndex &index) const
{
QLineEdit *text = new EditWidget(parent, index);
text->installEventFilter(const_cast<ExtraBrowsersDelegate *>(this));
text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding,
QSizePolicy::ControlType::LineEdit));
return text;
}
void ExtraBrowsersDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
QLineEdit *text = reinterpret_cast<QLineEdit *>(editor);
text->blockSignals(true);
text->setText(index.data().toString());
text->blockSignals(false);
}
bool ExtraBrowsersDelegate::eventFilter(QObject *object, QEvent *event)
{
QLineEdit *edit = qobject_cast<QLineEdit *>(object);
if (!edit)
return false;
if (LineEditCanceled(event)) {
RevertText(edit);
}
if (LineEditChanged(event)) {
UpdateText(edit);
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Tab) {
model->TabSelection(true);
} else if (keyEvent->key() == Qt::Key_Backtab) {
model->TabSelection(false);
}
}
return true;
}
return false;
}
bool ExtraBrowsersDelegate::ValidName(const QString &name) const
{
for (auto &item : model->items) {
if (name.compare(item.title, Qt::CaseInsensitive) == 0) {
return false;
}
}
return true;
}
void ExtraBrowsersDelegate::RevertText(QLineEdit *edit_)
{
EditWidget *edit = reinterpret_cast<EditWidget *>(edit_);
int row = edit->index.row();
int col = edit->index.column();
bool newItem = (row == model->items.size());
QString oldText;
if (col == (int)Column::Title) {
oldText = newItem ? model->newTitle : model->items[row].title;
} else {
oldText = newItem ? model->newURL : model->items[row].url;
}
edit->setText(oldText);
}
bool ExtraBrowsersDelegate::UpdateText(QLineEdit *edit_)
{
EditWidget *edit = reinterpret_cast<EditWidget *>(edit_);
int row = edit->index.row();
int col = edit->index.column();
bool newItem = (row == model->items.size());
QString text = edit->text().trimmed();
if (!newItem && text.isEmpty()) {
return false;
}
if (col == (int)Column::Title) {
QString oldText = newItem ? model->newTitle : model->items[row].title;
bool same = oldText.compare(text, Qt::CaseInsensitive) == 0;
if (!same && !ValidName(text)) {
edit->setText(oldText);
return false;
}
}
if (!newItem) {
/* if edited existing item, update it*/
switch (col) {
case (int)Column::Title:
model->items[row].title = text;
break;
case (int)Column::Url:
model->items[row].url = text;
break;
}
} else {
/* if both new values filled out, create new one */
switch (col) {
case (int)Column::Title:
model->newTitle = text;
break;
case (int)Column::Url:
model->newURL = text;
break;
}
model->CheckToAdd();
}
emit commitData(edit);
return true;
}
/* ------------------------------------------------------------------------- */
OBSExtraBrowsers::OBSExtraBrowsers(QWidget *parent) : QDialog(parent), ui(new Ui::OBSExtraBrowsers)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose, true);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
model = new ExtraBrowsersModel(ui->table);
ui->table->setModel(model);
ui->table->setItemDelegateForColumn((int)Column::Title, new ExtraBrowsersDelegate(model));
ui->table->setItemDelegateForColumn((int)Column::Url, new ExtraBrowsersDelegate(model));
ui->table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
ui->table->horizontalHeader()->setSectionResizeMode((int)Column::Delete, QHeaderView::ResizeMode::Fixed);
ui->table->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged);
}
OBSExtraBrowsers::~OBSExtraBrowsers() {}
void OBSExtraBrowsers::closeEvent(QCloseEvent *event)
{
QDialog::closeEvent(event);
model->Apply();
}
void OBSExtraBrowsers::on_apply_clicked()
{
model->Apply();
}
/* ------------------------------------------------------------------------- */
void OBSBasic::ClearExtraBrowserDocks()
{
extraBrowserDockTargets.clear();
extraBrowserDockNames.clear();
extraBrowserDocks.clear();
}
void OBSBasic::LoadExtraBrowserDocks()
{
const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks");
std::string err;
Json json = Json::parse(jsonStr, err);
if (!err.empty())
return;
Json::array array = json.array_items();
if (!array.empty())
extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator();
for (Json &item : array) {
std::string title = item["title"].string_value();
std::string url = item["url"].string_value();
std::string uuid = item["uuid"].string_value();
AddExtraBrowserDock(title.c_str(), url.c_str(), uuid.c_str(), false);
}
}
void OBSBasic::SaveExtraBrowserDocks()
{
Json::array array;
for (int i = 0; i < extraBrowserDocks.size(); i++) {
QDockWidget *dock = extraBrowserDocks[i].get();
QString title = extraBrowserDockNames[i];
QString url = extraBrowserDockTargets[i];
QString uuid = dock->property("uuid").toString();
Json::object obj{
{"title", QT_TO_UTF8(title)},
{"url", QT_TO_UTF8(url)},
{"uuid", QT_TO_UTF8(uuid)},
};
array.push_back(obj);
}
std::string output = Json(array).dump();
config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str());
}
void OBSBasic::ManageExtraBrowserDocks()
{
if (!extraBrowsers.isNull()) {
extraBrowsers->show();
extraBrowsers->raise();
return;
}
extraBrowsers = new OBSExtraBrowsers(this);
extraBrowsers->show();
}
void OBSBasic::AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate)
{
static int panel_version = -1;
if (panel_version == -1) {
panel_version = obs_browser_qcef_version();
}
BrowserDock *dock = new BrowserDock(title);
QString bId(uuid.isEmpty() ? QUuid::createUuid().toString() : uuid);
bId.replace(QRegularExpression("[{}-]"), "");
dock->setProperty("uuid", bId);
dock->setObjectName(title + OBJ_NAME_SUFFIX);
dock->resize(460, 600);
dock->setMinimumSize(80, 80);
dock->setWindowTitle(title);
dock->setAllowedAreas(Qt::AllDockWidgetAreas);
QCefWidget *browser = cef->create_widget(dock, QT_TO_UTF8(url), nullptr);
if (browser && panel_version >= 1)
browser->allowAllPopups(true);
dock->SetWidget(browser);
/* Add support for Twitch Dashboard panels */
if (url.contains("twitch.tv/popout") && url.contains("dashboard/live")) {
QRegularExpression re("twitch.tv\\/popout\\/([^/]+)\\/");
QRegularExpressionMatch match = re.match(url);
QString username = match.captured(1);
if (username.length() > 0) {
std::string script;
script = "Object.defineProperty(document, 'referrer', { get: () => '";
script += "https://twitch.tv/";
script += QT_TO_UTF8(username);
script += "/dashboard/live";
script += "'});";
browser->setStartupScript(script);
}
}
AddDockWidget(dock, Qt::RightDockWidgetArea, true);
extraBrowserDocks.push_back(std::shared_ptr<QDockWidget>(dock));
extraBrowserDockNames.push_back(title);
extraBrowserDockTargets.push_back(url);
if (firstCreate) {
dock->setFloating(true);
QPoint curPos = pos();
QSize wSizeD2 = size() / 2;
QSize dSizeD2 = dock->size() / 2;
curPos.setX(curPos.x() + wSizeD2.width() - dSizeD2.width());
curPos.setY(curPos.y() + wSizeD2.height() - dSizeD2.height());
dock->move(curPos);
dock->setVisible(true);
}
}

View File

@@ -1,88 +0,0 @@
#pragma once
#include <QDialog>
#include <QScopedPointer>
#include <QAbstractTableModel>
#include <QStyledItemDelegate>
#include <memory>
class Ui_OBSExtraBrowsers;
class ExtraBrowsersModel;
class QCefWidget;
class OBSExtraBrowsers : public QDialog {
Q_OBJECT
std::unique_ptr<Ui_OBSExtraBrowsers> ui;
ExtraBrowsersModel *model;
public:
OBSExtraBrowsers(QWidget *parent);
~OBSExtraBrowsers();
void closeEvent(QCloseEvent *event) override;
public slots:
void on_apply_clicked();
};
class ExtraBrowsersModel : public QAbstractTableModel {
Q_OBJECT
public:
inline ExtraBrowsersModel(QObject *parent = nullptr) : QAbstractTableModel(parent)
{
Reset();
QMetaObject::invokeMethod(this, "Init", Qt::QueuedConnection);
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
struct Item {
int prevIdx;
QString title;
QString url;
};
void TabSelection(bool forward);
void AddDeleteButton(int idx);
void Reset();
void CheckToAdd();
void UpdateItem(Item &item);
void DeleteItem();
void Apply();
QVector<Item> items;
QVector<int> deleted;
QString newTitle;
QString newURL;
public slots:
void Init();
};
class ExtraBrowsersDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
inline ExtraBrowsersDelegate(ExtraBrowsersModel *model_) : QStyledItemDelegate(nullptr), model(model_) {}
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
bool eventFilter(QObject *object, QEvent *event) override;
void RevertText(QLineEdit *edit);
bool UpdateText(QLineEdit *edit);
bool ValidName(const QString &text) const;
ExtraBrowsersModel *model;
};

View File

@@ -1,570 +0,0 @@
/******************************************************************************
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 "moc_window-importer.cpp"
#include "obs-app.hpp"
#include <QPushButton>
#include <QLineEdit>
#include <QToolButton>
#include <QMimeData>
#include <QStyledItemDelegate>
#include <QDirIterator>
#include <QDropEvent>
#include <qt-wrappers.hpp>
#include "importers/importers.hpp"
extern bool SceneCollectionExists(const char *findName);
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->setContentsMargins(0, 0, 0, 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);
QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText);
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());
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 = OpenFiles(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.empty()) {
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, &ImporterModel::dataChanged, this, &OBSImporter::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), &QPushButton::clicked, this,
&OBSImporter::importCollections);
connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport);
connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close);
ImportersInit();
bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt");
if (!autoSearchPrompt) {
QMessageBox::StandardButton button = OBSMessageBox::question(
parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText"));
if (button == QMessageBox::Yes) {
config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true);
} else {
config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false);
}
config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true);
}
bool autoSearch = config_get_bool(App()->GetUserConfig(), "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();
}
void OBSImporter::browseImport()
{
QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "",
QTStr("Importer.Collection") + QString(" ") + Pattern);
if (!paths.empty()) {
for (int i = 0; i < paths.count(); i++) {
addImportOption(paths[i], false);
}
}
}
bool GetUnusedName(std::string &name)
{
OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
if (!basic->GetSceneCollectionByName(name)) {
return false;
}
std::string newName;
int inc = 2;
do {
newName = name;
newName += " ";
newName += std::to_string(inc++);
} while (basic->GetSceneCollectionByName(newName));
name = newName;
return true;
}
constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/";
void OBSImporter::importCollections()
{
setEnabled(false);
const std::filesystem::path sceneCollectionLocation =
App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath);
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;
std::string pathStr = optionsModel->index(i, ImporterColumn::Path)
.data(Qt::DisplayRole)
.value<QString>()
.toStdString();
std::string nameStr = optionsModel->index(i, ImporterColumn::Name)
.data(Qt::DisplayRole)
.value<QString>()
.toStdString();
json11::Json res;
ImportSC(pathStr, nameStr, res);
if (res != json11::Json()) {
json11::Json::object out = res.object_items();
std::string name = res["name"].string_value();
std::string file;
if (GetUnusedName(name)) {
json11::Json::object newOut = out;
newOut["name"] = name;
out = newOut;
}
std::string fileName;
if (!GetFileSafeName(name.c_str(), fileName)) {
blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str());
}
std::string collectionFile;
collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size());
collectionFile.append(sceneCollectionLocation.u8string()).append(fileName);
if (!GetClosestUnusedFileName(collectionFile, "json")) {
blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str());
}
std::string out_str = json11::Json(out).dump();
bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(),
false);
blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(),
success ? "SUCCESS" : "FAILURE");
}
}
close();
}
void OBSImporter::dataChanged()
{
ui->tableView->resizeColumnToContents(ImporterColumn::Name);
}

View File

@@ -1,542 +0,0 @@
/******************************************************************************
Copyright (C) 2019 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 "moc_window-missing-files.cpp"
#include "window-basic-main.hpp"
#include "obs-app.hpp"
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <qt-wrappers.hpp>
enum MissingFilesColumn {
Source,
OriginalPath,
NewPath,
State,
Count
};
enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole };
/**********************************************************
Delegate - Presents cells in the grid.
**********************************************************/
MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath)
: QStyledItemDelegate(),
isOutput(isOutput),
defaultPath(defaultPath)
{
}
QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */,
const QModelIndex &) const
{
QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding,
QSizePolicy::ControlType::PushButton);
QWidget *container = new QWidget(parent);
auto browseCallback = [this, container]() {
const_cast<MissingFilesPathItemDelegate *>(this)->handleBrowse(container);
};
auto clearCallback = [this, container]() {
const_cast<MissingFilesPathItemDelegate *>(this)->handleClear(container);
};
QHBoxLayout *layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 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 input cells
if (isOutput) {
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 MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
QLineEdit *text = editor->findChild<QLineEdit *>();
text->setText(index.data().toString());
editor->setProperty(PATH_LIST_PROP, QVariant());
}
void MissingFilesPathItemDelegate::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();
if (isOutput) {
model->setData(index, list);
} else
model->setData(index, list, MissingFilesRole::NewPathsToProcessRole);
} else {
QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
model->setData(index, lineEdit->text(), 0);
}
}
void MissingFilesPathItemDelegate::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 MissingFilesPathItemDelegate::handleBrowse(QWidget *container)
{
QLineEdit *text = container->findChild<QLineEdit *>();
QString currentPath = text->text();
if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0)
currentPath = defaultPath;
bool isSet = false;
if (isOutput) {
QString newPath =
QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr);
#ifdef __APPLE__
// TODO: Revisit when QTBUG-42661 is fixed
container->window()->raise();
#endif
if (!newPath.isEmpty()) {
container->setProperty(PATH_LIST_PROP, QStringList() << newPath);
isSet = true;
}
}
if (isSet)
emit commitData(container);
}
void MissingFilesPathItemDelegate::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() << QTStr("MissingFiles.Clear"));
container->findChild<QLineEdit *>()->clearFocus();
((QWidget *)container->parent())->setFocus();
emit commitData(container);
}
/**
Model
**/
MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent)
{
QStyle *style = QApplication::style();
warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning);
}
int MissingFilesModel::rowCount(const QModelIndex &) const
{
return files.length();
}
int MissingFilesModel::columnCount(const QModelIndex &) const
{
return MissingFilesColumn::Count;
}
int MissingFilesModel::found() const
{
int res = 0;
for (int i = 0; i < files.length(); i++) {
if (files[i].state != Missing && files[i].state != Cleared)
res++;
}
return res;
}
QVariant MissingFilesModel::data(const QModelIndex &index, int role) const
{
QVariant result = QVariant();
if (index.row() >= files.length()) {
return QVariant();
} else if (role == Qt::DisplayRole) {
QFileInfo fi(files[index.row()].originalPath);
switch (index.column()) {
case MissingFilesColumn::Source:
result = files[index.row()].source;
break;
case MissingFilesColumn::OriginalPath:
result = fi.fileName();
break;
case MissingFilesColumn::NewPath:
result = files[index.row()].newPath;
break;
case MissingFilesColumn::State:
switch (files[index.row()].state) {
case MissingFilesState::Missing:
result = QTStr("MissingFiles.Missing");
break;
case MissingFilesState::Replaced:
result = QTStr("MissingFiles.Replaced");
break;
case MissingFilesState::Found:
result = QTStr("MissingFiles.Found");
break;
case MissingFilesState::Cleared:
result = QTStr("MissingFiles.Cleared");
break;
}
break;
}
} else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) {
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str());
if (source) {
result = main->GetSourceIcon(obs_source_get_id(source));
}
} else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) {
QFont font = QFont();
font.setBold(true);
result = font;
} else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) {
switch (files[index.row()].state) {
case MissingFilesState::Missing:
result = QTStr("MissingFiles.Missing");
break;
case MissingFilesState::Replaced:
result = QTStr("MissingFiles.Replaced");
break;
case MissingFilesState::Found:
result = QTStr("MissingFiles.Found");
break;
case MissingFilesState::Cleared:
result = QTStr("MissingFiles.Cleared");
break;
default:
break;
}
} else if (role == Qt::ToolTipRole) {
switch (index.column()) {
case MissingFilesColumn::OriginalPath:
result = files[index.row()].originalPath;
break;
case MissingFilesColumn::NewPath:
result = files[index.row()].newPath;
break;
default:
break;
}
}
return result;
}
Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() == MissingFilesColumn::OriginalPath) {
flags &= ~Qt::ItemIsEditable;
} else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) {
flags |= Qt::ItemIsEditable;
}
return flags;
}
void MissingFilesModel::fileCheckLoop(QList<MissingFileEntry> files, QString path, bool skipPrompt)
{
loop = false;
QUrl url = QUrl().fromLocalFile(path);
QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile);
bool prompted = skipPrompt;
for (int i = 0; i < files.length(); i++) {
if (files[i].state != MissingFilesState::Missing)
continue;
QUrl origFile = QUrl().fromLocalFile(files[i].originalPath);
QString filename = origFile.fileName();
QString testFile = dir + filename;
if (os_file_exists(testFile.toStdString().c_str())) {
if (!prompted) {
QMessageBox::StandardButton button =
QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"),
QTStr("MissingFiles.AutoSearchText"));
if (button == QMessageBox::No)
break;
prompted = true;
}
QModelIndex in = index(i, MissingFilesColumn::NewPath);
setData(in, testFile, 0);
}
}
loop = true;
}
bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
bool success = false;
if (role == MissingFilesRole::NewPathsToProcessRole) {
QStringList list = value.toStringList();
int row = index.row() + 1;
beginInsertRows(QModelIndex(), row, row);
MissingFileEntry entry;
entry.originalPath = list[0].replace("\\", "/");
entry.source = list[1];
files.insert(row, entry);
row++;
endInsertRows();
success = true;
} else {
QString path = value.toString();
if (index.column() == MissingFilesColumn::NewPath) {
files[index.row()].newPath = value.toString();
QString fileName = QUrl(path).fileName();
QString origFileName = QUrl(files[index.row()].originalPath).fileName();
if (path.isEmpty()) {
files[index.row()].state = MissingFilesState::Missing;
} else if (path.compare(QTStr("MissingFiles.Clear")) == 0) {
files[index.row()].state = MissingFilesState::Cleared;
} else if (fileName.compare(origFileName) == 0) {
files[index.row()].state = MissingFilesState::Found;
if (loop)
fileCheckLoop(files, path, false);
} else {
files[index.row()].state = MissingFilesState::Replaced;
if (loop)
fileCheckLoop(files, path, false);
}
emit dataChanged(index, index);
success = true;
}
}
return success;
}
QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
QVariant result = QVariant();
if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) {
switch (section) {
case MissingFilesColumn::State:
result = QTStr("MissingFiles.State");
break;
case MissingFilesColumn::Source:
result = QTStr("Basic.Main.Source");
break;
case MissingFilesColumn::OriginalPath:
result = QTStr("MissingFiles.MissingFile");
break;
case MissingFilesColumn::NewPath:
result = QTStr("MissingFiles.NewFile");
break;
}
}
return result;
}
OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent)
: QDialog(parent),
filesModel(new MissingFilesModel),
ui(new Ui::OBSMissingFiles)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
ui->tableView->setModel(filesModel);
ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath,
new MissingFilesPathItemDelegate(false, ""));
ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath,
new MissingFilesPathItemDelegate(true, ""));
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source,
QHeaderView::ResizeMode::ResizeToContents);
ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3);
ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State,
QHeaderView::ResizeMode::ResizeToContents);
ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged);
ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32)));
for (size_t i = 0; i < obs_missing_files_count(files); i++) {
obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i);
const char *oldPath = obs_missing_file_get_path(f);
const char *name = obs_missing_file_get_source_name(f);
addMissingFile(oldPath, name);
}
QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files)));
ui->found->setText(found);
fileStore = files;
connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles);
connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders);
connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close);
connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged);
QModelIndex index = filesModel->createIndex(0, 1);
QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection,
Q_ARG(const QModelIndex &, index));
}
OBSMissingFiles::~OBSMissingFiles()
{
obs_missing_files_destroy(fileStore);
}
void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName)
{
QStringList list;
list.append(originalPath);
list.append(sourceName);
QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source);
filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole);
}
void OBSMissingFiles::saveFiles()
{
for (int i = 0; i < filesModel->files.length(); i++) {
MissingFilesState state = filesModel->files[i].state;
if (state != MissingFilesState::Missing) {
obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i);
QString path = filesModel->files[i].newPath;
if (state == MissingFilesState::Cleared) {
obs_missing_file_issue_callback(f, "");
} else {
char *p = bstrdup(path.toStdString().c_str());
obs_missing_file_issue_callback(f, p);
bfree(p);
}
}
}
QDialog::accept();
}
void OBSMissingFiles::browseFolders()
{
QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "",
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (dir != "") {
dir += "/";
filesModel->fileCheckLoop(filesModel->files, dir, true);
}
}
void OBSMissingFiles::dataChanged()
{
QString found =
QTStr("MissingFiles.NumFound")
.arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore)));
ui->found->setText(found);
ui->tableView->resizeColumnToContents(MissingFilesColumn::State);
ui->tableView->resizeColumnToContents(MissingFilesColumn::Source);
}
QIcon OBSMissingFiles::GetWarningIcon()
{
return filesModel->warningIcon;
}
void OBSMissingFiles::SetWarningIcon(const QIcon &icon)
{
ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32)));
filesModel->warningIcon = icon;
}

View File

@@ -1,924 +0,0 @@
/******************************************************************************
Copyright (C) 2014 by Ruwen Hahn <palana@stunned.de>
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 "moc_window-remux.cpp"
#include "obs-app.hpp"
#include <QCloseEvent>
#include <QDirIterator>
#include <QItemDelegate>
#include <QLineEdit>
#include <QMessageBox>
#include <QMimeData>
#include <QPainter>
#include <QPushButton>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QTimer>
#include <qt-wrappers.hpp>
#include "window-basic-main.hpp"
#include <memory>
#include <cmath>
using namespace std;
enum RemuxEntryColumn {
State,
InputPath,
OutputPath,
Count
};
enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole };
/**********************************************************
Delegate - Presents cells in the grid.
**********************************************************/
RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath)
: QStyledItemDelegate(),
isOutput(isOutput),
defaultPath(defaultPath)
{
}
QWidget *RemuxEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */,
const QModelIndex &index) const
{
RemuxEntryState state = index.model()
->index(index.row(), RemuxEntryColumn::State)
.data(RemuxEntryRole::EntryStateRole)
.value<RemuxEntryState>();
if (state == RemuxEntryState::Pending || state == RemuxEntryState::InProgress) {
// Never allow modification of rows that are
// in progress.
return Q_NULLPTR;
} else if (isOutput && state != RemuxEntryState::Ready) {
// Do not allow modification of output rows
// that aren't associated with a valid input.
return Q_NULLPTR;
} else if (!isOutput && state == RemuxEntryState::Complete) {
// Don't allow modification of rows that are
// already complete.
return Q_NULLPTR;
} else {
QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding,
QSizePolicy::ControlType::PushButton);
QWidget *container = new QWidget(parent);
auto browseCallback = [this, container]() {
const_cast<RemuxEntryPathItemDelegate *>(this)->handleBrowse(container);
};
auto clearCallback = [this, container]() {
const_cast<RemuxEntryPathItemDelegate *>(this)->handleClear(container);
};
QHBoxLayout *layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 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);
QObject::connect(text, &QLineEdit::editingFinished, this, &RemuxEntryPathItemDelegate::updateText);
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 (!isOutput && state != RemuxEntryState::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 RemuxEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
QLineEdit *text = editor->findChild<QLineEdit *>();
text->setText(index.data().toString());
editor->setProperty(PATH_LIST_PROP, QVariant());
}
void RemuxEntryPathItemDelegate::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();
if (isOutput) {
if (list.size() > 0)
model->setData(index, list);
} else
model->setData(index, list, RemuxEntryRole::NewPathsToProcessRole);
} else {
QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
model->setData(index, lineEdit->text());
}
}
void RemuxEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
RemuxEntryState state = index.model()
->index(index.row(), RemuxEntryColumn::State)
.data(RemuxEntryRole::EntryStateRole)
.value<RemuxEntryState>();
QStyleOptionViewItem localOption = option;
initStyleOption(&localOption, index);
if (isOutput) {
if (state != Ready) {
QColor background =
localOption.palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Window);
localOption.backgroundBrush = QBrush(background);
}
}
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter);
}
void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container)
{
QString ExtensionPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)";
QLineEdit *text = container->findChild<QLineEdit *>();
QString currentPath = text->text();
if (currentPath.isEmpty())
currentPath = defaultPath;
bool isSet = false;
if (isOutput) {
QString newPath = SaveFile(container, QTStr("Remux.SelectTarget"), currentPath, ExtensionPattern);
if (!newPath.isEmpty()) {
container->setProperty(PATH_LIST_PROP, QStringList() << newPath);
isSet = true;
}
} else {
QStringList paths = OpenFiles(container, QTStr("Remux.SelectRecording"), currentPath,
QTStr("Remux.OBSRecording") + QString(" ") + ExtensionPattern);
if (!paths.empty()) {
container->setProperty(PATH_LIST_PROP, paths);
isSet = true;
}
#ifdef __APPLE__
// TODO: Revisit when QTBUG-42661 is fixed
container->window()->raise();
#endif
}
if (isSet)
emit commitData(container);
}
void RemuxEntryPathItemDelegate::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 RemuxEntryPathItemDelegate::updateText()
{
QLineEdit *lineEdit = dynamic_cast<QLineEdit *>(sender());
QWidget *editor = lineEdit->parentWidget();
emit commitData(editor);
}
/**********************************************************
Model - Manages the queue's data
**********************************************************/
int RemuxQueueModel::rowCount(const QModelIndex &) const
{
return queue.length() + (isProcessing ? 0 : 1);
}
int RemuxQueueModel::columnCount(const QModelIndex &) const
{
return RemuxEntryColumn::Count;
}
QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const
{
QVariant result = QVariant();
if (index.row() >= queue.length()) {
return QVariant();
} else if (role == Qt::DisplayRole) {
switch (index.column()) {
case RemuxEntryColumn::InputPath:
result = queue[index.row()].sourcePath;
break;
case RemuxEntryColumn::OutputPath:
result = queue[index.row()].targetPath;
break;
}
} else if (role == Qt::DecorationRole && index.column() == RemuxEntryColumn::State) {
result = getIcon(queue[index.row()].state);
} else if (role == RemuxEntryRole::EntryStateRole) {
result = queue[index.row()].state;
}
return result;
}
QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation, int role) const
{
QVariant result = QVariant();
if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) {
switch (section) {
case RemuxEntryColumn::State:
result = QString();
break;
case RemuxEntryColumn::InputPath:
result = QTStr("Remux.SourceFile");
break;
case RemuxEntryColumn::OutputPath:
result = QTStr("Remux.TargetFile");
break;
}
}
return result;
}
Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() == RemuxEntryColumn::InputPath) {
flags |= Qt::ItemIsEditable;
} else if (index.column() == RemuxEntryColumn::OutputPath && index.row() != queue.length()) {
flags |= Qt::ItemIsEditable;
}
return flags;
}
bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
bool success = false;
if (role == RemuxEntryRole::NewPathsToProcessRole) {
QStringList pathList = value.toStringList();
if (pathList.size() == 0) {
if (index.row() < queue.size()) {
beginRemoveRows(QModelIndex(), index.row(), index.row());
queue.removeAt(index.row());
endRemoveRows();
}
} else {
if (pathList.size() >= 1 && index.row() < queue.length()) {
queue[index.row()].sourcePath = pathList[0];
checkInputPath(index.row());
pathList.removeAt(0);
success = true;
}
if (pathList.size() > 0) {
int row = index.row();
int lastRow = row + pathList.size() - 1;
beginInsertRows(QModelIndex(), row, lastRow);
for (QString path : pathList) {
RemuxQueueEntry entry;
entry.sourcePath = path;
entry.state = RemuxEntryState::Empty;
queue.insert(row, entry);
row++;
}
endInsertRows();
for (row = index.row(); row <= lastRow; row++) {
checkInputPath(row);
}
success = true;
}
}
} else if (index.row() == queue.length()) {
QString path = value.toString();
if (!path.isEmpty()) {
RemuxQueueEntry entry;
entry.sourcePath = path;
entry.state = RemuxEntryState::Empty;
beginInsertRows(QModelIndex(), queue.length() + 1, queue.length() + 1);
queue.append(entry);
endInsertRows();
checkInputPath(index.row());
success = true;
}
} else {
QString path = value.toString();
if (path.isEmpty()) {
if (index.column() == RemuxEntryColumn::InputPath) {
beginRemoveRows(QModelIndex(), index.row(), index.row());
queue.removeAt(index.row());
endRemoveRows();
}
} else {
switch (index.column()) {
case RemuxEntryColumn::InputPath:
queue[index.row()].sourcePath = value.toString();
checkInputPath(index.row());
success = true;
break;
case RemuxEntryColumn::OutputPath:
queue[index.row()].targetPath = value.toString();
emit dataChanged(index, index);
success = true;
break;
}
}
}
return success;
}
QVariant RemuxQueueModel::getIcon(RemuxEntryState state)
{
QVariant icon;
QStyle *style = QApplication::style();
switch (state) {
case RemuxEntryState::Complete:
icon = style->standardIcon(QStyle::SP_DialogApplyButton);
break;
case RemuxEntryState::InProgress:
icon = style->standardIcon(QStyle::SP_ArrowRight);
break;
case RemuxEntryState::Error:
icon = style->standardIcon(QStyle::SP_DialogCancelButton);
break;
case RemuxEntryState::InvalidPath:
icon = style->standardIcon(QStyle::SP_MessageBoxWarning);
break;
default:
break;
}
return icon;
}
void RemuxQueueModel::checkInputPath(int row)
{
RemuxQueueEntry &entry = queue[row];
if (entry.sourcePath.isEmpty()) {
entry.state = RemuxEntryState::Empty;
} else {
entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath);
QFileInfo fileInfo(entry.sourcePath);
if (fileInfo.exists())
entry.state = RemuxEntryState::Ready;
else
entry.state = RemuxEntryState::InvalidPath;
QString newExt = ".mp4";
QString suffix = fileInfo.suffix();
if (suffix.contains("mov", Qt::CaseInsensitive) || suffix.contains("mp4", Qt::CaseInsensitive)) {
newExt = ".remuxed." + suffix;
}
if (entry.state == RemuxEntryState::Ready)
entry.targetPath = QDir::toNativeSeparators(fileInfo.path() + QDir::separator() +
fileInfo.completeBaseName() + newExt);
}
if (entry.state == RemuxEntryState::Ready && isProcessing)
entry.state = RemuxEntryState::Pending;
emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count));
}
QFileInfoList RemuxQueueModel::checkForOverwrites() const
{
QFileInfoList list;
for (const RemuxQueueEntry &entry : queue) {
if (entry.state == RemuxEntryState::Ready) {
QFileInfo fileInfo(entry.targetPath);
if (fileInfo.exists()) {
list.append(fileInfo);
}
}
}
return list;
}
bool RemuxQueueModel::checkForErrors() const
{
bool hasErrors = false;
for (const RemuxQueueEntry &entry : queue) {
if (entry.state == RemuxEntryState::Error) {
hasErrors = true;
break;
}
}
return hasErrors;
}
void RemuxQueueModel::clearAll()
{
beginRemoveRows(QModelIndex(), 0, queue.size() - 1);
queue.clear();
endRemoveRows();
}
void RemuxQueueModel::clearFinished()
{
int index = 0;
for (index = 0; index < queue.size(); index++) {
const RemuxQueueEntry &entry = queue[index];
if (entry.state == RemuxEntryState::Complete) {
beginRemoveRows(QModelIndex(), index, index);
queue.removeAt(index);
endRemoveRows();
index--;
}
}
}
bool RemuxQueueModel::canClearFinished() const
{
bool canClearFinished = false;
for (const RemuxQueueEntry &entry : queue)
if (entry.state == RemuxEntryState::Complete) {
canClearFinished = true;
break;
}
return canClearFinished;
}
void RemuxQueueModel::beginProcessing()
{
for (RemuxQueueEntry &entry : queue)
if (entry.state == RemuxEntryState::Ready)
entry.state = RemuxEntryState::Pending;
// Signal that the insertion point no longer exists.
beginRemoveRows(QModelIndex(), queue.length(), queue.length());
endRemoveRows();
isProcessing = true;
emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State));
}
void RemuxQueueModel::endProcessing()
{
for (RemuxQueueEntry &entry : queue) {
if (entry.state == RemuxEntryState::Pending) {
entry.state = RemuxEntryState::Ready;
}
}
// Signal that the insertion point exists again.
isProcessing = false;
if (!autoRemux) {
beginInsertRows(QModelIndex(), queue.length(), queue.length());
endInsertRows();
}
emit dataChanged(index(0, RemuxEntryColumn::State), index(queue.length(), RemuxEntryColumn::State));
}
bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath)
{
bool anyStarted = false;
for (int row = 0; row < queue.length(); row++) {
RemuxQueueEntry &entry = queue[row];
if (entry.state == RemuxEntryState::Pending) {
entry.state = RemuxEntryState::InProgress;
inputPath = entry.sourcePath;
outputPath = entry.targetPath;
QModelIndex index = this->index(row, RemuxEntryColumn::State);
emit dataChanged(index, index);
anyStarted = true;
break;
}
}
return anyStarted;
}
void RemuxQueueModel::finishEntry(bool success)
{
for (int row = 0; row < queue.length(); row++) {
RemuxQueueEntry &entry = queue[row];
if (entry.state == RemuxEntryState::InProgress) {
if (success)
entry.state = RemuxEntryState::Complete;
else
entry.state = RemuxEntryState::Error;
QModelIndex index = this->index(row, RemuxEntryColumn::State);
emit dataChanged(index, index);
break;
}
}
}
/**********************************************************
The actual remux window implementation
**********************************************************/
OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_)
: QDialog(parent),
queueModel(new RemuxQueueModel),
worker(new RemuxWorker()),
ui(new Ui::OBSRemux),
recPath(path),
autoRemux(autoRemux_)
{
setAcceptDrops(true);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false);
if (autoRemux) {
resize(280, 40);
ui->tableView->hide();
ui->buttonBox->hide();
ui->label->hide();
}
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(1000);
ui->progressBar->setValue(0);
ui->tableView->setModel(queueModel);
ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath,
new RemuxEntryPathItemDelegate(false, recPath));
ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath,
new RemuxEntryPathItemDelegate(true, recPath));
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
ui->tableView->horizontalHeader()->setSectionResizeMode(RemuxEntryColumn::State,
QHeaderView::ResizeMode::Fixed);
ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged);
ui->tableView->setTextElideMode(Qt::ElideMiddle);
ui->tableView->setWordWrap(false);
installEventFilter(CreateShortcutFilter());
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux"));
ui->buttonBox->button(QDialogButtonBox::Reset)->setText(QTStr("Remux.ClearFinished"));
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(QTStr("Remux.ClearAll"));
ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true);
connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &OBSRemux::beginRemux);
connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &OBSRemux::clearFinished);
connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,
&OBSRemux::clearAll);
connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSRemux::close);
worker->moveToThread(&remuxer);
remuxer.start();
connect(worker.data(), &RemuxWorker::updateProgress, this, &OBSRemux::updateProgress);
connect(&remuxer, &QThread::finished, worker.data(), &QObject::deleteLater);
connect(worker.data(), &RemuxWorker::remuxFinished, this, &OBSRemux::remuxFinished);
connect(this, &OBSRemux::remux, worker.data(), &RemuxWorker::remux);
connect(queueModel.data(), &RemuxQueueModel::rowsInserted, this, &OBSRemux::rowCountChanged);
connect(queueModel.data(), &RemuxQueueModel::rowsRemoved, this, &OBSRemux::rowCountChanged);
QModelIndex index = queueModel->createIndex(0, 1);
QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection,
Q_ARG(const QModelIndex &, index));
}
bool OBSRemux::stopRemux()
{
if (!worker->isWorking)
return true;
// By locking the worker thread's mutex, we ensure that its
// update poll will be blocked as long as we're in here with
// the popup open.
QMutexLocker lock(&worker->updateMutex);
bool exit = false;
if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"), QTStr("Remux.ExitUnfinished"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
exit = true;
}
if (exit) {
// Inform the worker it should no longer be
// working. It will interrupt accordingly in
// its next update callback.
worker->isWorking = false;
}
return exit;
}
OBSRemux::~OBSRemux()
{
stopRemux();
remuxer.quit();
remuxer.wait();
}
void OBSRemux::rowCountChanged(const QModelIndex &, int, int)
{
// See if there are still any rows ready to remux. Change
// the state of the "go" button accordingly.
// There must be more than one row, since there will always be
// at least one row for the empty insertion point.
if (queueModel->rowCount() > 1) {
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true);
ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished());
} else {
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false);
}
}
void OBSRemux::dropEvent(QDropEvent *ev)
{
QStringList urlList;
for (QUrl url : ev->mimeData()->urls()) {
QFileInfo fileInfo(url.toLocalFile());
if (fileInfo.isDir()) {
QStringList directoryFilter;
directoryFilter << "*.flv"
<< "*.mp4"
<< "*.mov"
<< "*.mkv"
<< "*.ts"
<< "*.m3u8";
QDirIterator dirIter(fileInfo.absoluteFilePath(), directoryFilter, QDir::Files,
QDirIterator::Subdirectories);
while (dirIter.hasNext()) {
urlList.append(dirIter.next());
}
} else {
urlList.append(fileInfo.canonicalFilePath());
}
}
if (urlList.empty()) {
QMessageBox::information(nullptr, QTStr("Remux.NoFilesAddedTitle"), QTStr("Remux.NoFilesAdded"),
QMessageBox::Ok);
} else if (!autoRemux) {
QModelIndex insertIndex = queueModel->index(queueModel->rowCount() - 1, RemuxEntryColumn::InputPath);
queueModel->setData(insertIndex, urlList, RemuxEntryRole::NewPathsToProcessRole);
}
}
void OBSRemux::dragEnterEvent(QDragEnterEvent *ev)
{
if (ev->mimeData()->hasUrls() && !worker->isWorking)
ev->accept();
}
void OBSRemux::beginRemux()
{
if (worker->isWorking) {
stopRemux();
return;
}
bool proceedWithRemux = true;
QFileInfoList overwriteFiles = queueModel->checkForOverwrites();
if (!overwriteFiles.empty()) {
QString message = QTStr("Remux.FileExists");
message += "\n\n";
for (QFileInfo fileInfo : overwriteFiles)
message += fileInfo.canonicalFilePath() + "\n";
if (OBSMessageBox::question(this, QTStr("Remux.FileExistsTitle"), message) != QMessageBox::Yes)
proceedWithRemux = false;
}
if (!proceedWithRemux)
return;
// Set all jobs to "pending" first.
queueModel->beginProcessing();
ui->progressBar->setVisible(true);
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Stop"));
setAcceptDrops(false);
remuxNextEntry();
}
void OBSRemux::AutoRemux(QString inFile, QString outFile)
{
if (inFile != "" && outFile != "" && autoRemux) {
ui->progressBar->setVisible(true);
emit remux(inFile, outFile);
autoRemuxFile = outFile;
}
}
void OBSRemux::remuxNextEntry()
{
worker->lastProgress = 0.f;
QString inputPath, outputPath;
if (queueModel->beginNextEntry(inputPath, outputPath)) {
emit remux(inputPath, outputPath);
} else {
queueModel->autoRemux = autoRemux;
queueModel->endProcessing();
if (!autoRemux) {
OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"),
queueModel->checkForErrors() ? QTStr("Remux.FinishedError")
: QTStr("Remux.Finished"));
}
ui->progressBar->setVisible(autoRemux);
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Remux.Remux"));
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(true);
ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(queueModel->canClearFinished());
setAcceptDrops(true);
}
}
void OBSRemux::closeEvent(QCloseEvent *event)
{
if (!stopRemux())
event->ignore();
else
QDialog::closeEvent(event);
}
void OBSRemux::reject()
{
if (!stopRemux())
return;
QDialog::reject();
}
void OBSRemux::updateProgress(float percent)
{
ui->progressBar->setValue(percent * 10);
}
void OBSRemux::remuxFinished(bool success)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
queueModel->finishEntry(success);
if (autoRemux && autoRemuxFile != "") {
QTimer::singleShot(3000, this, &OBSRemux::close);
OBSBasic *main = OBSBasic::Get();
main->ShowStatusBarMessage(QTStr("Basic.StatusBar.AutoRemuxedTo").arg(autoRemuxFile));
}
remuxNextEntry();
}
void OBSRemux::clearFinished()
{
queueModel->clearFinished();
}
void OBSRemux::clearAll()
{
queueModel->clearAll();
}
/**********************************************************
Worker thread - Executes the libobs remux operation as a
background process.
**********************************************************/
void RemuxWorker::UpdateProgress(float percent)
{
if (abs(lastProgress - percent) < 0.1f)
return;
emit updateProgress(percent);
lastProgress = percent;
}
void RemuxWorker::remux(const QString &source, const QString &target)
{
isWorking = true;
auto callback = [](void *data, float percent) {
RemuxWorker *rw = static_cast<RemuxWorker *>(data);
QMutexLocker lock(&rw->updateMutex);
rw->UpdateProgress(percent);
return rw->isWorking;
};
bool stopped = false;
bool success = false;
media_remux_job_t mr_job = nullptr;
if (media_remux_job_create(&mr_job, QT_TO_UTF8(source), QT_TO_UTF8(target))) {
success = media_remux_job_process(mr_job, callback, this);
media_remux_job_destroy(mr_job);
stopped = !isWorking;
}
isWorking = false;
emit remuxFinished(!stopped && success);
}

View File

@@ -1,173 +0,0 @@
/******************************************************************************
Copyright (C) 2014 by Ruwen Hahn <palana@stunned.de>
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 <QFileInfo>
#include <QMutex>
#include <QPointer>
#include <QThread>
#include <QStyledItemDelegate>
#include <memory>
#include "ui_OBSRemux.h"
#include <media-io/media-remux.h>
#include <util/threading.h>
class RemuxQueueModel;
class RemuxWorker;
enum RemuxEntryState { Empty, Ready, Pending, InProgress, Complete, InvalidPath, Error };
Q_DECLARE_METATYPE(RemuxEntryState);
class OBSRemux : public QDialog {
Q_OBJECT
QPointer<RemuxQueueModel> queueModel;
QThread remuxer;
QPointer<RemuxWorker> worker;
std::unique_ptr<Ui::OBSRemux> ui;
const char *recPath;
virtual void closeEvent(QCloseEvent *event) override;
virtual void reject() override;
bool autoRemux;
QString autoRemuxFile;
public:
explicit OBSRemux(const char *recPath, QWidget *parent = nullptr, bool autoRemux = false);
virtual ~OBSRemux() override;
using job_t = std::shared_ptr<struct media_remux_job>;
void AutoRemux(QString inFile, QString outFile);
protected:
virtual void dropEvent(QDropEvent *ev) override;
virtual void dragEnterEvent(QDragEnterEvent *ev) override;
void remuxNextEntry();
private slots:
void rowCountChanged(const QModelIndex &parent, int first, int last);
public slots:
void updateProgress(float percent);
void remuxFinished(bool success);
void beginRemux();
bool stopRemux();
void clearFinished();
void clearAll();
signals:
void remux(const QString &source, const QString &target);
};
class RemuxQueueModel : public QAbstractTableModel {
Q_OBJECT
friend class OBSRemux;
public:
RemuxQueueModel(QObject *parent = 0) : QAbstractTableModel(parent), isProcessing(false) {}
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);
QFileInfoList checkForOverwrites() const;
bool checkForErrors() const;
void beginProcessing();
void endProcessing();
bool beginNextEntry(QString &inputPath, QString &outputPath);
void finishEntry(bool success);
bool canClearFinished() const;
void clearFinished();
void clearAll();
bool autoRemux = false;
private:
struct RemuxQueueEntry {
RemuxEntryState state;
QString sourcePath;
QString targetPath;
};
QList<RemuxQueueEntry> queue;
bool isProcessing;
static QVariant getIcon(RemuxEntryState state);
void checkInputPath(int row);
};
class RemuxWorker : public QObject {
Q_OBJECT
QMutex updateMutex;
bool isWorking;
float lastProgress;
void UpdateProgress(float percent);
explicit RemuxWorker() : isWorking(false) {}
virtual ~RemuxWorker(){};
private slots:
void remux(const QString &source, const QString &target);
signals:
void updateProgress(float percent);
void remuxFinished(bool success);
friend class OBSRemux;
};
class RemuxEntryPathItemDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath);
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:
bool isOutput;
QString defaultPath;
const char *PATH_LIST_PROP = "pathList";
void handleBrowse(QWidget *container);
void handleClear(QWidget *container);
private slots:
void updateText();
};

View File

@@ -54,7 +54,7 @@ invoke_formatter() {
exit 2
fi
if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N))
if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins|deps|shared)/**/*.(c|cpp|h|hpp|m|mm)(.N))
source_files=(${source_files:#*/(obs-websocket/deps|decklink/*/decklink-sdk|mac-syphon/syphon-framework|libdshowcapture)/*})
@@ -102,7 +102,7 @@ invoke_formatter() {
fi
}
if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|UI|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N))
if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N))
source_files=(${source_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*})
source_files=(${source_files:#(cmake/Modules/*|*/legacy.cmake)})
@@ -150,7 +150,7 @@ invoke_formatter() {
exit 2
}
if (( ! #source_files )) source_files=((libobs|libobs-*|UI|plugins)/**/*.swift(.N))
if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N))
check_files() {
local -i num_failures=0

View File

@@ -5,7 +5,7 @@ include_guard(GLOBAL)
include(cpackconfig_common)
# Add GPLv2 license file to CPack
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt")
set(CPACK_PACKAGE_EXECUTABLES "obs")
if(ENABLE_RELEASE_BUILD)

View File

@@ -5,7 +5,7 @@ include_guard(GLOBAL)
include(cpackconfig_common)
# Add GPLv2 license file to CPack
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/UI/data/license/gplv2.txt")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/frontend/data/license/gplv2.txt")
set(CPACK_PACKAGE_VERSION "${OBS_VERSION_CANONICAL}")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-windows-${CMAKE_VS_PLATFORM_NAME}")
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY FALSE)

View File

@@ -1,10 +1,10 @@
cmake_minimum_required(VERSION 3.28...3.30)
add_subdirectory(obs-frontend-api)
add_subdirectory(api)
option(ENABLE_UI "Enable building with UI (requires Qt)" ON)
option(ENABLE_FRONTEND "Enable building with UI frontend (requires Qt6)" ON)
if(NOT ENABLE_UI)
if(NOT ENABLE_FRONTEND)
target_disable_feature(obs "User Interface")
return()
else()
@@ -15,11 +15,15 @@ find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat)
find_package(CURL REQUIRED)
if(NOT TARGET OBS::json11)
add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" "${CMAKE_BINARY_DIR}/deps/json11")
add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" json11)
endif()
if(NOT TARGET OBS::libobs)
add_subdirectory("${CMAKE_SOURCE_DIR}/libobs" libobs)
endif()
if(NOT TARGET OBS::bpm)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" "${CMAKE_BINARY_DIR}/shared/bpm")
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/bpm" bpm)
endif()
add_executable(obs-studio)
@@ -32,17 +36,23 @@ target_link_libraries(
FFmpeg::avcodec
FFmpeg::avutil
FFmpeg::avformat
OBS::bpm
OBS::libobs
OBS::frontend-api
OBS::json11
OBS::bpm
)
include(cmake/ui-qt.cmake)
include(cmake/ui-elements.cmake)
include(cmake/ui-windows.cmake)
include(cmake/ui-components.cmake)
include(cmake/ui-dialogs.cmake)
include(cmake/ui-docks.cmake)
include(cmake/feature-importers.cmake)
include(cmake/ui-oauth.cmake)
include(cmake/feature-browserpanels.cmake)
include(cmake/ui-qt.cmake)
include(cmake/ui-settings.cmake)
include(cmake/ui-utility.cmake)
include(cmake/ui-widgets.cmake)
include(cmake/ui-wizards.cmake)
if(NOT OAUTH_BASE_URL)
set(OAUTH_BASE_URL "https://auth.obsproject.com/" CACHE STRING "Default OAuth base URL")
@@ -53,56 +63,13 @@ include(cmake/feature-restream.cmake)
include(cmake/feature-youtube.cmake)
include(cmake/feature-whatsnew.cmake)
add_subdirectory(frontend-plugins)
add_subdirectory(plugins)
configure_file(ui-config.h.in ui-config.h)
configure_file(cmake/templates/ui-config.h.in ui-config.h)
target_sources(
obs-studio
PRIVATE
api-interface.cpp
auth-base.cpp
auth-base.hpp
auth-listener.cpp
auth-listener.hpp
auth-oauth.cpp
auth-oauth.hpp
display-helpers.hpp
ffmpeg-utils.cpp
ffmpeg-utils.hpp
multiview.cpp
multiview.hpp
obf.c
obf.h
obs-app-theming.cpp
obs-app-theming.hpp
obs-app.cpp
obs-app.hpp
obs-proxy-style.cpp
obs-proxy-style.hpp
platform.hpp
qt-display.cpp
qt-display.hpp
ui-config.h
ui-validation.cpp
ui-validation.hpp
)
target_sources(
obs-studio
PRIVATE
goliveapi-censoredjson.cpp
goliveapi-censoredjson.hpp
goliveapi-network.cpp
goliveapi-network.hpp
goliveapi-postdata.cpp
goliveapi-postdata.hpp
models/multitrack-video.hpp
multitrack-video-error.cpp
multitrack-video-error.hpp
multitrack-video-output.cpp
multitrack-video-output.hpp
system-info.hpp
PRIVATE obs-main.cpp OBSStudioAPI.cpp OBSStudioAPI.hpp OBSApp.cpp OBSApp.hpp OBSApp_Themes.cpp ui-config.h
)
if(OS_WINDOWS)
@@ -132,4 +99,18 @@ get_property(obs_module_list GLOBAL PROPERTY OBS_MODULES_ENABLED)
list(JOIN obs_module_list "|" SAFE_MODULES)
target_compile_definitions(obs-studio PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"")
get_target_property(target_sources obs-studio SOURCES)
set(target_cpp_sources ${target_sources})
set(target_hpp_sources ${target_sources})
set(target_qt_sources ${target_sources})
list(FILTER target_cpp_sources INCLUDE REGEX ".+\\.(cpp|mm|c|m)")
list(SORT target_cpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING)
list(FILTER target_hpp_sources INCLUDE REGEX ".+\\.(hpp|h)")
list(SORT target_hpp_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING)
list(FILTER target_qt_sources INCLUDE REGEX ".+\\.(ui|qrc)")
list(SORT target_qt_sources COMPARE NATURAL CASE SENSITIVE ORDER ASCENDING)
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${target_cpp_sources})
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Header Files" FILES ${target_hpp_sources})
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Qt Files" FILES ${target_qt_sources})
set_target_properties_obs(obs-studio PROPERTIES FOLDER frontend OUTPUT_NAME "$<IF:$<PLATFORM_ID:Windows>,obs64,obs>")

View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,63 +17,30 @@
#pragma once
#include <QApplication>
#include <QTranslator>
#include <QPointer>
#include <QFileSystemWatcher>
#include <utility/OBSTheme.hpp>
#include <widgets/OBSMainWindow.hpp>
#ifndef _WIN32
#include <QSocketNotifier>
#else
#include <QSessionManager>
#endif
#include <obs.hpp>
#include <util/lexer.h>
#include <util/profiler.h>
#include <util/util.hpp>
#include <util/platform.h>
#include <obs-frontend-api.h>
#include <util/platform.h>
#include <util/profiler.hpp>
#include <util/util.hpp>
#include <QApplication>
#include <QPalette>
#include <QPointer>
#include <deque>
#include <functional>
#include <string>
#include <memory>
#include <vector>
#include <deque>
#include <filesystem>
#include "window-main.hpp"
#include "obs-app-theming.hpp"
std::string CurrentTimeString();
std::string CurrentDateTimeString();
std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false);
std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format);
std::string GetFormatString(const char *format, const char *prefix, const char *suffix);
std::string GetFormatExt(const char *container);
std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite,
const char *format);
QObject *CreateShortcutFilter();
struct BaseLexer {
lexer lex;
public:
inline BaseLexer() { lexer_init(&lex); }
inline ~BaseLexer() { lexer_free(&lex); }
operator lexer *() { return &lex; }
};
class OBSTranslator : public QTranslator {
Q_OBJECT
public:
virtual bool isEmpty() const override { return false; }
virtual QString translate(const char *context, const char *sourceText, const char *disambiguation,
int n) const override;
};
typedef std::function<void()> VoidFunc;
Q_DECLARE_METATYPE(VoidFunc)
class QFileSystemWatcher;
class QSocketNotifier;
struct UpdateBranch {
QString name;
QString display_name;
@@ -233,9 +200,6 @@ signals:
int GetAppConfigPath(char *path, size_t size, const char *name);
char *GetAppConfigPathPtr(const char *name);
int GetProgramDataPath(char *path, size_t size, const char *name);
char *GetProgramDataPathPtr(const char *name);
inline OBSApp *App()
{
return static_cast<OBSApp *>(qApp);
@@ -251,30 +215,23 @@ inline QString QTStr(const char *lookupVal)
return QString::fromUtf8(Str(lookupVal));
}
int GetProgramDataPath(char *path, size_t size, const char *name);
char *GetProgramDataPathPtr(const char *name);
bool GetFileSafeName(const char *name, std::string &file);
bool GetClosestUnusedFileName(std::string &path, const char *extension);
bool GetUnusedSceneCollectionFile(std::string &name, std::string &file);
bool WindowPositionValid(QRect rect);
extern bool portable_mode;
extern bool steam;
extern bool safe_mode;
extern bool disable_3p_plugins;
extern bool opt_start_streaming;
extern bool opt_start_recording;
extern bool opt_start_replaybuffer;
extern bool opt_start_virtualcam;
extern bool opt_minimize_tray;
extern bool opt_studio_mode;
extern bool opt_allow_opengl;
extern bool opt_always_on_top;
extern std::string opt_starting_scene;
extern bool restart;
extern bool restart_safe;
#ifdef _WIN32
extern "C" void install_dll_blocklist_hook(void);
extern "C" void log_blocked_dlls(void);
#endif
std::string CurrentDateTimeString();
std::string GetFormatString(const char *format, const char *prefix, const char *suffix);
std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false);
std::string GetFormatExt(const char *container);
std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite,
const char *format);
QObject *CreateShortcutFilter();

View File

@@ -15,25 +15,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include <cinttypes>
#include "OBSApp.hpp"
#include <utility/OBSProxyStyle.hpp>
#include <utility/OBSThemeVariable.hpp>
#include <utility/platform.hpp>
#include <qt-wrappers.hpp>
#include <ui-config.h>
#include <util/cf-parser.h>
#include <QDir>
#include <QFile>
#include <QTimer>
#include <QMetaEnum>
#include <QDirIterator>
#include <QGuiApplication>
#include <QFile>
#include <QFileSystemWatcher>
#include <QMetaEnum>
#include <QRandomGenerator>
#include "qt-wrappers.hpp"
#include "obs-app.hpp"
#include "obs-app-theming.hpp"
#include "obs-proxy-style.hpp"
#include "platform.hpp"
#include "ui-config.h"
#include <QTimer>
using namespace std;

726
frontend/OBSStudioAPI.cpp Normal file
View File

@@ -0,0 +1,726 @@
#include "OBSStudioAPI.hpp"
#include <widgets/OBSBasic.hpp>
#include <widgets/OBSProjector.hpp>
#include <qt-wrappers.hpp>
extern volatile bool streaming_active;
extern volatile bool recording_active;
extern volatile bool recording_paused;
extern volatile bool replaybuf_active;
extern volatile bool virtualcam_active;
template<typename T>
inline size_t GetCallbackIdx(vector<OBSStudioCallback<T>> &callbacks, T callback, void *private_data)
{
for (size_t i = 0; i < callbacks.size(); i++) {
OBSStudioCallback<T> curCB = callbacks[i];
if (curCB.callback == callback && curCB.private_data == private_data)
return i;
}
return (size_t)-1;
}
void *OBSStudioAPI::obs_frontend_get_main_window()
{
return (void *)main;
}
void *OBSStudioAPI::obs_frontend_get_main_window_handle()
{
return (void *)main->winId();
}
void *OBSStudioAPI::obs_frontend_get_system_tray()
{
return (void *)main->trayIcon.data();
}
void OBSStudioAPI::obs_frontend_get_scenes(struct obs_frontend_source_list *sources)
{
for (int i = 0; i < main->ui->scenes->count(); i++) {
QListWidgetItem *item = main->ui->scenes->item(i);
OBSScene scene = GetOBSRef<OBSScene>(item);
obs_source_t *source = obs_scene_get_source(scene);
if (obs_source_get_ref(source) != nullptr)
da_push_back(sources->sources, &source);
}
}
obs_source_t *OBSStudioAPI::obs_frontend_get_current_scene()
{
if (main->IsPreviewProgramMode()) {
return obs_weak_source_get_source(main->programScene);
} else {
OBSSource source = main->GetCurrentSceneSource();
return obs_source_get_ref(source);
}
}
void OBSStudioAPI::obs_frontend_set_current_scene(obs_source_t *scene)
{
if (main->IsPreviewProgramMode()) {
QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(),
Q_ARG(OBSSource, OBSSource(scene)));
} else {
QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), Q_ARG(OBSSource, OBSSource(scene)),
Q_ARG(bool, false));
}
}
void OBSStudioAPI::obs_frontend_get_transitions(struct obs_frontend_source_list *sources)
{
for (int i = 0; i < main->ui->transitions->count(); i++) {
OBSSource tr = main->ui->transitions->itemData(i).value<OBSSource>();
if (!tr)
continue;
if (obs_source_get_ref(tr) != nullptr)
da_push_back(sources->sources, &tr);
}
}
obs_source_t *OBSStudioAPI::obs_frontend_get_current_transition()
{
OBSSource tr = main->GetCurrentTransition();
return obs_source_get_ref(tr);
}
void OBSStudioAPI::obs_frontend_set_current_transition(obs_source_t *transition)
{
QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition)));
}
int OBSStudioAPI::obs_frontend_get_transition_duration()
{
return main->ui->transitionDuration->value();
}
void OBSStudioAPI::obs_frontend_set_transition_duration(int duration)
{
QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration));
}
void OBSStudioAPI::obs_frontend_release_tbar()
{
QMetaObject::invokeMethod(main, "TBarReleased");
}
void OBSStudioAPI::obs_frontend_set_tbar_position(int position)
{
QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position));
}
int OBSStudioAPI::obs_frontend_get_tbar_position()
{
return main->tBar->value();
}
void OBSStudioAPI::obs_frontend_get_scene_collections(std::vector<std::string> &strings)
{
for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) {
strings.emplace_back(collectionName);
}
}
char *OBSStudioAPI::obs_frontend_get_current_scene_collection()
{
const OBSSceneCollection &currentCollection = main->GetCurrentSceneCollection();
return bstrdup(currentCollection.name.c_str());
}
void OBSStudioAPI::obs_frontend_set_current_scene_collection(const char *collection)
{
QList<QAction *> menuActions = main->ui->sceneCollectionMenu->actions();
QString qstrCollection = QT_UTF8(collection);
for (int i = 0; i < menuActions.count(); i++) {
QAction *action = menuActions[i];
QVariant v = action->property("file_name");
if (v.typeName() != nullptr) {
if (action->text() == qstrCollection) {
action->trigger();
break;
}
}
}
}
bool OBSStudioAPI::obs_frontend_add_scene_collection(const char *name)
{
bool success = false;
QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), Q_RETURN_ARG(bool, success),
Q_ARG(QString, QT_UTF8(name)));
return success;
}
void OBSStudioAPI::obs_frontend_get_profiles(std::vector<std::string> &strings)
{
const OBSProfileCache &profiles = main->GetProfileCache();
for (auto &[profileName, profile] : profiles) {
strings.emplace_back(profileName);
}
}
char *OBSStudioAPI::obs_frontend_get_current_profile()
{
const OBSProfile &profile = main->GetCurrentProfile();
return bstrdup(profile.name.c_str());
}
char *OBSStudioAPI::obs_frontend_get_current_profile_path()
{
const OBSProfile &profile = main->GetCurrentProfile();
return bstrdup(profile.path.u8string().c_str());
}
void OBSStudioAPI::obs_frontend_set_current_profile(const char *profile)
{
QList<QAction *> menuActions = main->ui->profileMenu->actions();
QString qstrProfile = QT_UTF8(profile);
for (int i = 0; i < menuActions.count(); i++) {
QAction *action = menuActions[i];
QVariant v = action->property("file_name");
if (v.typeName() != nullptr) {
if (action->text() == qstrProfile) {
action->trigger();
break;
}
}
}
}
void OBSStudioAPI::obs_frontend_create_profile(const char *name)
{
QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name));
}
void OBSStudioAPI::obs_frontend_duplicate_profile(const char *name)
{
QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name));
}
void OBSStudioAPI::obs_frontend_delete_profile(const char *profile)
{
QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile));
}
void OBSStudioAPI::obs_frontend_streaming_start()
{
QMetaObject::invokeMethod(main, "StartStreaming");
}
void OBSStudioAPI::obs_frontend_streaming_stop()
{
QMetaObject::invokeMethod(main, "StopStreaming");
}
bool OBSStudioAPI::obs_frontend_streaming_active()
{
return os_atomic_load_bool(&streaming_active);
}
void OBSStudioAPI::obs_frontend_recording_start()
{
QMetaObject::invokeMethod(main, "StartRecording");
}
void OBSStudioAPI::obs_frontend_recording_stop()
{
QMetaObject::invokeMethod(main, "StopRecording");
}
bool OBSStudioAPI::obs_frontend_recording_active()
{
return os_atomic_load_bool(&recording_active);
}
void OBSStudioAPI::obs_frontend_recording_pause(bool pause)
{
QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording");
}
bool OBSStudioAPI::obs_frontend_recording_paused()
{
return os_atomic_load_bool(&recording_paused);
}
bool OBSStudioAPI::obs_frontend_recording_split_file()
{
if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) {
proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput);
uint8_t stack[128];
calldata cd;
calldata_init_fixed(&cd, stack, sizeof(stack));
proc_handler_call(ph, "split_file", &cd);
bool result = calldata_bool(&cd, "split_file_enabled");
return result;
} else {
return false;
}
}
bool OBSStudioAPI::obs_frontend_recording_add_chapter(const char *name)
{
if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused))
return false;
proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput);
calldata cd;
calldata_init(&cd);
calldata_set_string(&cd, "chapter_name", name);
bool result = proc_handler_call(ph, "add_chapter", &cd);
calldata_free(&cd);
return result;
}
void OBSStudioAPI::obs_frontend_replay_buffer_start()
{
QMetaObject::invokeMethod(main, "StartReplayBuffer");
}
void OBSStudioAPI::obs_frontend_replay_buffer_save()
{
QMetaObject::invokeMethod(main, "ReplayBufferSave");
}
void OBSStudioAPI::obs_frontend_replay_buffer_stop()
{
QMetaObject::invokeMethod(main, "StopReplayBuffer");
}
bool OBSStudioAPI::obs_frontend_replay_buffer_active()
{
return os_atomic_load_bool(&replaybuf_active);
}
void *OBSStudioAPI::obs_frontend_add_tools_menu_qaction(const char *name)
{
main->ui->menuTools->setEnabled(true);
return (void *)main->ui->menuTools->addAction(QT_UTF8(name));
}
void OBSStudioAPI::obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data)
{
main->ui->menuTools->setEnabled(true);
auto func = [private_data, callback]() {
callback(private_data);
};
QAction *action = main->ui->menuTools->addAction(QT_UTF8(name));
QObject::connect(action, &QAction::triggered, func);
}
void *OBSStudioAPI::obs_frontend_add_dock(void *dock)
{
QDockWidget *d = reinterpret_cast<QDockWidget *>(dock);
QString name = d->objectName();
if (name.isEmpty() || main->IsDockObjectNameUsed(name)) {
blog(LOG_WARNING, "The object name of the added dock is empty or already used,"
" a temporary one will be set to avoid conflicts");
char *uuid = os_generate_uuid();
name = QT_UTF8(uuid);
bfree(uuid);
name.append("_oldExtraDock");
d->setObjectName(name);
}
return (void *)main->AddDockWidget(d);
}
bool OBSStudioAPI::obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget)
{
if (main->IsDockObjectNameUsed(QT_UTF8(id))) {
blog(LOG_WARNING,
"Dock id '%s' already used! "
"Duplicate library?",
id);
return false;
}
OBSDock *dock = new OBSDock(main);
dock->setWidget((QWidget *)widget);
dock->setWindowTitle(QT_UTF8(title));
dock->setObjectName(QT_UTF8(id));
main->AddDockWidget(dock, Qt::RightDockWidgetArea);
dock->setVisible(false);
dock->setFloating(true);
return true;
}
void OBSStudioAPI::obs_frontend_remove_dock(const char *id)
{
main->RemoveDockWidget(QT_UTF8(id));
}
bool OBSStudioAPI::obs_frontend_add_custom_qdock(const char *id, void *dock)
{
if (main->IsDockObjectNameUsed(QT_UTF8(id))) {
blog(LOG_WARNING,
"Dock id '%s' already used! "
"Duplicate library?",
id);
return false;
}
QDockWidget *d = reinterpret_cast<QDockWidget *>(dock);
d->setObjectName(QT_UTF8(id));
main->AddCustomDockWidget(d);
return true;
}
void OBSStudioAPI::obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(callbacks, callback, private_data);
if (idx == (size_t)-1)
callbacks.emplace_back(callback, private_data);
}
void OBSStudioAPI::obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(callbacks, callback, private_data);
if (idx == (size_t)-1)
return;
callbacks.erase(callbacks.begin() + idx);
}
obs_output_t *OBSStudioAPI::obs_frontend_get_streaming_output()
{
auto multitrackVideo = main->outputHandler->multitrackVideo.get();
auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr;
if (mtvOutput)
return mtvOutput;
OBSOutput output = main->outputHandler->streamOutput.Get();
return obs_output_get_ref(output);
}
obs_output_t *OBSStudioAPI::obs_frontend_get_recording_output()
{
OBSOutput out = main->outputHandler->fileOutput.Get();
return obs_output_get_ref(out);
}
obs_output_t *OBSStudioAPI::obs_frontend_get_replay_buffer_output()
{
OBSOutput out = main->outputHandler->replayBuffer.Get();
return obs_output_get_ref(out);
}
config_t *OBSStudioAPI::obs_frontend_get_profile_config()
{
return main->activeConfiguration;
}
config_t *OBSStudioAPI::obs_frontend_get_global_config()
{
blog(LOG_WARNING,
"DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead.");
return App()->GetAppConfig();
}
config_t *OBSStudioAPI::obs_frontend_get_app_config()
{
return App()->GetAppConfig();
}
config_t *OBSStudioAPI::obs_frontend_get_user_config()
{
return App()->GetUserConfig();
}
void OBSStudioAPI::obs_frontend_open_projector(const char *type, int monitor, const char *geometry, const char *name)
{
SavedProjectorInfo proj = {
ProjectorType::Preview,
monitor,
geometry ? geometry : "",
name ? name : "",
};
if (type) {
if (astrcmpi(type, "Source") == 0)
proj.type = ProjectorType::Source;
else if (astrcmpi(type, "Scene") == 0)
proj.type = ProjectorType::Scene;
else if (astrcmpi(type, "StudioProgram") == 0)
proj.type = ProjectorType::StudioProgram;
else if (astrcmpi(type, "Multiview") == 0)
proj.type = ProjectorType::Multiview;
}
QMetaObject::invokeMethod(main, "OpenSavedProjector", WaitConnection(), Q_ARG(SavedProjectorInfo *, &proj));
}
void OBSStudioAPI::obs_frontend_save()
{
main->SaveProject();
}
void OBSStudioAPI::obs_frontend_defer_save_begin()
{
QMetaObject::invokeMethod(main, "DeferSaveBegin");
}
void OBSStudioAPI::obs_frontend_defer_save_end()
{
QMetaObject::invokeMethod(main, "DeferSaveEnd");
}
void OBSStudioAPI::obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data);
if (idx == (size_t)-1)
saveCallbacks.emplace_back(callback, private_data);
}
void OBSStudioAPI::obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(saveCallbacks, callback, private_data);
if (idx == (size_t)-1)
return;
saveCallbacks.erase(saveCallbacks.begin() + idx);
}
void OBSStudioAPI::obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data);
if (idx == (size_t)-1)
preloadCallbacks.emplace_back(callback, private_data);
}
void OBSStudioAPI::obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data)
{
size_t idx = GetCallbackIdx(preloadCallbacks, callback, private_data);
if (idx == (size_t)-1)
return;
preloadCallbacks.erase(preloadCallbacks.begin() + idx);
}
void OBSStudioAPI::obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate)
{
App()->PushUITranslation(translate);
}
void OBSStudioAPI::obs_frontend_pop_ui_translation()
{
App()->PopUITranslation();
}
void OBSStudioAPI::obs_frontend_set_streaming_service(obs_service_t *service)
{
main->SetService(service);
}
obs_service_t *OBSStudioAPI::obs_frontend_get_streaming_service()
{
return main->GetService();
}
void OBSStudioAPI::obs_frontend_save_streaming_service()
{
main->SaveService();
}
bool OBSStudioAPI::obs_frontend_preview_program_mode_active()
{
return main->IsPreviewProgramMode();
}
void OBSStudioAPI::obs_frontend_set_preview_program_mode(bool enable)
{
main->SetPreviewProgramMode(enable);
}
void OBSStudioAPI::obs_frontend_preview_program_trigger_transition()
{
QMetaObject::invokeMethod(main, "TransitionClicked");
}
bool OBSStudioAPI::obs_frontend_preview_enabled()
{
return main->previewEnabled;
}
void OBSStudioAPI::obs_frontend_set_preview_enabled(bool enable)
{
if (main->previewEnabled != enable)
main->EnablePreviewDisplay(enable);
}
obs_source_t *OBSStudioAPI::obs_frontend_get_current_preview_scene()
{
if (main->IsPreviewProgramMode()) {
OBSSource source = main->GetCurrentSceneSource();
return obs_source_get_ref(source);
}
return nullptr;
}
void OBSStudioAPI::obs_frontend_set_current_preview_scene(obs_source_t *scene)
{
if (main->IsPreviewProgramMode()) {
QMetaObject::invokeMethod(main, "SetCurrentScene", Q_ARG(OBSSource, OBSSource(scene)),
Q_ARG(bool, false));
}
}
void OBSStudioAPI::obs_frontend_take_screenshot()
{
QMetaObject::invokeMethod(main, "Screenshot");
}
void OBSStudioAPI::obs_frontend_take_source_screenshot(obs_source_t *source)
{
QMetaObject::invokeMethod(main, "Screenshot", Q_ARG(OBSSource, OBSSource(source)));
}
obs_output_t *OBSStudioAPI::obs_frontend_get_virtualcam_output()
{
OBSOutput output = main->outputHandler->virtualCam.Get();
return obs_output_get_ref(output);
}
void OBSStudioAPI::obs_frontend_start_virtualcam()
{
QMetaObject::invokeMethod(main, "StartVirtualCam");
}
void OBSStudioAPI::obs_frontend_stop_virtualcam()
{
QMetaObject::invokeMethod(main, "StopVirtualCam");
}
bool OBSStudioAPI::obs_frontend_virtualcam_active()
{
return os_atomic_load_bool(&virtualcam_active);
}
void OBSStudioAPI::obs_frontend_reset_video()
{
main->ResetVideo();
}
void OBSStudioAPI::obs_frontend_open_source_properties(obs_source_t *source)
{
QMetaObject::invokeMethod(main, "OpenProperties", Q_ARG(OBSSource, OBSSource(source)));
}
void OBSStudioAPI::obs_frontend_open_source_filters(obs_source_t *source)
{
QMetaObject::invokeMethod(main, "OpenFilters", Q_ARG(OBSSource, OBSSource(source)));
}
void OBSStudioAPI::obs_frontend_open_source_interaction(obs_source_t *source)
{
QMetaObject::invokeMethod(main, "OpenInteraction", Q_ARG(OBSSource, OBSSource(source)));
}
void OBSStudioAPI::obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item)
{
QMetaObject::invokeMethod(main, "OpenEditTransform", Q_ARG(OBSSceneItem, OBSSceneItem(item)));
}
char *OBSStudioAPI::obs_frontend_get_current_record_output_path()
{
const char *recordOutputPath = main->GetCurrentOutputPath();
return bstrdup(recordOutputPath);
}
const char *OBSStudioAPI::obs_frontend_get_locale_string(const char *string)
{
return Str(string);
}
bool OBSStudioAPI::obs_frontend_is_theme_dark()
{
return App()->IsThemeDark();
}
char *OBSStudioAPI::obs_frontend_get_last_recording()
{
return bstrdup(main->outputHandler->lastRecordingPath.c_str());
}
char *OBSStudioAPI::obs_frontend_get_last_screenshot()
{
return bstrdup(main->lastScreenshot.c_str());
}
char *OBSStudioAPI::obs_frontend_get_last_replay()
{
return bstrdup(main->lastReplay.c_str());
}
void OBSStudioAPI::obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo,
const char *undo_data, const char *redo_data, bool repeatable)
{
main->undo_s.add_action(
name, [undo](const std::string &data) { undo(data.c_str()); },
[redo](const std::string &data) { redo(data.c_str()); }, undo_data, redo_data, repeatable);
}
void OBSStudioAPI::on_load(obs_data_t *settings)
{
for (size_t i = saveCallbacks.size(); i > 0; i--) {
auto cb = saveCallbacks[i - 1];
cb.callback(settings, false, cb.private_data);
}
}
void OBSStudioAPI::on_preload(obs_data_t *settings)
{
for (size_t i = preloadCallbacks.size(); i > 0; i--) {
auto cb = preloadCallbacks[i - 1];
cb.callback(settings, false, cb.private_data);
}
}
void OBSStudioAPI::on_save(obs_data_t *settings)
{
for (size_t i = saveCallbacks.size(); i > 0; i--) {
auto cb = saveCallbacks[i - 1];
cb.callback(settings, true, cb.private_data);
}
}
void OBSStudioAPI::on_event(enum obs_frontend_event event)
{
if (main->disableSaving && event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP &&
event != OBS_FRONTEND_EVENT_EXIT)
return;
for (size_t i = callbacks.size(); i > 0; i--) {
auto cb = callbacks[i - 1];
cb.callback(event, cb.private_data);
}
}
obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main)
{
obs_frontend_callbacks *api = new OBSStudioAPI(main);
obs_frontend_set_callbacks_internal(api);
return api;
}

236
frontend/OBSStudioAPI.hpp Normal file
View File

@@ -0,0 +1,236 @@
/******************************************************************************
Copyright (C) 2024 by Patrick Heyer <opensource@patrickheyer.com>
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-frontend-internal.hpp>
class OBSBasic;
using namespace std;
template<typename T> struct OBSStudioCallback {
T callback;
void *private_data;
inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {}
};
struct OBSStudioAPI : obs_frontend_callbacks {
OBSBasic *main;
vector<OBSStudioCallback<obs_frontend_event_cb>> callbacks;
vector<OBSStudioCallback<obs_frontend_save_cb>> saveCallbacks;
vector<OBSStudioCallback<obs_frontend_save_cb>> preloadCallbacks;
inline OBSStudioAPI(OBSBasic *main_) : main(main_) {}
void *obs_frontend_get_main_window(void) override;
void *obs_frontend_get_main_window_handle(void) override;
void *obs_frontend_get_system_tray(void) override;
void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override;
obs_source_t *obs_frontend_get_current_scene(void) override;
void obs_frontend_set_current_scene(obs_source_t *scene) override;
void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override;
obs_source_t *obs_frontend_get_current_transition(void) override;
void obs_frontend_set_current_transition(obs_source_t *transition) override;
int obs_frontend_get_transition_duration(void) override;
void obs_frontend_set_transition_duration(int duration) override;
void obs_frontend_release_tbar(void) override;
void obs_frontend_set_tbar_position(int position) override;
int obs_frontend_get_tbar_position(void) override;
void obs_frontend_get_scene_collections(std::vector<std::string> &strings) override;
char *obs_frontend_get_current_scene_collection(void) override;
void obs_frontend_set_current_scene_collection(const char *collection) override;
bool obs_frontend_add_scene_collection(const char *name) override;
void obs_frontend_get_profiles(std::vector<std::string> &strings) override;
char *obs_frontend_get_current_profile(void) override;
char *obs_frontend_get_current_profile_path(void) override;
void obs_frontend_set_current_profile(const char *profile) override;
void obs_frontend_create_profile(const char *name) override;
void obs_frontend_duplicate_profile(const char *name) override;
void obs_frontend_delete_profile(const char *profile) override;
void obs_frontend_streaming_start(void) override;
void obs_frontend_streaming_stop(void) override;
bool obs_frontend_streaming_active(void) override;
void obs_frontend_recording_start(void) override;
void obs_frontend_recording_stop(void) override;
bool obs_frontend_recording_active(void) override;
void obs_frontend_recording_pause(bool pause) override;
bool obs_frontend_recording_paused(void) override;
bool obs_frontend_recording_split_file(void) override;
bool obs_frontend_recording_add_chapter(const char *name) override;
void obs_frontend_replay_buffer_start(void) override;
void obs_frontend_replay_buffer_save(void) override;
void obs_frontend_replay_buffer_stop(void) override;
bool obs_frontend_replay_buffer_active(void) override;
void *obs_frontend_add_tools_menu_qaction(const char *name) override;
void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override;
void *obs_frontend_add_dock(void *dock) override;
bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override;
void obs_frontend_remove_dock(const char *id) override;
bool obs_frontend_add_custom_qdock(const char *id, void *dock) override;
void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override;
void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override;
obs_output_t *obs_frontend_get_streaming_output(void) override;
obs_output_t *obs_frontend_get_recording_output(void) override;
obs_output_t *obs_frontend_get_replay_buffer_output(void) override;
config_t *obs_frontend_get_profile_config(void) override;
config_t *obs_frontend_get_global_config(void) override;
config_t *obs_frontend_get_app_config(void) override;
config_t *obs_frontend_get_user_config(void) override;
void obs_frontend_open_projector(const char *type, int monitor, const char *geometry,
const char *name) override;
void obs_frontend_save(void) override;
void obs_frontend_defer_save_begin(void) override;
void obs_frontend_defer_save_end(void) override;
void obs_frontend_add_save_callback(obs_frontend_save_cb callback, void *private_data) override;
void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, void *private_data) override;
void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, void *private_data) override;
void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, void *private_data) override;
void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate) override;
void obs_frontend_pop_ui_translation(void) override;
void obs_frontend_set_streaming_service(obs_service_t *service) override;
obs_service_t *obs_frontend_get_streaming_service(void) override;
void obs_frontend_save_streaming_service(void) override;
bool obs_frontend_preview_program_mode_active(void) override;
void obs_frontend_set_preview_program_mode(bool enable) override;
void obs_frontend_preview_program_trigger_transition(void) override;
bool obs_frontend_preview_enabled(void) override;
void obs_frontend_set_preview_enabled(bool enable) override;
obs_source_t *obs_frontend_get_current_preview_scene(void) override;
void obs_frontend_set_current_preview_scene(obs_source_t *scene) override;
void obs_frontend_take_screenshot(void) override;
void obs_frontend_take_source_screenshot(obs_source_t *source) override;
obs_output_t *obs_frontend_get_virtualcam_output(void) override;
void obs_frontend_start_virtualcam(void) override;
void obs_frontend_stop_virtualcam(void) override;
bool obs_frontend_virtualcam_active(void) override;
void obs_frontend_reset_video(void) override;
void obs_frontend_open_source_properties(obs_source_t *source) override;
void obs_frontend_open_source_filters(obs_source_t *source) override;
void obs_frontend_open_source_interaction(obs_source_t *source) override;
void obs_frontend_open_sceneitem_edit_transform(obs_sceneitem_t *item) override;
char *obs_frontend_get_current_record_output_path(void) override;
const char *obs_frontend_get_locale_string(const char *string) override;
bool obs_frontend_is_theme_dark(void) override;
char *obs_frontend_get_last_recording(void) override;
char *obs_frontend_get_last_screenshot(void) override;
char *obs_frontend_get_last_replay(void) override;
void obs_frontend_add_undo_redo_action(const char *name, const undo_redo_cb undo, const undo_redo_cb redo,
const char *undo_data, const char *redo_data, bool repeatable) override;
void on_load(obs_data_t *settings) override;
void on_preload(obs_data_t *settings) override;
void on_save(obs_data_t *settings) override;
void on_event(enum obs_frontend_event event) override;
};
obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);

View File

@@ -0,0 +1,18 @@
if(TARGET OBS::browser-panels)
target_enable_feature(obs-studio "Browser panels" BROWSER_AVAILABLE)
target_link_libraries(obs-studio PRIVATE OBS::browser-panels)
target_sources(
obs-studio
PRIVATE
dialogs/OBSExtraBrowsers.cpp
dialogs/OBSExtraBrowsers.hpp
docks/BrowserDock.cpp
docks/BrowserDock.hpp
utility/ExtraBrowsersDelegate.cpp
utility/ExtraBrowsersDelegate.hpp
utility/ExtraBrowsersModel.cpp
utility/ExtraBrowsersModel.hpp
)
endif()

View File

@@ -0,0 +1,16 @@
target_sources(
obs-studio
PRIVATE
importer/ImporterEntryPathItemDelegate.cpp
importer/ImporterEntryPathItemDelegate.hpp
importer/ImporterModel.cpp
importer/ImporterModel.hpp
importer/OBSImporter.cpp
importer/OBSImporter.hpp
importers/classic.cpp
importers/importers.cpp
importers/importers.hpp
importers/sl.cpp
importers/studio.cpp
importers/xsplit.cpp
)

View File

@@ -9,14 +9,16 @@ endif()
target_sources(
obs-studio
PRIVATE
update/crypto-helpers-mac.mm
update/crypto-helpers.hpp
update/models/branches.hpp
update/models/whatsnew.hpp
update/shared-update.cpp
update/shared-update.hpp
update/update-helpers.cpp
update/update-helpers.hpp
utility/crypto-helpers-mac.mm
utility/crypto-helpers.hpp
utility/models/branches.hpp
utility/models/whatsnew.hpp
utility/update-helpers.cpp
utility/update-helpers.hpp
utility/WhatsNewBrowserInitThread.cpp
utility/WhatsNewBrowserInitThread.hpp
utility/WhatsNewInfoThread.cpp
utility/WhatsNewInfoThread.hpp
)
target_link_libraries(

View File

@@ -1,5 +1,5 @@
if(RESTREAM_CLIENTID AND RESTREAM_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels)
target_sources(obs-studio PRIVATE auth-restream.cpp auth-restream.hpp)
target_sources(obs-studio PRIVATE oauth/RestreamAuth.cpp oauth/RestreamAuth.hpp)
target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED)
else()
target_disable_feature(obs-studio "Restream API connection")

View File

@@ -1,8 +1,17 @@
if(SPARKLE_APPCAST_URL AND SPARKLE_PUBLIC_KEY)
find_library(SPARKLE Sparkle)
mark_as_advanced(SPARKLE)
target_sources(obs-studio PRIVATE update/mac-update.cpp update/mac-update.hpp update/sparkle-updater.mm)
set_source_files_properties(update/sparkle-updater.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_sources(
obs-studio
PRIVATE
utility/MacUpdateThread.cpp
utility/MacUpdateThread.hpp
utility/OBSSparkle.hpp
utility/OBSSparkle.mm
utility/OBSUpdateDelegate.h
utility/OBSUpdateDelegate.mm
)
set_source_files_properties(utility/OBSSparkle.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_libraries(obs-studio PRIVATE "$<LINK_LIBRARY:FRAMEWORK,${SPARKLE}>")

View File

@@ -1,5 +1,5 @@
if(TWITCH_CLIENTID AND TWITCH_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels)
target_sources(obs-studio PRIVATE auth-twitch.cpp auth-twitch.hpp)
target_sources(obs-studio PRIVATE oauth/TwitchAuth.cpp oauth/TwitchAuth.hpp)
target_enable_feature(obs-studio "Twitch API connection" TWITCH_ENABLED)
else()
target_disable_feature(obs-studio "Twitch API connection")

View File

@@ -20,13 +20,15 @@ if(ENABLE_WHATSNEW AND TARGET OBS::browser-panels)
target_sources(
obs-studio
PRIVATE
update/crypto-helpers-mbedtls.cpp
update/crypto-helpers.hpp
update/models/whatsnew.hpp
update/shared-update.cpp
update/shared-update.hpp
update/update-helpers.cpp
update/update-helpers.hpp
utility/crypto-helpers-mbedtls.cpp
utility/crypto-helpers.hpp
utility/models/whatsnew.hpp
utility/update-helpers.cpp
utility/update-helpers.hpp
utility/WhatsNewBrowserInitThread.cpp
utility/WhatsNewBrowserInitThread.hpp
utility/WhatsNewInfoThread.cpp
utility/WhatsNewInfoThread.hpp
)
endif()

View File

@@ -8,14 +8,17 @@ if(
target_sources(
obs-studio
PRIVATE
auth-youtube.cpp
auth-youtube.hpp
window-dock-youtube-app.cpp
window-dock-youtube-app.hpp
window-youtube-actions.cpp
window-youtube-actions.hpp
youtube-api-wrappers.cpp
youtube-api-wrappers.hpp
dialogs/OBSYoutubeActions.cpp
dialogs/OBSYoutubeActions.hpp
docks/YouTubeAppDock.cpp
docks/YouTubeAppDock.hpp
docks/YouTubeChatDock.cpp
docks/YouTubeChatDock.hpp
forms/OBSYoutubeActions.ui
oauth/YoutubeAuth.cpp
oauth/YoutubeAuth.hpp
utility/YoutubeApiWrappers.cpp
utility/YoutubeApiWrappers.hpp
)
target_enable_feature(obs-studio "YouTube API connection" YOUTUBE_ENABLED)

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 542 KiB

After

Width:  |  Height:  |  Size: 542 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 542 KiB

After

Width:  |  Height:  |  Size: 542 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -1,9 +1,7 @@
target_sources(obs-studio PRIVATE platform-x11.cpp)
target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp)
target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}")
target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus procstat)
target_sources(obs-studio PRIVATE system-info-posix.cpp)
if(TARGET OBS::python)
find_package(Python REQUIRED COMPONENTS Interpreter Development)
target_link_libraries(obs-studio PRIVATE Python::Python)

View File

@@ -1,12 +1,10 @@
target_sources(obs-studio PRIVATE platform-x11.cpp)
target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp)
target_compile_definitions(
obs-studio
PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" $<$<BOOL:${ENABLE_PORTABLE_CONFIG}>:ENABLE_PORTABLE_CONFIG>
)
target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus)
target_sources(obs-studio PRIVATE system-info-posix.cpp)
if(TARGET OBS::python)
find_package(Python REQUIRED COMPONENTS Interpreter Development)
target_link_libraries(obs-studio PRIVATE Python::Python)

View File

@@ -1,10 +1,16 @@
include(cmake/feature-sparkle.cmake)
target_sources(obs-studio PRIVATE platform-osx.mm forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp)
target_sources(
obs-studio
PRIVATE
dialogs/OBSPermissions.cpp
dialogs/OBSPermissions.hpp
forms/OBSPermissions.ui
utility/platform-osx.mm
utility/system-info-macos.mm
)
target_compile_options(obs-studio PRIVATE -Wno-quoted-include-in-framework-header -Wno-comma)
target_sources(obs-studio PRIVATE system-info-macos.mm)
set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 14.0.3)

View File

@@ -18,42 +18,48 @@ target_sources(
obs-studio
PRIVATE
cmake/windows/obs.manifest
dialogs/OBSUpdate.cpp
dialogs/OBSUpdate.hpp
forms/OBSUpdate.ui
obs.rc
platform-windows.cpp
update/crypto-helpers-mbedtls.cpp
update/crypto-helpers.hpp
update/models/branches.hpp
update/models/whatsnew.hpp
update/shared-update.cpp
update/shared-update.hpp
update/update-helpers.cpp
update/update-helpers.hpp
update/update-window.cpp
update/update-window.hpp
update/win-update.cpp
update/win-update.hpp
win-dll-blocklist.c
win-update/updater/manifest.hpp
utility/AutoUpdateThread.cpp
utility/AutoUpdateThread.hpp
utility/crypto-helpers-mbedtls.cpp
utility/crypto-helpers.hpp
utility/models/branches.hpp
utility/models/whatsnew.hpp
utility/platform-windows.cpp
utility/system-info-windows.cpp
utility/update-helpers.cpp
utility/update-helpers.hpp
utility/WhatsNewBrowserInitThread.cpp
utility/WhatsNewBrowserInitThread.hpp
utility/WhatsNewInfoThread.cpp
utility/WhatsNewInfoThread.hpp
utility/win-dll-blocklist.c
)
target_sources(obs-studio PRIVATE system-info-windows.cpp)
add_library(obs-updater-manifest INTERFACE)
add_library(OBS::updater-manifest ALIAS obs-updater-manifest)
target_sources(obs-updater-manifest INTERFACE updater/manifest.hpp)
target_link_libraries(
obs-studio
PRIVATE crypt32 OBS::blake2 OBS::w32-pthreads MbedTLS::mbedtls nlohmann_json::nlohmann_json Detours::Detours
PRIVATE
crypt32
OBS::blake2
OBS::updater-manifest
OBS::w32-pthreads
MbedTLS::mbedtls
nlohmann_json::nlohmann_json
Detours::Detours
)
target_compile_definitions(obs-studio PRIVATE PSAPI_VERSION=2)
target_link_options(obs-studio PRIVATE /IGNORE:4099 $<$<CONFIG:DEBUG>:/NODEFAULTLIB:MSVCRT>)
add_library(obs-update-helpers INTERFACE)
add_library(OBS::update-helpers ALIAS obs-update-helpers)
target_sources(obs-update-helpers INTERFACE win-update/win-update-helpers.cpp win-update/win-update-helpers.hpp)
target_include_directories(obs-update-helpers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/win-update")
# Set commit for untagged version comparisons in the Windows updater
if(OBS_VERSION MATCHES ".+g[a-f0-9]+.*")
string(REGEX REPLACE ".+g([a-f0-9]+).*$" "\\1" OBS_COMMIT ${OBS_VERSION})
@@ -61,9 +67,9 @@ else()
set(OBS_COMMIT "")
endif()
set_source_files_properties(update/win-update.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}")
set_source_files_properties(utility/AutoUpdateThread.cpp PROPERTIES COMPILE_DEFINITIONS OBS_COMMIT="${OBS_COMMIT}")
add_subdirectory(win-update/updater)
add_subdirectory(updater)
set_property(TARGET obs-studio APPEND PROPERTY AUTORCC_OPTIONS --format-version 1)

View File

@@ -0,0 +1,85 @@
if(NOT TARGET OBS::qt-slider-ignorewheel)
add_subdirectory(
"${CMAKE_SOURCE_DIR}/shared/qt/slider-ignorewheel"
"${CMAKE_BINARY_DIR}/shared/qt/slider-ignorewheel"
)
endif()
target_link_libraries(obs-studio PRIVATE OBS::qt-slider-ignorewheel)
target_sources(
obs-studio
PRIVATE
components/AbsoluteSlider.cpp
components/AbsoluteSlider.hpp
components/ApplicationAudioCaptureToolbar.cpp
components/ApplicationAudioCaptureToolbar.hpp
components/AudioCaptureToolbar.cpp
components/AudioCaptureToolbar.hpp
components/BalanceSlider.hpp
components/BrowserToolbar.cpp
components/BrowserToolbar.hpp
components/ClickableLabel.hpp
components/ColorSourceToolbar.cpp
components/ColorSourceToolbar.hpp
components/ComboSelectToolbar.cpp
components/ComboSelectToolbar.hpp
components/DelButton.hpp
components/DeviceCaptureToolbar.cpp
components/DeviceCaptureToolbar.hpp
components/DisplayCaptureToolbar.cpp
components/DisplayCaptureToolbar.hpp
components/EditWidget.hpp
components/FocusList.cpp
components/FocusList.hpp
components/GameCaptureToolbar.cpp
components/GameCaptureToolbar.hpp
components/HScrollArea.cpp
components/HScrollArea.hpp
components/ImageSourceToolbar.cpp
components/ImageSourceToolbar.hpp
components/MediaControls.cpp
components/MediaControls.hpp
components/MenuButton.cpp
components/MenuButton.hpp
components/Multiview.cpp
components/Multiview.hpp
components/MuteCheckBox.hpp
components/NonCheckableButton.hpp
components/OBSAdvAudioCtrl.cpp
components/OBSAdvAudioCtrl.hpp
components/OBSPreviewScalingComboBox.cpp
components/OBSPreviewScalingComboBox.hpp
components/OBSPreviewScalingLabel.cpp
components/OBSPreviewScalingLabel.hpp
components/OBSSourceLabel.cpp
components/OBSSourceLabel.hpp
components/SceneTree.cpp
components/SceneTree.hpp
components/SilentUpdateCheckBox.hpp
components/SilentUpdateSpinBox.hpp
components/SourceToolbar.cpp
components/SourceToolbar.hpp
components/SourceTree.cpp
components/SourceTree.hpp
components/SourceTreeDelegate.cpp
components/SourceTreeDelegate.hpp
components/SourceTreeItem.cpp
components/SourceTreeItem.hpp
components/SourceTreeModel.cpp
components/SourceTreeModel.hpp
components/TextSourceToolbar.cpp
components/TextSourceToolbar.hpp
components/UIValidation.cpp
components/UIValidation.hpp
components/UrlPushButton.cpp
components/UrlPushButton.hpp
components/VisibilityItemDelegate.cpp
components/VisibilityItemDelegate.hpp
components/VisibilityItemWidget.cpp
components/VisibilityItemWidget.hpp
components/VolumeSlider.cpp
components/VolumeSlider.hpp
components/WindowCaptureToolbar.cpp
components/WindowCaptureToolbar.hpp
)

View File

@@ -0,0 +1,40 @@
if(NOT TARGET OBS::properties-view)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/properties-view" "${CMAKE_BINARY_DIR}/shared/properties-view")
endif()
target_link_libraries(obs-studio PRIVATE OBS::properties-view)
target_sources(
obs-studio
PRIVATE
dialogs/NameDialog.cpp
dialogs/NameDialog.hpp
dialogs/OAuthLogin.cpp
dialogs/OAuthLogin.hpp
dialogs/OBSAbout.cpp
dialogs/OBSAbout.hpp
dialogs/OBSBasicAdvAudio.cpp
dialogs/OBSBasicAdvAudio.hpp
dialogs/OBSBasicFilters.cpp
dialogs/OBSBasicFilters.hpp
dialogs/OBSBasicInteraction.cpp
dialogs/OBSBasicInteraction.hpp
dialogs/OBSBasicProperties.cpp
dialogs/OBSBasicProperties.hpp
dialogs/OBSBasicSourceSelect.cpp
dialogs/OBSBasicSourceSelect.hpp
dialogs/OBSBasicTransform.cpp
dialogs/OBSBasicTransform.hpp
dialogs/OBSBasicVCamConfig.cpp
dialogs/OBSBasicVCamConfig.hpp
dialogs/OBSLogReply.cpp
dialogs/OBSLogReply.hpp
dialogs/OBSLogViewer.cpp
dialogs/OBSLogViewer.hpp
dialogs/OBSMissingFiles.cpp
dialogs/OBSMissingFiles.hpp
dialogs/OBSRemux.cpp
dialogs/OBSRemux.hpp
dialogs/OBSWhatsNew.cpp
dialogs/OBSWhatsNew.hpp
)

View File

@@ -0,0 +1 @@
target_sources(obs-studio PRIVATE docks/OBSDock.cpp docks/OBSDock.hpp)

View File

@@ -0,0 +1,4 @@
target_sources(
obs-studio
PRIVATE oauth/Auth.cpp oauth/Auth.hpp oauth/AuthListener.cpp oauth/AuthListener.hpp oauth/OAuth.cpp oauth/OAuth.hpp
)

View File

@@ -0,0 +1,58 @@
find_package(Qt6 REQUIRED Widgets Network Svg Xml)
if(OS_LINUX OR OS_FREEBSD OR OS_OPENBSD)
find_package(Qt6 REQUIRED Gui DBus)
endif()
if(NOT TARGET OBS::qt-wrappers)
add_subdirectory("${CMAKE_SOURCE_DIR}/shared/qt/wrappers" "${CMAKE_BINARY_DIR}/shared/qt/wrappers")
endif()
target_link_libraries(
obs-studio
PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network OBS::qt-wrappers
)
set_target_properties(
obs-studio
PROPERTIES AUTOMOC TRUE AUTOUIC TRUE AUTORCC TRUE AUTOGEN_PARALLEL AUTO
)
set_property(TARGET obs-studio APPEND PROPERTY AUTOUIC_SEARCH_PATHS forms forms/source-toolbar)
target_sources(
obs-studio
PRIVATE
forms/AutoConfigFinishPage.ui
forms/AutoConfigStartPage.ui
forms/AutoConfigStartPage.ui
forms/AutoConfigStreamPage.ui
forms/AutoConfigTestPage.ui
forms/AutoConfigVideoPage.ui
forms/ColorSelect.ui
forms/obs.qrc
forms/OBSAbout.ui
forms/OBSAdvAudio.ui
forms/OBSBasic.ui
forms/OBSBasicControls.ui
forms/OBSBasicFilters.ui
forms/OBSBasicInteraction.ui
forms/OBSBasicProperties.ui
forms/OBSBasicSettings.ui
forms/OBSBasicSourceSelect.ui
forms/OBSBasicVCamConfig.ui
forms/OBSExtraBrowsers.ui
forms/OBSImporter.ui
forms/OBSLogReply.ui
forms/OBSLogReply.ui
forms/OBSMissingFiles.ui
forms/OBSRemux.ui
forms/source-toolbar/browser-source-toolbar.ui
forms/source-toolbar/color-source-toolbar.ui
forms/source-toolbar/device-select-toolbar.ui
forms/source-toolbar/game-capture-toolbar.ui
forms/source-toolbar/image-source-toolbar.ui
forms/source-toolbar/media-controls.ui
forms/source-toolbar/text-source-toolbar.ui
forms/StatusBarWidget.ui
)

View File

@@ -0,0 +1,15 @@
target_sources(
obs-studio
PRIVATE
settings/OBSBasicSettings_A11y.cpp
settings/OBSBasicSettings_Appearance.cpp
settings/OBSBasicSettings_Stream.cpp
settings/OBSBasicSettings.cpp
settings/OBSBasicSettings.hpp
settings/OBSHotkeyEdit.cpp
settings/OBSHotkeyEdit.hpp
settings/OBSHotkeyLabel.cpp
settings/OBSHotkeyLabel.hpp
settings/OBSHotkeyWidget.cpp
settings/OBSHotkeyWidget.hpp
)

View File

@@ -0,0 +1,69 @@
target_sources(
obs-studio
PRIVATE
utility/AdvancedOutput.cpp
utility/AdvancedOutput.hpp
utility/audio-encoders.cpp
utility/audio-encoders.hpp
utility/BaseLexer.hpp
utility/BasicOutputHandler.cpp
utility/BasicOutputHandler.hpp
utility/display-helpers.hpp
utility/FFmpegCodec.cpp
utility/FFmpegCodec.hpp
utility/FFmpegFormat.cpp
utility/FFmpegFormat.hpp
utility/FFmpegShared.hpp
utility/GoLiveAPI_CensoredJson.cpp
utility/GoLiveAPI_CensoredJson.hpp
utility/GoLiveAPI_Network.cpp
utility/GoLiveAPI_Network.hpp
utility/GoLiveAPI_PostData.cpp
utility/GoLiveAPI_PostData.hpp
utility/item-widget-helpers.cpp
utility/item-widget-helpers.hpp
utility/MissingFilesModel.cpp
utility/MissingFilesModel.hpp
utility/MissingFilesPathItemDelegate.cpp
utility/MissingFilesPathItemDelegate.hpp
utility/models/multitrack-video.hpp
utility/MultitrackVideoError.cpp
utility/MultitrackVideoError.hpp
utility/MultitrackVideoOutput.cpp
utility/MultitrackVideoOutput.hpp
utility/obf.c
utility/obf.h
utility/OBSEventFilter.hpp
utility/OBSProxyStyle.cpp
utility/OBSProxyStyle.hpp
utility/OBSTheme.hpp
utility/OBSThemeVariable.hpp
utility/OBSTranslator.cpp
utility/OBSTranslator.hpp
utility/platform.hpp
utility/QuickTransition.cpp
utility/QuickTransition.hpp
utility/RemoteTextThread.cpp
utility/RemoteTextThread.hpp
utility/RemuxEntryPathItemDelegate.cpp
utility/RemuxEntryPathItemDelegate.hpp
utility/RemuxQueueModel.cpp
utility/RemuxQueueModel.hpp
utility/RemuxWorker.cpp
utility/RemuxWorker.hpp
utility/SceneRenameDelegate.cpp
utility/SceneRenameDelegate.hpp
utility/ScreenshotObj.cpp
utility/ScreenshotObj.hpp
utility/SettingsEventFilter.hpp
utility/SimpleOutput.cpp
utility/SimpleOutput.hpp
utility/StartMultiTrackVideoStreamingGuard.hpp
utility/SurfaceEventFilter.hpp
utility/system-info.hpp
utility/undo_stack.cpp
utility/undo_stack.hpp
utility/VCamConfig.hpp
utility/VolumeMeterTimer.cpp
utility/VolumeMeterTimer.hpp
)

View File

@@ -0,0 +1,66 @@
if(NOT TARGET OBS::qt-vertical-scroll-area)
add_subdirectory(
"${CMAKE_SOURCE_DIR}/shared/qt/vertical-scroll-area"
"${CMAKE_BINARY_DIR}/shared/qt/vertical-scroll-area"
)
endif()
target_link_libraries(obs-studio PRIVATE OBS::qt-vertical-scroll-area)
target_sources(
obs-studio
PRIVATE
widgets/ColorSelect.cpp
widgets/ColorSelect.hpp
widgets/OBSBasic.cpp
widgets/OBSBasic.hpp
widgets/OBSBasic_Browser.cpp
widgets/OBSBasic_Clipboard.cpp
widgets/OBSBasic_ContextToolbar.cpp
widgets/OBSBasic_Docks.cpp
widgets/OBSBasic_Dropfiles.cpp
widgets/OBSBasic_Hotkeys.cpp
widgets/OBSBasic_Icons.cpp
widgets/OBSBasic_MainControls.cpp
widgets/OBSBasic_OutputHandler.cpp
widgets/OBSBasic_Preview.cpp
widgets/OBSBasic_Profiles.cpp
widgets/OBSBasic_Projectors.cpp
widgets/OBSBasic_Recording.cpp
widgets/OBSBasic_ReplayBuffer.cpp
widgets/OBSBasic_SceneCollections.cpp
widgets/OBSBasic_SceneItems.cpp
widgets/OBSBasic_Scenes.cpp
widgets/OBSBasic_Screenshots.cpp
widgets/OBSBasic_Service.cpp
widgets/OBSBasic_StatusBar.cpp
widgets/OBSBasic_Streaming.cpp
widgets/OBSBasic_StudioMode.cpp
widgets/OBSBasic_SysTray.cpp
widgets/OBSBasic_Transitions.cpp
widgets/OBSBasic_Updater.cpp
widgets/OBSBasic_VirtualCam.cpp
widgets/OBSBasic_VolControl.cpp
widgets/OBSBasic_YouTube.cpp
widgets/OBSBasicControls.cpp
widgets/OBSBasicControls.hpp
widgets/OBSBasicPreview.cpp
widgets/OBSBasicPreview.hpp
widgets/OBSBasicStats.cpp
widgets/OBSBasicStats.hpp
widgets/OBSBasicStatusBar.cpp
widgets/OBSBasicStatusBar.hpp
widgets/OBSMainWindow.hpp
widgets/OBSProjector.cpp
widgets/OBSProjector.hpp
widgets/OBSQTDisplay.cpp
widgets/OBSQTDisplay.hpp
widgets/StatusBarWidget.cpp
widgets/StatusBarWidget.hpp
widgets/VolControl.cpp
widgets/VolControl.hpp
widgets/VolumeAccessibleInterface.cpp
widgets/VolumeAccessibleInterface.hpp
widgets/VolumeMeter.cpp
widgets/VolumeMeter.hpp
)

View File

@@ -0,0 +1,15 @@
target_sources(
obs-studio
PRIVATE
wizards/AutoConfig.cpp
wizards/AutoConfig.hpp
wizards/AutoConfigStartPage.cpp
wizards/AutoConfigStartPage.hpp
wizards/AutoConfigStreamPage.cpp
wizards/AutoConfigStreamPage.hpp
wizards/AutoConfigTestPage.cpp
wizards/AutoConfigTestPage.hpp
wizards/AutoConfigVideoPage.cpp
wizards/AutoConfigVideoPage.hpp
wizards/TestMode.hpp
)

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,4 +1,5 @@
#include "moc_absolute-slider.cpp"
#include "AbsoluteSlider.hpp"
#include "moc_AbsoluteSlider.cpp"
AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent)
{

View File

@@ -1,6 +1,5 @@
#pragma once
#include <QMouseEvent>
#include <slider-ignorewheel.hpp>
class AbsoluteSlider : public SliderIgnoreScroll {

View File

@@ -0,0 +1,22 @@
#include "ApplicationAudioCaptureToolbar.hpp"
#include "ui_device-select-toolbar.h"
#include "moc_ApplicationAudioCaptureToolbar.cpp"
ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source)
: ComboSelectToolbar(parent, source)
{
}
void ApplicationAudioCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = obs_get_module("win-wasapi");
const char *device_str = obs_module_get_locale_text(mod, "Window");
ui->deviceLabel->setText(device_str);
prop_name = "window";
ComboSelectToolbar::Init();
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ComboSelectToolbar.hpp"
class ApplicationAudioCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};

View File

@@ -0,0 +1,33 @@
#include "AudioCaptureToolbar.hpp"
#include "ui_device-select-toolbar.h"
#include "moc_AudioCaptureToolbar.cpp"
#ifdef _WIN32
#define get_os_module(win, mac, linux) obs_get_module(win)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win)
#elif __APPLE__
#define get_os_module(win, mac, linux) obs_get_module(mac)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac)
#else
#define get_os_module(win, mac, linux) obs_get_module(linux)
#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux)
#endif
AudioCaptureToolbar::AudioCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {}
void AudioCaptureToolbar::Init()
{
delete ui->activateButton;
ui->activateButton = nullptr;
obs_module_t *mod = get_os_module("win-wasapi", "mac-capture", "linux-pulseaudio");
if (!mod)
return;
const char *device_str = get_os_text(mod, "Device", "CoreAudio.Device", "Device");
ui->deviceLabel->setText(device_str);
prop_name = "device_id";
ComboSelectToolbar::Init();
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ComboSelectToolbar.hpp"
class AudioCaptureToolbar : public ComboSelectToolbar {
Q_OBJECT
public:
AudioCaptureToolbar(QWidget *parent, OBSSource source);
void Init() override;
};

Some files were not shown because too many files have changed in this diff Show More