diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini index 3bb763897..33f606f12 100644 --- a/obs/data/locale/en-US.ini +++ b/obs/data/locale/en-US.ini @@ -353,6 +353,10 @@ Basic.AdvAudio.Panning="Panning" Basic.AdvAudio.SyncOffset="Sync Offset (ms)" Basic.AdvAudio.AudioTracks="Tracks" +# basic mode 'hotkeys' settings +Basic.Settings.Hotkeys="Hotkeys" +Basic.Settings.Hotkeys.Pair="Key combinations shared with '%1' act as toggles" + # hotkeys that may lack translation on certain operating systems Hotkeys.Insert="Insert" Hotkeys.Delete="Delete" diff --git a/obs/forms/OBSBasicSettings.ui b/obs/forms/OBSBasicSettings.ui index 9726f64fb..efe763354 100644 --- a/obs/forms/OBSBasicSettings.ui +++ b/obs/forms/OBSBasicSettings.ui @@ -87,6 +87,15 @@ :/settings/images/settings/video-display-3.png:/settings/images/settings/video-display-3.png + + + Basic.Settings.Hotkeys + + + + :/settings/images/settings/preferences-desktop-keyboard-shortcuts.png:/settings/images/settings/preferences-desktop-keyboard-shortcuts.png + + Basic.Settings.Advanced @@ -2330,6 +2339,24 @@ + + + true + + + + + 0 + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + diff --git a/obs/forms/images/settings/preferences-desktop-keyboard-shortcuts.png b/obs/forms/images/settings/preferences-desktop-keyboard-shortcuts.png new file mode 100644 index 000000000..95a3aa87e Binary files /dev/null and b/obs/forms/images/settings/preferences-desktop-keyboard-shortcuts.png differ diff --git a/obs/forms/obs.qrc b/obs/forms/obs.qrc index e6a14d7ef..60ab59a4b 100644 --- a/obs/forms/obs.qrc +++ b/obs/forms/obs.qrc @@ -22,5 +22,6 @@ images/settings/applications-system-2.png images/settings/system-settings-3.png images/settings/network-bluetooth.png + images/settings/preferences-desktop-keyboard-shortcuts.png diff --git a/obs/window-basic-settings.cpp b/obs/window-basic-settings.cpp index 483bf8735..99b637e78 100644 --- a/obs/window-basic-settings.cpp +++ b/obs/window-basic-settings.cpp @@ -28,7 +28,10 @@ #include #include #include +#include +#include "hotkey-edit.hpp" +#include "source-label.hpp" #include "obs-app.hpp" #include "platform.hpp" #include "properties-view.hpp" @@ -313,6 +316,26 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) LoadEncoderTypes(); LoadColorRanges(); LoadFormats(); + + auto ReloadHotkeys = [](void *data, calldata_t*) + { + auto settings = static_cast(data); + QMetaObject::invokeMethod(settings, "ReloadHotkeys"); + }; + hotkeyRegistered.Connect(obs_get_signal_handler(), "hotkey_register", + ReloadHotkeys, this); + + auto ReloadHotkeysIgnore = [](void *data, calldata_t *param) + { + auto settings = static_cast(data); + auto key = static_cast( + calldata_ptr(param,"key")); + QMetaObject::invokeMethod(settings, "ReloadHotkeys", + Q_ARG(obs_hotkey_id, obs_hotkey_get_id(key))); + }; + hotkeyUnregistered.Connect(obs_get_signal_handler(), + "hotkey_unregister", ReloadHotkeysIgnore, this); + LoadSettings(false); } @@ -1278,6 +1301,290 @@ void OBSBasicSettings::LoadAdvancedSettings() loading = false; } +template +static inline void LayoutHotkey(obs_hotkey_id id, obs_hotkey_t *key, Func &&fun, + const map> &keys) +{ + auto *label = new OBSHotkeyLabel; + label->setText(obs_hotkey_get_description(key)); + + OBSHotkeyWidget *hw = nullptr; + + 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), + combos->second); + + hw->label = label; + label->widget = hw; + + fun(key, label, hw); +} + +template +static QLabel *makeLabel(T &t, Func &&getName) +{ + return new QLabel(getName(t)); +} + +template +static QLabel *makeLabel(const OBSSource &source, Func &&) +{ + return new OBSSourceLabel(source); +} + +template +static inline void AddHotkeys(QFormLayout &layout, + Func &&getName, std::vector< + std::tuple, QPointer> + > &hotkeys) +{ + if (hotkeys.empty()) + return; + + auto line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + + layout.setItem(layout.rowCount(), QFormLayout::SpanningRole, + new QSpacerItem(0, 10)); + layout.addRow(line); + + using tuple_type = + std::tuple, QPointer>; + + stable_sort(begin(hotkeys), end(hotkeys), + [&](const tuple_type &a, const tuple_type &b) + { + const auto &o_a = get<0>(a); + const auto &o_b = get<0>(b); + return o_a != o_b && + string(getName(o_a)) < + getName(o_b); + }); + + string prevName; + for (const auto &hotkey : hotkeys) { + const auto &o = get<0>(hotkey); + const char *name = getName(o); + if (prevName != name) { + prevName = name; + layout.setItem(layout.rowCount(), + QFormLayout::SpanningRole, + new QSpacerItem(0, 10)); + layout.addRow(makeLabel(o, getName)); + } + + auto hlabel = get<1>(hotkey); + auto widget = get<2>(hotkey); + layout.addRow(hlabel, widget); + } +} + +void OBSBasicSettings::LoadHotkeySettings(obs_hotkey_id ignoreKey) +{ + hotkeys.clear(); + ui->hotkeyPage->takeWidget()->deleteLater(); + + using keys_t = map>; + keys_t keys; + obs_enum_hotkey_bindings([](void *data, + size_t, obs_hotkey_binding_t *binding) + { + auto &keys = *static_cast(data); + + keys[obs_hotkey_binding_get_hotkey_id(binding)].emplace_back( + obs_hotkey_binding_get_key_combination(binding)); + + return true; + }, &keys); + + auto layout = new QFormLayout(); + layout->setVerticalSpacing(0); + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + layout->setLabelAlignment( + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); + + auto widget = new QWidget(); + widget->setLayout(layout); + ui->hotkeyPage->setWidget(widget); + + using namespace std; + using encoders_elem_t = + tuple, QPointer>; + using outputs_elem_t = + tuple, QPointer>; + using services_elem_t = + tuple, QPointer>; + using sources_elem_t = + tuple, QPointer>; + vector encoders; + vector outputs; + vector services; + vector scenes; + vector sources; + + vector pairIds; + map> pairLabels; + + using std::move; + + auto HandleEncoder = [&](void *registerer, OBSHotkeyLabel *label, + OBSHotkeyWidget *hw) + { + auto weak_encoder = + static_cast(registerer); + auto encoder = OBSGetStrongRef(weak_encoder); + + if (!encoder) + return true; + + encoders.emplace_back(move(encoder), label, hw); + return false; + }; + + auto HandleOutput = [&](void *registerer, OBSHotkeyLabel *label, + OBSHotkeyWidget *hw) + { + auto weak_output = static_cast(registerer); + auto output = OBSGetStrongRef(weak_output); + + if (!output) + return true; + + outputs.emplace_back(move(output), label, hw); + return false; + }; + + auto HandleService = [&](void *registerer, OBSHotkeyLabel *label, + OBSHotkeyWidget *hw) + { + auto weak_service = + static_cast(registerer); + auto service = OBSGetStrongRef(weak_service); + + if (!service) + return true; + + services.emplace_back(move(service), label, hw); + return false; + }; + + auto HandleSource = [&](void *registerer, OBSHotkeyLabel *label, + OBSHotkeyWidget *hw) + { + auto weak_source = static_cast(registerer); + auto source = OBSGetStrongRef(weak_source); + + if (!source) + return true; + + if (obs_scene_from_source(source)) + scenes.emplace_back(source, label, hw); + else + sources.emplace_back(source, label, hw); + + return false; + }; + + auto RegisterHotkey = [&](obs_hotkey_t *key, OBSHotkeyLabel *label, + OBSHotkeyWidget *hw) + { + auto registerer_type = obs_hotkey_get_registerer_type(key); + void *registerer = obs_hotkey_get_registerer(key); + + obs_hotkey_id partner = obs_hotkey_get_pair_partner_id(key); + if (partner != OBS_INVALID_HOTKEY_ID) { + pairLabels.emplace(obs_hotkey_get_id(key), + make_pair(partner, label)); + pairIds.push_back(obs_hotkey_get_id(key)); + } + + using std::move; + + switch (registerer_type) { + case OBS_HOTKEY_REGISTERER_FRONTEND: + layout->addRow(label, hw); + break; + + case OBS_HOTKEY_REGISTERER_ENCODER: + if (HandleEncoder(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_OUTPUT: + if (HandleOutput(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_SERVICE: + if (HandleService(registerer, label, hw)) + return; + break; + + case OBS_HOTKEY_REGISTERER_SOURCE: + if (HandleSource(registerer, label, hw)) + return; + break; + } + + hotkeys.emplace_back(registerer_type == + OBS_HOTKEY_REGISTERER_FRONTEND, hw); + connect(hw, &OBSHotkeyWidget::KeyChanged, + this, &OBSBasicSettings::HotkeysChanged); + }; + + auto data = make_tuple(RegisterHotkey, std::move(keys), ignoreKey); + 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)); + return true; + }, &data); + + for (auto keyId : pairIds) { + auto data1 = pairLabels.find(keyId); + if (data1 == end(pairLabels)) + continue; + + auto &label1 = data1->second.second; + if (label1->pairPartner) + continue; + + auto data2 = pairLabels.find(data1->second.first); + if (data2 == end(pairLabels)) + continue; + + auto &label2 = data2->second.second; + if (label2->pairPartner) + continue; + + QString tt = QTStr("Basic.Settings.Hotkeys.Pair"); + auto name1 = label1->text(); + auto name2 = label2->text(); + + auto Update = [&](OBSHotkeyLabel *label, const QString &name, + OBSHotkeyLabel *other, const QString &otherName) + { + label->setToolTip(tt.arg(otherName)); + label->setText(name + " ✳"); + label->pairPartner = other; + }; + Update(label1, name1, label2, name2); + 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); +} + void OBSBasicSettings::LoadSettings(bool changedOnly) { if (!changedOnly || generalChanged) @@ -1290,6 +1597,8 @@ void OBSBasicSettings::LoadSettings(bool changedOnly) LoadAudioSettings(); if (!changedOnly || videoChanged) LoadVideoSettings(); + if (!changedOnly || hotkeysChanged) + LoadHotkeySettings(); if (!changedOnly || advancedChanged) LoadAdvancedSettings(); } @@ -1560,6 +1869,33 @@ void OBSBasicSettings::SaveAudioSettings() main->ResetAudioDevices(); } +void OBSBasicSettings::SaveHotkeySettings() +{ + const auto &config = main->Config(); + + using namespace std; + + std::vector combinations; + for (auto &hotkey : hotkeys) { + auto &hw = *hotkey.second; + if (!hw.Changed()) + continue; + + hw.Save(combinations); + + if (!hotkey.first) + continue; + + obs_data_array_t *array = obs_hotkey_save(hw.id); + obs_data_t *data = obs_data_create(); + obs_data_set_array(data, "bindings", array); + const char *json = obs_data_get_json(data); + config_set_string(config, "Hotkeys", hw.name.c_str(), json); + obs_data_release(data); + obs_data_array_release(array); + } +} + void OBSBasicSettings::SaveSettings() { if (generalChanged) @@ -1572,6 +1908,8 @@ void OBSBasicSettings::SaveSettings() SaveAudioSettings(); if (videoChanged) SaveVideoSettings(); + if (hotkeysChanged) + SaveHotkeySettings(); if (advancedChanged) SaveAdvancedSettings(); @@ -1937,6 +2275,28 @@ void OBSBasicSettings::VideoChanged() } } +void OBSBasicSettings::HotkeysChanged() +{ + using namespace std; + if (loading) + return; + + hotkeysChanged = any_of(begin(hotkeys), end(hotkeys), + [](const pair> &hotkey) + { + const auto &hw = *hotkey.second; + return hw.Changed(); + }); + + if (hotkeysChanged) + EnableApplyButton(true); +} + +void OBSBasicSettings::ReloadHotkeys(obs_hotkey_id ignoreKey) +{ + LoadHotkeySettings(ignoreKey); +} + void OBSBasicSettings::AdvancedChanged() { if (!loading) { diff --git a/obs/window-basic-settings.hpp b/obs/window-basic-settings.hpp index 93bf7b9d2..85fdfb68d 100644 --- a/obs/window-basic-settings.hpp +++ b/obs/window-basic-settings.hpp @@ -31,6 +31,7 @@ class OBSBasic; class QAbstractButton; class QComboBox; class OBSPropertiesView; +class OBSHotkeyWidget; #include "ui_OBSBasicSettings.h" @@ -58,11 +59,13 @@ private: OBSBasic *main; std::unique_ptr ui; + bool generalChanged = false; bool stream1Changed = false; bool outputsChanged = false; bool audioChanged = false; bool videoChanged = false; + bool hotkeysChanged = false; bool advancedChanged = false; int pageIndex = 0; bool loading = true; @@ -74,6 +77,10 @@ private: OBSPropertiesView *streamEncoderProps = nullptr; OBSPropertiesView *recordEncoderProps = nullptr; + std::vector>> hotkeys; + OBSSignal hotkeyRegistered; + OBSSignal hotkeyUnregistered; + void SaveCombo(QComboBox *widget, const char *section, const char *value); void SaveComboData(QComboBox *widget, const char *section, @@ -91,7 +98,8 @@ private: inline bool Changed() const { return generalChanged || outputsChanged || stream1Changed || - audioChanged || videoChanged || advancedChanged; + audioChanged || videoChanged || advancedChanged || + hotkeysChanged; } inline void EnableApplyButton(bool en) @@ -106,6 +114,7 @@ private: outputsChanged = false; audioChanged = false; videoChanged = false; + hotkeysChanged = false; advancedChanged= false; EnableApplyButton(false); } @@ -125,6 +134,7 @@ private: void LoadOutputSettings(); void LoadAudioSettings(); void LoadVideoSettings(); + void LoadHotkeySettings(obs_hotkey_id ignoreKey=OBS_INVALID_HOTKEY_ID); void LoadAdvancedSettings(); void LoadSettings(bool changedOnly); @@ -165,6 +175,7 @@ private: void SaveOutputSettings(); void SaveAudioSettings(); void SaveVideoSettings(); + void SaveHotkeySettings(); void SaveAdvancedSettings(); void SaveSettings(); @@ -199,6 +210,8 @@ private slots: void VideoChanged(); void VideoChangedResolution(); void VideoChangedRestart(); + void HotkeysChanged(); + void ReloadHotkeys(obs_hotkey_id ignoreKey=OBS_INVALID_HOTKEY_ID); void AdvancedChanged(); void AdvancedChangedRestart();