From 128b90af9d04061ee886c4d8f28edc3715cfd0af Mon Sep 17 00:00:00 2001 From: VodBox Date: Wed, 24 Jul 2019 13:08:08 +1200 Subject: [PATCH] UI: Add box select to preview This change adds the ability to box select by clicking and dragging from an empty part of the preview. Shift + Drag add any items in the box to the selection. Alt + Drag will remove items in the box from the selection. Ctrl + Drag inverts the selected state for items in the box. --- UI/CMakeLists.txt | 3 + UI/source-tree.cpp | 36 +++- UI/source-tree.hpp | 2 + UI/window-basic-preview.cpp | 364 ++++++++++++++++++++++++++++++++++-- UI/window-basic-preview.hpp | 17 +- 5 files changed, 400 insertions(+), 22 deletions(-) diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 62a8a9503..586607417 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -194,6 +194,9 @@ if(MSVC) ../deps/libff/libff/ff-util.c PROPERTIES COMPILE_FLAGS -Dinline=__inline ) + set(obs_PLATFORM_LIBRARIES + ${obs_PLATFORM_LIBRARIES} + w32-pthreads) endif() set(obs_SOURCES diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp index 0447bcb07..4032f3a3e 100644 --- a/UI/source-tree.cpp +++ b/UI/source-tree.cpp @@ -263,6 +263,27 @@ void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) } } +void SourceTreeItem::enterEvent(QEvent *event) +{ + QWidget::enterEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + preview->hoveredPreviewItems.push_back(sceneitem); +} + +void SourceTreeItem::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + + OBSBasicPreview *preview = OBSBasicPreview::Get(); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); +} + bool SourceTreeItem::IsEditing() { return editor != nullptr; @@ -1281,19 +1302,22 @@ void SourceTree::mouseMoveEvent(QMouseEvent *event) OBSBasicPreview *preview = OBSBasicPreview::Get(); - if (item) - preview->hoveredListItem = item->sceneitem; - else - preview->hoveredListItem = nullptr; - QListView::mouseMoveEvent(event); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); + if (item) + preview->hoveredPreviewItems.push_back(item->sceneitem); } void SourceTree::leaveEvent(QEvent *event) { OBSBasicPreview *preview = OBSBasicPreview::Get(); - preview->hoveredListItem = nullptr; + QListView::leaveEvent(event); + + std::lock_guard lock(preview->selectMutex); + preview->hoveredPreviewItems.clear(); } void SourceTree::selectionChanged(const QItemSelection &selected, diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp index 15e9ddbd0..d613e61d0 100644 --- a/UI/source-tree.hpp +++ b/UI/source-tree.hpp @@ -30,6 +30,8 @@ class SourceTreeItem : public QWidget { friend class SourceTreeModel; void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; virtual bool eventFilter(QObject *object, QEvent *event) override; diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp index 8520b0b5b..ebda31a19 100644 --- a/UI/window-basic-preview.cpp +++ b/UI/window-basic-preview.cpp @@ -28,11 +28,14 @@ OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags) OBSBasicPreview::~OBSBasicPreview() { - if (overflow) { - obs_enter_graphics(); + obs_enter_graphics(); + + if (overflow) gs_texture_destroy(overflow); - obs_leave_graphics(); - } + if (rectFill) + gs_vertexbuffer_destroy(rectFill); + + obs_leave_graphics(); } vec2 OBSBasicPreview::GetMouseEventPos(QMouseEvent *event) @@ -70,6 +73,22 @@ struct SceneFindData { } }; +struct SceneFindBoxData { + const vec2 &startPos; + const vec2 &pos; + std::vector sceneItems; + + SceneFindBoxData(const SceneFindData &) = delete; + SceneFindBoxData(SceneFindData &&) = delete; + SceneFindBoxData &operator=(const SceneFindData &) = delete; + SceneFindBoxData &operator=(SceneFindData &&) = delete; + + inline SceneFindBoxData(const vec2 &startPos_, const vec2 &pos_) + : startPos(startPos_), pos(pos_) + { + } +}; + static bool SceneItemHasVideo(obs_sceneitem_t *item) { obs_source_t *source = obs_sceneitem_get_source(item); @@ -518,6 +537,8 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event) float y = float(event->y()) - main->previewY / pixelRatio; Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers(); bool altDown = (modifiers & Qt::AltModifier); + bool shiftDown = (modifiers & Qt::ShiftModifier); + bool ctrlDown = (modifiers & Qt::ControlModifier); OBSQTDisplay::mousePressEvent(event); @@ -528,9 +549,25 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event) if (event->button() == Qt::LeftButton) mouseDown = true; + { + std::lock_guard lock(selectMutex); + selectedItems.clear(); + } + if (altDown) cropping = true; + if (altDown || shiftDown || ctrlDown) { + vec2 s; + SceneFindBoxData data(s, s); + + obs_scene_enum_items(main->GetCurrentScene(), FindSelected, + &data); + + std::lock_guard lock(selectMutex); + selectedItems = data.sceneItems; + } + vec2_set(&startPos, x, y); GetStretchHandleData(startPos); @@ -540,6 +577,8 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event) mouseOverItems = SelectedAtPos(startPos); vec2_zero(&lastMoveOffset); + + mousePos = startPos; } static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param) @@ -601,6 +640,37 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) if (!mouseMoved) ProcessClick(pos); + if (selectionBox) { + Qt::KeyboardModifiers modifiers = + QGuiApplication::keyboardModifiers(); + + bool altDown = modifiers & Qt::AltModifier; + bool shiftDown = modifiers & Qt::ShiftModifier; + bool ctrlDown = modifiers & Qt::ControlModifier; + + std::lock_guard lock(selectMutex); + if (altDown || ctrlDown || shiftDown) { + for (int i = 0; i < selectedItems.size(); i++) { + obs_sceneitem_select(selectedItems[i], + true); + } + } + + for (int i = 0; i < hoveredPreviewItems.size(); i++) { + bool select = true; + obs_sceneitem_t *item = hoveredPreviewItems[i]; + + if (altDown) { + select = false; + } else if (ctrlDown) { + select = !obs_sceneitem_selected(item); + } + + obs_sceneitem_select(hoveredPreviewItems[i], + select); + } + } + if (stretchGroup) { obs_sceneitem_defer_group_resize_end(stretchGroup); } @@ -610,9 +680,14 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) mouseDown = false; mouseMoved = false; cropping = false; + selectionBox = false; OBSSceneItem item = GetItemAtPos(pos, true); - hoveredPreviewItem = item; + + std::lock_guard lock(selectMutex); + hoveredPreviewItems.clear(); + hoveredPreviewItems.push_back(item); + selectedItems.clear(); } } @@ -833,6 +908,191 @@ void OBSBasicPreview::MoveItems(const vec2 &pos) obs_scene_enum_items(scene, move_items, &moveOffset); } +static bool CounterClockwise(float x1, float x2, float x3, float y1, float y2, + float y3) +{ + return (y3 - y1) * (x2 - x1) > (y2 - y1) * (x3 - x1); +} + +static bool IntersectLine(float x1, float x2, float x3, float x4, float y1, + float y2, float y3, float y4) +{ + bool a = CounterClockwise(x1, x2, x3, y1, y2, y3); + bool b = CounterClockwise(x1, x2, x4, y1, y2, y4); + bool c = CounterClockwise(x3, x4, x1, y3, y4, y1); + bool d = CounterClockwise(x3, x4, x2, y3, y4, y2); + + return (a != b) && (c != d); +} + +static bool IntersectBox(matrix4 transform, float x1, float x2, float y1, + float y2) +{ + float x3, x4, y3, y4; + + x3 = transform.t.x; + y3 = transform.t.y; + x4 = x3 + transform.x.x; + y4 = y3 + transform.x.y; + + if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) || + IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) + return true; + + x4 = x3 + transform.y.x; + y4 = y3 + transform.y.y; + + if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) || + IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) + return true; + + x3 = transform.t.x + transform.x.x; + y3 = transform.t.y + transform.x.y; + x4 = x3 + transform.y.x; + y4 = y3 + transform.y.y; + + if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) || + IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) + return true; + + x3 = transform.t.x + transform.y.x; + y3 = transform.t.y + transform.y.y; + x4 = x3 + transform.x.x; + y4 = y3 + transform.x.y; + + if (IntersectLine(x1, x1, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y1, y1, y3, y4) || + IntersectLine(x2, x2, x3, x4, y1, y2, y3, y4) || + IntersectLine(x1, x2, x3, x4, y2, y2, y3, y4)) + return true; + + return false; +} +#undef PI + +bool OBSBasicPreview::FindSelected(obs_scene_t *scene, obs_sceneitem_t *item, + void *param) +{ + SceneFindBoxData *data = reinterpret_cast(param); + + if (obs_sceneitem_selected(item)) + data->sceneItems.push_back(item); + + UNUSED_PARAMETER(scene); + return true; +} + +static bool FindItemsInBox(obs_scene_t *scene, obs_sceneitem_t *item, + void *param) +{ + SceneFindBoxData *data = reinterpret_cast(param); + matrix4 transform; + matrix4 invTransform; + vec3 transformedPos; + vec3 pos3; + vec3 pos3_; + + float x1 = std::min(data->startPos.x, data->pos.x); + float x2 = std::max(data->startPos.x, data->pos.x); + float y1 = std::min(data->startPos.y, data->pos.y); + float y2 = std::max(data->startPos.y, data->pos.y); + + if (!SceneItemHasVideo(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + if (!obs_sceneitem_visible(item)) + return true; + + vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f); + + obs_sceneitem_get_box_transform(item, &transform); + + matrix4_inv(&invTransform, &transform); + vec3_transform(&transformedPos, &pos3, &invTransform); + vec3_transform(&pos3_, &transformedPos, &transform); + + if (CloseFloat(pos3.x, pos3_.x) && CloseFloat(pos3.y, pos3_.y) && + transformedPos.x >= 0.0f && transformedPos.x <= 1.0f && + transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) { + + data->sceneItems.push_back(item); + return true; + } + + if (transform.t.x > x1 && transform.t.x < x2 && transform.t.y > y1 && + transform.t.y < y2) { + + data->sceneItems.push_back(item); + return true; + } + + if (transform.t.x + transform.x.x > x1 && + transform.t.x + transform.x.x < x2 && + transform.t.y + transform.x.y > y1 && + transform.t.y + transform.x.y < y2) { + + data->sceneItems.push_back(item); + return true; + } + + if (transform.t.x + transform.y.x > x1 && + transform.t.x + transform.y.x < x2 && + transform.t.y + transform.y.y > y1 && + transform.t.y + transform.y.y < y2) { + + data->sceneItems.push_back(item); + return true; + } + + if (transform.t.x + transform.x.x + transform.y.x > x1 && + transform.t.x + transform.x.x + transform.y.x < x2 && + transform.t.y + transform.x.y + transform.y.y > y1 && + transform.t.y + transform.x.y + transform.y.y < y2) { + + data->sceneItems.push_back(item); + return true; + } + + if (transform.t.x + 0.5 * (transform.x.x + transform.y.x) > x1 && + transform.t.x + 0.5 * (transform.x.x + transform.y.x) < x2 && + transform.t.y + 0.5 * (transform.x.y + transform.y.y) > y1 && + transform.t.y + 0.5 * (transform.x.y + transform.y.y) < y2) { + + data->sceneItems.push_back(item); + return true; + } + + if (IntersectBox(transform, x1, x2, y1, y2)) { + data->sceneItems.push_back(item); + return true; + } + + UNUSED_PARAMETER(scene); + return true; +} + +void OBSBasicPreview::BoxItems(const vec2 &startPos, const vec2 &pos) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + OBSScene scene = main->GetCurrentScene(); + if (!scene) + return; + + SceneFindBoxData data(startPos, pos); + obs_scene_enum_items(scene, FindItemsInBox, &data); + + std::lock_guard lock(selectMutex); + hoveredPreviewItems = data.sceneItems; +} + vec3 OBSBasicPreview::CalculateStretchPos(const vec3 &tl, const vec3 &br) { uint32_t alignment = obs_sceneitem_get_alignment(stretchItem); @@ -1160,8 +1420,6 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) return; if (mouseDown) { - hoveredPreviewItem = nullptr; - vec2 pos = GetMouseEventPos(event); if (!mouseMoved && !mouseOverItems && @@ -1174,6 +1432,8 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) pos.y = std::round(pos.y); if (stretchHandle != ItemHandle::None) { + selectionBox = false; + OBSBasic *main = reinterpret_cast( App()->GetMainWindow()); OBSScene scene = main->GetCurrentScene(); @@ -1194,23 +1454,32 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) StretchItem(pos); } else if (mouseOverItems) { + selectionBox = false; MoveItems(pos); + } else { + selectionBox = true; + if (!mouseMoved) + DoSelect(startPos); + BoxItems(startPos, pos); } mouseMoved = true; + mousePos = pos; } else { vec2 pos = GetMouseEventPos(event); OBSSceneItem item = GetItemAtPos(pos, true); - hoveredPreviewItem = item; + std::lock_guard lock(selectMutex); + hoveredPreviewItems.clear(); + hoveredPreviewItems.push_back(item); } } -void OBSBasicPreview::leaveEvent(QEvent *event) +void OBSBasicPreview::leaveEvent(QEvent *) { - hoveredPreviewItem = nullptr; - - UNUSED_PARAMETER(event); + std::lock_guard lock(selectMutex); + if (!selectionBox) + hoveredPreviewItems.clear(); } static void DrawSquareAtPos(float x, float y) @@ -1405,8 +1674,17 @@ bool OBSBasicPreview::DrawSelectedItem(obs_scene_t *scene, OBSBasicPreview *prev = reinterpret_cast(param); OBSBasic *main = OBSBasic::Get(); - bool hovered = prev->hoveredPreviewItem == item || - prev->hoveredListItem == item; + bool hovered = false; + { + std::lock_guard lock(prev->selectMutex); + for (int i = 0; i < prev->hoveredPreviewItems.size(); i++) { + if (prev->hoveredPreviewItems[i] == item) { + hovered = true; + break; + } + } + } + bool selected = obs_sceneitem_selected(item); if (!selected && !hovered) @@ -1510,6 +1788,44 @@ bool OBSBasicPreview::DrawSelectedItem(obs_scene_t *scene, return true; } +bool OBSBasicPreview::DrawSelectionBox(float x1, float y1, float x2, float y2, + gs_vertbuffer_t *rectFill) +{ + x1 = std::round(x1); + x2 = std::round(x2); + y1 = std::round(y1); + y2 = std::round(y2); + + gs_effect_t *eff = gs_get_effect(); + gs_eparam_t *colParam = gs_effect_get_param_by_name(eff, "color"); + + vec4 fillColor; + vec4_set(&fillColor, 0.7f, 0.7f, 0.7f, 0.5f); + + vec4 borderColor; + vec4_set(&borderColor, 1.0f, 1.0f, 1.0f, 1.0f); + + vec2 scale; + vec2_set(&scale, std::abs(x2 - x1), std::abs(y2 - y1)); + + gs_matrix_push(); + gs_matrix_identity(); + + gs_matrix_translate3f(x1, y1, 0.0f); + gs_matrix_scale3f(x2 - x1, y2 - y1, 1.0f); + + gs_effect_set_vec4(colParam, &fillColor); + gs_load_vertexbuffer(rectFill); + gs_draw(GS_TRISTRIP, 0, 0); + + gs_effect_set_vec4(colParam, &borderColor); + DrawRect(HANDLE_RADIUS / 2, scale); + + gs_matrix_pop(); + + return true; +} + void OBSBasicPreview::DrawOverflow() { if (locked) @@ -1573,6 +1889,26 @@ void OBSBasicPreview::DrawSceneEditing() gs_matrix_pop(); } + if (selectionBox) { + if (!rectFill) { + gs_render_start(true); + + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + + rectFill = gs_render_save(); + } + + DrawSelectionBox(startPos.x * main->previewScale, + startPos.y * main->previewScale, + mousePos.x * main->previewScale, + mousePos.y * main->previewScale, rectFill); + } + gs_load_vertexbuffer(nullptr); gs_technique_end_pass(tech); diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp index 39fba86bf..2473f62ae 100644 --- a/UI/window-basic-preview.hpp +++ b/UI/window-basic-preview.hpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "qt-display.hpp" #include "obs-app.hpp" @@ -32,6 +35,7 @@ class OBSBasicPreview : public OBSQTDisplay { Q_OBJECT friend class SourceTree; + friend class SourceTreeItem; private: obs_sceneitem_crop startCrop; @@ -46,8 +50,10 @@ private: matrix4 invGroupTransform; gs_texture_t *overflow = nullptr; + gs_vertbuffer_t *rectFill = nullptr; vec2 startPos; + vec2 mousePos; vec2 lastMoveOffset; vec2 scrollingFrom; vec2 scrollingOffset; @@ -58,17 +64,23 @@ private: bool locked = false; bool scrollMode = false; bool fixedScaling = false; + bool selectionBox = false; int32_t scalingLevel = 0; float scalingAmount = 1.0f; - obs_sceneitem_t *hoveredPreviewItem = nullptr; - obs_sceneitem_t *hoveredListItem = nullptr; + std::vector hoveredPreviewItems; + std::vector selectedItems; + std::mutex selectMutex; static vec2 GetMouseEventPos(QMouseEvent *event); + static bool FindSelected(obs_scene_t *scene, obs_sceneitem_t *item, + void *param); static bool DrawSelectedOverflow(obs_scene_t *scene, obs_sceneitem_t *item, void *param); static bool DrawSelectedItem(obs_scene_t *scene, obs_sceneitem_t *item, void *param); + static bool DrawSelectionBox(float x1, float y1, float x2, float y2, + gs_vertbuffer_t *box); static OBSSceneItem GetItemAtPos(const vec2 &pos, bool selectBelow); static bool SelectedAtPos(const vec2 &pos); @@ -88,6 +100,7 @@ private: static void SnapItemMovement(vec2 &offset); void MoveItems(const vec2 &pos); + void BoxItems(const vec2 &startPos, const vec2 &pos); void ProcessClick(const vec2 &pos);