Files
obs-studio/frontend/widgets/OBSBasic_Preview.cpp
Sebastian Beckmann f6a56227eb frontend: Remove implicit capture of "this" using "="
Implicitly capturing "this" with the capture default "=" is deprecated
with C++20. We fix this by either explicitly passing this, or by copying
the required members manually.
While this exposes some rather expensive copies like the QList
selectedItems in OBSBasic_Preview, it doesn't introduce them ("=" copies
implicitly).
2025-12-18 17:30:42 -05:00

667 lines
19 KiB
C++

/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
Zachary Lund <admin@computerquip.com>
Philippe Groarke <philippe.groarke@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "OBSBasic.hpp"
#include <utility/display-helpers.hpp>
#include <widgets/OBSProjector.hpp>
#include <qt-wrappers.hpp>
#include <QColorDialog>
#include <sstream>
extern void undo_redo(const std::string &data);
using namespace std;
void OBSBasic::InitPrimitives()
{
ProfileScope("OBSBasic::InitPrimitives");
obs_enter_graphics();
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(0.0f, 1.0f);
gs_vertex2f(1.0f, 0.0f);
gs_vertex2f(1.0f, 1.0f);
box = gs_render_save();
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(0.0f, 1.0f);
boxLeft = gs_render_save();
gs_render_start(true);
gs_vertex2f(0.0f, 0.0f);
gs_vertex2f(1.0f, 0.0f);
boxTop = gs_render_save();
gs_render_start(true);
gs_vertex2f(1.0f, 0.0f);
gs_vertex2f(1.0f, 1.0f);
boxRight = gs_render_save();
gs_render_start(true);
gs_vertex2f(0.0f, 1.0f);
gs_vertex2f(1.0f, 1.0f);
boxBottom = gs_render_save();
gs_render_start(true);
for (int i = 0; i <= 360; i += (360 / 20)) {
float pos = RAD(float(i));
gs_vertex2f(cosf(pos), sinf(pos));
}
circle = gs_render_save();
InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine);
obs_leave_graphics();
}
void OBSBasic::UpdatePreviewScalingMenu()
{
bool fixedScaling = ui->preview->IsFixedScaling();
float scalingAmount = ui->preview->GetScalingAmount();
if (!fixedScaling) {
ui->actionScaleWindow->setChecked(true);
ui->actionScaleCanvas->setChecked(false);
ui->actionScaleOutput->setChecked(false);
return;
}
obs_video_info ovi;
obs_get_video_info(&ovi);
ui->actionScaleWindow->setChecked(false);
ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f);
ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width));
}
void OBSBasic::DrawBackdrop(float cx, float cy)
{
if (!box)
return;
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "DrawBackdrop");
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
vec4 colorVal;
vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f);
gs_effect_set_vec4(color, &colorVal);
gs_technique_begin(tech);
gs_technique_begin_pass(tech, 0);
gs_matrix_push();
gs_matrix_identity();
gs_matrix_scale3f(float(cx), float(cy), 1.0f);
gs_load_vertexbuffer(box);
gs_draw(GS_TRISTRIP, 0, 0);
gs_matrix_pop();
gs_technique_end_pass(tech);
gs_technique_end(tech);
gs_load_vertexbuffer(nullptr);
GS_DEBUG_MARKER_END();
}
void OBSBasic::RenderMain(void *data, uint32_t, uint32_t)
{
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain");
OBSBasic *window = static_cast<OBSBasic *>(data);
obs_video_info ovi;
obs_get_video_info(&ovi);
window->previewCX = int(window->previewScale * float(ovi.base_width));
window->previewCY = int(window->previewScale * float(ovi.base_height));
gs_viewport_push();
gs_projection_push();
obs_display_t *display = window->ui->preview->GetDisplay();
uint32_t width, height;
obs_display_size(display, &width, &height);
float right = float(width) - window->previewX;
float bottom = float(height) - window->previewY;
gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f);
window->ui->preview->DrawOverflow();
/* --------------------------------------- */
gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f);
gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY);
if (window->IsPreviewProgramMode()) {
window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
OBSScene scene = window->GetCurrentScene();
obs_source_t *source = obs_scene_get_source(scene);
if (source)
obs_source_video_render(source);
} else {
obs_render_main_texture_src_color_only();
}
gs_load_vertexbuffer(nullptr);
/* --------------------------------------- */
gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f);
gs_reset_viewport();
uint32_t targetCX = window->previewCX;
uint32_t targetCY = window->previewCY;
if (window->drawSafeAreas) {
RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY);
RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY);
RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY);
RenderSafeAreas(window->leftLine, targetCX, targetCY);
RenderSafeAreas(window->topLine, targetCX, targetCY);
RenderSafeAreas(window->rightLine, targetCX, targetCY);
}
window->ui->preview->DrawSceneEditing();
if (window->drawSpacingHelpers)
window->ui->preview->DrawSpacingHelpers();
/* --------------------------------------- */
gs_projection_pop();
gs_viewport_pop();
GS_DEBUG_MARKER_END();
}
void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
{
QSize targetSize;
bool isFixedScaling;
obs_video_info ovi;
/* resize preview panel to fix to the top section of the window */
targetSize = GetPixelSize(ui->preview);
isFixedScaling = ui->preview->IsFixedScaling();
obs_get_video_info(&ovi);
if (isFixedScaling) {
previewScale = ui->preview->GetScalingAmount();
ui->preview->ClampScrollingOffsets();
GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2,
targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY,
previewScale);
previewX += ui->preview->GetScrollX();
previewY += ui->preview->GetScrollY();
} else {
GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2,
targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale);
}
ui->preview->SetScalingAmount(previewScale);
previewX += float(PREVIEW_EDGE_SIZE);
previewY += float(PREVIEW_EDGE_SIZE);
}
void OBSBasic::on_preview_customContextMenuRequested()
{
CreateSourcePopupMenu(GetTopSelectedSourceItem(), true);
}
void OBSBasic::on_previewDisabledWidget_customContextMenuRequested()
{
QMenu popup(this);
delete previewProjectorMain;
QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview);
action->setCheckable(true);
action->setChecked(obs_display_enabled(ui->preview->GetDisplay()));
previewProjectorMain = new QMenu(QTStr("Projector.Open.Preview"));
AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector);
previewProjectorMain->addSeparator();
previewProjectorMain->addAction(QTStr("Projector.Window"), this, &OBSBasic::OpenPreviewWindow);
popup.addMenu(previewProjectorMain);
popup.exec(QCursor::pos());
}
void OBSBasic::EnablePreviewDisplay(bool enable)
{
obs_display_set_enabled(ui->preview->GetDisplay(), enable);
ui->previewContainer->setVisible(enable);
ui->previewDisabledWidget->setVisible(!enable);
}
void OBSBasic::TogglePreview()
{
previewEnabled = !previewEnabled;
EnablePreviewDisplay(previewEnabled);
}
void OBSBasic::EnablePreview()
{
if (previewProgramMode)
return;
previewEnabled = true;
EnablePreviewDisplay(true);
}
void OBSBasic::DisablePreview()
{
if (previewProgramMode)
return;
previewEnabled = false;
EnablePreviewDisplay(false);
}
static bool nudge_callback(obs_scene_t *, obs_sceneitem_t *item, void *param)
{
if (obs_sceneitem_locked(item))
return true;
struct vec2 &offset = *static_cast<struct vec2 *>(param);
struct vec2 pos;
if (!obs_sceneitem_selected(item)) {
if (obs_sceneitem_is_group(item)) {
struct vec3 offset3;
vec3_set(&offset3, offset.x, offset.y, 0.0f);
struct matrix4 matrix;
obs_sceneitem_get_draw_transform(item, &matrix);
vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f);
matrix4_inv(&matrix, &matrix);
vec3_transform(&offset3, &offset3, &matrix);
struct vec2 new_offset;
vec2_set(&new_offset, offset3.x, offset3.y);
obs_sceneitem_group_enum_items(item, nudge_callback, &new_offset);
}
return true;
}
obs_sceneitem_get_pos(item, &pos);
vec2_add(&pos, &pos, &offset);
obs_sceneitem_set_pos(item, &pos);
return true;
}
void OBSBasic::Nudge(int dist, MoveDir dir)
{
if (ui->preview->Locked())
return;
struct vec2 offset;
vec2_set(&offset, 0.0f, 0.0f);
switch (dir) {
case MoveDir::Up:
offset.y = (float)-dist;
break;
case MoveDir::Down:
offset.y = (float)dist;
break;
case MoveDir::Left:
offset.x = (float)-dist;
break;
case MoveDir::Right:
offset.x = (float)dist;
break;
}
if (!recent_nudge) {
recent_nudge = true;
OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true);
std::string undo_data(obs_data_get_json(wrapper));
nudge_timer = new QTimer;
QObject::connect(nudge_timer, &QTimer::timeout, this, [this, &recent_nudge = recent_nudge, undo_data]() {
OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true);
std::string redo_data(obs_data_get_json(rwrapper));
undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())),
undo_redo, undo_redo, undo_data, redo_data);
recent_nudge = false;
});
connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater);
nudge_timer->setSingleShot(true);
}
if (nudge_timer) {
nudge_timer->stop();
nudge_timer->start(1000);
} else {
blog(LOG_ERROR, "No nudge timer!");
}
obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset);
}
void OBSBasic::on_actionLockPreview_triggered()
{
ui->preview->ToggleLocked();
ui->actionLockPreview->setChecked(ui->preview->Locked());
}
void OBSBasic::on_scalingMenu_aboutToShow()
{
obs_video_info ovi;
obs_get_video_info(&ovi);
QAction *action = ui->actionScaleCanvas;
QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas");
text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height));
action->setText(text);
action = ui->actionScaleOutput;
text = QTStr("Basic.MainMenu.Edit.Scale.Output");
text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height));
action->setText(text);
action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height));
UpdatePreviewScalingMenu();
}
void OBSBasic::setPreviewScalingWindow()
{
ui->preview->SetFixedScaling(false);
ui->preview->ResetScrollingOffset();
emit ui->preview->DisplayResized();
}
void OBSBasic::setPreviewScalingCanvas()
{
ui->preview->SetFixedScaling(true);
ui->preview->SetScalingLevel(0);
emit ui->preview->DisplayResized();
}
void OBSBasic::setPreviewScalingOutput()
{
obs_video_info ovi;
obs_get_video_info(&ovi);
ui->preview->SetFixedScaling(true);
float scalingAmount = float(ovi.output_width) / float(ovi.base_width);
// log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY)
int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY)));
ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount);
emit ui->preview->DisplayResized();
}
static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems)
{
for (int x = 0; x < selectedItems.count(); x++) {
SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row());
treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb));
treeItem->style()->unpolish(treeItem);
treeItem->style()->polish(treeItem);
OBSSceneItem sceneItem = sources->Get(selectedItems[x].row());
OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem);
obs_data_set_int(privData, "color-preset", 1);
obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb)));
}
}
void OBSBasic::ColorChange()
{
QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes();
QAction *action = qobject_cast<QAction *>(sender());
QPushButton *colorButton = qobject_cast<QPushButton *>(sender());
if (selectedItems.count() == 0)
return;
if (colorButton) {
int preset = colorButton->property("bgColor").value<int>();
for (int x = 0; x < selectedItems.count(); x++) {
SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row());
treeItem->setStyleSheet("");
treeItem->setProperty("bgColor", preset);
treeItem->style()->unpolish(treeItem);
treeItem->style()->polish(treeItem);
OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row());
OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem);
obs_data_set_int(privData, "color-preset", preset + 1);
obs_data_set_string(privData, "color", "");
}
for (int i = 1; i < 9; i++) {
stringstream button;
button << "preset" << i;
QPushButton *cButton =
colorButton->parentWidget()->findChild<QPushButton *>(button.str().c_str());
cButton->setStyleSheet("border: 1px solid black");
}
colorButton->setStyleSheet("border: 2px solid black");
} else if (action) {
int preset = action->property("bgColor").value<int>();
if (preset == 1) {
OBSSceneItem curSceneItem = GetCurrentSceneItem();
SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem);
OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem);
int oldPreset = obs_data_get_int(curPrivData, "color-preset");
const QString oldSheet = curTreeItem->styleSheet();
auto liveChangeColor = [=](const QColor &color) {
if (color.isValid()) {
curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb));
}
};
auto changedColor = [this, selectedItems](const QColor &color) {
if (color.isValid()) {
ConfirmColor(ui->sources, color, selectedItems);
}
};
auto rejected = [=]() {
if (oldPreset == 1) {
curTreeItem->setStyleSheet(oldSheet);
curTreeItem->setProperty("bgColor", 0);
} else if (oldPreset == 0) {
curTreeItem->setStyleSheet("background: none");
curTreeItem->setProperty("bgColor", 0);
} else {
curTreeItem->setStyleSheet("");
curTreeItem->setProperty("bgColor", oldPreset - 1);
}
curTreeItem->style()->unpolish(curTreeItem);
curTreeItem->style()->polish(curTreeItem);
};
QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel;
const char *oldColor = obs_data_get_string(curPrivData, "color");
const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000";
#ifdef __linux__
// TODO: Revisit hang on Ubuntu with native dialog
options |= QColorDialog::DontUseNativeDialog;
#endif
QColorDialog *colorDialog = new QColorDialog(this);
colorDialog->setOptions(options);
colorDialog->setCurrentColor(QColor(customColor));
connect(colorDialog, &QColorDialog::currentColorChanged, this, liveChangeColor);
connect(colorDialog, &QColorDialog::colorSelected, this, changedColor);
connect(colorDialog, &QColorDialog::rejected, this, rejected);
colorDialog->open();
} else {
for (int x = 0; x < selectedItems.count(); x++) {
SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row());
treeItem->setStyleSheet("background: none");
treeItem->setProperty("bgColor", preset);
treeItem->style()->unpolish(treeItem);
treeItem->style()->polish(treeItem);
OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row());
OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem);
obs_data_set_int(privData, "color-preset", preset);
obs_data_set_string(privData, "color", "");
}
}
}
}
void OBSBasic::UpdateProjectorHideCursor()
{
for (size_t i = 0; i < projectors.size(); i++)
projectors[i]->SetHideCursor();
}
void OBSBasic::UpdateProjectorAlwaysOnTop(bool top)
{
for (size_t i = 0; i < projectors.size(); i++)
SetAlwaysOnTop(projectors[i], top);
}
void OBSBasic::ResetProjectors()
{
OBSDataArrayAutoRelease savedProjectorList = SaveProjectors();
ClearProjectors();
LoadSavedProjectors(savedProjectorList);
}
void OBSBasic::UpdatePreviewSafeAreas()
{
drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas");
}
void OBSBasic::UpdatePreviewOverflowSettings()
{
bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden");
bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden");
bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible");
ui->preview->SetOverflowHidden(hidden);
ui->preview->SetOverflowSelectionHidden(select);
ui->preview->SetOverflowAlwaysVisible(always);
}
static inline QColor color_from_int(long long val)
{
return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff);
}
QColor OBSBasic::GetSelectionColor() const
{
if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed"));
} else {
return QColor::fromRgb(255, 0, 0);
}
}
QColor OBSBasic::GetCropColor() const
{
if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen"));
} else {
return QColor::fromRgb(0, 255, 0);
}
}
QColor OBSBasic::GetHoverColor() const
{
if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) {
return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue"));
} else {
return QColor::fromRgb(0, 127, 255);
}
}
void OBSBasic::UpdatePreviewSpacingHelpers()
{
drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled");
}
float OBSBasic::GetDevicePixelRatio()
{
return dpi;
}
void OBSBasic::UpdatePreviewControls()
{
const int scalingLevel = ui->preview->GetScalingLevel();
if (!ui->preview->IsFixedScaling()) {
ui->previewXScrollBar->setRange(0, 0);
ui->previewYScrollBar->setRange(0, 0);
ui->actionPreviewResetZoom->setEnabled(false);
return;
}
const bool minZoom = scalingLevel == MAX_SCALING_LEVEL;
const bool maxZoom = scalingLevel == -MAX_SCALING_LEVEL;
ui->actionPreviewZoomIn->setEnabled(!minZoom);
ui->previewZoomInButton->setEnabled(!minZoom);
ui->actionPreviewZoomOut->setEnabled(!maxZoom);
ui->previewZoomOutButton->setEnabled(!maxZoom);
ui->actionPreviewResetZoom->setEnabled(scalingLevel != 0);
}
void OBSBasic::PreviewScalingModeChanged(int value)
{
switch (value) {
case 0:
setPreviewScalingWindow();
break;
case 1:
setPreviewScalingCanvas();
break;
case 2:
setPreviewScalingOutput();
break;
};
}