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