Переглянути джерело

UI: Undo/Redo Transformations

Implements undo/redo for transformations of sources, both through
preview and the transformations properties.
Ford Smith 4 роки тому
батько
коміт
3a620c485e

+ 224 - 2
UI/window-basic-main.cpp

@@ -925,8 +925,6 @@ void OBSBasic::LogScenes()
 
 void OBSBasic::Load(const char *file)
 {
-	disableSaving++;
-
 	obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak");
 	if (!data) {
 		disableSaving--;
@@ -7024,8 +7022,23 @@ void OBSBasic::on_actionCopyTransform_triggered()
 	ui->actionPasteTransform->setEnabled(true);
 }
 
+void undo_redo(const std::string &data)
+{
+	obs_data_t *dat = obs_data_create_from_json(data.c_str());
+	obs_source_t *source =
+		obs_get_source_by_name(obs_data_get_string(dat, "scene_name"));
+	reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+		->SetCurrentScene(source);
+	obs_source_release(source);
+	obs_data_release(dat);
+
+	obs_scene_load_transform_states(data.c_str());
+}
+
 void OBSBasic::on_actionPasteTransform_triggered()
 {
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	auto func = [](obs_scene_t *scene, obs_sceneitem_t *item, void *param) {
 		if (!obs_sceneitem_selected(item))
 			return true;
@@ -7041,6 +7054,19 @@ void OBSBasic::on_actionPasteTransform_triggered()
 	};
 
 	obs_scene_enum_items(GetCurrentScene(), func, nullptr);
+
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(
+		QTStr("Undo.Transform.Paste")
+			.arg(obs_source_get_name(GetCurrentSceneSource())),
+		undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
@@ -7076,6 +7102,22 @@ static bool reset_tr(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
 
 void OBSBasic::on_actionResetTransform_triggered()
 {
+	obs_scene_t *scene = GetCurrentScene();
+
+	obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
+	obs_scene_enum_items(scene, reset_tr, nullptr);
+	obs_data_t *rwrapper = obs_scene_save_transform_states(scene, false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(
+		QTStr("Undo.Transform.Reset")
+			.arg(obs_source_get_name(obs_scene_get_source(scene))),
+		undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
+
 	obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr);
 }
 
@@ -7153,19 +7195,61 @@ static bool RotateSelectedSources(obs_scene_t *scene, obs_sceneitem_t *item,
 void OBSBasic::on_actionRotate90CW_triggered()
 {
 	float f90CW = 90.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionRotate90CCW_triggered()
 {
 	float f90CCW = -90.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionRotate180_triggered()
 {
 	float f180 = 180.0f;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Rotate")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool MultiplySelectedItemScale(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -7200,16 +7284,44 @@ void OBSBasic::on_actionFlipHorizontal_triggered()
 {
 	vec2 scale;
 	vec2_set(&scale, -1.0f, 1.0f);
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
 			     &scale);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.HFlip")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionFlipVertical_triggered()
 {
 	vec2 scale;
 	vec2_set(&scale, 1.0f, -1.0f);
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale,
 			     &scale);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VFlip")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
@@ -7249,15 +7361,43 @@ static bool CenterAlignSelectedItems(obs_scene_t *scene, obs_sceneitem_t *item,
 void OBSBasic::on_actionFitToScreen_triggered()
 {
 	obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
 			     &boundsType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.FitToScreen")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionStretchToScreen_triggered()
 {
 	obs_bounds_type boundsType = OBS_BOUNDS_STRETCH;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems,
 			     &boundsType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.StretchToScreen")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 enum class CenterType {
@@ -7318,19 +7458,61 @@ static bool center_to_scene(obs_scene_t *, obs_sceneitem_t *item, void *param)
 void OBSBasic::on_actionCenterToScreen_triggered()
 {
 	CenterType centerType = CenterType::Scene;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.Center")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionVerticalCenter_triggered()
 {
 	CenterType centerType = CenterType::Vertical;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VCenter")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::on_actionHorizontalCenter_triggered()
 {
 	CenterType centerType = CenterType::Horizontal;
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
 	obs_scene_enum_items(GetCurrentScene(), center_to_scene, &centerType);
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(GetCurrentScene(), false);
+
+	std::string undo_data(obs_data_get_json(wrapper));
+	std::string redo_data(obs_data_get_json(rwrapper));
+	undo_s.add_action(QTStr("Undo.Transform.VCenter")
+				  .arg(obs_source_get_name(obs_scene_get_source(
+					  GetCurrentScene()))),
+			  undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+	obs_data_release(rwrapper);
 }
 
 void OBSBasic::EnablePreviewDisplay(bool enable)
@@ -7421,6 +7603,46 @@ void OBSBasic::Nudge(int dist, MoveDir dir)
 		break;
 	}
 
+	if (!recent_nudge) {
+		recent_nudge = true;
+		obs_data_t *wrapper = obs_scene_save_transform_states(
+			GetCurrentScene(), true);
+		std::string undo_data(obs_data_get_json(wrapper));
+
+		nudge_timer = new QTimer;
+		QObject::connect(
+			nudge_timer, &QTimer::timeout,
+			[this, &recent_nudge = recent_nudge, undo_data]() {
+				obs_data_t *rwrapper =
+					obs_scene_save_transform_states(
+						GetCurrentScene(), true);
+				std::string redo_data(
+					obs_data_get_json(rwrapper));
+
+				undo_s.add_action(
+					QTStr("Undo.Transform")
+						.arg(obs_source_get_name(
+							GetCurrentSceneSource())),
+					undo_redo, undo_redo, undo_data,
+					redo_data, NULL);
+
+				recent_nudge = false;
+				obs_data_release(rwrapper);
+			});
+		connect(nudge_timer, &QTimer::timeout, nudge_timer,
+			&QTimer::deleteLater);
+		nudge_timer->setSingleShot(true);
+
+		obs_data_release(wrapper);
+	}
+
+	if (nudge_timer) {
+		nudge_timer->stop();
+		nudge_timer->start(1000);
+	} else {
+		blog(LOG_ERROR, "No nudge timer!");
+	}
+
 	obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset);
 }
 

+ 4 - 0
UI/window-basic-main.hpp

@@ -157,6 +157,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class OBSBasicPreview;
 	friend class OBSBasicStatusBar;
 	friend class OBSBasicSourceSelect;
+	friend class OBSBasicTransform;
 	friend class OBSBasicSettings;
 	friend class Auth;
 	friend class AutoConfig;
@@ -222,6 +223,9 @@ private:
 	QPointer<QTimer> cpuUsageTimer;
 	QPointer<QTimer> diskFullTimer;
 
+	QPointer<QTimer> nudge_timer;
+	bool recent_nudge = false;
+
 	os_cpu_usage_info_t *cpuUsageInfo = nullptr;
 
 	OBSService service;

+ 45 - 0
UI/window-basic-preview.cpp

@@ -36,6 +36,9 @@ OBSBasicPreview::~OBSBasicPreview()
 		gs_vertexbuffer_destroy(rectFill);
 
 	obs_leave_graphics();
+
+	if (wrapper)
+		obs_data_release(wrapper);
 }
 
 vec2 OBSBasicPreview::GetMouseEventPos(QMouseEvent *event)
@@ -581,6 +584,11 @@ void OBSBasicPreview::mousePressEvent(QMouseEvent *event)
 	vec2_zero(&lastMoveOffset);
 
 	mousePos = startPos;
+	if (wrapper)
+		obs_data_release(wrapper);
+	wrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), true);
+	changed = false;
 }
 
 void OBSBasicPreview::UpdateCursor(uint32_t &flags)
@@ -713,6 +721,41 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
 		hoveredPreviewItems.push_back(item);
 		selectedItems.clear();
 	}
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	obs_data_t *rwrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), false);
+
+	auto undo_redo = [](const std::string &data) {
+		obs_data_t *dat = obs_data_create_from_json(data.c_str());
+		obs_source_t *source = obs_get_source_by_name(
+			obs_data_get_string(dat, "scene_name"));
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+			->SetCurrentScene(source);
+		obs_source_release(source);
+		obs_data_release(dat);
+
+		obs_scene_load_transform_states(data.c_str());
+	};
+
+	if (wrapper && rwrapper) {
+		std::string undo_data(obs_data_get_json(wrapper));
+		std::string redo_data(obs_data_get_json(rwrapper));
+		if (changed && undo_data.compare(redo_data) != 0)
+			main->undo_s.add_action(
+				QTStr("Undo.Transform")
+					.arg(obs_source_get_name(
+						main->GetCurrentSceneSource())),
+				undo_redo, undo_redo, undo_data, redo_data,
+				NULL);
+	}
+
+	if (wrapper)
+		obs_data_release(wrapper);
+
+	if (rwrapper)
+		obs_data_release(rwrapper);
+
+	wrapper = NULL;
 }
 
 struct SelectedItemBounds {
@@ -1434,6 +1477,8 @@ void OBSBasicPreview::StretchItem(const vec2 &pos)
 
 void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event)
 {
+	changed = true;
+
 	if (scrollMode && event->buttons() == Qt::LeftButton) {
 		scrollingOffset.x += event->x() - scrollingFrom.x;
 		scrollingOffset.y += event->y() - scrollingFrom.y;

+ 3 - 0
UI/window-basic-preview.hpp

@@ -106,6 +106,9 @@ private:
 
 	void ProcessClick(const vec2 &pos);
 
+	obs_data_t *wrapper = NULL;
+	bool changed;
+
 public:
 	OBSBasicPreview(QWidget *parent,
 			Qt::WindowFlags flags = Qt::WindowFlags());

+ 32 - 0
UI/window-basic-transform.cpp

@@ -73,10 +73,42 @@ OBSBasicTransform::OBSBasicTransform(OBSBasic *parent)
 	SetScene(scene);
 	SetItem(item);
 
+	obs_data_t *wrapper = obs_scene_save_transform_states(scene, false);
+	undo_data = std::string(obs_data_get_json(wrapper));
+
+	obs_data_release(wrapper);
+
 	channelChangedSignal.Connect(obs_get_signal_handler(), "channel_change",
 				     OBSChannelChanged, this);
 }
 
+OBSBasicTransform::~OBSBasicTransform()
+{
+	obs_data_t *wrapper =
+		obs_scene_save_transform_states(main->GetCurrentScene(), false);
+
+	auto undo_redo = [](const std::string &data) {
+		obs_data_t *dat = obs_data_create_from_json(data.c_str());
+		obs_source_t *source = obs_get_source_by_name(
+			obs_data_get_string(dat, "scene_name"));
+		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
+			->SetCurrentScene(source);
+		obs_source_release(source);
+		obs_data_release(dat);
+		obs_scene_load_transform_states(data.c_str());
+	};
+
+	std::string redo_data(obs_data_get_json(wrapper));
+	if (undo_data.compare(redo_data) != 0)
+		main->undo_s.add_action(
+			QTStr("Undo.Transform")
+				.arg(obs_source_get_name(obs_scene_get_source(
+					main->GetCurrentScene()))),
+			undo_redo, undo_redo, undo_data, redo_data, NULL);
+
+	obs_data_release(wrapper);
+}
+
 void OBSBasicTransform::SetScene(OBSScene scene)
 {
 	transformSignal.Disconnect();

+ 3 - 0
UI/window-basic-transform.hpp

@@ -21,6 +21,8 @@ private:
 	OBSSignal selectSignal;
 	OBSSignal deselectSignal;
 
+	std::string undo_data;
+
 	bool ignoreTransformSignal = false;
 	bool ignoreItemChange = false;
 
@@ -46,4 +48,5 @@ private slots:
 
 public:
 	OBSBasicTransform(OBSBasic *parent);
+	~OBSBasicTransform();
 };

+ 107 - 0
libobs/obs-scene.c

@@ -1890,6 +1890,113 @@ static void signal_parent(obs_scene_t *parent, const char *command,
 	signal_handler_signal(parent->source->context.signals, command, params);
 }
 
+struct passthrough {
+	obs_data_array_t *ids;
+	bool all_items;
+};
+
+bool save_transform_states(obs_scene_t *scene, obs_sceneitem_t *item,
+			   void *vp_pass)
+{
+	struct passthrough *pass = (struct passthrough *)vp_pass;
+	if (obs_sceneitem_selected(item) || pass->all_items) {
+		obs_data_t *temp = obs_data_create();
+		obs_data_array_t *item_ids = (obs_data_array_t *)pass->ids;
+
+		struct obs_transform_info info;
+		struct obs_sceneitem_crop crop;
+		obs_sceneitem_get_info(item, &info);
+		obs_sceneitem_get_crop(item, &crop);
+
+		struct vec2 pos = info.pos;
+		struct vec2 scale = info.scale;
+		float rot = info.rot;
+		uint32_t alignment = info.alignment;
+		uint32_t bounds_type = info.bounds_type;
+		uint32_t bounds_alignment = info.bounds_alignment;
+		struct vec2 bounds = info.bounds;
+
+		obs_data_set_int(temp, "id", obs_sceneitem_get_id(item));
+		obs_data_set_vec2(temp, "pos", &pos);
+		obs_data_set_vec2(temp, "scale", &scale);
+		obs_data_set_int(temp, "rot", rot);
+		obs_data_set_int(temp, "alignment", alignment);
+		obs_data_set_int(temp, "bounds_type", bounds_type);
+		obs_data_set_vec2(temp, "bounds", &bounds);
+		obs_data_set_int(temp, "bounds_alignment", bounds_alignment);
+		obs_data_set_int(temp, "top", crop.top);
+		obs_data_set_int(temp, "bottom", crop.bottom);
+		obs_data_set_int(temp, "left", crop.left);
+		obs_data_set_int(temp, "right", crop.right);
+
+		obs_data_array_push_back(item_ids, temp);
+
+		obs_data_release(temp);
+	}
+
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
+obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene, bool all_items)
+{
+	obs_data_t *wrapper = obs_data_create();
+	obs_data_array_t *item_ids = obs_data_array_create();
+	struct passthrough pass = {item_ids, all_items};
+
+	obs_scene_enum_items(scene, save_transform_states, (void *)&pass);
+	obs_data_set_array(wrapper, "item_ids", item_ids);
+	obs_data_set_string(wrapper, "scene_name",
+			    obs_source_get_name(obs_scene_get_source(scene)));
+
+	obs_data_array_release(item_ids);
+
+	return wrapper;
+}
+
+void load_transform_states(obs_data_t *temp, void *vp_scene)
+{
+	obs_scene_t *scene = (obs_scene_t *)vp_scene;
+	int64_t id = obs_data_get_int(temp, "id");
+	obs_sceneitem_t *item = obs_scene_find_sceneitem_by_id(scene, id);
+
+	struct obs_transform_info info;
+	struct obs_sceneitem_crop crop;
+	obs_data_get_vec2(temp, "pos", &info.pos);
+	obs_data_get_vec2(temp, "scale", &info.scale);
+	info.rot = obs_data_get_int(temp, "rot");
+	info.alignment = obs_data_get_int(temp, "alignment");
+	info.bounds_type =
+		(enum obs_bounds_type)obs_data_get_int(temp, "bounds_type");
+	info.bounds_alignment = obs_data_get_int(temp, "bounds_alignment");
+	obs_data_get_vec2(temp, "bounds", &info.bounds);
+	crop.top = obs_data_get_int(temp, "top");
+	crop.bottom = obs_data_get_int(temp, "bottom");
+	crop.left = obs_data_get_int(temp, "left");
+	crop.right = obs_data_get_int(temp, "right");
+
+	obs_sceneitem_defer_update_begin(item);
+
+	obs_sceneitem_set_info(item, &info);
+	obs_sceneitem_set_crop(item, &crop);
+
+	obs_sceneitem_defer_update_end(item);
+}
+
+void obs_scene_load_transform_states(const char *data)
+{
+	obs_data_t *dat = obs_data_create_from_json(data);
+	obs_data_array_t *item_ids = obs_data_get_array(dat, "item_ids");
+	obs_source_t *source =
+		obs_get_source_by_name(obs_data_get_string(dat, "scene_name"));
+	obs_scene_t *scene = obs_scene_from_source(source);
+	obs_data_array_enum(item_ids, load_transform_states, (void *)scene);
+
+	obs_data_release(dat);
+	obs_data_array_release(item_ids);
+	obs_source_release(source);
+}
+
 void obs_sceneitem_select(obs_sceneitem_t *item, bool select)
 {
 	struct calldata params;

+ 8 - 0
libobs/obs.h

@@ -1598,6 +1598,14 @@ EXPORT void obs_sceneitem_set_id(obs_sceneitem_t *sceneitem, int64_t id);
 /** Tries to find the sceneitem of the source in a given scene. Returns NULL if not found */
 EXPORT obs_sceneitem_t *obs_scene_sceneitem_from_source(obs_scene_t *scene,
 							obs_source_t *source);
+
+/** Save all the transform states for a current scene's sceneitems */
+EXPORT obs_data_t *obs_scene_save_transform_states(obs_scene_t *scene,
+						   bool all_items);
+
+/** Load all the transform states of sceneitems in that scene */
+EXPORT void obs_scene_load_transform_states(const char *state);
+
 /**  Gets a sceneitem's order in its scene */
 EXPORT int obs_sceneitem_get_order_position(obs_sceneitem_t *item);