diff --git a/obs/CMakeLists.txt b/obs/CMakeLists.txt
index 3180f0819..c98b9bce4 100644
--- a/obs/CMakeLists.txt
+++ b/obs/CMakeLists.txt
@@ -105,6 +105,7 @@ set(obs_SOURCES
window-basic-main-outputs.cpp
window-basic-source-select.cpp
window-basic-main-scene-collections.cpp
+ window-basic-main-transitions.cpp
window-basic-main-profiles.cpp
window-license-agreement.cpp
window-basic-status-bar.cpp
diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini
index 67793c074..014035d81 100644
--- a/obs/data/locale/en-US.ini
+++ b/obs/data/locale/en-US.ini
@@ -46,6 +46,22 @@ Enable="Enable"
DisableOSXVSync="Disable OSX V-Sync"
ResetOSXVSyncOnExit="Reset OSX V-Sync on Exit"
HighResourceUsage="Encoding overloaded! Consider turning down video settings or using a faster encoding preset."
+Transition="Transition"
+QuickTransitions="Quick Transitions"
+
+# quick transitions
+QuickTransitions.SwapScenes="Swap Preview/Output Scenes After Transitioning"
+QuickTransitions.SwapScenesTT="Swaps the preview and output scenes after transitioning (if the output's original scene still exists).\nThis will not undo any changes that may have been made to the output's original scene."
+QuickTransitions.DuplicateScene="Duplicate Scene"
+QuickTransitions.DuplicateSceneTT="When editing the same scene, allows editing transform/visibility of sources without modifying the output.\nTo edit properties of sources without modifying the output, enable 'Duplicate Sources'.\nChanging this value will reset the current output scene (if it still exists)."
+QuickTransitions.EditProperties="Duplicate Sources"
+QuickTransitions.EditPropertiesTT="When editing the same scene, allows editing properties of sources without modifying the output.\nThis can only be used if 'Duplicate Scene' is enabled.\nCertain sources (such as capture or media sources) do not support this and cannot be edited separately.\nChanging this value will reset the current output scene (if it still exists).\n\nWarning: Because sources will be duplicated, this may require extra system or video resources."
+QuickTransitions.HotkeyName="Quick Transition: %1"
+
+# transitions
+Basic.SceneTransitions="Scene Transitions"
+Basic.TransitionDuration="Duration"
+Basic.TogglePreviewProgramMode="Studio Mode"
# title bar strings
TitleBar.Profile="Profile"
diff --git a/obs/forms/OBSBasic.ui b/obs/forms/OBSBasic.ui
index 9449a8549..fb4608f43 100644
--- a/obs/forms/OBSBasic.ui
+++ b/obs/forms/OBSBasic.ui
@@ -46,26 +46,40 @@
-
-
-
-
- 0
- 0
-
+
+
+ 2
-
-
- 32
- 32
-
-
-
- Qt::ClickFocus
-
-
- Qt::CustomContextMenu
-
-
+
-
+
+
+ 4
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+ Qt::ClickFocus
+
+
+ Qt::CustomContextMenu
+
+
+
+
+
+
-
@@ -90,6 +104,103 @@
+
-
+
+
+ 2
+
+
-
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ Basic.Main.StartStreaming
+
+
+ false
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 130
+ 0
+
+
+
+ Basic.Main.StartRecording
+
+
+
+ -
+
+
+ Basic.TogglePreviewProgramMode
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Settings
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Exit
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 0
+ 0
+
+
+
+
+
+
-
@@ -118,7 +229,7 @@
0
0
- 242
+ 201
16
@@ -460,62 +571,117 @@
-
-
+
- 2
+ 4
-
-
-
- true
+
+
+ 2
-
- Basic.Main.StartStreaming
-
-
- false
-
-
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+ :/res/images/configuration21_16.png:/res/images/configuration21_16.png
+
+
+ true
+
+
+ configIconSmall
+
+
+
+ -
+
+
+
+ 120
+ 0
+
+
+
+
+
-
-
-
- true
+
+
+ 4
-
- Basic.Main.StartRecording
-
-
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Basic.TransitionDuration
+
+
+
+ -
+
+
+ ms
+
+
+ 2
+
+
+ 10000
+
+
+ 50
+
+
+ 300
+
+
+
+
-
-
-
- Settings
-
-
-
- -
-
-
- Exit
-
-
-
- -
-
+
Qt::Vertical
- 0
- 0
+ 20
+ 40
+ -
+
+
+ Basic.SceneTransitions
+
+
+
diff --git a/obs/obs-app.cpp b/obs/obs-app.cpp
index d0ab9902b..663c62a3f 100644
--- a/obs/obs-app.cpp
+++ b/obs/obs-app.cpp
@@ -333,6 +333,12 @@ bool OBSApp::InitGlobalConfigDefaults()
config_set_default_bool(globalConfig, "BasicWindow", "PreviewEnabled",
true);
+ config_set_default_bool(globalConfig, "BasicWindow",
+ "PreviewProgramMode", false);
+ config_set_default_bool(globalConfig, "BasicWindow",
+ "SceneDuplicationMode", true);
+ config_set_default_bool(globalConfig, "BasicWindow",
+ "SwapScenesMode", true);
#ifdef __APPLE__
config_set_default_bool(globalConfig, "Video", "DisableOSXVSync", true);
diff --git a/obs/window-basic-main-transitions.cpp b/obs/window-basic-main-transitions.cpp
new file mode 100644
index 000000000..bf49eb89f
--- /dev/null
+++ b/obs/window-basic-main-transitions.cpp
@@ -0,0 +1,867 @@
+/******************************************************************************
+ Copyright (C) 2016 by Hugh Bailey
+
+ 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
+#include
+#include
+#include
+#include "window-basic-main.hpp"
+#include "display-helpers.hpp"
+#include "menu-button.hpp"
+#include "qt-wrappers.hpp"
+
+using namespace std;
+
+Q_DECLARE_METATYPE(OBSScene);
+Q_DECLARE_METATYPE(OBSSource);
+Q_DECLARE_METATYPE(QuickTransition);
+
+static inline QString MakeQuickTransitionText(QuickTransition *qt)
+{
+ QString name = QT_UTF8(obs_source_get_name(qt->source));
+ if (!obs_transition_fixed(qt->source))
+ name += QString(" (%1ms)").arg(QString::number(qt->duration));
+ return name;
+}
+
+void OBSBasic::InitDefaultTransitions()
+{
+ std::vector transitions;
+ size_t idx = 0;
+ const char *id;
+
+ /* automatically add transitions that have no configuration (things
+ * such as cut/fade/etc) */
+ while (obs_enum_transition_types(idx++, &id)) {
+ if (!obs_is_source_configurable(id)) {
+ const char *name = obs_source_get_display_name(id);
+
+ obs_source_t *tr = obs_source_create_private(
+ id, name, NULL);
+ InitTransition(tr);
+ transitions.emplace_back(tr);
+
+ if (strcmp(id, "fade_transition") == 0)
+ fadeTransition = tr;
+
+ obs_source_release(tr);
+ }
+ }
+
+ for (OBSSource &tr : transitions) {
+ ui->transitions->addItem(QT_UTF8(obs_source_get_name(tr)),
+ QVariant::fromValue(OBSSource(tr)));
+ }
+}
+
+void OBSBasic::AddQuickTransitionHotkey(QuickTransition *qt)
+{
+ DStr hotkeyId;
+ QString hotkeyName;
+
+ dstr_printf(hotkeyId, "OBSBasic.QuickTransition.%d", qt->id);
+ hotkeyName = QTStr("QuickTransitions.HotkeyName")
+ .arg(MakeQuickTransitionText(qt));
+
+ auto quickTransition = [] (void *data, obs_hotkey_id, obs_hotkey_t*,
+ bool pressed)
+ {
+ int id = (int)(uintptr_t)data;
+ OBSBasic *main =
+ reinterpret_cast(App()->GetMainWindow());
+
+ if (pressed)
+ QMetaObject::invokeMethod(main,
+ "TriggerQuickTransition",
+ Qt::QueuedConnection,
+ Q_ARG(int, id));
+ };
+
+ qt->hotkey = obs_hotkey_register_frontend(hotkeyId->array,
+ QT_TO_UTF8(hotkeyName), quickTransition,
+ (void*)(uintptr_t)qt->id);
+}
+
+void OBSBasic::TriggerQuickTransition(int id)
+{
+ QuickTransition *qt = GetQuickTransition(id);
+
+ if (qt && previewProgramMode) {
+ OBSScene scene = GetCurrentScene();
+ obs_source_t *source = obs_scene_get_source(scene);
+
+ ui->transitionDuration->setValue(qt->duration);
+ if (GetCurrentTransition() != qt->source)
+ SetTransition(qt->source);
+
+ TransitionToScene(source);
+ }
+}
+
+void OBSBasic::RemoveQuickTransitionHotkey(QuickTransition *qt)
+{
+ obs_hotkey_unregister(qt->hotkey);
+}
+
+void OBSBasic::InitTransition(obs_source_t *transition)
+{
+ auto onTransitionStop = [] (void *data, calldata_t*) {
+ OBSBasic *window = (OBSBasic*)data;
+ QMetaObject::invokeMethod(window, "TransitionStopped",
+ Qt::QueuedConnection);
+ };
+
+ signal_handler_t *handler = obs_source_get_signal_handler(transition);
+ signal_handler_connect(handler, "transition_video_stop",
+ onTransitionStop, this);
+}
+
+static inline OBSSource GetTransitionComboItem(QComboBox *combo, int idx)
+{
+ return combo->itemData(idx).value();
+}
+
+void OBSBasic::CreateDefaultQuickTransitions()
+{
+ /* non-configurable transitions are always available, so add them
+ * to the "default quick transitions" list */
+ quickTransitions.emplace_back(
+ GetTransitionComboItem(ui->transitions, 0),
+ 300, quickTransitionIdCounter++);
+ quickTransitions.emplace_back(
+ GetTransitionComboItem(ui->transitions, 1),
+ 300, quickTransitionIdCounter++);
+}
+
+void OBSBasic::LoadQuickTransitions(obs_data_array_t *array)
+{
+ size_t count = obs_data_array_count(array);
+
+ quickTransitionIdCounter = 1;
+
+ for (size_t i = 0; i < count; i++) {
+ obs_data_t *data = obs_data_array_item(array, i);
+ obs_data_array_t *hotkeys = obs_data_get_array(data, "hotkeys");
+ const char *name = obs_data_get_string(data, "name");
+ int duration = obs_data_get_int(data, "duration");
+ int id = obs_data_get_int(data, "id");
+
+ if (id) {
+ obs_source_t *source = FindTransition(name);
+ quickTransitions.emplace_back(source, duration, id);
+
+ if (quickTransitionIdCounter <= id)
+ quickTransitionIdCounter = id + 1;
+
+ int idx = (int)quickTransitions.size() - 1;
+ AddQuickTransitionHotkey(&quickTransitions[idx]);
+ obs_hotkey_load(quickTransitions[idx].hotkey, hotkeys);
+ }
+
+ obs_data_release(data);
+ obs_data_array_release(hotkeys);
+ }
+}
+
+obs_data_array_t *OBSBasic::SaveQuickTransitions()
+{
+ obs_data_array_t *array = obs_data_array_create();
+
+ for (QuickTransition &qt : quickTransitions) {
+ obs_data_t *data = obs_data_create();
+ obs_data_array_t *hotkeys = obs_hotkey_save(qt.hotkey);
+
+ obs_data_set_string(data, "name",
+ obs_source_get_name(qt.source));
+ obs_data_set_int(data, "duration", qt.duration);
+ obs_data_set_array(data, "hotkeys", hotkeys);
+ obs_data_set_int(data, "id", qt.id);
+
+ obs_data_array_push_back(array, data);
+
+ obs_data_release(data);
+ obs_data_array_release(hotkeys);
+ }
+
+ return array;
+}
+
+obs_source_t *OBSBasic::FindTransition(const char *name)
+{
+ for (int i = 0; i < ui->transitions->count(); i++) {
+ OBSSource tr = ui->transitions->itemData(i)
+ .value();
+
+ const char *trName = obs_source_get_name(tr);
+ if (strcmp(trName, name) == 0)
+ return tr;
+ }
+
+ return nullptr;
+}
+
+void OBSBasic::TransitionToScene(obs_scene_t *scene, bool force)
+{
+ obs_source_t *source = obs_scene_get_source(scene);
+ TransitionToScene(source, force);
+}
+
+void OBSBasic::TransitionStopped()
+{
+ if (swapScenesMode) {
+ OBSSource scene = OBSGetStrongRef(swapScene);
+ if (scene)
+ SetCurrentScene(scene);
+ }
+
+ swapScene = nullptr;
+}
+
+void OBSBasic::TransitionToScene(obs_source_t *source, bool force)
+{
+ obs_scene_t *scene = obs_scene_from_source(source);
+ bool usingPreviewProgram = IsPreviewProgramMode();
+ if (!scene)
+ return;
+
+ OBSWeakSource lastProgramScene;
+
+ if (usingPreviewProgram) {
+ lastProgramScene = programScene;
+ programScene = OBSGetWeakRef(source);
+
+ if (swapScenesMode && !force) {
+ OBSSource newScene = OBSGetStrongRef(lastProgramScene);
+
+ if (!sceneDuplicationMode && newScene == source)
+ return;
+
+ if (newScene && newScene != GetCurrentSceneSource())
+ swapScene = lastProgramScene;
+ }
+ }
+
+ if (usingPreviewProgram && sceneDuplicationMode) {
+ scene = obs_scene_duplicate(scene, NULL,
+ editPropertiesMode ?
+ OBS_SCENE_DUP_PRIVATE_COPY :
+ OBS_SCENE_DUP_PRIVATE_REFS);
+ source = obs_scene_get_source(scene);
+ }
+
+ obs_source_t *transition = obs_get_output_source(0);
+
+ if (force)
+ obs_transition_set(transition, source);
+ else
+ obs_transition_start(transition, OBS_TRANSITION_MODE_AUTO,
+ ui->transitionDuration->value(), source);
+
+ if (usingPreviewProgram && sceneDuplicationMode)
+ obs_scene_release(scene);
+
+ obs_source_release(transition);
+}
+
+static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr)
+{
+ int idx = combo->findData(QVariant::fromValue(tr));
+ if (idx != -1) {
+ combo->blockSignals(true);
+ combo->setCurrentIndex(idx);
+ combo->blockSignals(false);
+ }
+}
+
+void OBSBasic::SetTransition(obs_source_t *transition)
+{
+ obs_source_t *oldTransition = obs_get_output_source(0);
+
+ if (oldTransition && transition) {
+ obs_transition_swap_begin(transition, oldTransition);
+ if (transition != GetCurrentTransition())
+ SetComboTransition(ui->transitions, transition);
+ obs_set_output_source(0, transition);
+ obs_transition_swap_end(transition, oldTransition);
+
+ bool showPropertiesButton = obs_source_configurable(transition);
+ ui->transitionProps->setVisible(showPropertiesButton);
+ } else {
+ obs_set_output_source(0, transition);
+ }
+
+ if (oldTransition)
+ obs_source_release(oldTransition);
+
+ bool fixed = transition ? obs_transition_fixed(transition) : false;
+ ui->transitionDurationLabel->setVisible(!fixed);
+ ui->transitionDuration->setVisible(!fixed);
+}
+
+OBSSource OBSBasic::GetCurrentTransition()
+{
+ return ui->transitions->currentData().value();
+}
+
+void OBSBasic::on_transitions_currentIndexChanged(int)
+{
+ OBSSource transition = GetCurrentTransition();
+ SetTransition(transition);
+}
+
+void OBSBasic::on_transitionProps_clicked()
+{
+ // TODO
+}
+
+QuickTransition *OBSBasic::GetQuickTransition(int id)
+{
+ for (QuickTransition &qt : quickTransitions) {
+ if (qt.id == id)
+ return &qt;
+ }
+
+ return nullptr;
+}
+
+int OBSBasic::GetQuickTransitionIdx(int id)
+{
+ for (int idx = 0; idx < (int)quickTransitions.size(); idx++) {
+ QuickTransition &qt = quickTransitions[idx];
+
+ if (qt.id == id)
+ return idx;
+ }
+
+ return -1;
+}
+
+void OBSBasic::SetCurrentScene(obs_scene_t *scene, bool force)
+{
+ obs_source_t *source = obs_scene_get_source(scene);
+ SetCurrentScene(source, force);
+}
+
+template
+static T GetOBSRef(QListWidgetItem *item)
+{
+ return item->data(static_cast(QtDataRole::OBSRef)).value();
+}
+
+void OBSBasic::SetCurrentScene(obs_source_t *scene, bool force)
+{
+ if (!IsPreviewProgramMode()) {
+ TransitionToScene(scene, force);
+
+ } else {
+ OBSSource actualLastScene = OBSGetStrongRef(lastScene);
+ if (actualLastScene != scene) {
+ if (scene)
+ obs_source_inc_showing(scene);
+ if (actualLastScene)
+ obs_source_dec_showing(actualLastScene);
+ lastScene = OBSGetWeakRef(scene);
+ }
+ }
+
+ if (obs_scene_get_source(GetCurrentScene()) != scene) {
+ for (int i = 0; i < ui->scenes->count(); i++) {
+ QListWidgetItem *item = ui->scenes->item(i);
+ OBSScene itemScene = GetOBSRef(item);
+ obs_source_t *source = obs_scene_get_source(itemScene);
+
+ if (source == scene) {
+ ui->scenes->blockSignals(true);
+ ui->scenes->setCurrentItem(item);
+ ui->scenes->blockSignals(false);
+ break;
+ }
+ }
+ }
+
+ UpdateSceneSelection(scene);
+}
+
+void OBSBasic::CreateProgramDisplay()
+{
+ program = new OBSQTDisplay();
+
+ auto displayResize = [this]() {
+ struct obs_video_info ovi;
+
+ if (obs_get_video_info(&ovi))
+ ResizeProgram(ovi.base_width, ovi.base_height);
+ };
+
+ connect(program, &OBSQTDisplay::DisplayResized,
+ displayResize);
+
+ auto addDisplay = [this] (OBSQTDisplay *window)
+ {
+ obs_display_add_draw_callback(window->GetDisplay(),
+ OBSBasic::RenderProgram, this);
+
+ struct obs_video_info ovi;
+ if (obs_get_video_info(&ovi))
+ ResizeProgram(ovi.base_width, ovi.base_height);
+ };
+
+ connect(program, &OBSQTDisplay::DisplayCreated, addDisplay);
+
+ program->setSizePolicy(QSizePolicy::Expanding,
+ QSizePolicy::Expanding);
+}
+
+void OBSBasic::TransitionClicked()
+{
+ if (previewProgramMode)
+ TransitionToScene(GetCurrentScene());
+}
+
+void OBSBasic::CreateProgramOptions()
+{
+ programOptions = new QWidget();
+ QVBoxLayout *layout = new QVBoxLayout();
+ layout->setSpacing(4);
+
+ QPushButton *configTransitions = new QPushButton();
+ configTransitions->setMaximumSize(22, 22);
+ configTransitions->setProperty("themeID", "configIconSmall");
+ configTransitions->setFlat(true);
+
+ QHBoxLayout *mainButtonLayout = new QHBoxLayout();
+ mainButtonLayout->setSpacing(2);
+
+ QPushButton *transitionButton = new QPushButton(QTStr("Transition"));
+ QHBoxLayout *quickTransitions = new QHBoxLayout();
+ quickTransitions->setSpacing(2);
+
+ QPushButton *addQuickTransition = new QPushButton();
+ addQuickTransition->setMaximumSize(22, 22);
+ addQuickTransition->setProperty("themeID", "addIconSmall");
+ addQuickTransition->setFlat(true);
+
+ QLabel *quickTransitionsLabel = new QLabel(QTStr("QuickTransitions"));
+
+ quickTransitions->addWidget(quickTransitionsLabel);
+ quickTransitions->addWidget(addQuickTransition);
+
+ mainButtonLayout->addWidget(transitionButton);
+ mainButtonLayout->addWidget(configTransitions);
+
+ layout->addStretch(0);
+ layout->addLayout(mainButtonLayout);
+ layout->addLayout(quickTransitions);
+ layout->addStretch(0);
+
+ programOptions->setLayout(layout);
+
+ auto onAdd = [this] () {
+ QPointer menu = CreateTransitionMenu(this, nullptr);
+ menu->exec(QCursor::pos());
+ };
+
+ auto onConfig = [this] () {
+ QMenu menu(this);
+ QAction *action;
+
+ auto toggleEditProperties = [this] () {
+ editPropertiesMode = !editPropertiesMode;
+
+ OBSSource actualScene = OBSGetStrongRef(programScene);
+ if (actualScene)
+ TransitionToScene(actualScene, true);
+ };
+
+ auto toggleSwapScenesMode = [this] () {
+ swapScenesMode = !swapScenesMode;
+ };
+
+ auto toggleSceneDuplication = [this] () {
+ sceneDuplicationMode = !sceneDuplicationMode;
+
+ OBSSource actualScene = OBSGetStrongRef(programScene);
+ if (actualScene)
+ TransitionToScene(actualScene, true);
+ };
+
+ auto showToolTip = [&] () {
+ QAction *act = menu.activeAction();
+ QToolTip::showText(QCursor::pos(), act->toolTip(),
+ &menu, menu.actionGeometry(act));
+ };
+
+ action = menu.addAction(QTStr("QuickTransitions.DuplicateScene"));
+ action->setToolTip(QTStr("QuickTransitions.DuplicateSceneTT"));
+ action->setCheckable(true);
+ action->setChecked(sceneDuplicationMode);
+ connect(action, &QAction::triggered, toggleSceneDuplication);
+ connect(action, &QAction::hovered, showToolTip);
+
+ action = menu.addAction(QTStr("QuickTransitions.EditProperties"));
+ action->setToolTip(QTStr("QuickTransitions.EditPropertiesTT"));
+ action->setCheckable(true);
+ action->setChecked(editPropertiesMode);
+ action->setEnabled(sceneDuplicationMode);
+ connect(action, &QAction::triggered, toggleEditProperties);
+ connect(action, &QAction::hovered, showToolTip);
+
+ action = menu.addAction(QTStr("QuickTransitions.SwapScenes"));
+ action->setToolTip(QTStr("QuickTransitions.SwapScenesTT"));
+ action->setCheckable(true);
+ action->setChecked(swapScenesMode);
+ connect(action, &QAction::triggered, toggleSwapScenesMode);
+ connect(action, &QAction::hovered, showToolTip);
+
+ menu.exec(QCursor::pos());
+ };
+
+ connect(transitionButton, &QAbstractButton::clicked,
+ this, &OBSBasic::TransitionClicked);
+ connect(addQuickTransition, &QAbstractButton::clicked, onAdd);
+ connect(configTransitions, &QAbstractButton::clicked, onConfig);
+}
+
+void OBSBasic::on_modeSwitch_clicked()
+{
+ SetPreviewProgramMode(!IsPreviewProgramMode());
+}
+
+static inline void ResetQuickTransitionText(QuickTransition *qt)
+{
+ qt->button->setText(MakeQuickTransitionText(qt));
+}
+
+QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt)
+{
+ QMenu *menu = new QMenu(parent);
+ QAction *action;
+
+ if (qt) {
+ action = menu->addAction(QTStr("Remove"));
+ action->setProperty("id", qt->id);
+ connect(action, &QAction::triggered,
+ this, &OBSBasic::QuickTransitionRemoveClicked);
+
+ menu->addSeparator();
+ }
+
+ QSpinBox *duration = new QSpinBox(menu);
+ if (qt)
+ duration->setProperty("id", qt->id);
+ duration->setMinimum(50);
+ duration->setSuffix("ms");
+ duration->setMaximum(20000);
+ duration->setSingleStep(50);
+ duration->setValue(qt ? qt->duration : 300);
+
+ if (qt) {
+ connect(duration, (void (QSpinBox::*)(int))&QSpinBox::valueChanged,
+ this, &OBSBasic::QuickTransitionChangeDuration);
+ }
+
+ for (int i = 0; i < ui->transitions->count(); i++) {
+ OBSSource tr = GetTransitionComboItem(ui->transitions, i);
+
+ action = menu->addAction(obs_source_get_name(tr));
+ action->setProperty("transition_index", i);
+
+ if (qt) {
+ action->setProperty("id", qt->id);
+ connect(action, &QAction::triggered, this,
+ &OBSBasic::QuickTransitionChange);
+ } else {
+ action->setProperty("duration",
+ QVariant::fromValue(duration));
+ connect(action, &QAction::triggered, this,
+ &OBSBasic::AddQuickTransition);
+ }
+ }
+
+ QWidgetAction *durationAction = new QWidgetAction(menu);
+ durationAction->setDefaultWidget(duration);
+
+ menu->addSeparator();
+ menu->addAction(durationAction);
+ return menu;
+}
+
+void OBSBasic::AddQuickTransitionId(int id)
+{
+ QuickTransition *qt = GetQuickTransition(id);
+ if (!qt)
+ return;
+
+ /* --------------------------------- */
+
+ QPushButton *button = new MenuButton();
+ button->setProperty("id", id);
+
+ qt->button = button;
+ ResetQuickTransitionText(qt);
+
+ /* --------------------------------- */
+
+ QMenu *buttonMenu = CreateTransitionMenu(button, qt);
+
+ /* --------------------------------- */
+
+ button->setMenu(buttonMenu);
+ connect(button, &QAbstractButton::clicked,
+ this, &OBSBasic::QuickTransitionClicked);
+
+ QVBoxLayout *programLayout =
+ reinterpret_cast(programOptions->layout());
+
+ int idx = 3;
+ for (;; idx++) {
+ QLayoutItem *item = programLayout->itemAt(idx);
+ if (!item)
+ break;
+
+ QWidget *widget = item->widget();
+ if (!widget || !widget->property("id").isValid())
+ break;
+ }
+
+ programLayout->insertWidget(idx, button);
+}
+
+void OBSBasic::AddQuickTransition()
+{
+ int trIdx = sender()->property("transition_index").toInt();
+ QSpinBox *duration = sender()->property("duration").value();
+ OBSSource transition = GetTransitionComboItem(ui->transitions, trIdx);
+ int id = quickTransitionIdCounter++;
+
+ quickTransitions.emplace_back(transition, duration->value(), id);
+ AddQuickTransitionId(id);
+
+ int idx = (int)quickTransitions.size() - 1;
+ AddQuickTransitionHotkey(&quickTransitions[idx]);
+}
+
+void OBSBasic::ClearQuickTransitions()
+{
+ for (QuickTransition &qt : quickTransitions)
+ RemoveQuickTransitionHotkey(&qt);
+ quickTransitions.clear();
+
+ if (!programOptions)
+ return;
+
+ QVBoxLayout *programLayout =
+ reinterpret_cast(programOptions->layout());
+
+ for (int idx = 0;; idx++) {
+ QLayoutItem *item = programLayout->itemAt(idx);
+ if (!item)
+ break;
+
+ QWidget *widget = item->widget();
+ if (!widget)
+ continue;
+
+ int id = widget->property("id").toInt();
+ if (id != 0) {
+ delete widget;
+ idx--;
+ }
+ }
+}
+
+void OBSBasic::QuickTransitionClicked()
+{
+ int id = sender()->property("id").toInt();
+ TriggerQuickTransition(id);
+}
+
+void OBSBasic::QuickTransitionChange()
+{
+ int id = sender()->property("id").toInt();
+ int trIdx = sender()->property("transition_index").toInt();
+ QuickTransition *qt = GetQuickTransition(id);
+
+ if (qt) {
+ qt->source = GetTransitionComboItem(ui->transitions, trIdx);
+ ResetQuickTransitionText(qt);
+ }
+}
+
+void OBSBasic::QuickTransitionChangeDuration(int value)
+{
+ int id = sender()->property("id").toInt();
+ QuickTransition *qt = GetQuickTransition(id);
+
+ if (qt) {
+ qt->duration = value;
+ ResetQuickTransitionText(qt);
+ }
+}
+
+void OBSBasic::QuickTransitionRemoveClicked()
+{
+ int id = sender()->property("id").toInt();
+ int idx = GetQuickTransitionIdx(id);
+ if (idx == -1)
+ return;
+
+ QuickTransition &qt = quickTransitions[idx];
+
+ if (qt.button)
+ qt.button->deleteLater();
+
+ RemoveQuickTransitionHotkey(&qt);
+ quickTransitions.erase(quickTransitions.begin() + idx);
+}
+
+void OBSBasic::RefreshQuickTransitions()
+{
+ if (!IsPreviewProgramMode())
+ return;
+
+ for (QuickTransition &qt : quickTransitions)
+ AddQuickTransitionId(qt.id);
+}
+
+void OBSBasic::SetPreviewProgramMode(bool enabled)
+{
+ if (IsPreviewProgramMode() == enabled)
+ return;
+
+ ui->modeSwitch->setChecked(enabled);
+ os_atomic_set_bool(&previewProgramMode, enabled);
+
+ if (IsPreviewProgramMode()) {
+ if (!previewEnabled)
+ EnablePreviewDisplay(true);
+
+ CreateProgramDisplay();
+ CreateProgramOptions();
+
+ OBSScene curScene = GetCurrentScene();
+
+ obs_scene_t *dup = obs_scene_duplicate(curScene, nullptr,
+ editPropertiesMode ?
+ OBS_SCENE_DUP_PRIVATE_COPY :
+ OBS_SCENE_DUP_PRIVATE_REFS);
+
+ obs_source_t *transition = obs_get_output_source(0);
+ obs_source_t *dup_source = obs_scene_get_source(dup);
+ obs_transition_set(transition, dup_source);
+ obs_source_release(transition);
+ obs_scene_release(dup);
+
+ if (curScene) {
+ obs_source_t *source = obs_scene_get_source(curScene);
+ obs_source_inc_showing(source);
+ lastScene = OBSGetWeakRef(source);
+ programScene = OBSGetWeakRef(source);
+ }
+
+ RefreshQuickTransitions();
+
+ ui->previewLayout->addWidget(programOptions);
+ ui->previewLayout->addWidget(program);
+ program->show();
+
+ blog(LOG_INFO, "Switched to Preview/Program mode");
+ blog(LOG_INFO, "-----------------------------"
+ "-------------------");
+ } else {
+ OBSSource actualProgramScene = OBSGetStrongRef(programScene);
+ if (!actualProgramScene)
+ actualProgramScene = GetCurrentSceneSource();
+ else
+ SetCurrentScene(actualProgramScene);
+ TransitionToScene(actualProgramScene, true);
+
+ delete programOptions;
+ delete program;
+
+ if (lastScene) {
+ OBSSource actualLastScene = OBSGetStrongRef(lastScene);
+ if (actualLastScene)
+ obs_source_dec_showing(actualLastScene);
+ lastScene = nullptr;
+ }
+
+ programScene = nullptr;
+ swapScene = nullptr;
+
+ for (QuickTransition &qt : quickTransitions)
+ qt.button = nullptr;
+
+ if (!previewEnabled)
+ EnablePreviewDisplay(false);
+
+ blog(LOG_INFO, "Switched to regular Preview mode");
+ blog(LOG_INFO, "-----------------------------"
+ "-------------------");
+ }
+
+ UpdateTitleBar();
+}
+
+void OBSBasic::RenderProgram(void *data, uint32_t cx, uint32_t cy)
+{
+ OBSBasic *window = static_cast(data);
+ obs_video_info ovi;
+
+ obs_get_video_info(&ovi);
+
+ window->programCX = int(window->programScale * float(ovi.base_width));
+ window->programCY = int(window->programScale * float(ovi.base_height));
+
+ gs_viewport_push();
+ gs_projection_push();
+
+ /* --------------------------------------- */
+
+ gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height),
+ -100.0f, 100.0f);
+ gs_set_viewport(window->programX, window->programY,
+ window->programCX, window->programCY);
+
+ window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
+
+ obs_render_main_view();
+ gs_load_vertexbuffer(nullptr);
+
+ /* --------------------------------------- */
+
+ gs_projection_pop();
+ gs_viewport_pop();
+
+ UNUSED_PARAMETER(cx);
+ UNUSED_PARAMETER(cy);
+}
+
+void OBSBasic::ResizeProgram(uint32_t cx, uint32_t cy)
+{
+ QSize targetSize;
+
+ /* resize program panel to fix to the top section of the window */
+ targetSize = GetPixelSize(program);
+ GetScaleAndCenterPos(int(cx), int(cy),
+ targetSize.width() - PREVIEW_EDGE_SIZE * 2,
+ targetSize.height() - PREVIEW_EDGE_SIZE * 2,
+ programX, programY, programScale);
+
+ programX += float(PREVIEW_EDGE_SIZE);
+ programY += float(PREVIEW_EDGE_SIZE);
+}
diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp
index 7a77adabc..5d9f213b5 100644
--- a/obs/window-basic-main.cpp
+++ b/obs/window-basic-main.cpp
@@ -56,8 +56,6 @@
#include
#include
-#define PREVIEW_EDGE_SIZE 10
-
using namespace std;
namespace {
@@ -247,7 +245,9 @@ static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent,
obs_source_release(source);
}
-static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder)
+static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
+ obs_data_array_t *quickTransitionData, int transitionDuration,
+ OBSScene &scene, OBSSource &curProgramScene)
{
obs_data_t *saveData = obs_data_create();
@@ -273,18 +273,26 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder)
return (*static_cast(data))(source);
}, static_cast(&FilterAudioSources));
- obs_source_t *currentScene = obs_get_output_source(0);
+ obs_source_t *transition = obs_get_output_source(0);
+ obs_source_t *currentScene = obs_scene_get_source(scene);
const char *sceneName = obs_source_get_name(currentScene);
+ const char *programName = obs_source_get_name(curProgramScene);
const char *sceneCollection = config_get_string(App()->GlobalConfig(),
"Basic", "SceneCollection");
obs_data_set_string(saveData, "current_scene", sceneName);
+ obs_data_set_string(saveData, "current_program_scene", programName);
obs_data_set_array(saveData, "scene_order", sceneOrder);
obs_data_set_string(saveData, "name", sceneCollection);
obs_data_set_array(saveData, "sources", sourcesArray);
+ obs_data_set_array(saveData, "quick_transitions", quickTransitionData);
obs_data_array_release(sourcesArray);
- obs_source_release(currentScene);
+
+ obs_data_set_string(saveData, "current_transition",
+ obs_source_get_name(transition));
+ obs_data_set_int(saveData, "transition_duration", transitionDuration);
+ obs_source_release(transition);
return saveData;
}
@@ -338,14 +346,23 @@ obs_data_array_t *OBSBasic::SaveSceneListOrder()
void OBSBasic::Save(const char *file)
{
+ OBSScene scene = GetCurrentScene();
+ OBSSource curProgramScene = OBSGetStrongRef(programScene);
+ if (!curProgramScene)
+ curProgramScene = obs_scene_get_source(scene);
+
obs_data_array_t *sceneOrder = SaveSceneListOrder();
- obs_data_t *saveData = GenerateSaveData(sceneOrder);
+ obs_data_array_t *quickTrData = SaveQuickTransitions();
+ obs_data_t *saveData = GenerateSaveData(sceneOrder, quickTrData,
+ ui->transitionDuration->value(),
+ scene, curProgramScene);
if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
blog(LOG_ERROR, "Could not save scene data to %s", file);
obs_data_release(saveData);
obs_data_array_release(sceneOrder);
+ obs_data_array_release(quickTrData);
}
static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
@@ -399,14 +416,18 @@ void OBSBasic::CreateDefaultScene(bool firstStart)
disableSaving++;
ClearSceneData();
+ InitDefaultTransitions();
+ CreateDefaultQuickTransitions();
+ ui->transitionDuration->setValue(300);
+ SetTransition(fadeTransition);
obs_scene_t *scene = obs_scene_create(Str("Basic.Scene"));
if (firstStart)
CreateFirstRunSources();
- obs_set_output_source(0, obs_scene_get_source(scene));
AddScene(obs_scene_get_source(scene));
+ SetCurrentScene(scene, true);
obs_scene_release(scene);
disableSaving--;
@@ -463,11 +484,23 @@ void OBSBasic::Load(const char *file)
}
ClearSceneData();
+ InitDefaultTransitions();
obs_data_array_t *sceneOrder = obs_data_get_array(data, "scene_order");
obs_data_array_t *sources = obs_data_get_array(data, "sources");
const char *sceneName = obs_data_get_string(data,
"current_scene");
+ const char *programSceneName = obs_data_get_string(data,
+ "current_program_scene");
+ const char *transitionName = obs_data_get_string(data,
+ "current_transition");
+
+ int newDuration = obs_data_get_int(data, "transition_duration");
+ if (!newDuration)
+ newDuration = 300;
+
+ if (!transitionName)
+ transitionName = obs_source_get_name(fadeTransition);
const char *curSceneCollection = config_get_string(
App()->GlobalConfig(), "Basic", "SceneCollection");
@@ -476,6 +509,8 @@ void OBSBasic::Load(const char *file)
const char *name = obs_data_get_string(data, "name");
obs_source_t *curScene;
+ obs_source_t *curProgramScene;
+ obs_source_t *curTransition;
if (!name || !*name)
name = curSceneCollection;
@@ -491,9 +526,25 @@ void OBSBasic::Load(const char *file)
if (sceneOrder)
LoadSceneListOrder(sceneOrder);
+ curTransition = FindTransition(transitionName);
+ if (!curTransition)
+ curTransition = fadeTransition;
+
+ ui->transitionDuration->setValue(newDuration);
+ SetTransition(curTransition);
+
curScene = obs_get_source_by_name(sceneName);
- obs_set_output_source(0, curScene);
+ curProgramScene = obs_get_source_by_name(programSceneName);
+ if (!curProgramScene) {
+ curProgramScene = curScene;
+ obs_source_addref(curScene);
+ }
+
+ SetCurrentScene(curScene, true);
+ if (IsPreviewProgramMode())
+ TransitionToScene(curProgramScene, true);
obs_source_release(curScene);
+ obs_source_release(curProgramScene);
obs_data_array_release(sources);
obs_data_array_release(sceneOrder);
@@ -506,6 +557,13 @@ void OBSBasic::Load(const char *file)
config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile",
file_base.c_str());
+ obs_data_array_t *quickTransitionData = obs_data_get_array(data,
+ "quick_transitions");
+ LoadQuickTransitions(quickTransitionData);
+ obs_data_array_release(quickTransitionData);
+
+ RefreshQuickTransitions();
+
obs_data_release(data);
disableSaving--;
@@ -772,8 +830,6 @@ void OBSBasic::InitOBSCallbacks()
OBSBasic::SourceLoaded, this);
signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove",
OBSBasic::SourceRemoved, this);
- signalHandlers.emplace_back(obs_get_signal_handler(), "channel_change",
- OBSBasic::ChannelChanged, this);
signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate",
OBSBasic::SourceActivated, this);
signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate",
@@ -885,6 +941,15 @@ void OBSBasic::OBSInit()
InitPrimitives();
+ sceneDuplicationMode = config_get_bool(App()->GlobalConfig(),
+ "BasicWindow", "SceneDuplicationMode");
+ swapScenesMode = config_get_bool(App()->GlobalConfig(),
+ "BasicWindow", "SwapScenesMode");
+ editPropertiesMode = config_get_bool(App()->GlobalConfig(),
+ "BasicWindow", "EditPropertiesMode");
+ SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
+ "BasicWindow", "PreviewProgramMode"));
+
{
ProfileScope("OBSBasic::Load");
disableSaving--;
@@ -895,11 +960,13 @@ void OBSBasic::OBSInit()
TimedCheckForUpdates();
loaded = true;
- bool previewEnabled = config_get_bool(App()->GlobalConfig(),
+ previewEnabled = config_get_bool(App()->GlobalConfig(),
"BasicWindow", "PreviewEnabled");
- if (!previewEnabled)
- QMetaObject::invokeMethod(this, "TogglePreview",
- Qt::QueuedConnection);
+
+ if (!previewEnabled && !IsPreviewProgramMode())
+ QMetaObject::invokeMethod(this, "EnablePreviewDisplay",
+ Qt::QueuedConnection,
+ Q_ARG(bool, previewEnabled));
#ifdef _WIN32
uint32_t winVer = GetWindowsVersion();
@@ -1121,8 +1188,36 @@ void OBSBasic::CreateHotkeys()
this, this);
LoadHotkeyPair(recordingHotkeys,
"OBSBasic.StartRecording", "OBSBasic.StopRecording");
-
#undef MAKE_CALLBACK
+
+ auto togglePreviewProgram = [] (void *data, obs_hotkey_id,
+ obs_hotkey_t*, bool pressed)
+ {
+ if (pressed)
+ QMetaObject::invokeMethod(static_cast(data),
+ "on_modeSwitch_clicked",
+ Qt::QueuedConnection);
+ };
+
+ togglePreviewProgramHotkey = obs_hotkey_register_frontend(
+ "OBSBasic.TogglePreviewProgram",
+ Str("Basic.TogglePreviewProgramMode"),
+ togglePreviewProgram, this);
+ LoadHotkey(togglePreviewProgramHotkey, "OBSBasic.TogglePreviewProgram");
+
+ auto transition = [] (void *data, obs_hotkey_id, obs_hotkey_t*,
+ bool pressed)
+ {
+ if (pressed)
+ QMetaObject::invokeMethod(static_cast(data),
+ "TransitionClicked",
+ Qt::QueuedConnection);
+ };
+
+ transitionHotkey = obs_hotkey_register_frontend(
+ "OBSBasic.Transition",
+ Str("Transition"), transition, this);
+ LoadHotkey(transitionHotkey, "OBSBasic.Transition");
}
void OBSBasic::ClearHotkeys()
@@ -1130,11 +1225,14 @@ void OBSBasic::ClearHotkeys()
obs_hotkey_pair_unregister(streamingHotkeys);
obs_hotkey_pair_unregister(recordingHotkeys);
obs_hotkey_unregister(forceStreamingStopHotkey);
+ obs_hotkey_unregister(togglePreviewProgramHotkey);
+ obs_hotkey_unregister(transitionHotkey);
}
OBSBasic::~OBSBasic()
{
- bool previewEnabled = obs_display_enabled(ui->preview->GetDisplay());
+ delete programOptions;
+ delete program;
/* XXX: any obs data must be released before calling obs_shutdown.
* currently, we can't automate this with C++ RAII because of the
@@ -1206,6 +1304,14 @@ OBSBasic::~OBSBasic()
previewEnabled);
config_set_bool(App()->GlobalConfig(), "BasicWindow", "AlwaysOnTop",
alwaysOnTop);
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "SceneDuplicationMode", sceneDuplicationMode);
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "SwapScenesMode", swapScenesMode);
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "EditPropertiesMode", editPropertiesMode);
+ config_set_bool(App()->GlobalConfig(), "BasicWindow",
+ "PreviewProgramMode", IsPreviewProgramMode());
config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
#ifdef _WIN32
@@ -1358,10 +1464,13 @@ void OBSBasic::AddScene(OBSSource source)
[](void *data,
obs_hotkey_id, obs_hotkey_t*, bool pressed)
{
+ OBSBasic *main =
+ reinterpret_cast(App()->GetMainWindow());
+
auto potential_source = static_cast(data);
auto source = obs_source_get_ref(potential_source);
if (source && pressed)
- obs_set_output_source(0, source);
+ main->SetCurrentScene(source);
obs_source_release(source);
}, static_cast(source));
@@ -1771,10 +1880,10 @@ void OBSBasic::DuplicateSelectedScene()
obs_scene_t *scene = obs_scene_duplicate(curScene,
name.c_str(), OBS_SCENE_DUP_REFS);
source = obs_scene_get_source(scene);
+ AddScene(source);
+ SetCurrentScene(source, true);
obs_scene_release(scene);
-
- obs_set_output_source(0, source);
- return;
+ break;
}
}
@@ -1967,17 +2076,6 @@ void OBSBasic::SourceRenamed(void *data, calldata_t *params)
Q_ARG(QString, QT_UTF8(prevName)));
}
-void OBSBasic::ChannelChanged(void *data, calldata_t *params)
-{
- obs_source_t *source = (obs_source_t*)calldata_ptr(params, "source");
- uint32_t channel = (uint32_t)calldata_int(params, "channel");
-
- if (channel == 0)
- QMetaObject::invokeMethod(static_cast(data),
- "UpdateSceneSelection",
- Q_ARG(OBSSource, OBSSource(source)));
-}
-
void OBSBasic::DrawBackdrop(float cx, float cy)
{
if (!box)
@@ -2029,7 +2127,14 @@ void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy)
window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
- obs_render_main_view();
+ if (window->IsPreviewProgramMode()) {
+ OBSScene scene = window->GetCurrentScene();
+ obs_source_t *source = obs_scene_get_source(scene);
+ if (source)
+ obs_source_video_render(source);
+ } else {
+ obs_render_main_view();
+ }
gs_load_vertexbuffer(nullptr);
/* --------------------------------------- */
@@ -2267,6 +2372,8 @@ void OBSBasic::ClearSceneData()
ClearVolumeControls();
ClearListItems(ui->scenes);
ClearListItems(ui->sources);
+ ClearQuickTransitions();
+ ui->transitions->clear();
obs_set_output_source(0, nullptr);
obs_set_output_source(1, nullptr);
@@ -2274,6 +2381,9 @@ void OBSBasic::ClearSceneData()
obs_set_output_source(3, nullptr);
obs_set_output_source(4, nullptr);
obs_set_output_source(5, nullptr);
+ lastScene = nullptr;
+ swapScene = nullptr;
+ programScene = nullptr;
auto cb = [](void *unused, obs_source_t *source)
{
@@ -2393,8 +2503,7 @@ void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current,
source = obs_scene_get_source(scene);
}
- /* TODO: allow transitions */
- obs_set_output_source(0, source);
+ SetCurrentScene(source);
UNUSED_PARAMETER(prev);
}
@@ -2519,7 +2628,7 @@ void OBSBasic::on_actionAddScene_triggered()
obs_scene_t *scene = obs_scene_create(name.c_str());
source = obs_scene_get_source(scene);
AddScene(source);
- obs_set_output_source(0, source);
+ SetCurrentScene(source);
obs_scene_release(scene);
}
}
@@ -2627,6 +2736,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
action->setCheckable(true);
action->setChecked(
obs_display_enabled(ui->preview->GetDisplay()));
+ if (IsPreviewProgramMode())
+ action->setEnabled(false);
previewProjector = new QMenu(QTStr("PreviewProjector"));
AddProjectorMenuMonitors(previewProjector, this,
@@ -3694,12 +3805,17 @@ void OBSBasic::on_actionCenterToScreen_triggered()
obs_scene_enum_items(GetCurrentScene(), func, nullptr);
}
+void OBSBasic::EnablePreviewDisplay(bool enable)
+{
+ obs_display_set_enabled(ui->preview->GetDisplay(), enable);
+ ui->preview->setVisible(enable);
+ ui->previewDisabledLabel->setVisible(!enable);
+}
+
void OBSBasic::TogglePreview()
{
- bool enabled = !obs_display_enabled(ui->preview->GetDisplay());
- obs_display_set_enabled(ui->preview->GetDisplay(), enabled);
- ui->preview->setVisible(enabled);
- ui->previewDisabledLabel->setVisible(!enabled);
+ previewEnabled = !previewEnabled;
+ EnablePreviewDisplay(previewEnabled);
}
void OBSBasic::Nudge(int dist, MoveDir dir)
@@ -3791,7 +3907,11 @@ void OBSBasic::UpdateTitleBar()
const char *sceneCollection = config_get_string(App()->GlobalConfig(),
"Basic", "SceneCollection");
- name << "OBS " << App()->GetVersionString();
+ name << "OBS ";
+ if (previewProgramMode)
+ name << "Studio ";
+
+ name << App()->GetVersionString();
name << " - " << Str("TitleBar.Profile") << ": " << profile;
name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection;
diff --git a/obs/window-basic-main.hpp b/obs/window-basic-main.hpp
index 0663d4f29..8dc646864 100644
--- a/obs/window-basic-main.hpp
+++ b/obs/window-basic-main.hpp
@@ -30,6 +30,7 @@
#include "window-basic-filters.hpp"
#include
+#include
#include
#include
@@ -49,6 +50,8 @@ class QNetworkReply;
#define SIMPLE_ENCODER_X264 "x264"
#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu"
+#define PREVIEW_EDGE_SIZE 10
+
struct BasicOutputHandler;
enum class QtDataRole {
@@ -56,6 +59,21 @@ enum class QtDataRole {
OBSSignals,
};
+struct QuickTransition {
+ QPushButton *button = nullptr;
+ OBSSource source;
+ obs_hotkey_id hotkey = 0;
+ int duration = 0;
+ int id = 0;
+
+ inline QuickTransition() {}
+ inline QuickTransition(OBSSource source_, int duration_, int id_)
+ : source (source_),
+ duration (duration_),
+ id (id_)
+ {}
+};
+
class OBSBasic : public OBSMainWindow {
Q_OBJECT
@@ -77,6 +95,7 @@ private:
bool loaded = false;
long disableSaving = 1;
bool projectChanged = false;
+ bool previewEnabled = true;
QPointer updateCheckThread;
QPointer logUploadThread;
@@ -194,6 +213,65 @@ private:
obs_hotkey_pair_id streamingHotkeys, recordingHotkeys;
obs_hotkey_id forceStreamingStopHotkey;
+ void InitDefaultTransitions();
+ void InitTransition(obs_source_t *transition);
+ void TransitionToScene(obs_scene_t *scene, bool force = false);
+ void TransitionToScene(obs_source_t *scene, bool force = false);
+ obs_source_t *FindTransition(const char *name);
+ void SetTransition(obs_source_t *transition);
+ OBSSource GetCurrentTransition();
+
+ obs_source_t *fadeTransition;
+
+ void CreateProgramDisplay();
+ void CreateProgramOptions();
+ void AddQuickTransitionId(int id);
+ void AddQuickTransition();
+ void AddQuickTransitionHotkey(QuickTransition *qt);
+ void RemoveQuickTransitionHotkey(QuickTransition *qt);
+ void LoadQuickTransitions(obs_data_array_t *array);
+ obs_data_array_t *SaveQuickTransitions();
+ void RefreshQuickTransitions();
+ void CreateDefaultQuickTransitions();
+
+ QuickTransition *GetQuickTransition(int id);
+ int GetQuickTransitionIdx(int id);
+ QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt);
+ void ClearQuickTransitions();
+ void QuickTransitionClicked();
+ void QuickTransitionChange();
+ void QuickTransitionChangeDuration(int value);
+ void QuickTransitionRemoveClicked();
+
+ void SetPreviewProgramMode(bool enabled);
+ void ResizeProgram(uint32_t cx, uint32_t cy);
+ void SetCurrentScene(obs_scene_t *scene, bool force = false);
+ void SetCurrentScene(obs_source_t *scene, bool force = false);
+ static void RenderProgram(void *data, uint32_t cx, uint32_t cy);
+
+ std::vector quickTransitions;
+ QPointer programOptions;
+ QPointer program;
+ OBSWeakSource lastScene;
+ OBSWeakSource swapScene;
+ OBSWeakSource programScene;
+ bool editPropertiesMode = false;
+ bool sceneDuplicationMode = true;
+ bool swapScenesMode = true;
+ volatile bool previewProgramMode = false;
+ obs_hotkey_id togglePreviewProgramHotkey = 0;
+ obs_hotkey_id transitionHotkey = 0;
+ int quickTransitionIdCounter = 1;
+
+ int programX = 0, programY = 0;
+ int programCX = 0, programCY = 0;
+ float programScale = 0.0f;
+
+ inline bool IsPreviewProgramMode() const
+ {
+ return os_atomic_load_bool(&previewProgramMode);
+ }
+
public slots:
void StartStreaming();
void StopStreaming();
@@ -219,7 +297,6 @@ private slots:
void RemoveSceneItem(OBSSceneItem item);
void AddScene(OBSSource source);
void RemoveScene(OBSSource source);
- void UpdateSceneSelection(OBSSource source);
void RenameSources(QString newName, QString prevName);
void SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select);
@@ -237,6 +314,10 @@ private slots:
void ProcessHotkey(obs_hotkey_id id, bool pressed);
+ void TransitionClicked();
+ void TransitionStopped();
+ void TriggerQuickTransition(int id);
+
private:
/* OBS Callbacks */
static void SceneReordered(void *data, calldata_t *params);
@@ -249,7 +330,6 @@ private:
static void SourceActivated(void *data, calldata_t *params);
static void SourceDeactivated(void *data, calldata_t *params);
static void SourceRenamed(void *data, calldata_t *params);
- static void ChannelChanged(void *data, calldata_t *params);
static void RenderMain(void *data, uint32_t cx, uint32_t cy);
void ResizePreview(uint32_t cx, uint32_t cy);
@@ -307,6 +387,7 @@ public:
void CreateSourcePopupMenu(QListWidgetItem *item, bool preview);
void UpdateTitleBar();
+ void UpdateSceneSelection(OBSSource source);
protected:
virtual void closeEvent(QCloseEvent *event) override;
@@ -383,6 +464,11 @@ private slots:
void on_actionAlwaysOnTop_triggered();
+ void on_transitions_currentIndexChanged(int index);
+ void on_transitionProps_clicked();
+
+ void on_modeSwitch_clicked();
+
void logUploadFinished(const QString &text, const QString &error);
void updateFileFinished(const QString &text, const QString &error);
@@ -403,6 +489,7 @@ private slots:
void OpenSceneFilters();
void OpenFilters();
+ void EnablePreviewDisplay(bool enable);
void TogglePreview();
void NudgeUp();
diff --git a/obs/window-basic-source-select.cpp b/obs/window-basic-source-select.cpp
index 3944dfa1d..c27727ae3 100644
--- a/obs/window-basic-source-select.cpp
+++ b/obs/window-basic-source-select.cpp
@@ -95,12 +95,12 @@ static void AddSource(void *_data, obs_scene_t *scene)
static void AddExisting(const char *name, const bool visible)
{
- obs_source_t *source = obs_get_output_source(0);
- obs_scene_t *scene = obs_scene_from_source(source);
+ OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
+ OBSScene scene = main->GetCurrentScene();
if (!scene)
return;
- source = obs_get_source_by_name(name);
+ obs_source_t *source = obs_get_source_by_name(name);
if (source) {
AddSourceData data;
data.source = source;
@@ -109,20 +109,18 @@ static void AddExisting(const char *name, const bool visible)
obs_source_release(source);
}
-
- obs_scene_release(scene);
}
bool AddNew(QWidget *parent, const char *id, const char *name,
const bool visible, OBSSource &newSource)
{
- obs_source_t *source = obs_get_output_source(0);
- obs_scene_t *scene = obs_scene_from_source(source);
+ OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
+ OBSScene scene = main->GetCurrentScene();
bool success = false;
- if (!source)
+ if (!scene)
return false;
- source = obs_get_source_by_name(name);
+ obs_source_t *source = obs_get_source_by_name(name);
if (source) {
QMessageBox::information(parent,
QTStr("NameExists.Title"),
@@ -144,8 +142,6 @@ bool AddNew(QWidget *parent, const char *id, const char *name,
}
obs_source_release(source);
- obs_scene_release(scene);
-
return success;
}