@@ -0,0 +1,707 @@
+#include "window-basic-main.hpp"
+#include "moc_context-bar-controls.cpp"
+#include "obs-app.hpp"
+
+#include <qt-wrappers.hpp>
+#include <QStandardItemModel>
+#include <QColorDialog>
+#include <QFontDialog>
+#include "ui_browser-source-toolbar.h"
+#include "ui_device-select-toolbar.h"
+#include "ui_game-capture-toolbar.h"
+#include "ui_image-source-toolbar.h"
+#include "ui_color-source-toolbar.h"
+#include "ui_text-source-toolbar.h"
+#ifdef _WIN32
+#define get_os_module(win, mac, linux) obs_get_module(win)
+#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, win)
+#elif __APPLE__
+#define get_os_module(win, mac, linux) obs_get_module(mac)
+#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, mac)
+#else
+#define get_os_module(win, mac, linux) obs_get_module(linux)
+#define get_os_text(mod, win, mac, linux) obs_module_get_locale_text(mod, linux)
+#endif
+/* ========================================================================= */
+SourceToolbar::SourceToolbar(QWidget *parent, OBSSource source)
+ : QWidget(parent),
+ weakSource(OBSGetWeakRef(source)),
+ props(obs_source_properties(source), obs_properties_destroy)
+{
+}
+void SourceToolbar::SaveOldProperties(obs_source_t *source)
+ oldData = obs_data_create();
+ OBSDataAutoRelease oldSettings = obs_source_get_settings(source);
+ obs_data_apply(oldData, oldSettings);
+ obs_data_set_string(oldData, "undo_suuid", obs_source_get_uuid(source));
+void SourceToolbar::SetUndoProperties(obs_source_t *source, bool repeatable)
+ if (!oldData) {
+ blog(LOG_ERROR, "%s: somehow oldData was null.", __FUNCTION__);
+ return;
+ }
+ OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+ OBSSource currentSceneSource = main->GetCurrentSceneSource();
+ if (!currentSceneSource)
+ 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) {
+ obs_property_t *p = obs_properties_get(props.get(), "refreshnocache");
+ obs_property_button_clicked(p, source.Get());
+ComboSelectToolbar::ComboSelectToolbar(QWidget *parent, OBSSource source)
+ ui(new Ui_DeviceSelectToolbar)
+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);
+ cur_id = std::to_string(obs_data_get_int(settings, prop_name));
+ 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()
+ 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();
+ obs_data_set_int(settings, prop_name, id.toInt());
+ obs_data_set_string(settings, prop_name, QT_TO_UTF8(id));
+ obs_source_update(source, settings);
+void ComboSelectToolbar::on_device_currentIndexChanged(int idx)
+ if (idx == -1 || !source) {
+ 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)
+ 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()
+ obs_module_t *mod = get_os_module("win-capture", "mac-capture", "linux-capture");
+ const char *device_str = get_os_text(mod, "WindowCapture.Window", "WindowUtils.Window", "Window");
+#if !defined(_WIN32) && !defined(__APPLE__) //linux
+ prop_name = "capture_window";
+ prop_name = "window";
+#ifdef __APPLE__
+ is_int = true;
+ApplicationAudioCaptureToolbar::ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source)
+ : ComboSelectToolbar(parent, source)
+void ApplicationAudioCaptureToolbar::Init()
+ obs_module_t *mod = obs_get_module("win-wasapi");
+ const char *device_str = obs_module_get_locale_text(mod, "Window");
+DisplayCaptureToolbar::DisplayCaptureToolbar(QWidget *parent, OBSSource source) : ComboSelectToolbar(parent, source) {}
+void DisplayCaptureToolbar::Init()
+ const char *device_str = get_os_text(mod, "Monitor", "DisplayCapture.Display", "Screen");
+ prop_name = "monitor_id";
+ prop_name = "display_uuid";
+ prop_name = "screen";
+DeviceCaptureToolbar::DeviceCaptureToolbar(QWidget *parent, OBSSource source)
+ delete ui->deviceLabel;
+ delete ui->device;
+ ui->deviceLabel = nullptr;
+ ui->device = nullptr;
+ active = obs_data_get_bool(settings, "active");
+ obs_module_t *mod = obs_get_module("win-dshow");
+ 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);
+ 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) {
+ 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)
+ ui(new Ui_GameCaptureToolbar)
+ obs_property_t *p;
+ int cur_idx;
+ obs_module_t *mod = obs_get_module("win-capture");
+ ui->modeLabel->setText(obs_module_get_locale_text(mod, "Mode"));
+ ui->windowLabel->setText(obs_module_get_locale_text(mod, "WindowCapture.Window"));
+ 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)
+ QString id = ui->mode->itemData(idx).toString();
+ obs_data_set_string(settings, "capture_mode", QT_TO_UTF8(id));
+void GameCaptureToolbar::on_window_currentIndexChanged(int idx)
+ QString id = ui->window->itemData(idx).toString();
+ obs_data_set_string(settings, "window", QT_TO_UTF8(id));
+ImageSourceToolbar::ImageSourceToolbar(QWidget *parent, OBSSource source)
+ ui(new Ui_ImageSourceToolbar)
+ obs_module_t *mod = obs_get_module("image-source");
+ ui->pathLabel->setText(obs_module_get_locale_text(mod, "File"));
+ std::string file = obs_data_get_string(settings, "file");
+ ui->path->setText(file.c_str());
+ImageSourceToolbar::~ImageSourceToolbar() {}
+void ImageSourceToolbar::on_browse_clicked()
+ 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()) {
+ ui->path->setText(path);
+ obs_data_set_string(settings, "file", QT_TO_UTF8(path));
+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)
+ ui(new Ui_ColorSourceToolbar)
+ 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()
+ obs_property_t *p = obs_properties_get(props.get(), "color");
+ QColorDialog::ColorDialogOptions options;
+ options |= QColorDialog::ShowAlphaChannel;
+#ifdef __linux__
+ // TODO: Revisit hang on Ubuntu with native dialog
+ options |= QColorDialog::DontUseNativeDialog;
+ QColor newColor = QColorDialog::getColor(color, this, desc, options);
+ if (!newColor.isValid()) {
+ color = newColor;
+ obs_data_set_int(settings, "color", color_to_int(color));
+extern void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false);
+TextSourceToolbar::TextSourceToolbar(QWidget *parent, OBSSource source)
+ ui(new Ui_TextSourceToolbar)
+ 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");
+ 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()
+ QFontDialog::FontDialogOptions options;
+ uint32_t flags;
+ bool success;
+#ifndef _WIN32
+ options = QFontDialog::DontUseNativeDialog;
+ font = QFontDialog::getFont(&success, font, this, QTStr("Basic.PropertiesWindow.SelectFont.WindowTitle"),
+ options);
+ if (!success) {
+ 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);
+ obs_data_set_obj(settings, "font", font_obj);
+void TextSourceToolbar::on_selectColor_clicked()
+ 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");
+ if (freetype) {
+ obs_data_set_int(settings, "color1", color_to_int(color));
+ obs_data_set_int(settings, "color2", color_to_int(color));
+void TextSourceToolbar::on_text_textChanged()
+ std::string newText = QT_TO_UTF8(ui->text->text());
+ if (newText == obs_data_get_string(settings, "text")) {
+ obs_data_set_string(settings, "text", newText.c_str());
+ obs_source_update(source, nullptr);
+ SetUndoProperties(source, true);
@@ -0,0 +1,178 @@
+#pragma once
+#include <memory>
+#include <obs.hpp>
+#include <QWidget>
+class Ui_BrowserSourceToolbar;
+class Ui_DeviceSelectToolbar;
+class Ui_GameCaptureToolbar;
+class Ui_ImageSourceToolbar;
+class Ui_ColorSourceToolbar;
+class Ui_TextSourceToolbar;
+class SourceToolbar : public QWidget {
+ Q_OBJECT
+ OBSWeakSource weakSource;
+protected:
+ using properties_delete_t = decltype(&obs_properties_destroy);
+ using properties_t = std::unique_ptr<obs_properties_t, properties_delete_t>;
+ properties_t props;
+ OBSDataAutoRelease oldData;
+ void SaveOldProperties(obs_source_t *source);
+ void SetUndoProperties(obs_source_t *source, bool repeatable = false);
+public:
+ SourceToolbar(QWidget *parent, OBSSource source);
+ OBSSource GetSource() { return OBSGetStrongRef(weakSource); }
+public slots:
+ virtual void Update() {}
+};
+class BrowserToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_BrowserSourceToolbar> ui;
+ BrowserToolbar(QWidget *parent, OBSSource source);
+ ~BrowserToolbar();
+ void on_refresh_clicked();
+class ComboSelectToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_DeviceSelectToolbar> ui;
+ const char *prop_name;
+ bool is_int = false;
+ ComboSelectToolbar(QWidget *parent, OBSSource source);
+ ~ComboSelectToolbar();
+ virtual void Init();
+ void on_device_currentIndexChanged(int idx);
+class AudioCaptureToolbar : public ComboSelectToolbar {
+ AudioCaptureToolbar(QWidget *parent, OBSSource source);
+ void Init() override;
+class WindowCaptureToolbar : public ComboSelectToolbar {
+ WindowCaptureToolbar(QWidget *parent, OBSSource source);
+class ApplicationAudioCaptureToolbar : public ComboSelectToolbar {
+ ApplicationAudioCaptureToolbar(QWidget *parent, OBSSource source);
+class DisplayCaptureToolbar : public ComboSelectToolbar {
+ DisplayCaptureToolbar(QWidget *parent, OBSSource source);
+class DeviceCaptureToolbar : public QWidget {
+ const char *activateText;
+ const char *deactivateText;
+ bool active;
+ DeviceCaptureToolbar(QWidget *parent, OBSSource source);
+ ~DeviceCaptureToolbar();
+ void on_activateButton_clicked();
+class GameCaptureToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_GameCaptureToolbar> ui;
+ void UpdateWindowVisibility();
+ GameCaptureToolbar(QWidget *parent, OBSSource source);
+ ~GameCaptureToolbar();
+ void on_mode_currentIndexChanged(int idx);
+ void on_window_currentIndexChanged(int idx);
+class ImageSourceToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_ImageSourceToolbar> ui;
+ ImageSourceToolbar(QWidget *parent, OBSSource source);
+ ~ImageSourceToolbar();
+ void on_browse_clicked();
+class ColorSourceToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_ColorSourceToolbar> ui;
+ QColor color;
+ void UpdateColor();
+ ColorSourceToolbar(QWidget *parent, OBSSource source);
+ ~ColorSourceToolbar();
+ void on_choose_clicked();
+class TextSourceToolbar : public SourceToolbar {
+ std::unique_ptr<Ui_TextSourceToolbar> ui;
+ QFont font;
+ TextSourceToolbar(QWidget *parent, OBSSource source);
+ ~TextSourceToolbar();
+ void on_selectFont_clicked();
+ void on_selectColor_clicked();
+ void on_text_textChanged();
@@ -0,0 +1,133 @@
+/******************************************************************************
+ Copyright (C) 2024 by Taylor Giampaolo <[email protected]>
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+#include "preview-controls.hpp"
+#include <obs-app.hpp>
+/* 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)
+ 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)
+ if (fixedScaling) {
+ UpdateAllText();
+ UpdateScaledText();
+void OBSPreviewScalingComboBox::SetScaleOutputEnabled(bool show)
+ if (scaleOutputEnabled == show)
+ scaleOutputEnabled = show;
+ if (scaleOutputEnabled) {
+ addItem(QTStr("Basic.MainMenu.Edit.Scale.Output"));
+ removeItem(2);
+void OBSPreviewScalingComboBox::UpdateAllText()
+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()
+ 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);
+ if (previewScale == 1.0f) {
+ setCurrentIndex(1);
+ } else if (scaleOutputEnabled && (previewScale == outputScale)) {
+ setCurrentIndex(2);
+ setCurrentIndex(-1);
@@ -0,0 +1,79 @@
+#include <QLabel>
+#include <QComboBox>
+class OBSPreviewScalingLabel : public QLabel {
+ OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {}
+ void PreviewScaleChanged(float scale);
+private:
+ float previewScale = 0.0f;
+ void UpdateScaleLabel();
+class OBSPreviewScalingComboBox : public QComboBox {
+ 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();
+ void PreviewFixedScalingChanged(bool fixed);
+ void CanvasResized(uint32_t width, uint32_t height);
+ void OutputResized(uint32_t width, uint32_t height);
+ uint32_t canvas_width = 0;
+ uint32_t canvas_height = 0;
+ uint32_t output_width = 0;
+ uint32_t output_height = 0;
+ bool fixedScaling = false;
+ bool scaleOutputEnabled = false;
+ void SetScaleOutputEnabled(bool show);
+ void UpdateCanvasText();
+ void UpdateOutputText();
+ void UpdateScaledText();
+ void UpdateSelection();
@@ -0,0 +1,1613 @@
+#include "source-tree.hpp"
+#include "platform.hpp"
+#include "source-label.hpp"
+#include <obs-frontend-api.h>
+#include <obs.h>
+#include <string>
+#include <QLineEdit>
+#include <QSpacerItem>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QMouseEvent>
+#include <QAccessible>
+#include <QStylePainter>
+#include <QStyleOptionFocusRect>
+static inline OBSScene GetCurrentScene()
+ 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);
+ setStyleSheet("background: none");
+ 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);
+ vis->setAttribute(Qt::WA_LayoutUsesWidgetRect);
+ lock->setAttribute(Qt::WA_LayoutUsesWidgetRect);
+ 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);
+ /* Hack: Fixes a bug where scrollbars would be above the lock icon */
+ boxLayout->addSpacing(16);
+ 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);
+ 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) {
+ 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)
+ auto removeItem = [](void *data, calldata_t *cd) {
+ SourceTreeItem *this_ = reinterpret_cast<SourceTreeItem *>(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) {
+ 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) {
+ bool locked = calldata_bool(cd, "locked");
+ QMetaObject::invokeMethod(this_, "LockedChanged", Q_ARG(bool, locked));
+ auto itemSelect = [](void *data, calldata_t *cd) {
+ QMetaObject::invokeMethod(this_, "Select");
+ auto itemDeselect = [](void *data, calldata_t *cd) {
+ QMetaObject::invokeMethod(this_, "Deselect");
+ auto reorderGroup = [](void *data, calldata_t *) {
+ QMetaObject::invokeMethod(this_->tree, "ReorderItems");
+ 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)) {
+ signal = obs_source_get_signal_handler(source);
+ sigs.emplace_back(signal, "reorder", reorderGroup, this);
+ auto removeSource = [](void *data, calldata_t *) {
+ this_->DisconnectSignals();
+ this_->sceneitem = nullptr;
+ QMetaObject::invokeMethod(this_->tree, "RefreshItems");
+ sigs.emplace_back(signal, "remove", removeSource, this);
+void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event)
+ QWidget::mouseDoubleClickEvent(event);
+ if (expand) {
+ expand->setChecked(!expand->isChecked());
+ if (obs_source_configurable(source)) {
+ main->CreatePropertiesWindow(source);
+void SourceTreeItem::enterEvent(QEnterEvent *event)
+ QWidget::enterEvent(event);
+ OBSBasicPreview *preview = OBSBasicPreview::Get();
+ std::lock_guard<std::mutex> lock(preview->selectMutex);
+ preview->hoveredPreviewItems.clear();
+ preview->hoveredPreviewItems.push_back(sceneitem);
+void SourceTreeItem::leaveEvent(QEvent *event)
+ QWidget::leaveEvent(event);
+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) {
+ 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) {
+ 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)
+ if (newName.empty()) {
+ OBSMessageBox::information(main, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text"));
+ /* Check for same name */
+ if (newName == obs_source_get_name(source))
+ /* 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"));
+ /* rename */
+ 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());
+ std::string editedName = newName;
+ auto redo = [scene_uuid, main, editedName](const std::string &data) {
+ obs_source_set_name(source, editedName.c_str());
+ 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));
+void SourceTreeItem::VisibilityChanged(bool visible)
+ 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 */
+ newType = Type::Group;
+ /* if it's a group sub-item */
+ } else if (itemScene != scene) {
+ newType = Type::SubItem;
+ /* if it's a regular item */
+ newType = Type::Item;
+ if (!force && newType == type) {
+ 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");
+ expand->setAttribute(Qt::WA_LayoutUsesWidgetRect);
+ 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);
+ spacer = new QSpacerItem(3, 1);
+void SourceTreeItem::ExpandClicked(bool checked)
+ obs_data_set_bool(data, "collapsed", checked);
+ if (!checked)
+ tree->GetStm()->ExpandGroup(sceneitem);
+ tree->GetStm()->CollapseGroup(sceneitem);
+void SourceTreeItem::Select()
+ tree->SelectItem(sceneitem, true);
+ OBSBasic::Get()->UpdateContextBarDeferred();
+void SourceTreeItem::Deselect()
+ tree->SelectItem(sceneitem, false);
+void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr)
+ SourceTreeModel *stm = reinterpret_cast<SourceTreeModel *>(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);
+ case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP:
+ default:
+void SourceTreeModel::Clear()
+ beginResetModel();
+ items.clear();
+ endResetModel();
+ hasGroups = false;
+static bool enumItem(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
+ QVector<OBSSceneItem> &items = *reinterpret_cast<QVector<OBSSceneItem> *>(ptr);
+ obs_source_t *src = obs_sceneitem_get_source(item);
+ if (obs_source_removed(src)) {
+ 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);
+void SourceTreeModel::SceneChanged()
+ 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<OBSSceneItem> &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()
+ QVector<OBSSceneItem> newitems;
+ obs_scene_enum_items(scene, enumItem, &newitems);
+ /* if item list has changed size, do full reset */
+ if (newitems.count() != items.count()) {
+ SceneChanged();
+ 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;
+ /* if everything is the same, break */
+ if (i == newitems.count()) {
+ /* find new starting index */
+ for (i = idx1Old + 1; i < newitems.count(); i++) {
+ obs_sceneitem_t *oldItem = items[idx1Old];
+ if (oldItem == newItem) {
+ idx1New = i;
+ /* if item could not be found, do full reset */
+ /* 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];
+ /* 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)
+ beginInsertRows(QModelIndex(), 0, 0);
+ endInsertRows();
+ st->UpdateWidget(createIndex(0, 0, nullptr), item);
+void SourceTreeModel::Remove(obs_sceneitem_t *item)
+ int idx = -1;
+ if (items[i] == item) {
+ idx = i;
+ if (idx == -1)
+ int startIdx = idx;
+ int endIdx = idx;
+ bool is_group = obs_sceneitem_is_group(item);
+ if (is_group) {
+ 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;
+ beginRemoveRows(QModelIndex(), startIdx, endIdx);
+ items.remove(idx, endIdx - startIdx + 1);
+ endRemoveRows();
+ if (is_group)
+ UpdateGroupState(true);
+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()];
+ 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()
+ QString name = QTStr("Group");
+ int i = 2;
+ OBSSourceAutoRelease group = obs_get_source_by_name(QT_TO_UTF8(name));
+ if (!group)
+ 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));
+ items.insert(0, group);
+ st->UpdateWidget(createIndex(0, 0, nullptr), group);
+ QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, Q_ARG(int, 0));
+void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices)
+ if (indices.count() == 0)
+ QVector<obs_sceneitem_t *> 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;
+ 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)
+ OBSData undoData = main->BackupScene(scene);
+ obs_sceneitem_group_ungroup(item);
+ 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)
+ itemIdx++;
+ QVector<OBSSceneItem> subItems;
+ obs_scene_enum_items(scene, enumItem, &subItems);
+ if (!subItems.size())
+ beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1);
+ for (int i = 0; i < subItems.size(); i++)
+ items.insert(i + itemIdx, subItems[i]);
+ st->UpdateWidgets();
+void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item)
+ int startIdx = -1;
+ int endIdx = -1;
+ 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;
+ items.remove(startIdx, endIdx - startIdx + 1);
+void SourceTreeModel::UpdateGroupState(bool update)
+ bool nowHasGroups = false;
+ for (auto &item : items) {
+ nowHasGroups = true;
+ if (nowHasGroups != hasGroups) {
+ hasGroups = nowHasGroups;
+ if (update) {
+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();
+void SourceTree::SetIconsVisible(bool visible)
+ iconsVisible = visible;
+void SourceTree::ResetWidgets()
+ 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)
+ 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);
+ widget->Update(force);
+void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select)
+ int i = 0;
+ for (; i < stm->items.count(); i++) {
+ if (stm->items[i] == sceneitem)
+ if (i == stm->items.count())
+ 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);
+ 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()) {
+ 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)
+ /* 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()) {
+ /* determine if any base group is selected */
+ bool hasGroups = false;
+ for (int i = 0; i < indices.size(); i++) {
+ /* if dropping a group, detect if it's */
+ /* below another group */
+ obs_sceneitem_t *itemBelow;
+ if (row == stm->items.count())
+ itemBelow = nullptr;
+ itemBelow = stm->items[row];
+ if (hasGroups) {
+ if (!itemBelow || obs_sceneitem_get_group(scene, itemBelow) != dropGroup) {
+ dropOnCollapsed = false;
+ /* if dropping groups on other groups, */
+ /* disregard as invalid drag/drop */
+ if (dropGroup && hasGroups) {
+ /* save undo data */
+ std::vector<obs_source_t *> sources;
+ 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 */
+ /* remove sub-items if selected */
+ for (int i = indices.size() - 1; i >= 0; i--) {
+ obs_scene_t *itemScene = obs_sceneitem_get_scene(item);
+ if (itemScene != scene) {
+ indices.removeAt(i);
+ /* add all sub-items of selected groups */
+ 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<QPersistentModelIndex> 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;
+ int firstIdx = persistentIndices.front().row();
+ int lastIdx = persistentIndices.back().row();
+ /* reorder scene items in back-end */
+ QVector<struct obs_sceneitem_order_info> 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<insertCollapsed_t *>(param))(item);
+ 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);
+ info.group = nullptr;
+ info.item = lastGroup;
+ orderList.insert(0, info);
+ auto updateScene = [&]() {
+ obs_sceneitem_t *item = items[i];
+ obs_sceneitem_t *group;
+ if (lastGroup) {
+ insertLastGroup();
+ lastGroup = item;
+ continue;
+ if (!hasGroups && i >= firstIdx && i <= lastIdx)
+ group = dropGroup;
+ group = obs_sceneitem_get_group(scene, item);
+ if (lastGroup && lastGroup != group) {
+ lastGroup = group;
+ info.group = group;
+ obs_scene_reorder_items2(scene, orderList.data(), orderList.size());
+ using updateScene_t = decltype(updateScene);
+ auto preUpdateScene = [](void *data, obs_scene_t *) {
+ (*reinterpret_cast<updateScene_t *>(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 */
+ 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);
+void SourceTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
+ 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)) {
+ 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.");
+ QString text = QTStr("Undo.GroupItems").arg("Unknown");
+ main->CreateSceneUndoRedoAction(text, undoSceneData, redoSceneData);
+ undoSceneData = nullptr;
+bool SourceTree::Edit(int row)
+ if (row < 0 || row >= stm->items.count())
+ QModelIndex index = stm->createIndex(row, 0);
+ QWidget *widget = indexWidget(index);
+ SourceTreeItem *itemWidget = reinterpret_cast<SourceTreeItem *>(widget);
+ if (itemWidget->IsEditing()) {
+ itemWidget->ExitEditMode(true);
+ itemWidget->EnterEditMode();
+ edit(index);
+bool SourceTree::MultipleBaseSelected() const
+ QModelIndexList selectedIndices = selectedIndexes();
+ if (selectedIndices.size() < 1) {
+ for (auto &idx : selectedIndices) {
+ obs_sceneitem_t *item = stm->items[idx.row()];
+ obs_scene *itemScene = obs_sceneitem_get_scene(item);
+bool SourceTree::GroupsSelected() const
+ if (!obs_sceneitem_is_group(item)) {
+bool SourceTree::GroupedItemsSelected() const
+ if (!selectedIndices.size()) {
+void SourceTree::Remove(OBSSceneItem item, OBSScene scene)
+ GetStm()->Remove(item);
+ main->SaveProject();
+ if (!main->SavingDisabled()) {
+ 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()
+ std::sort(indices.begin(), indices.end());
+ GetStm()->GroupSelectedItems(indices);
+void SourceTree::UngroupSelectedGroups()
+ 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", "<br/>"));
+ textPrepared = false;
+void SourceTree::paintEvent(QPaintEvent *event)
+ 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);
+ QListView::paintEvent(event);
+SourceTreeDelegate::SourceTreeDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
+QSize SourceTreeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
+ SourceTree *tree = qobject_cast<SourceTree *>(parent());
+ QWidget *item = tree->indexWidget(index);
+ if (!item)
+ return (QSize(0, 0));
+ return (QSize(option.widget->minimumWidth(), item->height()));
@@ -0,0 +1,202 @@
+#include <QList>
+#include <QVector>
+#include <QPointer>
+#include <QListView>
+#include <QCheckBox>
+#include <QStaticText>
+#include <QSvgRenderer>
+#include <QAbstractListModel>
+#include <QStyledItemDelegate>
+class QLabel;
+class OBSSourceLabel;
+class QCheckBox;
+class QLineEdit;
+class SourceTree;
+class QSpacerItem;
+class QHBoxLayout;
+class VisibilityItemWidget;
+class SourceTreeItem : public QFrame {
+ 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;
+ explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem);
+ bool IsEditing();
+ QSpacerItem *spacer = nullptr;
+ QCheckBox *expand = nullptr;
+ QLabel *iconLabel = nullptr;
+ QCheckBox *vis = nullptr;
+ QCheckBox *lock = nullptr;
+ QHBoxLayout *boxLayout = nullptr;
+ OBSSourceLabel *label = nullptr;
+ QLineEdit *editor = nullptr;
+ std::string newName;
+ SourceTree *tree;
+ OBSSceneItem sceneitem;
+ std::vector<OBSSignal> sigs;
+ virtual void paintEvent(QPaintEvent *event) override;
+ void ExitEditModeInternal(bool save);
+private slots:
+ void Clear();
+ void EnterEditMode();
+ void ExitEditMode(bool save);
+ void VisibilityChanged(bool visible);
+ void LockedChanged(bool locked);
+ void ExpandClicked(bool checked);
+ void Select();
+ void Deselect();
+class SourceTreeModel : public QAbstractListModel {
+ friend class SourceTreeItem;
+ SourceTree *st;
+ QVector<OBSSceneItem> items;
+ static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr);
+ 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);
+ 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 {
+ bool ignoreReorder = false;
+ bool textPrepared = false;
+ QStaticText textNoSources;
+ QSvgRenderer iconNoSources;
+ OBSData undoSceneData;
+ bool iconsVisible = true;
+ void UpdateNoSourcesMessage();
+ void ResetWidgets();
+ void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item);
+ void UpdateWidgets(bool force = false);
+ inline SourceTreeModel *GetStm() const { return reinterpret_cast<SourceTreeModel *>(model()); }
+ inline SourceTreeItem *GetItemWidget(int idx)
+ QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0));
+ return reinterpret_cast<SourceTreeItem *>(widget);
+ explicit SourceTree(QWidget *parent = nullptr);
+ inline bool IgnoreReorder() const { return ignoreReorder; }
+ inline void Clear() { GetStm()->Clear(); }
+ inline void Add(obs_sceneitem_t *item) { GetStm()->Add(item); }
+ inline OBSSceneItem Get(int idx) { return GetStm()->Get(idx); }
+ inline QString GetNewGroupName() { return GetStm()->GetNewGroupName(); }
+ void SelectItem(obs_sceneitem_t *sceneitem, bool select);
+ bool MultipleBaseSelected() const;
+ bool GroupsSelected() const;
+ bool GroupedItemsSelected() const;
+ void UpdateIcons();
+ void SetIconsVisible(bool visible);
+ inline void ReorderItems() { GetStm()->ReorderItems(); }
+ inline void RefreshItems() { GetStm()->SceneChanged(); }
+ void Remove(OBSSceneItem item, OBSScene scene);
+ void GroupSelectedItems();
+ void UngroupSelectedGroups();
+ bool Edit(int idx);
+ void NewGroupEdit(int idx);
+ virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
+ virtual void dropEvent(QDropEvent *event) override;
+ virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override;
+class SourceTreeDelegate : public QStyledItemDelegate {
+ SourceTreeDelegate(QObject *parent);
+ virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+#include "moc_visibility-item-widget.cpp"
+#include <QListWidget>
+#include <QMessageBox>
+#include <QKeyEvent>
+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->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+ vis->setChecked(enabled);
+ QHBoxLayout *itemLayout = new QHBoxLayout();
+ itemLayout->addWidget(vis);
+ itemLayout->addWidget(label);
+ itemLayout->setContentsMargins(0, 0, 0, 0);
+ setLayout(itemLayout);
+ connect(vis, &QCheckBox::clicked, [this](bool visible) { obs_source_set_enabled(source, visible); });
+void VisibilityItemWidget::OBSSourceEnabled(void *param, calldata_t *data)
+ VisibilityItemWidget *window = reinterpret_cast<VisibilityItemWidget *>(param);
+ bool enabled = calldata_bool(data, "enabled");
+ QMetaObject::invokeMethod(window, "SourceEnabled", Q_ARG(bool, enabled));
+void VisibilityItemWidget::SourceEnabled(bool enabled)
+ if (vis->isChecked() != enabled)
+void VisibilityItemWidget::SetColor(const QColor &color, bool active_, bool selected_)
+ /* Do not update unless the state has actually changed */
+ if (active_ == active && selected_ == selected)
+ QPalette pal = vis->palette();
+ pal.setColor(QPalette::WindowText, color);
+ vis->setPalette(pal);
+ label->setStyleSheet(QString("color: %1;").arg(color.name()));
+ active = active_;
+ selected = selected_;
+VisibilityItemDelegate::VisibilityItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
+void VisibilityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+ QStyledItemDelegate::paint(painter, option, index);
+ QObject *parentObj = parent();
+ QListWidget *list = qobject_cast<QListWidget *>(parentObj);
+ if (!list)
+ QListWidgetItem *item = list->item(index.row());
+ VisibilityItemWidget *widget = qobject_cast<VisibilityItemWidget *>(list->itemWidget(item));
+ if (!widget)
+ 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;
+ QPalette::ColorGroup group = QPalette::Active;
+ QPalette::ColorRole highlightRole = QPalette::WindowText;
+ QPalette::ColorRole highlightRole = QPalette::HighlightedText;
+ QPalette::ColorRole role;
+ if (selected && active)
+ role = highlightRole;
+ role = QPalette::WindowText;
+ widget->SetColor(palette.color(group, role), active, selected);
+bool VisibilityItemDelegate::eventFilter(QObject *object, QEvent *event)
+ QWidget *editor = qobject_cast<QWidget *>(object);
+ if (!editor)
+ if (event->type() == QEvent::KeyPress) {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
+ return QStyledItemDelegate::eventFilter(object, event);
+void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source)
+ VisibilityItemWidget *baseWidget = new VisibilityItemWidget(source);
+ list->setItemWidget(item, baseWidget);
@@ -0,0 +1,50 @@
+class QListWidget;
+class QListWidgetItem;
+class VisibilityItemWidget : public QWidget {
+ OBSSource source;
+ OBSSignal enabledSignal;
+ bool active = false;
+ bool selected = false;
+ static void OBSSourceEnabled(void *param, calldata_t *data);
+ void SourceEnabled(bool enabled);
+ VisibilityItemWidget(obs_source_t *source);
+ void SetColor(const QColor &color, bool active, bool selected);
+class VisibilityItemDelegate : public QStyledItemDelegate {
+ VisibilityItemDelegate(QObject *parent = nullptr);
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+ bool eventFilter(QObject *object, QEvent *event) override;
+void SetupVisibilityItem(QListWidget *list, QListWidgetItem *item, obs_source_t *source);