Merge pull request #11622 from PatTheMav/frontend-refactor
UI: Reorganize and refactor entire frontend codebase
4
.github/actions/qt-xml-validator/action.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/scripts/.build.zsh
vendored
@@ -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
@@ -7,6 +7,7 @@
|
||||
!/cmake
|
||||
!/deps
|
||||
!/docs
|
||||
!/frontend
|
||||
!/libobs*
|
||||
!/plugins
|
||||
!/shared
|
||||
|
||||
@@ -29,6 +29,6 @@ add_subdirectory(plugins)
|
||||
|
||||
add_subdirectory(test/test-input)
|
||||
|
||||
add_subdirectory(UI)
|
||||
add_subdirectory(frontend)
|
||||
|
||||
message_configuration()
|
||||
|
||||
@@ -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 ¤tCollection = 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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>
|
||||
26
UI/obs.rc.in
@@ -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
|
||||
1613
UI/source-tree.cpp
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 ¤tCollection = 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 ¤tCollection = 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 ¤tCollection = 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 ¤tCollection = 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);
|
||||
}
|
||||
10203
UI/window-basic-main.cpp
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>")
|
||||
@@ -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();
|
||||
@@ -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
@@ -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 ¤tCollection = 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
@@ -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);
|
||||
18
frontend/cmake/feature-browserpanels.cmake
Normal 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()
|
||||
16
frontend/cmake/feature-importers.cmake
Normal 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
|
||||
)
|
||||
@@ -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(
|
||||
@@ -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")
|
||||
@@ -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}>")
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 542 KiB After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 542 KiB After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
85
frontend/cmake/ui-components.cmake
Normal 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
|
||||
)
|
||||
40
frontend/cmake/ui-dialogs.cmake
Normal 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
|
||||
)
|
||||
1
frontend/cmake/ui-docks.cmake
Normal file
@@ -0,0 +1 @@
|
||||
target_sources(obs-studio PRIVATE docks/OBSDock.cpp docks/OBSDock.hpp)
|
||||
4
frontend/cmake/ui-oauth.cmake
Normal 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
|
||||
)
|
||||
58
frontend/cmake/ui-qt.cmake
Normal 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
|
||||
)
|
||||
15
frontend/cmake/ui-settings.cmake
Normal 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
|
||||
)
|
||||
69
frontend/cmake/ui-utility.cmake
Normal 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
|
||||
)
|
||||
66
frontend/cmake/ui-widgets.cmake
Normal 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
|
||||
)
|
||||
15
frontend/cmake/ui-wizards.cmake
Normal 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
|
||||
)
|
||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -1,4 +1,5 @@
|
||||
#include "moc_absolute-slider.cpp"
|
||||
#include "AbsoluteSlider.hpp"
|
||||
#include "moc_AbsoluteSlider.cpp"
|
||||
|
||||
AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent)
|
||||
{
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <slider-ignorewheel.hpp>
|
||||
|
||||
class AbsoluteSlider : public SliderIgnoreScroll {
|
||||
22
frontend/components/ApplicationAudioCaptureToolbar.cpp
Normal 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();
|
||||
}
|
||||
11
frontend/components/ApplicationAudioCaptureToolbar.hpp
Normal 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;
|
||||
};
|
||||
33
frontend/components/AudioCaptureToolbar.cpp
Normal 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();
|
||||
}
|
||||
11
frontend/components/AudioCaptureToolbar.hpp
Normal 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;
|
||||
};
|
||||