diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index da98079a0..2b685d111 100644
--- a/UI/data/locale/en-US.ini
+++ b/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"
diff --git a/UI/forms/images/warning.svg b/UI/forms/images/warning.svg
new file mode 100644
index 000000000..29374f59b
--- /dev/null
+++ b/UI/forms/images/warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/UI/forms/obs.qrc b/UI/forms/obs.qrc
index 874e6a577..ec7a3f759 100644
--- a/UI/forms/obs.qrc
+++ b/UI/forms/obs.qrc
@@ -27,6 +27,7 @@
images/trash.svg
images/revert.svg
images/alert.svg
+ images/warning.svg
images/sources/brush.svg
images/sources/camera.svg
images/sources/gamepad.svg
diff --git a/UI/hotkey-edit.cpp b/UI/hotkey-edit.cpp
index 5307c0ca4..c18b7a3d0 100644
--- a/UI/hotkey-edit.cpp
+++ b/UI/hotkey-edit.cpp
@@ -15,27 +15,17 @@
along with this program. If not, see .
******************************************************************************/
+#include "window-basic-settings.hpp"
#include "hotkey-edit.hpp"
#include
#include
#include
+#include
#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 &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;
diff --git a/UI/hotkey-edit.hpp b/UI/hotkey-edit.hpp
index c625e3a8e..7d61a7203 100644
--- a/UI/hotkey-edit.hpp
+++ b/UI/hotkey-edit.hpp
@@ -27,6 +27,19 @@
#include
+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 &combos = {},
- QWidget *parent = nullptr)
- : QWidget(parent),
+ OBSBasicSettings *settings,
+ const std::vector &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 label;
+ std::vector> edits;
QString toolTip;
void setToolTip(const QString &toolTip_)
@@ -148,11 +172,11 @@ private:
static void BindingsChanged(void *data, calldata_t *param);
- std::vector> edits;
std::vector> removeButtons;
std::vector> 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);
};
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index e088a12f4..c6a424402 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -47,6 +47,7 @@
#include "window-projector.hpp"
#include
+#include
#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(data);
QMetaObject::invokeMethod(settings, "ReloadHotkeys");
@@ -2600,7 +2604,8 @@ void OBSBasicSettings::LoadAdvancedSettings()
template
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> &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 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 filterWidget = new QWidget();
- filterWidget->setLayout(filterLayout);
-
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(
- label->widget());
- if (item) {
- QString fullname =
- item->property("fullName")
- .value();
- 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 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(label->widget());
+ if (!item)
+ continue;
+
+ item->widget->GetCombinations(combos);
+ QString fullname =
+ item->property("fullName").value();
+
+ 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);
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 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(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);
diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp
index a807aed29..b27c9e143 100644
--- a/UI/window-basic-settings.hpp
+++ b/UI/window-basic-settings.hpp
@@ -165,6 +165,8 @@ private:
QPointer vodTrackContainer;
QPointer 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;
+ }
};