Sfoglia il codice sorgente

Merge pull request #114 from jp9000/scene-editing

Scene editing
Jim 11 anni fa
parent
commit
111c4e84d2

+ 47 - 1
build/data/obs-studio/locale/en.txt

@@ -15,7 +15,7 @@ MoveUp="Move Up"
 MoveDown="Move Down"
 Settings="Settings"
 Exit="Exit"
-Volume="Volume"
+Mixer="Mixer"
 Browse="Browse"
 Mono="Mono"
 Stereo="Stereo"
@@ -66,6 +66,34 @@ Basic.Main.DefaultSceneName.Text="Scene %1"
 Basic.SourceSelect.CreateNew="Create new"
 Basic.SourceSelect.AddExisting="Add Existing"
 
+# transform window
+Basic.TransformWindow="Scene Item Transform"
+Basic.TransformWindow.Position="Position"
+Basic.TransformWindow.Rotation="Rotation"
+Basic.TransformWindow.Scale="Scale"
+Basic.TransformWindow.Alignment="Positional Alignment"
+Basic.TransformWindow.BoundsType="Bounding Box Type"
+Basic.TransformWindow.BoundsAlignment="Alignment in Bounding Box"
+Basic.TransformWindow.Bounds="Bounding Box Size"
+
+Basic.TransformWindow.Alignment.TopLeft="Top Left"
+Basic.TransformWindow.Alignment.TopCenter="Top Center"
+Basic.TransformWindow.Alignment.TopRight="Top Right"
+Basic.TransformWindow.Alignment.CenterLeft="Center Left"
+Basic.TransformWindow.Alignment.Center="Center"
+Basic.TransformWindow.Alignment.CenterRight="Center Right"
+Basic.TransformWindow.Alignment.BottomLeft="Bottom Left"
+Basic.TransformWindow.Alignment.BottomCenter="Bottom Center"
+Basic.TransformWindow.Alignment.BottomRight="Bottom Right"
+
+Basic.TransformWindow.BoundsType.None="No bounds"
+Basic.TransformWindow.BoundsType.MaxOnly="Maximum size only"
+Basic.TransformWindow.BoundsType.ScaleInner="Scale to inner bounds"
+Basic.TransformWindow.BoundsType.ScaleOuter="Scale to outer bounds"
+Basic.TransformWindow.BoundsType.ScaleToWidth="Scale to width of bounds"
+Basic.TransformWindow.BoundsType.ScaleToHeight="Scale to height of bounds"
+Basic.TransformWindow.BoundsType.Stretch="Stretch to bounds"
+
 # no scene warning
 Basic.Main.AddSourceHelp.Title="Cannot Add Source"
 Basic.Main.AddSourceHelp.Text="You need to have at least 1 scene to add a source."
@@ -86,6 +114,24 @@ Basic.MainMenu.File.Import="&Import"
 Basic.MainMenu.File.Settings="&Settings"
 Basic.MainMenu.File.Exit="E&xit"
 
+# basic mode edit menu
+Basic.MainMenu.Edit="&Edit"
+Basic.MainMenu.Edit.Undo="&Undo"
+Basic.MainMenu.Edit.Redo="&Redo"
+Basic.MainMenu.Edit.UndoAction="&Undo $1"
+Basic.MainMenu.Edit.RedoAction="&Redo $1"
+Basic.MainMenu.Edit.Transform="&Transform"
+Basic.MainMenu.Edit.Transform.EditTransform="&Edit Transform..."
+Basic.MainMenu.Edit.Transform.ResetTransform="&Reset Transform"
+Basic.MainMenu.Edit.Transform.Rotate90CW="Rotate 90 degrees CW"
+Basic.MainMenu.Edit.Transform.Rotate90CCW="Rotate 90 degrees CCW"
+Basic.MainMenu.Edit.Transform.Rotate180="Rotate 180 degrees"
+Basic.MainMenu.Edit.Transform.FlipHorizontal="Flip &Horizontal"
+Basic.MainMenu.Edit.Transform.FlipVertical="Flip &Vertical"
+Basic.MainMenu.Edit.Transform.FitToScreen="&Fit to screen"
+Basic.MainMenu.Edit.Transform.StretchToScreen="&Stretch to screen"
+Basic.MainMenu.Edit.Transform.CenterToScreen="&Center to screen"
+
 # basic mode help menu
 Basic.MainMenu.Help="&Help"
 Basic.MainMenu.Help.Logs="&Log Files"

+ 2 - 2
libobs/obs-config.h

@@ -34,14 +34,14 @@
  *
  * Reset to zero each major version
  */
-#define LIBOBS_API_MINOR_VER  2
+#define LIBOBS_API_MINOR_VER  3
 
 /*
  * Increment if backward-compatible bug fix
  *
  * Reset to zero each major or minor version
  */
-#define LIBOBS_API_PATCH_VER  4
+#define LIBOBS_API_PATCH_VER  0
 
 #define LIBOBS_API_VER       ((LIBOBS_API_MAJOR_VER << 24) | \
                               (LIBOBS_API_MINOR_VER << 16) | \

+ 6 - 0
libobs/obs-defs.h

@@ -20,6 +20,12 @@
 /** Maximum number of source channels for output and per display */
 #define MAX_CHANNELS 64
 
+#define OBS_ALIGN_CENTER (0)
+#define OBS_ALIGN_LEFT   (1<<0)
+#define OBS_ALIGN_RIGHT  (1<<1)
+#define OBS_ALIGN_TOP    (1<<2)
+#define OBS_ALIGN_BOTTOM (1<<3)
+
 #define MODULE_SUCCESS             0
 #define MODULE_ERROR              -1
 #define MODULE_FILE_NOT_FOUND     -2

+ 296 - 25
libobs/obs-scene.c

@@ -26,6 +26,9 @@ static const char *obs_scene_signals[] = {
 	"void item_move_down(ptr scene, ptr item)",
 	"void item_move_top(ptr scene, ptr item)",
 	"void item_move_bottom(ptr scene, ptr item)",
+	"void item_select(ptr scene, ptr item)",
+	"void item_deselect(ptr scene, ptr item)",
+	"void item_transform(ptr scene, ptr item)",
 	NULL
 };
 
@@ -155,6 +158,143 @@ static inline void attach_sceneitem(struct obs_scene *parent,
 	}
 }
 
+static void add_alignment(struct vec2 *v, uint32_t align, int cx, int cy)
+{
+	if (align & OBS_ALIGN_RIGHT)
+		v->x += (float)cx;
+	else if ((align & OBS_ALIGN_LEFT) == 0)
+		v->x += (float)(cx / 2);
+
+	if (align & OBS_ALIGN_BOTTOM)
+		v->y += (float)cy;
+	else if ((align & OBS_ALIGN_TOP) == 0)
+		v->y += (float)(cy / 2);
+}
+
+static void calculate_bounds_data(struct obs_scene_item *item,
+		struct vec2 *origin, struct vec2 *scale,
+		uint32_t *cx, uint32_t *cy)
+{
+	float    width         = (float)(*cx) * fabsf(scale->x);
+	float    height        = (float)(*cy) * fabsf(scale->y);
+	float    item_aspect   = width / height;
+	float    bounds_aspect = item->bounds.x / item->bounds.y;
+	uint32_t bounds_type   = item->bounds_type;
+	float    width_diff, height_diff;
+
+	if (item->bounds_type == OBS_BOUNDS_MAX_ONLY)
+		if (width > item->bounds.x || height > item->bounds.y)
+			bounds_type = OBS_BOUNDS_SCALE_INNER;
+
+	if (bounds_type == OBS_BOUNDS_SCALE_INNER ||
+	    bounds_type == OBS_BOUNDS_SCALE_OUTER) {
+		bool  use_width = (bounds_aspect < item_aspect);
+		float mul;
+
+		if (item->bounds_type == OBS_BOUNDS_SCALE_OUTER)
+			use_width = !use_width;
+
+		mul = use_width ?
+			item->bounds.x / width :
+			item->bounds.y / height;
+
+		vec2_mulf(scale, scale, mul);
+
+	} else if (bounds_type == OBS_BOUNDS_SCALE_TO_WIDTH) {
+		vec2_mulf(scale, scale, item->bounds.x / width);
+
+	} else if (bounds_type == OBS_BOUNDS_SCALE_TO_HEIGHT) {
+		vec2_mulf(scale, scale, item->bounds.y / height);
+
+	} else if (bounds_type == OBS_BOUNDS_STRETCH) {
+		scale->x = item->bounds.x / (float)(*cx);
+		scale->y = item->bounds.y / (float)(*cy);
+	}
+
+	width       = (float)(*cx) * scale->x;
+	height      = (float)(*cy) * scale->y;
+	width_diff  = item->bounds.x - width;
+	height_diff = item->bounds.y - height;
+	*cx         = (uint32_t)item->bounds.x;
+	*cy         = (uint32_t)item->bounds.y;
+
+	add_alignment(origin, item->bounds_align,
+			(int)-width_diff, (int)-height_diff);
+}
+
+static void update_item_transform(struct obs_scene_item *item)
+{
+	uint32_t        width         = obs_source_getwidth(item->source);
+	uint32_t        height        = obs_source_getheight(item->source);
+	uint32_t        cx            = width;
+	uint32_t        cy            = height;
+	struct vec2     base_origin   = {0.0f, 0.0f};
+	struct vec2     origin        = {0.0f, 0.0f};
+	struct vec2     scale         = item->scale;
+	struct calldata params        = {0};
+
+	/* ----------------------- */
+
+	if (item->bounds_type != OBS_BOUNDS_NONE) {
+		calculate_bounds_data(item, &origin, &scale, &cx, &cy);
+	} else {
+		cx = (uint32_t)((float)cx * scale.x);
+		cy = (uint32_t)((float)cy * scale.y);
+	}
+
+	add_alignment(&origin, item->align, (int)cx, (int)cy);
+
+	matrix4_identity(&item->draw_transform);
+	matrix4_scale3f(&item->draw_transform, &item->draw_transform,
+			scale.x, scale.y, 1.0f);
+	matrix4_translate3f(&item->draw_transform, &item->draw_transform,
+			-origin.x, -origin.y, 0.0f);
+	matrix4_rotate_aa4f(&item->draw_transform, &item->draw_transform,
+			0.0f, 0.0f, 1.0f, RAD(item->rot));
+	matrix4_translate3f(&item->draw_transform, &item->draw_transform,
+			item->pos.x, item->pos.y, 0.0f);
+
+	/* ----------------------- */
+
+	if (item->bounds_type != OBS_BOUNDS_NONE) {
+		vec2_copy(&scale, &item->bounds);
+	} else {
+		scale.x = (float)width  * item->scale.x;
+		scale.y = (float)height * item->scale.y;
+	}
+
+	add_alignment(&base_origin, item->align, (int)scale.x, (int)scale.y);
+
+	matrix4_identity(&item->box_transform);
+	matrix4_scale3f(&item->box_transform, &item->box_transform,
+			scale.x, scale.y, 1.0f);
+	matrix4_translate3f(&item->box_transform, &item->box_transform,
+			-base_origin.x, -base_origin.y, 0.0f);
+	matrix4_rotate_aa4f(&item->box_transform, &item->box_transform,
+			0.0f, 0.0f, 1.0f, RAD(item->rot));
+	matrix4_translate3f(&item->box_transform, &item->box_transform,
+			item->pos.x, item->pos.y, 0.0f);
+
+	/* ----------------------- */
+
+	item->last_width  = width;
+	item->last_height = height;
+
+	calldata_setptr(&params, "scene", item->parent);
+	calldata_setptr(&params, "item", item);
+	signal_handler_signal(item->parent->source->context.signals,
+			"item_transform", &params);
+	calldata_free(&params);
+}
+
+static inline bool source_size_changed(struct obs_scene_item *item)
+{
+	uint32_t width  = obs_source_getwidth(item->source);
+	uint32_t height = obs_source_getheight(item->source);
+
+	return item->last_width != width || item->last_height != height;
+}
+
 static void scene_video_render(void *data, effect_t effect)
 {
 	struct obs_scene *scene = data;
@@ -173,14 +313,12 @@ static void scene_video_render(void *data, effect_t effect)
 			continue;
 		}
 
-		gs_matrix_push();
-		gs_matrix_translate3f(item->origin.x, item->origin.y, 0.0f);
-		gs_matrix_scale3f(item->scale.x, item->scale.y, 1.0f);
-		gs_matrix_rotaa4f(0.0f, 0.0f, 1.0f, RAD(-item->rot));
-		gs_matrix_translate3f(-item->pos.x, -item->pos.y, 0.0f);
+		if (source_size_changed(item))
+			update_item_transform(item);
 
+		gs_matrix_push();
+		gs_matrix_mul(&item->draw_transform);
 		obs_source_video_render(item->source);
-
 		gs_matrix_pop();
 
 		item = item->next;
@@ -205,12 +343,24 @@ static void scene_load_item(struct obs_scene *scene, obs_data_t item_data)
 
 	item = obs_scene_add(scene, source);
 
+	obs_data_set_default_int(item_data, "align",
+			OBS_ALIGN_TOP | OBS_ALIGN_LEFT);
+
 	item->rot     = (float)obs_data_getdouble(item_data, "rot");
+	item->align   = (uint32_t)obs_data_getint(item_data, "align");
 	item->visible = obs_data_getbool(item_data, "visible");
-	obs_data_get_vec2(item_data, "origin", &item->origin);
 	obs_data_get_vec2(item_data, "pos",    &item->pos);
 	obs_data_get_vec2(item_data, "scale",  &item->scale);
+
+	item->bounds_type =
+		(enum obs_bounds_type)obs_data_getint(item_data, "bounds_type");
+	item->bounds_align =
+		(uint32_t)obs_data_getint(item_data, "bounds_align");
+	obs_data_get_vec2(item_data, "bounds", &item->bounds);
+
 	obs_source_release(source);
+
+	update_item_transform(item);
 }
 
 static void scene_load(void *scene, obs_data_t settings)
@@ -238,12 +388,15 @@ static void scene_save_item(obs_data_array_t array, struct obs_scene_item *item)
 	obs_data_t item_data = obs_data_create();
 	const char *name     = obs_source_getname(item->source);
 
-	obs_data_setstring(item_data, "name",    name);
-	obs_data_setbool  (item_data, "visible", item->visible);
-	obs_data_setdouble(item_data, "rot",     item->rot);
-	obs_data_set_vec2 (item_data, "origin",  &item->origin);
-	obs_data_set_vec2 (item_data, "pos",     &item->pos);
-	obs_data_set_vec2 (item_data, "scale",   &item->scale);
+	obs_data_setstring(item_data, "name",         name);
+	obs_data_setbool  (item_data, "visible",      item->visible);
+	obs_data_setdouble(item_data, "rot",          item->rot);
+	obs_data_set_vec2 (item_data, "pos",          &item->pos);
+	obs_data_set_vec2 (item_data, "scale",        &item->scale);
+	obs_data_setint   (item_data, "align",        (int)item->align);
+	obs_data_setint   (item_data, "bounds_type",  (int)item->bounds_type);
+	obs_data_setint   (item_data, "bounds_align", (int)item->bounds_align);
+	obs_data_set_vec2 (item_data, "bounds",       &item->bounds);
 
 	obs_data_array_push_back(array, item_data);
 	obs_data_release(item_data);
@@ -400,7 +553,10 @@ obs_sceneitem_t obs_scene_add(obs_scene_t scene, obs_source_t source)
 	item->visible = true;
 	item->parent  = scene;
 	item->ref     = 1;
+	item->align   = OBS_ALIGN_TOP | OBS_ALIGN_LEFT;
 	vec2_set(&item->scale, 1.0f, 1.0f);
+	matrix4_identity(&item->draw_transform);
+	matrix4_identity(&item->box_transform);
 
 	obs_source_addref(source);
 	obs_source_add_child(scene->source, source);
@@ -495,28 +651,59 @@ obs_source_t obs_sceneitem_getsource(obs_sceneitem_t item)
 	return item ? item->source : NULL;
 }
 
+void obs_sceneitem_select(obs_sceneitem_t item, bool select)
+{
+	struct calldata params = {0};
+	const char *command = select ? "item_select" : "item_deselect";
+
+	if (!item || item->selected == select)
+		return;
+
+	item->selected = select;
+
+	calldata_setptr(&params, "scene", item->parent);
+	calldata_setptr(&params, "item",  item);
+	signal_handler_signal(item->parent->source->context.signals,
+			command, &params);
+
+	calldata_free(&params);
+}
+
+bool obs_sceneitem_selected(obs_sceneitem_t item)
+{
+	return item ? item->selected : false;
+}
+
 void obs_sceneitem_setpos(obs_sceneitem_t item, const struct vec2 *pos)
 {
-	if (item)
+	if (item) {
 		vec2_copy(&item->pos, pos);
+		update_item_transform(item);
+	}
 }
 
 void obs_sceneitem_setrot(obs_sceneitem_t item, float rot)
 {
-	if (item)
+	if (item) {
 		item->rot = rot;
+		update_item_transform(item);
+	}
 }
 
-void obs_sceneitem_setorigin(obs_sceneitem_t item, const struct vec2 *origin)
+void obs_sceneitem_setscale(obs_sceneitem_t item, const struct vec2 *scale)
 {
-	if (item)
-		vec2_copy(&item->origin, origin);
+	if (item) {
+		vec2_copy(&item->scale, scale);
+		update_item_transform(item);
+	}
 }
 
-void obs_sceneitem_setscale(obs_sceneitem_t item, const struct vec2 *scale)
+void obs_sceneitem_setalignment(obs_sceneitem_t item, uint32_t alignment)
 {
-	if (item)
-		vec2_copy(&item->scale, scale);
+	if (item) {
+		item->align = alignment;
+		update_item_transform(item);
+	}
 }
 
 static inline void signal_move_dir(struct obs_scene_item *item,
@@ -583,6 +770,32 @@ void obs_sceneitem_setorder(obs_sceneitem_t item, enum order_movement movement)
 	obs_scene_release(scene);
 }
 
+void obs_sceneitem_set_bounds_type(obs_sceneitem_t item,
+		enum obs_bounds_type type)
+{
+	if (item) {
+		item->bounds_type = type;
+		update_item_transform(item);
+	}
+}
+
+void obs_sceneitem_set_bounds_alignment(obs_sceneitem_t item,
+		uint32_t alignment)
+{
+	if (item) {
+		item->bounds_align = alignment;
+		update_item_transform(item);
+	}
+}
+
+void obs_sceneitem_set_bounds(obs_sceneitem_t item, const struct vec2 *bounds)
+{
+	if (item) {
+		item->bounds = *bounds;
+		update_item_transform(item);
+	}
+}
+
 void obs_sceneitem_getpos(obs_sceneitem_t item, struct vec2 *pos)
 {
 	if (item)
@@ -594,14 +807,72 @@ float obs_sceneitem_getrot(obs_sceneitem_t item)
 	return item ? item->rot : 0.0f;
 }
 
-void obs_sceneitem_getorigin(obs_sceneitem_t item, struct vec2 *origin)
+void obs_sceneitem_getscale(obs_sceneitem_t item, struct vec2 *scale)
 {
 	if (item)
-		vec2_copy(origin, &item->origin);
+		vec2_copy(scale, &item->scale);
 }
 
-void obs_sceneitem_getscale(obs_sceneitem_t item, struct vec2 *scale)
+uint32_t obs_sceneitem_getalignment(obs_sceneitem_t item)
+{
+	return item ? item->align : 0;
+}
+
+enum obs_bounds_type obs_sceneitem_get_bounds_type(obs_sceneitem_t item)
+{
+	return item ? item->bounds_type : OBS_BOUNDS_NONE;
+}
+
+uint32_t obs_sceneitem_get_bounds_alignment(obs_sceneitem_t item)
+{
+	return item ? item->bounds_align : 0;
+}
+
+void obs_sceneitem_get_bounds(obs_sceneitem_t item, struct vec2 *bounds)
 {
 	if (item)
-		vec2_copy(scale, &item->scale);
+		*bounds = item->bounds;
+}
+
+void obs_sceneitem_get_info(obs_sceneitem_t item,
+		struct obs_sceneitem_info *info)
+{
+	if (item && info) {
+		info->pos              = item->pos;
+		info->rot              = item->rot;
+		info->scale            = item->scale;
+		info->alignment        = item->align;
+		info->bounds_type      = item->bounds_type;
+		info->bounds_alignment = item->bounds_align;
+		info->bounds           = item->bounds;
+	}
+}
+
+void obs_sceneitem_set_info(obs_sceneitem_t item,
+		const struct obs_sceneitem_info *info)
+{
+	if (item && info) {
+		item->pos          = info->pos;
+		item->rot          = info->rot;
+		item->scale        = info->scale;
+		item->align        = info->alignment;
+		item->bounds_type  = info->bounds_type;
+		item->bounds_align = info->bounds_alignment;
+		item->bounds       = info->bounds;
+		update_item_transform(item);
+	}
+}
+
+void obs_sceneitem_get_draw_transform(obs_sceneitem_t item,
+		struct matrix4 *transform)
+{
+	if (item)
+		matrix4_copy(transform, &item->draw_transform);
+}
+
+void obs_sceneitem_get_box_transform(obs_sceneitem_t item,
+		struct matrix4 *transform)
+{
+	if (item)
+		matrix4_copy(transform, &item->box_transform);
 }

+ 15 - 1
libobs/obs-scene.h

@@ -19,6 +19,7 @@
 
 #include "obs.h"
 #include "obs-internal.h"
+#include "graphics/matrix4.h"
 
 /* how obs scene! */
 
@@ -29,11 +30,24 @@ struct obs_scene_item {
 	struct obs_scene      *parent;
 	struct obs_source     *source;
 	bool                  visible;
+	bool                  selected;
 
-	struct vec2           origin;
 	struct vec2           pos;
 	struct vec2           scale;
 	float                 rot;
+	uint32_t              align;
+
+	/* last width/height of the source, this is used to check whether
+	 * ths transform needs updating */
+	uint32_t              last_width;
+	uint32_t              last_height;
+
+	struct matrix4        box_transform;
+	struct matrix4        draw_transform;
+
+	enum obs_bounds_type  bounds_type;
+	uint32_t              bounds_align;
+	struct vec2           bounds;
 
 	/* would do **prev_next, but not really great for reordering */
 	struct obs_scene_item *prev;

+ 59 - 7
libobs/obs.h

@@ -21,6 +21,7 @@
 #include "util/bmem.h"
 #include "graphics/graphics.h"
 #include "graphics/vec2.h"
+#include "graphics/vec3.h"
 #include "media-io/audio-io.h"
 #include "media-io/video-io.h"
 #include "callback/signal.h"
@@ -32,6 +33,8 @@
 #include "obs-ui.h"
 #include "obs-properties.h"
 
+struct matrix4;
+
 /* opaque types */
 struct obs_display;
 struct obs_view;
@@ -85,13 +88,38 @@ enum allow_direct_render {
 	ALLOW_DIRECT_RENDERING,
 };
 
+/**
+ * Used with scene items to indicate the type of bounds to use for scene items.
+ * Mostly determines how the image will be scaled within those bounds, or
+ * whether to use bounds at all.
+ */
+enum obs_bounds_type {
+	OBS_BOUNDS_NONE,            /**< no bounds */
+	OBS_BOUNDS_STRETCH,         /**< stretch (ignores base scale) */
+	OBS_BOUNDS_SCALE_INNER,     /**< scales to inner rectangle */
+	OBS_BOUNDS_SCALE_OUTER,     /**< scales to outer rectangle */
+	OBS_BOUNDS_SCALE_TO_WIDTH,  /**< scales to the width  */
+	OBS_BOUNDS_SCALE_TO_HEIGHT, /**< scales to the height */
+	OBS_BOUNDS_MAX_ONLY,        /**< no scaling, maximum size only */
+};
+
+struct obs_sceneitem_info {
+	struct vec2          pos;
+	float                rot;
+	struct vec2          scale;
+	uint32_t             alignment;
+
+	enum obs_bounds_type bounds_type;
+	uint32_t             bounds_alignment;
+	struct vec2          bounds;
+};
+
 /**
  * Video initialization structure
  */
 struct obs_video_info {
 	/**
-	 * Graphics module to use (usually "libobs-opengl" or
-	 * "libobs-d3d11")
+	 * Graphics module to use (usually "libobs-opengl" or "libobs-d3d11")
 	 */
 	const char          *graphics_module;
 
@@ -669,20 +697,44 @@ EXPORT obs_scene_t obs_sceneitem_getscene(obs_sceneitem_t item);
 /** Gets the source of a scene item. */
 EXPORT obs_source_t obs_sceneitem_getsource(obs_sceneitem_t item);
 
-/* Functions for gettings/setting specific oriantation of a scene item */
+EXPORT void obs_sceneitem_select(obs_sceneitem_t item, bool select);
+EXPORT bool obs_sceneitem_selected(obs_sceneitem_t item);
+
+/* Functions for gettings/setting specific orientation of a scene item */
 EXPORT void obs_sceneitem_setpos(obs_sceneitem_t item, const struct vec2 *pos);
-EXPORT void obs_sceneitem_setrot(obs_sceneitem_t item, float rot);
-EXPORT void obs_sceneitem_setorigin(obs_sceneitem_t item,
-		const struct vec2 *origin);
+EXPORT void obs_sceneitem_setrot(obs_sceneitem_t item, float rot_deg);
 EXPORT void obs_sceneitem_setscale(obs_sceneitem_t item,
 		const struct vec2 *scale);
+EXPORT void obs_sceneitem_setalignment(obs_sceneitem_t item,
+		uint32_t alignment);
 EXPORT void obs_sceneitem_setorder(obs_sceneitem_t item,
 		enum order_movement movement);
 
+EXPORT void obs_sceneitem_set_bounds_type(obs_sceneitem_t item,
+		enum obs_bounds_type type);
+EXPORT void obs_sceneitem_set_bounds_alignment(obs_sceneitem_t item,
+		uint32_t alignment);
+EXPORT void obs_sceneitem_set_bounds(obs_sceneitem_t item,
+		const struct vec2 *bounds);
+
 EXPORT void  obs_sceneitem_getpos(obs_sceneitem_t item, struct vec2 *pos);
 EXPORT float obs_sceneitem_getrot(obs_sceneitem_t item);
-EXPORT void  obs_sceneitem_getorigin(obs_sceneitem_t item, struct vec2 *center);
 EXPORT void  obs_sceneitem_getscale(obs_sceneitem_t item, struct vec2 *scale);
+EXPORT uint32_t obs_sceneitem_getalignment(obs_sceneitem_t item);
+
+EXPORT enum obs_bounds_type obs_sceneitem_get_bounds_type(obs_sceneitem_t item);
+EXPORT uint32_t obs_sceneitem_get_bounds_alignment(obs_sceneitem_t item);
+EXPORT void obs_sceneitem_get_bounds(obs_sceneitem_t item, struct vec2 *bounds);
+
+EXPORT void obs_sceneitem_get_info(obs_sceneitem_t item,
+		struct obs_sceneitem_info *info);
+EXPORT void obs_sceneitem_set_info(obs_sceneitem_t item,
+		const struct obs_sceneitem_info *info);
+
+EXPORT void obs_sceneitem_get_draw_transform(obs_sceneitem_t item,
+		struct matrix4 *transform);
+EXPORT void obs_sceneitem_get_box_transform(obs_sceneitem_t item,
+		struct matrix4 *transform);
 
 
 /* ------------------------------------------------------------------------- */

+ 5 - 0
obs/CMakeLists.txt

@@ -57,6 +57,8 @@ set(obs_SOURCES
 	window-basic-settings.cpp
 	window-basic-properties.cpp
 	window-basic-source-select.cpp
+	window-basic-transform.cpp
+	window-basic-preview.cpp
 	window-namedialog.cpp
 	window-log-reply.cpp
 	properties-view.cpp
@@ -71,6 +73,8 @@ set(obs_HEADERS
 	window-basic-settings.hpp
 	window-basic-properties.hpp
 	window-basic-source-select.hpp
+	window-basic-transform.hpp
+	window-basic-preview.hpp
 	window-namedialog.hpp
 	window-log-reply.hpp
 	properties-view.hpp
@@ -83,6 +87,7 @@ set(obs_UI
 	forms/NameDialog.ui
 	forms/OBSLogReply.ui
 	forms/OBSBasic.ui
+	forms/OBSBasicTransform.ui
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSourceSelect.ui
 	forms/OBSBasicProperties.ui)

+ 110 - 4
obs/forms/OBSBasic.ui

@@ -29,7 +29,7 @@
   <widget class="QWidget" name="centralwidget">
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
-     <widget class="OBSQTDisplay" name="preview" native="true">
+     <widget class="OBSBasicPreview" name="preview" native="true">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
         <horstretch>0</horstretch>
@@ -305,7 +305,7 @@
           <item>
            <widget class="QLabel" name="label_3">
             <property name="text">
-             <string>Volume</string>
+             <string>Mixer</string>
             </property>
            </widget>
           </item>
@@ -463,7 +463,35 @@
     </widget>
     <addaction name="menuLogFiles"/>
    </widget>
+   <widget class="QMenu" name="menuBasic_MainMenu_Edit">
+    <property name="title">
+     <string>Basic.MainMenu.Edit</string>
+    </property>
+    <widget class="QMenu" name="menuBasic_MainMenu_Edit_Transform">
+     <property name="title">
+      <string>Basic.MainMenu.Edit.Transform</string>
+     </property>
+     <addaction name="actionEditTransform"/>
+     <addaction name="actionResetTransform"/>
+     <addaction name="separator"/>
+     <addaction name="actionRotate90CW"/>
+     <addaction name="actionRotate90CCW"/>
+     <addaction name="actionRotate180"/>
+     <addaction name="separator"/>
+     <addaction name="actionFlipHorizontal"/>
+     <addaction name="actionFlipVertical"/>
+     <addaction name="separator"/>
+     <addaction name="actionFitToScreen"/>
+     <addaction name="actionStretchToScreen"/>
+     <addaction name="actionCenterToScreen"/>
+    </widget>
+    <addaction name="actionUndo"/>
+    <addaction name="actionRedo"/>
+    <addaction name="separator"/>
+    <addaction name="menuBasic_MainMenu_Edit_Transform"/>
+   </widget>
    <addaction name="menu_File"/>
+   <addaction name="menuBasic_MainMenu_Edit"/>
    <addaction name="menuBasic_MainMenu_Help"/>
   </widget>
   <widget class="QStatusBar" name="statusbar"/>
@@ -610,12 +638,90 @@
     <string>Basic.MainMenu.Help.Logs.UploadCurrentLog</string>
    </property>
   </action>
+  <action name="actionUndo">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Undo</string>
+   </property>
+  </action>
+  <action name="actionRedo">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Redo</string>
+   </property>
+  </action>
+  <action name="actionEditTransform">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.EditTransform</string>
+   </property>
+  </action>
+  <action name="actionRotate90CW">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.Rotate90CW</string>
+   </property>
+  </action>
+  <action name="actionRotate90CCW">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.Rotate90CCW</string>
+   </property>
+  </action>
+  <action name="actionRotate180">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.Rotate180</string>
+   </property>
+  </action>
+  <action name="actionFitToScreen">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.FitToScreen</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+F</string>
+   </property>
+  </action>
+  <action name="actionStretchToScreen">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.StretchToScreen</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+S</string>
+   </property>
+  </action>
+  <action name="actionResetTransform">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.ResetTransform</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+R</string>
+   </property>
+  </action>
+  <action name="actionCenterToScreen">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.CenterToScreen</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+C</string>
+   </property>
+  </action>
+  <action name="actionFlipHorizontal">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.FlipHorizontal</string>
+   </property>
+  </action>
+  <action name="actionFlipVertical">
+   <property name="text">
+    <string>Basic.MainMenu.Edit.Transform.FlipVertical</string>
+   </property>
+  </action>
  </widget>
  <customwidgets>
   <customwidget>
-   <class>OBSQTDisplay</class>
+   <class>OBSBasicPreview</class>
    <extends>QWidget</extends>
-   <header>qt-display.hpp</header>
+   <header>window-basic-preview.hpp</header>
    <container>1</container>
   </customwidget>
  </customwidgets>

+ 462 - 0
obs/forms/OBSBasicTransform.ui

@@ -0,0 +1,462 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSBasicTransform</class>
+ <widget class="QDialog" name="OBSBasicTransform">
+  <property name="enabled">
+   <bool>false</bool>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>564</width>
+    <height>241</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Basic.TransformWindow</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <property name="fieldGrowthPolicy">
+      <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+     </property>
+     <property name="labelAlignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>170</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Basic.TransformWindow.Position</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QWidget" name="widget" native="true">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QDoubleSpinBox" name="positionX">
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>-9001.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QDoubleSpinBox" name="positionY">
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>-9001.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Basic.TransformWindow.Rotation</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QDoubleSpinBox" name="rotation">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>100</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="minimum">
+        <double>-360.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>360.000000000000000</double>
+       </property>
+       <property name="singleStep">
+        <double>0.100000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>Basic.TransformWindow.Scale</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QWidget" name="widget_2" native="true">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QDoubleSpinBox" name="scaleX">
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>-9001.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+          <property name="singleStep">
+           <double>0.010000000000000</double>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QDoubleSpinBox" name="scaleY">
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>-9001.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+          <property name="singleStep">
+           <double>0.010000000000000</double>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item row="3" column="0">
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>Basic.TransformWindow.Alignment</string>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="1">
+      <widget class="QComboBox" name="align">
+       <property name="currentText">
+        <string>Basic.TransformWindow.Alignment.TopLeft</string>
+       </property>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopCenter</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopRight</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.CenterLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.Center</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.CenterRight</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomCenter</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomRight</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item row="5" column="0">
+      <widget class="QLabel" name="label_5">
+       <property name="text">
+        <string>Basic.TransformWindow.BoundsType</string>
+       </property>
+      </widget>
+     </item>
+     <item row="5" column="1">
+      <widget class="QComboBox" name="boundsType">
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.None</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.Stretch</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.ScaleInner</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.ScaleOuter</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.ScaleToWidth</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.ScaleToHeight</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.BoundsType.MaxOnly</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item row="4" column="1">
+      <spacer name="verticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item row="6" column="0">
+      <widget class="QLabel" name="label_6">
+       <property name="text">
+        <string>Basic.TransformWindow.BoundsAlignment</string>
+       </property>
+      </widget>
+     </item>
+     <item row="7" column="0">
+      <widget class="QLabel" name="label_7">
+       <property name="text">
+        <string>Basic.TransformWindow.Bounds</string>
+       </property>
+      </widget>
+     </item>
+     <item row="7" column="1">
+      <widget class="QWidget" name="widget_3" native="true">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QDoubleSpinBox" name="boundsWidth">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>1.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QDoubleSpinBox" name="boundsHeight">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>100</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="minimum">
+           <double>1.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>9001.000000000000000</double>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item row="6" column="1">
+      <widget class="QComboBox" name="boundsAlign">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="currentText">
+        <string>Basic.TransformWindow.Alignment.TopLeft</string>
+       </property>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopCenter</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.TopRight</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.CenterLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.Center</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.CenterRight</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomLeft</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomCenter</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Basic.TransformWindow.Alignment.BottomRight</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 305 - 18
obs/window-basic-main.cpp

@@ -26,6 +26,7 @@
 #include <util/dstr.h>
 #include <util/util.hpp>
 #include <util/platform.h>
+#include <graphics/math-defs.h>
 
 #include "obs-app.hpp"
 #include "platform.hpp"
@@ -47,6 +48,8 @@
 #include <QScreen>
 #include <QWindow>
 
+#define PREVIEW_EDGE_SIZE 10
+
 using namespace std;
 
 Q_DECLARE_METATYPE(OBSScene);
@@ -55,15 +58,6 @@ Q_DECLARE_METATYPE(order_movement);
 
 OBSBasic::OBSBasic(QWidget *parent)
 	: OBSMainWindow  (parent),
-	  properties     (nullptr),
-	  fileOutput     (nullptr),
-	  streamOutput   (nullptr),
-	  service        (nullptr),
-	  aac            (nullptr),
-	  x264           (nullptr),
-	  sceneChanging  (false),
-	  resizeTimer    (0),
-	  activeRefs     (0),
 	  ui             (new Ui::OBSBasic)
 {
 	ui->setupUi(this);
@@ -444,6 +438,28 @@ void OBSBasic::InitOBSCallbacks()
 			OBSBasic::SourceDeactivated, this);
 }
 
+void OBSBasic::InitPrimitives()
+{
+	gs_entercontext(obs_graphics());
+
+	gs_renderstart(true);
+	gs_vertex2f(0.0f, 0.0f);
+	gs_vertex2f(0.0f, 1.0f);
+	gs_vertex2f(1.0f, 1.0f);
+	gs_vertex2f(1.0f, 0.0f);
+	gs_vertex2f(0.0f, 0.0f);
+	box = gs_rendersave();
+
+	gs_renderstart(true);
+	for (int i = 0; i <= 360; i += (360/20)) {
+		float pos = RAD(float(i));
+		gs_vertex2f(cosf(pos), sinf(pos));
+	}
+	circle = gs_rendersave();
+
+	gs_leavecontext();
+}
+
 void OBSBasic::OBSInit()
 {
 	BPtr<char> savePath(os_get_config_path("obs-studio/basic/scenes.json"));
@@ -491,6 +507,8 @@ void OBSBasic::OBSInit()
 	if (!InitService())
 		throw "Failed to initialize service";
 
+	InitPrimitives();
+
 	Load(savePath);
 	ResetAudioDevices();
 }
@@ -501,14 +519,26 @@ OBSBasic::~OBSBasic()
 	SaveService();
 	Save(savePath);
 
+	/* XXX: any obs data must be released before calling obs_shutdown.
+	 * currently, we can't automate this with C++ RAII because of the
+	 * delicate nature of obs_shutdown needing to be freed before the UI
+	 * can be freed, and we have no control over the destruction order of
+	 * the Qt UI stuff, so we have to manually clear any references to
+	 * libobs. */
 	if (properties)
 		delete properties;
+	if (transformWindow)
+		delete transformWindow;
 
-	/* free the lists before shutting down to remove the scene/item
-	 * references */
 	ClearVolumeControls();
 	ui->sources->clear();
 	ui->scenes->clear();
+
+	gs_entercontext(obs_graphics());
+	vertexbuffer_destroy(box);
+	vertexbuffer_destroy(circle);
+	gs_leavecontext();
+
 	obs_shutdown();
 }
 
@@ -788,24 +818,73 @@ void OBSBasic::ChannelChanged(void *data, calldata_t params)
 				Q_ARG(OBSSource, OBSSource(source)));
 }
 
+void OBSBasic::DrawBackdrop(float cx, float cy)
+{
+	if (!box)
+		return;
+
+	effect_t    solid = obs_get_solid_effect();
+	eparam_t    color = effect_getparambyname(solid, "color");
+	technique_t tech  = effect_gettechnique(solid, "Solid");
+
+	vec4 colorVal;
+	vec4_set(&colorVal, 0.0f, 0.0f, 0.0f, 1.0f);
+	effect_setvec4(solid, color, &colorVal);
+
+	technique_begin(tech);
+	technique_beginpass(tech, 0);
+	gs_matrix_push();
+	gs_matrix_identity();
+	gs_matrix_scale3f(float(cx), float(cy), 1.0f);
+
+	gs_load_vertexbuffer(box);
+	gs_draw(GS_TRISTRIP, 0, 0);
+
+	gs_matrix_pop();
+	technique_endpass(tech);
+	technique_end(tech);
+
+	gs_load_vertexbuffer(nullptr);
+}
+
 void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy)
 {
 	OBSBasic *window = static_cast<OBSBasic*>(data);
 	obs_video_info ovi;
-	int newCX, newCY;
 
 	obs_get_video_info(&ovi);
 
-	newCX = int(window->previewScale * float(ovi.base_width));
-	newCY = int(window->previewScale * float(ovi.base_height));
+	window->previewCX = int(window->previewScale * float(ovi.base_width));
+	window->previewCY = int(window->previewScale * float(ovi.base_height));
 
 	gs_viewport_push();
 	gs_projection_push();
+
+	/* --------------------------------------- */
+
 	gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height),
 			-100.0f, 100.0f);
-	gs_setviewport(window->previewX, window->previewY, newCX, newCY);
+	gs_setviewport(window->previewX, window->previewY,
+			window->previewCX, window->previewCY);
+
+	window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
 
 	obs_render_main_view();
+	gs_load_vertexbuffer(nullptr);
+
+	/* --------------------------------------- */
+
+	float right  = float(window->ui->preview->width())  - window->previewX;
+	float bottom = float(window->ui->preview->height()) - window->previewY;
+
+	gs_ortho(-window->previewX, right,
+	         -window->previewY, bottom,
+	         -100.0f, 100.0f);
+	gs_resetviewport();
+
+	window->ui->preview->DrawSceneEditing();
+
+	/* --------------------------------------- */
 
 	gs_projection_pop();
 	gs_viewport_pop();
@@ -981,9 +1060,13 @@ void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy)
 	/* resize preview panel to fix to the top section of the window */
 	targetSize = GetPixelSize(ui->preview);
 	GetScaleAndCenterPos(int(cx), int(cy),
-			targetSize.width(), targetSize.height(),
+			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);
+
 	if (isVisible()) {
 		if (resizeTimer)
 			killTimer(resizeTimer);
@@ -1155,8 +1238,23 @@ void OBSBasic::on_actionSceneDown_triggered()
 void OBSBasic::on_sources_currentItemChanged(QListWidgetItem *current,
 		QListWidgetItem *prev)
 {
-	/* TODO */
-	UNUSED_PARAMETER(current);
+	auto select_one = [] (obs_scene_t scene, obs_sceneitem_t item,
+			void *param)
+	{
+		obs_sceneitem_t selectedItem =
+			*reinterpret_cast<OBSSceneItem*>(param);
+		obs_sceneitem_select(item, (selectedItem == item));
+
+		UNUSED_PARAMETER(scene);
+		return true;
+	};
+
+	if (!current)
+		return;
+
+	OBSSceneItem item = current->data(Qt::UserRole).value<OBSSceneItem>();
+	obs_scene_enum_items(GetCurrentScene(), select_one, &item);
+
 	UNUSED_PARAMETER(prev);
 }
 
@@ -1569,3 +1667,192 @@ config_t OBSBasic::Config() const
 {
 	return basicConfig;
 }
+
+void OBSBasic::on_actionEditTransform_triggered()
+{
+	delete transformWindow;
+	transformWindow = new OBSBasicTransform(this);
+	transformWindow->show();
+}
+
+void OBSBasic::on_actionResetTransform_triggered()
+{
+	auto func = [] (obs_scene_t scene, obs_sceneitem_t item, void *param)
+	{
+		if (!obs_sceneitem_selected(item))
+			return true;
+
+		obs_sceneitem_info info;
+		vec2_set(&info.pos, 0.0f, 0.0f);
+		vec2_set(&info.scale, 1.0f, 1.0f);
+		info.rot = 0.0f;
+		info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT;
+		info.bounds_type = OBS_BOUNDS_NONE;
+		info.bounds_alignment = OBS_ALIGN_CENTER;
+		vec2_set(&info.bounds, 0.0f, 0.0f);
+		obs_sceneitem_set_info(item, &info);
+
+		UNUSED_PARAMETER(scene);
+		UNUSED_PARAMETER(param);
+		return true;
+	};
+
+	obs_scene_enum_items(GetCurrentScene(), func, nullptr);
+}
+
+static vec3 GetItemTL(obs_sceneitem_t item)
+{
+	matrix4 boxTransform;
+	obs_sceneitem_get_box_transform(item, &boxTransform);
+
+	vec3 tl;
+	vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f);
+
+	auto GetMinPos = [&] (vec3 &val, float x, float y)
+	{
+		vec3 pos;
+		vec3_set(&pos, x, y, 0.0f);
+		vec3_transform(&pos, &pos, &boxTransform);
+		vec3_min(&val, &val, &pos);
+	};
+
+	GetMinPos(tl, 0.0f, 0.0f);
+	GetMinPos(tl, 1.0f, 0.0f);
+	GetMinPos(tl, 0.0f, 1.0f);
+	GetMinPos(tl, 1.0f, 1.0f);
+	return tl;
+}
+
+static void SetItemTL(obs_sceneitem_t item, const vec3 &tl)
+{
+	vec3 newTL;
+	vec2 pos;
+
+	obs_sceneitem_getpos(item, &pos);
+	newTL = GetItemTL(item);
+	pos.x += tl.x - newTL.x;
+	pos.y += tl.y - newTL.y;
+	obs_sceneitem_setpos(item, &pos);
+}
+
+static bool RotateSelectedSources(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	float rot = *reinterpret_cast<float*>(param);
+
+	vec3 tl = GetItemTL(item);
+
+	rot += obs_sceneitem_getrot(item);
+	if (rot >= 360.0f)       rot -= 360.0f;
+	else if (rot <= -360.0f) rot += 360.0f;
+	obs_sceneitem_setrot(item, rot);
+
+	SetItemTL(item, tl);
+
+	UNUSED_PARAMETER(scene);
+	UNUSED_PARAMETER(param);
+	return true;
+};
+
+void OBSBasic::on_actionRotate90CW_triggered()
+{
+	float f90CW = 90.0f;
+	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW);
+}
+
+void OBSBasic::on_actionRotate90CCW_triggered()
+{
+	float f90CCW = -90.0f;
+	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW);
+}
+
+void OBSBasic::on_actionRotate180_triggered()
+{
+	float f180 = 180.0f;
+	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180);
+}
+
+static bool MultiplySelectedItemScale(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	vec2 &mul = *reinterpret_cast<vec2*>(param);
+
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	vec3 tl = GetItemTL(item);
+
+	vec2 scale;
+	obs_sceneitem_getscale(item, &scale);
+	vec2_mul(&scale, &scale, &mul);
+	obs_sceneitem_setscale(item, &scale);
+
+	SetItemTL(item, tl);
+	return true;
+}
+
+void OBSBasic::on_actionFlipHorizontal_triggered()
+{
+	vec2 scale = {-1.0f, 1.0f};
+	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
+			&scale);
+}
+
+void OBSBasic::on_actionFlipVertical_triggered()
+{
+	vec2 scale = {1.0f, -1.0f};
+	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
+			&scale);
+}
+
+static bool CenterAlignSelectedItems(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	obs_bounds_type boundsType = *reinterpret_cast<obs_bounds_type*>(param);
+
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	obs_sceneitem_info itemInfo;
+	vec2_set(&itemInfo.pos, 0.0f, 0.0f);
+	vec2_set(&itemInfo.scale, 1.0f, 1.0f);
+	itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP;
+	itemInfo.rot = 0.0f;
+
+	vec2_set(&itemInfo.bounds,
+			float(ovi.base_width), float(ovi.base_height));
+	itemInfo.bounds_type = boundsType;
+	itemInfo.bounds_alignment = OBS_ALIGN_CENTER;
+
+	obs_sceneitem_set_info(item, &itemInfo);
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+void OBSBasic::on_actionFitToScreen_triggered()
+{
+	obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
+	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
+			&boundsType);
+}
+
+void OBSBasic::on_actionStretchToScreen_triggered()
+{
+	obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
+	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
+			&boundsType);
+}
+
+void OBSBasic::on_actionCenterToScreen_triggered()
+{
+	obs_bounds_type boundsType = OBS_BOUNDS_MAX_ONLY;
+	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
+			&boundsType);
+}

+ 48 - 14
obs/window-basic-main.hpp

@@ -25,6 +25,7 @@
 #include <memory>
 #include "window-main.hpp"
 #include "window-basic-properties.hpp"
+#include "window-basic-transform.hpp"
 
 #include <util/util.hpp>
 
@@ -45,34 +46,43 @@ class QNetworkReply;
 class OBSBasic : public OBSMainWindow {
 	Q_OBJECT
 
+	friend class OBSBasicPreview;
+
 private:
 	std::unordered_map<obs_source_t, int> sourceSceneRefs;
 
 	std::vector<VolControl*> volumes;
 
 	QPointer<OBSBasicProperties> properties;
+	QPointer<OBSBasicTransform> transformWindow;
 
 	QNetworkAccessManager networkManager;
 
 	QBuffer       logUploadPostData;
-	QNetworkReply *logUploadReply;
+	QNetworkReply *logUploadReply = nullptr;
 	QByteArray    logUploadReturnData;
 
-	obs_output_t  fileOutput;
-	obs_output_t  streamOutput;
-	obs_service_t service;
-	obs_encoder_t aac;
-	obs_encoder_t x264;
+	obs_output_t  fileOutput = nullptr;
+	obs_output_t  streamOutput = nullptr;
+	obs_service_t service = nullptr;
+	obs_encoder_t aac = nullptr;
+	obs_encoder_t x264 = nullptr;
+
+	vertbuffer_t  box = nullptr;
+	vertbuffer_t  circle = nullptr;
 
-	bool          sceneChanging;
+	bool          sceneChanging = false;
 
-	int           previewX,  previewY;
-	float         previewScale;
-	int           resizeTimer;
+	int           previewX = 0,  previewY = 0;
+	int           previewCX = 0, previewCY = 0;
+	float         previewScale = 0.0f;
+	int           resizeTimer = 0;
 
 	ConfigFile    basicConfig;
 
-	int           activeRefs;
+	int           activeRefs = 0;
+
+	void          DrawBackdrop(float cx, float cy);
 
 	void          SetupEncoders();
 
@@ -97,7 +107,8 @@ private:
 
 	void          InitOBSCallbacks();
 
-	OBSScene      GetCurrentScene();
+	void          InitPrimitives();
+
 	OBSSceneItem  GetCurrentSceneItem();
 
 	void GetFPSCommon(uint32_t &num, uint32_t &den) const;
@@ -153,6 +164,8 @@ private:
 	void AddSourcePopupMenu(const QPoint &pos);
 
 public:
+	OBSScene      GetCurrentScene();
+
 	obs_service_t GetService();
 	void          SetService(obs_service_t service);
 
@@ -167,6 +180,14 @@ public:
 	void SaveProject();
 	void LoadProject();
 
+	inline void GetDisplayRect(int &x, int &y, int &cx, int &cy)
+	{
+		x  = previewX;
+		y  = previewY;
+		cx = previewCX;
+		cy = previewCY;
+	}
+
 protected:
 	virtual void closeEvent(QCloseEvent *event) override;
 	virtual void changeEvent(QEvent *event) override;
@@ -178,6 +199,20 @@ private slots:
 	void on_action_Open_triggered();
 	void on_action_Save_triggered();
 	void on_action_Settings_triggered();
+	void on_actionUploadCurrentLog_triggered();
+	void on_actionUploadLastLog_triggered();
+
+	void on_actionEditTransform_triggered();
+	void on_actionResetTransform_triggered();
+	void on_actionRotate90CW_triggered();
+	void on_actionRotate90CCW_triggered();
+	void on_actionRotate180_triggered();
+	void on_actionFlipHorizontal_triggered();
+	void on_actionFlipVertical_triggered();
+	void on_actionFitToScreen_triggered();
+	void on_actionStretchToScreen_triggered();
+	void on_actionCenterToScreen_triggered();
+
 	void on_scenes_currentItemChanged(QListWidgetItem *current,
 			QListWidgetItem *prev);
 	void on_scenes_customContextMenuRequested(const QPoint &pos);
@@ -194,8 +229,7 @@ private slots:
 	void on_actionSourceProperties_triggered();
 	void on_actionSourceUp_triggered();
 	void on_actionSourceDown_triggered();
-	void on_actionUploadCurrentLog_triggered();
-	void on_actionUploadLastLog_triggered();
+
 	void on_streamButton_clicked();
 	void on_recordButton_clicked();
 	void on_settingsButton_clicked();

+ 718 - 0
obs/window-basic-preview.cpp

@@ -0,0 +1,718 @@
+#include <QGuiApplication>
+#include <QMouseEvent>
+
+#include <cmath>
+#include <graphics/vec4.h>
+#include <graphics/matrix4.h>
+#include "window-basic-preview.hpp"
+#include "window-basic-main.hpp"
+#include "obs-app.hpp"
+
+#define HANDLE_RADIUS     4.0f
+#define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f)
+#define CLAMP_DISTANCE    10.0f
+
+/* TODO: make C++ math classes and clean up code here later */
+
+OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags)
+	: OBSQTDisplay(parent, flags)
+{
+	setMouseTracking(true);
+}
+
+vec2 OBSBasicPreview::GetMouseEventPos(QMouseEvent *event)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+	vec2 pos = {
+		(float(event->x()) - main->previewX) / main->previewScale,
+		(float(event->y()) - main->previewY) / main->previewScale
+	};
+
+	return pos;
+}
+
+struct SceneFindData {
+	const vec2   &pos;
+	OBSSceneItem item;
+	bool         selectBelow;
+
+	inline SceneFindData(const vec2 &pos_, bool selectBelow_)
+		: pos         (pos_),
+		  selectBelow (selectBelow_)
+	{}
+};
+
+static bool FindItemAtPos(obs_scene_t scene, obs_sceneitem_t item, void *param)
+{
+	SceneFindData *data = reinterpret_cast<SceneFindData*>(param);
+	matrix4       transform;
+	vec3          transformedPos;
+	vec3          pos3 = {data->pos.x, data->pos.y, 0.0f};
+
+	obs_sceneitem_get_box_transform(item, &transform);
+
+	matrix4_inv(&transform, &transform);
+	vec3_transform(&transformedPos, &pos3, &transform);
+
+	if (transformedPos.x >= 0.0f && transformedPos.x <= 1.0f &&
+	    transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) {
+		if (data->selectBelow && obs_sceneitem_selected(item)) {
+			if (data->item)
+				return false;
+			else
+				data->selectBelow = false;
+		}
+
+		data->item = item;
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+static vec3 GetTransformedPos(float x, float y, const matrix4 &mat)
+{
+	vec3 result;
+	vec3_set(&result, x, y, 0.0f);
+	vec3_transform(&result, &result, &mat);
+	return result;
+}
+
+static vec3 GetTransformedPosScaled(float x, float y, const matrix4 &mat,
+		float scale)
+{
+	vec3 result;
+	vec3_set(&result, x, y, 0.0f);
+	vec3_transform(&result, &result, &mat);
+	vec3_mulf(&result, &result, scale);
+	return result;
+}
+
+static inline vec2 GetOBSScreenSize()
+{
+	obs_video_info ovi;
+	vec2 size = {0.0f, 0.0f};
+
+	if (obs_get_video_info(&ovi)) {
+		size.x = float(ovi.base_width);
+		size.y = float(ovi.base_height);
+	}
+
+	return size;
+}
+
+vec3 OBSBasicPreview::GetScreenSnapOffset(const vec3 &tl, const vec3 &br)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+	vec2 screenSize = GetOBSScreenSize();
+	vec3 clampOffset;
+
+	vec3_zero(&clampOffset);
+
+	const float clampDist = CLAMP_DISTANCE / main->previewScale;
+
+	if (fabsf(tl.x) < clampDist)
+		clampOffset.x = -tl.x;
+	if (fabsf(clampOffset.x) < EPSILON &&
+	    fabsf(screenSize.x - br.x) < clampDist)
+		clampOffset.x = screenSize.x - br.x;
+
+	if (fabsf(tl.y) < clampDist)
+		clampOffset.y = -tl.y;
+	if (fabsf(clampOffset.y) < EPSILON &&
+	    fabsf(screenSize.y - br.y) < clampDist)
+		clampOffset.y = screenSize.y - br.y;
+
+	return clampOffset;
+}
+
+OBSSceneItem OBSBasicPreview::GetItemAtPos(const vec2 &pos, bool selectBelow)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	OBSScene scene = main->GetCurrentScene();
+	if (!scene)
+		return OBSSceneItem();
+
+	SceneFindData data(pos, selectBelow);
+	obs_scene_enum_items(scene, FindItemAtPos, &data);
+	return data.item;
+}
+
+static bool CheckItemSelected(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	SceneFindData *data = reinterpret_cast<SceneFindData*>(param);
+	matrix4       transform;
+	vec3          transformedPos;
+	vec3          pos3 = {data->pos.x, data->pos.y, 0.0f};
+
+	obs_sceneitem_get_box_transform(item, &transform);
+
+	matrix4_inv(&transform, &transform);
+	vec3_transform(&transformedPos, &pos3, &transform);
+
+	if (transformedPos.x >= 0.0f && transformedPos.x <= 1.0f &&
+	    transformedPos.y >= 0.0f && transformedPos.y <= 1.0f) {
+		if (obs_sceneitem_selected(item)) {
+			data->item = item;
+			return false;
+		}
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+bool OBSBasicPreview::SelectedAtPos(const vec2 &pos)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	OBSScene scene = main->GetCurrentScene();
+	if (!scene)
+		return false;
+
+	SceneFindData data(pos, false);
+	obs_scene_enum_items(scene, CheckItemSelected, &data);
+	return !!data.item;
+}
+
+struct HandleFindData {
+	const vec2   &pos;
+	const float  scale;
+
+	OBSSceneItem item;
+	ItemHandle   handle = ItemHandle::None;
+
+	inline HandleFindData(const vec2 &pos_, float scale_)
+		: pos   (pos_),
+		  scale (scale_)
+	{}
+};
+
+static bool FindHandleAtPos(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	HandleFindData *data = reinterpret_cast<HandleFindData*>(param);
+	matrix4        transform;
+	vec3           pos3 = {data->pos.x, data->pos.y, 0.0f};
+	float          closestHandle = HANDLE_SEL_RADIUS;
+
+	obs_sceneitem_get_box_transform(item, &transform);
+
+	auto TestHandle = [&] (float x, float y, ItemHandle handle)
+	{
+		vec3 handlePos = GetTransformedPosScaled(x, y, transform,
+				data->scale);
+
+		float dist = vec3_dist(&handlePos, &pos3);
+		if (dist < HANDLE_SEL_RADIUS) {
+			if (dist < closestHandle) {
+				closestHandle = dist;
+				data->handle  = handle;
+				data->item    = item;
+			}
+		}
+	};
+
+	TestHandle(0.0f, 0.0f, ItemHandle::TopLeft);
+	TestHandle(0.5f, 0.0f, ItemHandle::TopCenter);
+	TestHandle(1.0f, 0.0f, ItemHandle::TopRight);
+	TestHandle(0.0f, 0.5f, ItemHandle::CenterLeft);
+	TestHandle(1.0f, 0.5f, ItemHandle::CenterRight);
+	TestHandle(0.0f, 1.0f, ItemHandle::BottomLeft);
+	TestHandle(0.5f, 1.0f, ItemHandle::BottomCenter);
+	TestHandle(1.0f, 1.0f, ItemHandle::BottomRight);
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+static vec2 GetItemSize(obs_sceneitem_t item)
+{
+	obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(item);
+	vec2 size;
+
+	if (boundsType != OBS_BOUNDS_NONE) {
+		obs_sceneitem_get_bounds(item, &size);
+	} else {
+		obs_source_t source = obs_sceneitem_getsource(item);
+		vec2 scale;
+
+		obs_sceneitem_getscale(item, &scale);
+		size.x = float(obs_source_getwidth(source))  * scale.x;
+		size.y = float(obs_source_getheight(source)) * scale.y;
+	}
+
+	return size;
+}
+
+void OBSBasicPreview::GetStretchHandleData(const vec2 &pos)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	OBSScene scene = main->GetCurrentScene();
+	if (!scene)
+		return;
+
+	HandleFindData data(pos, main->previewScale);
+	obs_scene_enum_items(scene, FindHandleAtPos, &data);
+
+	stretchItem     = std::move(data.item);
+	stretchHandle   = data.handle;
+
+	if (stretchHandle != ItemHandle::None) {
+		matrix4 boxTransform;
+		vec3    itemUL;
+		float   itemRot;
+
+		stretchItemSize = GetItemSize(stretchItem);
+
+		obs_sceneitem_get_box_transform(stretchItem, &boxTransform);
+		itemRot = obs_sceneitem_getrot(stretchItem);
+		vec3_from_vec4(&itemUL, &boxTransform.t);
+
+		/* build the item space conversion matrices */
+		matrix4_identity(&itemToScreen);
+		matrix4_rotate_aa4f(&itemToScreen, &itemToScreen,
+				0.0f, 0.0f, 1.0f, RAD(itemRot));
+		matrix4_translate3f(&itemToScreen, &itemToScreen,
+				itemUL.x, itemUL.y, 0.0f);
+
+		matrix4_identity(&screenToItem);
+		matrix4_translate3f(&screenToItem, &screenToItem,
+				-itemUL.x, -itemUL.y, 0.0f);
+		matrix4_rotate_aa4f(&screenToItem, &screenToItem,
+				0.0f, 0.0f, 1.0f, RAD(-itemRot));
+	}
+}
+
+void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+	float x = float(event->x()) - main->previewX;
+	float y = float(event->y()) - main->previewY;
+
+	if (event->button() != Qt::LeftButton ||
+	    x < 0.0f || y < 0.0f || x > main->previewCX || y > main->previewCY)
+		return;
+
+	mouseDown = true;
+
+	vec2_set(&startPos, x, y);
+	GetStretchHandleData(startPos);
+
+	vec2_divf(&startPos, &startPos, main->previewScale);
+	startPos.x = std::round(startPos.x);
+	startPos.y = std::round(startPos.y);
+
+	mouseOverItems = SelectedAtPos(startPos);
+	vec2_zero(&lastMoveOffset);
+}
+
+static bool select_one(obs_scene_t scene, obs_sceneitem_t item, void *param)
+{
+	obs_sceneitem_t selectedItem = reinterpret_cast<obs_sceneitem_t>(param);
+	obs_sceneitem_select(item, (selectedItem == item));
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+void OBSBasicPreview::DoSelect(const vec2 &pos)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	OBSScene     scene = main->GetCurrentScene();
+	OBSSceneItem item  = GetItemAtPos(pos, true);
+
+	obs_scene_enum_items(scene, select_one, (obs_sceneitem_t)item);
+}
+
+void OBSBasicPreview::DoCtrlSelect(const vec2 &pos)
+{
+	OBSSceneItem item = GetItemAtPos(pos, false);
+	if (!item)
+		return;
+
+	bool selected = obs_sceneitem_selected(item);
+	obs_sceneitem_select(item, !selected);
+}
+
+void OBSBasicPreview::ProcessClick(const vec2 &pos)
+{
+	Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
+
+	if (modifiers & Qt::ControlModifier)
+		DoCtrlSelect(pos);
+	else
+		DoSelect(pos);
+}
+
+void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
+{
+	if (mouseDown) {
+		vec2 pos = GetMouseEventPos(event);
+
+		if (!mouseMoved)
+			ProcessClick(pos);
+
+		stretchItem = nullptr;
+		mouseDown   = false;
+		mouseMoved  = false;
+	}
+}
+
+struct SelectedItemBounds {
+	bool first = true;
+	vec3 tl, br;
+};
+
+static bool AddItemBounds(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	SelectedItemBounds *data = reinterpret_cast<SelectedItemBounds*>(param);
+
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	matrix4 boxTransform;
+	obs_sceneitem_get_box_transform(item, &boxTransform);
+
+	vec3 t[4] = {
+		GetTransformedPos(0.0f, 0.0f, boxTransform),
+		GetTransformedPos(1.0f, 0.0f, boxTransform),
+		GetTransformedPos(0.0f, 1.0f, boxTransform),
+		GetTransformedPos(1.0f, 1.0f, boxTransform)
+	};
+
+	for (const vec3 &v : t) {
+		if (data->first) {
+			vec3_copy(&data->tl, &v);
+			vec3_copy(&data->br, &v);
+			data->first = false;
+		} else {
+			vec3_min(&data->tl, &data->tl, &v);
+			vec3_max(&data->br, &data->br, &v);
+		}
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+void OBSBasicPreview::SnapItemMovement(vec2 &offset)
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+	OBSScene scene = main->GetCurrentScene();
+
+	SelectedItemBounds data;
+	obs_scene_enum_items(scene, AddItemBounds, &data);
+
+	data.tl.x += offset.x;
+	data.tl.y += offset.y;
+	data.br.x += offset.x;
+	data.br.y += offset.y;
+
+	vec3 snapOffset = GetScreenSnapOffset(data.tl, data.br);
+	offset.x += snapOffset.x;
+	offset.y += snapOffset.y;
+}
+
+static bool move_items(obs_scene_t scene, obs_sceneitem_t item, void *param)
+{
+	vec2 *offset = reinterpret_cast<vec2*>(param);
+
+	if (obs_sceneitem_selected(item)) {
+		vec2 pos;
+		obs_sceneitem_getpos(item, &pos);
+		vec2_add(&pos, &pos, offset);
+		obs_sceneitem_setpos(item, &pos);
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+void OBSBasicPreview::MoveItems(const vec2 &pos)
+{
+	Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+	OBSScene scene = main->GetCurrentScene();
+
+	vec2 offset, moveOffset;
+	vec2_sub(&offset, &pos, &startPos);
+	vec2_sub(&moveOffset, &offset, &lastMoveOffset);
+
+	if (!(modifiers & Qt::ControlModifier))
+		SnapItemMovement(moveOffset);
+
+	vec2_add(&lastMoveOffset, &lastMoveOffset, &moveOffset);
+
+	obs_scene_enum_items(scene, move_items, &moveOffset);
+}
+
+vec3 OBSBasicPreview::CalculateStretchPos(const vec3 &tl, const vec3 &br)
+{
+	uint32_t alignment = obs_sceneitem_getalignment(stretchItem);
+	vec3 pos;
+
+	vec3_zero(&pos);
+
+	if (alignment & OBS_ALIGN_LEFT)
+		pos.x = tl.x;
+	else if (alignment & OBS_ALIGN_RIGHT)
+		pos.x = br.x;
+	else
+		pos.x = (br.x - tl.x) * 0.5f + tl.x;
+
+	if (alignment & OBS_ALIGN_TOP)
+		pos.y = tl.y;
+	else if (alignment & OBS_ALIGN_BOTTOM)
+		pos.y = br.y;
+	else
+		pos.y = (br.y - tl.y) * 0.5f + tl.y;
+
+	return pos;
+}
+
+void OBSBasicPreview::ClampAspect(vec3 &tl, vec3 &br, vec2 &size,
+		const vec2 &baseSize)
+{
+	float    baseAspect   = baseSize.x / baseSize.y;
+	float    aspect       = size.x / size.y;
+	uint32_t stretchFlags = (uint32_t)stretchHandle;
+
+	if (stretchHandle == ItemHandle::TopLeft    ||
+	    stretchHandle == ItemHandle::TopRight   ||
+	    stretchHandle == ItemHandle::BottomLeft ||
+	    stretchHandle == ItemHandle::BottomRight) {
+		if (aspect < baseAspect)
+			size.x = size.y * baseAspect;
+		else
+			size.y = size.x / baseAspect;
+
+	} else if (stretchHandle == ItemHandle::TopCenter ||
+	           stretchHandle == ItemHandle::BottomCenter) {
+		size.x = size.y * baseAspect;
+
+	} else if (stretchHandle == ItemHandle::CenterLeft ||
+	           stretchHandle == ItemHandle::CenterRight) {
+		size.y = size.x / baseAspect;
+	}
+
+	size.x = std::round(size.x);
+	size.y = std::round(size.y);
+
+	if (stretchFlags & ITEM_LEFT)
+		tl.x = br.x - size.x;
+	else if (stretchFlags & ITEM_RIGHT)
+		br.x = tl.x + size.x;
+
+	if (stretchFlags & ITEM_TOP)
+		tl.y = br.y - size.y;
+	else if (stretchFlags & ITEM_BOTTOM)
+		br.y = tl.y + size.y;
+}
+
+void OBSBasicPreview::SnapStretchingToScreen(vec3 &tl, vec3 &br)
+{
+	uint32_t stretchFlags = (uint32_t)stretchHandle;
+	vec3     newTL        = GetTransformedPos(tl.x, tl.y, itemToScreen);
+	vec3     newTR        = GetTransformedPos(br.x, tl.y, itemToScreen);
+	vec3     newBL        = GetTransformedPos(tl.x, br.y, itemToScreen);
+	vec3     newBR        = GetTransformedPos(br.x, br.y, itemToScreen);
+	vec3     boundingTL;
+	vec3     boundingBR;
+
+	vec3_copy(&boundingTL, &newTL);
+	vec3_min(&boundingTL, &boundingTL, &newTR);
+	vec3_min(&boundingTL, &boundingTL, &newBL);
+	vec3_min(&boundingTL, &boundingTL, &newBR);
+
+	vec3_copy(&boundingBR, &newTL);
+	vec3_max(&boundingBR, &boundingBR, &newTR);
+	vec3_max(&boundingBR, &boundingBR, &newBL);
+	vec3_max(&boundingBR, &boundingBR, &newBR);
+
+	vec3 offset = GetScreenSnapOffset(boundingTL, boundingBR);
+	vec3_add(&offset, &offset, &newTL);
+	vec3_transform(&offset, &offset, &screenToItem);
+	vec3_sub(&offset, &offset, &tl);
+
+	if (stretchFlags & ITEM_LEFT)
+		tl.x += offset.x;
+	else if (stretchFlags & ITEM_RIGHT)
+		br.x += offset.x;
+
+	if (stretchFlags & ITEM_TOP)
+		tl.y += offset.y;
+	else if (stretchFlags & ITEM_BOTTOM)
+		br.y += offset.y;
+}
+
+void OBSBasicPreview::StretchItem(const vec2 &pos)
+{
+	Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
+	obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(stretchItem);
+	uint32_t stretchFlags = (uint32_t)stretchHandle;
+	bool shiftDown = (modifiers & Qt::ShiftModifier);
+	vec3 tl, br, pos3;
+
+	vec3_zero(&tl);
+	vec3_set(&br, stretchItemSize.x, stretchItemSize.y, 0.0f);
+
+	vec3_set(&pos3, pos.x, pos.y, 0.0f);
+	vec3_transform(&pos3, &pos3, &screenToItem);
+
+	if (stretchFlags & ITEM_LEFT)
+		tl.x = pos3.x;
+	else if (stretchFlags & ITEM_RIGHT)
+		br.x = pos3.x;
+
+	if (stretchFlags & ITEM_TOP)
+		tl.y = pos3.y;
+	else if (stretchFlags & ITEM_BOTTOM)
+		br.y = pos3.y;
+
+	if (!(modifiers & Qt::ControlModifier))
+		SnapStretchingToScreen(tl, br);
+
+	obs_source_t source = obs_sceneitem_getsource(stretchItem);
+	vec2 baseSize = {
+		float(obs_source_getwidth(source)),
+		float(obs_source_getheight(source))
+	};
+
+	vec2 size = {br.x - tl.x, br.y - tl.y};
+
+	if (boundsType != OBS_BOUNDS_NONE) {
+		if (boundsType == OBS_BOUNDS_STRETCH && !shiftDown)
+			ClampAspect(tl, br, size, baseSize);
+
+		if (tl.x > br.x) std::swap(tl.x, br.x);
+		if (tl.y > br.y) std::swap(tl.y, br.y);
+
+		vec2_abs(&size, &size);
+
+		obs_sceneitem_set_bounds(stretchItem, &size);
+	} else {
+		if (!shiftDown)
+			ClampAspect(tl, br, size, baseSize);
+
+		vec2_div(&size, &size, &baseSize);
+		obs_sceneitem_setscale(stretchItem, &size);
+	}
+
+	pos3 = CalculateStretchPos(tl, br);
+	vec3_transform(&pos3, &pos3, &itemToScreen);
+	vec2 newPos = {pos3.x, pos3.y};
+
+	newPos.x = std::round(newPos.x);
+	newPos.y = std::round(newPos.y);
+
+	obs_sceneitem_setpos(stretchItem, &newPos);
+}
+
+void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
+{
+	if (mouseDown) {
+		vec2 pos = GetMouseEventPos(event);
+
+		if (!mouseMoved && !mouseOverItems &&
+		    stretchHandle == ItemHandle::None) {
+			ProcessClick(startPos);
+			mouseOverItems = SelectedAtPos(startPos);
+		}
+
+		pos.x = std::round(pos.x);
+		pos.y = std::round(pos.y);
+
+		if (stretchHandle != ItemHandle::None)
+			StretchItem(pos);
+		else if (mouseOverItems)
+			MoveItems(pos);
+
+		mouseMoved = true;
+	}
+}
+
+static void DrawCircleAtPos(float x, float y, matrix4 &matrix,
+		float previewScale)
+{
+	struct vec3 pos;
+	vec3_set(&pos, x, y, 0.0f);
+	vec3_transform(&pos, &pos, &matrix);
+	vec3_mulf(&pos, &pos, previewScale);
+
+	gs_matrix_push();
+	gs_matrix_translate(&pos);
+	gs_draw(GS_LINESTRIP, 0, 0);
+	gs_matrix_pop();
+}
+
+bool OBSBasicPreview::DrawSelectedItem(obs_scene_t scene, obs_sceneitem_t item,
+		void *param)
+{
+	if (!obs_sceneitem_selected(item))
+		return true;
+
+	OBSBasicPreview *preview = reinterpret_cast<OBSBasicPreview*>(param);
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	gs_load_vertexbuffer(main->circle);
+
+	matrix4 boxTransform;
+	obs_sceneitem_get_box_transform(item, &boxTransform);
+
+	gs_matrix_push();
+	gs_matrix_scale3f(HANDLE_RADIUS, HANDLE_RADIUS, 1.0f);
+	DrawCircleAtPos(0.0f, 0.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(0.0f, 1.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(1.0f, 0.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(1.0f, 1.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(0.5f, 0.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(0.0f, 0.5f, boxTransform, main->previewScale);
+	DrawCircleAtPos(0.5f, 1.0f, boxTransform, main->previewScale);
+	DrawCircleAtPos(1.0f, 0.5f, boxTransform, main->previewScale);
+	gs_matrix_pop();
+
+	gs_load_vertexbuffer(main->box);
+
+	gs_matrix_push();
+	gs_matrix_set(&boxTransform);
+	gs_matrix_scale3f(main->previewScale, main->previewScale, 1.0f);
+	gs_draw(GS_LINESTRIP, 0, 0);
+
+	gs_matrix_pop();
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+void OBSBasicPreview::DrawSceneEditing()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
+
+	effect_t    solid = obs_get_solid_effect();
+	technique_t tech  = effect_gettechnique(solid, "Solid");
+
+	vec4 color;
+	vec4_set(&color, 1.0f, 0.0f, 0.0f, 1.0f);
+	effect_setvec4(solid, effect_getparambyname(solid, "color"), &color);
+
+	technique_begin(tech);
+	technique_beginpass(tech, 0);
+
+	OBSScene scene = main->GetCurrentScene();
+	if (scene)
+		obs_scene_enum_items(scene, DrawSelectedItem, this);
+
+	gs_load_vertexbuffer(nullptr);
+
+	technique_endpass(tech);
+	technique_end(tech);
+}

+ 84 - 0
obs/window-basic-preview.hpp

@@ -0,0 +1,84 @@
+#pragma once
+
+#include <obs.hpp>
+#include <graphics/vec2.h>
+#include <graphics/matrix4.h>
+#include "qt-display.hpp"
+#include "obs-app.hpp"
+
+class OBSBasic;
+class QMouseEvent;
+
+#define ITEM_LEFT   (1<<0)
+#define ITEM_RIGHT  (1<<1)
+#define ITEM_TOP    (1<<2)
+#define ITEM_BOTTOM (1<<3)
+
+enum class ItemHandle : uint32_t {
+	None         = 0,
+	TopLeft      = ITEM_TOP | ITEM_LEFT,
+	TopCenter    = ITEM_TOP,
+	TopRight     = ITEM_TOP | ITEM_RIGHT,
+	CenterLeft   = ITEM_LEFT,
+	CenterRight  = ITEM_RIGHT,
+	BottomLeft   = ITEM_BOTTOM | ITEM_LEFT,
+	BottomCenter = ITEM_BOTTOM,
+	BottomRight  = ITEM_BOTTOM | ITEM_RIGHT
+};
+
+class OBSBasicPreview : public OBSQTDisplay {
+	Q_OBJECT
+
+private:
+	OBSSceneItem stretchItem;
+	ItemHandle   stretchHandle = ItemHandle::None;
+	vec2         stretchItemSize;
+	matrix4      screenToItem;
+	matrix4      itemToScreen;
+
+	vec2         startPos;
+	vec2         lastMoveOffset;
+	bool         mouseDown      = false;
+	bool         mouseMoved     = false;
+	bool         mouseOverItems = false;
+
+	static vec2 GetMouseEventPos(QMouseEvent *event);
+	static bool DrawSelectedItem(obs_scene_t scene, obs_sceneitem_t item,
+		void *param);
+
+	static OBSSceneItem GetItemAtPos(const vec2 &pos, bool selectBelow);
+	static bool SelectedAtPos(const vec2 &pos);
+
+	static void DoSelect(const vec2 &pos);
+	static void DoCtrlSelect(const vec2 &pos);
+
+	static vec3 GetScreenSnapOffset(const vec3 &tl, const vec3 &br);
+
+	void GetStretchHandleData(const vec2 &pos);
+
+	void SnapStretchingToScreen(vec3 &tl, vec3 &br);
+	void ClampAspect(vec3 &tl, vec3 &br, vec2 &size, const vec2 &baseSize);
+	vec3 CalculateStretchPos(const vec3 &tl, const vec3 &br);
+	void StretchItem(const vec2 &pos);
+
+	static void SnapItemMovement(vec2 &offset);
+	void MoveItems(const vec2 &pos);
+
+	void ProcessClick(const vec2 &pos);
+
+public:
+	OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags = 0);
+
+	virtual void mousePressEvent(QMouseEvent *event) override;
+	virtual void mouseReleaseEvent(QMouseEvent *event) override;
+	virtual void mouseMoveEvent(QMouseEvent *event) override;
+
+	void DrawSceneEditing();
+
+	/* 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
+	 * byte boundry. */
+	static inline void* operator new(size_t size) {return bmalloc(size);}
+	static inline void operator delete(void* ptr) {bfree(ptr);}
+};

+ 0 - 1
obs/window-basic-settings.cpp

@@ -92,7 +92,6 @@ void OBSBasicSettings::HookWidget(QWidget *widget, const char *signal,
 	QObject::connect(widget, signal, this, slot);
 }
 
-#define COMBO_CHANGED   SIGNAL(currentIndexChanged(int))
 #define COMBO_CHANGED   SIGNAL(currentIndexChanged(int))
 #define EDIT_CHANGED    SIGNAL(textChanged(const QString &))
 #define CBEDIT_CHANGED  SIGNAL(editTextChanged(const QString &))

+ 253 - 0
obs/window-basic-transform.cpp

@@ -0,0 +1,253 @@
+#include "window-basic-transform.hpp"
+#include "window-basic-main.hpp"
+
+Q_DECLARE_METATYPE(OBSSceneItem);
+
+static OBSSceneItem FindASelectedItem(OBSScene scene)
+{
+	auto func = [] (obs_scene_t scene, obs_sceneitem_t item, void *param)
+	{
+		OBSSceneItem &dst = *reinterpret_cast<OBSSceneItem*>(param);
+
+		if (obs_sceneitem_selected(item)) {
+			dst = item;
+			return false;
+		}
+
+		return true;
+	};
+
+	OBSSceneItem item;
+	obs_scene_enum_items(scene, func, &item);
+	return item;
+}
+
+void OBSBasicTransform::HookWidget(QWidget *widget, const char *signal,
+		const char *slot)
+{
+	QObject::connect(widget, signal, this, slot);
+}
+
+#define COMBO_CHANGED   SIGNAL(currentIndexChanged(int))
+#define DSCROLL_CHANGED SIGNAL(valueChanged(double))
+
+OBSBasicTransform::OBSBasicTransform(OBSBasic *parent)
+	: QDialog (parent),
+	  ui      (new Ui::OBSBasicTransform),
+	  main    (parent)
+{
+	setAttribute(Qt::WA_DeleteOnClose);
+
+	ui->setupUi(this);
+
+	HookWidget(ui->positionX,    DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->positionY,    DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->rotation,     DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->scaleX,       DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->scaleY,       DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->align,        COMBO_CHANGED,   SLOT(OnControlChanged()));
+	HookWidget(ui->boundsType,   COMBO_CHANGED,   SLOT(OnBoundsType(int)));
+	HookWidget(ui->boundsAlign,  COMBO_CHANGED,   SLOT(OnControlChanged()));
+	HookWidget(ui->boundsWidth,  DSCROLL_CHANGED, SLOT(OnControlChanged()));
+	HookWidget(ui->boundsHeight, DSCROLL_CHANGED, SLOT(OnControlChanged()));
+
+	OBSScene curScene = main->GetCurrentScene();
+	SetScene(curScene);
+	SetItem(FindASelectedItem(curScene));
+
+	channelChangedSignal.Connect(obs_signalhandler(), "channel_change",
+			OBSChannelChanged, this);
+}
+
+void OBSBasicTransform::SetScene(OBSScene scene)
+{
+	transformSignal.Disconnect();
+	selectSignal.Disconnect();
+	deselectSignal.Disconnect();
+	removeSignal.Disconnect();
+
+	if (scene) {
+		OBSSource source = obs_scene_getsource(scene);
+		signal_handler_t signal = obs_source_signalhandler(source);
+
+		transformSignal.Connect(signal, "item_transform",
+				OBSSceneItemTransform, this);
+		removeSignal.Connect(signal, "item_remove",
+				OBSSceneItemRemoved, this);
+		selectSignal.Connect(signal, "item_select",
+				OBSSceneItemSelect, this);
+		deselectSignal.Connect(signal, "item_deselect",
+				OBSSceneItemDeselect, this);
+	}
+}
+
+void OBSBasicTransform::SetItem(OBSSceneItem newItem)
+{
+	QMetaObject::invokeMethod(this, "SetItemQt",
+			Q_ARG(OBSSceneItem, OBSSceneItem(newItem)));
+}
+
+void OBSBasicTransform::SetItemQt(OBSSceneItem newItem)
+{
+	item = newItem;
+	if (item)
+		RefreshControls();
+
+	setEnabled(!!item);
+}
+
+void OBSBasicTransform::OBSChannelChanged(void *param, calldata_t data)
+{
+	OBSBasicTransform *window = reinterpret_cast<OBSBasicTransform*>(param);
+	uint32_t channel = (uint32_t)calldata_int(data, "channel");
+	OBSSource source = (obs_source_t)calldata_ptr(data, "source");
+
+	if (channel == 0) {
+		OBSScene scene = obs_scene_fromsource(source);
+		window->SetScene(scene);
+
+		if (!scene)
+			window->SetItem(nullptr);
+		else
+			window->SetItem(FindASelectedItem(scene));
+	}
+}
+
+void OBSBasicTransform::OBSSceneItemTransform(void *param, calldata_t data)
+{
+	OBSBasicTransform *window = reinterpret_cast<OBSBasicTransform*>(param);
+	OBSSceneItem item = (obs_sceneitem_t)calldata_ptr(data, "item");
+
+	if (item == window->item && !window->ignoreTransformSignal)
+		QMetaObject::invokeMethod(window, "RefreshControls");
+}
+
+void OBSBasicTransform::OBSSceneItemRemoved(void *param, calldata_t data)
+{
+	OBSBasicTransform *window = reinterpret_cast<OBSBasicTransform*>(param);
+	OBSScene     scene = (obs_scene_t)calldata_ptr(data, "scene");
+	OBSSceneItem item = (obs_sceneitem_t)calldata_ptr(data, "item");
+
+	if (item == window->item)
+		window->SetItem(FindASelectedItem(scene));
+}
+
+void OBSBasicTransform::OBSSceneItemSelect(void *param, calldata_t data)
+{
+	OBSBasicTransform *window = reinterpret_cast<OBSBasicTransform*>(param);
+	OBSSceneItem item  = (obs_sceneitem_t)calldata_ptr(data, "item");
+
+	if (item != window->item)
+		window->SetItem(item);
+}
+
+void OBSBasicTransform::OBSSceneItemDeselect(void *param, calldata_t data)
+{
+	OBSBasicTransform *window = reinterpret_cast<OBSBasicTransform*>(param);
+	OBSScene     scene = (obs_scene_t)calldata_ptr(data, "scene");
+	OBSSceneItem item  = (obs_sceneitem_t)calldata_ptr(data, "item");
+
+	if (item == window->item)
+		window->SetItem(FindASelectedItem(scene));
+}
+
+static const uint32_t listToAlign[] = {
+	OBS_ALIGN_TOP | OBS_ALIGN_LEFT,
+	OBS_ALIGN_TOP,
+	OBS_ALIGN_TOP | OBS_ALIGN_RIGHT,
+	OBS_ALIGN_LEFT,
+	OBS_ALIGN_CENTER,
+	OBS_ALIGN_RIGHT,
+	OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT,
+	OBS_ALIGN_BOTTOM,
+	OBS_ALIGN_BOTTOM | OBS_ALIGN_RIGHT
+};
+
+static int AlignToList(uint32_t align)
+{
+	int index = 0;
+	for (uint32_t curAlign : listToAlign) {
+		if (curAlign == align)
+			return index;
+
+		index++;
+	}
+
+	return 0;
+}
+
+void OBSBasicTransform::RefreshControls()
+{
+	if (!item)
+		return;
+
+	obs_sceneitem_info osi;
+	obs_sceneitem_get_info(item, &osi);
+
+	int alignIndex       = AlignToList(osi.alignment);
+	int boundsAlignIndex = AlignToList(osi.bounds_alignment);
+
+	ignoreItemChange = true;
+	ui->positionX->setValue(osi.pos.x);
+	ui->positionY->setValue(osi.pos.y);
+	ui->rotation->setValue(osi.rot);
+	ui->scaleX->setValue(osi.scale.x);
+	ui->scaleY->setValue(osi.scale.y);
+	ui->align->setCurrentIndex(alignIndex);
+
+	ui->boundsType->setCurrentIndex(int(osi.bounds_type));
+	ui->boundsAlign->setCurrentIndex(boundsAlignIndex);
+	ui->boundsWidth->setValue(osi.bounds.x);
+	ui->boundsHeight->setValue(osi.bounds.y);
+	ignoreItemChange = false;
+}
+
+void OBSBasicTransform::OnBoundsType(int index)
+{
+	if (index == -1)
+		return;
+
+	obs_bounds_type type   = (obs_bounds_type)index;
+	bool            enable = (type != OBS_BOUNDS_NONE);
+
+	ui->boundsAlign->setEnabled(enable);
+	ui->boundsWidth->setEnabled(enable);
+	ui->boundsHeight->setEnabled(enable);
+
+	if (!ignoreItemChange) {
+		obs_bounds_type lastType = obs_sceneitem_get_bounds_type(item);
+		if (lastType == OBS_BOUNDS_NONE) {
+			OBSSource source = obs_sceneitem_getsource(item);
+			int width  = (int)obs_source_getwidth(source);
+			int height = (int)obs_source_getheight(source);
+
+			ui->boundsWidth->setValue(width);
+			ui->boundsHeight->setValue(height);
+		}
+	}
+
+	OnControlChanged();
+}
+
+void OBSBasicTransform::OnControlChanged()
+{
+	if (ignoreItemChange)
+		return;
+
+	obs_sceneitem_info osi;
+	osi.pos.x            = float(ui->positionX->value());
+	osi.pos.y            = float(ui->positionY->value());
+	osi.rot              = float(ui->rotation->value());
+	osi.scale.x          = float(ui->scaleX->value());
+	osi.scale.y          = float(ui->scaleY->value());
+	osi.alignment        = listToAlign[ui->align->currentIndex()];
+
+	osi.bounds_type      = (obs_bounds_type)ui->boundsType->currentIndex();
+	osi.bounds_alignment = listToAlign[ui->boundsAlign->currentIndex()];
+	osi.bounds.x         = float(ui->boundsWidth->value());
+	osi.bounds.y         = float(ui->boundsHeight->value());
+
+	ignoreTransformSignal = true;
+	obs_sceneitem_set_info(item, &osi);
+	ignoreTransformSignal = false;
+}

+ 47 - 0
obs/window-basic-transform.hpp

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <obs.hpp>
+#include <memory>
+
+#include "ui_OBSBasicTransform.h"
+
+class OBSBasic;
+
+class OBSBasicTransform : public QDialog {
+	Q_OBJECT
+
+private:
+	std::unique_ptr<Ui::OBSBasicTransform> ui;
+
+	OBSBasic     *main;
+	OBSSceneItem item;
+	OBSSignal    channelChangedSignal;
+	OBSSignal    transformSignal;
+	OBSSignal    removeSignal;
+	OBSSignal    selectSignal;
+	OBSSignal    deselectSignal;
+
+	bool         ignoreTransformSignal = false;
+	bool         ignoreItemChange      = false;
+
+	void HookWidget(QWidget *widget, const char *signal, const char *slot);
+
+	void SetScene(OBSScene scene);
+	void SetItem(OBSSceneItem newItem);
+
+	static void OBSChannelChanged(void *param, calldata_t data);
+
+	static void OBSSceneItemTransform(void *param, calldata_t data);
+	static void OBSSceneItemRemoved(void *param, calldata_t data);
+	static void OBSSceneItemSelect(void *param, calldata_t data);
+	static void OBSSceneItemDeselect(void *param, calldata_t data);
+
+private slots:
+	void RefreshControls();
+	void SetItemQt(OBSSceneItem newItem);
+	void OnBoundsType(int index);
+	void OnControlChanged();
+
+public:
+	OBSBasicTransform(OBSBasic *parent);
+};

+ 75 - 0
vs/2013/obs-studio/obs-studio.vcxproj

@@ -22,6 +22,43 @@
     <ClInclude Include="..\..\..\libobs\obs-ui.h" />
     <ClInclude Include="..\..\..\obs\display-helpers.hpp" />
     <ClInclude Include="..\..\..\obs\platform.hpp" />
+    <CustomBuild Include="..\..\..\obs\window-basic-preview.hpp">
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Moc%27ing window-basic-preview.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing window-basic-preview.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Moc%27ing window-basic-preview.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing window-basic-preview.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+    </CustomBuild>
+    <CustomBuild Include="..\..\..\obs\window-basic-transform.hpp">
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Moc%27ing window-basic-transform.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing window-basic-transform.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Moc%27ing window-basic-transform.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing window-basic-transform.hpp...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe"  "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp"  -DUNICODE -DWIN32 -DWIN64 -DQT_DLL -DQT_NO_DEBUG -DNDEBUG -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -DQT_NETWORK_LIB "-I.\..\..\..\libobs" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include\QtCore" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtWidgets" "-I.\..\..\..\obs" "-I$(QTDIR)\include\QtNetwork"</Command>
+    </CustomBuild>
+    <ClInclude Include="GeneratedFiles\ui_OBSBasicTransform.h" />
     <ClInclude Include="GeneratedFiles\ui_OBSLogReply.h" />
     <CustomBuild Include="..\..\..\obs\window-basic-source-select.hpp">
       <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
@@ -235,9 +272,11 @@
     <ClCompile Include="..\..\..\obs\qt-wrappers.cpp" />
     <ClCompile Include="..\..\..\obs\volume-control.cpp" />
     <ClCompile Include="..\..\..\obs\window-basic-main.cpp" />
+    <ClCompile Include="..\..\..\obs\window-basic-preview.cpp" />
     <ClCompile Include="..\..\..\obs\window-basic-properties.cpp" />
     <ClCompile Include="..\..\..\obs\window-basic-settings.cpp" />
     <ClCompile Include="..\..\..\obs\window-basic-source-select.cpp" />
+    <ClCompile Include="..\..\..\obs\window-basic-transform.cpp" />
     <ClCompile Include="..\..\..\obs\window-log-reply.cpp" />
     <ClCompile Include="..\..\..\obs\window-namedialog.cpp" />
     <ClCompile Include="GeneratedFiles\Debug\moc_obs-app.cpp">
@@ -260,6 +299,10 @@
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="GeneratedFiles\Debug\moc_window-basic-preview.cpp">
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
+    </ClCompile>
     <ClCompile Include="GeneratedFiles\Debug\moc_window-basic-properties.cpp">
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
@@ -272,6 +315,10 @@
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="GeneratedFiles\Debug\moc_window-basic-transform.cpp">
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
+    </ClCompile>
     <ClCompile Include="GeneratedFiles\Debug\moc_window-log-reply.cpp">
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
@@ -314,6 +361,10 @@
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="GeneratedFiles\Release\moc_window-basic-preview.cpp">
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
+    </ClCompile>
     <ClCompile Include="GeneratedFiles\Release\moc_window-basic-properties.cpp">
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
@@ -326,6 +377,10 @@
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="GeneratedFiles\Release\moc_window-basic-transform.cpp">
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
+    </ClCompile>
     <ClCompile Include="GeneratedFiles\Release\moc_window-log-reply.cpp">
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
@@ -553,6 +608,26 @@
       <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
     </CustomBuild>
   </ItemGroup>
+  <ItemGroup>
+    <CustomBuild Include="..\..\..\obs\forms\OBSBasicTransform.ui">
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Uic%27ing %(Identity)...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">.\GeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">"$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\GeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Uic%27ing %(Identity)...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">.\GeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">"$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
+      <Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Uic%27ing %(Identity)...</Message>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
+      <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\GeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
+    </CustomBuild>
+  </ItemGroup>
   <PropertyGroup Label="Globals">
     <ProjectGuid>{B12702AD-ABFB-343A-A199-8E24837244A3}</ProjectGuid>
     <Keyword>Qt4VSv1.0</Keyword>

+ 30 - 0
vs/2013/obs-studio/obs-studio.vcxproj.filters

@@ -89,6 +89,15 @@
     <CustomBuild Include="..\..\..\obs\forms\OBSLogReply.ui">
       <Filter>Form Files</Filter>
     </CustomBuild>
+    <CustomBuild Include="..\..\..\obs\window-basic-preview.hpp">
+      <Filter>Header Files</Filter>
+    </CustomBuild>
+    <CustomBuild Include="..\..\..\obs\forms\OBSBasicTransform.ui">
+      <Filter>Form Files</Filter>
+    </CustomBuild>
+    <CustomBuild Include="..\..\..\obs\window-basic-transform.hpp">
+      <Filter>Header Files</Filter>
+    </CustomBuild>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\obs\platform.hpp">
@@ -121,6 +130,9 @@
     <ClInclude Include="GeneratedFiles\ui_OBSLogReply.h">
       <Filter>Generated Files</Filter>
     </ClInclude>
+    <ClInclude Include="GeneratedFiles\ui_OBSBasicTransform.h">
+      <Filter>Generated Files</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="..\..\..\obs\obs-app.cpp">
@@ -225,6 +237,24 @@
     <ClCompile Include="..\..\..\obs\window-log-reply.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="GeneratedFiles\Debug\moc_window-basic-preview.cpp">
+      <Filter>Generated Files\Debug</Filter>
+    </ClCompile>
+    <ClCompile Include="GeneratedFiles\Release\moc_window-basic-preview.cpp">
+      <Filter>Generated Files\Release</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\obs\window-basic-preview.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="GeneratedFiles\Debug\moc_window-basic-transform.cpp">
+      <Filter>Generated Files\Debug</Filter>
+    </ClCompile>
+    <ClCompile Include="GeneratedFiles\Release\moc_window-basic-transform.cpp">
+      <Filter>Generated Files\Release</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\obs\window-basic-transform.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <Image Include="..\..\..\obs\forms\images\add.ico">