Browse Source

UI: Add preview scaling options

Adds preview scaling to the right-click context menu for the preview
pane.  This allows the ability to, for example, zoom the preview and
edit the preview 1:1 (canvas/base resolution).  This was a missing
parity feature.  Additionally, also allows scaling to the
"output/scaled" resolution the program is set to.

When the preview is in scale mode and is the focused widget, you can
hold space and drag with left click to change the zoomed position.

(Notes added by Jim)

Closes jp9000/obs-studio#687
Joseph El-Khouri 9 years ago
parent
commit
3b616823a1

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

@@ -330,6 +330,10 @@ Basic.MainMenu.Edit.Redo="&Redo"
 Basic.MainMenu.Edit.UndoAction="&Undo $1"
 Basic.MainMenu.Edit.UndoAction="&Undo $1"
 Basic.MainMenu.Edit.RedoAction="&Redo $1"
 Basic.MainMenu.Edit.RedoAction="&Redo $1"
 Basic.MainMenu.Edit.LockPreview="&Lock Preview"
 Basic.MainMenu.Edit.LockPreview="&Lock Preview"
+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.Transform="&Transform"
 Basic.MainMenu.Edit.Transform="&Transform"
 Basic.MainMenu.Edit.Transform.EditTransform="&Edit Transform..."
 Basic.MainMenu.Edit.Transform.EditTransform="&Edit Transform..."
 Basic.MainMenu.Edit.Transform.ResetTransform="&Reset Transform"
 Basic.MainMenu.Edit.Transform.ResetTransform="&Reset Transform"

+ 8 - 0
UI/display-helpers.hpp

@@ -41,6 +41,14 @@ static inline void GetScaleAndCenterPos(
 	y = windowCY/2 - newCY/2;
 	y = windowCY/2 - newCY/2;
 }
 }
 
 
+static inline void GetCenterPosFromFixedScale(
+		int baseCX, int baseCY, int windowCX, int windowCY,
+		int &x, int &y, float scale)
+{
+	x = (float(windowCX) - float(baseCX)*scale) / 2.0f;
+	y = (float(windowCY) - float(baseCY)*scale) / 2.0f;
+}
+
 static inline QSize GetPixelSize(QWidget *widget)
 static inline QSize GetPixelSize(QWidget *widget)
 {
 {
 	return widget->size() * widget->devicePixelRatio();
 	return widget->size() * widget->devicePixelRatio();

+ 33 - 0
UI/forms/OBSBasic.ui

@@ -873,8 +873,17 @@
      <addaction name="actionMoveToTop"/>
      <addaction name="actionMoveToTop"/>
      <addaction name="actionMoveToBottom"/>
      <addaction name="actionMoveToBottom"/>
     </widget>
     </widget>
+    <widget class="QMenu" name="scalingMenu">
+     <property name="title">
+      <string>Basic.MainMenu.Edit.Scale</string>
+     </property>
+     <addaction name="actionScaleWindow"/>
+     <addaction name="actionScaleCanvas"/>
+     <addaction name="actionScaleOutput"/>
+    </widget>
     <addaction name="transformMenu"/>
     <addaction name="transformMenu"/>
     <addaction name="orderMenu"/>
     <addaction name="orderMenu"/>
+    <addaction name="scalingMenu"/>
     <addaction name="actionLockPreview"/>
     <addaction name="actionLockPreview"/>
     <addaction name="separator"/>
     <addaction name="separator"/>
     <addaction name="actionAdvAudioProperties"/>
     <addaction name="actionAdvAudioProperties"/>
@@ -1328,6 +1337,30 @@
     <string>Basic.MainMenu.Edit.LockPreview</string>
     <string>Basic.MainMenu.Edit.LockPreview</string>
    </property>
    </property>
   </action>
   </action>
+  <action name="actionScaleWindow">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Scale.Window</string>
+   </property>
+  </action>
+  <action name="actionScaleCanvas">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Scale.Canvas</string>
+   </property>
+  </action>
+  <action name="actionScaleOutput">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Scale.Output</string>
+   </property>
+  </action>
  </widget>
  </widget>
  <customwidgets>
  <customwidgets>
   <customwidget>
   <customwidget>

+ 97 - 9
UI/window-basic-main.cpp

@@ -375,6 +375,8 @@ void OBSBasic::Save(const char *file)
 			scene, curProgramScene);
 			scene, curProgramScene);
 
 
 	obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
 	obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
+	obs_data_set_int(saveData, "scaling_mode",
+			static_cast<uint32_t>(ui->preview->GetScalingMode()));
 
 
 	if (api) {
 	if (api) {
 		obs_data_t *moduleObj = obs_data_create();
 		obs_data_t *moduleObj = obs_data_create();
@@ -665,6 +667,19 @@ retryScene:
 	ui->preview->SetLocked(previewLocked);
 	ui->preview->SetLocked(previewLocked);
 	ui->actionLockPreview->setChecked(previewLocked);
 	ui->actionLockPreview->setChecked(previewLocked);
 
 
+	ScalingMode previewScaling = static_cast<ScalingMode>(
+			obs_data_get_int(data, "scaling_mode"));
+	switch (previewScaling) {
+	case ScalingMode::Window:
+	case ScalingMode::Canvas:
+	case ScalingMode::Output:
+		break;
+	default:
+		previewScaling = ScalingMode::Window;
+	}
+
+	ui->preview->SetScaling(previewScaling);
+
 	if (api) {
 	if (api) {
 		obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
 		obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
 		api->on_load(modulesObj);
 		api->on_load(modulesObj);
@@ -1567,6 +1582,17 @@ OBSSceneItem OBSBasic::GetCurrentSceneItem()
 	return GetSceneItem(GetTopSelectedSourceItem());
 	return GetSceneItem(GetTopSelectedSourceItem());
 }
 }
 
 
+void OBSBasic::UpdatePreviewScalingMenu()
+{
+	ScalingMode scalingMode = ui->preview->GetScalingMode();
+	ui->actionScaleWindow->setChecked(
+			scalingMode == ScalingMode::Window);
+	ui->actionScaleCanvas->setChecked(
+			scalingMode == ScalingMode::Canvas);
+	ui->actionScaleOutput->setChecked(
+			scalingMode == ScalingMode::Output);
+}
+
 void OBSBasic::UpdateSources(OBSScene scene)
 void OBSBasic::UpdateSources(OBSScene scene)
 {
 {
 	ClearListItems(ui->sources);
 	ClearListItems(ui->sources);
@@ -2570,13 +2596,39 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId,
 void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
 void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
 {
 {
 	QSize  targetSize;
 	QSize  targetSize;
+	ScalingMode scalingMode;
+	obs_video_info ovi;
 
 
 	/* resize preview panel to fix to the top section of the window */
 	/* resize preview panel to fix to the top section of the window */
 	targetSize = GetPixelSize(ui->preview);
 	targetSize = GetPixelSize(ui->preview);
-	GetScaleAndCenterPos(int(cx), int(cy),
-			targetSize.width()  - PREVIEW_EDGE_SIZE * 2,
-			targetSize.height() - PREVIEW_EDGE_SIZE * 2,
-			previewX, previewY, previewScale);
+
+	scalingMode = ui->preview->GetScalingMode();
+	obs_get_video_info(&ovi);
+
+	if (scalingMode == ScalingMode::Canvas) {
+		previewScale = 1.0f;
+		GetCenterPosFromFixedScale(int(cx), int(cy),
+				targetSize.width() - PREVIEW_EDGE_SIZE * 2,
+				targetSize.height() - PREVIEW_EDGE_SIZE * 2,
+				previewX, previewY, previewScale);
+		previewX += ui->preview->ScrollX();
+		previewY += ui->preview->ScrollY();
+
+	} else if (scalingMode == ScalingMode::Output) {
+		previewScale = float(ovi.output_width) / float(ovi.base_width);
+		GetCenterPosFromFixedScale(int(cx), int(cy),
+				targetSize.width() - PREVIEW_EDGE_SIZE * 2,
+				targetSize.height() - PREVIEW_EDGE_SIZE * 2,
+				previewX, previewY, previewScale);
+		previewX += ui->preview->ScrollX();
+		previewY += ui->preview->ScrollY();
+
+	} else {
+		GetScaleAndCenterPos(int(cx), int(cy),
+				targetSize.width() - PREVIEW_EDGE_SIZE * 2,
+				targetSize.height() - PREVIEW_EDGE_SIZE * 2,
+				previewX, previewY, previewScale);
+	}
 
 
 	previewX += float(PREVIEW_EDGE_SIZE);
 	previewX += float(PREVIEW_EDGE_SIZE);
 	previewY += float(PREVIEW_EDGE_SIZE);
 	previewY += float(PREVIEW_EDGE_SIZE);
@@ -3083,11 +3135,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
 		if (IsPreviewProgramMode())
 		if (IsPreviewProgramMode())
 			action->setEnabled(false);
 			action->setEnabled(false);
 
 
-		action = popup.addAction(
-				QTStr("Basic.MainMenu.Edit.LockPreview"),
-				this, SLOT(on_actionLockPreview_triggered()));
-		action->setCheckable(true);
-		action->setChecked(ui->preview->Locked());
+		popup.addAction(ui->actionLockPreview);
+		popup.addMenu(ui->scalingMenu);
 
 
 		previewProjector = new QMenu(QTStr("PreviewProjector"));
 		previewProjector = new QMenu(QTStr("PreviewProjector"));
 		AddProjectorMenuMonitors(previewProjector, this,
 		AddProjectorMenuMonitors(previewProjector, this,
@@ -4565,6 +4614,45 @@ void OBSBasic::on_actionLockPreview_triggered()
 	ui->actionLockPreview->setChecked(ui->preview->Locked());
 	ui->actionLockPreview->setChecked(ui->preview->Locked());
 }
 }
 
 
+void OBSBasic::on_scalingMenu_aboutToShow()
+{
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	QAction *action = ui->actionScaleCanvas;
+	QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas");
+	text = text.arg(QString::number(ovi.base_width),
+			QString::number(ovi.base_height));
+	action->setText(text);
+
+	action = ui->actionScaleOutput;
+	text = QTStr("Basic.MainMenu.Edit.Scale.Output");
+	text = text.arg(QString::number(ovi.output_width),
+			QString::number(ovi.output_height));
+	action->setText(text);
+
+	UpdatePreviewScalingMenu();
+}
+
+void OBSBasic::on_actionScaleWindow_triggered()
+{
+	ui->preview->SetScaling(ScalingMode::Window);
+	ui->preview->ResetScrollingOffset();
+	emit ui->preview->DisplayResized();
+}
+
+void OBSBasic::on_actionScaleCanvas_triggered()
+{
+	ui->preview->SetScaling(ScalingMode::Canvas);
+	emit ui->preview->DisplayResized();
+}
+
+void OBSBasic::on_actionScaleOutput_triggered()
+{
+	ui->preview->SetScaling(ScalingMode::Output);
+	emit ui->preview->DisplayResized();
+}
+
 void OBSBasic::SetShowing(bool showing)
 void OBSBasic::SetShowing(bool showing)
 {
 {
 	if (!showing && isVisible()) {
 	if (!showing && isVisible()) {

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

@@ -200,6 +200,8 @@ private:
 	void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const;
 	void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const;
 	void GetConfigFPS(uint32_t &num, uint32_t &den) const;
 	void GetConfigFPS(uint32_t &num, uint32_t &den) const;
 
 
+	void UpdatePreviewScalingMenu();
+
 	void UpdateSources(OBSScene scene);
 	void UpdateSources(OBSScene scene);
 	void InsertSceneItem(obs_sceneitem_t *item);
 	void InsertSceneItem(obs_sceneitem_t *item);
 
 
@@ -514,6 +516,11 @@ private slots:
 
 
 	void on_actionLockPreview_triggered();
 	void on_actionLockPreview_triggered();
 
 
+	void on_scalingMenu_aboutToShow();
+	void on_actionScaleWindow_triggered();
+	void on_actionScaleCanvas_triggered();
+	void on_actionScaleOutput_triggered();
+
 	void on_streamButton_clicked();
 	void on_streamButton_clicked();
 	void on_recordButton_clicked();
 	void on_recordButton_clicked();
 	void on_settingsButton_clicked();
 	void on_settingsButton_clicked();

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

@@ -17,6 +17,7 @@
 OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags)
 OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags)
 	: OBSQTDisplay(parent, flags)
 	: OBSQTDisplay(parent, flags)
 {
 {
+	ResetScrollingOffset();
 	setMouseTracking(true);
 	setMouseTracking(true);
 }
 }
 
 
@@ -377,6 +378,42 @@ void OBSBasicPreview::GetStretchHandleData(const vec2 &pos)
 	}
 	}
 }
 }
 
 
+void OBSBasicPreview::keyPressEvent(QKeyEvent *event)
+{
+	if (locked ||
+	    GetScalingMode() == ScalingMode::Window ||
+	    event->isAutoRepeat()) {
+		OBSQTDisplay::keyPressEvent(event);
+		return;
+	}
+
+	switch (event->key()) {
+	case Qt::Key_Space:
+		setCursor(Qt::OpenHandCursor);
+		scrollMode = true;
+		break;
+	}
+
+	OBSQTDisplay::keyPressEvent(event);
+}
+
+void OBSBasicPreview::keyReleaseEvent(QKeyEvent *event)
+{
+	if (event->isAutoRepeat()) {
+		OBSQTDisplay::keyReleaseEvent(event);
+		return;
+	}
+
+	switch (event->key()) {
+	case Qt::Key_Space:
+		scrollMode = false;
+		setCursor(Qt::ArrowCursor);
+		break;
+	}
+
+	OBSQTDisplay::keyReleaseEvent(event);
+}
+
 void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
 void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
 {
 {
 	if (locked) {
 	if (locked) {
@@ -393,6 +430,13 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
 
 
 	OBSQTDisplay::mousePressEvent(event);
 	OBSQTDisplay::mousePressEvent(event);
 
 
+	if (scrollMode && GetScalingMode() != ScalingMode::Window) {
+		setCursor(Qt::ClosedHandCursor);
+		scrollingFrom.x = event->x();
+		scrollingFrom.y = event->y();
+		return;
+	}
+
 	if (event->button() != Qt::LeftButton &&
 	if (event->button() != Qt::LeftButton &&
 	    event->button() != Qt::RightButton)
 	    event->button() != Qt::RightButton)
 		return;
 		return;
@@ -461,6 +505,9 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
 		return;
 		return;
 	}
 	}
 
 
+	if (scrollMode)
+		setCursor(Qt::OpenHandCursor);
+
 	if (mouseDown) {
 	if (mouseDown) {
 		vec2 pos = GetMouseEventPos(event);
 		vec2 pos = GetMouseEventPos(event);
 
 
@@ -954,6 +1001,15 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
 	if (locked)
 	if (locked)
 		return;
 		return;
 
 
+	if (scrollMode && event->buttons() == Qt::LeftButton) {
+		scrollingOffset.x += event->x() - scrollingFrom.x;
+		scrollingOffset.y += event->y() - scrollingFrom.y;
+		scrollingFrom.x = event->x();
+		scrollingFrom.y = event->y();
+		emit DisplayResized();
+		return;
+	}
+
 	if (mouseDown) {
 	if (mouseDown) {
 		vec2 pos = GetMouseEventPos(event);
 		vec2 pos = GetMouseEventPos(event);
 
 
@@ -1113,3 +1169,8 @@ void OBSBasicPreview::DrawSceneEditing()
 	gs_technique_end_pass(tech);
 	gs_technique_end_pass(tech);
 	gs_technique_end(tech);
 	gs_technique_end(tech);
 }
 }
+
+void OBSBasicPreview::ResetScrollingOffset()
+{
+	vec2_zero(&scrollingOffset);
+}

+ 20 - 0
UI/window-basic-preview.hpp

@@ -26,6 +26,12 @@ enum class ItemHandle : uint32_t {
 	BottomRight  = ITEM_BOTTOM | ITEM_RIGHT
 	BottomRight  = ITEM_BOTTOM | ITEM_RIGHT
 };
 };
 
 
+enum class ScalingMode : uint32_t {
+	Window = 0,
+	Canvas = 1,
+	Output = 2
+};
+
 class OBSBasicPreview : public OBSQTDisplay {
 class OBSBasicPreview : public OBSQTDisplay {
 	Q_OBJECT
 	Q_OBJECT
 
 
@@ -35,17 +41,21 @@ private:
 	vec2         cropSize;
 	vec2         cropSize;
 	OBSSceneItem stretchItem;
 	OBSSceneItem stretchItem;
 	ItemHandle   stretchHandle = ItemHandle::None;
 	ItemHandle   stretchHandle = ItemHandle::None;
+	ScalingMode  scale = ScalingMode::Window;
 	vec2         stretchItemSize;
 	vec2         stretchItemSize;
 	matrix4      screenToItem;
 	matrix4      screenToItem;
 	matrix4      itemToScreen;
 	matrix4      itemToScreen;
 
 
 	vec2         startPos;
 	vec2         startPos;
 	vec2         lastMoveOffset;
 	vec2         lastMoveOffset;
+	vec2         scrollingFrom;
+	vec2         scrollingOffset;
 	bool         mouseDown      = false;
 	bool         mouseDown      = false;
 	bool         mouseMoved     = false;
 	bool         mouseMoved     = false;
 	bool         mouseOverItems = false;
 	bool         mouseOverItems = false;
 	bool         cropping       = false;
 	bool         cropping       = false;
 	bool         locked         = false;
 	bool         locked         = false;
+	bool         scrollMode     = false;
 
 
 	static vec2 GetMouseEventPos(QMouseEvent *event);
 	static vec2 GetMouseEventPos(QMouseEvent *event);
 	static bool DrawSelectedItem(obs_scene_t *scene, obs_sceneitem_t *item,
 	static bool DrawSelectedItem(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -75,16 +85,26 @@ private:
 public:
 public:
 	OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags = 0);
 	OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags = 0);
 
 
+	virtual void keyPressEvent(QKeyEvent *event) override;
+	virtual void keyReleaseEvent(QKeyEvent *event) override;
+
 	virtual void mousePressEvent(QMouseEvent *event) override;
 	virtual void mousePressEvent(QMouseEvent *event) override;
 	virtual void mouseReleaseEvent(QMouseEvent *event) override;
 	virtual void mouseReleaseEvent(QMouseEvent *event) override;
 	virtual void mouseMoveEvent(QMouseEvent *event) override;
 	virtual void mouseMoveEvent(QMouseEvent *event) override;
 
 
 	void DrawSceneEditing();
 	void DrawSceneEditing();
+	void ResetScrollingOffset();
 
 
 	inline void SetLocked(bool newLockedVal) {locked = newLockedVal;}
 	inline void SetLocked(bool newLockedVal) {locked = newLockedVal;}
 	inline void ToggleLocked() {locked = !locked;}
 	inline void ToggleLocked() {locked = !locked;}
 	inline bool Locked() const {return locked;}
 	inline bool Locked() const {return locked;}
 
 
+	inline void SetScaling(ScalingMode newScaledVal) {scale = newScaledVal;}
+	inline ScalingMode GetScalingMode() const {return scale;}
+
+	inline float ScrollX() const {return scrollingOffset.x;}
+	inline float ScrollY() const {return scrollingOffset.y;}
+
 	/* use libobs allocator for alignment because the matrices itemToScreen
 	/* use libobs allocator for alignment because the matrices itemToScreen
 	 * and screenToItem may contain SSE data, which will cause SSE
 	 * and screenToItem may contain SSE data, which will cause SSE
 	 * instructions to crash if the data is not aligned to at least a 16
 	 * instructions to crash if the data is not aligned to at least a 16