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; + } };