/****************************************************************************** Copyright (C) 2023 by Lain Bailey Zachary Lund Philippe Groarke 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 "OBSBasic.hpp" #include "ColorSelect.hpp" #include "OBSProjector.hpp" #include "VolControl.hpp" #include #include #include #include #include #include #include using namespace std; static inline bool HasAudioDevices(const char *source_id) { const char *output_id = source_id; obs_properties_t *props = obs_get_source_properties(output_id); size_t count = 0; if (!props) return false; obs_property_t *devices = obs_properties_get(props, "device_id"); if (devices) count = obs_property_list_item_count(devices); obs_properties_destroy(props); return count != 0; } void OBSBasic::CreateFirstRunSources() { bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); #ifdef __APPLE__ /* On macOS 13 and above, the SCK based audio capture provides a * better alternative to the device-based audio capture. */ if (__builtin_available(macOS 13.0, *)) { hasDesktopAudio = false; } #endif if (hasDesktopAudio) ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); if (hasInputAudio) ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); } OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) { return item ? GetOBSRef(item) : nullptr; } OBSSceneItem OBSBasic::GetCurrentSceneItem() { return ui->sources->Get(GetTopSelectedSourceItem()); } static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) { QList items = listWidget->findItems(prevName, Qt::MatchExactly); for (int i = 0; i < items.count(); i++) items[i]->setText(newName); } void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) { RenameListValues(ui->scenes, newName, prevName); if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) vcamConfig.source = newName.toStdString(); if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) vcamConfig.scene = newName.toStdString(); SaveProject(); obs_scene_t *scene = obs_scene_from_source(source); if (scene) OBSProjector::UpdateMultiviewProjectors(); UpdateContextBar(); UpdatePreviewProgramIndicators(); } void OBSBasic::GetAudioSourceFilters() { QAction *action = reinterpret_cast(sender()); VolControl *vol = action->property("volControl").value(); obs_source_t *source = vol->GetSource(); CreateFiltersWindow(source); } void OBSBasic::GetAudioSourceProperties() { QAction *action = reinterpret_cast(sender()); VolControl *vol = action->property("volControl").value(); obs_source_t *source = vol->GetSource(); CreatePropertiesWindow(source); } void OBSBasic::MixerRenameSource() { QAction *action = reinterpret_cast(sender()); VolControl *vol = action->property("volControl").value(); OBSSource source = vol->GetSource(); const char *prevName = obs_source_get_name(source); for (;;) { string name; bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); if (!accepted) return; if (name.empty()) { OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); continue; } OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); if (sourceTest) { OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); continue; } obs_source_set_name(source, name.c_str()); break; } } void OBSBasic::ActivateAudioSource(OBSSource source) { if (SourceMixerHidden(source)) return; if (!obs_source_active(source)) return; if (!obs_source_audio_active(source)) return; bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); VolControl *vol = new VolControl(source, true, vertical); vol->EnableSlider(!SourceVolumeLocked(source)); double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); vol->SetMeterDecayRate(meterDecayRate); uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); enum obs_peak_meter_type peakMeterType; switch (peakMeterTypeIdx) { case 0: peakMeterType = SAMPLE_PEAK_METER; break; case 1: peakMeterType = TRUE_PEAK_METER; break; default: peakMeterType = SAMPLE_PEAK_METER; break; } vol->setPeakMeterType(peakMeterType); vol->setContextMenuPolicy(Qt::CustomContextMenu); connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); InsertQObjectByName(volumes, vol); for (auto volume : volumes) { if (vertical) ui->vVolControlLayout->addWidget(volume); else ui->hVolControlLayout->addWidget(volume); } } void OBSBasic::DeactivateAudioSource(OBSSource source) { for (size_t i = 0; i < volumes.size(); i++) { if (volumes[i]->GetSource() == source) { delete volumes[i]; volumes.erase(volumes.begin() + i); break; } } } bool OBSBasic::QueryRemoveSource(obs_source_t *source) { if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { int count = ui->scenes->count(); if (count == 1) { OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); return false; } } const char *name = obs_source_get_name(source); QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); QMessageBox remove_source(this); remove_source.setText(text); QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); remove_source.setDefaultButton(Yes); remove_source.addButton(QTStr("No"), QMessageBox::NoRole); remove_source.setIcon(QMessageBox::Question); remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); remove_source.exec(); return Yes == remove_source.clickedButton(); } void OBSBasic::ReorderSources(OBSScene scene) { if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) return; ui->sources->ReorderItems(); SaveProject(); } void OBSBasic::RefreshSources(OBSScene scene) { if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) return; ui->sources->RefreshItems(); SaveProject(); } void OBSBasic::SourceCreated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); if (obs_scene_from_source(source) != NULL) QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceRemoved(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); if (obs_scene_from_source(source) != NULL) QMetaObject::invokeMethod(static_cast(data), "RemoveScene", Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceActivated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); uint32_t flags = obs_source_get_output_flags(source); if (flags & OBS_SOURCE_AUDIO) QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceDeactivated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); uint32_t flags = obs_source_get_output_flags(source); if (flags & OBS_SOURCE_AUDIO) QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); if (obs_source_active(source)) QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", Q_ARG(OBSSource, OBSSource(source))); } void OBSBasic::SourceRenamed(void *data, calldata_t *params) { obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); const char *newName = calldata_string(params, "new_name"); const char *prevName = calldata_string(params, "prev_name"); QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); } extern char *get_new_source_name(const char *name, const char *format); void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) { bool disable = deviceId && strcmp(deviceId, "disabled") == 0; OBSSourceAutoRelease source; OBSDataAutoRelease settings; source = obs_get_output_source(channel); if (source) { if (disable) { obs_set_output_source(channel, nullptr); } else { settings = obs_source_get_settings(source); const char *oldId = obs_data_get_string(settings, "device_id"); if (strcmp(oldId, deviceId) != 0) { obs_data_set_string(settings, "device_id", deviceId); obs_source_update(source, settings); } } } else if (!disable) { BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); settings = obs_data_create(); obs_data_set_string(settings, "device_id", deviceId); source = obs_source_create(sourceId, name, settings, nullptr); obs_set_output_source(channel, source); } } void OBSBasic::SetDeinterlacingMode() { QAction *action = reinterpret_cast(sender()); obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_source_t *source = obs_sceneitem_get_source(sceneItem); obs_source_set_deinterlace_mode(source, mode); } void OBSBasic::SetDeinterlacingOrder() { QAction *action = reinterpret_cast(sender()); obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_source_t *source = obs_sceneitem_get_source(sceneItem); obs_source_set_deinterlace_field_order(source, order); } QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) { obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); QAction *action; #define ADD_MODE(name, mode) \ action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ action->setProperty("mode", (int)mode); \ action->setCheckable(true); \ action->setChecked(deinterlaceMode == mode); ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); #undef ADD_MODE menu->addSeparator(); #define ADD_ORDER(name, order) \ action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ action->setProperty("order", (int)order); \ action->setCheckable(true); \ action->setChecked(deinterlaceOrder == order); ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); #undef ADD_ORDER return menu; } void OBSBasic::SetScaleFilter() { QAction *action = reinterpret_cast(sender()); obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_sceneitem_set_scale_filter(sceneItem, mode); } QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) { obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); QAction *action; #define ADD_MODE(name, mode) \ action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ action->setProperty("mode", (int)mode); \ action->setCheckable(true); \ action->setChecked(scaleFilter == mode); ADD_MODE("Disable", OBS_SCALE_DISABLE); ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); #undef ADD_MODE return menu; } void OBSBasic::SetBlendingMethod() { QAction *action = reinterpret_cast(sender()); obs_blending_method method = (obs_blending_method)action->property("method").toInt(); OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_sceneitem_set_blending_method(sceneItem, method); } QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) { obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); QAction *action; #define ADD_MODE(name, method) \ action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ action->setProperty("method", (int)method); \ action->setCheckable(true); \ action->setChecked(blendingMethod == method); ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); #undef ADD_MODE return menu; } void OBSBasic::SetBlendingMode() { QAction *action = reinterpret_cast(sender()); obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_sceneitem_set_blending_mode(sceneItem, mode); } QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) { obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); QAction *action; #define ADD_MODE(name, mode) \ action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ action->setProperty("mode", (int)mode); \ action->setCheckable(true); \ action->setChecked(blendingMode == mode); ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); #undef ADD_MODE return menu; } QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, obs_sceneitem_t *item) { QAction *action; menu->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%);}")); obs_data_t *privData = obs_sceneitem_get_private_settings(item); obs_data_release(privData); obs_data_set_default_int(privData, "color-preset", 0); int preset = obs_data_get_int(privData, "color-preset"); action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); action->setCheckable(true); action->setProperty("bgColor", 0); action->setChecked(preset == 0); action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); action->setCheckable(true); action->setProperty("bgColor", 1); action->setChecked(preset == 1); menu->addSeparator(); widgetAction->setDefaultWidget(select); for (int i = 1; i < 9; i++) { stringstream button; button << "preset" << i; QPushButton *colorButton = select->findChild(button.str().c_str()); if (preset == i + 1) colorButton->setStyleSheet("border: 2px solid black"); colorButton->setProperty("bgColor", i); select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); } menu->addAction(widgetAction); return menu; } void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) { QMenu popup(this); delete previewProjectorSource; delete sourceProjector; delete scaleFilteringMenu; delete blendingMethodMenu; delete blendingModeMenu; delete colorMenu; delete colorWidgetAction; delete colorSelect; delete deinterlaceMenu; OBSSceneItem sceneItem; obs_source_t *source; uint32_t flags; bool isAsyncVideo = false; bool hasAudio = false; bool hasVideo = false; bool sourceSelected = idx != -1; if (sourceSelected) { sceneItem = ui->sources->Get(idx); source = obs_sceneitem_get_source(sceneItem); flags = obs_source_get_output_flags(source); isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; } // Add new source QPointer addSourceMenu = CreateAddSourcePopupMenu(); if (addSourceMenu) { popup.addMenu(addSourceMenu); popup.addSeparator(); } // Preview menu entries if (preview) { QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); action->setCheckable(true); action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); if (IsPreviewProgramMode()) action->setEnabled(false); popup.addAction(ui->actionLockPreview); popup.addMenu(ui->scalingMenu); popup.addSeparator(); } // Projector menu entries if (preview) { previewProjectorSource = new QMenu(QTStr("Projector.Open.Preview")); AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); previewProjectorSource->addSeparator(); previewProjectorSource->addAction(QTStr("Projector.Window"), this, &OBSBasic::OpenPreviewWindow); popup.addMenu(previewProjectorSource); } if (hasVideo) { sourceProjector = new QMenu(QTStr("Projector.Open.Source")); AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); sourceProjector->addSeparator(); sourceProjector->addAction(QTStr("Projector.Window"), this, &OBSBasic::OpenSourceWindow); popup.addMenu(sourceProjector); } popup.addSeparator(); // Screenshot menu entries if (preview) { popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); } if (hasVideo) { popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); } popup.addSeparator(); if (sourceSelected) { // Sources list menu entries if (!preview) { colorMenu = new QMenu(QTStr("ChangeBG")); colorWidgetAction = new QWidgetAction(colorMenu); colorSelect = new ColorSelect(colorMenu); popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); if (hasAudio) { QAction *actionHideMixer = popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); actionHideMixer->setCheckable(true); actionHideMixer->setChecked(SourceMixerHidden(source)); } popup.addSeparator(); } // Scene item menu entries if (hasVideo && source) { scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); blendingModeMenu = new QMenu(QTStr("BlendingMode")); popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); if (isAsyncVideo) { deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); } popup.addMenu(CreateVisibilityTransitionMenu(true)); popup.addMenu(CreateVisibilityTransitionMenu(false)); popup.addSeparator(); QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, &OBSBasic::ResizeOutputSizeOfSource); int width = obs_source_get_width(source); int height = obs_source_get_height(source); resizeOutput->setEnabled(!obs_video_active()); if (width < 32 || height < 32) resizeOutput->setEnabled(false); } popup.addSeparator(); popup.addMenu(ui->orderMenu); if (hasVideo) { popup.addMenu(ui->transformMenu); } popup.addSeparator(); // Source grouping if (ui->sources->MultipleBaseSelected()) { popup.addSeparator(); popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); } else if (ui->sources->GroupsSelected()) { popup.addSeparator(); popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); } popup.addSeparator(); popup.addAction(ui->actionCopySource); popup.addAction(ui->actionPasteRef); popup.addAction(ui->actionPasteDup); popup.addSeparator(); if (hasVideo || hasAudio) { popup.addAction(ui->actionCopyFilters); popup.addAction(ui->actionPasteFilters); popup.addSeparator(); } popup.addAction(ui->actionRemoveSource); popup.addAction(renameSource); popup.addSeparator(); if (flags && flags & OBS_SOURCE_INTERACTION) popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); QAction *action = popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); action->setEnabled(obs_source_configurable(source)); } else { popup.addAction(ui->actionPasteRef); popup.addAction(ui->actionPasteDup); } popup.exec(QCursor::pos()); } void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) { if (ui->scenes->count()) { QModelIndex idx = ui->sources->indexAt(pos); CreateSourcePopupMenu(idx.row(), false); } } static inline bool should_show_properties(obs_source_t *source, const char *id) { if (!source) return false; if (strcmp(id, "group") == 0) return false; if (!obs_source_configurable(source)) return false; uint32_t caps = obs_source_get_output_flags(source); if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) return false; return true; } void OBSBasic::AddSource(const char *id) { if (id && *id) { OBSBasicSourceSelect sourceSelect(this, id, undo_s); sourceSelect.exec(); if (should_show_properties(sourceSelect.newSource, id)) { CreatePropertiesWindow(sourceSelect.newSource); } } } QMenu *OBSBasic::CreateAddSourcePopupMenu() { const char *unversioned_type; const char *type; bool foundValues = false; bool foundDeprecated = false; size_t idx = 0; QMenu *popup = new QMenu(QTStr("AddSource"), this); QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); auto getActionAfter = [](QMenu *menu, const QString &name) { QList actions = menu->actions(); for (QAction *menuAction : actions) { if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) return menuAction; } return (QAction *)nullptr; }; auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { QString qname = QT_UTF8(name); QAction *popupItem = new QAction(qname, this); connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); QIcon icon; if (strcmp(type, "scene") == 0) icon = GetSceneIcon(); else icon = GetSourceIcon(type); popupItem->setIcon(icon); QAction *after = getActionAfter(popup, qname); popup->insertAction(after, popupItem); }; while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { const char *name = obs_source_get_display_name(type); uint32_t caps = obs_get_source_output_flags(type); if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) continue; if ((caps & OBS_SOURCE_DEPRECATED) == 0) { addSource(popup, unversioned_type, name); } else { addSource(deprecated, unversioned_type, name); foundDeprecated = true; } foundValues = true; } addSource(popup, "scene", Str("Basic.Scene")); popup->addSeparator(); QAction *addGroup = new QAction(QTStr("Group"), this); addGroup->setIcon(GetGroupIcon()); connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); popup->addAction(addGroup); if (!foundDeprecated) { delete deprecated; deprecated = nullptr; } if (!foundValues) { delete popup; popup = nullptr; } else if (foundDeprecated) { popup->addSeparator(); popup->addMenu(deprecated); } return popup; } void OBSBasic::AddSourcePopupMenu(const QPoint &pos) { if (!GetCurrentScene()) { // Tell the user he needs a scene first (help beginners). OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), QTStr("Basic.Main.AddSourceHelp.Text")); return; } QScopedPointer popup(CreateAddSourcePopupMenu()); if (popup) popup->exec(pos); } void OBSBasic::on_actionAddSource_triggered() { AddSourcePopupMenu(QCursor::pos()); } static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) { vector &items = *static_cast *>(param); if (obs_sceneitem_selected(item)) { items.emplace_back(item); } else if (obs_sceneitem_is_group(item)) { obs_sceneitem_group_enum_items(item, remove_items, &items); } return true; }; void OBSBasic::on_actionRemoveSource_triggered() { vector items; OBSScene scene = GetCurrentScene(); obs_source_t *scene_source = obs_scene_get_source(scene); obs_scene_enum_items(scene, remove_items, &items); if (!items.size()) return; /* ------------------------------------- */ /* confirm action with user */ bool confirmed = false; if (items.size() > 1) { QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); QMessageBox remove_items(this); remove_items.setText(text); QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); remove_items.setDefaultButton(Yes); remove_items.addButton(QTStr("No"), QMessageBox::NoRole); remove_items.setIcon(QMessageBox::Question); remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); remove_items.exec(); confirmed = Yes == remove_items.clickedButton(); } else { OBSSceneItem &item = items[0]; obs_source_t *source = obs_sceneitem_get_source(item); if (source && QueryRemoveSource(source)) confirmed = true; } if (!confirmed) return; /* ----------------------------------------------- */ /* save undo data */ OBSData undo_data = BackupScene(scene_source); /* ----------------------------------------------- */ /* remove items */ for (auto &item : items) obs_sceneitem_remove(item); /* ----------------------------------------------- */ /* save redo data */ OBSData redo_data = BackupScene(scene_source); /* ----------------------------------------------- */ /* add undo/redo action */ QString action_name; if (items.size() > 1) { action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); } else { QString str = QTStr("Undo.Delete"); action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); } CreateSceneUndoRedoAction(action_name, undo_data, redo_data); } void OBSBasic::on_actionInteract_triggered() { OBSSceneItem item = GetCurrentSceneItem(); OBSSource source = obs_sceneitem_get_source(item); if (source) CreateInteractionWindow(source); } void OBSBasic::on_actionSourceProperties_triggered() { OBSSceneItem item = GetCurrentSceneItem(); OBSSource source = obs_sceneitem_get_source(item); if (source) CreatePropertiesWindow(source); } void OBSBasic::on_actionSourceUp_triggered() { MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); } void OBSBasic::on_actionSourceDown_triggered() { MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); } void OBSBasic::on_actionMoveUp_triggered() { MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); } void OBSBasic::on_actionMoveDown_triggered() { MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); } void OBSBasic::on_actionMoveToTop_triggered() { MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); } void OBSBasic::on_actionMoveToBottom_triggered() { MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); } void OBSBasic::OpenFilters(OBSSource source) { if (source == nullptr) { OBSSceneItem item = GetCurrentSceneItem(); source = obs_sceneitem_get_source(item); } CreateFiltersWindow(source); } void OBSBasic::OpenProperties(OBSSource source) { if (source == nullptr) { OBSSceneItem item = GetCurrentSceneItem(); source = obs_sceneitem_get_source(item); } CreatePropertiesWindow(source); } void OBSBasic::OpenInteraction(OBSSource source) { if (source == nullptr) { OBSSceneItem item = GetCurrentSceneItem(); source = obs_sceneitem_get_source(item); } CreateInteractionWindow(source); } void OBSBasic::OpenEditTransform(OBSSceneItem item) { if (!item) item = GetCurrentSceneItem(); if (!item) return; CreateEditTransformWindow(item); } int OBSBasic::GetTopSelectedSourceItem() { QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); return selectedItems.count() ? selectedItems[0].row() : -1; } QModelIndexList OBSBasic::GetAllSelectedSourceItems() { return ui->sources->selectionModel()->selectedIndexes(); } void OBSBasic::on_actionEditTransform_triggered() { const auto item = GetCurrentSceneItem(); if (!item) return; CreateEditTransformWindow(item); } void undo_redo(const std::string &data) { OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); OBSBasic::Get()->SetCurrentScene(source.Get(), true); obs_scene_load_transform_states(data.c_str()); } static void GetItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br) { matrix4 boxTransform; obs_sceneitem_get_box_transform(item, &boxTransform); vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f); vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f); auto GetMinPos = [&](float x, float y) { vec3 pos; vec3_set(&pos, x, y, 0.0f); vec3_transform(&pos, &pos, &boxTransform); vec3_min(&tl, &tl, &pos); vec3_max(&br, &br, &pos); }; GetMinPos(0.0f, 0.0f); GetMinPos(1.0f, 0.0f); GetMinPos(0.0f, 1.0f); GetMinPos(1.0f, 1.0f); } static vec3 GetItemTL(obs_sceneitem_t *item) { vec3 tl, br; GetItemBox(item, tl, br); return tl; } static void SetItemTL(obs_sceneitem_t *item, const vec3 &tl) { vec3 newTL; vec2 pos; obs_sceneitem_get_pos(item, &pos); newTL = GetItemTL(item); pos.x += tl.x - newTL.x; pos.y += tl.y - newTL.y; obs_sceneitem_set_pos(item, &pos); } static bool RotateSelectedSources(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) { if (obs_sceneitem_is_group(item)) obs_sceneitem_group_enum_items(item, RotateSelectedSources, param); if (!obs_sceneitem_selected(item)) return true; if (obs_sceneitem_locked(item)) return true; float rot = *static_cast(param); vec3 tl = GetItemTL(item); rot += obs_sceneitem_get_rot(item); if (rot >= 360.0f) rot -= 360.0f; else if (rot <= -360.0f) rot += 360.0f; obs_sceneitem_set_rot(item, rot); obs_sceneitem_force_update_transform(item); SetItemTL(item, tl); return true; }; void OBSBasic::on_actionRotate90CW_triggered() { float f90CW = 90.0f; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionRotate90CCW_triggered() { float f90CCW = -90.0f; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionRotate180_triggered() { float f180 = 180.0f; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } static bool MultiplySelectedItemScale(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) { vec2 &mul = *static_cast(param); if (obs_sceneitem_is_group(item)) obs_sceneitem_group_enum_items(item, MultiplySelectedItemScale, param); if (!obs_sceneitem_selected(item)) return true; if (obs_sceneitem_locked(item)) return true; vec3 tl = GetItemTL(item); vec2 scale; obs_sceneitem_get_scale(item, &scale); vec2_mul(&scale, &scale, &mul); obs_sceneitem_set_scale(item, &scale); obs_sceneitem_force_update_transform(item); SetItemTL(item, tl); return true; } void OBSBasic::on_actionFlipHorizontal_triggered() { vec2 scale; vec2_set(&scale, -1.0f, 1.0f); OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionFlipVertical_triggered() { vec2 scale; vec2_set(&scale, 1.0f, -1.0f); OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } static bool CenterAlignSelectedItems(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) { obs_bounds_type boundsType = *static_cast(param); if (obs_sceneitem_is_group(item)) obs_sceneitem_group_enum_items(item, CenterAlignSelectedItems, param); if (!obs_sceneitem_selected(item)) return true; if (obs_sceneitem_locked(item)) return true; obs_video_info ovi; obs_get_video_info(&ovi); obs_transform_info itemInfo; vec2_set(&itemInfo.pos, 0.0f, 0.0f); vec2_set(&itemInfo.scale, 1.0f, 1.0f); itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; itemInfo.rot = 0.0f; vec2_set(&itemInfo.bounds, float(ovi.base_width), float(ovi.base_height)); itemInfo.bounds_type = boundsType; itemInfo.bounds_alignment = OBS_ALIGN_CENTER; itemInfo.crop_to_bounds = obs_sceneitem_get_bounds_crop(item); obs_sceneitem_set_info2(item, &itemInfo); return true; } void OBSBasic::on_actionFitToScreen_triggered() { obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionStretchToScreen_triggered() { obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) { QModelIndexList selectedItems = GetAllSelectedSourceItems(); if (!selectedItems.count()) return; vector items; // Filter out items that have no size for (int x = 0; x < selectedItems.count(); x++) { OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); obs_transform_info oti; obs_sceneitem_get_info2(item, &oti); obs_source_t *source = obs_sceneitem_get_source(item); float width = float(obs_source_get_width(source)) * oti.scale.x; float height = float(obs_source_get_height(source)) * oti.scale.y; if (width == 0.0f || height == 0.0f) continue; items.emplace_back(item); } if (!items.size()) return; // Get center x, y coordinates of items vec3 center; float top = M_INFINITE; float left = M_INFINITE; float right = 0.0f; float bottom = 0.0f; for (auto &item : items) { vec3 tl, br; GetItemBox(item, tl, br); left = std::min(tl.x, left); top = std::min(tl.y, top); right = std::max(br.x, right); bottom = std::max(br.y, bottom); } center.x = (right + left) / 2.0f; center.y = (top + bottom) / 2.0f; center.z = 0.0f; // Get coordinates of screen center obs_video_info ovi; obs_get_video_info(&ovi); vec3 screenCenter; vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height), 0.0f); vec3_mulf(&screenCenter, &screenCenter, 0.5f); // Calculate difference between screen center and item center vec3 offset; vec3_sub(&offset, &screenCenter, ¢er); // Shift items by offset for (auto &item : items) { vec3 tl, br; GetItemBox(item, tl, br); vec3_add(&tl, &tl, &offset); vec3 itemTL = GetItemTL(item); if (centerType == CenterType::Vertical) tl.x = itemTL.x; else if (centerType == CenterType::Horizontal) tl.y = itemTL.y; SetItemTL(item, tl); } } void OBSBasic::on_actionCenterToScreen_triggered() { CenterType centerType = CenterType::Scene; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); CenterSelectedSceneItems(centerType); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionVerticalCenter_triggered() { CenterType centerType = CenterType::Vertical; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); CenterSelectedSceneItems(centerType); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_actionHorizontalCenter_triggered() { CenterType centerType = CenterType::Horizontal; OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); CenterSelectedSceneItems(centerType); OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); std::string undo_data(obs_data_get_json(wrapper)); std::string redo_data(obs_data_get_json(rwrapper)); undo_s.add_action( QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), undo_redo, undo_redo, undo_data, redo_data); } void OBSBasic::on_toggleSourceIcons_toggled(bool visible) { ui->sources->SetIconsVisible(visible); if (advAudioWindow != nullptr) advAudioWindow->SetIconsVisible(visible); config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); } void OBSBasic::on_sourcePropertiesButton_clicked() { on_actionSourceProperties_triggered(); } void OBSBasic::on_sourceFiltersButton_clicked() { OpenFilters(); } void OBSBasic::on_sourceInteractButton_clicked() { on_actionInteract_triggered(); }