Bläddra i källkod

UI: Add preview scrollbars

This adds scrollbars to the preview, so users can move around the
preview without using the spacebar + clicking.

Co-Authored-By: Clayton Groeneveld <[email protected]>
Warchamp7 1 år sedan
förälder
incheckning
81fa608cde

+ 2 - 0
UI/cmake/legacy.cmake

@@ -211,6 +211,8 @@ target_sources(
           menu-button.hpp
           mute-checkbox.hpp
           noncheckable-button.hpp
+          preview-controls.cpp
+          preview-controls.hpp
           remote-text.cpp
           remote-text.hpp
           scene-tree.cpp

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

@@ -58,6 +58,8 @@ target_sources(
     menu-button.hpp
     mute-checkbox.hpp
     noncheckable-button.hpp
+    preview-controls.cpp
+    preview-controls.hpp
     remote-text.cpp
     remote-text.hpp
     scene-tree.cpp

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

@@ -785,6 +785,7 @@ Basic.MainMenu.Edit.Scale="Preview &Scaling"
 Basic.MainMenu.Edit.Scale.Window="Scale to Window"
 Basic.MainMenu.Edit.Scale.Canvas="Canvas (%1x%2)"
 Basic.MainMenu.Edit.Scale.Output="Output (%1x%2)"
+Basic.MainMenu.Edit.Scale.Manual="Scaled (%1x%2)"
 Basic.MainMenu.Edit.Transform="&Transform"
 Basic.MainMenu.Edit.Transform.EditTransform="&Edit Transform..."
 Basic.MainMenu.Edit.Transform.CopyTransform="Copy Transform"

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

@@ -105,6 +105,7 @@
 
     --font_base: calc(1pt * var(--font_base_value));
     --font_small: calc(0.9pt * var(--font_base_value));
+    --font_xsmall: calc(0.85pt * var(--font_base_value));
     --font_large: calc(1.1pt * var(--font_base_value));
     --font_xlarge: calc(1.5pt * var(--font_base_value));
 
@@ -634,6 +635,11 @@ QScrollBar::handle:horizontal {
     min-width: 32px;
 }
 
+QScrollBar::handle:disabled {
+    background: transparent;
+    border-color: transparent;
+}
+
 /* Source Context Bar */
 
 #contextContainer {
@@ -1963,3 +1969,50 @@ OBSBasicStats {
 OBSBasicAdvAudio #scrollAreaWidgetContents {
     background: var(--bg_base);
 }
+
+#previewScalePercent,
+#previewScalingMode {
+    background: transparent;
+    color: var(--text_muted);
+    font-size: var(--font_xsmall);
+    height: 14px;
+    max-height: 14px;
+    padding: 0px var(--padding_xlarge);
+    margin: 0;
+    border: none;
+    border-radius: 0;
+}
+
+#previewXContainer {
+    border: 1px solid var(--grey6);
+}
+
+#previewScalingMode {
+    border: 1px solid var(--grey6);
+}
+
+#previewScalingMode:hover,
+#previewScalingMode:focus {
+    border-color: var(--input_border_hover);
+}
+
+#previewXScrollBar,
+#previewYScrollBar {
+    background: transparent;
+    border: 1px solid var(--grey6);
+    border-radius: 0;
+}
+
+#previewXScrollBar {
+    border-left: none;
+    height: 16px;
+}
+
+#previewXScrollBar::handle,
+#previewYScrollBar::handle {
+    margin: 3px;
+}
+
+#previewYScrollBar {
+    width: 16px;
+}

+ 170 - 47
UI/forms/OBSBasic.ui

@@ -69,6 +69,21 @@
            </sizepolicy>
           </property>
           <layout class="QVBoxLayout" name="verticalLayout_7">
+           <property name="spacing">
+            <number>0</number>
+           </property>
+           <property name="leftMargin">
+            <number>1</number>
+           </property>
+           <property name="topMargin">
+            <number>1</number>
+           </property>
+           <property name="rightMargin">
+            <number>1</number>
+           </property>
+           <property name="bottomMargin">
+            <number>1</number>
+           </property>
            <item>
             <spacer name="verticalSpacer_2">
              <property name="orientation">
@@ -152,53 +167,151 @@
          </widget>
         </item>
         <item>
-         <layout class="QVBoxLayout" name="previewTextLayout">
-          <property name="spacing">
-           <number>0</number>
-          </property>
-          <item>
-           <widget class="QLabel" name="previewLabel">
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Ignored" vsizetype="Preferred">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="text">
-             <string>StudioMode.PreviewSceneName</string>
-            </property>
-            <property name="alignment">
-             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
-            </property>
-            <property name="themeID" stdset="0">
-             <string>previewProgramLabels</string>
-            </property>
-           </widget>
-          </item>
-          <item>
-           <widget class="OBSBasicPreview" name="preview" native="true">
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="minimumSize">
-             <size>
-              <width>32</width>
-              <height>32</height>
-             </size>
-            </property>
-            <property name="focusPolicy">
-             <enum>Qt::ClickFocus</enum>
-            </property>
-            <property name="contextMenuPolicy">
-             <enum>Qt::CustomContextMenu</enum>
-            </property>
-            <addaction name="actionRemoveSource"/>
-           </widget>
-          </item>
-         </layout>
+         <widget class="QWidget" name="previewContainer" native="true">
+          <layout class="QVBoxLayout" name="previewTextLayout">
+           <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>
+            <widget class="QLabel" name="previewLabel">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Ignored" vsizetype="Preferred">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="text">
+              <string>StudioMode.PreviewSceneName</string>
+             </property>
+             <property name="alignment">
+              <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+             </property>
+             <property name="themeID" stdset="0">
+              <string>previewProgramLabels</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <layout class="QGridLayout" name="gridLayout">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item row="1" column="0" colspan="2">
+              <widget class="OBSBasicPreview" name="preview" native="true">
+               <property name="sizePolicy">
+                <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+                 <horstretch>0</horstretch>
+                 <verstretch>0</verstretch>
+                </sizepolicy>
+               </property>
+               <property name="minimumSize">
+                <size>
+                 <width>32</width>
+                 <height>32</height>
+                </size>
+               </property>
+               <property name="focusPolicy">
+                <enum>Qt::ClickFocus</enum>
+               </property>
+               <property name="contextMenuPolicy">
+                <enum>Qt::CustomContextMenu</enum>
+               </property>
+               <addaction name="actionRemoveSource"/>
+              </widget>
+             </item>
+             <item row="2" column="1">
+              <widget class="QWidget" name="previewXContainer" native="true">
+               <layout class="QHBoxLayout" name="horizontalLayout_7">
+                <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>
+                 <widget class="OBSPreviewScalingLabel" name="previewScalePercent">
+                  <property name="text">
+                   <string>100%</string>
+                  </property>
+                  <property name="alignment">
+                   <set>Qt::AlignCenter</set>
+                  </property>
+                 </widget>
+                </item>
+                <item>
+                 <widget class="OBSPreviewScalingComboBox" name="previewScalingMode">
+                  <item>
+                   <property name="text">
+                    <string>Basic.MainMenu.Edit.Scale.Window</string>
+                   </property>
+                  </item>
+                  <item>
+                   <property name="text">
+                    <string>Basic.MainMenu.Edit.Scale.Canvas</string>
+                   </property>
+                  </item>
+                 </widget>
+                </item>
+                <item>
+                 <widget class="QScrollBar" name="previewXScrollBar">
+                  <property name="sizePolicy">
+                   <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+                    <horstretch>0</horstretch>
+                    <verstretch>0</verstretch>
+                   </sizepolicy>
+                  </property>
+                  <property name="minimum">
+                   <number>-200</number>
+                  </property>
+                  <property name="maximum">
+                   <number>200</number>
+                  </property>
+                  <property name="singleStep">
+                   <number>10</number>
+                  </property>
+                  <property name="pageStep">
+                   <number>100</number>
+                  </property>
+                  <property name="orientation">
+                   <enum>Qt::Horizontal</enum>
+                  </property>
+                 </widget>
+                </item>
+               </layout>
+              </widget>
+             </item>
+             <item row="1" column="2">
+              <widget class="QScrollBar" name="previewYScrollBar">
+               <property name="orientation">
+                <enum>Qt::Vertical</enum>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </item>
+          </layout>
+         </widget>
         </item>
        </layout>
       </item>
@@ -2150,6 +2263,16 @@
    <header>window-dock.hpp</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>OBSPreviewScalingLabel</class>
+   <extends>QLabel</extends>
+   <header>preview-controls.hpp</header>
+  </customwidget>
+  <customwidget>
+   <class>OBSPreviewScalingComboBox</class>
+   <extends>QComboBox</extends>
+   <header>preview-controls.hpp</header>
+  </customwidget>
  </customwidgets>
  <resources>
   <include location="obs.qrc"/>

+ 137 - 0
UI/preview-controls.cpp

@@ -0,0 +1,137 @@
+/******************************************************************************
+    Copyright (C) 2024 by Taylor Giampaolo <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include "preview-controls.hpp"
+#include <obs-app.hpp>
+
+/* Preview Scale Label */
+void OBSPreviewScalingLabel::PreviewScaleChanged(float scale)
+{
+	previewScale = scale;
+	UpdateScaleLabel();
+}
+
+void OBSPreviewScalingLabel::UpdateScaleLabel()
+{
+	float previewScalePercent = floor(100.0f * previewScale);
+	setText(QString::number(previewScalePercent) + "%");
+}
+
+/* Preview Scaling ComboBox */
+void OBSPreviewScalingComboBox::PreviewFixedScalingChanged(bool fixed)
+{
+	if (fixedScaling == fixed)
+		return;
+
+	fixedScaling = fixed;
+	UpdateSelection();
+}
+
+void OBSPreviewScalingComboBox::CanvasResized(uint32_t width, uint32_t height)
+{
+	SetCanvasSize(width, height);
+	UpdateCanvasText();
+}
+
+void OBSPreviewScalingComboBox::OutputResized(uint32_t width, uint32_t height)
+{
+	SetOutputSize(width, height);
+
+	bool canvasMatchesOutput = output_width == canvas_width &&
+				   output_height == canvas_height;
+
+	SetScaleOutputEnabled(!canvasMatchesOutput);
+	UpdateOutputText();
+}
+
+void OBSPreviewScalingComboBox::PreviewScaleChanged(float scale)
+{
+	previewScale = scale;
+
+	if (fixedScaling) {
+		UpdateSelection();
+		UpdateAllText();
+	} else {
+		UpdateScaledText();
+	}
+}
+
+void OBSPreviewScalingComboBox::SetScaleOutputEnabled(bool show)
+{
+	if (scaleOutputEnabled == show)
+		return;
+
+	scaleOutputEnabled = show;
+
+	if (scaleOutputEnabled) {
+		addItem(QTStr("Basic.MainMenu.Edit.Scale.Output"));
+	} else {
+		removeItem(2);
+	}
+}
+
+void OBSPreviewScalingComboBox::UpdateAllText()
+{
+	UpdateCanvasText();
+	UpdateOutputText();
+	UpdateScaledText();
+}
+
+void OBSPreviewScalingComboBox::UpdateCanvasText()
+{
+	QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas");
+	text = text.arg(QString::number(canvas_width),
+			QString::number(canvas_height));
+	setItemText(1, text);
+}
+
+void OBSPreviewScalingComboBox::UpdateOutputText()
+{
+	if (scaleOutputEnabled) {
+		QString text = QTStr("Basic.MainMenu.Edit.Scale.Output");
+		text = text.arg(QString::number(output_width),
+				QString::number(output_height));
+		setItemText(2, text);
+	}
+}
+
+void OBSPreviewScalingComboBox::UpdateScaledText()
+{
+	QString text = QTStr("Basic.MainMenu.Edit.Scale.Manual");
+	text = text.arg(QString::number(floor(canvas_width * previewScale)),
+			QString::number(floor(canvas_height * previewScale)));
+	setPlaceholderText(text);
+}
+
+void OBSPreviewScalingComboBox::UpdateSelection()
+{
+	QSignalBlocker sb(this);
+	float outputScale = float(output_width) / float(canvas_width);
+
+	if (!fixedScaling) {
+		setCurrentIndex(0);
+	} else {
+		if (previewScale == 1.0f) {
+			setCurrentIndex(1);
+		} else if (scaleOutputEnabled &&
+			   (previewScale == outputScale)) {
+			setCurrentIndex(2);
+		} else {
+			setCurrentIndex(-1);
+		}
+	}
+}

+ 81 - 0
UI/preview-controls.hpp

@@ -0,0 +1,81 @@
+/******************************************************************************
+    Copyright (C) 2024 by Taylor Giampaolo <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#pragma once
+
+#include <QLabel>
+#include <QComboBox>
+
+class OBSPreviewScalingLabel : public QLabel {
+	Q_OBJECT
+
+public:
+	OBSPreviewScalingLabel(QWidget *parent = nullptr) : QLabel(parent) {}
+
+public slots:
+	void PreviewScaleChanged(float scale);
+
+private:
+	float previewScale = 0.0f;
+	void UpdateScaleLabel();
+};
+
+class OBSPreviewScalingComboBox : public QComboBox {
+	Q_OBJECT
+
+public:
+	OBSPreviewScalingComboBox(QWidget *parent = nullptr) : QComboBox(parent)
+	{
+	}
+
+	inline void SetCanvasSize(uint32_t width, uint32_t height)
+	{
+		canvas_width = width;
+		canvas_height = height;
+	};
+	inline void SetOutputSize(uint32_t width, uint32_t height)
+	{
+		output_width = width;
+		output_height = height;
+	};
+	void UpdateAllText();
+
+public slots:
+	void PreviewScaleChanged(float scale);
+	void PreviewFixedScalingChanged(bool fixed);
+	void CanvasResized(uint32_t width, uint32_t height);
+	void OutputResized(uint32_t width, uint32_t height);
+
+private:
+	uint32_t canvas_width = 0;
+	uint32_t canvas_height = 0;
+
+	uint32_t output_width = 0;
+	uint32_t output_height = 0;
+
+	float previewScale = 0.0f;
+
+	bool fixedScaling = false;
+
+	bool scaleOutputEnabled = false;
+	void SetScaleOutputEnabled(bool show);
+
+	void UpdateCanvasText();
+	void UpdateOutputText();
+	void UpdateScaledText();
+	void UpdateSelection();
+};

+ 73 - 4
UI/window-basic-main.cpp

@@ -464,6 +464,7 @@ OBSBasic::OBSBasic(QWidget *parent)
 			ResizePreview(ovi.base_width, ovi.base_height);
 
 		UpdateContextBarVisibility();
+		UpdatePreviewScrollbars();
 		dpi = devicePixelRatioF();
 	};
 	dpi = devicePixelRatioF();
@@ -471,6 +472,30 @@ OBSBasic::OBSBasic(QWidget *parent)
 	connect(windowHandle(), &QWindow::screenChanged, displayResize);
 	connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize);
 
+	/* TODO: Move these into window-basic-preview */
+	/* Preview Scaling label */
+	connect(ui->preview, &OBSBasicPreview::scalingChanged,
+		ui->previewScalePercent,
+		&OBSPreviewScalingLabel::PreviewScaleChanged);
+
+	/* Preview Scaling dropdown */
+	connect(ui->preview, &OBSBasicPreview::scalingChanged,
+		ui->previewScalingMode,
+		&OBSPreviewScalingComboBox::PreviewScaleChanged);
+
+	connect(ui->preview, &OBSBasicPreview::fixedScalingChanged,
+		ui->previewScalingMode,
+		&OBSPreviewScalingComboBox::PreviewFixedScalingChanged);
+
+	connect(ui->previewScalingMode,
+		&OBSPreviewScalingComboBox::currentIndexChanged, this,
+		&OBSBasic::PreviewScalingModeChanged);
+
+	connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode,
+		&OBSPreviewScalingComboBox::CanvasResized);
+	connect(this, &OBSBasic::OutputResized, ui->previewScalingMode,
+		&OBSPreviewScalingComboBox::OutputResized);
+
 	delete shortcutFilter;
 	shortcutFilter = CreateShortcutFilter();
 	installEventFilter(shortcutFilter);
@@ -1386,6 +1411,7 @@ retryScene:
 		ui->preview->SetScrollingOffset(scrollOffX, scrollOffY);
 	}
 	ui->preview->SetFixedScaling(fixedScaling);
+
 	emit ui->preview->DisplayResized();
 
 	if (vcamEnabled) {
@@ -2176,6 +2202,7 @@ void OBSBasic::OBSInit()
 
 	InitOBSCallbacks();
 	InitHotkeys();
+	ui->preview->Init();
 
 	/* hack to prevent elgato from loading its own QtNetwork that it tries
 	 * to ship with */
@@ -4924,6 +4951,9 @@ int OBSBasic::ResetVideo()
 		obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level);
 		OBSBasicStats::InitializeValues();
 		OBSProjector::UpdateMultiviewProjectors();
+
+		emit CanvasResized(ovi.base_width, ovi.base_height);
+		emit OutputResized(ovi.output_width, ovi.output_height);
 	}
 
 	return ret;
@@ -5013,8 +5043,10 @@ void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
 	obs_get_video_info(&ovi);
 
 	if (isFixedScaling) {
-		ui->preview->ClampScrollingOffsets();
 		previewScale = ui->preview->GetScalingAmount();
+
+		ui->preview->ClampScrollingOffsets();
+
 		GetCenterPosFromFixedScale(
 			int(cx), int(cy),
 			targetSize.width() - PREVIEW_EDGE_SIZE * 2,
@@ -5031,6 +5063,8 @@ void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
 				     previewX, previewY, previewScale);
 	}
 
+	ui->preview->SetScalingAmount(previewScale);
+
 	previewX += float(PREVIEW_EDGE_SIZE);
 	previewY += float(PREVIEW_EDGE_SIZE);
 }
@@ -9169,7 +9203,7 @@ void OBSBasic::on_actionHorizontalCenter_triggered()
 void OBSBasic::EnablePreviewDisplay(bool enable)
 {
 	obs_display_set_enabled(ui->preview->GetDisplay(), enable);
-	ui->preview->setVisible(enable);
+	ui->previewContainer->setVisible(enable);
 	ui->previewDisabledWidget->setVisible(!enable);
 }
 
@@ -9765,6 +9799,7 @@ void OBSBasic::on_actionScaleWindow_triggered()
 {
 	ui->preview->SetFixedScaling(false);
 	ui->preview->ResetScrollingOffset();
+
 	emit ui->preview->DisplayResized();
 }
 
@@ -9772,6 +9807,7 @@ void OBSBasic::on_actionScaleCanvas_triggered()
 {
 	ui->preview->SetFixedScaling(true);
 	ui->preview->SetScalingLevel(0);
+
 	emit ui->preview->DisplayResized();
 }
 
@@ -9785,8 +9821,8 @@ void OBSBasic::on_actionScaleOutput_triggered()
 	// log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY)
 	int32_t approxScalingLevel =
 		int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY)));
-	ui->preview->SetScalingLevel(approxScalingLevel);
-	ui->preview->SetScalingAmount(scalingAmount);
+	ui->preview->SetScalingLevelAndAmount(approxScalingLevel,
+					      scalingAmount);
 	emit ui->preview->DisplayResized();
 }
 
@@ -11093,3 +11129,36 @@ void OBSBasic::OnEvent(enum obs_frontend_event event)
 	if (api)
 		api->on_event(event);
 }
+
+void OBSBasic::UpdatePreviewScrollbars()
+{
+	if (!ui->preview->IsFixedScaling()) {
+		ui->previewXScrollBar->setRange(0, 0);
+		ui->previewYScrollBar->setRange(0, 0);
+	}
+}
+
+void OBSBasic::on_previewXScrollBar_valueChanged(int value)
+{
+	emit PreviewXScrollBarMoved(value);
+}
+
+void OBSBasic::on_previewYScrollBar_valueChanged(int value)
+{
+	emit PreviewYScrollBarMoved(value);
+}
+
+void OBSBasic::PreviewScalingModeChanged(int value)
+{
+	switch (value) {
+	case 0:
+		on_actionScaleWindow_triggered();
+		break;
+	case 1:
+		on_actionScaleCanvas_triggered();
+		break;
+	case 2:
+		on_actionScaleOutput_triggered();
+		break;
+	};
+}

+ 12 - 0
UI/window-basic-main.hpp

@@ -670,6 +670,7 @@ private:
 	std::string lastReplay;
 
 	void UpdatePreviewOverflowSettings();
+	void UpdatePreviewScrollbars();
 
 	bool streamingStarting = false;
 
@@ -810,6 +811,11 @@ private slots:
 	void AudioMixerPasteFilters();
 	void SourcePasteFilters(OBSSource source, OBSSource dstSource);
 
+	void on_previewXScrollBar_valueChanged(int value);
+	void on_previewYScrollBar_valueChanged(int value);
+
+	void PreviewScalingModeChanged(int value);
+
 	void ColorChange();
 
 	SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem);
@@ -1292,6 +1298,12 @@ signals:
 
 	/* Studio Mode signal */
 	void PreviewProgramModeChanged(bool enabled);
+	void CanvasResized(uint32_t width, uint32_t height);
+	void OutputResized(uint32_t width, uint32_t height);
+
+	/* Preview signals */
+	void PreviewXScrollBarMoved(int value);
+	void PreviewYScrollBarMoved(int value);
 
 private:
 	std::unique_ptr<Ui::OBSBasic> ui;

+ 84 - 0
UI/window-basic-preview.cpp

@@ -39,6 +39,15 @@ OBSBasicPreview::~OBSBasicPreview()
 	obs_leave_graphics();
 }
 
+void OBSBasicPreview::Init()
+{
+	OBSBasic *main = OBSBasic::Get();
+	connect(main, &OBSBasic::PreviewXScrollBarMoved, this,
+		&OBSBasicPreview::XScrollBarMoved);
+	connect(main, &OBSBasic::PreviewYScrollBarMoved, this,
+		&OBSBasicPreview::YScrollBarMoved);
+}
+
 vec2 OBSBasicPreview::GetMouseEventPos(QMouseEvent *event)
 {
 	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
@@ -2327,7 +2336,21 @@ void OBSBasicPreview::SetScalingAmount(float newScalingAmountVal)
 {
 	scrollingOffset.x *= newScalingAmountVal / scalingAmount;
 	scrollingOffset.y *= newScalingAmountVal / scalingAmount;
+
+	if (scalingAmount == newScalingAmountVal)
+		return;
+
 	scalingAmount = newScalingAmountVal;
+	emit scalingChanged(scalingAmount);
+}
+
+void OBSBasicPreview::SetScalingLevelAndAmount(int32_t newScalingLevelVal,
+					       float newScalingAmountVal)
+{
+	newScalingLevelVal = std::clamp(newScalingLevelVal, -MAX_SCALING_LEVEL,
+					MAX_SCALING_LEVEL);
+	scalingLevel = newScalingLevelVal;
+	SetScalingAmount(newScalingAmountVal);
 }
 
 OBSBasicPreview *OBSBasicPreview::Get()
@@ -2689,4 +2712,65 @@ void OBSBasicPreview::ClampScrollingOffsets()
 
 	scrollingOffset.x = std::clamp(scrollingOffset.x, -offset.x, offset.x);
 	scrollingOffset.y = std::clamp(scrollingOffset.y, -offset.y, offset.y);
+
+	UpdateXScrollBar(offset.x);
+	UpdateYScrollBar(offset.y);
+}
+
+void OBSBasicPreview::XScrollBarMoved(int value)
+{
+	updatingXScrollBar = true;
+	scrollingOffset.x = float(-value);
+
+	emit DisplayResized();
+	updatingXScrollBar = false;
+}
+
+void OBSBasicPreview::YScrollBarMoved(int value)
+{
+	updatingYScrollBar = true;
+	scrollingOffset.y = float(-value);
+
+	emit DisplayResized();
+	updatingYScrollBar = false;
+}
+
+void OBSBasicPreview::UpdateXScrollBar(float cx)
+{
+	if (updatingXScrollBar)
+		return;
+
+	OBSBasic *main = OBSBasic::Get();
+
+	if (!main->ui->previewXScrollBar->isVisible())
+		return;
+
+	main->ui->previewXScrollBar->setRange(int(-cx), int(cx));
+
+	QSize targetSize = GetPixelSize(this);
+	main->ui->previewXScrollBar->setPageStep(targetSize.width() /
+						 std::min(scalingAmount, 1.0f));
+
+	QSignalBlocker sig(main->ui->previewXScrollBar);
+	main->ui->previewXScrollBar->setValue(int(-scrollingOffset.x));
+}
+
+void OBSBasicPreview::UpdateYScrollBar(float cy)
+{
+	if (updatingYScrollBar)
+		return;
+
+	OBSBasic *main = OBSBasic::Get();
+
+	if (!main->ui->previewYScrollBar->isVisible())
+		return;
+
+	main->ui->previewYScrollBar->setRange(int(-cy), int(cy));
+
+	QSize targetSize = GetPixelSize(this);
+	main->ui->previewYScrollBar->setPageStep(targetSize.height() /
+						 std::min(scalingAmount, 1.0f));
+
+	QSignalBlocker sig(main->ui->previewYScrollBar);
+	main->ui->previewYScrollBar->setValue(int(-scrollingOffset.y));
 }

+ 23 - 2
UI/window-basic-preview.hpp

@@ -8,6 +8,7 @@
 #include <vector>
 #include "qt-display.hpp"
 #include "obs-app.hpp"
+#include "preview-controls.hpp"
 
 class OBSBasic;
 class QMouseEvent;
@@ -18,8 +19,8 @@ class QMouseEvent;
 #define ITEM_BOTTOM (1 << 3)
 #define ITEM_ROT (1 << 4)
 
-#define MAX_SCALING_LEVEL 20
-#define MAX_SCALING_AMOUNT 10.0f
+#define MAX_SCALING_LEVEL 32
+#define MAX_SCALING_AMOUNT 8.0f
 #define ZOOM_SENSITIVITY pow(MAX_SCALING_AMOUNT, 1.0f / MAX_SCALING_LEVEL)
 
 #define SPACER_LABEL_MARGIN 6.0f
@@ -81,6 +82,8 @@ private:
 	int32_t scalingLevel = 0;
 	float scalingAmount = 1.0f;
 	float groupRot = 0.0f;
+	bool updatingXScrollBar = false;
+	bool updatingYScrollBar = false;
 
 	std::vector<obs_sceneitem_t *> hoveredPreviewItems;
 	std::vector<obs_sceneitem_t *> selectedItems;
@@ -124,11 +127,17 @@ private:
 	OBSDataAutoRelease wrapper = nullptr;
 	bool changed;
 
+private slots:
+	void XScrollBarMoved(int value);
+	void YScrollBarMoved(int value);
+
 public:
 	OBSBasicPreview(QWidget *parent,
 			Qt::WindowFlags flags = Qt::WindowFlags());
 	~OBSBasicPreview();
 
+	void Init();
+
 	static OBSBasicPreview *Get();
 
 	virtual void keyPressEvent(QKeyEvent *event) override;
@@ -150,12 +159,18 @@ public:
 
 	inline void SetFixedScaling(bool newFixedScalingVal)
 	{
+		if (fixedScaling == newFixedScalingVal)
+			return;
+
 		fixedScaling = newFixedScalingVal;
+		emit fixedScalingChanged(fixedScaling);
 	}
 	inline bool IsFixedScaling() const { return fixedScaling; }
 
 	void SetScalingLevel(int32_t newScalingLevelVal);
 	void SetScalingAmount(float newScalingAmountVal);
+	void SetScalingLevelAndAmount(int32_t newScalingLevelVal,
+				      float newScalingAmountVal);
 	inline int32_t GetScalingLevel() const { return scalingLevel; }
 	inline float GetScalingAmount() const { return scalingAmount; }
 
@@ -198,4 +213,10 @@ public:
 
 	void DrawSpacingHelpers();
 	void ClampScrollingOffsets();
+	void UpdateXScrollBar(float cx);
+	void UpdateYScrollBar(float cy);
+
+signals:
+	void scalingChanged(float scalingAmount);
+	void fixedScalingChanged(bool isFixed);
 };