diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt
index d0c80793549f0495b78c29dc12f46cfad6ea7c7b..b66e98f2df915bf7f608bae815284ff10a6e9504 100644
--- a/libobs/CMakeLists.txt
+++ b/libobs/CMakeLists.txt
@@ -222,6 +222,7 @@ set(libobs_libobs_HEADERS
 	obs-ui.h
 	obs-properties.h
 	obs-data.h
+	obs-interaction.h
 	obs-module.h
 	obs-scene.h
 	obs-source.h
diff --git a/libobs/obs-interaction.h b/libobs/obs-interaction.h
new file mode 100644
index 0000000000000000000000000000000000000000..d213df2167bed7e21c651eae1fe1acf40e12ea01
--- /dev/null
+++ b/libobs/obs-interaction.h
@@ -0,0 +1,56 @@
+/******************************************************************************
+ Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
+
+ 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 "util/c99defs.h"
+
+enum obs_interaction_flags {
+	INTERACT_NONE          = 0,
+	INTERACT_CAPS_KEY      = 1,
+	INTERACT_SHIFT_KEY     = 1 << 1,
+	INTERACT_CONTROL_KEY   = 1 << 2,
+	INTERACT_ALT_KEY       = 1 << 3,
+	INTERACT_MOUSE_LEFT    = 1 << 4,
+	INTERACT_MOUSE_MIDDLE  = 1 << 5,
+	INTERACT_MOUSE_RIGHT   = 1 << 6,
+	INTERACT_COMMAND_KEY   = 1 << 7,
+	INTERACT_NUMLOCK_KEY   = 1 << 8,
+	INTERACT_IS_KEY_PAD    = 1 << 9,
+	INTERACT_IS_LEFT       = 1 << 10,
+	INTERACT_IS_RIGHT      = 1 << 11
+};
+
+enum obs_mouse_button_type {
+	MOUSE_LEFT,
+	MOUSE_MIDDLE,
+	MOUSE_RIGHT
+};
+
+struct obs_mouse_event {
+	uint32_t            modifiers;
+	int32_t             x;
+	int32_t             y;
+};
+
+struct obs_key_event {
+	uint32_t            modifiers;
+	char                *text;
+	uint32_t            native_modifiers;
+	uint32_t            native_scancode;
+	uint32_t            native_vkey;
+};
diff --git a/libobs/obs-source.c b/libobs/obs-source.c
index eb1f61d423f12fb67d45b03515d22629a0f643a1..6b0015b8773a72976f99081664ea5bbf3ff10322 100644
--- a/libobs/obs-source.c
+++ b/libobs/obs-source.c
@@ -389,6 +389,76 @@ void obs_source_update(obs_source_t source, obs_data_t settings)
 	}
 }
 
+void obs_source_send_mouse_click(obs_source_t source,
+		const struct obs_mouse_event *event,
+		int32_t type, bool mouse_up,
+		uint32_t click_count)
+{
+	if (!source)
+		return;
+
+	if (source->info.output_flags & OBS_SOURCE_INTERACTION) {
+		if (source->info.mouse_click) {
+			source->info.mouse_click(source->context.data,
+					event, type, mouse_up, click_count);
+		}
+	}
+}
+
+void obs_source_send_mouse_move(obs_source_t source,
+		const struct obs_mouse_event *event, bool mouse_leave)
+{
+	if (!source)
+		return;
+
+	if (source->info.output_flags & OBS_SOURCE_INTERACTION) {
+		if (source->info.mouse_move) {
+			source->info.mouse_move(source->context.data,
+					event, mouse_leave);
+		}
+	}
+}
+
+void obs_source_send_mouse_wheel(obs_source_t source,
+		const struct obs_mouse_event *event, int x_delta, int y_delta)
+{
+	if (!source)
+		return;
+
+	if (source->info.output_flags & OBS_SOURCE_INTERACTION) {
+		if (source->info.mouse_wheel) {
+			source->info.mouse_wheel(source->context.data,
+					event, x_delta, y_delta);
+		}
+	}
+}
+
+void obs_source_send_focus(obs_source_t source, bool focus)
+{
+	if (!source)
+		return;
+
+	if (source->info.output_flags & OBS_SOURCE_INTERACTION) {
+		if (source->info.focus) {
+			source->info.focus(source->context.data, focus);
+		}
+	}
+}
+
+void obs_source_send_key_click(obs_source_t source,
+		const struct obs_key_event *event, bool key_up)
+{
+	if (!source)
+		return;
+
+	if (source->info.output_flags & OBS_SOURCE_INTERACTION) {
+		if (source->info.key_click) {
+			source->info.key_click(source->context.data, event,
+					key_up);
+		}
+	}
+}
+
 static void activate_source(obs_source_t source)
 {
 	if (source->context.data && source->info.activate)
diff --git a/libobs/obs-source.h b/libobs/obs-source.h
index 20636a7b0c1b2cf62fc65fa97020429c3fc21ebe..9bdccd37571b316c295d2387297b745d95f76351 100644
--- a/libobs/obs-source.h
+++ b/libobs/obs-source.h
@@ -88,6 +88,14 @@ enum obs_source_type {
  */
 #define OBS_SOURCE_COLOR_MATRIX (1<<4)
 
+/**
+ * Source supports interaction.
+ * 
+ * When this is used, the source will receive interaction events
+ * if they provide the necessary callbacks in the source definition structure.
+ */
+#define OBS_SOURCE_INTERACTION (1<<5)
+
 /** @} */
 
 typedef void (*obs_source_enum_proc_t)(obs_source_t parent, obs_source_t child,
@@ -283,6 +291,60 @@ struct obs_source_info {
 	 * @param  settings  Settings
 	 */
 	void (*load)(void *data, obs_data_t settings);
+
+	/**
+	 * Called when interacting with a source and a mouse-down or mouse-up
+	 * occurs.
+	 * 
+	 * @param data         Source data
+	 * @param event        Mouse event properties
+	 * @param type         Mouse button pushed
+	 * @param mouse_up     Mouse event type (true if mouse-up)
+	 * @param click_count  Mouse click count (1 for single click, etc.)
+	 */
+	void (*mouse_click)(void *data,
+			const struct obs_mouse_event *event,
+			int32_t type, bool mouse_up, uint32_t click_count);
+	/**
+	 * Called when interacting with a source and a mouse-move occurs.
+	 *
+	 * @param data         Source data
+	 * @param event        Mouse event properties
+	 * @param mouse_leave  Mouse leave state (true if mouse left source)
+	 */
+	void (*mouse_move)(void *data,
+			const struct obs_mouse_event *event, bool mouse_leave);
+
+	/**
+	 * Called when interacting with a source and a mouse-wheel occurs.
+	 *
+	 * @param data         Source data
+	 * @param event        Mouse event properties
+	 * @param x_delta      Movement delta in the horizontal direction
+	 * @param y_delta      Movement delta in the vertical direction
+	 */
+	void (*mouse_wheel)(void *data,
+			const struct obs_mouse_event *event, int x_delta,
+			int y_delta);
+	/**
+	 * Called when interacting with a source and gain focus/lost focus event
+	 * occurs.
+	 *
+	 * @param data         Source data
+	 * @param focus        Focus state (true if focus gained)
+	 */
+	void (*focus)(void *data, bool focus);
+
+	/**
+	 * Called when interacting with a source and a key-up or key-down
+	 * occurs.
+	 *
+	 * @param data         Source data
+	 * @param event        Key event properties
+	 * @param focus        Key event type (true if mouse-up)
+	 */
+	void (*key_click)(void *data, const struct obs_key_event *event,
+			bool key_up);
 };
 
 EXPORT void obs_register_source_s(const struct obs_source_info *info,
diff --git a/libobs/obs.h b/libobs/obs.h
index e54f5a059405e4a622d8bb5c20b8586d84ce5390..8bbe8b2e5cb60f63c2fc40027856965e3035a7af 100644
--- a/libobs/obs.h
+++ b/libobs/obs.h
@@ -33,6 +33,7 @@
 #include "obs-data.h"
 #include "obs-ui.h"
 #include "obs-properties.h"
+#include "obs-interaction.h"
 
 struct matrix4;
 
@@ -751,6 +752,27 @@ EXPORT void obs_source_add_child(obs_source_t parent, obs_source_t child);
  */
 EXPORT void obs_source_remove_child(obs_source_t parent, obs_source_t child);
 
+/** Sends a mouse down/up event to a source */
+EXPORT void obs_source_send_mouse_click(obs_source_t source,
+		const struct obs_mouse_event *event,
+		int32_t type, bool mouse_up,
+		uint32_t click_count);
+
+/** Sends a mouse move event to a source. */
+EXPORT void obs_source_send_mouse_move(obs_source_t source,
+		const struct obs_mouse_event *event, bool mouse_leave);
+
+/** Sends a mouse wheel event to a source */
+EXPORT void obs_source_send_mouse_wheel(obs_source_t source,
+		const struct obs_mouse_event *event, int x_delta, int y_delta);
+
+/** Sends a got-focus or lost-focus event to a source */
+EXPORT void obs_source_send_focus(obs_source_t source, bool focus);
+
+/** Sends a key up/down event to a source */
+EXPORT void obs_source_send_key_click(obs_source_t source,
+		const struct obs_key_event *event, bool key_up);
+
 /** Begins transition frame.  Sets all transitioning volume values to 0.0f. */
 EXPORT void obs_transition_begin_frame(obs_source_t transition);
 
diff --git a/obs/CMakeLists.txt b/obs/CMakeLists.txt
index 84a14a844ed3e367e1cea652f3458f1c72213597..9cac34198e82bf79f882b3a0eaafd6b49c618142 100644
--- a/obs/CMakeLists.txt
+++ b/obs/CMakeLists.txt
@@ -74,6 +74,7 @@ set(obs_SOURCES
 	obs-app.cpp
 	window-basic-main.cpp
 	window-basic-settings.cpp
+	window-basic-interaction.cpp
 	window-basic-properties.cpp
 	window-basic-source-select.cpp
 	window-license-agreement.cpp
@@ -92,6 +93,7 @@ set(obs_HEADERS
 	window-main.hpp
 	window-basic-main.hpp
 	window-basic-settings.hpp
+	window-basic-interaction.hpp
 	window-basic-properties.hpp
 	window-basic-source-select.hpp
 	window-license-agreement.hpp
@@ -114,6 +116,7 @@ set(obs_UI
 	forms/OBSBasicTransform.ui
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSourceSelect.ui
+	forms/OBSBasicInteraction.ui
 	forms/OBSBasicProperties.ui)
 
 set(obs_QRC
diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini
index d6b620e645057f423d14cbd0d345e56c1eea1aac..3377dbf9842d1abc8ec18997112d8510a45e5b2f 100644
--- a/obs/data/locale/en-US.ini
+++ b/obs/data/locale/en-US.ini
@@ -95,6 +95,9 @@ Basic.PropertiesWindow.AutoSelectFormat="%1 (unsupported; autoselect: %2)"
 Basic.PropertiesWindow.SelectColor="Select color"
 Basic.PropertiesWindow.SelectFont="Select font"
 
+# interaction window
+Basic.InteractionWindow="Interacting with '%1'"
+
 # status bar
 Basic.StatusBar.Reconnecting="Disconnected, reconnecting (attempt %1)"
 Basic.StatusBar.ReconnectSuccessful="Reconnection successful"
diff --git a/obs/forms/OBSBasic.ui b/obs/forms/OBSBasic.ui
index bb5434d56dec20e300cc75c81564db34d647855f..19a529d71641372babc7a2b9f1eb16241ce1646b 100644
--- a/obs/forms/OBSBasic.ui
+++ b/obs/forms/OBSBasic.ui
@@ -791,6 +791,11 @@
     <string>Basic.MainMenu.Help.CheckForUpdates</string>
    </property>
   </action>
+  <action name="actionInteract">
+   <property name="text">
+    <string>Interact</string>
+   </property>
+  </action>
  </widget>
  <customwidgets>
   <customwidget>
diff --git a/obs/forms/OBSBasicInteraction.ui b/obs/forms/OBSBasicInteraction.ui
new file mode 100644
index 0000000000000000000000000000000000000000..65d313bc93f5ba10ad9621dd2123daea759a9c30
--- /dev/null
+++ b/obs/forms/OBSBasicInteraction.ui
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSBasicInteraction</class>
+ <widget class="QDialog" name="OBSBasicInteraction">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>664</width>
+    <height>562</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <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>20</width>
+       <height>20</height>
+      </size>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>OBSQTDisplay</class>
+   <extends>QWidget</extends>
+   <header>qt-display.hpp</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/obs/window-basic-interaction.cpp b/obs/window-basic-interaction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..15460b0cc7d06cc0e8cd406239bde17e000e4396
--- /dev/null
+++ b/obs/window-basic-interaction.cpp
@@ -0,0 +1,391 @@
+/******************************************************************************
+    Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
+
+    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 "obs-app.hpp"
+#include "window-basic-interaction.hpp"
+#include "window-basic-main.hpp"
+#include "qt-wrappers.hpp"
+#include "display-helpers.hpp"
+
+#include <QCloseEvent>
+#include <QScreen>
+#include <QWindow>
+
+using namespace std;
+
+OBSBasicInteraction::OBSBasicInteraction(QWidget *parent, OBSSource source_)
+	: QDialog       (parent),
+	  main          (qobject_cast<OBSBasic*>(parent)),
+	  resizeTimer   (0),
+	  ui            (new Ui::OBSBasicInteraction),
+	  source        (source_),
+	  removedSignal (obs_source_get_signal_handler(source), "remove",
+	                 OBSBasicInteraction::SourceRemoved, this)
+{
+	int cx = (int)config_get_int(App()->GlobalConfig(), "InteractionWindow",
+			"cx");
+	int cy = (int)config_get_int(App()->GlobalConfig(), "InteractionWindow",
+			"cy");
+
+	ui->setupUi(this);
+
+	ui->preview->setMouseTracking(true);
+	ui->preview->setFocusPolicy(Qt::StrongFocus);
+	ui->preview->installEventFilter(BuildEventFilter());
+
+	if (cx > 400 && cy > 400)
+		resize(cx, cy);
+
+	OBSData settings = obs_source_get_settings(source);
+	obs_data_release(settings);
+
+	connect(windowHandle(), &QWindow::screenChanged, [this]() {
+		if (resizeTimer)
+			killTimer(resizeTimer);
+		resizeTimer = startTimer(100);
+	});
+
+	const char *name = obs_source_get_name(source);
+	setWindowTitle(QTStr("Basic.InteractionWindow").arg(QT_UTF8(name)));
+}
+
+OBSEventFilter *OBSBasicInteraction::BuildEventFilter()
+{
+	return new OBSEventFilter(
+			[this](QObject *obj, QEvent *event)
+	{
+		UNUSED_PARAMETER(obj);
+
+		switch(event->type()) {
+		case QEvent::MouseButtonPress:
+		case QEvent::MouseButtonRelease:
+		case QEvent::MouseButtonDblClick:
+			return this->HandleMouseClickEvent(
+					static_cast<QMouseEvent *>(event));
+		case QEvent::MouseMove:
+		case QEvent::Enter:
+		case QEvent::Leave:
+			return this->HandleMouseMoveEvent(
+					static_cast<QMouseEvent *>(event));
+
+		case QEvent::Wheel:
+			return this->HandleMouseWheelEvent(
+					static_cast<QWheelEvent *>(event));
+		case QEvent::FocusIn:
+		case QEvent::FocusOut:
+			return this->HandleFocusEvent(
+					static_cast<QFocusEvent *>(event));
+		case QEvent::KeyPress:
+		case QEvent::KeyRelease:
+			return this->HandleKeyEvent(
+					static_cast<QKeyEvent *>(event));
+		default:
+			return false;
+		}
+	});
+}
+
+void OBSBasicInteraction::SourceRemoved(void *data, calldata_t params)
+{
+	QMetaObject::invokeMethod(static_cast<OBSBasicInteraction*>(data),
+			"close");
+
+	UNUSED_PARAMETER(params);
+}
+
+void OBSBasicInteraction::DrawPreview(void *data, uint32_t cx, uint32_t cy)
+{
+	OBSBasicInteraction *window = static_cast<OBSBasicInteraction*>(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();
+}
+
+void OBSBasicInteraction::OnInteractionResized()
+{
+	if (resizeTimer)
+		killTimer(resizeTimer);
+	resizeTimer = startTimer(100);
+}
+
+void OBSBasicInteraction::resizeEvent(QResizeEvent *event)
+{
+	if (isVisible()) {
+		if (resizeTimer)
+			killTimer(resizeTimer);
+		resizeTimer = startTimer(100);
+	}
+
+	UNUSED_PARAMETER(event);
+}
+
+void OBSBasicInteraction::timerEvent(QTimerEvent *event)
+{
+	if (event->timerId() == resizeTimer) {
+		killTimer(resizeTimer);
+		resizeTimer = 0;
+
+		QSize size = GetPixelSize(ui->preview);
+		obs_display_resize(display, size.width(), size.height());
+	}
+}
+
+void OBSBasicInteraction::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,
+			OBSBasicInteraction::DrawPreview, this);
+	display = nullptr;
+
+	config_set_int(App()->GlobalConfig(), "InteractionWindow", "cx",
+			width());
+	config_set_int(App()->GlobalConfig(), "InteractionWindow", "cy",
+			height());
+}
+
+static int TranslateQtKeyboardEventModifiers(QInputEvent *event, bool mouseEvent) {
+	int obsModifiers = INTERACT_NONE;
+
+	if (event->modifiers().testFlag(Qt::ShiftModifier))
+		obsModifiers |= INTERACT_SHIFT_KEY;
+	if (event->modifiers().testFlag(Qt::AltModifier))
+		obsModifiers |= INTERACT_ALT_KEY;
+#ifdef APPLE
+	// Mac: Meta = Control, Control = Command
+	if (event->modifiers().testFlag(Qt::ControlModifier))
+		obsModifiers |= INTERACT_COMMAND_KEY;
+	if (event->modifiers().testFlag(Qt::MetaModifier))
+		obsModifiers |= INTERACT_CONTROL_KEY;
+#else
+	// Handle windows key? Can a browser even trap that key?
+	if (event->modifiers().testFlag(Qt::ControlModifier))
+		obsModifiers |= INTERACT_CONTROL_KEY;
+#endif
+
+	if (!mouseEvent) {
+		if (event->modifiers().testFlag(Qt::KeypadModifier))
+			obsModifiers |= INTERACT_IS_KEY_PAD;
+	}
+
+	return obsModifiers;
+}
+
+static int TranslateQtMouseEventModifiers(
+	QMouseEvent *event)
+{
+	int modifiers = TranslateQtKeyboardEventModifiers(event, true);
+
+	if (event->buttons().testFlag(Qt::LeftButton))
+		modifiers |= INTERACT_MOUSE_LEFT;
+	if (event->buttons().testFlag(Qt::MiddleButton))
+		modifiers |= INTERACT_MOUSE_MIDDLE;
+	if (event->buttons().testFlag(Qt::RightButton))
+		modifiers |= INTERACT_MOUSE_RIGHT;
+
+	return modifiers;
+}
+
+bool OBSBasicInteraction::GetSourceRelativeXY(
+      int mouseX, int mouseY, int &relX, int &relY)
+{
+	QSize size = GetPixelSize(ui->preview);
+
+	uint32_t sourceCX = max(obs_source_get_width(source), 1u);
+	uint32_t sourceCY = max(obs_source_get_height(source), 1u);
+
+	int   x, y;
+	float scale;
+
+	GetScaleAndCenterPos(sourceCX, sourceCY, size.width(), size.height(),
+			x, y, scale);
+
+	if (x > 0) {
+		relX = int(float(mouseX - x) / scale);
+		relY = int(float(mouseY / scale));
+	} else {
+		relX = int(float(mouseX / scale));
+		relY = int(float(mouseY - y) / scale);
+	}
+
+	// Confirm mouse is inside the source
+	if (relX < 0 || relX > int(sourceCX))
+		return false;
+	if (relY < 0 || relY > int(sourceCY))
+		return false;
+
+	return true;
+}
+
+bool OBSBasicInteraction::HandleMouseClickEvent(
+	QMouseEvent *event)
+{
+	bool mouseUp = event->type() == QEvent::MouseButtonRelease;
+	int clickCount = 1;
+	if (event->type() == QEvent::MouseButtonDblClick)
+		clickCount = 2;
+
+	struct obs_mouse_event mouseEvent = {};
+
+	mouseEvent.modifiers = TranslateQtMouseEventModifiers(event);
+
+	int32_t button = 0;
+
+	switch (event->button()) {
+	case Qt::LeftButton:
+		button = MOUSE_LEFT;
+		break;
+	case Qt::MiddleButton:
+		button = MOUSE_MIDDLE;
+		break;
+	case Qt::RightButton:
+		button = MOUSE_RIGHT;
+		break;
+	default:
+		blog(LOG_WARNING, "unknown button type %d",
+				event->button());
+		return false;
+	}
+
+	// Why doesn't this work?
+	//if (event->flags().testFlag(Qt::MouseEventCreatedDoubleClick))
+	//	clickCount = 2;
+
+	bool insideSource = GetSourceRelativeXY(event->x(), event->y(),
+			mouseEvent.x, mouseEvent.y);
+
+	if (mouseUp || insideSource)
+		obs_source_send_mouse_click(source, &mouseEvent, button,
+				mouseUp, clickCount);
+
+	return true;
+}
+
+bool OBSBasicInteraction::HandleMouseMoveEvent(QMouseEvent *event)
+{
+	struct obs_mouse_event mouseEvent = {};
+
+	bool mouseLeave = event->type() == QEvent::Leave;
+
+	if (!mouseLeave) {
+		mouseEvent.modifiers = TranslateQtMouseEventModifiers(event);
+		mouseLeave = !GetSourceRelativeXY(event->x(), event->y(),
+				mouseEvent.x, mouseEvent.y);
+	}
+
+	obs_source_send_mouse_move(source, &mouseEvent, mouseLeave);
+
+	return true;
+}
+
+bool OBSBasicInteraction::HandleMouseWheelEvent(QWheelEvent *event)
+{
+	struct obs_mouse_event mouseEvent = {};
+
+	mouseEvent.modifiers = TranslateQtKeyboardEventModifiers(event, true);
+
+	int xDelta = 0;
+	int yDelta = 0;
+
+	if (!event->pixelDelta().isNull()) {
+		if (event->orientation() == Qt::Horizontal)
+			xDelta = event->pixelDelta().x();
+		else
+			yDelta = event->pixelDelta().y();
+	} else {
+		if (event->orientation() == Qt::Horizontal)
+			xDelta = event->delta();
+		else
+			yDelta = event->delta();
+	}
+
+	obs_source_send_mouse_wheel(source, &mouseEvent, xDelta, yDelta);
+
+	return true;
+}
+
+bool OBSBasicInteraction::HandleFocusEvent(QFocusEvent *event)
+{
+	bool focus = event->type() == QEvent::FocusIn;
+
+	obs_source_send_focus(source, focus);
+
+	return true;
+}
+
+bool OBSBasicInteraction::HandleKeyEvent(QKeyEvent *event)
+{
+	struct obs_key_event keyEvent;
+
+	QByteArray text = event->text().toUtf8();
+	keyEvent.modifiers = TranslateQtKeyboardEventModifiers(event, false);
+	keyEvent.text = text.data();
+	keyEvent.native_modifiers = event->nativeModifiers();
+	keyEvent.native_scancode = event->nativeScanCode();
+	keyEvent.native_vkey = event->nativeVirtualKey();
+
+	bool keyUp = event->type() == QEvent::KeyRelease;
+
+	obs_source_send_key_click(source, &keyEvent, keyUp);
+
+	return true;
+}
+
+void OBSBasicInteraction::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,
+				OBSBasicInteraction::DrawPreview, this);
+}
diff --git a/obs/window-basic-interaction.hpp b/obs/window-basic-interaction.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e2633b49b9ea290e90dd4f4b42c0d342a669f70d
--- /dev/null
+++ b/obs/window-basic-interaction.hpp
@@ -0,0 +1,90 @@
+/******************************************************************************
+    Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
+
+    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 <memory>
+#include <functional>
+
+#include <obs.hpp>
+
+#include "properties-view.hpp"
+
+class OBSBasic;
+
+#include "ui_OBSBasicInteraction.h"
+
+class OBSEventFilter;
+
+class OBSBasicInteraction : public QDialog {
+	Q_OBJECT
+
+private:
+	OBSBasic   *main;
+	int        resizeTimer;
+
+	std::unique_ptr<Ui::OBSBasicInteraction> ui;
+	OBSSource  source;
+	OBSDisplay display;
+	OBSSignal  removedSignal;
+
+	static void SourceRemoved(void *data, calldata_t params);
+	static void DrawPreview(void *data, uint32_t cx, uint32_t cy);
+
+	bool GetSourceRelativeXY(int mouseX, int mouseY, int &x, int &y);
+
+	bool HandleMouseClickEvent(QMouseEvent *event);
+	bool HandleMouseMoveEvent(QMouseEvent *event);
+	bool HandleMouseWheelEvent(QWheelEvent *event);
+	bool HandleFocusEvent(QFocusEvent *event);
+	bool HandleKeyEvent(QKeyEvent *event);
+
+	OBSEventFilter *BuildEventFilter();
+
+private slots:
+	void OnInteractionResized();
+
+public:
+	OBSBasicInteraction(QWidget *parent, OBSSource source_);
+
+	void Init();
+
+protected:
+	virtual void resizeEvent(QResizeEvent *event) override;
+	virtual void timerEvent(QTimerEvent *event) override;
+	virtual void closeEvent(QCloseEvent *event) override;
+};
+
+typedef std::function<bool(QObject *, QEvent *)> EventFilterFunc;
+
+class OBSEventFilter : public QObject
+{
+	Q_OBJECT
+public:
+	OBSEventFilter(EventFilterFunc filter_)
+		: filter(filter_)
+	{}
+
+protected:
+	bool eventFilter(QObject *obj, QEvent *event)
+	{
+		return filter(obj, event);
+	}
+private:
+	EventFilterFunc filter;
+};
diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp
index bf516015b6270ca680a0a8c367cec5540aee3d4a..705e3badf1142a0357047d9fcc4c1c663946c4c8 100644
--- a/obs/window-basic-main.cpp
+++ b/obs/window-basic-main.cpp
@@ -596,6 +596,9 @@ OBSBasic::~OBSBasic()
 	delete cpuUsageTimer;
 	os_cpu_usage_info_destroy(cpuUsageInfo);
 
+	if (interaction)
+		delete interaction;
+
 	if (properties)
 		delete properties;
 
@@ -624,12 +627,16 @@ OBSScene OBSBasic::GetCurrentScene()
 	return item ? item->data(Qt::UserRole).value<OBSScene>() : nullptr;
 }
 
-OBSSceneItem OBSBasic::GetCurrentSceneItem()
+OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item)
 {
-	QListWidgetItem *item = ui->sources->currentItem();
 	return item ? item->data(Qt::UserRole).value<OBSSceneItem>() : nullptr;
 }
 
+OBSSceneItem OBSBasic::GetCurrentSceneItem()
+{
+	return GetSceneItem(ui->sources->currentItem());
+}
+
 void OBSBasic::UpdateSources(OBSScene scene)
 {
 	ui->sources->clear();
@@ -663,6 +670,16 @@ void OBSBasic::InsertSceneItem(obs_sceneitem_t item)
 		CreatePropertiesWindow(source);
 }
 
+void OBSBasic::CreateInteractionWindow(obs_source_t source)
+{
+	if (interaction)
+		interaction->close();
+
+	interaction = new OBSBasicInteraction(this, source);
+	interaction->Init();
+	interaction->setAttribute(Qt::WA_DeleteOnClose, true);
+}
+
 void OBSBasic::CreatePropertiesWindow(obs_source_t source)
 {
 	if (properties)
@@ -1606,6 +1623,10 @@ void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos)
 		if (addSourceMenu)
 			popup.addSeparator();
 
+		OBSSceneItem sceneItem = GetSceneItem(item);
+		obs_source_t source = obs_sceneitem_get_source(sceneItem);
+		QAction *action;
+
 		popup.addAction(QTStr("Rename"), this,
 				SLOT(EditSceneItemName()));
 		popup.addAction(QTStr("Remove"), this,
@@ -1615,6 +1636,13 @@ void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos)
 		popup.addMenu(ui->orderMenu);
 		popup.addMenu(ui->transformMenu);
 		popup.addSeparator();
+
+		action = popup.addAction(QTStr("Interact"), this,
+				SLOT(on_actionInteract_triggered()));
+
+		action->setEnabled(obs_source_get_output_flags(source) &
+				OBS_SOURCE_INTERACTION);
+
 		popup.addAction(QTStr("Properties"), this,
 				SLOT(on_actionSourceProperties_triggered()));
 	}
@@ -1699,6 +1727,15 @@ void OBSBasic::on_actionRemoveSource_triggered()
 		obs_sceneitem_remove(item);
 }
 
+void OBSBasic::on_actionInteract_triggered()
+{
+	OBSSceneItem item = GetCurrentSceneItem();
+	OBSSource source = obs_sceneitem_get_source(item);
+
+	if (source)
+		CreateInteractionWindow(source);
+}
+
 void OBSBasic::on_actionSourceProperties_triggered()
 {
 	OBSSceneItem item = GetCurrentSceneItem();
diff --git a/obs/window-basic-main.hpp b/obs/window-basic-main.hpp
index 516c179d3c9e109357a86ef0b57cd630a5b25785..72795183e5d49d659523472c951198b255a24d2f 100644
--- a/obs/window-basic-main.hpp
+++ b/obs/window-basic-main.hpp
@@ -25,6 +25,7 @@
 #include <vector>
 #include <memory>
 #include "window-main.hpp"
+#include "window-basic-interaction.hpp"
 #include "window-basic-properties.hpp"
 #include "window-basic-transform.hpp"
 
@@ -57,6 +58,7 @@ private:
 
 	bool loaded = false;
 
+	QPointer<OBSBasicInteraction> interaction;
 	QPointer<OBSBasicProperties> properties;
 	QPointer<OBSBasicTransform> transformWindow;
 
@@ -120,6 +122,7 @@ private:
 
 	void          InitPrimitives();
 
+	OBSSceneItem  GetSceneItem(QListWidgetItem *item);
 	OBSSceneItem  GetCurrentSceneItem();
 
 	bool          QueryRemoveSource(obs_source_t source);
@@ -140,6 +143,7 @@ private:
 	void TempStreamOutput(const char *url, const char *key,
 			int vBitrate, int aBitrate);
 
+	void CreateInteractionWindow(obs_source_t source);
 	void CreatePropertiesWindow(obs_source_t source);
 
 public slots:
@@ -259,6 +263,7 @@ private slots:
 	void on_sources_customContextMenuRequested(const QPoint &pos);
 	void on_actionAddSource_triggered();
 	void on_actionRemoveSource_triggered();
+	void on_actionInteract_triggered();
 	void on_actionSourceProperties_triggered();
 	void on_actionSourceUp_triggered();
 	void on_actionSourceDown_triggered();