diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 24feab0dc2fc69334e7965a0a5e73f8571538e6a..86b94374a521232126692602c45b4c4b8b1e02e6 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -330,6 +330,10 @@ Basic.MainMenu.Edit.Redo="&Redo"
Basic.MainMenu.Edit.UndoAction="&Undo $1"
Basic.MainMenu.Edit.RedoAction="&Redo $1"
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.EditTransform="&Edit Transform..."
Basic.MainMenu.Edit.Transform.ResetTransform="&Reset Transform"
diff --git a/UI/display-helpers.hpp b/UI/display-helpers.hpp
index d175932f3f90206d3c58e2ef5c25ffcf13d69903..27ef174ec297dce5d0069e06b6ea6ca90a242f4e 100644
--- a/UI/display-helpers.hpp
+++ b/UI/display-helpers.hpp
@@ -41,6 +41,14 @@ static inline void GetScaleAndCenterPos(
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)
{
return widget->size() * widget->devicePixelRatio();
diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui
index 5b922bcf8e17d9fd5492713027f6a3a3edefe0b9..041aa58970fd539f8c2c4fa71b0407cfa03ab9e8 100644
--- a/UI/forms/OBSBasic.ui
+++ b/UI/forms/OBSBasic.ui
@@ -873,8 +873,17 @@
+
+
@@ -1328,6 +1337,30 @@
Basic.MainMenu.Edit.LockPreview
+
+
+ true
+
+
+ Basic.MainMenu.Edit.Scale.Window
+
+
+
+
+ true
+
+
+ Basic.MainMenu.Edit.Scale.Canvas
+
+
+
+
+ true
+
+
+ Basic.MainMenu.Edit.Scale.Output
+
+
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index 01c2ae3c06d0083a496ce52daa0f47c1471c818b..9580e9b68282c8147b77ce2c013f4c7b99f0548b 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -375,6 +375,8 @@ void OBSBasic::Save(const char *file)
scene, curProgramScene);
obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
+ obs_data_set_int(saveData, "scaling_mode",
+ static_cast(ui->preview->GetScalingMode()));
if (api) {
obs_data_t *moduleObj = obs_data_create();
@@ -665,6 +667,19 @@ retryScene:
ui->preview->SetLocked(previewLocked);
ui->actionLockPreview->setChecked(previewLocked);
+ ScalingMode previewScaling = static_cast(
+ 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) {
obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
api->on_load(modulesObj);
@@ -1567,6 +1582,17 @@ OBSSceneItem OBSBasic::GetCurrentSceneItem()
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)
{
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)
{
QSize targetSize;
+ ScalingMode scalingMode;
+ obs_video_info ovi;
/* resize preview panel to fix to the top section of the window */
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);
previewY += float(PREVIEW_EDGE_SIZE);
@@ -3083,11 +3135,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
if (IsPreviewProgramMode())
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"));
AddProjectorMenuMonitors(previewProjector, this,
@@ -4565,6 +4614,45 @@ void OBSBasic::on_actionLockPreview_triggered()
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)
{
if (!showing && isVisible()) {
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index 3fa92d035f35decbe124b1d278076c42f85ca47b..ed477d0a45e28a29c9fb50164d85cfdc308f3354 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -200,6 +200,8 @@ private:
void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const;
void GetConfigFPS(uint32_t &num, uint32_t &den) const;
+ void UpdatePreviewScalingMenu();
+
void UpdateSources(OBSScene scene);
void InsertSceneItem(obs_sceneitem_t *item);
@@ -514,6 +516,11 @@ private slots:
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_recordButton_clicked();
void on_settingsButton_clicked();
diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp
index dc2c4f31005d745b1c33cd55f579751f1d8516f3..c265afbf14c26dd9a74f483bd60ecc40ff35de30 100644
--- a/UI/window-basic-preview.cpp
+++ b/UI/window-basic-preview.cpp
@@ -17,6 +17,7 @@
OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags)
: OBSQTDisplay(parent, flags)
{
+ ResetScrollingOffset();
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)
{
if (locked) {
@@ -393,6 +430,13 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *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 &&
event->button() != Qt::RightButton)
return;
@@ -461,6 +505,9 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
return;
}
+ if (scrollMode)
+ setCursor(Qt::OpenHandCursor);
+
if (mouseDown) {
vec2 pos = GetMouseEventPos(event);
@@ -954,6 +1001,15 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
if (locked)
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) {
vec2 pos = GetMouseEventPos(event);
@@ -1113,3 +1169,8 @@ void OBSBasicPreview::DrawSceneEditing()
gs_technique_end_pass(tech);
gs_technique_end(tech);
}
+
+void OBSBasicPreview::ResetScrollingOffset()
+{
+ vec2_zero(&scrollingOffset);
+}
diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp
index c11581b670da6b00a941622c26c32f59991f0b44..fed3c1885f5c9a3b9aeb5802c3413fb20796be93 100644
--- a/UI/window-basic-preview.hpp
+++ b/UI/window-basic-preview.hpp
@@ -26,6 +26,12 @@ enum class ItemHandle : uint32_t {
BottomRight = ITEM_BOTTOM | ITEM_RIGHT
};
+enum class ScalingMode : uint32_t {
+ Window = 0,
+ Canvas = 1,
+ Output = 2
+};
+
class OBSBasicPreview : public OBSQTDisplay {
Q_OBJECT
@@ -35,17 +41,21 @@ private:
vec2 cropSize;
OBSSceneItem stretchItem;
ItemHandle stretchHandle = ItemHandle::None;
+ ScalingMode scale = ScalingMode::Window;
vec2 stretchItemSize;
matrix4 screenToItem;
matrix4 itemToScreen;
vec2 startPos;
vec2 lastMoveOffset;
+ vec2 scrollingFrom;
+ vec2 scrollingOffset;
bool mouseDown = false;
bool mouseMoved = false;
bool mouseOverItems = false;
bool cropping = false;
bool locked = false;
+ bool scrollMode = false;
static vec2 GetMouseEventPos(QMouseEvent *event);
static bool DrawSelectedItem(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -75,16 +85,26 @@ private:
public:
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 mouseReleaseEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
void DrawSceneEditing();
+ void ResetScrollingOffset();
inline void SetLocked(bool newLockedVal) {locked = newLockedVal;}
inline void ToggleLocked() {locked = !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
* and screenToItem may contain SSE data, which will cause SSE
* instructions to crash if the data is not aligned to at least a 16