Browse Source

UI: Add Vertical Mixer option

Shaolin 7 years ago
parent
commit
c7c328dc83

+ 2 - 0
UI/CMakeLists.txt

@@ -161,6 +161,7 @@ set(obs_SOURCES
 	item-widget-helpers.cpp
 	visibility-checkbox.cpp
 	locked-checkbox.cpp
+	horizontal-scroll-area.cpp
 	vertical-scroll-area.cpp
 	visibility-item-widget.cpp
 	slider-absoluteset-style.cpp
@@ -209,6 +210,7 @@ set(obs_HEADERS
 	item-widget-helpers.hpp
 	visibility-checkbox.hpp
 	locked-checkbox.hpp
+	horizontal-scroll-area.hpp
 	vertical-scroll-area.hpp
 	visibility-item-widget.hpp
 	slider-absoluteset-style.hpp

+ 1 - 0
UI/data/locale/en-US.ini

@@ -83,6 +83,7 @@ None="None"
 StudioMode.Preview="Preview"
 StudioMode.Program="Program"
 ShowInMultiview="Show in Multiview"
+VerticalLayout="Vertical Layout"
 
 # warning if program already open
 AlreadyRunning.Title="OBS is already running"

+ 18 - 15
UI/data/themes/Acri.qss

@@ -505,16 +505,20 @@ QSlider::handle:horizontal {
 }
 
 QSlider::handle:horizontal:pressed {
-	background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0,
+	background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,
 		stop: 0 rgb(240,239,240),
 		stop: 0.25 rgb(200,199,200),
 		stop: 1 rgb(162,161,162));
 }
 
+QSlider::sub-page:horizontal {
+	background-color: #2a3a75;
+}
+
 QSlider::sub-page:horizontal:disabled {
-	background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,
-		stop: 0 rgb(31,30,31),
-		stop: 0.75 rgb(50, 49, 50));
+	background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0,
+		stop: 0 rgb(26,25,26),
+		stop: 0.75 rgb(10, 10, 10));
 	border-radius: 2px;
 }
 
@@ -533,23 +537,27 @@ QSlider::handle:vertical {
 		stop: 0.25 rgb(200,199,200),
 		stop: 1 rgb(162,161,162));
 	border: 1px solid rgb(24,24,25);
-	border-radius: 4px;
+	border-radius: 3px;
 	width: 10px;
 	height: 18px;
-	margin: -3px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
+	margin: 0 -3px; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
 }
 
 QSlider::handle:vertical:pressed {
-	background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+	background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0,
 		stop: 0 rgb(240,239,240),
 		stop: 0.25 rgb(200,199,200),
 		stop: 1 rgb(162,161,162));
 }
 
-QSlider::sub-page:vertical:disabled {
+QSlider::add-page:vertical {
+	background-color: #2a3a75;
+}
+
+QSlider::add-page:vertical:disabled {
 	background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0,
-		stop: 0 rgb(31,30,31),
-		stop: 0.75 rgb(50, 49, 50));
+		stop: 0 rgb(26,25,26),
+		stop: 0.75 rgb(10, 10, 10));
 	border-radius: 2px;
 }
 
@@ -557,15 +565,10 @@ QSlider::handle:hover {
 	background-color: rgb(200,199,200);
 }
 
-QSlider::sub-page {
-	background-color: #2a3a75;
-}
-
 QSlider::handle:disabled {
 	background-color: rgb(15,15,16);
 }
 
-
 /* Volume Control */
 
 /* Old Meters */

+ 24 - 20
UI/data/themes/Dark.qss

@@ -423,9 +423,9 @@ QPushButton::menu-indicator {
 /* Sliders */
 
 QSlider::groove:horizontal {
-    background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,
-        stop: 0 rgb(31,30,31), /* veryDark */
-        stop: 0.75 rgb(50, 49, 50));
+    background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0,
+        stop: 0 rgb(50, 49, 50), /* dark */
+        stop: 0.75 rgb(88,87,88)); /* kindaDark */
     height: 4px;
     border: none;
     border-radius: 2px;
@@ -450,32 +450,37 @@ QSlider::handle:horizontal:pressed {
         stop: 1 rgb(162,161,162)); /* light */
 }
 
+QSlider::sub-page:horizontal {
+    background-color: rgb(42,130,218); /* blue */
+    border-radius: 2px;
+}
+
 QSlider::sub-page:horizontal:disabled {
-    background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,
+    background-color: QLinearGradient(x1: 0, y1: 1, x2: 0, y2: 0,
         stop: 0 rgb(31,30,31), /* veryDark */
-        stop: 0.75 rgb(50, 49, 50));
+        stop: 0.75 rgb(50, 49, 50)); /* dark */
     border-radius: 2px;
 }
 
 QSlider::groove:vertical {
-    background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0,
-        stop: 0 rgb(31,30,31), /* veryDark */
-        stop: 0.75 rgb(50, 49, 50));
+    background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+        stop: 0 rgb(50, 49, 50), /* dark */
+        stop: 0.75 rgb(88,87,88)); /* kindaDark */
     width: 4px;
     border: none;
     border-radius: 2px;
 }
 
 QSlider::handle:vertical {
-    background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0,
+    background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
         stop: 0 rgb(240,239,240), /* lighter */
         stop: 0.25 rgb(200,199,200),
         stop: 1 rgb(162,161,162)); /* light */
     border: 1px solid rgb(58,57,58); /* dark */
-    border-radius: 4px;
+    border-radius: 3px;
     width: 10px;
     height: 18px;
-    margin: -3px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
+    margin: 0 -3px; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
 }
 
 QSlider::handle:vertical:pressed {
@@ -485,10 +490,15 @@ QSlider::handle:vertical:pressed {
         stop: 1 rgb(162,161,162)); /* light */
 }
 
-QSlider::sub-page:vertical:disabled {
-    background-color: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0,
+QSlider::add-page:vertical {
+    background-color: rgb(42,130,218); /* blue */
+    border-radius: 2px;
+}
+
+QSlider::add-page:vertical:disabled {
+    background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
         stop: 0 rgb(31,30,31), /* veryDark */
-        stop: 0.75 rgb(50, 49, 50));
+        stop: 0.75 rgb(50, 49, 50)); /* dark */
     border-radius: 2px;
 }
 
@@ -496,16 +506,10 @@ QSlider::handle:hover {
     background-color: rgb(200,199,200); /* veryLight */
 }
 
-QSlider::sub-page {
-    background-color: rgb(42,130,218); /* blue */
-    border-radius: 2px;
-}
-
 QSlider::handle:disabled {
     background-color: rgb(122,121,122); /* light */
 }
 
-
 /* Volume Control */
 
 VolumeMeter {

+ 43 - 3
UI/data/themes/Rachni.qss

@@ -1022,22 +1022,62 @@ QSlider::handle:horizontal:pressed {
 	stop: 1 rgb(162, 161, 162));
 }
 
+QSlider::sub-page:horizontal {
+	background-color: rgb(0, 188, 212); /* Cyan (Primary) */
+	border-radius: 2px;
+}
+
 QSlider::sub-page:horizontal:disabled {
 	background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,
 	stop: 0 rgb(35, 38, 41), /* Dark Gray */
+	stop: 0.75 rgb(35, 38, 41)); /* Dark Gray */
+	border-radius: 2px;
+}
+
+QSlider::groove:vertical {
+	background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+	stop: 0 rgb(35, 38, 41), /* Dark Gray */
 	stop: 0.75 rgb(50, 49, 50));
+	width: 4px;
+	border: none;
 	border-radius: 2px;
 }
 
-QSlider::handle:hover {
-	background-color: rgb(200, 199, 200);
+QSlider::handle:vertical {
+	background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+	stop: 0 rgb(240, 239, 240),
+	stop: 0.25 rgb(200, 199, 200),
+	stop: 1 rgb(162, 161, 162));
+	border: 1px solid rgb(58, 57, 58);
+	border-radius: 3px;
+	width: 10px;
+	height: 18px;
+	margin: 0 -3px;
+}
+
+QSlider::handle:vertical:pressed {
+	background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+	stop: 0 rgb(240, 239, 240),
+	stop: 0.25 rgb(200, 199, 200),
+	stop: 1 rgb(162, 161, 162));
 }
 
-QSlider::sub-page {
+QSlider::add-page:vertical {
 	background-color: rgb(0, 188, 212); /* Cyan (Primary) */
 	border-radius: 2px;
 }
 
+QSlider::add-page:vertical:disabled {
+	background-color: QLinearGradient(x1: 1, y1: 0, x2: 0, y2: 0,
+	stop: 0 rgb(35, 38, 41), /* Dark Gray */
+	stop: 0.75 rgb(35, 38, 41)); /* Dark Gray */
+	border-radius: 2px;
+}
+
+QSlider::handle:hover {
+	background-color: rgb(200, 199, 200);
+}
+
 QSlider::handle:disabled {
 	background-color: rgb(122, 121, 122);
 }

+ 112 - 51
UI/forms/OBSBasic.ui

@@ -615,63 +615,118 @@
       <number>4</number>
      </property>
      <item>
-      <widget class="VScrollArea" name="mixerScrollArea">
-       <property name="minimumSize">
-        <size>
-         <width>220</width>
-         <height>0</height>
-        </size>
-       </property>
-       <property name="contextMenuPolicy">
-        <enum>Qt::CustomContextMenu</enum>
-       </property>
-       <property name="frameShape">
-        <enum>QFrame::StyledPanel</enum>
-       </property>
-       <property name="frameShadow">
-        <enum>QFrame::Sunken</enum>
-       </property>
-       <property name="verticalScrollBarPolicy">
-        <enum>Qt::ScrollBarAlwaysOn</enum>
-       </property>
-       <property name="horizontalScrollBarPolicy">
-        <enum>Qt::ScrollBarAlwaysOff</enum>
-       </property>
-       <property name="widgetResizable">
-        <bool>true</bool>
-       </property>
-       <widget class="QWidget" name="volumeWidgets">
-        <property name="geometry">
-         <rect>
-          <x>0</x>
-          <y>0</y>
-          <width>230</width>
-          <height>16</height>
-         </rect>
+      <widget class="QStackedWidget" name="stackedMixerArea">
+       <widget class="VScrollArea" name="hMixerScrollArea">
+        <property name="minimumSize">
+         <size>
+          <width>175</width>
+          <height>220</height>
+         </size>
         </property>
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
+        <property name="contextMenuPolicy">
+         <enum>Qt::CustomContextMenu</enum>
         </property>
-        <layout class="QVBoxLayout" name="verticalLayout_18">
-         <property name="spacing">
-          <number>0</number>
-         </property>
-         <property name="leftMargin">
-          <number>0</number>
+        <property name="frameShape">
+         <enum>QFrame::StyledPanel</enum>
+        </property>
+        <property name="frameShadow">
+         <enum>QFrame::Sunken</enum>
+        </property>
+        <property name="verticalScrollBarPolicy">
+         <enum>Qt::ScrollBarAlwaysOn</enum>
+        </property>
+        <property name="horizontalScrollBarPolicy">
+         <enum>Qt::ScrollBarAlwaysOff</enum>
+        </property>
+        <property name="widgetResizable">
+         <bool>true</bool>
+        </property>
+        <widget class="QWidget" name="hVolumeWidgets">
+         <property name="geometry">
+          <rect>
+           <x>0</x>
+           <y>0</y>
+           <width>230</width>
+           <height>16</height>
+          </rect>
          </property>
-         <property name="topMargin">
-          <number>0</number>
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
          </property>
-         <property name="rightMargin">
-          <number>0</number>
+         <layout class="QVBoxLayout" name="hVolControlLayout">
+          <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>
+         </layout>
+        </widget>
+       </widget>
+       <widget class="HScrollArea" name="vMixerScrollArea">
+        <property name="contextMenuPolicy">
+         <enum>Qt::CustomContextMenu</enum>
+        </property>
+        <property name="frameShape">
+         <enum>QFrame::StyledPanel</enum>
+        </property>
+        <property name="frameShadow">
+         <enum>QFrame::Sunken</enum>
+        </property>
+        <property name="verticalScrollBarPolicy">
+         <enum>Qt::ScrollBarAlwaysOff</enum>
+        </property>
+        <property name="horizontalScrollBarPolicy">
+         <enum>Qt::ScrollBarAlwaysOn</enum>
+        </property>
+        <property name="widgetResizable">
+         <bool>true</bool>
+        </property>
+        <widget class="QWidget" name="vVolumeWidgets">
+         <property name="geometry">
+          <rect>
+           <x>0</x>
+           <y>0</y>
+           <width>16</width>
+           <height>230</height>
+          </rect>
          </property>
-         <property name="bottomMargin">
-          <number>0</number>
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
          </property>
-        </layout>
+         <layout class="QHBoxLayout" name="vVolControlLayout">
+          <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>
+         </layout>
+        </widget>
        </widget>
       </widget>
      </item>
@@ -1614,6 +1669,12 @@
    <extends>QStatusBar</extends>
    <header>window-basic-status-bar.hpp</header>
   </customwidget>
+  <customwidget>
+   <class>HScrollArea</class>
+   <extends>QScrollArea</extends>
+   <header>horizontal-scroll-area.hpp</header>
+   <container>1</container>
+  </customwidget>
   <customwidget>
    <class>VScrollArea</class>
    <extends>QScrollArea</extends>

+ 2 - 0
UI/frontend-plugins/frontend-tools/CMakeLists.txt

@@ -25,6 +25,7 @@ set(frontend-tools_HEADERS
 	tool-helpers.hpp
 	../../properties-view.hpp
 	../../properties-view.moc.hpp
+	../../horizontal-scroll-area.hpp
 	../../vertical-scroll-area.hpp
 	../../double-slider.hpp
 	)
@@ -34,6 +35,7 @@ set(frontend-tools_SOURCES
 	frontend-tools.c
 	output-timer.cpp
 	../../properties-view.cpp
+	../../horizontal-scroll-area.cpp
 	../../vertical-scroll-area.cpp
 	../../double-slider.cpp
 	)

+ 10 - 0
UI/horizontal-scroll-area.cpp

@@ -0,0 +1,10 @@
+#include <QResizeEvent>
+#include "horizontal-scroll-area.hpp"
+
+void HScrollArea::resizeEvent(QResizeEvent *event)
+{
+	if (!!widget())
+		widget()->setMaximumHeight(event->size().height());
+
+	QScrollArea::resizeEvent(event);
+}

+ 19 - 0
UI/horizontal-scroll-area.hpp

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <QScrollArea>
+
+class QResizeEvent;
+
+class HScrollArea : public QScrollArea {
+	Q_OBJECT
+
+public:
+	inline HScrollArea(QWidget *parent = nullptr)
+		: QScrollArea(parent)
+	{
+		setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	}
+
+protected:
+	virtual void resizeEvent(QResizeEvent *event) override;
+};

+ 3 - 0
UI/obs-app.cpp

@@ -418,6 +418,9 @@ bool OBSApp::InitGlobalConfigDefaults()
 				"CurrentTheme", "Dark");
 	}
 
+	config_set_default_bool(globalConfig, "BasicWindow",
+			"VerticalVolControl", false);
+
 #ifdef _WIN32
 	config_set_default_bool(globalConfig, "Audio", "DisableAudioDucking",
 			true);

+ 265 - 70
UI/volume-control.cpp

@@ -111,57 +111,20 @@ void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType)
 	volMeter->setPeakMeterType(peakMeterType);
 }
 
-VolControl::VolControl(OBSSource source_, bool showConfig)
+VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical)
 		: source      (std::move(source_)),
 		levelTotal    (0.0f),
 		levelCount    (0.0f),
 		obs_fader     (obs_fader_create(OBS_FADER_CUBIC)),
-		obs_volmeter  (obs_volmeter_create(OBS_FADER_LOG))
+		obs_volmeter  (obs_volmeter_create(OBS_FADER_LOG)),
+		vertical      (vertical)
 {
-	QHBoxLayout *volLayout  = new QHBoxLayout();
-	QVBoxLayout *mainLayout = new QVBoxLayout();
-	QHBoxLayout *textLayout = new QHBoxLayout();
-	QHBoxLayout *botLayout  = new QHBoxLayout();
-
 	nameLabel = new QLabel();
 	volLabel  = new QLabel();
-	volMeter  = new VolumeMeter(nullptr, obs_volmeter);
 	mute      = new MuteCheckBox();
-	slider    = new QSlider(Qt::Horizontal);
-
-	QFont font = nameLabel->font();
-	font.setPointSize(font.pointSize()-1);
-
 	QString sourceName = obs_source_get_name(source);
 	setObjectName(sourceName);
 
-	nameLabel->setText(sourceName);
-	nameLabel->setFont(font);
-	volLabel->setFont(font);
-	slider->setMinimum(0);
-	slider->setMaximum(100);
-
-//	slider->setMaximumHeight(13);
-
-	textLayout->setContentsMargins(0, 0, 0, 0);
-	textLayout->addWidget(nameLabel);
-	textLayout->addWidget(volLabel);
-	textLayout->setAlignment(nameLabel, Qt::AlignLeft);
-	textLayout->setAlignment(volLabel,  Qt::AlignRight);
-
-	bool muted = obs_source_muted(source);
-	mute->setChecked(muted);
-	mute->setAccessibleName(
-			QTStr("VolControl.Mute").arg(sourceName));
-
-	volLayout->addWidget(slider);
-	volLayout->addWidget(mute);
-	volLayout->setSpacing(5);
-
-	botLayout->setContentsMargins(0, 0, 0, 0);
-	botLayout->setSpacing(0);
-	botLayout->addLayout(volLayout);
-
 	if (showConfig) {
 		config = new QPushButton(this);
 		config->setProperty("themeID", "configIconSmall");
@@ -176,18 +139,99 @@ VolControl::VolControl(OBSSource source_, bool showConfig)
 
 		connect(config, &QAbstractButton::clicked,
 				this, &VolControl::EmitConfigClicked);
-
-		botLayout->addWidget(config);
 	}
 
+	QVBoxLayout *mainLayout = new QVBoxLayout;
 	mainLayout->setContentsMargins(4, 4, 4, 4);
 	mainLayout->setSpacing(2);
-	mainLayout->addItem(textLayout);
-	mainLayout->addWidget(volMeter);
-	mainLayout->addItem(botLayout);
+
+	if (vertical) {
+		QHBoxLayout *nameLayout = new QHBoxLayout;
+		QHBoxLayout *controlLayout = new QHBoxLayout;
+		QHBoxLayout *volLayout = new QHBoxLayout;
+		QHBoxLayout *meterLayout  = new QHBoxLayout;
+
+		volMeter  = new VolumeMeter(nullptr, obs_volmeter, true);
+		slider    = new QSlider(Qt::Vertical);
+
+		nameLayout->setAlignment(Qt::AlignCenter);
+		meterLayout->setAlignment(Qt::AlignCenter);
+		controlLayout->setAlignment(Qt::AlignCenter);
+		volLayout->setAlignment(Qt::AlignCenter);
+
+		nameLayout->setContentsMargins(0, 0, 0, 0);
+		nameLayout->setSpacing(0);
+		nameLayout->addWidget(nameLabel);
+
+		controlLayout->setContentsMargins(0, 0, 0, 0);
+		controlLayout->setSpacing(0);
+
+		if (showConfig)
+			controlLayout->addWidget(config);
+
+		controlLayout->addItem(new QSpacerItem(3, 0));
+		// Add Headphone (audio monitoring) widget here
+		controlLayout->addWidget(mute);
+
+		meterLayout->setContentsMargins(0, 0, 0, 0);
+		meterLayout->setSpacing(0);
+		meterLayout->addWidget(volMeter);
+		meterLayout->addWidget(slider);
+
+		volLayout->setContentsMargins(0, 0, 0, 0);
+		volLayout->setSpacing(0);
+		volLayout->addWidget(volLabel);
+
+		mainLayout->addItem(nameLayout);
+		mainLayout->addItem(volLayout);
+		mainLayout->addItem(meterLayout);
+		mainLayout->addItem(controlLayout);
+
+		setMaximumWidth(110);
+	} else {
+		QHBoxLayout *volLayout  = new QHBoxLayout;
+		QHBoxLayout *textLayout = new QHBoxLayout;
+		QHBoxLayout *botLayout  = new QHBoxLayout;
+
+		volMeter  = new VolumeMeter(nullptr, obs_volmeter, false);
+		slider    = new QSlider(Qt::Horizontal);
+
+		textLayout->setContentsMargins(0, 0, 0, 0);
+		textLayout->addWidget(nameLabel);
+		textLayout->addWidget(volLabel);
+		textLayout->setAlignment(nameLabel, Qt::AlignLeft);
+		textLayout->setAlignment(volLabel,  Qt::AlignRight);
+
+		volLayout->addWidget(slider);
+		volLayout->addWidget(mute);
+		volLayout->setSpacing(5);
+
+		botLayout->setContentsMargins(0, 0, 0, 0);
+		botLayout->setSpacing(0);
+		botLayout->addLayout(volLayout);
+
+		if (showConfig)
+			botLayout->addWidget(config);
+
+		mainLayout->addItem(textLayout);
+		mainLayout->addWidget(volMeter);
+		mainLayout->addItem(botLayout);
+	}
 
 	setLayout(mainLayout);
 
+	QFont font = nameLabel->font();
+	font.setPointSize(font.pointSize()-1);
+
+	nameLabel->setText(sourceName);
+	nameLabel->setFont(font);
+	volLabel->setFont(font);
+	slider->setMinimum(0);
+	slider->setMaximum(100);
+
+	bool muted = obs_source_muted(source);
+	mute->setChecked(muted);
+	mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName));
 	obs_fader_add_callback(obs_fader, OBSVolumeChanged, this);
 	obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this);
 
@@ -443,8 +487,10 @@ void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType)
 	}
 }
 
-VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter)
-		: QWidget(parent), obs_volmeter(obs_volmeter)
+VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter,
+		bool vertical)
+		: QWidget(parent), obs_volmeter(obs_volmeter),
+		vertical(vertical)
 {
 	// Use a font that can be rendered small.
 	tickFont = QFont("Arial");
@@ -532,9 +578,12 @@ inline void VolumeMeter::handleChannelCofigurationChange()
 	if (displayNrAudioChannels != currentNrAudioChannels) {
 		displayNrAudioChannels = currentNrAudioChannels;
 
-		// Make room for 3 pixels high meter, with one pixel between
-		// each. Then 9 pixels below it for ticks and numbers.
-		setMinimumSize(130, displayNrAudioChannels * 4 + 8);
+		// Make room for 3 pixels meter, with one pixel between each.
+		// Then 9/13 pixels for ticks and numbers.
+		if (vertical)
+			setMinimumSize(displayNrAudioChannels * 4 + 14, 130);
+		else
+			setMinimumSize(130, displayNrAudioChannels * 4 + 8);
 
 		resetLevels();
 	}
@@ -650,7 +699,7 @@ void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width,
 	painter.fillRect(x, y, width, height, color);
 }
 
-void VolumeMeter::paintTicks(QPainter &painter, int x, int y, int width,
+void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width,
 		int height)
 {
 	qreal scale = width / minimumLevel;
@@ -679,6 +728,36 @@ void VolumeMeter::paintTicks(QPainter &painter, int x, int y, int width,
 	}
 }
 
+void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height)
+{
+	qreal scale = height / minimumLevel;
+
+	painter.setFont(tickFont);
+	painter.setPen(majorTickColor);
+
+	// Draw major tick lines and numeric indicators.
+	for (int i = 0; i >= minimumLevel; i-= 5) {
+		int position = y + int((i * scale) - 1);
+		QString str = QString::number(i);
+
+		if (i == 0)
+			painter.drawText(x + 5, position + 4, str);
+		else if (i == -60)
+			painter.drawText(x + 4, position, str);
+		else
+			painter.drawText(x + 4, position + 2, str);
+		painter.drawLine(x, position, x + 2, position);
+	}
+
+	// Draw minor tick lines.
+	painter.setPen(minorTickColor);
+	for (int i = 0; i >= minimumLevel; i--) {
+		int position = y + int((i * scale) - 1);
+		if (i % 5 != 0)
+			painter.drawLine(x, position, x + 1, position);
+	}
+}
+
 #define CLIP_FLASH_DURATION_MS 1000
 
 void VolumeMeter::ClipEnding()
@@ -686,7 +765,7 @@ void VolumeMeter::ClipEnding()
 	clipping = false;
 }
 
-void VolumeMeter::paintMeter(QPainter &painter, int x, int y, int width,
+void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width,
 		int height, float magnitude, float peak, float peakHold)
 {
 	qreal scale = width / minimumLevel;
@@ -775,22 +854,111 @@ void VolumeMeter::paintMeter(QPainter &painter, int x, int y, int width,
 				magnitudeColor);
 }
 
-void VolumeMeter::paintEvent(QPaintEvent *event)
+void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width,
+		int height, float magnitude, float peak, float peakHold)
 {
-	UNUSED_PARAMETER(event);
+	qreal scale = height / minimumLevel;
+
+	QMutexLocker locker(&dataMutex);
+	int minimumPosition     = y + 0;
+	int maximumPosition     = y + height;
+	int magnitudePosition   = int(y + height - (magnitude * scale));
+	int peakPosition        = int(y + height - (peak * scale));
+	int peakHoldPosition    = int(y + height - (peakHold * scale));
+	int warningPosition     = int(y + height - (warningLevel * scale));
+	int errorPosition       = int(y + height - (errorLevel * scale));
+
+	int nominalLength       = warningPosition - minimumPosition;
+	int warningLength       = errorPosition - warningPosition;
+	int errorLength         = maximumPosition - errorPosition;
+	locker.unlock();
+
+	if (clipping) {
+		peakPosition = maximumPosition;
+	}
+
+	if (peakPosition < minimumPosition) {
+		painter.fillRect(x, minimumPosition, width, nominalLength,
+				backgroundNominalColor);
+		painter.fillRect(x, warningPosition, width, warningLength,
+				backgroundWarningColor);
+		painter.fillRect(x, errorPosition, width, errorLength,
+				backgroundErrorColor);
+	} else if (peakPosition < warningPosition) {
+		painter.fillRect(x, minimumPosition, width, peakPosition -
+				minimumPosition, foregroundNominalColor);
+		painter.fillRect(x, peakPosition, width, warningPosition -
+				peakPosition, backgroundNominalColor);
+		painter.fillRect(x, warningPosition, width, warningLength,
+				backgroundWarningColor);
+		painter.fillRect(x, errorPosition, width, errorLength,
+				backgroundErrorColor);
+	} else if (peakPosition < errorPosition) {
+		painter.fillRect(x,minimumPosition, width, nominalLength,
+				foregroundNominalColor);
+		painter.fillRect(x, warningPosition, width, peakPosition -
+				warningPosition, foregroundWarningColor);
+		painter.fillRect(x, peakPosition, width, errorPosition -
+				peakPosition, backgroundWarningColor);
+		painter.fillRect(x, errorPosition, width, errorLength,
+				backgroundErrorColor);
+	} else if (peakPosition < maximumPosition) {
+		painter.fillRect(x, minimumPosition, width, nominalLength,
+				foregroundNominalColor);
+		painter.fillRect(x, warningPosition, width, warningLength,
+				foregroundWarningColor);
+		painter.fillRect(x, errorPosition, width, peakPosition -
+				errorPosition, foregroundErrorColor);
+		painter.fillRect(x, peakPosition, width, maximumPosition -
+				peakPosition, backgroundErrorColor);
+	} else {
+		if (!clipping) {
+			QTimer::singleShot(CLIP_FLASH_DURATION_MS, this,
+					SLOT(ClipEnding()));
+			clipping = true;
+		}
+
+		int end = errorLength + warningLength + nominalLength;
+		painter.fillRect(x, minimumPosition, width, end,
+				QBrush(foregroundErrorColor));
+	}
+
+	if (peakHoldPosition - 3 < minimumPosition)
+		;// Peak-hold below minimum, no drawing.
+	else if (peakHoldPosition < warningPosition)
+		painter.fillRect(x, peakHoldPosition - 3, width, 3,
+				foregroundNominalColor);
+	else if (peakHoldPosition < errorPosition)
+		painter.fillRect(x, peakHoldPosition - 3, width, 3,
+				foregroundWarningColor);
+	else
+		painter.fillRect(x, peakHoldPosition - 3, width, 3,
+				foregroundErrorColor);
+
+	if (magnitudePosition - 3 >= minimumPosition)
+		painter.fillRect(x, magnitudePosition - 3, width, 3,
+				magnitudeColor);
+}
 
+void VolumeMeter::paintEvent(QPaintEvent *event)
+{
 	uint64_t ts = os_gettime_ns();
 	qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001;
 
-	int width  = size().width();
-	int height = size().height();
+	const QRect rect = event->region().boundingRect();
+	int width  = rect.width();
+	int height = rect.height();
 
 	handleChannelCofigurationChange();
 	calculateBallistics(ts, timeSinceLastRedraw);
 	bool idle = detectIdle(ts);
 
 	// Draw the ticks in a off-screen buffer when the widget changes size.
-	QSize tickPaintCacheSize = QSize(width, 9);
+	QSize tickPaintCacheSize;
+	if (vertical)
+		tickPaintCacheSize = QSize(14, height);
+	else
+		tickPaintCacheSize = QSize(width, 9);
 	if (tickPaintCache == nullptr ||
 		tickPaintCache->size() != tickPaintCacheSize) {
 		delete tickPaintCache;
@@ -800,29 +968,56 @@ void VolumeMeter::paintEvent(QPaintEvent *event)
 		tickPaintCache->fill(clearColor);
 
 		QPainter tickPainter(tickPaintCache);
-		paintTicks(tickPainter, 6, 0, tickPaintCacheSize.width() - 6,
-				tickPaintCacheSize.height());
+		if (vertical) {
+			tickPainter.translate(0, height);
+			tickPainter.scale(1, -1);
+			paintVTicks(tickPainter, 0, 11,
+					tickPaintCacheSize.height() - 11);
+		} else {
+			paintHTicks(tickPainter, 6, 0,
+					tickPaintCacheSize.width() - 6,
+					tickPaintCacheSize.height());
+		}
 		tickPainter.end();
 	}
 
 	// Actual painting of the widget starts here.
 	QPainter painter(this);
-	painter.drawPixmap(0, height - 9, *tickPaintCache);
+	if (vertical) {
+		// Invert the Y axis to ease the math
+		painter.translate(0, height);
+		painter.scale(1, -1);
+		painter.drawPixmap(displayNrAudioChannels * 4 - 1, 7,
+				*tickPaintCache);
+	} else {
+		painter.drawPixmap(0, height - 9, *tickPaintCache);
+	}
 
 	for (int channelNr = 0; channelNr < displayNrAudioChannels;
 		channelNr++) {
-		paintMeter(painter, 5, channelNr * 4, width - 5, 3,
-				displayMagnitude[channelNr],
-				displayPeak[channelNr],
-				displayPeakHold[channelNr]);
-
-		if (!idle) {
-			// By not drawing the input meter boxes the user can
-			// see that the audio stream has been stopped, without
-			// having too much visual impact.
+		if (vertical)
+			paintVMeter(painter, channelNr * 4, 8, 3, height - 10,
+					displayMagnitude[channelNr],
+					displayPeak[channelNr],
+					displayPeakHold[channelNr]);
+		else
+			paintHMeter(painter, 5, channelNr * 4, width - 5, 3,
+					displayMagnitude[channelNr],
+					displayPeak[channelNr],
+					displayPeakHold[channelNr]);
+
+		if (idle)
+			continue;
+
+		// By not drawing the input meter boxes the user can
+		// see that the audio stream has been stopped, without
+		// having too much visual impact.
+		if (vertical)
+			paintInputMeter(painter, channelNr * 4, 3, 3, 3,
+					displayInputPeakHold[channelNr]);
+		else
 			paintInputMeter(painter, 0, channelNr * 4, 3, 3,
 					displayInputPeakHold[channelNr]);
-		}
 	}
 
 	lastRedrawTime = ts;

+ 15 - 8
UI/volume-control.hpp

@@ -96,12 +96,15 @@ private:
 	inline void calculateBallisticsForChannel(int channelNr,
 		uint64_t ts, qreal timeSinceLastRedraw);
 
-	void paintInputMeter(QPainter &painter, int x, int y,
-		int width, int height, float peakHold);
-	void paintMeter(QPainter &painter, int x, int y,
-		int width, int height,
-		float magnitude, float peak, float peakHold);
-	void paintTicks(QPainter &painter, int x, int y, int width, int height);
+	void paintInputMeter(QPainter &painter, int x, int y, int width,
+			int height, float peakHold);
+	void paintHMeter(QPainter &painter, int x, int y, int width, int height,
+			float magnitude, float peak, float peakHold);
+	void paintHTicks(QPainter &painter, int x, int y, int width,
+			int height);
+	void paintVMeter(QPainter &painter, int x, int y, int width, int height,
+			float magnitude, float peak, float peakHold);
+	void paintVTicks(QPainter &painter, int x, int y, int height);
 
 	QMutex dataMutex;
 
@@ -142,10 +145,12 @@ private:
 
 	uint64_t lastRedrawTime = 0;
 	bool clipping = false;
+	bool vertical;
 
 public:
 	explicit VolumeMeter(QWidget *parent = nullptr,
-		obs_volmeter_t *obs_volmeter = nullptr);
+			obs_volmeter_t *obs_volmeter = nullptr,
+			bool vertical = false);
 	~VolumeMeter();
 
 	void setLevels(
@@ -230,6 +235,7 @@ private:
 	float           levelCount;
 	obs_fader_t     *obs_fader;
 	obs_volmeter_t  *obs_volmeter;
+	bool            vertical;
 
 	static void OBSVolumeChanged(void *param, float db);
 	static void OBSVolumeLevel(void *data,
@@ -252,7 +258,8 @@ signals:
 	void ConfigClicked();
 
 public:
-	explicit VolControl(OBSSource source, bool showConfig = false);
+	explicit VolControl(OBSSource source, bool showConfig = false,
+			bool vertical = false);
 	~VolControl();
 
 	inline obs_source_t *GetSource() const {return source;}

+ 68 - 3
UI/window-basic-main.cpp

@@ -1572,6 +1572,10 @@ void OBSBasic::OBSInit()
 		}
 	}
 
+	bool vertical = config_get_bool(App()->GlobalConfig(), "BasicWindow",
+			"VerticalVolControl");
+	ui->stackedMixerArea->setCurrentIndex(vertical);
+
 	if (config_get_bool(basicConfig, "General", "OpenStatsOnStartup"))
 		on_stats_triggered();
 
@@ -2471,6 +2475,11 @@ void OBSBasic::VolControlContextMenu()
 	QAction propertiesAction(QTStr("Properties"), this);
 	QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
 
+	QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
+	toggleControlLayoutAction.setCheckable(true);
+	toggleControlLayoutAction.setChecked(config_get_bool(GetGlobalConfig(),
+			"BasicWindow", "VerticalVolControl"));
+
 	/* ------------------- */
 
 	connect(&hideAction, &QAction::triggered,
@@ -2495,6 +2504,12 @@ void OBSBasic::VolControlContextMenu()
 
 	/* ------------------- */
 
+	connect(&toggleControlLayoutAction, &QAction::changed, this,
+			&OBSBasic::ToggleVolControlLayout,
+			Qt::DirectConnection);
+
+	/* ------------------- */
+
 	hideAction.setProperty("volControl",
 			QVariant::fromValue<VolControl*>(vol));
 	mixerRenameAction.setProperty("volControl",
@@ -2512,18 +2527,35 @@ void OBSBasic::VolControlContextMenu()
 	popup.addAction(&hideAction);
 	popup.addAction(&mixerRenameAction);
 	popup.addSeparator();
+	popup.addAction(&toggleControlLayoutAction);
+	popup.addSeparator();
 	popup.addAction(&filtersAction);
 	popup.addAction(&propertiesAction);
 	popup.addAction(&advPropAction);
 	popup.exec(QCursor::pos());
 }
 
-void OBSBasic::on_mixerScrollArea_customContextMenuRequested()
+void OBSBasic::on_hMixerScrollArea_customContextMenuRequested()
+{
+	StackedMixerAreaContextMenuRequested();
+}
+
+void OBSBasic::on_vMixerScrollArea_customContextMenuRequested()
+{
+	StackedMixerAreaContextMenuRequested();
+}
+
+void OBSBasic::StackedMixerAreaContextMenuRequested()
 {
 	QAction unhideAllAction(QTStr("UnhideAll"), this);
 
 	QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this);
 
+	QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this);
+	toggleControlLayoutAction.setCheckable(true);
+	toggleControlLayoutAction.setChecked(config_get_bool(GetGlobalConfig(),
+			"BasicWindow", "VerticalVolControl"));
+
 	/* ------------------- */
 
 	connect(&unhideAllAction, &QAction::triggered,
@@ -2536,19 +2568,49 @@ void OBSBasic::on_mixerScrollArea_customContextMenuRequested()
 
 	/* ------------------- */
 
+	connect(&toggleControlLayoutAction, &QAction::changed, this,
+			&OBSBasic::ToggleVolControlLayout,
+			Qt::DirectConnection);
+
+	/* ------------------- */
+
 	QMenu popup(this);
 	popup.addAction(&unhideAllAction);
 	popup.addSeparator();
+	popup.addAction(&toggleControlLayoutAction);
+	popup.addSeparator();
 	popup.addAction(&advPropAction);
 	popup.exec(QCursor::pos());
 }
 
+void OBSBasic::ToggleVolControlLayout()
+{
+	bool vertical = !config_get_bool(GetGlobalConfig(), "BasicWindow",
+			"VerticalVolControl");
+	config_set_bool(GetGlobalConfig(), "BasicWindow", "VerticalVolControl",
+			vertical);
+	ui->stackedMixerArea->setCurrentIndex(vertical);
+
+	// We need to store it so we can delete current and then add
+	// at the right order
+	vector<OBSSource> sources;
+	for (size_t i = 0; i != volumes.size(); i++)
+		sources.emplace_back(volumes[i]->GetSource());
+
+	ClearVolumeControls();
+
+	for (const auto &source : sources)
+		ActivateAudioSource(source);
+}
+
 void OBSBasic::ActivateAudioSource(OBSSource source)
 {
 	if (SourceMixerHidden(source))
 		return;
 
-	VolControl *vol = new VolControl(source, true);
+	bool vertical = config_get_bool(GetGlobalConfig(), "BasicWindow",
+			"VerticalVolControl");
+	VolControl *vol = new VolControl(source, true, vertical);
 
 	double meterDecayRate = config_get_double(basicConfig, "Audio",
 			"MeterDecayRate");
@@ -2582,7 +2644,10 @@ void OBSBasic::ActivateAudioSource(OBSSource source)
 	InsertQObjectByName(volumes, vol);
 
 	for (auto volume : volumes) {
-		ui->volumeWidgets->layout()->addWidget(volume);
+		if (vertical)
+			ui->vVolControlLayout->addWidget(volume);
+		else
+			ui->hVolControlLayout->addWidget(volume);
 	}
 }
 

+ 5 - 1
UI/window-basic-main.hpp

@@ -258,6 +258,7 @@ private:
 	void GetAudioSourceFilters();
 	void GetAudioSourceProperties();
 	void VolControlContextMenu();
+	void ToggleVolControlLayout();
 
 	void RefreshSceneCollections();
 	void ChangeSceneCollection();
@@ -455,7 +456,8 @@ private slots:
 
 	void MixerRenameSource();
 
-	void on_mixerScrollArea_customContextMenuRequested();
+	void on_vMixerScrollArea_customContextMenuRequested();
+	void on_hMixerScrollArea_customContextMenuRequested();
 
 	void on_actionCopySource_triggered();
 	void on_actionPasteRef_triggered();
@@ -715,6 +717,8 @@ private slots:
 
 	void DeferredLoad(const QString &file, int requeueCount);
 
+	void StackedMixerAreaContextMenuRequested();
+
 public slots:
 	void on_actionResetTransform_triggered();