Przeglądaj źródła

UI: Separate controls dock from the main window

tytan652 1 rok temu
rodzic
commit
511385891c

+ 329 - 0
UI/basic-controls.cpp

@@ -0,0 +1,329 @@
+#include "basic-controls.hpp"
+
+#include "window-basic-main.hpp"
+
+OBSBasicControls::OBSBasicControls(OBSBasic *main)
+	: QFrame(nullptr),
+	  ui(new Ui::OBSBasicControls)
+{
+	/* Create UI elements */
+	ui->setupUi(this);
+
+	streamButtonMenu.reset(new QMenu());
+	startStreamAction =
+		streamButtonMenu->addAction(QTStr("Basic.Main.StartStreaming"));
+	stopStreamAction =
+		streamButtonMenu->addAction(QTStr("Basic.Main.StopStreaming"));
+	QAction *forceStopStreamAction = streamButtonMenu->addAction(
+		QTStr("Basic.Main.ForceStopStreaming"));
+
+	/* Transfer buttons signals as OBSBasicControls signals */
+	connect(
+		ui->streamButton, &QPushButton::clicked, this,
+		[this]() { emit this->StreamButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->broadcastButton, &QPushButton::clicked, this,
+		[this]() { emit this->BroadcastButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->recordButton, &QPushButton::clicked, this,
+		[this]() { emit this->RecordButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->pauseRecordButton, &QPushButton::clicked, this,
+		[this]() { emit this->PauseRecordButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->replayBufferButton, &QPushButton::clicked, this,
+		[this]() { emit this->ReplayBufferButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->saveReplayButton, &QPushButton::clicked, this,
+		[this]() { emit this->SaveReplayBufferButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->virtualCamButton, &QPushButton::clicked, this,
+		[this]() { emit this->VirtualCamButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->virtualCamConfigButton, &QPushButton::clicked, this,
+		[this]() { emit this->VirtualCamConfigButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->modeSwitch, &QPushButton::clicked, this,
+		[this]() { emit this->StudioModeButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->settingsButton, &QPushButton::clicked, this,
+		[this]() { emit this->SettingsButtonClicked(); },
+		Qt::DirectConnection);
+	connect(
+		ui->exitButton, &QPushButton::clicked, this,
+		[this]() { emit this->ExitButtonClicked(); },
+		Qt::DirectConnection);
+
+	/* Transfer menu actions signals as OBSBasicControls signals */
+	connect(
+		startStreamAction.get(), &QAction::triggered, this,
+		[this]() { emit this->StartStreamMenuActionClicked(); },
+		Qt::DirectConnection);
+	connect(
+		stopStreamAction.get(), &QAction::triggered, this,
+		[this]() { emit this->StopStreamMenuActionClicked(); },
+		Qt::DirectConnection);
+	connect(
+		forceStopStreamAction, &QAction::triggered, this,
+		[this]() { emit this->ForceStopStreamMenuActionClicked(); },
+		Qt::DirectConnection);
+
+	/* Set up default visibilty */
+	ui->broadcastButton->setVisible(false);
+	ui->pauseRecordButton->setVisible(false);
+	ui->replayBufferButton->setVisible(false);
+	ui->saveReplayButton->setVisible(false);
+	ui->virtualCamButton->setVisible(false);
+	ui->virtualCamConfigButton->setVisible(false);
+
+	/* Set up state update connections */
+	connect(main, &OBSBasic::StreamingPreparing, this,
+		&OBSBasicControls::StreamingPreparing);
+	connect(main, &OBSBasic::StreamingStarting, this,
+		&OBSBasicControls::StreamingStarting);
+	connect(main, &OBSBasic::StreamingStarted, this,
+		&OBSBasicControls::StreamingStarted);
+	connect(main, &OBSBasic::StreamingStopping, this,
+		&OBSBasicControls::StreamingStopping);
+	connect(main, &OBSBasic::StreamingStopped, this,
+		&OBSBasicControls::StreamingStopped);
+
+	connect(main, &OBSBasic::BroadcastStreamReady, this,
+		&OBSBasicControls::BroadcastStreamReady);
+	connect(main, &OBSBasic::BroadcastStreamActive, this,
+		&OBSBasicControls::BroadcastStreamActive);
+	connect(main, &OBSBasic::BroadcastStreamStarted, this,
+		&OBSBasicControls::BroadcastStreamStarted);
+
+	connect(main, &OBSBasic::RecordingStarted, this,
+		&OBSBasicControls::RecordingStarted);
+	connect(main, &OBSBasic::RecordingPaused, this,
+		&OBSBasicControls::RecordingPaused);
+	connect(main, &OBSBasic::RecordingUnpaused, this,
+		&OBSBasicControls::RecordingUnpaused);
+	connect(main, &OBSBasic::RecordingStopping, this,
+		&OBSBasicControls::RecordingStopping);
+	connect(main, &OBSBasic::RecordingStopped, this,
+		&OBSBasicControls::RecordingStopped);
+
+	connect(main, &OBSBasic::ReplayBufStarted, this,
+		&OBSBasicControls::ReplayBufferStarted);
+	connect(main, &OBSBasic::ReplayBufferStopping, this,
+		&OBSBasicControls::ReplayBufferStopping);
+	connect(main, &OBSBasic::ReplayBufStopped, this,
+		&OBSBasicControls::ReplayBufferStopped);
+
+	connect(main, &OBSBasic::VirtualCamStarted, this,
+		&OBSBasicControls::VirtualCamStarted);
+	connect(main, &OBSBasic::VirtualCamStopped, this,
+		&OBSBasicControls::VirtualCamStopped);
+
+	connect(main, &OBSBasic::PreviewProgramModeChanged, this,
+		&OBSBasicControls::UpdateStudioModeState);
+
+	/* Set up enablement connection */
+	connect(main, &OBSBasic::BroadcastFlowEnabled, this,
+		&OBSBasicControls::EnableBroadcastFlow);
+	connect(main, &OBSBasic::ReplayBufEnabled, this,
+		&OBSBasicControls::EnableReplayBufferButtons);
+	connect(main, &OBSBasic::VirtualCamEnabled, this,
+		&OBSBasicControls::EnableVirtualCamButtons);
+}
+
+void OBSBasicControls::StreamingPreparing()
+{
+	ui->streamButton->setEnabled(false);
+	ui->streamButton->setText(QTStr("Basic.Main.PreparingStream"));
+}
+
+void OBSBasicControls::StreamingStarting(bool broadcastAutoStart)
+{
+	ui->streamButton->setText(QTStr("Basic.Main.Connecting"));
+
+	if (!broadcastAutoStart) {
+		// well, we need to disable button while stream is not active
+		ui->broadcastButton->setEnabled(false);
+
+		ui->broadcastButton->setText(
+			QTStr("Basic.Main.StartBroadcast"));
+
+		ui->broadcastButton->setProperty("broadcastState", "ready");
+		ui->broadcastButton->style()->unpolish(ui->broadcastButton);
+		ui->broadcastButton->style()->polish(ui->broadcastButton);
+	}
+}
+
+void OBSBasicControls::StreamingStarted(bool withDelay)
+{
+	ui->streamButton->setEnabled(true);
+	ui->streamButton->setChecked(true);
+	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
+
+	if (withDelay) {
+		ui->streamButton->setMenu(streamButtonMenu.get());
+		startStreamAction->setVisible(false);
+		stopStreamAction->setVisible(true);
+	}
+}
+
+void OBSBasicControls::StreamingStopping()
+{
+	ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming"));
+}
+
+void OBSBasicControls::StreamingStopped(bool withDelay)
+{
+	ui->streamButton->setEnabled(true);
+	ui->streamButton->setChecked(false);
+	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
+
+	if (withDelay) {
+		if (!ui->streamButton->menu())
+			ui->streamButton->setMenu(streamButtonMenu.get());
+
+		startStreamAction->setVisible(true);
+		stopStreamAction->setVisible(false);
+	} else {
+		ui->streamButton->setMenu(nullptr);
+	}
+}
+
+void OBSBasicControls::BroadcastStreamReady(bool ready)
+{
+	ui->broadcastButton->setChecked(ready);
+}
+
+void OBSBasicControls::BroadcastStreamActive()
+{
+	ui->broadcastButton->setEnabled(true);
+}
+
+void OBSBasicControls::BroadcastStreamStarted(bool autoStop)
+{
+	ui->broadcastButton->setText(
+		QTStr(autoStop ? "Basic.Main.AutoStopEnabled"
+			       : "Basic.Main.StopBroadcast"));
+	if (autoStop)
+		ui->broadcastButton->setEnabled(false);
+
+	ui->broadcastButton->setProperty("broadcastState", "active");
+	ui->broadcastButton->style()->unpolish(ui->broadcastButton);
+	ui->broadcastButton->style()->polish(ui->broadcastButton);
+}
+
+void OBSBasicControls::RecordingStarted(bool pausable)
+{
+	ui->recordButton->setChecked(true);
+	ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
+
+	if (pausable) {
+		ui->pauseRecordButton->setVisible(pausable);
+		RecordingUnpaused();
+	}
+}
+
+void OBSBasicControls::RecordingPaused()
+{
+	QString text = QTStr("Basic.Main.UnpauseRecording");
+
+	ui->pauseRecordButton->setChecked(true);
+	ui->pauseRecordButton->setAccessibleName(text);
+	ui->pauseRecordButton->setToolTip(text);
+
+	ui->saveReplayButton->setEnabled(false);
+}
+
+void OBSBasicControls::RecordingUnpaused()
+{
+	QString text = QTStr("Basic.Main.PauseRecording");
+
+	ui->pauseRecordButton->setChecked(false);
+	ui->pauseRecordButton->setAccessibleName(text);
+	ui->pauseRecordButton->setToolTip(text);
+
+	ui->saveReplayButton->setEnabled(true);
+}
+
+void OBSBasicControls::RecordingStopping()
+{
+	ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
+}
+
+void OBSBasicControls::RecordingStopped()
+{
+	ui->recordButton->setChecked(false);
+	ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
+
+	ui->pauseRecordButton->setVisible(false);
+}
+
+void OBSBasicControls::ReplayBufferStarted()
+{
+	ui->replayBufferButton->setChecked(true);
+	ui->replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
+
+	ui->saveReplayButton->setVisible(true);
+}
+
+void OBSBasicControls::ReplayBufferStopping()
+{
+	ui->replayBufferButton->setText(
+		QTStr("Basic.Main.StoppingReplayBuffer"));
+}
+
+void OBSBasicControls::ReplayBufferStopped()
+{
+	ui->replayBufferButton->setChecked(false);
+	ui->replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
+
+	ui->saveReplayButton->setVisible(false);
+}
+
+void OBSBasicControls::VirtualCamStarted()
+{
+	ui->virtualCamButton->setChecked(true);
+	ui->virtualCamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
+}
+
+void OBSBasicControls::VirtualCamStopped()
+{
+	ui->virtualCamButton->setChecked(false);
+	ui->virtualCamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
+}
+
+void OBSBasicControls::UpdateStudioModeState(bool enabled)
+{
+	ui->modeSwitch->setChecked(enabled);
+}
+
+void OBSBasicControls::EnableBroadcastFlow(bool enabled)
+{
+	ui->broadcastButton->setVisible(enabled);
+	ui->broadcastButton->setEnabled(enabled);
+
+	ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast"));
+
+	ui->broadcastButton->setProperty("broadcastState", "idle");
+	ui->broadcastButton->style()->unpolish(ui->broadcastButton);
+	ui->broadcastButton->style()->polish(ui->broadcastButton);
+}
+
+void OBSBasicControls::EnableReplayBufferButtons(bool enabled)
+{
+	ui->replayBufferButton->setVisible(enabled);
+}
+
+void OBSBasicControls::EnableVirtualCamButtons()
+{
+	ui->virtualCamButton->setVisible(true);
+	ui->virtualCamConfigButton->setVisible(true);
+}

+ 72 - 0
UI/basic-controls.hpp

@@ -0,0 +1,72 @@
+#pragma once
+
+#include <memory>
+
+#include <QFrame>
+#include <QPointer>
+#include <QScopedPointer>
+
+class OBSBasic;
+
+#include "ui_OBSBasicControls.h"
+
+class OBSBasicControls : public QFrame {
+	Q_OBJECT
+
+	std::unique_ptr<Ui::OBSBasicControls> ui;
+
+	QScopedPointer<QMenu> streamButtonMenu;
+	QPointer<QAction> startStreamAction;
+	QPointer<QAction> stopStreamAction;
+
+private slots:
+	void StreamingPreparing();
+	void StreamingStarting(bool broadcastAutoStart);
+	void StreamingStarted(bool withDelay);
+	void StreamingStopping();
+	void StreamingStopped(bool withDelay);
+
+	void BroadcastStreamReady(bool ready);
+	void BroadcastStreamActive();
+	void BroadcastStreamStarted(bool autoStop);
+
+	void RecordingStarted(bool pausable);
+	void RecordingPaused();
+	void RecordingUnpaused();
+	void RecordingStopping();
+	void RecordingStopped();
+
+	void ReplayBufferStarted();
+	void ReplayBufferStopping();
+	void ReplayBufferStopped();
+
+	void VirtualCamStarted();
+	void VirtualCamStopped();
+
+	void UpdateStudioModeState(bool enabled);
+
+	void EnableBroadcastFlow(bool enabled);
+	void EnableReplayBufferButtons(bool enabled);
+	void EnableVirtualCamButtons();
+
+public:
+	OBSBasicControls(OBSBasic *main);
+	inline ~OBSBasicControls() {}
+
+signals:
+	void StreamButtonClicked();
+	void BroadcastButtonClicked();
+	void RecordButtonClicked();
+	void PauseRecordButtonClicked();
+	void ReplayBufferButtonClicked();
+	void SaveReplayBufferButtonClicked();
+	void VirtualCamButtonClicked();
+	void VirtualCamConfigButtonClicked();
+	void StudioModeButtonClicked();
+	void SettingsButtonClicked();
+	void ExitButtonClicked();
+
+	void StartStreamMenuActionClicked();
+	void StopStreamMenuActionClicked();
+	void ForceStopStreamMenuActionClicked();
+};

+ 4 - 2
UI/cmake/legacy.cmake

@@ -101,6 +101,7 @@ target_sources(
           forms/OBSAbout.ui
           forms/OBSAdvAudio.ui
           forms/OBSBasic.ui
+          forms/OBSBasicControls.ui
           forms/OBSBasicFilters.ui
           forms/OBSBasicInteraction.ui
           forms/OBSBasicProperties.ui
@@ -167,6 +168,8 @@ target_sources(
           audio-encoders.cpp
           audio-encoders.hpp
           balance-slider.hpp
+          basic-controls.cpp
+          basic-controls.hpp
           clickable-label.hpp
           double-slider.cpp
           double-slider.hpp
@@ -189,13 +192,12 @@ target_sources(
           menu-button.cpp
           menu-button.hpp
           mute-checkbox.hpp
+          noncheckable-button.hpp
           plain-text-edit.cpp
           plain-text-edit.hpp
           properties-view.cpp
           properties-view.hpp
           properties-view.moc.hpp
-          record-button.cpp
-          record-button.hpp
           remote-text.cpp
           remote-text.hpp
           scene-tree.cpp

+ 3 - 2
UI/cmake/ui-elements.cmake

@@ -38,6 +38,8 @@ target_sources(
           audio-encoders.cpp
           audio-encoders.hpp
           balance-slider.hpp
+          basic-controls.cpp
+          basic-controls.hpp
           context-bar-controls.cpp
           context-bar-controls.hpp
           focus-list.cpp
@@ -55,8 +57,7 @@ target_sources(
           menu-button.cpp
           menu-button.hpp
           mute-checkbox.hpp
-          record-button.cpp
-          record-button.hpp
+          noncheckable-button.hpp
           remote-text.cpp
           remote-text.hpp
           scene-tree.cpp

+ 1 - 0
UI/cmake/ui-qt.cmake

@@ -34,6 +34,7 @@ set(_qt_sources
     forms/OBSAbout.ui
     forms/OBSAdvAudio.ui
     forms/OBSBasic.ui
+    forms/OBSBasicControls.ui
     forms/OBSBasicFilters.ui
     forms/OBSBasicInteraction.ui
     forms/OBSBasicProperties.ui

+ 8 - 0
UI/data/themes/Yami.obt

@@ -990,6 +990,14 @@ QPushButton[themeID="replayBufferButton"],
     padding: var(--padding_large);
 }
 
+#pauseRecordButton,
+#saveReplayButton,
+#virtualCamConfigButton {
+    padding: var(--padding_large) var(--padding_large);
+    width: var(--input_height);
+    max-width: var(--input_height);
+}
+
 /* Primary Control Button Checked Coloring */
 #streamButton:!hover:!pressed:checked,
 #recordButton:!hover:!pressed:checked,

+ 0 - 243
UI/forms/OBSBasic.ui

@@ -1399,228 +1399,6 @@
     </layout>
    </widget>
   </widget>
-  <widget class="OBSDock" name="controlsDock">
-   <property name="features">
-    <set>QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
-   </property>
-   <property name="windowTitle">
-    <string>Basic.Main.Controls</string>
-   </property>
-   <attribute name="dockWidgetArea">
-    <number>8</number>
-   </attribute>
-   <widget class="QWidget" name="controlsDockContents">
-    <layout class="QVBoxLayout" name="verticalLayout_9">
-     <property name="spacing">
-      <number>0</number>
-     </property>
-     <property name="leftMargin">
-      <number>1</number>
-     </property>
-     <property name="topMargin">
-      <number>0</number>
-     </property>
-     <property name="rightMargin">
-      <number>1</number>
-     </property>
-     <property name="bottomMargin">
-      <number>1</number>
-     </property>
-     <item>
-      <widget class="QFrame" name="controlsFrame">
-       <layout class="QVBoxLayout" name="buttonsVLayout">
-        <property name="spacing">
-         <number>0</number>
-        </property>
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <layout class="QHBoxLayout" name="horizontalLayout_7">
-          <item>
-           <widget class="QPushButton" name="streamButton">
-            <property name="enabled">
-             <bool>true</bool>
-            </property>
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="minimumSize">
-             <size>
-              <width>150</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="text">
-             <string>Basic.Main.StartStreaming</string>
-            </property>
-            <property name="checkable">
-             <bool>true</bool>
-            </property>
-           </widget>
-          </item>
-          <item>
-           <widget class="QPushButton" name="broadcastButton">
-            <property name="enabled">
-             <bool>true</bool>
-            </property>
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="minimumSize">
-             <size>
-              <width>150</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="text">
-             <string>Basic.Main.StartBroadcast</string>
-            </property>
-            <property name="checkable">
-             <bool>true</bool>
-            </property>
-           </widget>
-          </item>
-         </layout>
-        </item>
-        <item>
-         <layout class="QHBoxLayout" name="recordingLayout">
-          <property name="spacing">
-           <number>2</number>
-          </property>
-          <property name="leftMargin">
-           <number>0</number>
-          </property>
-          <property name="topMargin">
-           <number>0</number>
-          </property>
-          <property name="rightMargin">
-           <number>0</number>
-          </property>
-          <property name="bottomMargin">
-           <number>0</number>
-          </property>
-          <item>
-           <widget class="RecordButton" name="recordButton">
-            <property name="enabled">
-             <bool>true</bool>
-            </property>
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="minimumSize">
-             <size>
-              <width>0</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="text">
-             <string>Basic.Main.StartRecording</string>
-            </property>
-            <property name="checkable">
-             <bool>true</bool>
-            </property>
-           </widget>
-          </item>
-         </layout>
-        </item>
-        <item>
-         <widget class="QPushButton" name="modeSwitch">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="minimumSize">
-           <size>
-            <width>150</width>
-            <height>0</height>
-           </size>
-          </property>
-          <property name="text">
-           <string>Basic.TogglePreviewProgramMode</string>
-          </property>
-          <property name="checkable">
-           <bool>true</bool>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="settingsButton">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="minimumSize">
-           <size>
-            <width>150</width>
-            <height>0</height>
-           </size>
-          </property>
-          <property name="text">
-           <string>Settings</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="exitButton">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="minimumSize">
-           <size>
-            <width>150</width>
-            <height>0</height>
-           </size>
-          </property>
-          <property name="text">
-           <string>Exit</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <spacer name="expVSpacer">
-          <property name="orientation">
-           <enum>Qt::Vertical</enum>
-          </property>
-          <property name="sizeHint" stdset="0">
-           <size>
-            <width>0</width>
-            <height>0</height>
-           </size>
-          </property>
-         </spacer>
-        </item>
-       </layout>
-      </widget>
-     </item>
-    </layout>
-   </widget>
-  </widget>
   <action name="actionAddScene">
    <property name="icon">
     <iconset resource="obs.qrc">
@@ -2372,11 +2150,6 @@
    <header>window-dock.hpp</header>
    <container>1</container>
   </customwidget>
-  <customwidget>
-   <class>RecordButton</class>
-   <extends>QPushButton</extends>
-   <header>record-button.hpp</header>
-  </customwidget>
  </customwidgets>
  <resources>
   <include location="obs.qrc"/>
@@ -2398,21 +2171,5 @@
     </hint>
    </hints>
   </connection>
-  <connection>
-   <sender>exitButton</sender>
-   <signal>clicked()</signal>
-   <receiver>OBSBasic</receiver>
-   <slot>close()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>976</x>
-     <y>601</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>862</x>
-     <y>-11</y>
-    </hint>
-   </hints>
-  </connection>
  </connections>
 </ui>

+ 397 - 0
UI/forms/OBSBasicControls.ui

@@ -0,0 +1,397 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSBasicControls</class>
+ <widget class="QWidget" name="OBSBasicControls">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>318</width>
+    <height>213</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_9">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>1</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>1</number>
+   </property>
+   <property name="bottomMargin">
+    <number>1</number>
+   </property>
+   <item>
+    <widget class="QFrame" name="controlsFrame">
+     <layout class="QVBoxLayout" name="buttonsVLayout">
+      <property name="spacing">
+       <number>0</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_7">
+        <item>
+         <widget class="NonCheckableButton" name="streamButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>150</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Basic.Main.StartStreaming</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="NonCheckableButton" name="broadcastButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>150</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Basic.Main.StartBroadcast</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="recordingLayout">
+        <property name="spacing">
+         <number>2</number>
+        </property>
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="NonCheckableButton" name="recordButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Basic.Main.StartRecording</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="pauseRecordButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="toolTip">
+           <string>Basic.Main.PauseRecording</string>
+          </property>
+          <property name="accessibleName">
+           <string>Basic.Main.PauseRecording</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="themeID" stdset="0">
+           <string>pauseIconSmall</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="replayBufferLayout">
+        <property name="spacing">
+         <number>2</number>
+        </property>
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="NonCheckableButton" name="replayBufferButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Basic.Main.StartReplayBuffer</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="themeID" stdset="0">
+           <string>replayBufferButton</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="saveReplayButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="toolTip">
+           <string>Basic.Main.SaveReplay</string>
+          </property>
+          <property name="accessibleName">
+           <string>Basic.Main.SaveReplay</string>
+          </property>
+          <property name="themeID" stdset="0">
+           <string>replayIconSmall</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="virtualCamLayout">
+        <property name="spacing">
+         <number>2</number>
+        </property>
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="NonCheckableButton" name="virtualCamButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Basic.Main.StartVirtualCam</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="themeID" stdset="0">
+           <string>vcamButton</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="virtualCamConfigButton">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="toolTip">
+           <string>Basic.Main.VirtualCamConfig</string>
+          </property>
+          <property name="accessibleName">
+           <string>Basic.Main.VirtualCamConfig</string>
+          </property>
+          <property name="themeID" stdset="0">
+           <string>configIconSmall</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="NonCheckableButton" name="modeSwitch">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>150</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Basic.TogglePreviewProgramMode</string>
+        </property>
+        <property name="checkable">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="settingsButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>150</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Settings</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="exitButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>150</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Exit</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="expVSpacer">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>NonCheckableButton</class>
+   <extends>QPushButton</extends>
+   <header>noncheckable-button.hpp</header>
+  </customwidget>
+ </customwidgets>
+ <resources>
+  <include location="obs.qrc"/>
+ </resources>
+ <connections/>
+</ui>

+ 23 - 0
UI/noncheckable-button.hpp

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <QPushButton>
+
+/* Button with its checked property not changed when clicked.
+ * Meant to be used in situations where manually changing the property
+ * is always preferred. */
+class NonCheckableButton : public QPushButton {
+	Q_OBJECT
+
+	inline void nextCheckState() override {}
+
+public:
+	inline NonCheckableButton(QWidget *parent = nullptr)
+		: QPushButton(parent)
+	{
+	}
+	inline NonCheckableButton(const QString &text,
+				  QWidget *parent = nullptr)
+		: QPushButton(text, parent)
+	{
+	}
+};

+ 0 - 159
UI/record-button.cpp

@@ -1,159 +0,0 @@
-#include "record-button.hpp"
-#include "window-basic-main.hpp"
-
-void RecordButton::resizeEvent(QResizeEvent *event)
-{
-	OBSBasic *main = OBSBasic::Get();
-	if (!main->pause)
-		return;
-
-	QSize pauseSize = main->pause->size();
-	int height = main->ui->recordButton->size().height();
-
-	if (pauseSize.height() != height || pauseSize.width() != height) {
-		main->pause->setMinimumSize(height, height);
-		main->pause->setMaximumSize(height, height);
-	}
-
-	event->accept();
-}
-
-static QWidget *firstWidget(QLayoutItem *item)
-{
-	auto widget = item->widget();
-	if (widget)
-		return widget;
-
-	auto layout = item->layout();
-	if (!layout)
-		return nullptr;
-
-	auto n = layout->count();
-	for (auto i = 0; i < n; i++) {
-		widget = firstWidget(layout->itemAt(i));
-		if (widget)
-			return widget;
-	}
-	return nullptr;
-}
-
-static QWidget *lastWidget(QLayoutItem *item)
-{
-	auto widget = item->widget();
-	if (widget)
-		return widget;
-
-	auto layout = item->layout();
-	if (!layout)
-		return nullptr;
-
-	for (auto i = layout->count(); i > 0; i--) {
-		widget = lastWidget(layout->itemAt(i - 1));
-		if (widget)
-			return widget;
-	}
-	return nullptr;
-}
-
-static QWidget *getNextWidget(QBoxLayout *container, QLayoutItem *item)
-{
-	for (auto i = 1, n = container->count(); i < n; i++) {
-		if (container->itemAt(i - 1) == item)
-			return firstWidget(container->itemAt(i));
-	}
-	return nullptr;
-}
-
-ControlsSplitButton::ControlsSplitButton(const QString &text,
-					 const QVariant &themeID,
-					 void (OBSBasic::*clicked)())
-	: QHBoxLayout()
-{
-	button.reset(new QPushButton(text));
-	button->setCheckable(true);
-	button->setProperty("themeID", themeID);
-
-	button->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
-	button->installEventFilter(this);
-
-	OBSBasic *main = OBSBasic::Get();
-	connect(button.data(), &QPushButton::clicked, main, clicked);
-
-	addWidget(button.data());
-}
-
-void ControlsSplitButton::addIcon(const QString &name, const QVariant &themeID,
-				  void (OBSBasic::*clicked)())
-{
-	icon.reset(new QPushButton());
-	icon->setAccessibleName(name);
-	icon->setToolTip(name);
-	icon->setChecked(false);
-	icon->setProperty("themeID", themeID);
-
-	QSizePolicy sp;
-	sp.setHeightForWidth(true);
-	icon->setSizePolicy(sp);
-
-	OBSBasic *main = OBSBasic::Get();
-	connect(icon.data(), &QAbstractButton::clicked, main, clicked);
-
-	addWidget(icon.data());
-	QWidget::setTabOrder(button.data(), icon.data());
-
-	auto next = getNextWidget(main->ui->buttonsVLayout, this);
-	if (next)
-		QWidget::setTabOrder(icon.data(), next);
-}
-
-void ControlsSplitButton::removeIcon()
-{
-	icon.reset();
-}
-
-void ControlsSplitButton::insert(int index)
-{
-	OBSBasic *main = OBSBasic::Get();
-	auto count = main->ui->buttonsVLayout->count();
-	if (index < 0)
-		index = 0;
-	else if (index > count)
-		index = count;
-
-	main->ui->buttonsVLayout->insertLayout(index, this);
-
-	QWidget *prev = button.data();
-
-	if (index > 0) {
-		prev = lastWidget(main->ui->buttonsVLayout->itemAt(index - 1));
-		if (prev)
-			QWidget::setTabOrder(prev, button.data());
-		prev = button.data();
-	}
-
-	if (icon) {
-		QWidget::setTabOrder(button.data(), icon.data());
-		prev = icon.data();
-	}
-
-	if (index < count) {
-		auto next = firstWidget(
-			main->ui->buttonsVLayout->itemAt(index + 1));
-		if (next)
-			QWidget::setTabOrder(prev, next);
-	}
-}
-
-bool ControlsSplitButton::eventFilter(QObject *obj, QEvent *event)
-{
-	if (event->type() == QEvent::Resize && icon) {
-		QSize iconSize = icon->size();
-		int height = button->height();
-
-		if (iconSize.height() != height || iconSize.width() != height) {
-			icon->setMinimumSize(height, height);
-			icon->setMaximumSize(height, height);
-		}
-	}
-	return QObject::eventFilter(obj, event);
-}

+ 0 - 39
UI/record-button.hpp

@@ -1,39 +0,0 @@
-#pragma once
-
-#include <QPushButton>
-#include <QBoxLayout>
-#include <QScopedPointer>
-
-class RecordButton : public QPushButton {
-	Q_OBJECT
-
-public:
-	inline RecordButton(QWidget *parent = nullptr) : QPushButton(parent) {}
-
-	virtual void resizeEvent(QResizeEvent *event) override;
-};
-
-class OBSBasic;
-
-class ControlsSplitButton : public QHBoxLayout {
-	Q_OBJECT
-
-public:
-	ControlsSplitButton(const QString &text, const QVariant &themeID,
-			    void (OBSBasic::*clicked)());
-
-	void addIcon(const QString &name, const QVariant &themeID,
-		     void (OBSBasic::*clicked)());
-	void removeIcon();
-	void insert(int index);
-
-	inline QPushButton *first() { return button.data(); }
-	inline QPushButton *second() { return icon.data(); }
-
-protected:
-	virtual bool eventFilter(QObject *obj, QEvent *event) override;
-
-private:
-	QScopedPointer<QPushButton> button;
-	QScopedPointer<QPushButton> icon;
-};

+ 108 - 291
UI/window-basic-main.cpp

@@ -47,6 +47,7 @@
 #include "platform.hpp"
 #include "visibility-item-widget.hpp"
 #include "item-widget-helpers.hpp"
+#include "basic-controls.hpp"
 #include "window-basic-settings.hpp"
 #include "window-namedialog.hpp"
 #include "window-basic-auto-config.hpp"
@@ -332,13 +333,6 @@ OBSBasic::OBSBasic(QWidget *parent)
 
 	ui->setupUi(this);
 	ui->previewDisabledWidget->setVisible(false);
-	ui->broadcastButton->setVisible(false);
-
-	/* Setup Studio Mode button connections */
-	connect(this, &OBSBasic::PreviewProgramModeChanged, ui->modeSwitch,
-		&QAbstractButton::setChecked);
-	connect(ui->modeSwitch, &QAbstractButton::clicked, this,
-		&OBSBasic::TogglePreviewProgramMode);
 
 	/* Set up streaming connections */
 	connect(
@@ -378,6 +372,52 @@ OBSBasic::OBSBasic(QWidget *parent)
 		},
 		Qt::DirectConnection);
 
+	/* Add controls dock */
+	OBSBasicControls *controls = new OBSBasicControls(this);
+	controlsDock = new OBSDock(this);
+	controlsDock->setObjectName(QString::fromUtf8("controlsDock"));
+	controlsDock->setWindowTitle(QTStr("Basic.Main.Controls"));
+	/* Parenting is done there so controls will be deleted alongside controlsDock */
+	controlsDock->setWidget(controls);
+	addDockWidget(Qt::BottomDockWidgetArea, controlsDock);
+
+	connect(controls, &OBSBasicControls::StreamButtonClicked, this,
+		&OBSBasic::StreamActionTriggered);
+
+	connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this,
+		&OBSBasic::StartStreaming);
+	connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this,
+		&OBSBasic::StopStreaming);
+	connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked,
+		this, &OBSBasic::ForceStopStreaming);
+
+	connect(controls, &OBSBasicControls::BroadcastButtonClicked, this,
+		&OBSBasic::BroadcastButtonClicked);
+
+	connect(controls, &OBSBasicControls::RecordButtonClicked, this,
+		&OBSBasic::RecordActionTriggered);
+	connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this,
+		&OBSBasic::RecordPauseToggled);
+
+	connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this,
+		&OBSBasic::ReplayBufferActionTriggered);
+	connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked,
+		this, &OBSBasic::ReplayBufferSave);
+
+	connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this,
+		&OBSBasic::VirtualCamActionTriggered);
+	connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked,
+		this, &OBSBasic::OpenVirtualCamConfig);
+
+	connect(controls, &OBSBasicControls::StudioModeButtonClicked, this,
+		&OBSBasic::TogglePreviewProgramMode);
+
+	connect(controls, &OBSBasicControls::SettingsButtonClicked, this,
+		&OBSBasic::on_action_Settings_triggered);
+
+	connect(controls, &OBSBasicControls::ExitButtonClicked, this,
+		&QMainWindow::close);
+
 	startingDockLayout = saveState();
 
 	statsDock = new OBSDock();
@@ -518,7 +558,7 @@ OBSBasic::OBSBasic(QWidget *parent)
 	SETUP_DOCK(ui->sourcesDock);
 	SETUP_DOCK(ui->mixerDock);
 	SETUP_DOCK(ui->transitionsDock);
-	SETUP_DOCK(ui->controlsDock);
+	SETUP_DOCK(controlsDock);
 	SETUP_DOCK(statsDock);
 #undef SETUP_DOCK
 
@@ -577,9 +617,6 @@ OBSBasic::OBSBasic(QWidget *parent)
 	connect(ui->scenes, &SceneTree::scenesReordered,
 		[]() { OBSProjector::UpdateMultiviewProjectors(); });
 
-	connect(ui->broadcastButton, &QPushButton::clicked, this,
-		&OBSBasic::BroadcastButtonClicked);
-
 	connect(App(), &OBSApp::StyleChanged, this, [this]() {
 		if (api)
 			api->on_event(OBS_FRONTEND_EVENT_THEME_CHANGED);
@@ -1985,7 +2022,7 @@ void OBSBasic::InitPrimitives()
 	obs_leave_graphics();
 }
 
-void OBSBasic::ReplayBufferClicked()
+void OBSBasic::ReplayBufferActionTriggered()
 {
 	if (outputHandler->ReplayBufferActive())
 		StopReplayBuffer();
@@ -1993,19 +2030,6 @@ void OBSBasic::ReplayBufferClicked()
 		StartReplayBuffer();
 };
 
-void OBSBasic::AddVCamButton()
-{
-	vcamButton = new ControlsSplitButton(
-		QTStr("Basic.Main.StartVirtualCam"), "vcamButton",
-		&OBSBasic::VCamButtonClicked);
-	vcamButton->addIcon(QTStr("Basic.Main.VirtualCamConfig"),
-			    QStringLiteral("configIconSmall"),
-			    &OBSBasic::VCamConfigButtonClicked);
-	vcamButton->insert(2);
-	vcamButton->first()->setSizePolicy(QSizePolicy::Minimum,
-					   QSizePolicy::Minimum);
-}
-
 void OBSBasic::ResetOutputs()
 {
 	ProfileScope("OBSBasic::ResetOutputs");
@@ -2019,15 +2043,7 @@ void OBSBasic::ResetOutputs()
 		outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this)
 					   : CreateSimpleOutputHandler(this));
 
-		delete replayBufferButton;
-
-		if (outputHandler->replayBuffer) {
-			replayBufferButton = new ControlsSplitButton(
-				QTStr("Basic.Main.StartReplayBuffer"),
-				"replayBufferButton",
-				&OBSBasic::ReplayBufferClicked);
-			replayBufferButton->insert(2);
-		}
+		emit ReplayBufEnabled(outputHandler->replayBuffer);
 
 		if (sysTrayReplayBuffer)
 			sysTrayReplayBuffer->setEnabled(
@@ -2169,7 +2185,7 @@ void OBSBasic::OBSInit()
 	vcamEnabled =
 		(obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0;
 	if (vcamEnabled) {
-		AddVCamButton();
+		emit VirtualCamEnabled();
 	}
 
 	InitBasicConfigDefaults2();
@@ -6945,9 +6961,6 @@ void OBSBasic::DisplayStreamStartError()
 				  : QTStr("Output.StartFailedGeneric");
 
 	emit StreamingStopped();
-	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
-	ui->streamButton->setEnabled(true);
-	ui->streamButton->setChecked(false);
 
 	if (sysTrayStream) {
 		sysTrayStream->setText(QTStr("Basic.Main.StartStreaming"));
@@ -6981,6 +6994,8 @@ void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id,
 	autoStopBroadcast = autostop;
 	broadcastReady = true;
 
+	emit BroadcastStreamReady(broadcastReady);
+
 	if (start_now)
 		QMetaObject::invokeMethod(this, "StartStreaming");
 }
@@ -7023,9 +7038,7 @@ void OBSBasic::YoutubeStreamCheck(const std::string &key)
 		auto item = json["items"][0];
 		auto status = item["status"]["streamStatus"].string_value();
 		if (status == "active") {
-			QMetaObject::invokeMethod(ui->broadcastButton,
-						  "setEnabled",
-						  Q_ARG(bool, true));
+			emit BroadcastStreamActive();
 			break;
 		} else {
 			QThread::sleep(1);
@@ -7077,8 +7090,6 @@ void OBSBasic::StartStreaming()
 
 	if (auth && auth->broadcastFlow()) {
 		if (!broadcastActive && !broadcastReady) {
-			ui->streamButton->setChecked(false);
-
 			QMessageBox no_broadcast(this);
 			no_broadcast.setText(QTStr("Output.NoBroadcast.Text"));
 			QPushButton *SetupBroadcast = no_broadcast.addButton(
@@ -7099,16 +7110,12 @@ void OBSBasic::StartStreaming()
 		}
 	}
 
-	emit StreamingStarting();
-	ui->streamButton->setEnabled(false);
-	ui->streamButton->setChecked(false);
-	ui->broadcastButton->setChecked(false);
-	if (sysTrayStream)
-		sysTrayStream->setEnabled(false);
+	emit StreamingPreparing();
 
-	ui->streamButton->setText("Basic.Main.PreparingStream");
-	if (sysTrayStream)
+	if (sysTrayStream) {
+		sysTrayStream->setEnabled(false);
 		sysTrayStream->setText("Basic.Main.PreparingStream");
+	}
 
 	auto holder = outputHandler->SetupStreaming(service);
 	auto future = holder.future.then(this, [&](bool setupStreamingResult) {
@@ -7122,7 +7129,8 @@ void OBSBasic::StartStreaming()
 
 		SaveProject();
 
-		ui->streamButton->setText("Basic.Main.Connecting");
+		emit StreamingStarting(autoStartBroadcast);
+
 		if (sysTrayStream)
 			sysTrayStream->setText("Basic.Main.Connecting");
 
@@ -7131,32 +7139,8 @@ void OBSBasic::StartStreaming()
 			return;
 		}
 
-		if (!autoStartBroadcast) {
-			ui->broadcastButton->setText(
-				QTStr("Basic.Main.StartBroadcast"));
-			ui->broadcastButton->setProperty("broadcastState",
-							 "ready");
-			ui->broadcastButton->style()->unpolish(
-				ui->broadcastButton);
-			ui->broadcastButton->style()->polish(
-				ui->broadcastButton);
-			// well, we need to disable button while stream is not active
-			ui->broadcastButton->setEnabled(false);
-		} else {
-			if (!autoStopBroadcast) {
-				ui->broadcastButton->setText(
-					QTStr("Basic.Main.StopBroadcast"));
-			} else {
-				ui->broadcastButton->setText(
-					QTStr("Basic.Main.AutoStopEnabled"));
-				ui->broadcastButton->setEnabled(false);
-			}
-			ui->broadcastButton->setProperty("broadcastState",
-							 "active");
-			ui->broadcastButton->style()->unpolish(
-				ui->broadcastButton);
-			ui->broadcastButton->style()->polish(
-				ui->broadcastButton);
+		if (autoStartBroadcast) {
+			emit BroadcastStreamStarted(autoStopBroadcast);
 			broadcastActive = true;
 		}
 
@@ -7185,8 +7169,6 @@ void OBSBasic::BroadcastButtonClicked()
 	if (!broadcastReady ||
 	    (!broadcastActive && !outputHandler->StreamingActive())) {
 		SetupBroadcast();
-		if (broadcastReady)
-			ui->broadcastButton->setChecked(true);
 		return;
 	}
 
@@ -7210,7 +7192,6 @@ void OBSBasic::BroadcastButtonClicked()
 					this,
 					QTStr("Output.BroadcastStartFailed"),
 					last_error, true);
-				ui->broadcastButton->setChecked(false);
 				return;
 			}
 		}
@@ -7218,18 +7199,7 @@ void OBSBasic::BroadcastButtonClicked()
 		broadcastActive = true;
 		autoStartBroadcast = true; // and clear the flag
 
-		if (!autoStopBroadcast) {
-			ui->broadcastButton->setText(
-				QTStr("Basic.Main.StopBroadcast"));
-		} else {
-			ui->broadcastButton->setText(
-				QTStr("Basic.Main.AutoStopEnabled"));
-			ui->broadcastButton->setEnabled(false);
-		}
-
-		ui->broadcastButton->setProperty("broadcastState", "active");
-		ui->broadcastButton->style()->unpolish(ui->broadcastButton);
-		ui->broadcastButton->style()->polish(ui->broadcastButton);
+		emit BroadcastStreamStarted(autoStopBroadcast);
 	} else if (!autoStopBroadcast) {
 #ifdef YOUTUBE_ENABLED
 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
@@ -7241,10 +7211,8 @@ void OBSBasic::BroadcastButtonClicked()
 				QMessageBox::Yes | QMessageBox::No,
 				QMessageBox::No);
 
-			if (button == QMessageBox::No) {
-				ui->broadcastButton->setChecked(true);
+			if (button == QMessageBox::No)
 				return;
-			}
 		}
 
 		std::shared_ptr<YoutubeApiWrappers> ytAuth =
@@ -7273,19 +7241,14 @@ void OBSBasic::BroadcastButtonClicked()
 
 		autoStopBroadcast = true;
 		QMetaObject::invokeMethod(this, "StopStreaming");
+		emit BroadcastStreamReady(broadcastReady);
 		SetBroadcastFlowEnabled(true);
 	}
 }
 
 void OBSBasic::SetBroadcastFlowEnabled(bool enabled)
 {
-	ui->broadcastButton->setEnabled(enabled);
-	ui->broadcastButton->setVisible(enabled);
-	ui->broadcastButton->setChecked(broadcastReady);
-	ui->broadcastButton->setProperty("broadcastState", "idle");
-	ui->broadcastButton->style()->unpolish(ui->broadcastButton);
-	ui->broadcastButton->style()->polish(ui->broadcastButton);
-	ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast"));
+	emit BroadcastFlowEnabled(enabled);
 }
 
 void OBSBasic::SetupBroadcast()
@@ -7296,11 +7259,7 @@ void OBSBasic::SetupBroadcast()
 		OBSYoutubeActions dialog(this, auth, broadcastReady);
 		connect(&dialog, &OBSYoutubeActions::ok, this,
 			&OBSBasic::YouTubeActionDialogOk);
-		int result = dialog.Valid() ? dialog.exec() : QDialog::Rejected;
-		if (result != QDialog::Accepted) {
-			if (!broadcastReady)
-				ui->broadcastButton->setChecked(false);
-		}
+		dialog.exec();
 	}
 #endif
 }
@@ -7433,6 +7392,8 @@ void OBSBasic::StopStreaming()
 		broadcastReady = false;
 	}
 
+	emit BroadcastStreamReady(broadcastReady);
+
 	OnDeactivate();
 
 	bool recordWhenStreaming = config_get_bool(
@@ -7473,6 +7434,8 @@ void OBSBasic::ForceStopStreaming()
 		broadcastReady = false;
 	}
 
+	emit BroadcastStreamReady(broadcastReady);
+
 	OnDeactivate();
 
 	bool recordWhenStreaming = config_get_bool(
@@ -7494,26 +7457,13 @@ void OBSBasic::ForceStopStreaming()
 
 void OBSBasic::StreamDelayStarting(int sec)
 {
-	emit StreamingStarted();
-	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
-	ui->streamButton->setEnabled(true);
-	ui->streamButton->setChecked(true);
+	emit StreamingStarted(true);
 
 	if (sysTrayStream) {
 		sysTrayStream->setText(QTStr("Basic.Main.StopStreaming"));
 		sysTrayStream->setEnabled(true);
 	}
 
-	if (!startStreamMenu.isNull())
-		startStreamMenu->deleteLater();
-
-	startStreamMenu = new QMenu();
-	startStreamMenu->addAction(QTStr("Basic.Main.StopStreaming"), this,
-				   &OBSBasic::StopStreaming);
-	startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
-				   &OBSBasic::ForceStopStreaming);
-	ui->streamButton->setMenu(startStreamMenu);
-
 	ui->statusbar->StreamDelayStarting(sec);
 
 	OnActivate();
@@ -7521,26 +7471,13 @@ void OBSBasic::StreamDelayStarting(int sec)
 
 void OBSBasic::StreamDelayStopping(int sec)
 {
-	emit StreamingStopped();
-	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
-	ui->streamButton->setEnabled(true);
-	ui->streamButton->setChecked(false);
+	emit StreamingStopped(true);
 
 	if (sysTrayStream) {
 		sysTrayStream->setText(QTStr("Basic.Main.StartStreaming"));
 		sysTrayStream->setEnabled(true);
 	}
 
-	if (!startStreamMenu.isNull())
-		startStreamMenu->deleteLater();
-
-	startStreamMenu = new QMenu();
-	startStreamMenu->addAction(QTStr("Basic.Main.StartStreaming"), this,
-				   &OBSBasic::StartStreaming);
-	startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), this,
-				   &OBSBasic::ForceStopStreaming);
-	ui->streamButton->setMenu(startStreamMenu);
-
 	ui->statusbar->StreamDelayStopping(sec);
 
 	if (api)
@@ -7551,10 +7488,6 @@ void OBSBasic::StreamingStart()
 {
 	emit StreamingStarted();
 	OBSOutputAutoRelease output = obs_frontend_get_streaming_output();
-
-	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));
-	ui->streamButton->setEnabled(true);
-	ui->streamButton->setChecked(true);
 	ui->statusbar->StreamStarted(output);
 
 	if (sysTrayStream) {
@@ -7594,7 +7527,7 @@ void OBSBasic::StreamingStart()
 
 void OBSBasic::StreamStopping()
 {
-	ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming"));
+	emit StreamingStopping();
 
 	if (sysTrayStream)
 		sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming"));
@@ -7655,9 +7588,6 @@ void OBSBasic::StreamingStop(int code, QString last_error)
 	ui->statusbar->StreamStopped();
 
 	emit StreamingStopped();
-	ui->streamButton->setText(QTStr("Basic.Main.StartStreaming"));
-	ui->streamButton->setEnabled(true);
-	ui->streamButton->setChecked(false);
 
 	if (sysTrayStream) {
 		sysTrayStream->setText(QTStr("Basic.Main.StartStreaming"));
@@ -7696,12 +7626,6 @@ void OBSBasic::StreamingStop(int code, QString last_error)
 			      QSystemTrayIcon::Warning);
 	}
 
-	if (!startStreamMenu.isNull()) {
-		ui->streamButton->setMenu(nullptr);
-		startStreamMenu->deleteLater();
-		startStreamMenu = nullptr;
-	}
-
 	// Reset broadcast button state/text
 	if (!broadcastActive)
 		SetBroadcastFlowEnabled(auth && auth->broadcastFlow());
@@ -7796,13 +7720,11 @@ void OBSBasic::StartRecording()
 
 	if (!OutputPathValid()) {
 		OutputPathInvalidMessage();
-		ui->recordButton->setChecked(false);
 		return;
 	}
 
 	if (!IsFFmpegOutputToURL() && LowDiskSpace()) {
 		DiskSpaceMessage();
-		ui->recordButton->setChecked(false);
 		return;
 	}
 
@@ -7811,13 +7733,12 @@ void OBSBasic::StartRecording()
 
 	SaveProject();
 
-	if (!outputHandler->StartRecording())
-		ui->recordButton->setChecked(false);
+	outputHandler->StartRecording();
 }
 
 void OBSBasic::RecordStopping()
 {
-	ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
+	emit RecordingStopping();
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording"));
@@ -7840,9 +7761,7 @@ void OBSBasic::StopRecording()
 void OBSBasic::RecordingStart()
 {
 	ui->statusbar->RecordingStarted(outputHandler->fileOutput);
-	emit RecordingStarted();
-	ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
-	ui->recordButton->setChecked(true);
+	emit RecordingStarted(isRecordingPausable);
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(QTStr("Basic.Main.StopRecording"));
@@ -7855,7 +7774,6 @@ void OBSBasic::RecordingStart()
 		diskFullTimer->start(1000);
 
 	OnActivate();
-	UpdatePause();
 
 	blog(LOG_INFO, RECORDING_START);
 }
@@ -7864,8 +7782,6 @@ void OBSBasic::RecordingStop(int code, QString last_error)
 {
 	ui->statusbar->RecordingStopped();
 	emit RecordingStopped();
-	ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
-	ui->recordButton->setChecked(false);
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(QTStr("Basic.Main.StartRecording"));
@@ -7935,7 +7851,6 @@ void OBSBasic::RecordingStop(int code, QString last_error)
 	AutoRemux(outputHandler->lastRecordingPath.c_str());
 
 	OnDeactivate();
-	UpdatePause(false);
 }
 
 void OBSBasic::RecordingFileChanged(QString lastRecordingPath)
@@ -7986,20 +7901,16 @@ void OBSBasic::StartReplayBuffer()
 	if (disableOutputsRef)
 		return;
 
-	if (!UIValidation::NoSourcesConfirmation(this)) {
-		replayBufferButton->first()->setChecked(false);
+	if (!UIValidation::NoSourcesConfirmation(this))
 		return;
-	}
 
 	if (!OutputPathValid()) {
 		OutputPathInvalidMessage();
-		replayBufferButton->first()->setChecked(false);
 		return;
 	}
 
 	if (LowDiskSpace()) {
 		DiskSpaceMessage();
-		replayBufferButton->first()->setChecked(false);
 		return;
 	}
 
@@ -8008,9 +7919,8 @@ void OBSBasic::StartReplayBuffer()
 
 	SaveProject();
 
-	if (!outputHandler->StartReplayBuffer()) {
-		replayBufferButton->first()->setChecked(false);
-	} else if (os_atomic_load_bool(&recording_paused)) {
+	if (outputHandler->StartReplayBuffer() &&
+	    os_atomic_load_bool(&recording_paused)) {
 		ShowReplayBufferPauseWarning();
 	}
 }
@@ -8020,8 +7930,7 @@ void OBSBasic::ReplayBufferStopping()
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 
-	replayBufferButton->first()->setText(
-		QTStr("Basic.Main.StoppingReplayBuffer"));
+	emit ReplayBufStopping();
 
 	if (sysTrayReplayBuffer)
 		sysTrayReplayBuffer->setText(
@@ -8050,9 +7959,7 @@ void OBSBasic::ReplayBufferStart()
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 
-	replayBufferButton->first()->setText(
-		QTStr("Basic.Main.StopReplayBuffer"));
-	replayBufferButton->first()->setChecked(true);
+	emit ReplayBufStarted();
 
 	if (sysTrayReplayBuffer)
 		sysTrayReplayBuffer->setText(
@@ -8063,7 +7970,6 @@ void OBSBasic::ReplayBufferStart()
 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED);
 
 	OnActivate();
-	UpdateReplayBuffer();
 
 	blog(LOG_INFO, REPLAY_BUFFER_START);
 }
@@ -8111,9 +8017,7 @@ void OBSBasic::ReplayBufferStop(int code)
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 
-	replayBufferButton->first()->setText(
-		QTStr("Basic.Main.StartReplayBuffer"));
-	replayBufferButton->first()->setChecked(false);
+	emit ReplayBufStopped();
 
 	if (sysTrayReplayBuffer)
 		sysTrayReplayBuffer->setText(
@@ -8151,7 +8055,6 @@ void OBSBasic::ReplayBufferStop(int code)
 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED);
 
 	OnDeactivate();
-	UpdateReplayBuffer(false);
 }
 
 void OBSBasic::StartVirtualCam()
@@ -8165,9 +8068,7 @@ void OBSBasic::StartVirtualCam()
 
 	SaveProject();
 
-	if (!outputHandler->StartVirtualCam()) {
-		vcamButton->first()->setChecked(false);
-	}
+	outputHandler->StartVirtualCam();
 }
 
 void OBSBasic::StopVirtualCam()
@@ -8188,10 +8089,10 @@ void OBSBasic::OnVirtualCamStart()
 	if (!outputHandler || !outputHandler->virtualCam)
 		return;
 
-	vcamButton->first()->setText(QTStr("Basic.Main.StopVirtualCam"));
+	emit VirtualCamStarted();
+
 	if (sysTrayVirtualCam)
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
-	vcamButton->first()->setChecked(true);
 
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
@@ -8206,10 +8107,10 @@ void OBSBasic::OnVirtualCamStop(int)
 	if (!outputHandler || !outputHandler->virtualCam)
 		return;
 
-	vcamButton->first()->setText(QTStr("Basic.Main.StartVirtualCam"));
+	emit VirtualCamStopped();
+
 	if (sysTrayVirtualCam)
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
-	vcamButton->first()->setChecked(false);
 
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
@@ -8226,7 +8127,7 @@ void OBSBasic::OnVirtualCamStop(int)
 	QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam);
 }
 
-void OBSBasic::on_streamButton_clicked()
+void OBSBasic::StreamActionTriggered()
 {
 	if (outputHandler->StreamingActive()) {
 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
@@ -8241,10 +8142,8 @@ void OBSBasic::on_streamButton_clicked()
 				QMessageBox::Yes | QMessageBox::No,
 				QMessageBox::No);
 
-			if (button == QMessageBox::No) {
-				ui->streamButton->setChecked(true);
+			if (button == QMessageBox::No)
 				return;
-			}
 
 			confirm = false;
 		}
@@ -8257,18 +8156,14 @@ void OBSBasic::on_streamButton_clicked()
 					QMessageBox::Yes | QMessageBox::No,
 					QMessageBox::No);
 
-			if (button == QMessageBox::No) {
-				ui->streamButton->setChecked(true);
+			if (button == QMessageBox::No)
 				return;
-			}
 		}
 
 		StopStreaming();
 	} else {
-		if (!UIValidation::NoSourcesConfirmation(this)) {
-			ui->streamButton->setChecked(false);
+		if (!UIValidation::NoSourcesConfirmation(this))
 			return;
-		}
 
 		Auth *auth = GetAuth();
 
@@ -8282,10 +8177,8 @@ void OBSBasic::on_streamButton_clicked()
 			break;
 		case StreamSettingsAction::OpenSettings:
 			on_action_Settings_triggered();
-			ui->streamButton->setChecked(false);
 			return;
 		case StreamSettingsAction::Cancel:
-			ui->streamButton->setChecked(false);
 			return;
 		}
 
@@ -8310,10 +8203,8 @@ void OBSBasic::on_streamButton_clicked()
 					this, QTStr("ConfirmBWTest.Title"),
 					QTStr("ConfirmBWTest.Text"));
 
-			if (button == QMessageBox::No) {
-				ui->streamButton->setChecked(false);
+			if (button == QMessageBox::No)
 				return;
-			}
 		} else if (confirm && isVisible()) {
 			QMessageBox::StandardButton button =
 				OBSMessageBox::question(
@@ -8322,17 +8213,15 @@ void OBSBasic::on_streamButton_clicked()
 					QMessageBox::Yes | QMessageBox::No,
 					QMessageBox::No);
 
-			if (button == QMessageBox::No) {
-				ui->streamButton->setChecked(false);
+			if (button == QMessageBox::No)
 				return;
-			}
 		}
 
 		StartStreaming();
 	}
 }
 
-void OBSBasic::on_recordButton_clicked()
+void OBSBasic::RecordActionTriggered()
 {
 	if (outputHandler->RecordingActive()) {
 		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
@@ -8346,37 +8235,31 @@ void OBSBasic::on_recordButton_clicked()
 					QMessageBox::Yes | QMessageBox::No,
 					QMessageBox::No);
 
-			if (button == QMessageBox::No) {
-				ui->recordButton->setChecked(true);
+			if (button == QMessageBox::No)
 				return;
-			}
 		}
 		StopRecording();
 	} else {
-		if (!UIValidation::NoSourcesConfirmation(this)) {
-			ui->recordButton->setChecked(false);
+		if (!UIValidation::NoSourcesConfirmation(this))
 			return;
-		}
 
 		StartRecording();
 	}
 }
 
-void OBSBasic::VCamButtonClicked()
+void OBSBasic::VirtualCamActionTriggered()
 {
 	if (outputHandler->VirtualCamActive()) {
 		StopVirtualCam();
 	} else {
-		if (!UIValidation::NoSourcesConfirmation(this)) {
-			vcamButton->first()->setChecked(false);
+		if (!UIValidation::NoSourcesConfirmation(this))
 			return;
-		}
 
 		StartVirtualCam();
 	}
 }
 
-void OBSBasic::VCamConfigButtonClicked()
+void OBSBasic::OpenVirtualCamConfig()
 {
 	OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(),
 				  this);
@@ -8440,11 +8323,6 @@ void OBSBasic::RestartingVirtualCam()
 	restartingVCam = false;
 }
 
-void OBSBasic::on_settingsButton_clicked()
-{
-	on_action_Settings_triggered();
-}
-
 void OBSBasic::on_actionHelpPortal_triggered()
 {
 	QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode);
@@ -9735,7 +9613,7 @@ void OBSBasic::on_resetDocks_triggered(bool force)
 
 	QList<QDockWidget *> docks{ui->scenesDock, ui->sourcesDock,
 				   ui->mixerDock, ui->transitionsDock,
-				   ui->controlsDock};
+				   controlsDock};
 
 	QList<int> sizes{cx22_5, cx22_5, mixerSize, cx5, cx21};
 
@@ -9743,7 +9621,7 @@ void OBSBasic::on_resetDocks_triggered(bool force)
 	ui->sourcesDock->setVisible(true);
 	ui->mixerDock->setVisible(true);
 	ui->transitionsDock->setVisible(true);
-	ui->controlsDock->setVisible(true);
+	controlsDock->setVisible(true);
 	statsDock->setVisible(false);
 	statsDock->setFloating(true);
 
@@ -9768,7 +9646,7 @@ void OBSBasic::on_lockDocks_toggled(bool lock)
 	ui->sourcesDock->setFeatures(mainFeatures);
 	ui->mixerDock->setFeatures(mainFeatures);
 	ui->transitionsDock->setFeatures(mainFeatures);
-	ui->controlsDock->setFeatures(mainFeatures);
+	controlsDock->setFeatures(mainFeatures);
 	statsDock->setFeatures(features);
 
 	for (int i = extraDocks.size() - 1; i >= 0; i--)
@@ -10071,13 +9949,13 @@ void OBSBasic::SystemTrayInit()
 		&OBSBasic::IconActivated);
 	connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide);
 	connect(sysTrayStream, &QAction::triggered, this,
-		&OBSBasic::on_streamButton_clicked);
+		&OBSBasic::StreamActionTriggered);
 	connect(sysTrayRecord, &QAction::triggered, this,
-		&OBSBasic::on_recordButton_clicked);
+		&OBSBasic::RecordActionTriggered);
 	connect(sysTrayReplayBuffer.data(), &QAction::triggered, this,
-		&OBSBasic::ReplayBufferClicked);
+		&OBSBasic::ReplayBufferActionTriggered);
 	connect(sysTrayVirtualCam.data(), &QAction::triggered, this,
-		&OBSBasic::VCamButtonClicked);
+		&OBSBasic::VirtualCamActionTriggered);
 	connect(exit, &QAction::triggered, this, &OBSBasic::close);
 }
 
@@ -10851,11 +10729,6 @@ void OBSBasic::PauseRecording()
 		os_atomic_set_bool(&recording_paused, true);
 
 		emit RecordingPaused();
-		pause->setAccessibleName(QTStr("Basic.Main.UnpauseRecording"));
-		pause->setToolTip(QTStr("Basic.Main.UnpauseRecording"));
-		pause->blockSignals(true);
-		pause->setChecked(true);
-		pause->blockSignals(false);
 
 		ui->statusbar->RecordingPaused();
 
@@ -10873,11 +10746,6 @@ void OBSBasic::PauseRecording()
 							   trayIconFile));
 		}
 
-		auto replay = replayBufferButton ? replayBufferButton->second()
-						 : nullptr;
-		if (replay)
-			replay->setEnabled(false);
-
 		if (api)
 			api->on_event(OBS_FRONTEND_EVENT_RECORDING_PAUSED);
 
@@ -10899,11 +10767,6 @@ void OBSBasic::UnpauseRecording()
 		os_atomic_set_bool(&recording_paused, false);
 
 		emit RecordingUnpaused();
-		pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
-		pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
-		pause->blockSignals(true);
-		pause->setChecked(false);
-		pause->blockSignals(false);
 
 		ui->statusbar->RecordingUnpaused();
 
@@ -10921,17 +10784,12 @@ void OBSBasic::UnpauseRecording()
 							   trayIconFile));
 		}
 
-		auto replay = replayBufferButton ? replayBufferButton->second()
-						 : nullptr;
-		if (replay)
-			replay->setEnabled(true);
-
 		if (api)
 			api->on_event(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED);
 	}
 }
 
-void OBSBasic::PauseToggled()
+void OBSBasic::RecordPauseToggled()
 {
 	if (!isRecordingPausable || !outputHandler ||
 	    !outputHandler->fileOutput)
@@ -10973,47 +10831,6 @@ void OBSBasic::UpdateIsRecordingPausable()
 	isRecordingPausable = !shared;
 }
 
-void OBSBasic::UpdatePause(bool activate)
-{
-	if (!activate || !outputHandler || !outputHandler->RecordingActive()) {
-		pause.reset();
-		return;
-	}
-
-	if (isRecordingPausable) {
-		pause.reset(new QPushButton());
-		pause->setAccessibleName(QTStr("Basic.Main.PauseRecording"));
-		pause->setToolTip(QTStr("Basic.Main.PauseRecording"));
-		pause->setCheckable(true);
-		pause->setChecked(false);
-		pause->setProperty("themeID",
-				   QVariant(QStringLiteral("pauseIconSmall")));
-
-		QSizePolicy sp;
-		sp.setHeightForWidth(true);
-		pause->setSizePolicy(sp);
-
-		connect(pause.data(), &QAbstractButton::clicked, this,
-			&OBSBasic::PauseToggled);
-		ui->recordingLayout->addWidget(pause.data());
-	} else {
-		pause.reset();
-	}
-}
-
-void OBSBasic::UpdateReplayBuffer(bool activate)
-{
-	if (!activate || !outputHandler ||
-	    !outputHandler->ReplayBufferActive()) {
-		replayBufferButton->removeIcon();
-		return;
-	}
-
-	replayBufferButton->addIcon(QTStr("Basic.Main.SaveReplay"),
-				    QStringLiteral("replayIconSmall"),
-				    &OBSBasic::ReplayBufferSave);
-}
-
 #define MBYTE (1024ULL * 1024ULL)
 #define MBYTES_LEFT_STOP_REC 50ULL
 #define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE)

+ 43 - 21
UI/window-basic-main.hpp

@@ -319,13 +319,8 @@ private:
 	QPointer<QWidget> extraBrowsers;
 	QPointer<QWidget> importer;
 
-	QPointer<QMenu> startStreamMenu;
-
 	QPointer<QPushButton> transitionButton;
-	QPointer<ControlsSplitButton> replayBufferButton;
-	QScopedPointer<QPushButton> pause;
 
-	QPointer<ControlsSplitButton> vcamButton;
 	bool vcamEnabled = false;
 	VCamConfig vcamConfig;
 
@@ -546,8 +541,6 @@ private:
 	void dragMoveEvent(QDragMoveEvent *event) override;
 	void dropEvent(QDropEvent *event) override;
 
-	void ReplayBufferClicked();
-
 	bool sysTrayMinimizeToTray();
 
 	void EnumDialogs();
@@ -888,8 +881,6 @@ private:
 	void AutoRemux(QString input, bool no_show = false);
 
 	void UpdateIsRecordingPausable();
-	void UpdatePause(bool activate = true);
-	void UpdateReplayBuffer(bool activate = true);
 
 	bool IsFFmpegOutputToURL() const;
 	bool OutputPathValid();
@@ -933,7 +924,6 @@ public:
 	int ResetVideo();
 	bool ResetAudio();
 
-	void AddVCamButton();
 	void ResetOutputs();
 
 	void RefreshVolumeColors();
@@ -1132,11 +1122,6 @@ private slots:
 	void on_actionScaleCanvas_triggered();
 	void on_actionScaleOutput_triggered();
 
-	void on_streamButton_clicked();
-	void on_recordButton_clicked();
-	void VCamButtonClicked();
-	void VCamConfigButtonClicked();
-	void on_settingsButton_clicked();
 	void Screenshot(OBSSource source_ = nullptr);
 	void ScreenshotSelectedSource();
 	void ScreenshotProgram();
@@ -1198,8 +1183,6 @@ private slots:
 	void on_multiviewProjectorWindowed_triggered();
 	void on_sideDocks_toggled(bool side);
 
-	void PauseToggled();
-
 	void logUploadFinished(const QString &text, const QString &error);
 	void crashUploadFinished(const QString &text, const QString &error);
 	void openLogDialog(const QString &text, const bool crash);
@@ -1241,6 +1224,23 @@ private slots:
 	void RepairOldExtraDockName();
 	void RepairCustomExtraDockName();
 
+	/* Stream action (start/stop) slot */
+	void StreamActionTriggered();
+
+	/* Record action (start/stop) slot */
+	void RecordActionTriggered();
+
+	/* Record pause (pause/unpause) slot */
+	void RecordPauseToggled();
+
+	/* Replay Buffer action (start/stop) slot */
+	void ReplayBufferActionTriggered();
+
+	/* Virtual Cam action (start/stop) slots */
+	void VirtualCamActionTriggered();
+
+	void OpenVirtualCamConfig();
+
 	/* Studio Mode toggle slot */
 	void TogglePreviewProgramMode();
 
@@ -1259,22 +1259,44 @@ public slots:
 
 signals:
 	/* Streaming signals */
-	void StreamingStarting();
-	void StreamingStarted();
-	void StreamingStopped();
+	void StreamingPreparing();
+	void StreamingStarting(bool broadcastAutoStart);
+	void StreamingStarted(bool withDelay = false);
+	void StreamingStopping();
+	void StreamingStopped(bool withDelay = false);
+
+	/* Broadcast Flow signals */
+	void BroadcastFlowEnabled(bool enabled);
+	void BroadcastStreamReady(bool ready);
+	void BroadcastStreamActive();
+	void BroadcastStreamStarted(bool autoStop);
 
 	/* Recording signals */
-	void RecordingStarted();
+	void RecordingStarted(bool pausable = false);
 	void RecordingPaused();
 	void RecordingUnpaused();
+	void RecordingStopping();
 	void RecordingStopped();
 
+	/* Replay Buffer signals */
+	void ReplayBufEnabled(bool enabled);
+	void ReplayBufStarted();
+	void ReplayBufStopping();
+	void ReplayBufStopped();
+
+	/* Virtual Camera signals */
+	void VirtualCamEnabled();
+	void VirtualCamStarted();
+	void VirtualCamStopped();
+
 	/* Studio Mode signal */
 	void PreviewProgramModeChanged(bool enabled);
 
 private:
 	std::unique_ptr<Ui::OBSBasic> ui;
 
+	QPointer<OBSDock> controlsDock;
+
 public:
 	/* `undo_s` needs to be declared after `ui` to prevent an uninitialized
 	 * warning for `ui` while initializing `undo_s`. */