Release v4.5: import shortcuts on pre-4.4 upgrade + clearer shortcuts dialog
- Pre-v4.4 upgraders are offered a one-time dialog to copy their keyboard shortcuts from one of their profiles (still readable in the profile files) instead of being reset. Users who already went through v4.4's reset are deliberately NOT re-offered (gated on KeyboardShortcutsGlobalNoticeShown). New KeyboardShortcutImportDialog + AppConfig.KeyboardShortcutsImportOffered + MainFormHotkeyController.ReloadAndReRegisterAll. - Keyboard shortcuts dialog: new "Clear this shortcut" button; Delete inside the capture box leaves a shortcut unassigned. - Relabelled the three RemSound-app remote rows to "Send remote RemSound volume/..." to distinguish them from the Windows-global ones. - Manual (readme.html + MANUAL.md), About changelog, RELEASE_NOTES updated; version 4.5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a1bc0c813
commit
962f35ab80
@@ -20,6 +20,26 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v4.5
|
||||
|
||||
Keyboard shortcuts get easier to manage.
|
||||
|
||||
If you're updating from before version 4.4,
|
||||
RemSound now offers to bring your shortcuts
|
||||
across from one of your profiles instead of
|
||||
resetting them — pick the profile you set them
|
||||
up in, or start fresh.
|
||||
|
||||
The Keyboard shortcuts dialog has a new "Clear
|
||||
this shortcut" button, and when you're setting a
|
||||
shortcut you can press Delete to leave it
|
||||
unassigned.
|
||||
|
||||
And the three remote-control rows now name
|
||||
RemSound — "Send remote RemSound volume", and so
|
||||
on — so they're easy to tell apart from the
|
||||
Windows-volume ones.
|
||||
|
||||
RemSound v4.4
|
||||
|
||||
Keyboard shortcuts are now shared across all
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using RemSound.Core;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// One-time upgrade dialog: keyboard shortcuts moved from per-profile to machine-wide storage, so the
|
||||
/// user is offered the choice of copying their shortcut set from one of their existing profiles (whose
|
||||
/// old per-profile shortcuts are still readable in the profile files) — or starting fresh at the
|
||||
/// defaults. Accessible: a profile list plus two clearly-labelled buttons, focus landing on the list.
|
||||
/// </summary>
|
||||
internal sealed class KeyboardShortcutImportDialog : Form
|
||||
{
|
||||
private readonly ListBox profileList = new()
|
||||
{
|
||||
IntegralHeight = false,
|
||||
Width = 400,
|
||||
Height = 150,
|
||||
AccessibleName = "Profile to copy keyboard shortcuts from",
|
||||
TabIndex = 0,
|
||||
};
|
||||
|
||||
/// <summary>The profile the user chose to copy shortcuts from, or null if they chose to start fresh.
|
||||
/// Only meaningful when <see cref="Form.ShowDialog()"/> returned <see cref="DialogResult.OK"/>.</summary>
|
||||
public string? ChosenProfileTitle { get; private set; }
|
||||
|
||||
public KeyboardShortcutImportDialog(IReadOnlyList<string> profileTitles)
|
||||
{
|
||||
Text = "Keyboard shortcuts";
|
||||
AccessibleName = "Bring your keyboard shortcuts across";
|
||||
AccessibleRole = AccessibleRole.Dialog;
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MinimizeBox = false;
|
||||
MaximizeBox = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
KeyPreview = true;
|
||||
ClientSize = new Size(470, 330);
|
||||
|
||||
var info = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(440, 0),
|
||||
Text = "Keyboard shortcuts are now shared across all your profiles, instead of being saved "
|
||||
+ "separately in each one.\n\n"
|
||||
+ "You can copy your shortcuts from one of your existing profiles, or start fresh with "
|
||||
+ "the defaults. Pick a profile below, then choose what to do.",
|
||||
};
|
||||
|
||||
var listLabel = new MnemonicLabel
|
||||
{
|
||||
Text = "Copy shortcuts from this &profile:",
|
||||
AutoSize = true,
|
||||
Anchor = AnchorStyles.Left,
|
||||
MnemonicTarget = profileList,
|
||||
};
|
||||
|
||||
foreach (var t in profileTitles) profileList.Items.Add(t);
|
||||
if (profileList.Items.Count > 0) profileList.SelectedIndex = 0;
|
||||
|
||||
var useButton = new Button
|
||||
{
|
||||
Text = "&Use the shortcuts from this profile",
|
||||
AccessibleName = "Use the shortcuts from this profile",
|
||||
AutoSize = true,
|
||||
TabIndex = 1,
|
||||
};
|
||||
useButton.Click += (_, _) =>
|
||||
{
|
||||
if (profileList.SelectedItem is string title)
|
||||
{
|
||||
ChosenProfileTitle = title;
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
};
|
||||
|
||||
var freshButton = new Button
|
||||
{
|
||||
Text = "Start &fresh with the defaults",
|
||||
AccessibleName = "Start fresh with the defaults",
|
||||
AutoSize = true,
|
||||
TabIndex = 2,
|
||||
};
|
||||
freshButton.Click += (_, _) =>
|
||||
{
|
||||
ChosenProfileTitle = null;
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
};
|
||||
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 0),
|
||||
};
|
||||
buttons.Controls.Add(useButton);
|
||||
buttons.Controls.Add(freshButton);
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = 4,
|
||||
};
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.Controls.Add(info, 0, 0);
|
||||
layout.Controls.Add(listLabel, 0, 1);
|
||||
layout.Controls.Add(profileList, 0, 2);
|
||||
layout.Controls.Add(buttons, 0, 3);
|
||||
Controls.Add(layout);
|
||||
|
||||
AcceptButton = useButton;
|
||||
|
||||
// Escape leaves the choice unmade — the caller treats a non-OK result as "ask again next launch",
|
||||
// so the user is never forced into a decision they didn't mean to make.
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape) { DialogResult = DialogResult.Cancel; Close(); e.Handled = true; e.SuppressKeyPress = true; }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
// Land focus on the profile list so NVDA announces the dialog and the first profile.
|
||||
ActiveControl = null;
|
||||
profileList.Focus();
|
||||
WinEventNotifier.NotifyFocus(profileList);
|
||||
}
|
||||
}
|
||||
+102
-37
@@ -1381,7 +1381,7 @@ public sealed class MainForm : Form
|
||||
private void RunStartupNotices()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
MaybeShowKeyboardShortcutsGlobalNotice();
|
||||
MaybeOfferKeyboardShortcutImport();
|
||||
if (IsDisposed) return;
|
||||
MaybeShowWhatsNewAfterUpdate();
|
||||
if (IsDisposed) return;
|
||||
@@ -1433,53 +1433,118 @@ public sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One-time notice (v4.4) telling upgraders that keyboard shortcuts have moved from
|
||||
/// per-profile to machine-wide storage (issue #14), so their shortcuts have reset to defaults and
|
||||
/// need re-setting. Shown once to anyone who ran an earlier version; on a brand-new install (which
|
||||
/// has nothing to reset) it's silently marked done and never shown. Must run BEFORE
|
||||
/// <see cref="MaybeShowWhatsNewAfterUpdate"/>, which overwrites the LastWhatsNewVersion we read to
|
||||
/// tell upgraders apart from fresh installs.</summary>
|
||||
private void MaybeShowKeyboardShortcutsGlobalNotice()
|
||||
/// <summary>One-time upgrade flow (replaces the v4.4 reset notice): keyboard shortcuts moved from
|
||||
/// per-profile to machine-wide storage (issue #14). Offers upgraders the choice of copying their
|
||||
/// shortcuts from one of their existing profiles (still readable in the profile files) or starting
|
||||
/// fresh. Only offered to people coming straight from a PRE-v4.4 version (where shortcuts were still
|
||||
/// per-profile), to spare them the reset — anyone who already went through v4.4's reset is left
|
||||
/// alone (re-offering would only annoy them), as is a fresh install or a user with no saved shortcuts
|
||||
/// to import. Runs BEFORE <see cref="MaybeShowWhatsNewAfterUpdate"/>, which overwrites the
|
||||
/// LastWhatsNewVersion we read to tell upgraders apart from fresh installs.</summary>
|
||||
private void MaybeOfferKeyboardShortcutImport()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
AppConfig cfg;
|
||||
try { cfg = AppConfig.Load(); }
|
||||
catch { return; }
|
||||
if (cfg.KeyboardShortcutsGlobalNoticeShown) return;
|
||||
if (cfg.KeyboardShortcutsImportOffered) return;
|
||||
|
||||
// A non-empty LastWhatsNewVersion means a previous version has run on this machine — i.e. this
|
||||
// is an upgrade, so there were per-profile shortcuts that have now reset. A fresh install has it
|
||||
// empty (it's set later, by MaybeShowWhatsNewAfterUpdate) and has nothing to reset.
|
||||
// Leave the v4.4 crowd alone. v4.4's reset set KeyboardShortcutsGlobalNoticeShown for everyone
|
||||
// who ran it; those people have already re-done their shortcuts, so re-offering an import would
|
||||
// only annoy them. We only want to catch people coming straight from a PRE-v4.4 version (where
|
||||
// shortcuts were still per-profile), before they lose anything.
|
||||
if (cfg.KeyboardShortcutsGlobalNoticeShown) { MarkShortcutImportOffered(); return; }
|
||||
|
||||
// Only relevant to upgraders (a previous version ran here, so LastWhatsNewVersion is set) who
|
||||
// actually have old per-profile shortcuts to bring across.
|
||||
var isUpgrade = !string.IsNullOrEmpty(cfg.LastWhatsNewVersion);
|
||||
if (isUpgrade)
|
||||
{
|
||||
logFile.Event("keyboard shortcuts: showing one-time 'now shared across profiles' notice");
|
||||
var page = new TaskDialogPage
|
||||
{
|
||||
Caption = "RemSound",
|
||||
Heading = "Your keyboard shortcuts are now shared across profiles",
|
||||
Text = "Keyboard shortcuts used to be saved separately for each profile, so a shortcut you set on one "
|
||||
+ "profile wouldn't work on another. From this version they're shared across all your profiles "
|
||||
+ "instead — one set for the whole app, which our users have requested.\n\n"
|
||||
+ "Because of this change, your shortcuts have started fresh at their defaults. If you'd set up any "
|
||||
+ "shortcuts of your own, please set them again in Options → Keyboard shortcuts (Ctrl+K). You only "
|
||||
+ "need to do this once — from now on they'll stay put whatever profile you're on.",
|
||||
Icon = TaskDialogIcon.Information,
|
||||
Buttons = { TaskDialogButton.OK },
|
||||
AllowCancel = true,
|
||||
};
|
||||
try { ForegroundDialog.Show(owner => TaskDialog.ShowDialog(owner, page)); }
|
||||
catch (Exception ex) { logFile.Event($"keyboard shortcuts notice failed: {ex.GetType().Name}: {ex.Message}"); }
|
||||
}
|
||||
if (!isUpgrade) { MarkShortcutImportOffered(); return; }
|
||||
|
||||
// Mark done either way (shown to upgraders, silently to fresh installs) so it's strictly one-time.
|
||||
var titles = ProfilesWithSavedShortcuts();
|
||||
if (titles.Count == 0) { MarkShortcutImportOffered(); return; }
|
||||
|
||||
logFile.Event($"keyboard shortcuts: offering import from {titles.Count} profile(s) with saved shortcuts");
|
||||
try
|
||||
{
|
||||
var fresh = AppConfig.Load();
|
||||
fresh.KeyboardShortcutsGlobalNoticeShown = true;
|
||||
fresh.Save();
|
||||
using var dlg = new KeyboardShortcutImportDialog(titles);
|
||||
var result = ForegroundDialog.Show(owner => dlg.ShowDialog(owner));
|
||||
if (result != DialogResult.OK) return; // dismissed (Escape) — offer again next launch
|
||||
|
||||
if (dlg.ChosenProfileTitle is { } title)
|
||||
{
|
||||
var imported = ImportShortcutsFromProfile(title);
|
||||
logFile.Event($"keyboard shortcuts: imported {imported} shortcut(s) from profile \"{title}\"");
|
||||
hotkeyController.ReloadAndReRegisterAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
logFile.Event("keyboard shortcuts: user chose to start fresh with the defaults");
|
||||
}
|
||||
MarkShortcutImportOffered();
|
||||
}
|
||||
catch { /* harmless — at worst the notice shows again next launch */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
logFile.Event($"keyboard shortcuts import failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkShortcutImportOffered()
|
||||
{
|
||||
try { var c = AppConfig.Load(); c.KeyboardShortcutsImportOffered = true; c.Save(); }
|
||||
catch { /* harmless — at worst the offer shows again next launch */ }
|
||||
}
|
||||
|
||||
/// <summary>Titles of profiles that have at least one customised keyboard shortcut saved in their
|
||||
/// file (the only ones worth importing — unchanged shortcuts were stored as null).</summary>
|
||||
private List<string> ProfilesWithSavedShortcuts()
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (profileStore is null) return result;
|
||||
try
|
||||
{
|
||||
foreach (var title in profileStore.ListProfileTitles())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (profileStore.Load(title) is { } p && ProfileHasAnyShortcut(p)) result.Add(title);
|
||||
}
|
||||
catch { /* skip an unreadable profile */ }
|
||||
}
|
||||
}
|
||||
catch { /* enumeration failed — offer nothing */ }
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ProfileHasAnyShortcut(Profile p) =>
|
||||
p.ReceiveMuteHotkey is not null || p.SendMuteHotkey is not null || p.TrayHotkey is not null
|
||||
|| p.VolumeUpHotkey is not null || p.VolumeDownHotkey is not null || p.ToggleRecordingHotkey is not null
|
||||
|| p.RemoteVolumeUpHotkey is not null || p.RemoteVolumeDownHotkey is not null || p.RemoteMuteToggleHotkey is not null
|
||||
|| p.SystemVolumeUpHotkey is not null || p.SystemVolumeDownHotkey is not null || p.SystemMuteToggleHotkey is not null
|
||||
|| p.QuickProfileSwitchHotkey is not null || p.SpeakStatusLineHotkey is not null;
|
||||
|
||||
/// <summary>Copy a profile's saved (non-null) keyboard shortcuts into the machine-wide store, via the
|
||||
/// settings store's now-global Save* methods. Returns how many were copied; shortcuts the profile
|
||||
/// never customised (null) are left at the global default.</summary>
|
||||
private int ImportShortcutsFromProfile(string title)
|
||||
{
|
||||
if (profileStore is null || profileStore.Load(title) is not { } p) return 0;
|
||||
var n = 0;
|
||||
void Copy(HotkeyRecord? rec, Action<HotkeyInfo> save) { if (rec is not null) { save(rec.ToHotkeyInfo()); n++; } }
|
||||
Copy(p.ReceiveMuteHotkey, settings.SaveReceiveMuteHotkey);
|
||||
Copy(p.SendMuteHotkey, settings.SaveSendMuteHotkey);
|
||||
Copy(p.TrayHotkey, settings.SaveTrayHotkey);
|
||||
Copy(p.VolumeUpHotkey, settings.SaveVolumeUpHotkey);
|
||||
Copy(p.VolumeDownHotkey, settings.SaveVolumeDownHotkey);
|
||||
Copy(p.ToggleRecordingHotkey, settings.SaveToggleRecordingHotkey);
|
||||
Copy(p.RemoteVolumeUpHotkey, settings.SaveRemoteVolumeUpHotkey);
|
||||
Copy(p.RemoteVolumeDownHotkey, settings.SaveRemoteVolumeDownHotkey);
|
||||
Copy(p.RemoteMuteToggleHotkey, settings.SaveRemoteMuteToggleHotkey);
|
||||
Copy(p.SystemVolumeUpHotkey, settings.SaveSystemVolumeUpHotkey);
|
||||
Copy(p.SystemVolumeDownHotkey, settings.SaveSystemVolumeDownHotkey);
|
||||
Copy(p.SystemMuteToggleHotkey, settings.SaveSystemMuteToggleHotkey);
|
||||
Copy(p.QuickProfileSwitchHotkey, settings.SaveQuickProfileSwitchHotkey);
|
||||
Copy(p.SpeakStatusLineHotkey, settings.SaveSpeakStatusLineHotkey);
|
||||
return n;
|
||||
}
|
||||
|
||||
/// <summary>If the user opted in (<see cref="AppConfig.ShowWhatsNewAfterUpdate"/>) and the
|
||||
|
||||
@@ -174,6 +174,41 @@ internal sealed class MainFormHotkeyController : IDisposable
|
||||
RegisterSpeakStatusLineHotkey();
|
||||
}
|
||||
|
||||
/// <summary>Re-read every hotkey from the (now machine-wide) settings store and re-register it.
|
||||
/// Used after the one-time upgrade import writes a fresh set of shortcuts into the global config,
|
||||
/// so the just-imported bindings take effect immediately without relaunching.</summary>
|
||||
public void ReloadAndReRegisterAll()
|
||||
{
|
||||
sendMuteHotkey = settingsStore.LoadSendMuteHotkey();
|
||||
receiveMuteHotkey = settingsStore.LoadReceiveMuteHotkey();
|
||||
trayHotkey = settingsStore.LoadTrayHotkey();
|
||||
volumeUpHotkey = settingsStore.LoadVolumeUpHotkey();
|
||||
volumeDownHotkey = settingsStore.LoadVolumeDownHotkey();
|
||||
toggleRecordingHotkey = settingsStore.LoadToggleRecordingHotkey();
|
||||
remoteVolumeUpHotkey = settingsStore.LoadRemoteVolumeUpHotkey();
|
||||
remoteVolumeDownHotkey = settingsStore.LoadRemoteVolumeDownHotkey();
|
||||
remoteMuteToggleHotkey = settingsStore.LoadRemoteMuteToggleHotkey();
|
||||
systemVolumeUpHotkey = settingsStore.LoadSystemVolumeUpHotkey();
|
||||
systemVolumeDownHotkey = settingsStore.LoadSystemVolumeDownHotkey();
|
||||
systemMuteToggleHotkey = settingsStore.LoadSystemMuteToggleHotkey();
|
||||
quickProfileSwitchHotkey = settingsStore.LoadQuickProfileSwitchHotkey();
|
||||
speakStatusLineHotkey = settingsStore.LoadSpeakStatusLineHotkey();
|
||||
RegisterSendMuteHotkey();
|
||||
RegisterReceiveMuteHotkey();
|
||||
RegisterTrayHotkey();
|
||||
RegisterVolumeUpHotkey();
|
||||
RegisterVolumeDownHotkey();
|
||||
RegisterToggleRecordingHotkey();
|
||||
RegisterRemoteVolumeUpHotkey();
|
||||
RegisterRemoteVolumeDownHotkey();
|
||||
RegisterRemoteMuteToggleHotkey();
|
||||
RegisterSystemVolumeUpHotkey();
|
||||
RegisterSystemVolumeDownHotkey();
|
||||
RegisterSystemMuteToggleHotkey();
|
||||
RegisterQuickProfileSwitchHotkey();
|
||||
RegisterSpeakStatusLineHotkey();
|
||||
}
|
||||
|
||||
public void ShowKeyboardShortcutsDialog(IWin32Window dialogOwner)
|
||||
{
|
||||
// Modeled on the SpaceBlaster menu dialogs:
|
||||
@@ -224,7 +259,7 @@ internal sealed class MainFormHotkeyController : IDisposable
|
||||
|
||||
var introLabel = new Label
|
||||
{
|
||||
Text = "Arrow up and down to pick a shortcut. Press Enter to rebind it, Del to clear it. Escape closes the dialog.\n\n"
|
||||
Text = "Arrow up and down to pick a shortcut. Press Enter to rebind it, or Del (or the Clear this shortcut button) to clear it. Escape closes the dialog.\n\n"
|
||||
+ "The remote-control rows send commands to connected peers; they only have an effect on peers that have 'Accept remote volume commands from peers' enabled.",
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(600, 0),
|
||||
@@ -249,8 +284,14 @@ internal sealed class MainFormHotkeyController : IDisposable
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 0),
|
||||
};
|
||||
var closeButton = new Button { Text = "Close", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 1 };
|
||||
var closeButton = new Button { Text = "Close", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 };
|
||||
// Discoverable "clear" alongside the Del key — the Del shortcut isn't obvious to a screen-reader
|
||||
// user. Clears whichever shortcut is selected in the list (UnsetSelected reads list.SelectedIndex,
|
||||
// which the ListBox keeps even when focus is on this button).
|
||||
var clearButton = new Button { Text = "&Clear this shortcut", AccessibleName = "Clear this shortcut", AutoSize = true, TabIndex = 1 };
|
||||
clearButton.Click += (_, _) => UnsetSelected();
|
||||
buttonsPanel.Controls.Add(closeButton);
|
||||
buttonsPanel.Controls.Add(clearButton);
|
||||
root.Controls.Add(buttonsPanel, 0, 2);
|
||||
|
||||
dialog.Controls.Add(root);
|
||||
@@ -274,9 +315,9 @@ internal sealed class MainFormHotkeyController : IDisposable
|
||||
list.Items.Add($"Volume up for received sound on this machine: {volumeUpHotkey}");
|
||||
list.Items.Add($"Volume down for received sound on this machine: {volumeDownHotkey}");
|
||||
list.Items.Add($"Start / Stop recording: {toggleRecordingHotkey}");
|
||||
list.Items.Add($"Send remote volume up to peers: {remoteVolumeUpHotkey}");
|
||||
list.Items.Add($"Send remote volume down to peers: {remoteVolumeDownHotkey}");
|
||||
list.Items.Add($"Send remote receive mute toggle to peers: {remoteMuteToggleHotkey}");
|
||||
list.Items.Add($"Send remote RemSound volume up to peers: {remoteVolumeUpHotkey}");
|
||||
list.Items.Add($"Send remote RemSound volume down to peers: {remoteVolumeDownHotkey}");
|
||||
list.Items.Add($"Send remote RemSound receive mute toggle to peers: {remoteMuteToggleHotkey}");
|
||||
list.Items.Add($"Send Windows global volume up to peers: {systemVolumeUpHotkey}");
|
||||
list.Items.Add($"Send Windows global volume down to peers: {systemVolumeDownHotkey}");
|
||||
list.Items.Add($"Send Windows global mute toggle to peers: {systemMuteToggleHotkey}");
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>4.4</Version>
|
||||
<Version>4.5</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user