Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d562157
Feature: Configurable volume step snapping (1-50%)
dantheman4700 Jan 12, 2026
6505bd3
Merge branch 'dev' into feature/volume-step-snapping-dev
dantheman4700 Aug 27, 2026
c23b729
Feature: apply the volume step settings to every volume control
dantheman4700 Aug 27, 2026
9c9764a
Fix Debug build: implement new volume methods on mock audio classes
tanjunior Sep 27, 2026
c6bcfd9
Merge branch 'pr1792' into modernize-win11
VictorGugug Oct 1, 2026
c972edc
Add setting to disable scroll wheel in volume sliders (from #1814)
VictorGugug Oct 1, 2026
4bd94d5
Fix resource doc comment
VictorGugug Oct 1, 2026
330634b
Settings: merge pages into General, Windows 11 style cards with title…
VictorGugug Oct 1, 2026
c33b97a
Win11-style tray context menu; settings opens with page sidebar
VictorGugug Oct 1, 2026
4929c97
Smaller menu clamped above taskbar; accent-tinted settings
VictorGugug Oct 1, 2026
17f7173
Shortcuts page uses settings cards
VictorGugug Oct 1, 2026
c04402b
Developer options: drop dead ColorTool entry, add restore devices, fi…
VictorGugug Oct 1, 2026
76810ff
Tray menu: Fluent icons for non-checked items
VictorGugug Oct 3, 2026
5554d3a
Settings: About page cards, clickable link rows, layout polish
VictorGugug Oct 3, 2026
1bf6e8d
Add translucent windows appearance setting, es/en strings, xunit tests
VictorGugug Oct 3, 2026
03865a2
Return focus to previous window after hotkey flyout (#1751), no blank…
VictorGugug Oct 3, 2026
4f8c59a
Fix apps vanishing when persisted device is unavailable, retry sessio…
VictorGugug Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions EarTrumpet.Tests/EarTrumpet.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<IsPackable>false</IsPackable>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<OutputPath>..\Build\Tests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EarTrumpet\EarTrumpet.csproj" />
</ItemGroup>
</Project>
61 changes: 61 additions & 0 deletions EarTrumpet.Tests/LocalizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Xunit;

namespace EarTrumpet.Tests;

// Guards the "no hardcoded text" rule for the settings UI: every resource XAML points at must exist,
// and the strings this redesign introduced must be translated for the two supported languages.
public class LocalizationTests
{
private static readonly string s_project = FindProjectDir();

private static string FindProjectDir()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "EarTrumpet", "EarTrumpet.csproj")))
{
dir = dir.Parent;
}
return Path.Combine(dir?.FullName ?? throw new InvalidOperationException("repo root not found"), "EarTrumpet");
}

private static string[] Keys(string resx)
=> XDocument.Load(Path.Combine(s_project, "Properties", resx)).Root.Elements("data").Select(e => (string)e.Attribute("name")).ToArray();

private static string[] XamlResourceRefs()
=> Directory.GetFiles(s_project, "*.xaml", SearchOption.AllDirectories)
.Where(f => !f.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar))
.SelectMany(f => Regex.Matches(File.ReadAllText(f), @"resx:Resources\.(\w+)").Select(m => m.Groups[1].Value))
.Distinct().ToArray();

[Fact]
public void EveryXamlResourceExistsInDefaultResx()
{
var keys = Keys("Resources.resx");
Assert.Empty(XamlResourceRefs().Except(keys));
}

[Fact]
public void EveryDesignerPropertyExistsInDefaultResx()
{
var designer = File.ReadAllText(Path.Combine(s_project, "Properties", "Resources.Designer.cs"));
var props = Regex.Matches(designer, @"public static string (\w+) \{").Select(m => m.Groups[1].Value);
Assert.Empty(props.Except(Keys("Resources.resx")));
}

[Fact]
public void SettingsRedesignStringsAreTranslatedToSpanish()
{
var es = Keys("Resources.es-ES.resx");
var redesign = Keys("Resources.resx").Where(k =>
k == "GeneralSettingsPageText" || k.StartsWith("SettingsSection") ||
(k.StartsWith("Settings") && (k.EndsWith("Description") || k.Contains("Volume") || k.Contains("Snap") ||
k.Contains("ScrollWheel") || k.Contains("Translucent") || k.Contains("Legacy")))).ToArray();
Assert.NotEmpty(redesign);
Assert.Empty(redesign.Except(es));
}
}
68 changes: 68 additions & 0 deletions EarTrumpet.Tests/VolumeMathTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using EarTrumpet.DataModel;
using EarTrumpet.Extensions;
using EarTrumpet.UI.Helpers;
using Xunit;

namespace EarTrumpet.Tests;

public class VolumeMathTests
{
[Theory]
[InlineData(0, 5, 5)]
[InlineData(3, 5, 5)]
[InlineData(5, 5, 10)]
[InlineData(98, 5, 100)]
[InlineData(100, 5, 100)]
public void NextSnapPoint_LandsOnGridAboveAndNeverExceeds100(double current, int step, double expected)
=> Assert.Equal(expected, VolumeStepper.NextSnapPoint(current, step));

[Theory]
[InlineData(100, 5, 95)]
[InlineData(7, 5, 5)]
[InlineData(5, 5, 0)]
[InlineData(2, 5, 0)]
[InlineData(0, 5, 0)]
public void PreviousSnapPoint_LandsOnGridBelowAndNeverGoesNegative(double current, int step, double expected)
=> Assert.Equal(expected, VolumeStepper.PreviousSnapPoint(current, step));

[Theory]
[InlineData(12, 5, 10)]
[InlineData(13, 5, 15)]
[InlineData(99.6, 3, 100)] // 100 stays reachable when it is not on the grid
[InlineData(1, 3, 0)]
public void NearestSnapPoint_RoundsToGridButKeepsFullVolumeReachable(double current, int step, double expected)
=> Assert.Equal(expected, VolumeStepper.NearestSnapPoint(current, step));

[Fact]
public void Bound_ClampsBothEnds()
{
Assert.Equal(0d, (-5d).Bound(0, 1));
Assert.Equal(1d, 5d.Bound(0, 1));
Assert.Equal(0.5d, 0.5d.Bound(0, 1));
}

[Theory]
[InlineData(1.0)]
[InlineData(0.5)]
[InlineData(0.01)]
public void LogarithmicConversion_RoundTrips(double linear)
=> Assert.Equal(linear, linear.LinearToLog().LogToLinear(), 9);

[Fact]
public void FullVolumeIs0Db()
=> Assert.Equal(0d, 1d.LinearToLog(), 9);

[Fact]
public void ForceOpaque_DisablesTransparencyRegardlessOfWindows()
{
try
{
SystemSettings.ForceOpaque = true;
Assert.False(SystemSettings.IsTransparencyEnabled);
}
finally
{
SystemSettings.ForceOpaque = false;
}
}
}
20 changes: 20 additions & 0 deletions EarTrumpet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Chocolatey", "Chocolatey",
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "EarTrumpet.Package", "EarTrumpet.Package\EarTrumpet.Package.wapproj", "{EA5510ED-F014-4587-A505-64C59D5B2627}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EarTrumpet.Tests", "EarTrumpet.Tests\EarTrumpet.Tests.csproj", "{BA949B87-C0DF-4986-99CB-30458590F3FF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Expand Down Expand Up @@ -83,6 +85,24 @@ Global
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.ActiveCfg = Debug|x86
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Build.0 = Debug|x86
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Deploy.0 = Debug|x86
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|ARM64.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|ARM64.Build.0 = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x64.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x64.Build.0 = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x86.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x86.Build.0 = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|ARM64.ActiveCfg = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|ARM64.Build.0 = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x64.ActiveCfg = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x64.Build.0 = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x86.ActiveCfg = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x86.Build.0 = Release|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|ARM64.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|ARM64.Build.0 = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x64.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x64.Build.0 = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x86.ActiveCfg = Debug|x64
{BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x86.Build.0 = Debug|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ public ActionsCategoryViewModel()
Glyph = "\xE948",
GlyphFontSize = 15,
} ];

if (Pages.Count == 2)
{
Toolbar[0].Command.Execute(null);
}
}

internal void ReloadSavedPages()
Expand Down
49 changes: 34 additions & 15 deletions EarTrumpet/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -588,20 +588,21 @@
<Setter Property="ItemContainerTemplateSelector" Value="{StaticResource MenuSelector}" />
<Setter Property="Placement" Value="Mouse" />
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
<Setter Property="MinWidth" Value="311" />
<Setter Property="MinWidth" Value="240" />
<Setter Property="HasDropShadow" Value="True" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Theme:Brush.Foreground" Value="Theme={Theme}BaseHigh, HighContrast=MenuText" />
<Setter Property="Theme:Brush.Background" Value="Theme={Theme}ChromeMediumLow, HighContrast=Menu" />
<Setter Property="Theme:Brush.BorderBrush" Value="Light=LightChromeHigh, Dark=ControlDarkAppButtonTextDisabled/0.9, HighContrast=ControlText" />
<Setter Property="Theme:Brush.BorderBrush" Value="Light=#1A000000, Dark=#33FFFFFF, HighContrast=ControlText" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border x:Name="Border">
<Border Padding="2,8"
<Border Padding="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
BorderThickness="1"
CornerRadius="8">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
Expand All @@ -611,12 +612,12 @@
<Trigger Property="HasDropShadow" Value="True">
<Setter TargetName="Border" Property="CornerRadius" Value="0" />
<Setter TargetName="Border" Property="Background" Value="Transparent" />
<Setter TargetName="Border" Property="Padding" Value="0,0,5,5" />
<Setter TargetName="Border" Property="Padding" Value="0,0,12,12" />
<Setter TargetName="Border" Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="4"
Opacity="0.8"
ShadowDepth="2" />
<DropShadowEffect BlurRadius="12"
Opacity="0.35"
ShadowDepth="3" />
</Setter.Value>
</Setter>
</Trigger>
Expand All @@ -626,7 +627,7 @@
</Setter>
</Style>
<Style TargetType="{x:Type Separator}">
<Setter Property="Margin" Value="8,2,8,2" />
<Setter Property="Margin" Value="12,3" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Theme:Brush.Foreground" Value="Theme={Theme}BaseMediumLow, HighContrast=ControlText" />
<Setter Property="Template">
Expand All @@ -647,9 +648,11 @@
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Border x:Name="Border"
Height="32"
Height="30"
Margin="4,1"
Background="Transparent"
BorderThickness="0">
BorderThickness="0"
CornerRadius="4">
<Grid Margin="0,-1,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" SharedSizeGroup="Icon" />
Expand All @@ -662,11 +665,19 @@
BorderThickness="1">
<TextBlock x:Name="CheckMark"
Theme:Brush.Foreground="Theme={Theme}BaseMedium, HighContrast=ControlText"
FontFamily="Segoe MDL2 Assets"
FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
SnapsToDevicePixels="False"
Text="{TemplateBinding Tag}"
Visibility="Hidden" />
</Border>
<TextBlock x:Name="IconText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Theme:Brush.Foreground="Theme={Theme}BaseMedium, HighContrast=ControlText"
FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
FontSize="14"
SnapsToDevicePixels="False"
Text="{TemplateBinding Icon}" />
<ContentPresenter x:Name="HeaderHost"
Grid.Column="1"
Margin="0,0,8,0"
Expand All @@ -681,7 +692,7 @@
Visibility="Collapsed">
<TextBlock x:Name="GlyphText"
Theme:Brush.Foreground="Theme={Theme}BaseMedium, HighContrast=ControlText"
FontFamily="Segoe MDL2 Assets"
FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
SnapsToDevicePixels="False"
Text="&#xE0E3;" />
</Border>
Expand All @@ -696,11 +707,17 @@
PopupAnimation="Fade">

<Border x:Name="SubmenuBorder"
Margin="0,0,12,12"
Theme:Brush.Background="Theme={Theme}ChromeMediumLow, HighContrast=Menu"
Theme:Brush.BorderBrush="Theme={Theme}ChromeHigh, HighContrast=ControlText"
Theme:Brush.BorderBrush="Light=#1A000000, Dark=#33FFFFFF, HighContrast=ControlText"
BorderThickness="1"
CornerRadius="8"
SnapsToDevicePixels="True">
<Border Padding="2,6"
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.35" ShadowDepth="3" />
</Border.Effect>
<Border Padding="4"
CornerRadius="7"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1">
Expand Down Expand Up @@ -740,12 +757,14 @@
IsCheckable="{Binding IsChecked}"
IsChecked="{Binding IsChecked}"
IsEnabled="{Binding IsEnabled}"
Icon="{Binding Icon}"
Tag="{Binding Glyph}" />
</DataTemplate>
<HierarchicalDataTemplate x:Key="ContextMenuSubItemTemplate" ItemsSource="{Binding Children}">
<MenuItem Command="{Binding Command}"
Header="{Binding DisplayName}"
ItemContainerTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}, Path=ItemContainerTemplateSelector}"
Icon="{Binding Icon}"
ItemsSource="{Binding Children}"
UsesItemContainerTemplate="True" />
</HierarchicalDataTemplate>
Expand Down
Loading