Friendly Names might change, depending on driver/firmware updates.
Device-IDs are resilient against this and always unique.
On start old device names are migrated to ids if possible.
}
void MuteControl::SetManagedEndpoints(
- const std::vector<std::wstring>& endpoints, bool isAllowList)
+ const std::vector<ManagedEndpoint>& endpoints, bool isAllowList)
{
winAudio_->MuteSpecificEndpoints(true);
winAudio_->SetManagedEndpoints(endpoints, isAllowList);
void NotifyAudioDeviceArrived();
- void SetManagedEndpoints(const std::vector<std::wstring>& endpoints,
+ void SetManagedEndpoints(const std::vector<ManagedEndpoint>& endpoints,
bool isAllowList);
void ClearManagedEndpoints();
-----------------------------------------------------------------------------
*/
-#include "common.h"
#include <Functiondiscoverykeys_devpkey.h>
#include <atlbase.h>
#include <mmdeviceapi.h>
-static constexpr int ENDPOINT_NAME_MAX_LEN = 200;
+#include "common.h"
-struct EndpointData {
- std::wstring name;
+// Passed as lParam to the add/edit dialog; owned by the calling dialog.
+//
+// The combo box is pick-only, so every entry created here is bound to a real
+// endpoint id. The one exception is an entry carried over from an older
+// version (or created before the device list included it): that name is added
+// to the list as-is so editing does not silently discard it.
+struct EndpointAddCtx {
+ ManagedEndpoint* data = nullptr;
+ std::vector<ManagedEndpoint> candidates;
};
-static bool GetAudioEndpoints(std::vector<std::wstring>& endpoints)
-{
- WMLog& log = WMLog::GetInstance();
+// Item data of the "keep this legacy name" entry. Every other item carries an
+// index into EndpointAddCtx::candidates -- the combo box is sorted, so the
+// selection index on its own says nothing about which device was picked.
+static constexpr LONG_PTR ENDPOINT_ITEM_LEGACY_NAME = -1;
- CComPtr<IMMDeviceEnumerator> deviceEnumerator;
- if (FAILED(deviceEnumerator.CoCreateInstance(
- __uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER)))
- {
- return false;
- }
- // Must match the states VistaAudio manages, otherwise endpoints that are
- // currently unplugged (a sleeping monitor's HDMI/DisplayPort audio) could
- // not be put on the allow/block list while they are asleep.
- CComPtr<IMMDeviceCollection> audioEndpoints;
- if (FAILED(deviceEnumerator->EnumAudioEndpoints(
- eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED,
- &audioEndpoints)))
- {
- return false;
- }
-
- UINT epCount = 0;
- if (FAILED(audioEndpoints->GetCount(&epCount))) {
- return false;
- }
-
- for (UINT i = 0; i < epCount; ++i) {
- CComPtr<IMMDevice> device = nullptr;
- if (FAILED(audioEndpoints->Item(i, &device))) {
- log.LogError(L"Failed to get audio endpoint #{}", i);
- continue;
- }
+// State of the endpoint list dialog. The list box only renders friendly
+// names, so the endpoint ids have to be tracked alongside it.
+struct ManageEndpointsDlgState {
+ WMSettings* settings = nullptr;
+ std::vector<ManagedEndpoint> entries;
+};
- const auto deviceName = GetAudioDeviceName(device);
- if (deviceName) {
- endpoints.push_back(*deviceName);
- }
+static void RebuildEndpointList(HWND hList,
+ const std::vector<ManagedEndpoint>& entries)
+{
+ ListBox_ResetContent(hList);
+ for (const auto& entry : entries) {
+ ListBox_AddString(hList, entry.name.c_str());
}
-
- return true;
}
static void LoadManageEndpointsAddDlgTranslation(HWND hDlg, bool isEdit)
"settings.bluetooth.add-edit.device-name-label");
i18n.SetItemText(hDlg, IDOK, "settings.btn-save");
i18n.SetItemText(hDlg, IDCANCEL, "settings.btn-cancel");
-
- const auto placeholder = i18n.GetTranslationW(
- "settings.mute.manage-endpoints.add-edit.endpoint-name-placeholder");
- ComboBox_SetCueBannerText(GetDlgItem(hDlg, IDC_ENDPOINT_NAME),
- placeholder.c_str());
}
static INT_PTR CALLBACK Settings_EndpointAddDlgProc(HWND hDlg, UINT msg,
switch (msg) {
case WM_INITDIALOG: {
HWND hEndpointName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME);
- EndpointData* endpointData =
- reinterpret_cast<EndpointData*>(lParam);
- if (endpointData == nullptr) {
+ EndpointAddCtx* ctx = reinterpret_cast<EndpointAddCtx*>(lParam);
+ if (ctx == nullptr || ctx->data == nullptr) {
return FALSE;
}
- SetWindowLongPtr(hDlg, DWLP_USER,
- reinterpret_cast<LONG_PTR>(endpointData));
+ SetWindowLongPtr(hDlg, DWLP_USER, reinterpret_cast<LONG_PTR>(ctx));
- LoadManageEndpointsAddDlgTranslation(
- hDlg, endpointData->name.length() != 0);
- if (endpointData->name.length() != 0) {
- SetWindowTextW(GetDlgItem(hDlg, IDC_ENDPOINT_NAME),
- endpointData->name.c_str());
- }
+ const bool isEdit = ctx->data->name.length() != 0;
+ LoadManageEndpointsAddDlgTranslation(hDlg, isEdit);
- // Disable save button until at least one string change is made
+ // Disable save button until a device has been picked
EnableWindow(GetDlgItem(hDlg, IDOK), FALSE);
- // Fill Combobox
- std::vector<std::wstring> audioEndpoints;
- if (GetAudioEndpoints(audioEndpoints)) {
- for (const auto& devName : audioEndpoints) {
- ComboBox_AddString(hEndpointName, devName.c_str());
+ // The combo box sorts, so remember which device each item is via
+ // its item data rather than relying on the insertion order.
+ if (EnumerateAudioEndpoints(ctx->candidates)) {
+ for (size_t i = 0; i < ctx->candidates.size(); ++i) {
+ const int pos = ComboBox_AddString(
+ hEndpointName, ctx->candidates[i].name.c_str());
+ if (pos >= 0) {
+ ComboBox_SetItemData(hEndpointName, pos,
+ static_cast<LONG_PTR>(i));
+ }
}
}
- Edit_LimitText(hEndpointName, ENDPOINT_NAME_MAX_LEN);
- if (GetDlgCtrlID(reinterpret_cast<HWND>(wParam)) !=
- IDC_ENDPOINT_NAME)
- {
- SetFocus(hEndpointName);
- return FALSE;
+ if (isEdit) {
+ int pos = ComboBox_FindStringExact(hEndpointName, -1,
+ ctx->data->name.c_str());
+ if (pos == CB_ERR) {
+ // The entry refers to a device that is not around (or was
+ // stored by name only). Offer it so it can be kept.
+ pos = ComboBox_AddString(hEndpointName,
+ ctx->data->name.c_str());
+ if (pos >= 0) {
+ ComboBox_SetItemData(hEndpointName, pos,
+ ENDPOINT_ITEM_LEGACY_NAME);
+ }
+ }
+ if (pos >= 0) {
+ ComboBox_SetCurSel(hEndpointName, pos);
+ }
}
- return TRUE;
+
+ SetFocus(hEndpointName);
+ return FALSE;
}
case WM_COMMAND:
if (LOWORD(wParam) == IDC_ENDPOINT_NAME) {
- HWND hDevName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME);
- if (HIWORD(wParam) == CBN_EDITUPDATE) {
- const int textLen = Edit_GetTextLength(hDevName);
- EnableWindow(GetDlgItem(hDlg, IDOK), textLen > 0);
- } else if (HIWORD(wParam) == CBN_SELCHANGE) {
- const auto curSelIdx =
- SendMessage(hDevName, CB_GETCURSEL, 0, 0);
- EnableWindow(GetDlgItem(hDlg, IDOK), curSelIdx != CB_ERR);
+ if (HIWORD(wParam) == CBN_SELCHANGE) {
+ const int sel =
+ ComboBox_GetCurSel(GetDlgItem(hDlg, IDC_ENDPOINT_NAME));
+ EnableWindow(GetDlgItem(hDlg, IDOK), sel != CB_ERR);
}
} else if (LOWORD(wParam) == IDOK) {
HWND hEpName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME);
- const int textLen = Edit_GetTextLength(hEpName);
- if (textLen != 0) {
- wchar_t epNameBuf[ENDPOINT_NAME_MAX_LEN + 1] = {'L\0'};
- EndpointData* epData = reinterpret_cast<EndpointData*>(
- GetWindowLongPtr(hDlg, DWLP_USER));
- if (epData != nullptr) {
- Edit_GetText(hEpName, epNameBuf, ARRAY_SIZE(epNameBuf));
- epData->name = epNameBuf;
- EndDialog(hDlg, 0);
- } else {
- EndDialog(hDlg, 1);
+ EndpointAddCtx* ctx = reinterpret_cast<EndpointAddCtx*>(
+ GetWindowLongPtr(hDlg, DWLP_USER));
+ const int sel = ComboBox_GetCurSel(hEpName);
+ if (ctx == nullptr || ctx->data == nullptr || sel == CB_ERR) {
+ EndDialog(hDlg, 1);
+ return FALSE;
+ }
+ const LONG_PTR item =
+ static_cast<LONG_PTR>(ComboBox_GetItemData(hEpName, sel));
+ if (item != ENDPOINT_ITEM_LEGACY_NAME &&
+ static_cast<size_t>(item) < ctx->candidates.size())
+ {
+ ctx->data->name = ctx->candidates[item].name;
+ ctx->data->id = ctx->candidates[item].id;
+ } else {
+ // Kept an entry whose device is not currently around, so
+ // there is no id to bind; it stays matched by name.
+ const int len = ComboBox_GetLBTextLen(hEpName, sel);
+ if (len > 0) {
+ std::wstring name(static_cast<size_t>(len), L'\0');
+ ComboBox_GetLBText(hEpName, sel, name.data());
+ ctx->data->name = name;
}
+ ctx->data->id.clear();
}
+ EndDialog(hDlg, 0);
} else if (LOWORD(wParam) == IDCANCEL) {
EndDialog(hDlg, 1);
}
return FALSE;
}
-static std::vector<std::wstring> ExportEndpointNameList(HWND hList)
-{
- std::vector<std::wstring> items;
- DWORD itemCount = ListBox_GetCount(hList);
- for (DWORD i = 0; i < itemCount; ++i) {
- wchar_t textBuf[ENDPOINT_NAME_MAX_LEN + 1] = {0};
- DWORD textLen = ListBox_GetTextLen(hList, i);
- if (textLen < ARRAY_SIZE(textBuf)) {
- ListBox_GetText(hList, i, textBuf);
- items.push_back(textBuf);
- }
- }
- return items;
-}
-
static void LoadManageEndpointsDlgTranslation(HWND hDlg)
{
WMi18n& i18n = WMi18n::GetInstance();
WMSettings* settings = reinterpret_cast<WMSettings*>(lParam);
assert(settings != nullptr);
+ auto* state = new ManageEndpointsDlgState();
+ state->settings = settings;
SetWindowLongPtr(hDlg, DWLP_USER,
- reinterpret_cast<LONG_PTR>(settings));
+ reinterpret_cast<LONG_PTR>(state));
DWORD endpointMode = settings->QueryValue(
SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS_MODE);
}
HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST);
- const auto devices = settings->GetManagedAudioEndpoints();
- for (const auto& dev : devices) {
- ListBox_AddString(hList, dev.c_str());
- }
+ state->entries = settings->GetManagedAudioEndpoints();
+ RebuildEndpointList(hList, state->entries);
Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL),
ListBox_GetCount(hList) > 0);
return TRUE;
}
+ case WM_NCDESTROY: {
+ delete reinterpret_cast<ManageEndpointsDlgState*>(
+ GetWindowLongPtr(hDlg, DWLP_USER));
+ SetWindowLongPtr(hDlg, DWLP_USER, 0);
+ break;
+ }
case WM_COMMAND: {
+ auto* state = reinterpret_cast<ManageEndpointsDlgState*>(
+ GetWindowLongPtr(hDlg, DWLP_USER));
+ if (state == nullptr) {
+ return FALSE;
+ }
if (LOWORD(wParam) == IDC_ENDPOINT_LIST) {
HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST);
if (HIWORD(wParam) == LBN_SELCHANGE ||
entrySelected);
}
} else if (LOWORD(wParam) == IDC_ENDPOINT_ADD) {
- EndpointData epData;
+ ManagedEndpoint epData;
+ EndpointAddCtx ctx;
+ ctx.data = &epData;
if (DialogBoxParam(nullptr,
MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD),
hDlg, Settings_EndpointAddDlgProc,
- reinterpret_cast<LPARAM>(&epData)) == 0)
+ reinterpret_cast<LPARAM>(&ctx)) == 0)
{
- std::vector<std::wstring> devices = ExportEndpointNameList(
- GetDlgItem(hDlg, IDC_ENDPOINT_LIST));
- if (std::find(begin(devices), end(devices), epData.name) ==
- end(devices))
- {
- ListBox_AddString(GetDlgItem(hDlg, IDC_ENDPOINT_LIST),
- epData.name.c_str());
+ const bool duplicate = std::any_of(
+ state->entries.begin(), state->entries.end(),
+ [&epData](const ManagedEndpoint& e) {
+ return e.name == epData.name;
+ });
+ if (!duplicate) {
+ state->entries.push_back(epData);
+ RebuildEndpointList(GetDlgItem(hDlg, IDC_ENDPOINT_LIST),
+ state->entries);
HWND hRemoveAll =
GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL);
if (!IsWindowEnabled(hRemoveAll)) {
}
} else if (LOWORD(wParam) == IDC_ENDPOINT_EDIT) {
HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST);
- const WPARAM sel = ListBox_GetCurSel(hList);
- if (sel != LB_ERR) {
- const int len = ListBox_GetTextLen(hList, sel);
- wchar_t* textBuf = nullptr;
- if (len != LB_ERR) {
- if ((textBuf =
- new wchar_t[static_cast<size_t>(len) + 1]) !=
- nullptr)
- {
- ListBox_GetText(hList, sel, textBuf);
-
- EndpointData epData;
- epData.name = textBuf;
- delete[] textBuf;
-
- if (DialogBoxParam(
- nullptr,
- MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD),
- hDlg, Settings_EndpointAddDlgProc,
- reinterpret_cast<LPARAM>(&epData)) == 0)
+ const int sel = ListBox_GetCurSel(hList);
+ if (sel != LB_ERR &&
+ static_cast<size_t>(sel) < state->entries.size())
+ {
+ ManagedEndpoint epData = state->entries[sel];
+
+ EndpointAddCtx ctx;
+ ctx.data = &epData;
+ if (DialogBoxParam(
+ nullptr, MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD),
+ hDlg, Settings_EndpointAddDlgProc,
+ reinterpret_cast<LPARAM>(&ctx)) == 0)
+ {
+ // If the edit turned this entry into a duplicate of
+ // another one, drop it instead of keeping both.
+ bool duplicate = false;
+ for (size_t i = 0; i < state->entries.size(); ++i) {
+ if (static_cast<int>(i) != sel &&
+ state->entries[i].name == epData.name)
{
- std::vector<std::wstring> networks =
- ExportEndpointNameList(
- GetDlgItem(hDlg, IDC_WIFI_LIST));
- if (std::find(begin(networks), end(networks),
- epData.name) == end(networks))
- {
- ListBox_InsertString(hList, sel,
- epData.name.c_str());
- ListBox_DeleteString(hList, sel + 1);
- } else {
- ListBox_DeleteString(hList, sel);
- }
+ duplicate = true;
+ break;
}
}
+ if (duplicate) {
+ state->entries.erase(state->entries.begin() + sel);
+ } else {
+ state->entries[sel] = epData;
+ }
+ // Rebuilding drops the selection, so restore it.
+ RebuildEndpointList(hList, state->entries);
+ if (static_cast<size_t>(sel) < state->entries.size()) {
+ ListBox_SetCurSel(hList, sel);
+ } else {
+ Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT),
+ FALSE);
+ Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE),
+ FALSE);
+ }
}
}
} else if (LOWORD(wParam) == IDC_ENDPOINT_REMOVE) {
HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST);
- const WPARAM sel = ListBox_GetCurSel(hList);
- if (sel != LB_ERR) {
- ListBox_DeleteString(hList, sel);
+ const int sel = ListBox_GetCurSel(hList);
+ if (sel != LB_ERR &&
+ static_cast<size_t>(sel) < state->entries.size())
+ {
+ state->entries.erase(state->entries.begin() + sel);
+ // Nothing is selected after the rebuild.
+ RebuildEndpointList(hList, state->entries);
+ Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), FALSE);
+ Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), FALSE);
if (ListBox_GetCount(hList) == 0) {
- Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT),
- FALSE);
- Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE),
- FALSE);
Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL),
FALSE);
}
}
} else if (LOWORD(wParam) == IDC_ENDPOINT_REMOVEALL) {
+ state->entries.clear();
ListBox_ResetContent(GetDlgItem(hDlg, IDC_ENDPOINT_LIST));
Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), FALSE);
Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), FALSE);
Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL), FALSE);
} else if (LOWORD(wParam) == IDOK) {
- WMSettings* settings = reinterpret_cast<WMSettings*>(
- GetWindowLongPtr(hDlg, DWLP_USER));
- std::vector<std::wstring> devices =
- ExportEndpointNameList(GetDlgItem(hDlg, IDC_ENDPOINT_LIST));
- settings->StoreManagedAudioEndpoints(devices);
+ WMSettings* settings = state->settings;
+ settings->StoreManagedAudioEndpoints(state->entries);
if (Button_GetCheck(
GetDlgItem(hDlg, IDC_ENDPOINT_LIST_IS_ALLOWLIST)))
{
return id;
}
+bool EnumerateAudioEndpoints(std::vector<ManagedEndpoint>& endpoints)
+{
+ WMLog& log = WMLog::GetInstance();
+
+ CComPtr<IMMDeviceEnumerator> deviceEnumerator;
+ if (FAILED(deviceEnumerator.CoCreateInstance(
+ __uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER)))
+ {
+ return false;
+ }
+ // Must match the states VistaAudio manages, otherwise endpoints that are
+ // currently unplugged (a sleeping monitor's HDMI/DisplayPort audio) would
+ // be invisible here.
+ CComPtr<IMMDeviceCollection> audioEndpoints;
+ if (FAILED(deviceEnumerator->EnumAudioEndpoints(
+ eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED,
+ &audioEndpoints)))
+ {
+ return false;
+ }
+
+ UINT epCount = 0;
+ if (FAILED(audioEndpoints->GetCount(&epCount))) {
+ return false;
+ }
+
+ for (UINT i = 0; i < epCount; ++i) {
+ CComPtr<IMMDevice> device = nullptr;
+ if (FAILED(audioEndpoints->Item(i, &device))) {
+ log.LogError(L"Failed to get audio endpoint #{}", i);
+ continue;
+ }
+ const auto deviceName = GetAudioDeviceName(device);
+ const auto deviceId = GetAudioDeviceId(device);
+ if (deviceName && deviceId) {
+ endpoints.push_back({*deviceId, *deviceName});
+ }
+ }
+
+ return true;
+}
+
// =========================================================================
// String Conversion
// =========================================================================
std::optional<std::wstring> GetAudioDeviceId(const CComPtr<IMMDevice>& devicePtr);
+// Lists every render endpoint WinMute is willing to manage, with both its id
+// and its friendly name. Requires an initialized COM apartment.
+bool EnumerateAudioEndpoints(std::vector<ManagedEndpoint>& endpoints);
+
// =============================================================================
// COM Helper
template <class Interface>
}
bool anyManaged = false;
for (auto& e : endpoints_) {
- if (!IsEndpointManaged(e->deviceName)) {
+ if (!IsEndpointManaged(*e)) {
continue;
}
anyManaged = true;
std::set<std::wstring> presentIds;
for (auto& e : endpoints_) {
presentIds.insert(e->deviceId);
- if (!IsEndpointManaged(e->deviceName)) {
+ if (!IsEndpointManaged(*e)) {
log.LogInfo(L"Skipping Endpoint {}", e->deviceName);
continue;
}
if (pendingRestoreIds_.count(e->deviceId) == 0) {
continue;
}
- if (!IsEndpointManaged(e->deviceName)) {
+ if (!IsEndpointManaged(*e)) {
log.LogInfo(L"Skipping Endpoint {}", e->deviceName);
pendingRestoreIds_.erase(e->deviceId);
continue;
if (CheckForReInit()) {
for (auto& e : endpoints_) {
BOOL isMuted = !mute;
- if (!IsEndpointManaged(e->deviceName)) {
+ if (!IsEndpointManaged(*e)) {
log.LogInfo(L"Skipping Endpoint {}", e->deviceName);
continue;
}
}
}
-bool VistaAudio::IsEndpointManaged(const std::wstring& endpointName) const
+bool VistaAudio::IsEndpointManaged(const Endpoint& ep) const
{
if (!muteSpecificEndpoints_) {
return true;
}
+ // An entry that carries an id identifies exactly one device, so the
+ // friendly name is ignored for it.
const bool inManagedEndpoints =
- std::find(std::begin(managedEndpointNames_),
- std::end(managedEndpointNames_),
- endpointName) != std::end(managedEndpointNames_);
+ std::any_of(std::begin(managedEndpoints_), std::end(managedEndpoints_),
+ [&ep](const ManagedEndpoint& managed) {
+ return managed.id.empty()
+ ? managed.name == ep.deviceName
+ : managed.id == ep.deviceId;
+ });
// ------------+----------+-------------+
// | In List | Not in List |
muteSpecificEndpoints_ = muteSpecific;
}
-void VistaAudio::SetManagedEndpoints(const std::vector<std::wstring>& endpoints,
- bool isAllowList)
+void VistaAudio::SetManagedEndpoints(
+ const std::vector<ManagedEndpoint>& endpoints, bool isAllowList)
{
- managedEndpointNames_ = endpoints;
+ managedEndpoints_ = endpoints;
muteSpecificEndpointsAllowList_ = isAllowList;
}
case SettingsKey::CHECK_FOR_BETA_UPDATE:
keyStr = L"CheckForBetaUpdate";
break;
+ case SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED:
+ keyStr = L"ManagedEndpointsIdMigrated";
+ break;
case SettingsKey::ENABLE_GLOBAL_MUTE_HOTKEY:
keyStr = L"EnableGlobalMuteHotkey";
break;
return 0;
case SettingsKey::CHECK_FOR_BETA_UPDATE:
return 0;
+ case SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED:
+ return 0;
}
return 0;
}
+// Serialized form of a list entry: "<endpoint id>|<friendly name>", or just
+// "<friendly name>" when no id is bound. Endpoint ids always start with '{'
+// (e.g. "{0.0.0.00000000}.{9f81ed6c-...}"), which is what tells the two apart
+// -- a friendly name may well contain a '|', so the separator alone is not a
+// reliable discriminator.
+static std::wstring SerializeManagedEndpoint(const ManagedEndpoint& ep)
+{
+ return ep.id.empty() ? ep.name : ep.id + L"|" + ep.name;
+}
+
+static ManagedEndpoint ParseManagedEndpoint(const std::wstring& stored)
+{
+ if (!stored.empty() && stored.front() == L'{') {
+ const size_t sep = stored.find(L'|');
+ if (sep != std::wstring::npos) {
+ return {stored.substr(0, sep), stored.substr(sep + 1)};
+ }
+ }
+ return {L"", stored};
+}
+
+static void NormalizeEndpointList(std::vector<ManagedEndpoint>& items)
+{
+ if (items.size() > 1) {
+ std::sort(std::begin(items), std::end(items));
+ auto it = std::unique(std::begin(items), std::end(items));
+ items.resize(std::distance(std::begin(items), it));
+ }
+}
+
template <typename T>
static void NormalizeStringList(std::vector<std::basic_string<T>>& items)
{
}
bool WMSettings::StoreManagedAudioEndpoints(
- std::vector<std::wstring>& endpoints)
+ std::vector<ManagedEndpoint>& endpoints)
{
// Clear all stored keys
for (;;) {
}
}
- NormalizeStringList(endpoints);
+ NormalizeEndpointList(endpoints);
for (size_t i = 0; i < endpoints.size(); ++i) {
wchar_t valueName[25];
StringCchPrintfW(valueName, ARRAY_SIZE(valueName), L"Endpoint %03zu",
i + 1);
- const std::wstring& v = endpoints[i];
+ const std::wstring v = SerializeManagedEndpoint(endpoints[i]);
DWORD regError =
RegSetValueEx(hAudioEndpointsKey_, valueName, 0, REG_SZ,
reinterpret_cast<const BYTE*>(v.c_str()),
return true;
}
-std::vector<std::wstring> WMSettings::GetManagedAudioEndpoints() const
+std::vector<ManagedEndpoint> WMSettings::GetManagedAudioEndpoints() const
{
- std::vector<std::wstring> devices;
+ std::vector<ManagedEndpoint> devices;
for (int valIdx = 0;; ++valIdx) {
wchar_t valueName[260] = {0};
- wchar_t dataBuf[260] = {0};
+ // Holds "<endpoint id>|<friendly name>": an endpoint id runs to about
+ // 55 characters and the name is capped at ENDPOINT_NAME_MAX_LEN (200),
+ wchar_t dataBuf[512] = {0};
DWORD valueSize = ARRAY_SIZE(valueName);
DWORD valType = 0;
DWORD dataLen = sizeof(dataBuf) - sizeof(wchar_t);
ShowWindowsError(L"RegEnumValue", regError);
return {};
} else {
- devices.push_back(dataBuf);
+ devices.push_back(ParseManagedEndpoint(dataBuf));
}
}
- NormalizeStringList(devices);
+ NormalizeEndpointList(devices);
return devices;
}
+
+void WMSettings::MigrateManagedEndpointIds()
+{
+ WMLog& log = WMLog::GetInstance();
+
+ if (QueryValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED) != 0) {
+ return;
+ }
+
+ auto endpoints = GetManagedAudioEndpoints();
+ const bool anyUnbound =
+ std::any_of(endpoints.begin(), endpoints.end(),
+ [](const ManagedEndpoint& ep) { return ep.id.empty(); });
+ if (!anyUnbound) {
+ SetValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED, 1);
+ return;
+ }
+
+ std::vector<ManagedEndpoint> present;
+ if (!EnumerateAudioEndpoints(present)) {
+ log.LogError(
+ L"Could not enumerate audio endpoints;"
+ L" postponing endpoint id migration");
+ return;
+ }
+
+ int bound = 0;
+ for (auto& ep : endpoints) {
+ if (!ep.id.empty()) {
+ continue;
+ }
+ const auto match =
+ std::find_if(present.begin(), present.end(),
+ [&ep](const ManagedEndpoint& dev) {
+ return dev.name == ep.name;
+ });
+ if (match != present.end()) {
+ ep.id = match->id;
+ ++bound;
+ log.LogInfo(L"Bound managed endpoint \"{}\" to id {}", ep.name,
+ ep.id);
+ } else {
+ log.LogInfo(
+ L"Managed endpoint \"{}\" is not present;"
+ L" it stays matched by name",
+ ep.name);
+ }
+ }
+
+ if (bound == 0 || StoreManagedAudioEndpoints(endpoints)) {
+ SetValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED, 1);
+ log.LogInfo(L"Endpoint id migration finished ({} of {} entries bound)",
+ bound, endpoints.size());
+ }
+}
CHECK_FOR_UPDATE,
CHECK_FOR_BETA_UPDATE,
ENABLE_GLOBAL_MUTE_HOTKEY,
- GLOBAL_MUTE_HOTKEY
+ GLOBAL_MUTE_HOTKEY,
+ // Set once the one-time "resolve stored endpoint names to endpoint ids"
+ // pass has run. This is deliberately not folded into SETTINGS_VERSION:
+ // MigrateSettings() runs from WMSettings::Init(), which happens before
+ // CoInitializeEx(), so it cannot enumerate audio devices.
+ MANAGED_ENDPOINTS_ID_MIGRATED
};
class WMSettings {
bool StoreBluetoothDevices(std::vector<std::wstring>& networks);
std::vector<std::wstring> GetBluetoothDevices() const;
- bool StoreManagedAudioEndpoints(std::vector<std::wstring>& endpoints);
- std::vector<std::wstring> GetManagedAudioEndpoints() const;
+ bool StoreManagedAudioEndpoints(std::vector<ManagedEndpoint>& endpoints);
+ std::vector<ManagedEndpoint> GetManagedAudioEndpoints() const;
+
+ // Binds stored name-only entries to the endpoint id of a device with that
+ // name, for every such device present right now. Requires COM, so it must
+ // be called after CoInitializeEx. Runs at most once per installation.
+ void MigrateManagedEndpointIds();
bool StoreQuietHoursTimes(
const std::vector<std::pair<DWORD, DWORD>>& times);
virtual void RestoreArrivedEndpoints() = 0;
virtual void SetMute(bool mute) = 0;
virtual void MuteSpecificEndpoints(bool muteSpecific) = 0;
- virtual void SetManagedEndpoints(const std::vector<std::wstring>& endpoints,
- bool isAllowList) = 0;
+ virtual void SetManagedEndpoints(
+ const std::vector<ManagedEndpoint>& endpoints, bool isAllowList) = 0;
virtual ~WinAudio() noexcept {};
};
void SetMute(bool mute) override;
void MuteSpecificEndpoints(bool muteSpecific) override;
- void SetManagedEndpoints(const std::vector<std::wstring>& endpoints,
+ void SetManagedEndpoints(const std::vector<ManagedEndpoint>& endpoints,
bool isAllowList) override;
private:
bool CheckForReInit();
bool LoadAllEndpoints();
- bool IsEndpointManaged(const std::wstring& endpointName) const;
+ bool IsEndpointManaged(const Endpoint& ep) const;
bool RestoreEndpoint(const Endpoint& ep, bool wasMuted);
std::vector<std::unique_ptr<Endpoint>> endpoints_;
bool muteSpecificEndpointsAllowList_;
HWND hParent_;
- std::vector<std::wstring> managedEndpointNames_;
+ std::vector<ManagedEndpoint> managedEndpoints_;
// non copy-able
VistaAudio(const VistaAudio& other) = delete;
if (!settings_.QueryValue(SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS)) {
muteCtrl_.ClearManagedEndpoints();
} else {
+ // Not done in WMSettings::Init: this needs COM, which is only
+ // initialized once WinMute itself starts up.
+ settings_.MigrateManagedEndpointIds();
const auto endpoints = settings_.GetManagedAudioEndpoints();
const bool isAllowList =
settings_.QueryValue(SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS_MODE) ==
i18n_.GetTranslationW("traymenu.mute-on-logout");
menuText[ID_TRAYMENU_MUTE] =
i18n_.GetTranslationW("traymenu.mute-all-devices");
+ menuText[ID_TRAYMENU_SHOWLOG] =
+ i18n_.GetTranslationW("traymenu.show-log");
menuText[ID_TRAYMENU_SETTINGS] = i18n_.GetTranslationW("traymenu.settings");
menuText[ID_TRAYMENU_EXIT] = i18n_.GetTranslationW("traymenu.exit");
// TEXTINCLUDE
//
-1 TEXTINCLUDE
+1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
-2 TEXTINCLUDE
+2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
-3 TEXTINCLUDE
+3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
PUSHBUTTON "Cancel",IDCANCEL,144,41,50,14
END
-IDD_MANAGE_ENDPOINTS DIALOGEX 0, 0, 241, 200
+IDD_MANAGE_ENDPOINTS DIALOGEX 0, 0, 246, 201
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Manage Endpoints"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
- GROUPBOX "List behaviour",IDC_GROUP_LIST_BEHAVIOUR,7,7,227,41
+ GROUPBOX "List behaviour",IDC_GROUP_LIST_BEHAVIOUR,7,7,232,41
CONTROL "Mute only listed endpoints",IDC_ENDPOINT_LIST_IS_ALLOWLIST,
"Button",BS_AUTORADIOBUTTON,13,19,209,10
CONTROL "Mute all but the listed endpoints",IDC_ENDPOINT_LIST_IS_BLOCKLIST,
"Button",BS_AUTORADIOBUTTON,13,32,207,10
- GROUPBOX "Endpoints",IDC_GROUP_ENDPOINTS,7,48,227,128
+ GROUPBOX "Endpoints",IDC_GROUP_ENDPOINTS,7,48,232,128
LISTBOX IDC_ENDPOINT_LIST,13,60,156,110,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
- PUSHBUTTON "Add",IDC_ENDPOINT_ADD,175,60,59,16
- PUSHBUTTON "Edit",IDC_ENDPOINT_EDIT,175,78,59,16
- PUSHBUTTON "Remove",IDC_ENDPOINT_REMOVE,175,97,59,16
- PUSHBUTTON "Remove all",IDC_ENDPOINT_REMOVEALL,175,155,59,16
- DEFPUSHBUTTON "Save",IDOK,133,179,50,14
- PUSHBUTTON "Cancel",IDCANCEL,184,179,50,14
+ PUSHBUTTON "Add",IDC_ENDPOINT_ADD,176,60,59,16
+ PUSHBUTTON "Edit",IDC_ENDPOINT_EDIT,176,78,59,16
+ PUSHBUTTON "Remove",IDC_ENDPOINT_REMOVE,176,97,59,16
+ PUSHBUTTON "Remove all",IDC_ENDPOINT_REMOVEALL,176,155,59,16
+ DEFPUSHBUTTON "Save",IDOK,133,180,50,14
+ PUSHBUTTON "Cancel",IDCANCEL,189,180,50,14
END
IDD_MANAGE_ENDPOINTS_ADD DIALOGEX 0, 0, 201, 62
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "Endpoint name:",IDC_ENDPOINT_NAME_LABEL,7,7,187,14
- COMBOBOX IDC_ENDPOINT_NAME,7,22,187,17,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP
+ COMBOBOX IDC_ENDPOINT_NAME,7,22,187,17,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP
DEFPUSHBUTTON "Save",IDOK,91,41,50,14
PUSHBUTTON "Cancel",IDCANCEL,144,41,50,14
END
IDD_MANAGE_ENDPOINTS, DIALOG
BEGIN
LEFTMARGIN, 7
- RIGHTMARGIN, 234
+ RIGHTMARGIN, 239
TOPMARGIN, 7
- BOTTOMMARGIN, 193
+ BOTTOMMARGIN, 194
END
IDD_MANAGE_ENDPOINTS_ADD, DIALOG
MENUITEM SEPARATOR
MENUITEM "Mute all devices", ID_TRAYMENU_MUTE
MENUITEM SEPARATOR
+ MENUITEM "Show Log...", ID_TRAYMENU_SHOWLOG
+ MENUITEM SEPARATOR
MENUITEM "Settings...", ID_TRAYMENU_SETTINGS
MENUITEM SEPARATOR
MENUITEM "Exit", ID_TRAYMENU_EXIT
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
+
<ClInclude Include="BluetoothDetector.h" />
<ClInclude Include="common.h" />
<ClInclude Include="libs\json.hpp" />
+ <ClInclude Include="ManagedEndpoint.hpp" />
<ClInclude Include="MediaController.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="UpdateChecker.h" />
#include <endpointvolume.h>
#pragma warning(default : 4201)
+#include "ManagedEndpoint.hpp"
+
#include "BluetoothDetector.h"
#include "MediaController.h"
#include "MuteControl.h"
#define ID_TRAYMENU_MUTEONSCREE 40039
#define ID_TRAYMENU_LABEL_MUTEON_NO_RESTORE 40040
#define ID_TRAYMENU_LOG 40041
+#define ID_TRAYMENU_SHOWLOG 40042
+#define ID_TRAYMENU_40043 40043
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 146
-#define _APS_NEXT_COMMAND_VALUE 40042
+#define _APS_NEXT_COMMAND_VALUE 40044
#define _APS_NEXT_CONTROL_VALUE 1083
#define _APS_NEXT_SYMED_VALUE 101
#endif