diff --git a/UI/context-bar-controls.cpp b/frontend/components/ApplicationAudioCaptureToolbar.cpp similarity index 100% rename from UI/context-bar-controls.cpp rename to frontend/components/ApplicationAudioCaptureToolbar.cpp diff --git a/UI/context-bar-controls.hpp b/frontend/components/ApplicationAudioCaptureToolbar.hpp similarity index 100% rename from UI/context-bar-controls.hpp rename to frontend/components/ApplicationAudioCaptureToolbar.hpp diff --git a/frontend/components/AudioCaptureToolbar.cpp b/frontend/components/AudioCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/AudioCaptureToolbar.hpp b/frontend/components/AudioCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/AudioCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/BrowserToolbar.cpp b/frontend/components/BrowserToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/BrowserToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/BrowserToolbar.hpp b/frontend/components/BrowserToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/BrowserToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/ColorSourceToolbar.cpp b/frontend/components/ColorSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ColorSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/ColorSourceToolbar.hpp b/frontend/components/ColorSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ColorSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/ComboSelectToolbar.cpp b/frontend/components/ComboSelectToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ComboSelectToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/ComboSelectToolbar.hpp b/frontend/components/ComboSelectToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ComboSelectToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/DeviceCaptureToolbar.cpp b/frontend/components/DeviceCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/DeviceCaptureToolbar.hpp b/frontend/components/DeviceCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/DeviceCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/DisplayCaptureToolbar.cpp b/frontend/components/DisplayCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/DisplayCaptureToolbar.hpp b/frontend/components/DisplayCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/DisplayCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/GameCaptureToolbar.cpp b/frontend/components/GameCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/GameCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/GameCaptureToolbar.hpp b/frontend/components/GameCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/GameCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/frontend/components/ImageSourceToolbar.cpp b/frontend/components/ImageSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/ImageSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/ImageSourceToolbar.hpp b/frontend/components/ImageSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/ImageSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/UI/preview-controls.cpp b/frontend/components/OBSPreviewScalingComboBox.cpp similarity index 100% rename from UI/preview-controls.cpp rename to frontend/components/OBSPreviewScalingComboBox.cpp diff --git a/UI/preview-controls.hpp b/frontend/components/OBSPreviewScalingComboBox.hpp similarity index 100% rename from UI/preview-controls.hpp rename to frontend/components/OBSPreviewScalingComboBox.hpp diff --git a/frontend/components/OBSPreviewScalingLabel.cpp b/frontend/components/OBSPreviewScalingLabel.cpp new file mode 100644 index 000000000..7564e1bdb --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.cpp @@ -0,0 +1,133 @@ +/****************************************************************************** + Copyright (C) 2024 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#include "preview-controls.hpp" +#include + +/* Preview Scale Label */ +void OBSPreviewScalingLabel::PreviewScaleChanged(float scale) +{ + previewScale = scale; + UpdateScaleLabel(); +} + +void OBSPreviewScalingLabel::UpdateScaleLabel() +{ + float previewScalePercent = floor(100.0f * previewScale); + setText(QString::number(previewScalePercent) + "%"); +} + +/* Preview Scaling ComboBox */ +void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed) +{ + if (fixedScaling == fixed) + return; + + fixedScaling = fixed; + UpdateSelection(); +} + +void OBSPreviewScalingComboBox::CanvasResized(uint32_t width, uint32_t height) +{ + SetCanvasSize(width, height); + UpdateCanvasText(); +} + +void OBSPreviewScalingComboBox::OutputResized(uint32_t width, uint32_t height) +{ + SetOutputSize(width, height); + + bool canvasMatchesOutput = output_width == canvas_width && output_height == canvas_height; + + SetScaleOutputEnabled(!canvasMatchesOutput); + UpdateOutputText(); +} + +void OBSPreviewScalingComboBox::PreviewScaleChanged(float scale) +{ + previewScale = scale; + + if (fixedScaling) { + UpdateSelection(); + UpdateAllText(); + } else { + UpdateScaledText(); + } +} + +void OBSPreviewScalingComboBox::SetScaleOutputEnabled(bool show) +{ + if (scaleOutputEnabled == show) + return; + + scaleOutputEnabled = show; + + if (scaleOutputEnabled) { + addItem(QTStr("Basic.MainMenu.Edit.Scale.Output")); + } else { + removeItem(2); + } +} + +void OBSPreviewScalingComboBox::UpdateAllText() +{ + UpdateCanvasText(); + UpdateOutputText(); + UpdateScaledText(); +} + +void OBSPreviewScalingComboBox::UpdateCanvasText() +{ + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(canvas_width), QString::number(canvas_height)); + setItemText(1, text); +} + +void OBSPreviewScalingComboBox::UpdateOutputText() +{ + if (scaleOutputEnabled) { + QString text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(output_width), QString::number(output_height)); + setItemText(2, text); + } +} + +void OBSPreviewScalingComboBox::UpdateScaledText() +{ + QString text = QTStr("Basic.MainMenu.Edit.Scale.Manual"); + text = text.arg(QString::number(floor(canvas_width * previewScale)), + QString::number(floor(canvas_height * previewScale))); + setPlaceholderText(text); +} + +void OBSPreviewScalingComboBox::UpdateSelection() +{ + QSignalBlocker sb(this); + float outputScale = float(output_width) / float(canvas_width); + + if (!fixedScaling) { + setCurrentIndex(0); + } else { + if (previewScale == 1.0f) { + setCurrentIndex(1); + } else if (scaleOutputEnabled && (previewScale == outputScale)) { + setCurrentIndex(2); + } else { + setCurrentIndex(-1); + } + } +} diff --git a/frontend/components/OBSPreviewScalingLabel.hpp b/frontend/components/OBSPreviewScalingLabel.hpp new file mode 100644 index 000000000..4078dbea9 --- /dev/null +++ b/frontend/components/OBSPreviewScalingLabel.hpp @@ -0,0 +1,79 @@ +/****************************************************************************** + Copyright (C) 2024 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#pragma once + +#include +#include + +class OBSPreviewScalingLabel : public QLabel { + Q_OBJECT + +public: + OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {} + +public slots: + void PreviewScaleChanged(float scale); + +private: + float previewScale = 0.0f; + void UpdateScaleLabel(); +}; + +class OBSPreviewScalingComboBox : public QComboBox { + Q_OBJECT + +public: + OBSPreviewScalingComboBox(QWidget *parent = nullptr) : QComboBox(parent) {} + + inline void SetCanvasSize(uint32_t width, uint32_t height) + { + canvas_width = width; + canvas_height = height; + }; + inline void SetOutputSize(uint32_t width, uint32_t height) + { + output_width = width; + output_height = height; + }; + void UpdateAllText(); + +public slots: + void PreviewScaleChanged(float scale); + void PreviewFixedScalingChanged(bool fixed); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + +private: + uint32_t canvas_width = 0; + uint32_t canvas_height = 0; + + uint32_t output_width = 0; + uint32_t output_height = 0; + + float previewScale = 0.0f; + + bool fixedScaling = false; + + bool scaleOutputEnabled = false; + void SetScaleOutputEnabled(bool show); + + void UpdateCanvasText(); + void UpdateOutputText(); + void UpdateScaledText(); + void UpdateSelection(); +}; diff --git a/frontend/components/SourceToolbar.cpp b/frontend/components/SourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/SourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/SourceToolbar.hpp b/frontend/components/SourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/SourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/UI/source-tree.cpp b/frontend/components/SourceTree.cpp similarity index 100% rename from UI/source-tree.cpp rename to frontend/components/SourceTree.cpp diff --git a/UI/source-tree.hpp b/frontend/components/SourceTree.hpp similarity index 100% rename from UI/source-tree.hpp rename to frontend/components/SourceTree.hpp diff --git a/frontend/components/SourceTreeDelegate.cpp b/frontend/components/SourceTreeDelegate.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeDelegate.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeDelegate.hpp b/frontend/components/SourceTreeDelegate.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeDelegate.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(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; +}; diff --git a/frontend/components/SourceTreeItem.cpp b/frontend/components/SourceTreeItem.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeItem.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeItem.hpp b/frontend/components/SourceTreeItem.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeItem.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(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; +}; diff --git a/frontend/components/SourceTreeModel.cpp b/frontend/components/SourceTreeModel.cpp new file mode 100644 index 000000000..a0242e3cd --- /dev/null +++ b/frontend/components/SourceTreeModel.cpp @@ -0,0 +1,1613 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "platform.hpp" +#include "source-label.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tree(tree_), sceneitem(sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneitem); + int preset = obs_data_get_int(privData, "color-preset"); + + if (preset == 1) { + const char *color = obs_data_get_string(privData, "color"); + std::string col = "background: "; + col += color; + setStyleSheet(col.c_str()); + } else if (preset > 1) { + setStyleSheet(""); + setProperty("bgColor", preset - 1); + } else { + setStyleSheet("background: none"); + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *id = obs_source_get_id(source); + + bool sourceVisible = obs_sceneitem_visible(sceneitem); + + if (tree->iconsVisible) { + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = main->GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = main->GetGroupIcon(); + else + icon = main->GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + + iconLabel = new QLabel(); + iconLabel->setPixmap(pixmap); + iconLabel->setEnabled(sourceVisible); + iconLabel->setStyleSheet("background: none"); + iconLabel->setProperty("class", "source-icon"); + } + + vis = new QCheckBox(); + vis->setProperty("class", "checkbox-icon indicator-visibility"); + vis->setChecked(sourceVisible); + vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility")); + vis->setAccessibleDescription(QTStr("Basic.Main.Sources.VisibilityDescription").arg(name)); + + lock = new QCheckBox(); + lock->setProperty("class", "checkbox-icon indicator-lock"); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock")); + lock->setAccessibleDescription(QTStr("Basic.Main.Sources.LockDescription").arg(name)); + + label = new OBSSourceLabel(source); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + label->setEnabled(sourceVisible); + +#ifdef __APPLE__ + vis->setAttribute(Qt::WA_LayoutUsesWidgetRect); + lock->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + boxLayout = new QHBoxLayout(); + + boxLayout->setContentsMargins(0, 0, 0, 0); + boxLayout->setSpacing(0); + if (iconLabel) { + boxLayout->addWidget(iconLabel); + boxLayout->addSpacing(2); + } + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + boxLayout->addSpacing(16); +#endif + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this](bool val) { + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *scenesource = obs_scene_get_source(scene); + int64_t id = obs_sceneitem_get_id(sceneitem); + const char *name = obs_source_get_name(scenesource); + const char *uuid = obs_source_get_uuid(scenesource); + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + + auto undo_redo = [](const std::string &uuid, int64_t id, bool val) { + OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); + obs_scene_t *sc = obs_group_or_scene_from_source(s); + obs_sceneitem_t *si = obs_scene_find_sceneitem_by_id(sc, id); + if (si) + obs_sceneitem_set_visible(si, val); + }; + + QString str = QTStr(val ? "Undo.ShowSceneItem" : "Undo.HideSceneItem"); + + OBSBasic *main = OBSBasic::Get(); + main->undo_s.add_action(str.arg(obs_source_get_name(source), name), + std::bind(undo_redo, std::placeholders::_1, id, !val), + std::bind(undo_redo, std::placeholders::_1, id, val), uuid, uuid); + + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, val); + }; + + auto setItemLocked = [this](bool checked) { + QSignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void SourceTreeItem::DisconnectSignals() +{ + sigs.clear(); +} + +void SourceTreeItem::Clear() +{ + DisconnectSignals(); + sceneitem = nullptr; +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + obs_scene_t *curScene = (obs_scene_t *)calldata_ptr(cd, "scene"); + + if (curItem == this_->sceneitem) { + QMetaObject::invokeMethod(this_->tree, "Remove", Q_ARG(OBSSceneItem, curItem), + Q_ARG(OBSScene, curScene)); + curItem = nullptr; + } + if (!curItem) + QMetaObject::invokeMethod(this_, "Clear"); + }; + + auto itemVisible = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", Q_ARG(bool, visible)); + }; + + auto itemLocked = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + bool locked = calldata_bool(cd, "locked"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked)); + }; + + auto itemSelect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Select"); + }; + + auto itemDeselect = [](void *data, calldata_t *cd) { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = (obs_sceneitem_t *)calldata_ptr(cd, "item"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "Deselect"); + }; + + auto reorderGroup = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + QMetaObject::invokeMethod(this_->tree, "ReorderItems"); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sigs.emplace_back(signal, "remove", removeItem, this); + sigs.emplace_back(signal, "item_remove", removeItem, this); + sigs.emplace_back(signal, "item_visible", itemVisible, this); + sigs.emplace_back(signal, "item_locked", itemLocked, this); + sigs.emplace_back(signal, "item_select", itemSelect, this); + sigs.emplace_back(signal, "item_deselect", itemDeselect, this); + + if (obs_sceneitem_is_group(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + + sigs.emplace_back(signal, "reorder", reorderGroup, this); + } + + /* --------------------------------------------------------- */ + + auto removeSource = [](void *data, calldata_t *) { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + QMetaObject::invokeMethod(this_->tree, "RefreshItems"); + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + if (obs_source_configurable(source)) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::enterEvent(QEnterEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + +bool SourceTreeItem::IsEditing() +{ + return editor != nullptr; +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + int index = boxLayout->indexOf(label); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->setStyleSheet("background: none"); + editor->selectAll(); + editor->installEventFilter(this); + boxLayout->insertWidget(index, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + ExitEditModeInternal(save); + + if (tree->undoSceneData) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg(newName.c_str()); + main->CreateSceneUndoRedoAction(text, tree->undoSceneData, redoSceneData); + + tree->undoSceneData = nullptr; + } +} + +void SourceTreeItem::ExitEditModeInternal(bool save) +{ + if (!editor) { + return; + } + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + int index = boxLayout->indexOf(editor); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(index, label); + setFocus(); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + OBSSourceAutoRelease existingSource = obs_get_source_by_name(newName.c_str()); + bool exists = !!existingSource; + + if (exists) { + OBSMessageBox::information(main, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + QSignalBlocker sourcesSignalBlocker(this); + std::string prevName(obs_source_get_name(source)); + std::string scene_uuid = obs_source_get_uuid(main->GetCurrentSceneSource()); + auto undo = [scene_uuid, prevName, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prevName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + std::string editedName = newName; + + auto redo = [scene_uuid, main, editedName](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, editedName.c_str()); + + OBSSourceAutoRelease scene_source = obs_get_source_by_uuid(scene_uuid.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + + const char *uuid = obs_source_get_uuid(source); + main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo, redo, uuid, uuid); + + obs_source_set_name(source, newName.c_str()); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (LineEditCanceled(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, false)); + return true; + } + if (LineEditChanged(event)) { + QMetaObject::invokeMethod(this, "ExitEditMode", Qt::QueuedConnection, Q_ARG(bool, true)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + if (iconLabel) { + iconLabel->setEnabled(visible); + } + label->setEnabled(visible); + vis->setChecked(visible); +} + +void SourceTreeItem::LockedChanged(bool locked) +{ + lock->setChecked(locked); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + if (spacer) { + boxLayout->removeItem(spacer); + delete spacer; + spacer = nullptr; + } + + if (type == Type::Group) { + boxLayout->removeWidget(expand); + expand->deleteLater(); + expand = nullptr; + } + + type = newType; + + if (type == Type::SubItem) { + spacer = new QSpacerItem(16, 1); + boxLayout->insertItem(0, spacer); + + } else if (type == Type::Group) { + expand = new QCheckBox(); + expand->setProperty("class", "checkbox-icon indicator-expand"); +#ifdef __APPLE__ + expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + boxLayout->insertWidget(0, expand); + + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + + connect(expand, &QPushButton::toggled, this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(sceneitem); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +void SourceTreeItem::Select() +{ + tree->SelectItem(sceneitem, true); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +void SourceTreeItem::Deselect() +{ + tree->SelectItem(sceneitem, false); + OBSBasic::Get()->UpdateContextBarDeferred(); + OBSBasic::Get()->UpdateEditMenu(); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + stm->Clear(); + obs_frontend_remove_event_callback(OBSFrontendEvent, stm); + break; + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + default: + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = *reinterpret_cast *>(ptr); + + obs_source_t *src = obs_sceneitem_get_source(item); + if (obs_source_removed(src)) { + return true; + } + + if (obs_sceneitem_is_group(item)) { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, + select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + if (obs_sceneitem_is_group(item)) { + SceneChanged(); + } else { + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); + } +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); + + OBSBasic::Get()->UpdateContextBarDeferred(); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) : QAbstractListModel(st_), st(st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + OBSSceneItem item = items[index.row()]; + obs_source_t *source = obs_sceneitem_get_source(item); + return QVariant(QT_UTF8(obs_source_get_name(source))); + } + + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name = QTStr("Group"); + + int i = 2; + for (;;) { + OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name)); + if (!group) + break; + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSBasic *main = OBSBasic::Get(); + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + st->undoSceneData = main->BackupScene(scene); + + obs_sceneitem_t *item = obs_scene_insert_group(scene, QT_TO_UTF8(name), item_order.data(), item_order.size()); + if (!item) { + st->undoSceneData = nullptr; + return; + } + + main->undo_s.push_disabled(); + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + /* ----------------------------------------------------------------- */ + /* obs_scene_insert_group triggers a full refresh of scene items via */ + /* the item_add signal. No need to insert a row, just edit the one */ + /* that's created automatically. */ + + int newIdx = indices[0].row(); + QMetaObject::invokeMethod(st, "NewGroupEdit", Qt::QueuedConnection, Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + OBSBasic *main = OBSBasic::Get(); + if (indices.count() == 0) + return; + + OBSScene scene = main->GetCurrentScene(); + OBSData undoData = main->BackupScene(scene); + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); + + OBSData redoData = main->BackupScene(scene); + main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData, redoData); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); + setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + UpdateNoSourcesMessage(); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateNoSourcesMessage); + connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons); + + setItemDelegate(new SourceTreeDelegate(this)); +} + +void SourceTree::UpdateIcons() +{ + SourceTreeModel *stm = GetStm(); + stm->SceneChanged(); +} + +void SourceTree::SetIconsVisible(bool visible) +{ + SourceTreeModel *stm = GetStm(); + + iconsVisible = visible; + stm->SceneChanged(); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid() && select != selectionModel()->isSelected(index)) + selectionModel()->select(index, select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSBasic *main = OBSBasic::Get(); + + OBSScene scene = GetCurrentScene(); + obs_source_t *scenesource = obs_scene_get_source(scene); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->position().toPoint()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup ? dropItem : obs_sceneitem_get_group(scene, dropItem); + + /* not a group if moving above the group */ + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) + dropGroup = nullptr; + if (emptyDrop) + dropGroup = nullptr; + + /* --------------------------------------- */ + /* remember to remove list items if */ + /* dropping on collapsed group */ + + bool dropOnCollapsed = false; + if (dropGroup) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow; + if (row == stm->items.count()) + itemBelow = nullptr; + else + itemBelow = stm->items[row]; + + if (hasGroups) { + if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + /* --------------------------------------- */ + /* if dropping groups on other groups, */ + /* disregard as invalid drag/drop */ + + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* save undo data */ + std::vector sources; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_get_scene(item) != scene) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + } + if (dropGroup) + sources.push_back(obs_sceneitem_get_source(dropGroup)); + OBSData undo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* if selection includes base group items, */ + /* include all group sub-items and treat */ + /* them all as one */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = obs_sceneitem_get_group(scene, subitem); + + if (subitemGroup == item) { + QModelIndex idx = stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* move all items to destination index */ + + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) + itemTo--; + + if (itemTo != from) { + stm->beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&](obs_sceneitem_t *item) { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&]() { + OBSDataAutoRelease data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items(lastGroup, preInsertCollapsed, &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&]() { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(scene, item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [](void *data, obs_scene_t *) { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* save redo data */ + + OBSData redo_data = main->BackupScene(scene, &sources); + + /* --------------------------------------- */ + /* add undo/redo action */ + + const char *scene_name = obs_source_get_name(scenesource); + QString action_name = QTStr("Undo.ReorderSources").arg(scene_name); + main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data); + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + { + QSignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::NewGroupEdit(int row) +{ + if (!Edit(row)) { + OBSBasic *main = OBSBasic::Get(); + main->undo_s.pop_disabled(); + + blog(LOG_WARNING, "Uh, somehow the edit didn't process, this " + "code should never be reached.\nAnd by " + "\"never be reached\", I mean that " + "theoretically, it should be\nimpossible " + "for this code to be reached. But if this " + "code is reached,\nfeel free to laugh at " + "Lain, because apparently it is, in fact, " + "actually\npossible for this code to be " + "reached. But I mean, again, theoretically\n" + "it should be impossible. So if you see " + "this in your log, just know that\nit's " + "really dumb, and depressing. But at least " + "the undo/redo action is\nstill covered, so " + "in theory things *should* be fine. But " + "it's entirely\npossible that they might " + "not be exactly. But again, yea. This " + "really\nshould not be possible."); + + OBSData redoSceneData = main->BackupScene(GetCurrentScene()); + + QString text = QTStr("Undo.GroupItems").arg("Unknown"); + main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData); + + undoSceneData = nullptr; + } +} + +bool SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return false; + + QModelIndex index = stm->createIndex(row, 0); + QWidget *widget = indexWidget(index); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + if (itemWidget->IsEditing()) { +#ifdef __APPLE__ + itemWidget->ExitEditMode(true); +#endif + return false; + } + + itemWidget->EnterEditMode(); + edit(index); + return true; +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::Remove(OBSSceneItem item, OBSScene scene) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + GetStm()->Remove(item); + main->SaveProject(); + + if (!main->SavingDisabled()) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + } +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} + +void SourceTree::UpdateNoSourcesMessage() +{ + QString file = !App()->IsThemeDark() ? ":res/images/no_sources.svg" : "theme:Dark/no_sources.svg"; + iconNoSources.load(file); + + QTextOption opt(Qt::AlignHCenter); + opt.setWrapMode(QTextOption::WordWrap); + textNoSources.setTextOption(opt); + textNoSources.setText(QTStr("NoSources.Label").replace("\n", "
")); + + textPrepared = false; +} + +void SourceTree::paintEvent(QPaintEvent *event) +{ + SourceTreeModel *stm = GetStm(); + if (stm && !stm->items.count()) { + QPainter p(viewport()); + + if (!textPrepared) { + textNoSources.prepare(QTransform(), p.font()); + textPrepared = true; + } + + QRectF iconRect = iconNoSources.viewBoxF(); + iconRect.setSize(QSizeF(32.0, 32.0)); + + QSizeF iconSize = iconRect.size(); + QSizeF textSize = textNoSources.size(); + QSizeF thisSize = size(); + const qreal spacing = 16.0; + + qreal totalHeight = iconSize.height() + spacing + textSize.height(); + + qreal x = thisSize.width() / 2.0 - iconSize.width() / 2.0; + qreal y = thisSize.height() / 2.0 - totalHeight / 2.0; + iconRect.moveTo(std::round(x), std::round(y)); + iconNoSources.render(&p, iconRect); + + x = thisSize.width() / 2.0 - textSize.width() / 2.0; + y += spacing + iconSize.height(); + p.drawStaticText(x, y, textNoSources); + } else { + QListView::paintEvent(event); + } +} + +SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + SourceTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) + return (QSize(0, 0)); + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/frontend/components/SourceTreeModel.hpp b/frontend/components/SourceTreeModel.hpp new file mode 100644 index 000000000..8f8922d52 --- /dev/null +++ b/frontend/components/SourceTreeModel.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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(model()); } + +public: + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(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; +}; diff --git a/frontend/components/TextSourceToolbar.cpp b/frontend/components/TextSourceToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/TextSourceToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/TextSourceToolbar.hpp b/frontend/components/TextSourceToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/TextSourceToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +}; diff --git a/UI/visibility-item-widget.cpp b/frontend/components/VisibilityItemDelegate.cpp similarity index 100% rename from UI/visibility-item-widget.cpp rename to frontend/components/VisibilityItemDelegate.cpp diff --git a/UI/visibility-item-widget.hpp b/frontend/components/VisibilityItemDelegate.hpp similarity index 100% rename from UI/visibility-item-widget.hpp rename to frontend/components/VisibilityItemDelegate.hpp diff --git a/frontend/components/VisibilityItemWidget.cpp b/frontend/components/VisibilityItemWidget.cpp new file mode 100644 index 000000000..50ea425ad --- /dev/null +++ b/frontend/components/VisibilityItemWidget.cpp @@ -0,0 +1,133 @@ +#include "moc_visibility-item-widget.cpp" +#include "obs-app.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(parentObj); + if (!list) + return; + + QListWidgetItem *item = list->item(index.row()); + VisibilityItemWidget *widget = qobject_cast(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(object); + if (!editor) + return false; + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(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); +} diff --git a/frontend/components/VisibilityItemWidget.hpp b/frontend/components/VisibilityItemWidget.hpp new file mode 100644 index 000000000..ec6b35e37 --- /dev/null +++ b/frontend/components/VisibilityItemWidget.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +class QLabel; +class QLineEdit; +class QListWidget; +class QListWidgetItem; +class QCheckBox; +class OBSSourceLabel; + +class VisibilityItemWidget : public QWidget { + Q_OBJECT + +private: + OBSSource source; + OBSSourceLabel *label = nullptr; + QCheckBox *vis = nullptr; + + OBSSignal enabledSignal; + + bool active = false; + bool selected = false; + + static void OBSSourceEnabled(void *param, calldata_t *data); + +private slots: + void SourceEnabled(bool enabled); + +public: + VisibilityItemWidget(obs_source_t *source); + + void SetColor(const QColor &color, bool active, bool selected); +}; + +class VisibilityItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + VisibilityItemDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; +}; + +void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source); diff --git a/frontend/components/WindowCaptureToolbar.cpp b/frontend/components/WindowCaptureToolbar.cpp new file mode 100644 index 000000000..c45d06976 --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.cpp @@ -0,0 +1,707 @@ +#include "window-basic-main.hpp" +#include "moc_context-bar-controls.cpp" +#include "obs-app.hpp" + +#include +#include +#include +#include + +#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(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); +} diff --git a/frontend/components/WindowCaptureToolbar.hpp b/frontend/components/WindowCaptureToolbar.hpp new file mode 100644 index 000000000..acf88a5fb --- /dev/null +++ b/frontend/components/WindowCaptureToolbar.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +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; + + 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; + +public: + BrowserToolbar(QWidget *parent, OBSSource source); + ~BrowserToolbar(); + +public slots: + void on_refresh_clicked(); +}; + +class ComboSelectToolbar : public SourceToolbar { + Q_OBJECT + +protected: + std::unique_ptr 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; + 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; + + 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; + +public: + ImageSourceToolbar(QWidget *parent, OBSSource source); + ~ImageSourceToolbar(); + +public slots: + void on_browse_clicked(); +}; + +class ColorSourceToolbar : public SourceToolbar { + Q_OBJECT + + std::unique_ptr 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; + 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(); +};