1 /******************************************************************************
2     Copyright (C) 2015 by Hugh Bailey <obs.jim@gmail.com>
3 
4     This program is free software: you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation, either version 2 of the License, or
7     (at your option) any later version.
8 
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13 
14     You should have received a copy of the GNU General Public License
15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 ******************************************************************************/
17 
18 #include "properties-view.hpp"
19 #include "window-namedialog.hpp"
20 #include "window-basic-main.hpp"
21 #include "window-basic-filters.hpp"
22 #include "display-helpers.hpp"
23 #include "qt-wrappers.hpp"
24 #include "visibility-item-widget.hpp"
25 #include "item-widget-helpers.hpp"
26 #include "obs-app.hpp"
27 #include "undo-stack-obs.hpp"
28 
29 #include <QMessageBox>
30 #include <QCloseEvent>
31 #include <obs-data.h>
32 #include <obs.h>
33 #include <util/base.h>
34 #include <vector>
35 #include <string>
36 #include <QMenu>
37 #include <QVariant>
38 
39 using namespace std;
40 
41 Q_DECLARE_METATYPE(OBSSource);
42 
OBSBasicFilters(QWidget * parent,OBSSource source_)43 OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_)
44 	: QDialog(parent),
45 	  ui(new Ui::OBSBasicFilters),
46 	  source(source_),
47 	  addSignal(obs_source_get_signal_handler(source), "filter_add",
48 		    OBSBasicFilters::OBSSourceFilterAdded, this),
49 	  removeSignal(obs_source_get_signal_handler(source), "filter_remove",
50 		       OBSBasicFilters::OBSSourceFilterRemoved, this),
51 	  reorderSignal(obs_source_get_signal_handler(source),
52 			"reorder_filters", OBSBasicFilters::OBSSourceReordered,
53 			this),
54 	  removeSourceSignal(obs_source_get_signal_handler(source), "remove",
55 			     OBSBasicFilters::SourceRemoved, this),
56 	  renameSourceSignal(obs_source_get_signal_handler(source), "rename",
57 			     OBSBasicFilters::SourceRenamed, this),
58 	  noPreviewMargin(13)
59 {
60 	main = reinterpret_cast<OBSBasic *>(parent);
61 
62 	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
63 
64 	ui->setupUi(this);
65 	UpdateFilters();
66 
67 	ui->asyncFilters->setItemDelegate(
68 		new VisibilityItemDelegate(ui->asyncFilters));
69 	ui->effectFilters->setItemDelegate(
70 		new VisibilityItemDelegate(ui->effectFilters));
71 
72 	const char *name = obs_source_get_name(source);
73 	setWindowTitle(QTStr("Basic.Filters.Title").arg(QT_UTF8(name)));
74 
75 #ifndef QT_NO_SHORTCUT
76 	ui->actionRemoveFilter->setShortcut(
77 		QApplication::translate("OBSBasicFilters", "Del", nullptr));
78 #endif // QT_NO_SHORTCUT
79 
80 	addAction(ui->actionRemoveFilter);
81 	addAction(ui->actionMoveUp);
82 	addAction(ui->actionMoveDown);
83 
84 	installEventFilter(CreateShortcutFilter());
85 
86 	connect(ui->asyncFilters->itemDelegate(),
87 		SIGNAL(closeEditor(QWidget *,
88 				   QAbstractItemDelegate::EndEditHint)),
89 		this,
90 		SLOT(AsyncFilterNameEdited(
91 			QWidget *, QAbstractItemDelegate::EndEditHint)));
92 
93 	connect(ui->effectFilters->itemDelegate(),
94 		SIGNAL(closeEditor(QWidget *,
95 				   QAbstractItemDelegate::EndEditHint)),
96 		this,
97 		SLOT(EffectFilterNameEdited(
98 			QWidget *, QAbstractItemDelegate::EndEditHint)));
99 
100 	QPushButton *close = ui->buttonBox->button(QDialogButtonBox::Close);
101 	connect(close, SIGNAL(clicked()), this, SLOT(close()));
102 	close->setDefault(true);
103 
104 	ui->buttonBox->button(QDialogButtonBox::Reset)
105 		->setText(QTStr("Defaults"));
106 
107 	connect(ui->buttonBox->button(QDialogButtonBox::Reset),
108 		SIGNAL(clicked()), this, SLOT(ResetFilters()));
109 
110 	uint32_t caps = obs_source_get_output_flags(source);
111 	bool audio = (caps & OBS_SOURCE_AUDIO) != 0;
112 	bool audioOnly = (caps & OBS_SOURCE_VIDEO) == 0;
113 	bool async = (caps & OBS_SOURCE_ASYNC) != 0;
114 
115 	if (!async && !audio) {
116 		ui->asyncWidget->setVisible(false);
117 		ui->separatorLine->setVisible(false);
118 	}
119 	if (audioOnly) {
120 		ui->effectWidget->setVisible(false);
121 		ui->separatorLine->setVisible(false);
122 	}
123 
124 	if (audioOnly || (audio && !async))
125 		ui->asyncLabel->setText(QTStr("Basic.Filters.AudioFilters"));
126 
127 	auto addDrawCallback = [this]() {
128 		obs_display_add_draw_callback(ui->preview->GetDisplay(),
129 					      OBSBasicFilters::DrawPreview,
130 					      this);
131 	};
132 
133 	enum obs_source_type type = obs_source_get_type(source);
134 	bool drawable_type = type == OBS_SOURCE_TYPE_INPUT ||
135 			     type == OBS_SOURCE_TYPE_SCENE;
136 
137 	if ((caps & OBS_SOURCE_VIDEO) != 0) {
138 		ui->rightLayout->setContentsMargins(0, 0, 0, 0);
139 		ui->preview->show();
140 		if (drawable_type)
141 			connect(ui->preview, &OBSQTDisplay::DisplayCreated,
142 				addDrawCallback);
143 	} else {
144 		ui->rightLayout->setContentsMargins(0, noPreviewMargin, 0, 0);
145 		ui->rightContainerLayout->insertStretch(1);
146 		ui->preview->hide();
147 	}
148 
149 	QAction *renameAsync = new QAction(ui->asyncWidget);
150 	renameAsync->setShortcutContext(Qt::WidgetWithChildrenShortcut);
151 	connect(renameAsync, SIGNAL(triggered()), this,
152 		SLOT(RenameAsyncFilter()));
153 	ui->asyncWidget->addAction(renameAsync);
154 
155 	QAction *renameEffect = new QAction(ui->effectWidget);
156 	renameEffect->setShortcutContext(Qt::WidgetWithChildrenShortcut);
157 	connect(renameEffect, SIGNAL(triggered()), this,
158 		SLOT(RenameEffectFilter()));
159 	ui->effectWidget->addAction(renameEffect);
160 
161 #ifdef __APPLE__
162 	renameAsync->setShortcut({Qt::Key_Return});
163 	renameEffect->setShortcut({Qt::Key_Return});
164 #else
165 	renameAsync->setShortcut({Qt::Key_F2});
166 	renameEffect->setShortcut({Qt::Key_F2});
167 #endif
168 }
169 
~OBSBasicFilters()170 OBSBasicFilters::~OBSBasicFilters()
171 {
172 	ClearListItems(ui->asyncFilters);
173 	ClearListItems(ui->effectFilters);
174 }
175 
Init()176 void OBSBasicFilters::Init()
177 {
178 	show();
179 }
180 
GetFilter(int row,bool async)181 inline OBSSource OBSBasicFilters::GetFilter(int row, bool async)
182 {
183 	if (row == -1)
184 		return OBSSource();
185 
186 	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
187 	QListWidgetItem *item = list->item(row);
188 	if (!item)
189 		return OBSSource();
190 
191 	QVariant v = item->data(Qt::UserRole);
192 	return v.value<OBSSource>();
193 }
194 
UpdatePropertiesView(int row,bool async)195 void OBSBasicFilters::UpdatePropertiesView(int row, bool async)
196 {
197 	if (view) {
198 		updatePropertiesSignal.Disconnect();
199 		ui->rightLayout->removeWidget(view);
200 		view->deleteLater();
201 		view = nullptr;
202 	}
203 
204 	OBSSource filter = GetFilter(row, async);
205 	if (!filter)
206 		return;
207 
208 	obs_data_t *settings = obs_source_get_settings(filter);
209 
210 	auto filter_change = [](void *vp, obs_data_t *nd_old_settings,
211 				obs_data_t *new_settings) {
212 		obs_source_t *source = reinterpret_cast<obs_source_t *>(vp);
213 		obs_source_t *parent = obs_filter_get_parent(source);
214 		const char *source_name = obs_source_get_name(source);
215 		OBSBasic *main = OBSBasic::Get();
216 
217 		obs_data_t *redo_wrapper = obs_data_create();
218 		obs_data_set_string(redo_wrapper, "name", source_name);
219 		obs_data_set_string(redo_wrapper, "settings",
220 				    obs_data_get_json(new_settings));
221 		obs_data_set_string(redo_wrapper, "parent",
222 				    obs_source_get_name(parent));
223 
224 		obs_data_t *filter_settings = obs_source_get_settings(source);
225 		obs_data_t *old_settings =
226 			obs_data_get_defaults(filter_settings);
227 		obs_data_apply(old_settings, nd_old_settings);
228 
229 		obs_data_t *undo_wrapper = obs_data_create();
230 		obs_data_set_string(undo_wrapper, "name", source_name);
231 		obs_data_set_string(undo_wrapper, "settings",
232 				    obs_data_get_json(old_settings));
233 		obs_data_set_string(undo_wrapper, "parent",
234 				    obs_source_get_name(parent));
235 
236 		auto undo_redo = [](const std::string &data) {
237 			obs_data_t *dat =
238 				obs_data_create_from_json(data.c_str());
239 			obs_source_t *parent_source = obs_get_source_by_name(
240 				obs_data_get_string(dat, "parent"));
241 			const char *filter_name =
242 				obs_data_get_string(dat, "name");
243 			obs_source_t *filter = obs_source_get_filter_by_name(
244 				parent_source, filter_name);
245 			obs_data_t *settings = obs_data_create_from_json(
246 				obs_data_get_string(dat, "settings"));
247 
248 			obs_source_update(filter, settings);
249 			obs_source_update_properties(filter);
250 
251 			obs_data_release(dat);
252 			obs_data_release(settings);
253 			obs_source_release(filter);
254 			obs_source_release(parent_source);
255 		};
256 
257 		main->undo_s.enable();
258 
259 		std::string name = std::string(obs_source_get_name(source));
260 		std::string undo_data = obs_data_get_json(undo_wrapper);
261 		std::string redo_data = obs_data_get_json(redo_wrapper);
262 		main->undo_s.add_action(QTStr("Undo.Filters").arg(name.c_str()),
263 					undo_redo, undo_redo, undo_data,
264 					redo_data);
265 
266 		obs_data_release(redo_wrapper);
267 		obs_data_release(undo_wrapper);
268 		obs_data_release(old_settings);
269 		obs_data_release(filter_settings);
270 
271 		obs_source_update(source, new_settings);
272 	};
273 
274 	auto disabled_undo = [](void *vp, obs_data_t *settings) {
275 		OBSBasic *main =
276 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
277 		main->undo_s.disable();
278 		obs_source_t *source = reinterpret_cast<obs_source_t *>(vp);
279 		obs_source_update(source, settings);
280 	};
281 
282 	view = new OBSPropertiesView(
283 		settings, filter,
284 		(PropertiesReloadCallback)obs_source_properties,
285 		(PropertiesUpdateCallback)filter_change,
286 		(PropertiesVisualUpdateCb)disabled_undo);
287 
288 	updatePropertiesSignal.Connect(obs_source_get_signal_handler(filter),
289 				       "update_properties",
290 				       OBSBasicFilters::UpdateProperties, this);
291 
292 	obs_data_release(settings);
293 
294 	view->setMaximumHeight(250);
295 	view->setMinimumHeight(150);
296 	ui->rightLayout->addWidget(view);
297 	view->show();
298 }
299 
UpdateProperties(void * data,calldata_t *)300 void OBSBasicFilters::UpdateProperties(void *data, calldata_t *)
301 {
302 	QMetaObject::invokeMethod(static_cast<OBSBasicFilters *>(data)->view,
303 				  "ReloadProperties");
304 }
305 
AddFilter(OBSSource filter,bool focus)306 void OBSBasicFilters::AddFilter(OBSSource filter, bool focus)
307 {
308 	uint32_t flags = obs_source_get_output_flags(filter);
309 	bool async = (flags & OBS_SOURCE_ASYNC) != 0;
310 	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
311 
312 	QListWidgetItem *item = new QListWidgetItem();
313 	Qt::ItemFlags itemFlags = item->flags();
314 
315 	item->setFlags(itemFlags | Qt::ItemIsEditable);
316 	item->setData(Qt::UserRole, QVariant::fromValue(filter));
317 
318 	list->addItem(item);
319 	if (focus)
320 		list->setCurrentItem(item);
321 
322 	SetupVisibilityItem(list, item, filter);
323 }
324 
RemoveFilter(OBSSource filter)325 void OBSBasicFilters::RemoveFilter(OBSSource filter)
326 {
327 	uint32_t flags = obs_source_get_output_flags(filter);
328 	bool async = (flags & OBS_SOURCE_ASYNC) != 0;
329 	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
330 
331 	for (int i = 0; i < list->count(); i++) {
332 		QListWidgetItem *item = list->item(i);
333 		QVariant v = item->data(Qt::UserRole);
334 		OBSSource curFilter = v.value<OBSSource>();
335 
336 		if (filter == curFilter) {
337 			DeleteListItem(list, item);
338 			break;
339 		}
340 	}
341 
342 	const char *filterName = obs_source_get_name(filter);
343 	const char *sourceName = obs_source_get_name(source);
344 	if (!sourceName || !filterName)
345 		return;
346 
347 	const char *filterId = obs_source_get_id(filter);
348 
349 	blog(LOG_INFO, "User removed filter '%s' (%s) from source '%s'",
350 	     filterName, filterId, sourceName);
351 
352 	main->SaveProject();
353 }
354 
355 struct FilterOrderInfo {
356 	int asyncIdx = 0;
357 	int effectIdx = 0;
358 	OBSBasicFilters *window;
359 
FilterOrderInfoFilterOrderInfo360 	inline FilterOrderInfo(OBSBasicFilters *window_) : window(window_) {}
361 };
362 
ReorderFilter(QListWidget * list,obs_source_t * filter,size_t idx)363 void OBSBasicFilters::ReorderFilter(QListWidget *list, obs_source_t *filter,
364 				    size_t idx)
365 {
366 	int count = list->count();
367 
368 	for (int i = 0; i < count; i++) {
369 		QListWidgetItem *listItem = list->item(i);
370 		QVariant v = listItem->data(Qt::UserRole);
371 		OBSSource filterItem = v.value<OBSSource>();
372 
373 		if (filterItem == filter) {
374 			if ((int)idx != i) {
375 				bool sel = (list->currentRow() == i);
376 
377 				listItem = TakeListItem(list, i);
378 				if (listItem) {
379 					list->insertItem((int)idx, listItem);
380 					SetupVisibilityItem(list, listItem,
381 							    filterItem);
382 
383 					if (sel)
384 						list->setCurrentRow((int)idx);
385 				}
386 			}
387 
388 			break;
389 		}
390 	}
391 }
392 
ReorderFilters()393 void OBSBasicFilters::ReorderFilters()
394 {
395 	FilterOrderInfo info(this);
396 
397 	obs_source_enum_filters(
398 		source,
399 		[](obs_source_t *, obs_source_t *filter, void *p) {
400 			FilterOrderInfo *info =
401 				reinterpret_cast<FilterOrderInfo *>(p);
402 			uint32_t flags;
403 			bool async;
404 
405 			flags = obs_source_get_output_flags(filter);
406 			async = (flags & OBS_SOURCE_ASYNC) != 0;
407 
408 			if (async) {
409 				info->window->ReorderFilter(
410 					info->window->ui->asyncFilters, filter,
411 					info->asyncIdx++);
412 			} else {
413 				info->window->ReorderFilter(
414 					info->window->ui->effectFilters, filter,
415 					info->effectIdx++);
416 			}
417 		},
418 		&info);
419 }
420 
UpdateFilters()421 void OBSBasicFilters::UpdateFilters()
422 {
423 	if (!source)
424 		return;
425 
426 	ClearListItems(ui->effectFilters);
427 	ClearListItems(ui->asyncFilters);
428 
429 	obs_source_enum_filters(
430 		source,
431 		[](obs_source_t *, obs_source_t *filter, void *p) {
432 			OBSBasicFilters *window =
433 				reinterpret_cast<OBSBasicFilters *>(p);
434 
435 			window->AddFilter(filter, false);
436 		},
437 		this);
438 
439 	if (ui->asyncFilters->count() > 0) {
440 		ui->asyncFilters->setCurrentItem(ui->asyncFilters->item(0));
441 	} else if (ui->effectFilters->count() > 0) {
442 		ui->effectFilters->setCurrentItem(ui->effectFilters->item(0));
443 	}
444 
445 	main->SaveProject();
446 }
447 
filter_compatible(bool async,uint32_t sourceFlags,uint32_t filterFlags)448 static bool filter_compatible(bool async, uint32_t sourceFlags,
449 			      uint32_t filterFlags)
450 {
451 	bool filterVideo = (filterFlags & OBS_SOURCE_VIDEO) != 0;
452 	bool filterAsync = (filterFlags & OBS_SOURCE_ASYNC) != 0;
453 	bool filterAudio = (filterFlags & OBS_SOURCE_AUDIO) != 0;
454 	bool audio = (sourceFlags & OBS_SOURCE_AUDIO) != 0;
455 	bool audioOnly = (sourceFlags & OBS_SOURCE_VIDEO) == 0;
456 	bool asyncSource = (sourceFlags & OBS_SOURCE_ASYNC) != 0;
457 
458 	if (async && ((audioOnly && filterVideo) || (!audio && !asyncSource)))
459 		return false;
460 
461 	return (async && (filterAudio || filterAsync)) ||
462 	       (!async && !filterAudio && !filterAsync);
463 }
464 
CreateAddFilterPopupMenu(bool async)465 QMenu *OBSBasicFilters::CreateAddFilterPopupMenu(bool async)
466 {
467 	uint32_t sourceFlags = obs_source_get_output_flags(source);
468 	const char *type_str;
469 	bool foundValues = false;
470 	size_t idx = 0;
471 
472 	struct FilterInfo {
473 		string type;
474 		string name;
475 
476 		inline FilterInfo(const char *type_, const char *name_)
477 			: type(type_), name(name_)
478 		{
479 		}
480 	};
481 
482 	vector<FilterInfo> types;
483 	while (obs_enum_filter_types(idx++, &type_str)) {
484 		const char *name = obs_source_get_display_name(type_str);
485 		uint32_t caps = obs_get_source_output_flags(type_str);
486 
487 		if ((caps & OBS_SOURCE_DEPRECATED) != 0)
488 			continue;
489 		if ((caps & OBS_SOURCE_CAP_DISABLED) != 0)
490 			continue;
491 		if ((caps & OBS_SOURCE_CAP_OBSOLETE) != 0)
492 			continue;
493 
494 		auto it = types.begin();
495 		for (; it != types.end(); ++it) {
496 			if (it->name >= name)
497 				break;
498 		}
499 
500 		types.emplace(it, type_str, name);
501 	}
502 
503 	QMenu *popup = new QMenu(QTStr("Add"), this);
504 	for (FilterInfo &type : types) {
505 		uint32_t filterFlags =
506 			obs_get_source_output_flags(type.type.c_str());
507 
508 		if (!filter_compatible(async, sourceFlags, filterFlags))
509 			continue;
510 
511 		QAction *popupItem =
512 			new QAction(QT_UTF8(type.name.c_str()), this);
513 		popupItem->setData(QT_UTF8(type.type.c_str()));
514 		connect(popupItem, SIGNAL(triggered(bool)), this,
515 			SLOT(AddFilterFromAction()));
516 		popup->addAction(popupItem);
517 
518 		foundValues = true;
519 	}
520 
521 	if (!foundValues) {
522 		delete popup;
523 		popup = nullptr;
524 	}
525 
526 	return popup;
527 }
528 
AddNewFilter(const char * id)529 void OBSBasicFilters::AddNewFilter(const char *id)
530 {
531 	if (id && *id) {
532 		obs_source_t *existing_filter;
533 		string name = obs_source_get_display_name(id);
534 
535 		QString placeholder = QString::fromStdString(name);
536 		QString text{placeholder};
537 		int i = 2;
538 		while ((existing_filter = obs_source_get_filter_by_name(
539 				source, QT_TO_UTF8(text)))) {
540 			obs_source_release(existing_filter);
541 			text = QString("%1 %2").arg(placeholder).arg(i++);
542 		}
543 
544 		bool success = NameDialog::AskForName(
545 			this, QTStr("Basic.Filters.AddFilter.Title"),
546 			QTStr("Basic.Filters.AddFilter.Text"), name, text);
547 		if (!success)
548 			return;
549 
550 		if (name.empty()) {
551 			OBSMessageBox::warning(this,
552 					       QTStr("NoNameEntered.Title"),
553 					       QTStr("NoNameEntered.Text"));
554 			AddNewFilter(id);
555 			return;
556 		}
557 
558 		existing_filter =
559 			obs_source_get_filter_by_name(source, name.c_str());
560 		if (existing_filter) {
561 			OBSMessageBox::warning(this, QTStr("NameExists.Title"),
562 					       QTStr("NameExists.Text"));
563 			obs_source_release(existing_filter);
564 			AddNewFilter(id);
565 			return;
566 		}
567 
568 		obs_data_t *wrapper = obs_data_create();
569 		obs_data_set_string(wrapper, "sname",
570 				    obs_source_get_name(source));
571 		obs_data_set_string(wrapper, "fname", name.c_str());
572 		std::string scene_name = obs_source_get_name(
573 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
574 				->GetCurrentSceneSource());
575 		auto undo = [scene_name](const std::string &data) {
576 			obs_source_t *ssource =
577 				obs_get_source_by_name(scene_name.c_str());
578 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
579 				->SetCurrentScene(ssource, true);
580 			obs_source_release(ssource);
581 
582 			obs_data_t *dat =
583 				obs_data_create_from_json(data.c_str());
584 			obs_source_t *source = obs_get_source_by_name(
585 				obs_data_get_string(dat, "sname"));
586 			obs_source_t *filter = obs_source_get_filter_by_name(
587 				source, obs_data_get_string(dat, "fname"));
588 			obs_source_filter_remove(source, filter);
589 
590 			obs_data_release(dat);
591 			obs_source_release(source);
592 			obs_source_release(filter);
593 		};
594 
595 		obs_data_t *rwrapper = obs_data_create();
596 		obs_data_set_string(rwrapper, "sname",
597 				    obs_source_get_name(source));
598 		auto redo = [scene_name, id = std::string(id),
599 			     name](const std::string &data) {
600 			obs_source_t *ssource =
601 				obs_get_source_by_name(scene_name.c_str());
602 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
603 				->SetCurrentScene(ssource, true);
604 			obs_source_release(ssource);
605 
606 			obs_data_t *dat =
607 				obs_data_create_from_json(data.c_str());
608 			obs_source_t *source = obs_get_source_by_name(
609 				obs_data_get_string(dat, "sname"));
610 
611 			obs_source_t *filter = obs_source_create(
612 				id.c_str(), name.c_str(), nullptr, nullptr);
613 			if (filter) {
614 				obs_source_filter_add(source, filter);
615 				obs_source_release(filter);
616 			}
617 
618 			obs_data_release(dat);
619 			obs_source_release(source);
620 		};
621 
622 		std::string undo_data(obs_data_get_json(wrapper));
623 		std::string redo_data(obs_data_get_json(rwrapper));
624 		main->undo_s.add_action(QTStr("Undo.Add").arg(name.c_str()),
625 					undo, redo, undo_data, redo_data);
626 
627 		obs_data_release(wrapper);
628 		obs_data_release(rwrapper);
629 
630 		obs_source_t *filter =
631 			obs_source_create(id, name.c_str(), nullptr, nullptr);
632 		if (filter) {
633 			const char *sourceName = obs_source_get_name(source);
634 
635 			blog(LOG_INFO,
636 			     "User added filter '%s' (%s) "
637 			     "to source '%s'",
638 			     name.c_str(), id, sourceName);
639 
640 			obs_source_filter_add(source, filter);
641 			obs_source_release(filter);
642 		}
643 	}
644 }
645 
AddFilterFromAction()646 void OBSBasicFilters::AddFilterFromAction()
647 {
648 	QAction *action = qobject_cast<QAction *>(sender());
649 	if (!action)
650 		return;
651 
652 	AddNewFilter(QT_TO_UTF8(action->data().toString()));
653 }
654 
closeEvent(QCloseEvent * event)655 void OBSBasicFilters::closeEvent(QCloseEvent *event)
656 {
657 	QDialog::closeEvent(event);
658 	if (!event->isAccepted())
659 		return;
660 
661 	obs_display_remove_draw_callback(ui->preview->GetDisplay(),
662 					 OBSBasicFilters::DrawPreview, this);
663 
664 	main->SaveProject();
665 }
666 
667 /* OBS Signals */
668 
OBSSourceFilterAdded(void * param,calldata_t * data)669 void OBSBasicFilters::OBSSourceFilterAdded(void *param, calldata_t *data)
670 {
671 	OBSBasicFilters *window = reinterpret_cast<OBSBasicFilters *>(param);
672 	obs_source_t *filter = (obs_source_t *)calldata_ptr(data, "filter");
673 
674 	QMetaObject::invokeMethod(window, "AddFilter",
675 				  Q_ARG(OBSSource, OBSSource(filter)));
676 }
677 
OBSSourceFilterRemoved(void * param,calldata_t * data)678 void OBSBasicFilters::OBSSourceFilterRemoved(void *param, calldata_t *data)
679 {
680 	OBSBasicFilters *window = reinterpret_cast<OBSBasicFilters *>(param);
681 	obs_source_t *filter = (obs_source_t *)calldata_ptr(data, "filter");
682 
683 	QMetaObject::invokeMethod(window, "RemoveFilter",
684 				  Q_ARG(OBSSource, OBSSource(filter)));
685 }
686 
OBSSourceReordered(void * param,calldata_t * data)687 void OBSBasicFilters::OBSSourceReordered(void *param, calldata_t *data)
688 {
689 	QMetaObject::invokeMethod(reinterpret_cast<OBSBasicFilters *>(param),
690 				  "ReorderFilters");
691 
692 	UNUSED_PARAMETER(data);
693 }
694 
SourceRemoved(void * param,calldata_t * data)695 void OBSBasicFilters::SourceRemoved(void *param, calldata_t *data)
696 {
697 	UNUSED_PARAMETER(data);
698 
699 	QMetaObject::invokeMethod(static_cast<OBSBasicFilters *>(param),
700 				  "close");
701 }
702 
SourceRenamed(void * param,calldata_t * data)703 void OBSBasicFilters::SourceRenamed(void *param, calldata_t *data)
704 {
705 	const char *name = calldata_string(data, "new_name");
706 	QString title = QTStr("Basic.Filters.Title").arg(QT_UTF8(name));
707 
708 	QMetaObject::invokeMethod(static_cast<OBSBasicFilters *>(param),
709 				  "setWindowTitle", Q_ARG(QString, title));
710 }
711 
DrawPreview(void * data,uint32_t cx,uint32_t cy)712 void OBSBasicFilters::DrawPreview(void *data, uint32_t cx, uint32_t cy)
713 {
714 	OBSBasicFilters *window = static_cast<OBSBasicFilters *>(data);
715 
716 	if (!window->source)
717 		return;
718 
719 	uint32_t sourceCX = max(obs_source_get_width(window->source), 1u);
720 	uint32_t sourceCY = max(obs_source_get_height(window->source), 1u);
721 
722 	int x, y;
723 	int newCX, newCY;
724 	float scale;
725 
726 	GetScaleAndCenterPos(sourceCX, sourceCY, cx, cy, x, y, scale);
727 
728 	newCX = int(scale * float(sourceCX));
729 	newCY = int(scale * float(sourceCY));
730 
731 	gs_viewport_push();
732 	gs_projection_push();
733 	const bool previous = gs_set_linear_srgb(true);
734 
735 	gs_ortho(0.0f, float(sourceCX), 0.0f, float(sourceCY), -100.0f, 100.0f);
736 	gs_set_viewport(x, y, newCX, newCY);
737 	obs_source_video_render(window->source);
738 
739 	gs_set_linear_srgb(previous);
740 	gs_projection_pop();
741 	gs_viewport_pop();
742 }
743 
744 /* Qt Slots */
745 
QueryRemove(QWidget * parent,obs_source_t * source)746 static bool QueryRemove(QWidget *parent, obs_source_t *source)
747 {
748 	const char *name = obs_source_get_name(source);
749 
750 	QString text = QTStr("ConfirmRemove.Text");
751 	text.replace("$1", QT_UTF8(name));
752 
753 	QMessageBox remove_source(parent);
754 	remove_source.setText(text);
755 	QAbstractButton *Yes =
756 		remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole);
757 	remove_source.addButton(QTStr("No"), QMessageBox::NoRole);
758 	remove_source.setIcon(QMessageBox::Question);
759 	remove_source.setWindowTitle(QTStr("ConfirmRemove.Title"));
760 	remove_source.exec();
761 
762 	return Yes == remove_source.clickedButton();
763 }
764 
on_addAsyncFilter_clicked()765 void OBSBasicFilters::on_addAsyncFilter_clicked()
766 {
767 	QScopedPointer<QMenu> popup(CreateAddFilterPopupMenu(true));
768 	if (popup)
769 		popup->exec(QCursor::pos());
770 }
771 
on_removeAsyncFilter_clicked()772 void OBSBasicFilters::on_removeAsyncFilter_clicked()
773 {
774 	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
775 	if (filter) {
776 		if (QueryRemove(this, filter))
777 			delete_filter(filter);
778 	}
779 }
780 
on_moveAsyncFilterUp_clicked()781 void OBSBasicFilters::on_moveAsyncFilterUp_clicked()
782 {
783 	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
784 	if (filter)
785 		obs_source_filter_set_order(source, filter, OBS_ORDER_MOVE_UP);
786 }
787 
on_moveAsyncFilterDown_clicked()788 void OBSBasicFilters::on_moveAsyncFilterDown_clicked()
789 {
790 	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
791 	if (filter)
792 		obs_source_filter_set_order(source, filter,
793 					    OBS_ORDER_MOVE_DOWN);
794 }
795 
on_asyncFilters_GotFocus()796 void OBSBasicFilters::on_asyncFilters_GotFocus()
797 {
798 	UpdatePropertiesView(ui->asyncFilters->currentRow(), true);
799 	isAsync = true;
800 }
801 
on_asyncFilters_currentRowChanged(int row)802 void OBSBasicFilters::on_asyncFilters_currentRowChanged(int row)
803 {
804 	UpdatePropertiesView(row, true);
805 }
806 
on_addEffectFilter_clicked()807 void OBSBasicFilters::on_addEffectFilter_clicked()
808 {
809 	QScopedPointer<QMenu> popup(CreateAddFilterPopupMenu(false));
810 	if (popup)
811 		popup->exec(QCursor::pos());
812 }
813 
on_removeEffectFilter_clicked()814 void OBSBasicFilters::on_removeEffectFilter_clicked()
815 {
816 	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
817 	if (filter) {
818 		if (QueryRemove(this, filter)) {
819 			delete_filter(filter);
820 		}
821 	}
822 }
823 
on_moveEffectFilterUp_clicked()824 void OBSBasicFilters::on_moveEffectFilterUp_clicked()
825 {
826 	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
827 	if (filter)
828 		obs_source_filter_set_order(source, filter, OBS_ORDER_MOVE_UP);
829 }
830 
on_moveEffectFilterDown_clicked()831 void OBSBasicFilters::on_moveEffectFilterDown_clicked()
832 {
833 	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
834 	if (filter)
835 		obs_source_filter_set_order(source, filter,
836 					    OBS_ORDER_MOVE_DOWN);
837 }
838 
on_effectFilters_GotFocus()839 void OBSBasicFilters::on_effectFilters_GotFocus()
840 {
841 	UpdatePropertiesView(ui->effectFilters->currentRow(), false);
842 	isAsync = false;
843 }
844 
on_effectFilters_currentRowChanged(int row)845 void OBSBasicFilters::on_effectFilters_currentRowChanged(int row)
846 {
847 	UpdatePropertiesView(row, false);
848 }
849 
on_actionRemoveFilter_triggered()850 void OBSBasicFilters::on_actionRemoveFilter_triggered()
851 {
852 	if (ui->asyncFilters->hasFocus())
853 		on_removeAsyncFilter_clicked();
854 	else if (ui->effectFilters->hasFocus())
855 		on_removeEffectFilter_clicked();
856 }
857 
on_actionMoveUp_triggered()858 void OBSBasicFilters::on_actionMoveUp_triggered()
859 {
860 	if (ui->asyncFilters->hasFocus())
861 		on_moveAsyncFilterUp_clicked();
862 	else if (ui->effectFilters->hasFocus())
863 		on_moveEffectFilterUp_clicked();
864 }
865 
on_actionMoveDown_triggered()866 void OBSBasicFilters::on_actionMoveDown_triggered()
867 {
868 	if (ui->asyncFilters->hasFocus())
869 		on_moveAsyncFilterDown_clicked();
870 	else if (ui->effectFilters->hasFocus())
871 		on_moveEffectFilterDown_clicked();
872 }
873 
CustomContextMenu(const QPoint & pos,bool async)874 void OBSBasicFilters::CustomContextMenu(const QPoint &pos, bool async)
875 {
876 	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
877 	QListWidgetItem *item = list->itemAt(pos);
878 
879 	QMenu popup(window());
880 
881 	QPointer<QMenu> addMenu = CreateAddFilterPopupMenu(async);
882 	if (addMenu)
883 		popup.addMenu(addMenu);
884 
885 	if (item) {
886 		const char *dulpicateSlot =
887 			async ? SLOT(DuplicateAsyncFilter())
888 			      : SLOT(DuplicateEffectFilter());
889 
890 		const char *renameSlot = async ? SLOT(RenameAsyncFilter())
891 					       : SLOT(RenameEffectFilter());
892 		const char *removeSlot =
893 			async ? SLOT(on_removeAsyncFilter_clicked())
894 			      : SLOT(on_removeEffectFilter_clicked());
895 
896 		popup.addSeparator();
897 		popup.addAction(QTStr("Duplicate"), this, dulpicateSlot);
898 		popup.addSeparator();
899 		popup.addAction(QTStr("Rename"), this, renameSlot);
900 		popup.addAction(QTStr("Remove"), this, removeSlot);
901 		popup.addSeparator();
902 
903 		QAction *copyAction = new QAction(QTStr("Copy"));
904 		connect(copyAction, SIGNAL(triggered()), this,
905 			SLOT(CopyFilter()));
906 		copyAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C));
907 		ui->effectWidget->addAction(copyAction);
908 		ui->asyncWidget->addAction(copyAction);
909 		popup.addAction(copyAction);
910 	}
911 
912 	QAction *pasteAction = new QAction(QTStr("Paste"));
913 	pasteAction->setEnabled(main->copyFilter);
914 	connect(pasteAction, SIGNAL(triggered()), this, SLOT(PasteFilter()));
915 	pasteAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V));
916 	ui->effectWidget->addAction(pasteAction);
917 	ui->asyncWidget->addAction(pasteAction);
918 	popup.addAction(pasteAction);
919 
920 	popup.exec(QCursor::pos());
921 }
922 
EditItem(QListWidgetItem * item,bool async)923 void OBSBasicFilters::EditItem(QListWidgetItem *item, bool async)
924 {
925 	if (editActive)
926 		return;
927 
928 	Qt::ItemFlags flags = item->flags();
929 	OBSSource filter = item->data(Qt::UserRole).value<OBSSource>();
930 	const char *name = obs_source_get_name(filter);
931 	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
932 
933 	item->setText(QT_UTF8(name));
934 	item->setFlags(flags | Qt::ItemIsEditable);
935 	list->removeItemWidget(item);
936 	list->editItem(item);
937 	item->setFlags(flags);
938 	editActive = true;
939 }
940 
DuplicateItem(QListWidgetItem * item)941 void OBSBasicFilters::DuplicateItem(QListWidgetItem *item)
942 {
943 	OBSSource filter = item->data(Qt::UserRole).value<OBSSource>();
944 	string name = obs_source_get_name(filter);
945 	obs_source_t *existing_filter;
946 
947 	QString placeholder = QString::fromStdString(name);
948 	QString text{placeholder};
949 	int i = 2;
950 	while ((existing_filter = obs_source_get_filter_by_name(
951 			source, QT_TO_UTF8(text)))) {
952 		obs_source_release(existing_filter);
953 		text = QString("%1 %2").arg(placeholder).arg(i++);
954 	}
955 
956 	bool success = NameDialog::AskForName(
957 		this, QTStr("Basic.Filters.AddFilter.Title"),
958 		QTStr("Basic.Filters.AddFilter.Text"), name, text);
959 	if (!success)
960 		return;
961 
962 	if (name.empty()) {
963 		OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"),
964 				       QTStr("NoNameEntered.Text"));
965 		DuplicateItem(item);
966 		return;
967 	}
968 
969 	existing_filter = obs_source_get_filter_by_name(source, name.c_str());
970 	if (existing_filter) {
971 		OBSMessageBox::warning(this, QTStr("NameExists.Title"),
972 				       QTStr("NameExists.Text"));
973 		obs_source_release(existing_filter);
974 		DuplicateItem(item);
975 		return;
976 	}
977 	bool enabled = obs_source_enabled(filter);
978 	obs_source_t *new_filter =
979 		obs_source_duplicate(filter, name.c_str(), false);
980 	if (new_filter) {
981 		const char *sourceName = obs_source_get_name(source);
982 		const char *id = obs_source_get_id(new_filter);
983 		blog(LOG_INFO,
984 		     "User duplicated filter '%s' (%s) from '%s' "
985 		     "to source '%s'",
986 		     name.c_str(), id, name.c_str(), sourceName);
987 		obs_source_set_enabled(new_filter, enabled);
988 		obs_source_filter_add(source, new_filter);
989 		obs_source_release(new_filter);
990 	}
991 }
992 
on_asyncFilters_customContextMenuRequested(const QPoint & pos)993 void OBSBasicFilters::on_asyncFilters_customContextMenuRequested(
994 	const QPoint &pos)
995 {
996 	CustomContextMenu(pos, true);
997 }
998 
on_effectFilters_customContextMenuRequested(const QPoint & pos)999 void OBSBasicFilters::on_effectFilters_customContextMenuRequested(
1000 	const QPoint &pos)
1001 {
1002 	CustomContextMenu(pos, false);
1003 }
1004 
RenameAsyncFilter()1005 void OBSBasicFilters::RenameAsyncFilter()
1006 {
1007 	EditItem(ui->asyncFilters->currentItem(), true);
1008 }
1009 
RenameEffectFilter()1010 void OBSBasicFilters::RenameEffectFilter()
1011 {
1012 	EditItem(ui->effectFilters->currentItem(), false);
1013 }
1014 
DuplicateAsyncFilter()1015 void OBSBasicFilters::DuplicateAsyncFilter()
1016 {
1017 	DuplicateItem(ui->asyncFilters->currentItem());
1018 }
1019 
DuplicateEffectFilter()1020 void OBSBasicFilters::DuplicateEffectFilter()
1021 {
1022 	DuplicateItem(ui->effectFilters->currentItem());
1023 }
1024 
FilterNameEdited(QWidget * editor,QListWidget * list)1025 void OBSBasicFilters::FilterNameEdited(QWidget *editor, QListWidget *list)
1026 {
1027 	QListWidgetItem *listItem = list->currentItem();
1028 	OBSSource filter = listItem->data(Qt::UserRole).value<OBSSource>();
1029 	QLineEdit *edit = qobject_cast<QLineEdit *>(editor);
1030 	string name = QT_TO_UTF8(edit->text().trimmed());
1031 
1032 	const char *prevName = obs_source_get_name(filter);
1033 	bool sameName = (name == prevName);
1034 	obs_source_t *foundFilter = nullptr;
1035 
1036 	if (!sameName)
1037 		foundFilter =
1038 			obs_source_get_filter_by_name(source, name.c_str());
1039 
1040 	if (foundFilter || name.empty() || sameName) {
1041 		listItem->setText(QT_UTF8(prevName));
1042 
1043 		if (foundFilter) {
1044 			OBSMessageBox::information(window(),
1045 						   QTStr("NameExists.Title"),
1046 						   QTStr("NameExists.Text"));
1047 			obs_source_release(foundFilter);
1048 
1049 		} else if (name.empty()) {
1050 			OBSMessageBox::information(window(),
1051 						   QTStr("NoNameEntered.Title"),
1052 						   QTStr("NoNameEntered.Text"));
1053 		}
1054 	} else {
1055 		const char *sourceName = obs_source_get_name(source);
1056 
1057 		blog(LOG_INFO,
1058 		     "User renamed filter '%s' on source '%s' to '%s'",
1059 		     prevName, sourceName, name.c_str());
1060 
1061 		listItem->setText(QT_UTF8(name.c_str()));
1062 		obs_source_set_name(filter, name.c_str());
1063 
1064 		std::string scene_name = obs_source_get_name(
1065 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1066 				->GetCurrentSceneSource());
1067 		auto undo = [scene_name, prev = std::string(prevName),
1068 			     name](const std::string &data) {
1069 			obs_source_t *ssource =
1070 				obs_get_source_by_name(scene_name.c_str());
1071 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1072 				->SetCurrentScene(ssource, true);
1073 			obs_source_release(ssource);
1074 
1075 			obs_source_t *source =
1076 				obs_get_source_by_name(data.c_str());
1077 			obs_source_t *filter = obs_source_get_filter_by_name(
1078 				source, name.c_str());
1079 			obs_source_set_name(filter, prev.c_str());
1080 			obs_source_release(source);
1081 			obs_source_release(filter);
1082 		};
1083 
1084 		auto redo = [scene_name, prev = std::string(prevName),
1085 			     name](const std::string &data) {
1086 			obs_source_t *ssource =
1087 				obs_get_source_by_name(scene_name.c_str());
1088 			reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1089 				->SetCurrentScene(ssource, true);
1090 			obs_source_release(ssource);
1091 
1092 			obs_source_t *source =
1093 				obs_get_source_by_name(data.c_str());
1094 			obs_source_t *filter = obs_source_get_filter_by_name(
1095 				source, prev.c_str());
1096 			obs_source_set_name(filter, name.c_str());
1097 			obs_source_release(source);
1098 			obs_source_release(filter);
1099 		};
1100 
1101 		std::string undo_data(sourceName);
1102 		std::string redo_data(sourceName);
1103 		main->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()),
1104 					undo, redo, undo_data, redo_data);
1105 	}
1106 
1107 	listItem->setText(QString());
1108 	SetupVisibilityItem(list, listItem, filter);
1109 	editActive = false;
1110 }
1111 
AsyncFilterNameEdited(QWidget * editor,QAbstractItemDelegate::EndEditHint endHint)1112 void OBSBasicFilters::AsyncFilterNameEdited(
1113 	QWidget *editor, QAbstractItemDelegate::EndEditHint endHint)
1114 {
1115 	FilterNameEdited(editor, ui->asyncFilters);
1116 	UNUSED_PARAMETER(endHint);
1117 }
1118 
EffectFilterNameEdited(QWidget * editor,QAbstractItemDelegate::EndEditHint endHint)1119 void OBSBasicFilters::EffectFilterNameEdited(
1120 	QWidget *editor, QAbstractItemDelegate::EndEditHint endHint)
1121 {
1122 	FilterNameEdited(editor, ui->effectFilters);
1123 	UNUSED_PARAMETER(endHint);
1124 }
1125 
ResetFilters()1126 void OBSBasicFilters::ResetFilters()
1127 {
1128 	QListWidget *list = isAsync ? ui->asyncFilters : ui->effectFilters;
1129 	int row = list->currentRow();
1130 
1131 	OBSSource filter = GetFilter(row, isAsync);
1132 
1133 	if (!filter)
1134 		return;
1135 
1136 	obs_data_t *settings = obs_source_get_settings(filter);
1137 	obs_data_clear(settings);
1138 	obs_data_release(settings);
1139 
1140 	if (!view->DeferUpdate())
1141 		obs_source_update(filter, nullptr);
1142 
1143 	view->RefreshProperties();
1144 }
1145 
CopyFilter()1146 void OBSBasicFilters::CopyFilter()
1147 {
1148 	OBSSource filter = nullptr;
1149 
1150 	if (isAsync)
1151 		filter = GetFilter(ui->asyncFilters->currentRow(), true);
1152 	else
1153 		filter = GetFilter(ui->effectFilters->currentRow(), false);
1154 
1155 	main->copyFilter = OBSGetWeakRef(filter);
1156 }
1157 
PasteFilter()1158 void OBSBasicFilters::PasteFilter()
1159 {
1160 	OBSSource filter = OBSGetStrongRef(main->copyFilter);
1161 	if (!filter)
1162 		return;
1163 
1164 	obs_data_array_t *undo_array = obs_source_backup_filters(source);
1165 	obs_source_copy_single_filter(source, filter);
1166 	obs_data_array_t *redo_array = obs_source_backup_filters(source);
1167 
1168 	const char *filterName = obs_source_get_name(filter);
1169 	const char *sourceName = obs_source_get_name(source);
1170 	QString text =
1171 		QTStr("Undo.Filters.Paste.Single").arg(filterName, sourceName);
1172 
1173 	main->CreateFilterPasteUndoRedoAction(text, source, undo_array,
1174 					      redo_array);
1175 
1176 	obs_data_array_release(undo_array);
1177 	obs_data_array_release(redo_array);
1178 }
1179 
delete_filter(OBSSource filter)1180 void OBSBasicFilters::delete_filter(OBSSource filter)
1181 {
1182 	obs_data_t *wrapper = obs_save_source(filter);
1183 	std::string parent_name(obs_source_get_name(source));
1184 	obs_data_set_string(wrapper, "undo_name", parent_name.c_str());
1185 
1186 	std::string scene_name = obs_source_get_name(
1187 		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1188 			->GetCurrentSceneSource());
1189 	auto undo = [scene_name](const std::string &data) {
1190 		obs_source_t *ssource =
1191 			obs_get_source_by_name(scene_name.c_str());
1192 		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1193 			->SetCurrentScene(ssource, true);
1194 		obs_source_release(ssource);
1195 
1196 		obs_data_t *dat = obs_data_create_from_json(data.c_str());
1197 		obs_source_t *source = obs_get_source_by_name(
1198 			obs_data_get_string(dat, "undo_name"));
1199 		obs_source_t *filter = obs_load_source(dat);
1200 		obs_source_filter_add(source, filter);
1201 
1202 		obs_data_release(dat);
1203 		obs_source_release(source);
1204 		obs_source_release(filter);
1205 	};
1206 
1207 	obs_data_t *rwrapper = obs_data_create();
1208 	obs_data_set_string(rwrapper, "fname", obs_source_get_name(filter));
1209 	obs_data_set_string(rwrapper, "sname", parent_name.c_str());
1210 	auto redo = [scene_name](const std::string &data) {
1211 		obs_source_t *ssource =
1212 			obs_get_source_by_name(scene_name.c_str());
1213 		reinterpret_cast<OBSBasic *>(App()->GetMainWindow())
1214 			->SetCurrentScene(ssource, true);
1215 		obs_source_release(ssource);
1216 
1217 		obs_data_t *dat = obs_data_create_from_json(data.c_str());
1218 		obs_source_t *source = obs_get_source_by_name(
1219 			obs_data_get_string(dat, "sname"));
1220 		obs_source_t *filter = obs_source_get_filter_by_name(
1221 			source, obs_data_get_string(dat, "fname"));
1222 		obs_source_filter_remove(source, filter);
1223 
1224 		obs_data_release(dat);
1225 		obs_source_release(filter);
1226 		obs_source_release(source);
1227 	};
1228 
1229 	std::string undo_data(obs_data_get_json(wrapper));
1230 	std::string redo_data(obs_data_get_json(rwrapper));
1231 	main->undo_s.add_action(
1232 		QTStr("Undo.Delete").arg(obs_source_get_name(filter)), undo,
1233 		redo, undo_data, redo_data, false);
1234 	obs_source_filter_remove(source, filter);
1235 
1236 	obs_data_release(wrapper);
1237 	obs_data_release(rwrapper);
1238 }
1239