Ver código fonte

UI: Add Hotkey filter search and duplicate detection

furious 4 anos atrás
pai
commit
0c1524b53c

+ 2 - 0
UI/data/locale/en-US.ini

@@ -1051,6 +1051,8 @@ Basic.AdvAudio.AudioTracks="Tracks"
 Basic.Settings.Hotkeys="Hotkeys"
 Basic.Settings.Hotkeys.Pair="Key combinations shared with '%1' act as toggles"
 Basic.Settings.Hotkeys.Filter="Filter"
+Basic.Settings.Hotkeys.FilterByHotkey="Filter by Hotkey"
+Basic.Settings.Hotkeys.DuplicateWarning="This hotkey is shared by one or more other actions, click to show conflicts"
 
 # basic mode hotkeys
 Basic.Hotkeys.SelectScene="Switch to scene"

+ 1 - 0
UI/forms/images/warning.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#eed202" fill-rule="evenodd" d="m12.89267,5.4995c-0.183,-0.31 -0.52,-0.5 -0.887,-0.5s-0.703,0.19 -0.886,0.5l-6.982,11.999a0.98,0.98 0 0 0 0,1.001c0.193,0.31 0.53,0.501 0.886,0.501l13.964,0c0.367,0 0.704,-0.19 0.877,-0.5a1.03,1.03 0 0 0 0.01,-1.002l-6.982,-11.999zm0.133,11.497l-2.039,0l0,-2.003l2.039,0l0,2.003zm0,-3.004l-2.039,0l0,-4.006l2.039,0l0,4.006z"/></svg>

+ 1 - 0
UI/forms/obs.qrc

@@ -27,6 +27,7 @@
     <file>images/trash.svg</file>
     <file>images/revert.svg</file>
     <file>images/alert.svg</file>
+    <file>images/warning.svg</file>
     <file>images/sources/brush.svg</file>
     <file>images/sources/camera.svg</file>
     <file>images/sources/gamepad.svg</file>

+ 28 - 14
UI/hotkey-edit.cpp

@@ -15,27 +15,17 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
 
+#include "window-basic-settings.hpp"
 #include "hotkey-edit.hpp"
 
 #include <util/dstr.hpp>
 #include <QPointer>
 #include <QStyle>
+#include <QAction>
 
 #include "obs-app.hpp"
 #include "qt-wrappers.hpp"
 
-static inline bool operator!=(const obs_key_combination_t &c1,
-			      const obs_key_combination_t &c2)
-{
-	return c1.modifiers != c2.modifiers || c1.key != c2.key;
-}
-
-static inline bool operator==(const obs_key_combination_t &c1,
-			      const obs_key_combination_t &c2)
-{
-	return !(c1 != c2);
-}
-
 void OBSHotkeyEdit::keyPressEvent(QKeyEvent *event)
 {
 	if (event->isAutoRepeat())
@@ -183,6 +173,14 @@ void OBSHotkeyEdit::ClearKey()
 	RenderKey();
 }
 
+void OBSHotkeyEdit::UpdateDuplicationState()
+{
+	if (dupeIcon->isVisible() != hasDuplicate) {
+		dupeIcon->setVisible(hasDuplicate);
+		update();
+	}
+}
+
 void OBSHotkeyEdit::InitSignalHandler()
 {
 	layoutChanged = {
@@ -194,6 +192,16 @@ void OBSHotkeyEdit::InitSignalHandler()
 		this};
 }
 
+void OBSHotkeyEdit::CreateDupeIcon()
+{
+	dupeIcon = this->addAction(settings->GetHotkeyConflictIcon(),
+				   ActionPosition::TrailingPosition);
+	dupeIcon->setToolTip(QTStr("Basic.Settings.Hotkeys.DuplicateWarning"));
+	QObject::connect(dupeIcon, &QAction::triggered,
+			 [=] { emit SearchKey(key); });
+	dupeIcon->setVisible(false);
+}
+
 void OBSHotkeyEdit::ReloadKeyLayout()
 {
 	RenderKey();
@@ -266,7 +274,7 @@ void OBSHotkeyWidget::Save(std::vector<obs_key_combination_t> &combinations)
 
 void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx)
 {
-	auto edit = new OBSHotkeyEdit(combo);
+	auto edit = new OBSHotkeyEdit(combo, settings);
 	edit->setToolTip(toolTip);
 
 	auto revert = new QPushButton;
@@ -347,6 +355,10 @@ void OBSHotkeyWidget::AddEdit(obs_key_combination combo, int idx)
 
 	QObject::connect(edit, &OBSHotkeyEdit::KeyChanged,
 			 [&](obs_key_combination) { emit KeyChanged(); });
+	QObject::connect(edit, &OBSHotkeyEdit::SearchKey,
+			 [=](obs_key_combination combo) {
+				 emit SearchKey(combo);
+			 });
 }
 
 void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal)
@@ -354,7 +366,6 @@ void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal)
 	auto &edit = *(begin(edits) + idx);
 	if (!obs_key_combination_is_empty(edit->original) && signal) {
 		changed = true;
-		emit KeyChanged();
 	}
 
 	revertButtons.erase(begin(revertButtons) + idx);
@@ -371,6 +382,8 @@ void OBSHotkeyWidget::RemoveEdit(size_t idx, bool signal)
 
 	if (removeButtons.size() == 1)
 		removeButtons.front()->setEnabled(false);
+
+	emit KeyChanged();
 }
 
 void OBSHotkeyWidget::BindingsChanged(void *data, calldata_t *param)
@@ -458,6 +471,7 @@ void OBSHotkeyLabel::enterEvent(QEnterEvent *event)
 void OBSHotkeyLabel::enterEvent(QEvent *event)
 #endif
 {
+
 	if (!pairPartner)
 		return;
 

+ 33 - 8
UI/hotkey-edit.hpp

@@ -27,6 +27,19 @@
 
 #include <obs.hpp>
 
+static inline bool operator!=(const obs_key_combination_t &c1,
+			      const obs_key_combination_t &c2)
+{
+	return c1.modifiers != c2.modifiers || c1.key != c2.key;
+}
+
+static inline bool operator==(const obs_key_combination_t &c1,
+			      const obs_key_combination_t &c2)
+{
+	return !(c1 != c2);
+}
+
+class OBSBasicSettings;
 class OBSHotkeyWidget;
 
 class OBSHotkeyLabel : public QLabel {
@@ -49,8 +62,9 @@ class OBSHotkeyEdit : public QLineEdit {
 	Q_OBJECT;
 
 public:
-	OBSHotkeyEdit(obs_key_combination_t original, QWidget *parent = nullptr)
-		: QLineEdit(parent), original(original)
+	OBSHotkeyEdit(obs_key_combination_t original,
+		      OBSBasicSettings *settings)
+		: QLineEdit(nullptr), original(original), settings(settings)
 	{
 #ifdef __APPLE__
 		// disable the input cursor on OSX, focus should be clear
@@ -60,17 +74,24 @@ public:
 		setAttribute(Qt::WA_InputMethodEnabled, false);
 		setAttribute(Qt::WA_MacShowFocusRect, true);
 		InitSignalHandler();
+		CreateDupeIcon();
 		ResetKey();
 	}
 
 	obs_key_combination_t original;
 	obs_key_combination_t key;
+	OBSBasicSettings *settings;
 	bool changed = false;
 
+	void UpdateDuplicationState();
+	bool hasDuplicate = false;
+
 protected:
 	OBSSignal layoutChanged;
+	QAction *dupeIcon;
 
 	void InitSignalHandler();
+	void CreateDupeIcon();
 
 	void keyPressEvent(QKeyEvent *event) override;
 #ifdef __APPLE__
@@ -78,16 +99,17 @@ protected:
 #endif
 	void mousePressEvent(QMouseEvent *event) override;
 
-	void HandleNewKey(obs_key_combination_t new_key);
 	void RenderKey();
 
 public slots:
+	void HandleNewKey(obs_key_combination_t new_key);
 	void ReloadKeyLayout();
 	void ResetKey();
 	void ClearKey();
 
 signals:
 	void KeyChanged(obs_key_combination_t);
+	void SearchKey(obs_key_combination_t);
 };
 
 class OBSHotkeyWidget : public QWidget {
@@ -95,14 +117,15 @@ class OBSHotkeyWidget : public QWidget {
 
 public:
 	OBSHotkeyWidget(obs_hotkey_id id, std::string name,
-			const std::vector<obs_key_combination_t> &combos = {},
-			QWidget *parent = nullptr)
-		: QWidget(parent),
+			OBSBasicSettings *settings,
+			const std::vector<obs_key_combination_t> &combos = {})
+		: QWidget(nullptr),
 		  id(id),
 		  name(name),
 		  bindingsChanged(obs_get_signal_handler(),
 				  "hotkey_bindings_changed",
-				  &OBSHotkeyWidget::BindingsChanged, this)
+				  &OBSHotkeyWidget::BindingsChanged, this),
+		  settings(settings)
 	{
 		auto layout = new QVBoxLayout;
 		layout->setSpacing(0);
@@ -121,6 +144,7 @@ public:
 	bool Changed() const;
 
 	QPointer<OBSHotkeyLabel> label;
+	std::vector<QPointer<OBSHotkeyEdit>> edits;
 
 	QString toolTip;
 	void setToolTip(const QString &toolTip_)
@@ -148,11 +172,11 @@ private:
 
 	static void BindingsChanged(void *data, calldata_t *param);
 
-	std::vector<QPointer<OBSHotkeyEdit>> edits;
 	std::vector<QPointer<QPushButton>> removeButtons;
 	std::vector<QPointer<QPushButton>> revertButtons;
 	OBSSignal bindingsChanged;
 	bool ignoreChangedBindings = false;
+	OBSBasicSettings *settings;
 
 	QVBoxLayout *layout() const
 	{
@@ -164,4 +188,5 @@ private slots:
 
 signals:
 	void KeyChanged();
+	void SearchKey(obs_key_combination_t);
 };

+ 166 - 42
UI/window-basic-settings.cpp

@@ -47,6 +47,7 @@
 #include "window-projector.hpp"
 
 #include <util/platform.h>
+#include <util/dstr.hpp>
 #include "ui-config.h"
 
 #define ENCODER_HIDE_FLAGS \
@@ -713,6 +714,9 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 	channelChanged.Connect(obs_get_signal_handler(), "channel_change",
 			       ReloadAudioSources, this);
 
+	hotkeyConflictIcon =
+		QIcon::fromTheme("obs", QIcon(":/res/images/warning.svg"));
+
 	auto ReloadHotkeys = [](void *data, calldata_t *) {
 		auto settings = static_cast<OBSBasicSettings *>(data);
 		QMetaObject::invokeMethod(settings, "ReloadHotkeys");
@@ -2600,7 +2604,8 @@ void OBSBasicSettings::LoadAdvancedSettings()
 
 template<typename Func>
 static inline void
-LayoutHotkey(obs_hotkey_id id, obs_hotkey_t *key, Func &&fun,
+LayoutHotkey(OBSBasicSettings *settings, obs_hotkey_id id, obs_hotkey_t *key,
+	     Func &&fun,
 	     const map<obs_hotkey_id, vector<obs_key_combination_t>> &keys)
 {
 	auto *label = new OBSHotkeyLabel;
@@ -2619,9 +2624,10 @@ LayoutHotkey(obs_hotkey_id id, obs_hotkey_t *key, Func &&fun,
 
 	auto combos = keys.find(id);
 	if (combos == std::end(keys))
-		hw = new OBSHotkeyWidget(id, obs_hotkey_get_name(key));
-	else
 		hw = new OBSHotkeyWidget(id, obs_hotkey_get_name(key),
+					 settings);
+	else
+		hw = new OBSHotkeyWidget(id, obs_hotkey_get_name(key), settings,
 					 combos->second);
 
 	hw->label = label;
@@ -2715,58 +2721,104 @@ void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey)
 		},
 		&keys);
 
-	auto layout = new QFormLayout();
-	layout->setVerticalSpacing(0);
-	layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
-	layout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing |
-				  Qt::AlignVCenter);
+	auto layout = new QVBoxLayout();
+	ui->hotkeyPage->setLayout(layout);
 
 	auto widget = new QWidget();
-	widget->setLayout(layout);
-	ui->hotkeyPage->setWidget(widget);
+	auto scrollArea = new QScrollArea();
+	scrollArea->setWidgetResizable(true);
+	scrollArea->setWidget(widget);
 
-	auto filterLayout = new QGridLayout();
-	auto filterWidget = new QWidget();
-	filterWidget->setLayout(filterLayout);
+	auto hotkeysLayout = new QFormLayout();
+	hotkeysLayout->setVerticalSpacing(0);
+	hotkeysLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+	hotkeysLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing |
+					 Qt::AlignVCenter);
+	widget->setLayout(hotkeysLayout);
 
+	auto filterLayout = new QGridLayout();
 	auto filterLabel = new QLabel(QTStr("Basic.Settings.Hotkeys.Filter"));
 	auto filter = new QLineEdit();
+	auto filterHotkeyLabel =
+		new QLabel(QTStr("Basic.Settings.Hotkeys.FilterByHotkey"));
+	auto filterHotkeyInput = new OBSHotkeyEdit({}, this);
+	auto filterReset = new QPushButton;
+	filterReset->setProperty("themeID", "trashIcon");
+	filterReset->setToolTip(QTStr("Clear"));
+	filterReset->setFixedSize(24, 24);
+	filterReset->setFlat(true);
 
 	auto setRowVisible = [=](int row, bool visible, QLayoutItem *label) {
 		label->widget()->setVisible(visible);
 
-		auto field = layout->itemAt(row, QFormLayout::FieldRole);
+		auto field = hotkeysLayout->itemAt(row, QFormLayout::FieldRole);
 		if (field)
 			field->widget()->setVisible(visible);
 	};
 
-	auto searchFunction = [=](const QString &text) {
-		for (int i = 0; i < layout->rowCount(); i++) {
-			auto label = layout->itemAt(i, QFormLayout::LabelRole);
-			if (label) {
-				OBSHotkeyLabel *item =
-					qobject_cast<OBSHotkeyLabel *>(
-						label->widget());
-				if (item) {
-					QString fullname =
-						item->property("fullName")
-							.value<QString>();
-					if (fullname.toLower().contains(
-						    text.toLower()))
-						setRowVisible(i, true, label);
-					else
-						setRowVisible(i, false, label);
+	auto searchFunction = [=](const QString &text,
+				  obs_key_combination_t filterCombo) {
+		std::vector<obs_key_combination_t> combos;
+		bool showHotkey;
+		scrollArea->ensureVisible(0, 0);
+		scrollArea->setUpdatesEnabled(false);
+
+		for (int i = 0; i < hotkeysLayout->rowCount(); i++) {
+			auto label = hotkeysLayout->itemAt(
+				i, QFormLayout::LabelRole);
+			if (!label)
+				continue;
+
+			OBSHotkeyLabel *item =
+				qobject_cast<OBSHotkeyLabel *>(label->widget());
+			if (!item)
+				continue;
+
+			item->widget->GetCombinations(combos);
+			QString fullname =
+				item->property("fullName").value<QString>();
+
+			showHotkey =
+				text.isEmpty() ||
+				fullname.toLower().contains(text.toLower());
+
+			if (showHotkey &&
+			    !obs_key_combination_is_empty(filterCombo)) {
+				showHotkey = false;
+				for (auto combo : combos) {
+					if (combo == filterCombo) {
+						showHotkey = true;
+						continue;
+					}
 				}
 			}
+			setRowVisible(i, showHotkey, label);
 		}
+		scrollArea->setUpdatesEnabled(true);
 	};
 
-	connect(filter, &QLineEdit::textChanged, this, searchFunction);
+	connect(filter, &QLineEdit::textChanged, this, [=](const QString text) {
+		searchFunction(text, filterHotkeyInput->key);
+	});
+
+	connect(filterHotkeyInput, &OBSHotkeyEdit::KeyChanged, this,
+		[=](obs_key_combination_t combo) {
+			searchFunction(filter->text(), combo);
+		});
+
+	connect(filterReset, &QPushButton::clicked, this, [=]() {
+		filter->setText("");
+		filterHotkeyInput->ResetKey();
+	});
 
 	filterLayout->addWidget(filterLabel, 0, 0);
 	filterLayout->addWidget(filter, 0, 1);
+	filterLayout->addWidget(filterHotkeyLabel, 0, 2);
+	filterLayout->addWidget(filterHotkeyInput, 0, 3);
+	filterLayout->addWidget(filterReset, 0, 4);
 
-	layout->addRow(filterWidget);
+	layout->addLayout(filterLayout);
+	layout->addWidget(scrollArea);
 
 	using namespace std;
 	using encoders_elem_t =
@@ -2858,7 +2910,7 @@ void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey)
 
 		switch (registerer_type) {
 		case OBS_HOTKEY_REGISTERER_FRONTEND:
-			layout->addRow(label, hw);
+			hotkeysLayout->addRow(label, hw);
 			break;
 
 		case OBS_HOTKEY_REGISTERER_ENCODER:
@@ -2884,17 +2936,27 @@ void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey)
 
 		hotkeys.emplace_back(
 			registerer_type == OBS_HOTKEY_REGISTERER_FRONTEND, hw);
-		connect(hw, &OBSHotkeyWidget::KeyChanged, this,
-			&OBSBasicSettings::HotkeysChanged);
+		connect(hw, &OBSHotkeyWidget::KeyChanged, this, [=]() {
+			HotkeysChanged();
+			ScanDuplicateHotkeys(hotkeysLayout);
+		});
+		connect(hw, &OBSHotkeyWidget::SearchKey,
+			[=](obs_key_combination_t combo) {
+				filter->setText("");
+				filterHotkeyInput->HandleNewKey(combo);
+				filterHotkeyInput->KeyChanged(combo);
+			});
 	};
 
-	auto data = make_tuple(RegisterHotkey, std::move(keys), ignoreKey);
+	auto data =
+		make_tuple(RegisterHotkey, std::move(keys), ignoreKey, this);
 	using data_t = decltype(data);
 	obs_enum_hotkeys(
 		[](void *data, obs_hotkey_id id, obs_hotkey_t *key) {
 			data_t &d = *static_cast<data_t *>(data);
 			if (id != get<2>(d))
-				LayoutHotkey(id, key, get<0>(d), get<1>(d));
+				LayoutHotkey(get<3>(d), id, key, get<0>(d),
+					     get<1>(d));
 			return true;
 		},
 		&data);
@@ -2937,11 +2999,13 @@ void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey)
 		Update(label2, name2, label1, name1);
 	}
 
-	AddHotkeys(*layout, obs_output_get_name, outputs);
-	AddHotkeys(*layout, obs_source_get_name, scenes);
-	AddHotkeys(*layout, obs_source_get_name, sources);
-	AddHotkeys(*layout, obs_encoder_get_name, encoders);
-	AddHotkeys(*layout, obs_service_get_name, services);
+	AddHotkeys(*hotkeysLayout, obs_output_get_name, outputs);
+	AddHotkeys(*hotkeysLayout, obs_source_get_name, scenes);
+	AddHotkeys(*hotkeysLayout, obs_source_get_name, sources);
+	AddHotkeys(*hotkeysLayout, obs_encoder_get_name, encoders);
+	AddHotkeys(*hotkeysLayout, obs_service_get_name, services);
+
+	ScanDuplicateHotkeys(hotkeysLayout);
 }
 
 void OBSBasicSettings::LoadSettings(bool changedOnly)
@@ -4274,6 +4338,66 @@ void OBSBasicSettings::HotkeysChanged()
 		EnableApplyButton(true);
 }
 
+static bool MarkHotkeyConflicts(OBSHotkeyLabel *item1, OBSHotkeyLabel *item2)
+{
+	if (item1->pairPartner == item2)
+		return false;
+
+	auto &edits1 = item1->widget->edits;
+	auto &edits2 = item2->widget->edits;
+	bool hasDupes = false;
+
+	for (auto &edit1 : edits1) {
+		for (auto &edit2 : edits2) {
+			bool isDupe =
+				!obs_key_combination_is_empty(edit1->key) &&
+				edit1->key == edit2->key;
+
+			hasDupes |= isDupe;
+			edit1->hasDuplicate |= isDupe;
+			edit2->hasDuplicate |= isDupe;
+		}
+	}
+
+	return hasDupes;
+};
+
+bool OBSBasicSettings::ScanDuplicateHotkeys(QFormLayout *layout)
+{
+	vector<OBSHotkeyLabel *> items;
+	bool hasDupes = false;
+
+	for (int i = 0; i < layout->rowCount(); i++) {
+		auto label = layout->itemAt(i, QFormLayout::LabelRole);
+		if (!label)
+			continue;
+		OBSHotkeyLabel *item =
+			qobject_cast<OBSHotkeyLabel *>(label->widget());
+		if (!item)
+			continue;
+
+		items.push_back(item);
+
+		for (auto &edit : item->widget->edits)
+			edit->hasDuplicate = false;
+	}
+
+	for (int i = 0; i < items.size(); i++) {
+		OBSHotkeyLabel *item1 = items[i];
+
+		for (int j = i + 1; j < items.size(); j++)
+			hasDupes |= MarkHotkeyConflicts(item1, items[j]);
+	}
+
+	for (auto *item : items) {
+		for (auto &edit : item->widget->edits) {
+			edit->UpdateDuplicationState();
+		}
+	}
+
+	return hasDupes;
+}
+
 void OBSBasicSettings::ReloadHotkeys(obs_hotkey_id ignoreKey)
 {
 	LoadHotkeySettings(ignoreKey);

+ 8 - 0
UI/window-basic-settings.hpp

@@ -165,6 +165,8 @@ private:
 	QPointer<QWidget> vodTrackContainer;
 	QPointer<QRadioButton> vodTrack[MAX_AUDIO_MIXES];
 
+	QIcon hotkeyConflictIcon;
+
 	void SaveCombo(QComboBox *widget, const char *section,
 		       const char *value);
 	void SaveComboData(QComboBox *widget, const char *section,
@@ -370,6 +372,7 @@ private slots:
 	void VideoChangedResolution();
 	void VideoChangedRestart();
 	void HotkeysChanged();
+	bool ScanDuplicateHotkeys(QFormLayout *layout);
 	void ReloadHotkeys(obs_hotkey_id ignoreKey = OBS_INVALID_HOTKEY_ID);
 	void AdvancedChanged();
 	void AdvancedChangedRestart();
@@ -408,4 +411,9 @@ protected:
 public:
 	OBSBasicSettings(QWidget *parent);
 	~OBSBasicSettings();
+
+	inline const QIcon &GetHotkeyConflictIcon() const
+	{
+		return hotkeyConflictIcon;
+	}
 };