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();