Преглед на файлове

UI: Add user interface for filters

jp9000 преди 10 години
родител
ревизия
ceba24c7e9
променени са 7 файла, в които са добавени 1329 реда и са изтрити 0 реда
  1. 3 0
      obs/CMakeLists.txt
  2. 10 0
      obs/data/locale/en-US.ini
  3. 466 0
      obs/forms/OBSBasicFilters.ui
  4. 708 0
      obs/window-basic-filters.cpp
  5. 114 0
      obs/window-basic-filters.hpp
  6. 23 0
      obs/window-basic-main.cpp
  7. 5 0
      obs/window-basic-main.hpp

+ 3 - 0
obs/CMakeLists.txt

@@ -90,6 +90,7 @@ set(obs_SOURCES
 	${obs_PLATFORM_SOURCES}
 	obs-app.cpp
 	window-basic-main.cpp
+	window-basic-filters.cpp
 	window-basic-settings.cpp
 	window-basic-interaction.cpp
 	window-basic-properties.cpp
@@ -120,6 +121,7 @@ set(obs_HEADERS
 	platform.hpp
 	window-main.hpp
 	window-basic-main.hpp
+	window-basic-filters.hpp
 	window-basic-settings.hpp
 	window-basic-interaction.hpp
 	window-basic-properties.hpp
@@ -154,6 +156,7 @@ set(obs_UI
 	forms/OBSLogReply.ui
 	forms/OBSBasic.ui
 	forms/OBSBasicTransform.ui
+	forms/OBSBasicFilters.ui
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSourceSelect.ui
 	forms/OBSBasicInteraction.ui

+ 10 - 0
obs/data/locale/en-US.ini

@@ -17,6 +17,7 @@ No="No"
 Add="Add"
 Remove="Remove"
 Rename="Rename"
+Filters="Filters"
 Properties="Properties"
 MoveUp="Move Up"
 MoveDown="Move Down"
@@ -127,6 +128,15 @@ Basic.InteractionWindow="Interacting with '%1'"
 Basic.StatusBar.Reconnecting="Disconnected, reconnecting (attempt %1)"
 Basic.StatusBar.ReconnectSuccessful="Reconnection successful"
 
+# filters window
+Basic.Filters="Filters"
+Basic.Filters.AsyncFilters="Audio/Video Filters"
+Basic.Filters.AudioFilters="Audio Filters"
+Basic.Filters.EffectFilters="Effect Filters"
+Basic.Filters.Title="Filters for '%1'"
+Basic.Filters.AddFilter.Title="Filter name"
+Basic.Filters.AddFilter.Text="Please specify the name of the filter"
+
 # transform window
 Basic.TransformWindow="Scene Item Transform"
 Basic.TransformWindow.Position="Position"

+ 466 - 0
obs/forms/OBSBasicFilters.ui

@@ -0,0 +1,466 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSBasicFilters</class>
+ <widget class="QDialog" name="OBSBasicFilters">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>861</width>
+    <height>726</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Basic.Filters</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="sizeConstraint">
+      <enum>QLayout::SetMinimumSize</enum>
+     </property>
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QWidget" name="asyncWidget" native="true">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <property name="leftMargin">
+           <number>0</number>
+          </property>
+          <property name="topMargin">
+           <number>0</number>
+          </property>
+          <property name="rightMargin">
+           <number>0</number>
+          </property>
+          <property name="bottomMargin">
+           <number>0</number>
+          </property>
+          <item>
+           <widget class="QLabel" name="asyncLabel">
+            <property name="text">
+             <string>Basic.Filters.AsyncFilters</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="FocusList" name="asyncFilters">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="contextMenuPolicy">
+             <enum>Qt::CustomContextMenu</enum>
+            </property>
+           </widget>
+          </item>
+          <item alignment="Qt::AlignLeft">
+           <widget class="QWidget" name="widget" native="true">
+            <layout class="QHBoxLayout" name="horizontalLayout_4">
+             <property name="spacing">
+              <number>4</number>
+             </property>
+             <property name="leftMargin">
+              <number>0</number>
+             </property>
+             <property name="topMargin">
+              <number>0</number>
+             </property>
+             <property name="rightMargin">
+              <number>0</number>
+             </property>
+             <property name="bottomMargin">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QPushButton" name="addAsyncFilter">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/add.png</normaloff>:/res/images/add.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>addIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="removeAsyncFilter">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/list_remove.png</normaloff>:/res/images/list_remove.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>removeIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="moveAsyncFilterUp">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/up.png</normaloff>:/res/images/up.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>upArrowIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="moveAsyncFilterDown">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/down.png</normaloff>:/res/images/down.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>downArrowIconSmall</string>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="Line" name="separatorLine">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QWidget" name="effectWidget" native="true">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_4">
+          <property name="leftMargin">
+           <number>0</number>
+          </property>
+          <property name="topMargin">
+           <number>0</number>
+          </property>
+          <property name="rightMargin">
+           <number>0</number>
+          </property>
+          <property name="bottomMargin">
+           <number>0</number>
+          </property>
+          <item>
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>Basic.Filters.EffectFilters</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="FocusList" name="effectFilters">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="contextMenuPolicy">
+             <enum>Qt::CustomContextMenu</enum>
+            </property>
+           </widget>
+          </item>
+          <item alignment="Qt::AlignLeft">
+           <widget class="QWidget" name="widget_2" native="true">
+            <layout class="QHBoxLayout" name="horizontalLayout_6">
+             <property name="spacing">
+              <number>4</number>
+             </property>
+             <property name="leftMargin">
+              <number>0</number>
+             </property>
+             <property name="topMargin">
+              <number>0</number>
+             </property>
+             <property name="rightMargin">
+              <number>0</number>
+             </property>
+             <property name="bottomMargin">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QPushButton" name="addEffectFilter">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/add.png</normaloff>:/res/images/add.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>addIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="removeEffectFilter">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/list_remove.png</normaloff>:/res/images/list_remove.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>removeIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="moveEffectFilterUp">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/up.png</normaloff>:/res/images/up.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>upArrowIconSmall</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QPushButton" name="moveEffectFilterDown">
+               <property name="maximumSize">
+                <size>
+                 <width>22</width>
+                 <height>22</height>
+                </size>
+               </property>
+               <property name="text">
+                <string notr="true"/>
+               </property>
+               <property name="icon">
+                <iconset resource="obs.qrc">
+                 <normaloff>:/res/images/down.png</normaloff>:/res/images/down.png</iconset>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="flat">
+                <bool>true</bool>
+               </property>
+               <property name="themeID" stdset="0">
+                <string>downArrowIconSmall</string>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <layout class="QVBoxLayout" name="rightContainerLayout">
+       <item>
+        <layout class="QVBoxLayout" name="rightLayout">
+         <item>
+          <widget class="OBSQTDisplay" name="preview" native="true">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>24</width>
+             <height>24</height>
+            </size>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <property name="spacing">
+          <number>4</number>
+         </property>
+         <property name="sizeConstraint">
+          <enum>QLayout::SetMaximumSize</enum>
+         </property>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>0</width>
+             <height>0</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QPushButton" name="close">
+           <property name="text">
+            <string>Close</string>
+           </property>
+           <property name="autoDefault">
+            <bool>false</bool>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>OBSQTDisplay</class>
+   <extends>QWidget</extends>
+   <header>qt-display.hpp</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>FocusList</class>
+   <extends>QListWidget</extends>
+   <header>focus-list.hpp</header>
+  </customwidget>
+ </customwidgets>
+ <resources>
+  <include location="obs.qrc"/>
+ </resources>
+ <connections>
+  <connection>
+   <sender>close</sender>
+   <signal>clicked()</signal>
+   <receiver>OBSBasicFilters</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>811</x>
+     <y>701</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>722</x>
+     <y>727</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 708 - 0
obs/window-basic-filters.cpp

@@ -0,0 +1,708 @@
+/******************************************************************************
+    Copyright (C) 2015 by Hugh Bailey <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include "window-namedialog.hpp"
+#include "window-basic-filters.hpp"
+#include "display-helpers.hpp"
+#include "qt-wrappers.hpp"
+#include "visibility-item-widget.hpp"
+#include "obs-app.hpp"
+
+#include <QMessageBox>
+#include <QCloseEvent>
+#include <string>
+#include <QMenu>
+#include <QVariant>
+
+using namespace std;
+
+Q_DECLARE_METATYPE(OBSSource);
+
+OBSBasicFilters::OBSBasicFilters(QWidget *parent, OBSSource source_)
+	: QDialog                      (parent),
+	  ui                           (new Ui::OBSBasicFilters),
+	  source                       (source_),
+	  addSignal                    (obs_source_get_signal_handler(source),
+	                                "filter_add",
+	                                OBSBasicFilters::OBSSourceFilterAdded,
+	                                this),
+	  removeSignal                 (obs_source_get_signal_handler(source),
+	                                "filter_remove",
+	                                OBSBasicFilters::OBSSourceFilterRemoved,
+	                                this),
+	  reorderSignal                (obs_source_get_signal_handler(source),
+	                                "reorder_filters",
+	                                OBSBasicFilters::OBSSourceReordered,
+	                                this),
+	  removeSourceSignal           (obs_source_get_signal_handler(source),
+	                                "remove",
+	                                OBSBasicFilters::SourceRemoved, this),
+	  renameSourceSignal           (obs_source_get_signal_handler(source),
+	                                "rename",
+	                                OBSBasicFilters::SourceRenamed, this)
+{
+	ui->setupUi(this);
+	UpdateFilters();
+
+	ui->asyncFilters->setItemDelegate(
+			new VisibilityItemDelegate(ui->asyncFilters));
+	ui->effectFilters->setItemDelegate(
+			new VisibilityItemDelegate(ui->effectFilters));
+
+	const char *name = obs_source_get_name(source);
+	setWindowTitle(QTStr("Basic.Filters.Title").arg(QT_UTF8(name)));
+
+	connect(ui->preview, SIGNAL(DisplayResized()),
+			this, SLOT(OnPreviewResized()));
+
+	connect(ui->asyncFilters->itemDelegate(),
+			SIGNAL(closeEditor(QWidget*,
+					QAbstractItemDelegate::EndEditHint)),
+			this,
+			SLOT(AsyncFilterNameEdited(QWidget*,
+					QAbstractItemDelegate::EndEditHint)));
+
+	connect(ui->effectFilters->itemDelegate(),
+			SIGNAL(closeEditor(QWidget*,
+					QAbstractItemDelegate::EndEditHint)),
+			this,
+			SLOT(EffectFilterNameEdited(QWidget*,
+					QAbstractItemDelegate::EndEditHint)));
+
+	uint32_t flags = obs_source_get_output_flags(source);
+	bool audio     = (flags & OBS_SOURCE_AUDIO) != 0;
+	bool audioOnly = (flags & OBS_SOURCE_VIDEO) == 0;
+	bool async     = (flags & OBS_SOURCE_ASYNC) != 0;
+
+	if (!async && !audio) {
+		ui->asyncWidget->setVisible(false);
+		ui->separatorLine->setVisible(false);
+	}
+	if (audioOnly) {
+		ui->effectWidget->setVisible(false);
+		ui->separatorLine->setVisible(false);
+	}
+
+	if (audioOnly || (audio && !async))
+		ui->asyncLabel->setText(QTStr("Basic.Filters.AudioFilters"));
+}
+
+OBSBasicFilters::~OBSBasicFilters()
+{
+	ui->asyncFilters->clear();
+	ui->effectFilters->clear();
+	QApplication::sendPostedEvents(this);
+}
+
+void OBSBasicFilters::Init()
+{
+	gs_init_data init_data = {};
+
+	show();
+
+	QSize previewSize = GetPixelSize(ui->preview);
+	init_data.cx      = uint32_t(previewSize.width());
+	init_data.cy      = uint32_t(previewSize.height());
+	init_data.format  = GS_RGBA;
+	QTToGSWindow(ui->preview->winId(), init_data.window);
+
+	display = obs_display_create(&init_data);
+
+	if (display)
+		obs_display_add_draw_callback(display,
+				OBSBasicFilters::DrawPreview, this);
+}
+
+inline OBSSource OBSBasicFilters::GetFilter(int row, bool async)
+{
+	if (row == -1)
+		return OBSSource();
+
+	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
+	QListWidgetItem *item = list->item(row);
+	if (!item)
+		return OBSSource();
+
+	QVariant v = item->data(Qt::UserRole);
+	return v.value<OBSSource>();
+}
+
+void OBSBasicFilters::UpdatePropertiesView(int row, bool async)
+{
+	delete view;
+	view = nullptr;
+
+	OBSSource filter = GetFilter(row, async);
+	if (!filter)
+		return;
+
+	obs_data_t *settings = obs_source_get_settings(filter);
+
+	view = new OBSPropertiesView(settings, filter,
+			(PropertiesReloadCallback)obs_source_properties,
+			(PropertiesUpdateCallback)obs_source_update);
+
+	obs_data_release(settings);
+
+	view->setMaximumHeight(250);
+	view->setMinimumHeight(150);
+	ui->rightLayout->addWidget(view);
+	view->show();
+}
+
+void OBSBasicFilters::AddFilter(OBSSource filter)
+{
+	uint32_t flags = obs_source_get_output_flags(filter);
+	bool async = (flags & OBS_SOURCE_ASYNC) != 0;
+	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
+
+	QListWidgetItem *item = new QListWidgetItem();
+	Qt::ItemFlags itemFlags = item->flags();
+
+	item->setFlags(itemFlags | Qt::ItemIsEditable);
+	item->setData(Qt::UserRole, QVariant::fromValue(filter));
+
+	list->addItem(item);
+	list->setCurrentItem(item);
+	SetupVisibilityItem(list, item, filter);
+}
+
+void OBSBasicFilters::RemoveFilter(OBSSource filter)
+{
+	uint32_t flags = obs_source_get_output_flags(filter);
+	bool async = (flags & OBS_SOURCE_ASYNC) != 0;
+	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
+
+	for (int i = 0; i < list->count(); i++) {
+		QListWidgetItem *item = list->item(i);
+		QVariant v = item->data(Qt::UserRole);
+		OBSSource curFilter = v.value<OBSSource>();
+
+		if (filter == curFilter) {
+			delete item;
+			break;
+		}
+	}
+}
+
+struct FilterOrderInfo {
+	int asyncIdx = 0;
+	int effectIdx = 0;
+	OBSBasicFilters *window;
+
+	inline FilterOrderInfo(OBSBasicFilters *window_) : window(window_) {}
+};
+
+void OBSBasicFilters::ReorderFilter(QListWidget *list,
+		obs_source_t *filter, size_t idx)
+{
+	int count = list->count();
+
+	for (int i = 0; i < count; i++) {
+		QListWidgetItem *listItem = list->item(i);
+		QVariant v = listItem->data(Qt::UserRole);
+		OBSSource filterItem = v.value<OBSSource>();
+
+		if (filterItem == filter) {
+			if ((int)idx != i) {
+				bool sel = (list->currentRow() == i);
+
+				listItem = list->takeItem(i);
+				if (listItem)  {
+					list->insertItem((int)idx, listItem);
+					SetupVisibilityItem(list,
+							listItem, filterItem);
+
+					if (sel)
+						list->setCurrentRow((int)idx);
+				}
+			}
+
+			break;
+		}
+	}
+}
+
+void OBSBasicFilters::ReorderFilters()
+{
+	FilterOrderInfo info(this);
+
+	obs_source_enum_filters(source,
+			[] (obs_source_t*, obs_source_t *filter, void *p)
+			{
+				FilterOrderInfo *info =
+					reinterpret_cast<FilterOrderInfo*>(p);
+				uint32_t flags;
+				bool async;
+
+				flags = obs_source_get_output_flags(filter);
+				async = (flags & OBS_SOURCE_ASYNC) != 0;
+
+				if (async) {
+					info->window->ReorderFilter(
+						info->window->ui->asyncFilters,
+						filter, info->asyncIdx++);
+				} else {
+					info->window->ReorderFilter(
+						info->window->ui->effectFilters,
+						filter, info->effectIdx++);
+				}
+			}, &info);
+}
+
+void OBSBasicFilters::UpdateFilters()
+{
+	if (!source)
+		return;
+
+	ui->effectFilters->clear();
+	ui->asyncFilters->clear();
+
+	obs_source_enum_filters(source,
+			[] (obs_source_t*, obs_source_t *filter, void *p)
+			{
+				OBSBasicFilters *window =
+					reinterpret_cast<OBSBasicFilters*>(p);
+
+				window->AddFilter(filter);
+			}, this);
+}
+
+static bool filter_compatible(bool async, uint32_t sourceFlags,
+		uint32_t filterFlags)
+{
+	bool filterVideo = (filterFlags & OBS_SOURCE_VIDEO) != 0;
+	bool filterAsync = (filterFlags & OBS_SOURCE_ASYNC) != 0;
+	bool filterAudio = (filterFlags & OBS_SOURCE_AUDIO) != 0;
+	bool audio       = (sourceFlags & OBS_SOURCE_AUDIO) != 0;
+	bool audioOnly   = (sourceFlags & OBS_SOURCE_VIDEO) == 0;
+	bool asyncSource = (sourceFlags & OBS_SOURCE_ASYNC) != 0;
+
+	if (async && ((audioOnly && filterVideo) || (!audio && !asyncSource)))
+		return false;
+
+	return (async && (filterAudio || filterAsync)) ||
+		(!async && !filterAudio && !filterAsync);
+}
+
+QMenu *OBSBasicFilters::CreateAddFilterPopupMenu(bool async)
+{
+	uint32_t sourceFlags = obs_source_get_output_flags(source);
+	const char *type;
+	bool foundValues = false;
+	size_t idx = 0;
+
+	QMenu *popup = new QMenu(QTStr("Add"), this);
+	while (obs_enum_filter_types(idx++, &type)) {
+		const char *name = obs_source_get_display_name(
+				OBS_SOURCE_TYPE_FILTER, type);
+		uint32_t filterFlags = obs_get_source_output_flags(
+				OBS_SOURCE_TYPE_FILTER, type);
+
+		if (!filter_compatible(async, sourceFlags, filterFlags))
+			continue;
+
+		QAction *popupItem = new QAction(QT_UTF8(name), this);
+		popupItem->setData(QT_UTF8(type));
+		connect(popupItem, SIGNAL(triggered(bool)),
+				this, SLOT(AddFilterFromAction()));
+		popup->addAction(popupItem);
+
+		foundValues = true;
+	}
+
+	if (!foundValues) {
+		delete popup;
+		popup = nullptr;
+	}
+
+	return popup;
+}
+
+void OBSBasicFilters::AddNewFilter(const char *id)
+{
+	if (id && *id) {
+		obs_source_t *existing_filter;
+		string name = obs_source_get_display_name(
+				OBS_SOURCE_TYPE_FILTER, id);
+
+		bool success = NameDialog::AskForName(this,
+				QTStr("Basic.Filters.AddFilter.Title"),
+				QTStr("Basic.FIlters.AddFilter.Text"), name,
+				QT_UTF8(name.c_str()));
+		if (!success)
+			return;
+
+		if (name.empty()) {
+			QMessageBox::information(this,
+					QTStr("NoNameEntered.Title"),
+					QTStr("NoNameEntered.Text"));
+			AddNewFilter(id);
+			return;
+		}
+
+		existing_filter = obs_source_get_filter_by_name(source,
+				name.c_str());
+		if (existing_filter) {
+			QMessageBox::information(this,
+					QTStr("NameExists.Title"),
+					QTStr("NameExists.Text"));
+			obs_source_release(existing_filter);
+			AddNewFilter(id);
+			return;
+		}
+
+		obs_source_t *filter = obs_source_create(OBS_SOURCE_TYPE_FILTER,
+				id, name.c_str(), nullptr);
+		if (filter) {
+			obs_source_filter_add(source, filter);
+			obs_source_release(filter);
+		}
+	}
+}
+
+void OBSBasicFilters::AddFilterFromAction()
+{
+	QAction *action = qobject_cast<QAction*>(sender());
+	if (!action)
+		return;
+
+	AddNewFilter(QT_TO_UTF8(action->data().toString()));
+}
+
+void OBSBasicFilters::OnPreviewResized()
+{
+	if (resizeTimer)
+		killTimer(resizeTimer);
+	resizeTimer = startTimer(100);
+}
+
+void OBSBasicFilters::closeEvent(QCloseEvent *event)
+{
+	QDialog::closeEvent(event);
+	if (!event->isAccepted())
+		return;
+
+	// remove draw callback and release display in case our drawable
+	// surfaces go away before the destructor gets called
+	obs_display_remove_draw_callback(display,
+			OBSBasicFilters::DrawPreview, this);
+	display = nullptr;
+}
+
+void OBSBasicFilters::timerEvent(QTimerEvent *event)
+{
+	if (event->timerId() == resizeTimer) {
+		killTimer(resizeTimer);
+		resizeTimer = 0;
+
+		QSize size = GetPixelSize(ui->preview);
+		obs_display_resize(display, size.width(), size.height());
+	}
+}
+
+/* OBS Signals */
+
+void OBSBasicFilters::OBSSourceFilterAdded(void *param, calldata_t *data)
+{
+	OBSBasicFilters *window = reinterpret_cast<OBSBasicFilters*>(param);
+	obs_source_t *filter = (obs_source_t*)calldata_ptr(data, "filter");
+
+	QMetaObject::invokeMethod(window, "AddFilter",
+			Q_ARG(OBSSource, OBSSource(filter)));
+}
+
+void OBSBasicFilters::OBSSourceFilterRemoved(void *param, calldata_t *data)
+{
+	OBSBasicFilters *window = reinterpret_cast<OBSBasicFilters*>(param);
+	obs_source_t *filter = (obs_source_t*)calldata_ptr(data, "filter");
+
+	QMetaObject::invokeMethod(window, "RemoveFilter",
+			Q_ARG(OBSSource, OBSSource(filter)));
+}
+
+void OBSBasicFilters::OBSSourceReordered(void *param, calldata_t *data)
+{
+	QMetaObject::invokeMethod(reinterpret_cast<OBSBasicFilters*>(param),
+			"ReorderFilters");
+
+	UNUSED_PARAMETER(data);
+}
+
+void OBSBasicFilters::SourceRemoved(void *data, calldata_t *params)
+{
+	UNUSED_PARAMETER(params);
+
+	QMetaObject::invokeMethod(static_cast<OBSBasicFilters*>(data),
+	                "close");
+}
+
+void OBSBasicFilters::SourceRenamed(void *data, calldata_t *params)
+{
+	const char *name = calldata_string(params, "new_name");
+	QString title = QTStr("Basic.Filters.Title").arg(QT_UTF8(name));
+
+	QMetaObject::invokeMethod(static_cast<OBSBasicFilters*>(data),
+	                "setWindowTitle", Q_ARG(QString, title));
+}
+
+void OBSBasicFilters::DrawPreview(void *data, uint32_t cx, uint32_t cy)
+{
+	OBSBasicFilters *window = static_cast<OBSBasicFilters*>(data);
+
+	if (!window->source)
+		return;
+
+	uint32_t sourceCX = max(obs_source_get_width(window->source), 1u);
+	uint32_t sourceCY = max(obs_source_get_height(window->source), 1u);
+
+	int   x, y;
+	int   newCX, newCY;
+	float scale;
+
+	GetScaleAndCenterPos(sourceCX, sourceCY, cx, cy, x, y, scale);
+
+	newCX = int(scale * float(sourceCX));
+	newCY = int(scale * float(sourceCY));
+
+	gs_viewport_push();
+	gs_projection_push();
+	gs_ortho(0.0f, float(sourceCX), 0.0f, float(sourceCY), -100.0f, 100.0f);
+	gs_set_viewport(x, y, newCX, newCY);
+
+	obs_source_video_render(window->source);
+
+	gs_projection_pop();
+	gs_viewport_pop();
+}
+
+/* Qt Slots */
+
+static bool QueryRemove(QWidget *parent, obs_source_t *source)
+{
+	const char *name  = obs_source_get_name(source);
+
+	QString text = QTStr("ConfirmRemove.Text");
+	text.replace("$1", QT_UTF8(name));
+
+	QMessageBox remove_source(parent);
+	remove_source.setText(text);
+	QAbstractButton *Yes = remove_source.addButton(QTStr("Yes"),
+			QMessageBox::YesRole);
+	remove_source.addButton(QTStr("No"), QMessageBox::NoRole);
+	remove_source.setIcon(QMessageBox::Question);
+	remove_source.setWindowTitle(QTStr("ConfirmRemove.Title"));
+	remove_source.exec();
+
+	return Yes == remove_source.clickedButton();
+}
+
+void OBSBasicFilters::on_addAsyncFilter_clicked()
+{
+	QPointer<QMenu> popup = CreateAddFilterPopupMenu(true);
+	if (popup)
+		popup->exec(QCursor::pos());
+}
+
+void OBSBasicFilters::on_removeAsyncFilter_clicked()
+{
+	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
+	if (filter) {
+		if (QueryRemove(this, filter))
+			obs_source_filter_remove(source, filter);
+	}
+}
+
+void OBSBasicFilters::on_moveAsyncFilterUp_clicked()
+{
+	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
+	if (filter)
+		obs_source_filter_set_order(source, filter, OBS_ORDER_MOVE_UP);
+}
+
+void OBSBasicFilters::on_moveAsyncFilterDown_clicked()
+{
+	OBSSource filter = GetFilter(ui->asyncFilters->currentRow(), true);
+	if (filter)
+		obs_source_filter_set_order(source, filter,
+				OBS_ORDER_MOVE_DOWN);
+}
+
+void OBSBasicFilters::on_asyncFilters_GotFocus()
+{
+	UpdatePropertiesView(ui->asyncFilters->currentRow(), true);
+}
+
+void OBSBasicFilters::on_asyncFilters_currentRowChanged(int row)
+{
+	UpdatePropertiesView(row, true);
+}
+
+void OBSBasicFilters::on_addEffectFilter_clicked()
+{
+	QPointer<QMenu> popup = CreateAddFilterPopupMenu(false);
+	if (popup)
+		popup->exec(QCursor::pos());
+}
+
+void OBSBasicFilters::on_removeEffectFilter_clicked()
+{
+	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
+	if (filter) {
+		if (QueryRemove(this, filter))
+			obs_source_filter_remove(source, filter);
+	}
+}
+
+void OBSBasicFilters::on_moveEffectFilterUp_clicked()
+{
+	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
+	if (filter)
+		obs_source_filter_set_order(source, filter, OBS_ORDER_MOVE_UP);
+}
+
+void OBSBasicFilters::on_moveEffectFilterDown_clicked()
+{
+	OBSSource filter = GetFilter(ui->effectFilters->currentRow(), false);
+	if (filter)
+		obs_source_filter_set_order(source, filter,
+				OBS_ORDER_MOVE_DOWN);
+}
+
+void OBSBasicFilters::on_effectFilters_GotFocus()
+{
+	UpdatePropertiesView(ui->effectFilters->currentRow(), false);
+}
+
+void OBSBasicFilters::on_effectFilters_currentRowChanged(int row)
+{
+	UpdatePropertiesView(row, false);
+}
+
+void OBSBasicFilters::CustomContextMenu(const QPoint &pos, bool async)
+{
+	QListWidget *list = async ? ui->asyncFilters : ui->effectFilters;
+	QListWidgetItem *item = list->itemAt(pos);
+
+	QMenu popup(window());
+
+	QPointer<QMenu> addMenu = CreateAddFilterPopupMenu(async);
+	if (addMenu)
+		popup.addMenu(addMenu);
+
+	if (item) {
+		const char *renameSlot = async ?
+			SLOT(RenameAsyncFilter()) : SLOT(RenameEffectFilter());
+		const char *removeSlot = async ?
+			SLOT(on_removeAsyncFilter_clicked()) :
+			SLOT(on_removeEffectFilter_clicked());
+
+		popup.addSeparator();
+		popup.addAction(QTStr("Rename"), this, renameSlot);
+		popup.addAction(QTStr("Remove"), this, removeSlot);
+	}
+
+	popup.exec(QCursor::pos());
+}
+
+void OBSBasicFilters::EditItem(QListWidgetItem *item, bool async)
+{
+	Qt::ItemFlags flags = item->flags();
+	OBSSource filter    = item->data(Qt::UserRole).value<OBSSource>();
+	const char *name    = obs_source_get_name(filter);
+	QListWidget *list   = async ? ui->asyncFilters : ui->effectFilters;
+
+	item->setText(QT_UTF8(name));
+	item->setFlags(flags | Qt::ItemIsEditable);
+	list->removeItemWidget(item);
+	list->editItem(item);
+	item->setFlags(flags);
+}
+
+void OBSBasicFilters::on_asyncFilters_customContextMenuRequested(
+		const QPoint &pos)
+{
+	CustomContextMenu(pos, true);
+}
+
+void OBSBasicFilters::on_effectFilters_customContextMenuRequested(
+		const QPoint &pos)
+{
+	CustomContextMenu(pos, false);
+}
+
+void OBSBasicFilters::RenameAsyncFilter()
+{
+	EditItem(ui->asyncFilters->currentItem(), true);
+}
+
+void OBSBasicFilters::RenameEffectFilter()
+{
+	EditItem(ui->effectFilters->currentItem(), false);
+}
+
+void OBSBasicFilters::FilterNameEdited(QWidget *editor, QListWidget *list)
+{
+	QListWidgetItem *listItem = list->currentItem();
+	OBSSource filter = listItem->data(Qt::UserRole).value<OBSSource>();
+	QLineEdit *edit = qobject_cast<QLineEdit*>(editor);
+	string name = QT_TO_UTF8(edit->text().trimmed());
+
+	const char *prevName = obs_source_get_name(filter);
+	bool sameName = (name == prevName);
+	obs_source_t *foundFilter = nullptr;
+
+	if (!sameName)
+		foundFilter = obs_source_get_filter_by_name(source,
+				name.c_str());
+
+	if (foundFilter || name.empty() || sameName) {
+		listItem->setText(QT_UTF8(prevName));
+
+		if (foundFilter) {
+			QMessageBox::information(window(),
+				QTStr("NameExists.Title"),
+				QTStr("NameExists.Text"));
+			obs_source_release(foundFilter);
+
+		} else if (name.empty()) {
+			QMessageBox::information(window(),
+				QTStr("NoNameEntered.Title"),
+				QTStr("NoNameEntered.Text"));
+		}
+	} else {
+		listItem->setText(QT_UTF8(name.c_str()));
+		obs_source_set_name(filter, name.c_str());
+	}
+
+	listItem->setText(QString());
+	SetupVisibilityItem(list, listItem, filter);
+}
+
+void OBSBasicFilters::AsyncFilterNameEdited(QWidget *editor,
+		QAbstractItemDelegate::EndEditHint endHint)
+{
+	FilterNameEdited(editor, ui->asyncFilters);
+	UNUSED_PARAMETER(endHint);
+}
+
+void OBSBasicFilters::EffectFilterNameEdited(QWidget *editor,
+		QAbstractItemDelegate::EndEditHint endHint)
+{
+	FilterNameEdited(editor, ui->effectFilters);
+	UNUSED_PARAMETER(endHint);
+}

+ 114 - 0
obs/window-basic-filters.hpp

@@ -0,0 +1,114 @@
+/******************************************************************************
+    Copyright (C) 2015 by Hugh Bailey <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <memory>
+#include <obs.hpp>
+
+#include "properties-view.hpp"
+
+class OBSBasic;
+class QMenu;
+
+#include "ui_OBSBasicFilters.h"
+
+class OBSBasicFilters : public QDialog {
+	Q_OBJECT
+
+private:
+	OBSBasic *main;
+
+	std::unique_ptr<Ui::OBSBasicFilters> ui;
+	int resizeTimer = 0;
+	OBSSource source;
+	OBSPropertiesView *view = nullptr;
+
+	OBSDisplay display;
+	OBSSignal addSignal;
+	OBSSignal removeSignal;
+	OBSSignal reorderSignal;
+
+	OBSSignal removeSourceSignal;
+	OBSSignal renameSourceSignal;
+
+	inline OBSSource GetFilter(int row, bool async);
+
+	void UpdateFilters();
+	void UpdatePropertiesView(int row, bool async);
+
+	static void OBSSourceFilterAdded(void *param, calldata_t *data);
+	static void OBSSourceFilterRemoved(void *param, calldata_t *data);
+	static void OBSSourceReordered(void *param, calldata_t *data);
+	static void SourceRemoved(void *param, calldata_t *data);
+	static void SourceRenamed(void *param, calldata_t *data);
+	static void DrawPreview(void *data, uint32_t cx, uint32_t cy);
+
+	QMenu *CreateAddFilterPopupMenu(bool async);
+
+	void AddNewFilter(const char *id);
+	void ReorderFilter(QListWidget *list, obs_source_t *filter, size_t idx);
+
+	void CustomContextMenu(const QPoint &pos, bool async);
+	void EditItem(QListWidgetItem *item, bool async);
+
+	void FilterNameEdited(QWidget *editor, QListWidget *list);
+
+private slots:
+	void AddFilter(OBSSource filter);
+	void RemoveFilter(OBSSource filter);
+	void ReorderFilters();
+	void RenameAsyncFilter();
+	void RenameEffectFilter();
+
+	void AddFilterFromAction();
+
+	void OnPreviewResized();
+
+	void on_addAsyncFilter_clicked();
+	void on_removeAsyncFilter_clicked();
+	void on_moveAsyncFilterUp_clicked();
+	void on_moveAsyncFilterDown_clicked();
+	void on_asyncFilters_currentRowChanged(int row);
+	void on_asyncFilters_customContextMenuRequested(const QPoint &pos);
+	void on_asyncFilters_GotFocus();
+
+	void on_addEffectFilter_clicked();
+	void on_removeEffectFilter_clicked();
+	void on_moveEffectFilterUp_clicked();
+	void on_moveEffectFilterDown_clicked();
+	void on_effectFilters_currentRowChanged(int row);
+	void on_effectFilters_customContextMenuRequested(const QPoint &pos);
+	void on_effectFilters_GotFocus();
+
+	void AsyncFilterNameEdited(QWidget *editor,
+			QAbstractItemDelegate::EndEditHint endHint);
+	void EffectFilterNameEdited(QWidget *editor,
+			QAbstractItemDelegate::EndEditHint endHint);
+
+public:
+	OBSBasicFilters(QWidget *parent, OBSSource source_);
+	~OBSBasicFilters();
+
+	void Init();
+
+protected:
+	virtual void closeEvent(QCloseEvent *event) override;
+	virtual void timerEvent(QTimerEvent *event) override;
+};

+ 23 - 0
obs/window-basic-main.cpp

@@ -677,6 +677,9 @@ OBSBasic::~OBSBasic()
 	if (properties)
 		delete properties;
 
+	if (filters)
+		delete filters;
+
 	if (transformWindow)
 		delete transformWindow;
 
@@ -794,6 +797,16 @@ void OBSBasic::CreatePropertiesWindow(obs_source_t *source)
 	properties->setAttribute(Qt::WA_DeleteOnClose, true);
 }
 
+void OBSBasic::CreateFiltersWindow(obs_source_t *source)
+{
+	if (filters)
+		filters->close();
+
+	filters = new OBSBasicFilters(this, source);
+	filters->Init();
+	filters->setAttribute(Qt::WA_DeleteOnClose, true);
+}
+
 /* Qt callbacks for invokeMethod */
 
 void OBSBasic::AddScene(OBSSource source)
@@ -1957,6 +1970,8 @@ void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos)
 		action->setEnabled(obs_source_get_output_flags(source) &
 				OBS_SOURCE_INTERACTION);
 
+		popup.addAction(QTStr("Filters"), this,
+				SLOT(OpenFilters()));
 		popup.addAction(QTStr("Properties"), this,
 				SLOT(on_actionSourceProperties_triggered()));
 	}
@@ -2305,6 +2320,14 @@ void OBSBasic::SceneItemNameEdited(QWidget *editor,
 	UNUSED_PARAMETER(endHint);
 }
 
+void OBSBasic::OpenFilters()
+{
+	OBSSceneItem item = GetCurrentSceneItem();
+	OBSSource source = obs_sceneitem_get_source(item);
+
+	CreateFiltersWindow(source);
+}
+
 void OBSBasic::StreamingStart()
 {
 	ui->streamButton->setText(QTStr("Basic.Main.StopStreaming"));

+ 5 - 0
obs/window-basic-main.hpp

@@ -29,6 +29,7 @@
 #include "window-basic-properties.hpp"
 #include "window-basic-transform.hpp"
 #include "window-basic-adv-audio.hpp"
+#include "window-basic-filters.hpp"
 
 #include <util/platform.h>
 #include <util/util.hpp>
@@ -67,6 +68,7 @@ private:
 	QPointer<OBSBasicProperties> properties;
 	QPointer<OBSBasicTransform> transformWindow;
 	QPointer<OBSBasicAdvAudio> advAudioWindow;
+	QPointer<OBSBasicFilters> filters;
 
 	QNetworkAccessManager networkManager;
 
@@ -133,6 +135,7 @@ private:
 
 	void CreateInteractionWindow(obs_source_t *source);
 	void CreatePropertiesWindow(obs_source_t *source);
+	void CreateFiltersWindow(obs_source_t *source);
 
 public slots:
 	void StreamingStart();
@@ -297,6 +300,8 @@ private slots:
 	void SceneItemNameEdited(QWidget *editor,
 			QAbstractItemDelegate::EndEditHint endHint);
 
+	void OpenFilters();
+
 public:
 	explicit OBSBasic(QWidget *parent = 0);
 	virtual ~OBSBasic();