From 3b616823a1d18a7c648274c7dfed1e69567ab606 Mon Sep 17 00:00:00 2001 From: Joseph El-Khouri Date: Sat, 5 Nov 2016 12:48:46 -0400 Subject: [PATCH] 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 --- UI/data/locale/en-US.ini | 4 ++ UI/display-helpers.hpp | 8 +++ UI/forms/OBSBasic.ui | 33 +++++++++++ UI/window-basic-main.cpp | 106 +++++++++++++++++++++++++++++++++--- UI/window-basic-main.hpp | 7 +++ UI/window-basic-preview.cpp | 61 +++++++++++++++++++++ UI/window-basic-preview.hpp | 20 +++++++ 7 files changed, 230 insertions(+), 9 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 24feab0dc..86b94374a 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 d175932f3..27ef174ec 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 5b922bcf8..041aa5897 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -873,8 +873,17 @@ + + + Basic.MainMenu.Edit.Scale + + + + + + @@ -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 01c2ae3c0..9580e9b68 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 3fa92d035..ed477d0a4 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 dc2c4f310..c265afbf1 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 c11581b67..fed3c1885 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