From 9e05246e6aafab840cea5c987dc3f49b92821318 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Tue, 25 Nov 2025 21:52:51 +0200 Subject: [PATCH] frontend: Escape "&" in non-hardcoded menu items "&" is used to make shortcuts for menu items, so we need to escape non-hardcoded items with "&&". Applies escaping for these menus: - profiles menu - scene collections menu - projector names - source types (for plugins) - transitions menu (for plugins) - filters menu (for plugins) --- frontend/OBSStudioAPI.cpp | 4 ++-- frontend/dialogs/OBSBasicFilters.cpp | 2 +- frontend/widgets/OBSBasic.hpp | 2 +- frontend/widgets/OBSBasic_Profiles.cpp | 2 +- frontend/widgets/OBSBasic_SceneCollections.cpp | 2 +- frontend/widgets/OBSBasic_SceneItems.cpp | 2 +- frontend/widgets/OBSBasic_Transitions.cpp | 6 +++--- shared/qt/wrappers/qt-wrappers.cpp | 5 +++++ shared/qt/wrappers/qt-wrappers.hpp | 2 ++ 9 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frontend/OBSStudioAPI.cpp b/frontend/OBSStudioAPI.cpp index 7e8ab72bb9eff3..5189baeef6ff3d 100644 --- a/frontend/OBSStudioAPI.cpp +++ b/frontend/OBSStudioAPI.cpp @@ -147,7 +147,7 @@ void OBSStudioAPI::obs_frontend_set_current_scene_collection(const char *collect QVariant v = action->property("file_name"); if (v.typeName() != nullptr) { - if (action->text() == qstrCollection) { + if (action->property("collection_name").toString() == qstrCollection) { action->trigger(); break; } @@ -195,7 +195,7 @@ void OBSStudioAPI::obs_frontend_set_current_profile(const char *profile) QVariant v = action->property("file_name"); if (v.typeName() != nullptr) { - if (action->text() == qstrProfile) { + if (action->property("profile_name").toString() == qstrProfile) { action->trigger(); break; } diff --git a/frontend/dialogs/OBSBasicFilters.cpp b/frontend/dialogs/OBSBasicFilters.cpp index a7c42d7f6f05f8..69af063bbbde0f 100644 --- a/frontend/dialogs/OBSBasicFilters.cpp +++ b/frontend/dialogs/OBSBasicFilters.cpp @@ -484,7 +484,7 @@ QMenu *OBSBasicFilters::CreateAddFilterPopupMenu(bool async) if (!filter_compatible(async, sourceFlags, filterFlags)) continue; - QAction *popupItem = new QAction(QT_UTF8(type.name.c_str()), this); + QAction *popupItem = new QAction(EscapeMenuItem(QT_UTF8(type.name.c_str())), this); popupItem->setData(QT_UTF8(type.type.c_str())); connect(popupItem, &QAction::triggered, [this, type]() { AddNewFilter(type.type.c_str()); }); popup->addAction(popupItem); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index ca213433e0beb8..cada1a47b91af7 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -957,7 +957,7 @@ private slots: auto projectors = GetProjectorMenuMonitorsFormatted(); for (int i = 0; i < projectors.size(); i++) { QString str = projectors[i]; - QAction *action = parent->addAction(str, target, slot); + QAction *action = parent->addAction(EscapeMenuItem(str), target, slot); action->setProperty("monitor", i); } } diff --git a/frontend/widgets/OBSBasic_Profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp index 4c655915fe1edf..884324a9b3608f 100644 --- a/frontend/widgets/OBSBasic_Profiles.cpp +++ b/frontend/widgets/OBSBasic_Profiles.cpp @@ -309,7 +309,7 @@ void OBSBasic::RefreshProfiles(bool refreshCache) const OBSProfile &profile = profiles.at(profileName); const QString qProfileName = QString().fromStdString(profileName); - QAction *action = new QAction(qProfileName, this); + QAction *action = new QAction(EscapeMenuItem(qProfileName), this); action->setProperty("profile_name", qProfileName); action->setProperty("file_name", QString().fromStdString(profile.directoryName)); connect(action, &QAction::triggered, this, &OBSBasic::ChangeProfile); diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index ee9e4e6086f735..1d9ab111e95e83 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -398,7 +398,7 @@ void OBSBasic::RefreshSceneCollections(bool refreshCache) const SceneCollection &collection = collections.at(collectionName); const QString qCollectionName = QString().fromStdString(collectionName); - QAction *action = new QAction(qCollectionName, this); + QAction *action = new QAction(EscapeMenuItem(qCollectionName), this); action->setProperty("collection_name", qCollectionName); action->setProperty("file_name", QString().fromStdString(collection.getFileName())); connect(action, &QAction::triggered, this, &OBSBasic::ChangeSceneCollection); diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp index ce60b71ec046e4..0a08517c786522 100644 --- a/frontend/widgets/OBSBasic_SceneItems.cpp +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -801,7 +801,7 @@ QMenu *OBSBasic::CreateAddSourcePopupMenu() }; auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); + QString qname = EscapeMenuItem(QT_UTF8(name)); QAction *popupItem = new QAction(qname, this); connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp index b76f433762fc73..da5f00a8e0b6cc 100644 --- a/frontend/widgets/OBSBasic_Transitions.cpp +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -483,7 +483,7 @@ void OBSBasic::on_transitionAdd_clicked() while (obs_enum_transition_types(idx++, &id)) { if (obs_is_source_configurable(id)) { const char *name = obs_source_get_display_name(id); - QAction *action = new QAction(name, this); + QAction *action = new QAction(EscapeMenuItem(QT_UTF8(name)), this); connect(action, &QAction::triggered, [this, id]() { AddTransition(id); }); @@ -840,7 +840,7 @@ QMenu *OBSBasic::CreatePerSceneTransitionMenu() if (!name || !*name) name = Str("None"); - action = menu->addAction(QT_UTF8(name)); + action = menu->addAction(EscapeMenuItem(QT_UTF8(name))); action->setProperty("transition_uuid", QString::fromStdString(uuid)); action->setCheckable(true); action->setChecked(match); @@ -999,7 +999,7 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) while (obs_enum_transition_types(idx++, &id)) { const char *name = obs_source_get_display_name(id); const bool match = id && curId && strcmp(id, curId) == 0; - action = menu->addAction(QT_UTF8(name)); + action = menu->addAction(EscapeMenuItem(QT_UTF8(name))); action->setProperty("transition_id", QT_UTF8(id)); action->setCheckable(true); action->setChecked(match); diff --git a/shared/qt/wrappers/qt-wrappers.cpp b/shared/qt/wrappers/qt-wrappers.cpp index 490f59bb1f8358..bf43b5608a7832 100644 --- a/shared/qt/wrappers/qt-wrappers.cpp +++ b/shared/qt/wrappers/qt-wrappers.cpp @@ -373,3 +373,8 @@ void RefreshToolBarStyling(QToolBar *toolBar) widget->style()->polish(widget); } } + +QString EscapeMenuItem(QString name) +{ + return name.replace("&", "&&"); +} diff --git a/shared/qt/wrappers/qt-wrappers.hpp b/shared/qt/wrappers/qt-wrappers.hpp index 72ab917a5636c7..96b5889ed42d53 100644 --- a/shared/qt/wrappers/qt-wrappers.hpp +++ b/shared/qt/wrappers/qt-wrappers.hpp @@ -99,3 +99,5 @@ QStringList OpenFiles(QWidget *parent, QString title, QString path, QString exte void TruncateLabel(QLabel *label, QString newText, int length = MAX_LABEL_LENGTH); void RefreshToolBarStyling(QToolBar *toolBar); + +QString EscapeMenuItem(QString name);