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.
This commit is contained in:
VodBox 2019-07-24 13:08:08 +12:00 committed by jp9000
parent f838136597
commit 128b90af9d
5 changed files with 400 additions and 22 deletions

View File

@ -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

View File

@ -263,6 +263,27 @@ void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event)
}
}
void SourceTreeItem::enterEvent(QEvent *event)
{
QWidget::enterEvent(event);
OBSBasicPreview *preview = OBSBasicPreview::Get();
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(preview->selectMutex);
preview->hoveredPreviewItems.clear();
}
void SourceTree::selectionChanged(const QItemSelection &selected,

View File

@ -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;

View File

@ -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<obs_sceneitem_t *> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<SceneFindBoxData *>(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<SceneFindBoxData *>(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<OBSBasic *>(App()->GetMainWindow());
OBSScene scene = main->GetCurrentScene();
if (!scene)
return;
SceneFindBoxData data(startPos, pos);
obs_scene_enum_items(scene, FindItemsInBox, &data);
std::lock_guard<std::mutex> 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<OBSBasic *>(
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<std::mutex> 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<std::mutex> 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<OBSBasicPreview *>(param);
OBSBasic *main = OBSBasic::Get();
bool hovered = prev->hoveredPreviewItem == item ||
prev->hoveredListItem == item;
bool hovered = false;
{
std::lock_guard<std::mutex> 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);

View File

@ -3,6 +3,9 @@
#include <obs.hpp>
#include <graphics/vec2.h>
#include <graphics/matrix4.h>
#include <util/threading.h>
#include <mutex>
#include <vector>
#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<obs_sceneitem_t *> hoveredPreviewItems;
std::vector<obs_sceneitem_t *> 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);