Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 0 additions & 77 deletions Screenbox/Controls/AcceleratorService.cs

This file was deleted.

37 changes: 19 additions & 18 deletions Screenbox/Controls/PlayerControls.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
xmlns:converters="using:Screenbox.Converters"
xmlns:ctConverters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:extensions="using:Screenbox.Extensions"
xmlns:helpers="using:Screenbox.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
Expand Down Expand Up @@ -376,9 +377,9 @@
<Button
x:Name="PlayPauseButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind ViewModel.IsPlaying, Converter={StaticResource BoolToPlayPauseTextConverter}, Mode=OneWay}"
extensions:AcceleratorService.ToolTip="{x:Bind ViewModel.IsPlaying, Converter={StaticResource BoolToPlayPauseTextConverter}, Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerPlayPauseKey}"
AutomationProperties.Name="{x:Bind PlayPauseButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind PlayPauseButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Command="{x:Bind ViewModel.PlayPauseCommand}"
Style="{StaticResource PlayerButtonStyle}">
<FontIcon FontFamily="{StaticResource ScreenboxSymbolThemeFontFamily}" Glyph="{x:Bind converters:GlyphConverter.ToPlayPauseGlyph(ViewModel.IsPlaying), Mode=OneWay}" />
Expand All @@ -392,9 +393,9 @@
<StackPanel FlowDirection="LeftToRight" Orientation="Horizontal">
<Button
x:Name="PreviousButton"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.Previous}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.Previous}"
AccessKey="{strings:KeyboardResources Key=PlayerPreviousKey}"
AutomationProperties.Name="{x:Bind PreviousButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind PreviousButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Command="{x:Bind ViewModel.Playlist.PreviousCommand}"
CornerRadius="{Binding Source={StaticResource ControlCornerRadius}, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
Style="{StaticResource PlayerButtonStyle}">
Expand All @@ -406,9 +407,9 @@
</Button>
<Button
x:Name="NextButton"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.Next}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.Next}"
AccessKey="{strings:KeyboardResources Key=PlayerNextKey}"
AutomationProperties.Name="{x:Bind NextButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind NextButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Command="{x:Bind ViewModel.Playlist.NextCommand}"
CornerRadius="{Binding Source={StaticResource ControlCornerRadius}, Converter={StaticResource RightCornerRadiusFilterConverter}}"
Style="{StaticResource PlayerButtonStyle}">
Expand Down Expand Up @@ -442,9 +443,9 @@
<Button
x:Name="VolumeButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.Volume}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.Volume}"
AccessKey="{strings:KeyboardResources Key=PlayerVolumeButtonSliderKey}"
AutomationProperties.Name="{x:Bind VolumeButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind VolumeButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Flyout="{StaticResource VolumeControlFlyout}"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
Style="{StaticResource PlayerButtonStyle}"
Expand All @@ -464,9 +465,9 @@
<ToggleButton
x:Name="ShuffleButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.ShuffleMode(ViewModel.Playlist.ShuffleMode), Mode=OneWay}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.ShuffleMode(ViewModel.Playlist.ShuffleMode), Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerShuffleKey}"
AutomationProperties.Name="{x:Bind ShuffleButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind ShuffleButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
IsChecked="{x:Bind ViewModel.Playlist.ShuffleMode, Mode=TwoWay}"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
Style="{StaticResource PlayerToggleButtonStyle}">
Expand All @@ -479,9 +480,9 @@
<ToggleButton
x:Name="RepeatButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.RepeatMode(ViewModel.Playlist.RepeatMode), Mode=OneWay}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.RepeatMode(ViewModel.Playlist.RepeatMode), Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerRepeatKey}"
AutomationProperties.Name="{x:Bind RepeatButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind RepeatButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
IsChecked="{x:Bind ViewModel.Playlist.RepeatMode, Converter={StaticResource ToggleButtonCheckToRepeatModeConverter}, Mode=TwoWay}"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
IsThreeState="True"
Expand All @@ -495,9 +496,9 @@
<Button
x:Name="AudioAndCaptionButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.AudioAndCaption}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.AudioAndCaption}"
AccessKey="{strings:KeyboardResources Key=PlayerAudioAndCaptionsKey}"
AutomationProperties.Name="{x:Bind AudioAndCaptionButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind AudioAndCaptionButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
Style="{StaticResource PlayerButtonStyle}">
<FontIcon Glyph="&#xED1F;" MirroredWhenRightToLeft="True" />
Expand Down Expand Up @@ -527,9 +528,9 @@
<Button
x:Name="CompactOverlayButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.CompactOverlayToggle(ViewModel.IsCompact), Mode=OneWay}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.CompactOverlayToggle(ViewModel.IsCompact), Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerCompactOverlayKey}"
AutomationProperties.Name="{x:Bind CompactOverlayButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind CompactOverlayButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Command="{x:Bind ViewModel.ToggleCompactLayoutCommand}"
Style="{StaticResource PlayerButtonStyle}"
Visibility="{x:Bind helpers:DeviceInfoHelper.IsDesktop}">
Expand All @@ -547,9 +548,9 @@
<Button
x:Name="FullscreenButton"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.FullscreenToggle(ViewModel.IsFullscreen), Mode=OneWay}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.FullscreenToggle(ViewModel.IsFullscreen), Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerFullscreenKey}"
AutomationProperties.Name="{x:Bind FullscreenButton.(controls:AcceleratorService.ToolTip), Mode=OneWay}"
AutomationProperties.Name="{x:Bind FullscreenButton.(extensions:AcceleratorService.ToolTip), Mode=OneWay}"
Command="{x:Bind ViewModel.ToggleFullscreenCommand}"
Style="{StaticResource PlayerButtonStyle}"
Visibility="{x:Bind helpers:DeviceInfoHelper.IsDesktop}">
Expand Down
2 changes: 1 addition & 1 deletion Screenbox/Controls/PlaylistView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@
<Button
Padding="4,5,4,5"
VerticalAlignment="Top"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.ClearSelection}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.ClearSelection}"
AutomationProperties.Name="{strings:Resources Key=ClearSelection}"
BorderThickness="0"
Command="{x:Bind ViewModel.ClearSelectionCommand}"
Expand Down
3 changes: 2 additions & 1 deletion Screenbox/Controls/VolumeControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
xmlns:controls="using:Screenbox.Controls"
xmlns:converters="using:Screenbox.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:extensions="using:Screenbox.Extensions"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Screenbox.Strings"
d:DesignHeight="40"
Expand Down Expand Up @@ -45,8 +46,8 @@
x:Name="VolumeToggleButton"
Grid.Column="0"
Margin="0,0,4,0"
controls:AcceleratorService.ToolTip="{x:Bind strings:Resources.MuteToggle(ViewModel.IsMute), Mode=OneWay}"
d:Style="{StaticResource SmallPlayerToggleButtonStyle}"
extensions:AcceleratorService.ToolTip="{x:Bind strings:Resources.MuteToggle(ViewModel.IsMute), Mode=OneWay}"
AccessKey="{strings:KeyboardResources Key=PlayerMuteKey}"
AutomationProperties.Name="{x:Bind strings:Resources.MuteToggle(ViewModel.IsMute), Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsMute, Mode=TwoWay}"
Expand Down
101 changes: 101 additions & 0 deletions Screenbox/Extensions/AcceleratorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#nullable enable

using System.Globalization;
using Screenbox.Helpers;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Screenbox.Extensions;

/// <summary>
/// Represents a service that provides <see langword="static"/> methods to display a <see cref="ToolTip"/>,
/// with the corresponding key combinations appended.
/// </summary>
/// <remarks>If a control has more than one accelerator defined, only the first is displayed.</remarks>
/// <example>
/// In this example, we specify the ToolTip for a Button. The keyboard accelerator is
/// displayed in the UI element flyout as "Create a new document (Ctrl+Alt+N)".
/// <code lang="xml">
/// &lt;Button Content="New" local:AcceleratorService.ToolTip="Create a new document"&gt;
/// &lt;Button.KeyboardAccelerators&gt;
/// &lt;KeyboardAccelerator Key="N" Modifiers="Control,Menu" /&gt;
/// &lt;/Button.KeyboardAccelerators&gt;
/// &lt;/Button&gt;
/// </code>
/// or
/// <code lang="csharp">
/// var button = new Button { Content = "New" };
/// AcceleratorService.SetToolTip(button, "Create a new document");
/// button.KeyboardAccelerators.Add(new KeyboardAccelerator
/// {
/// Key = VirtualKey.N,
/// Modifiers = KeyboardAcceleratorModifiers.Control | KeyboardAcceleratorModifiers.Menu
/// });
/// </code>
/// </example>
[Windows.Foundation.Metadata.ContractVersion(typeof(Windows.Foundation.UniversalApiContract), 327680u)]
public sealed class AcceleratorService
{
private static readonly bool _isHebrew = CultureInfo.CurrentCulture.TwoLetterISOLanguageName is "he";

/// <summary>
/// Identifies the AcceleratorService.ToolTip XAML attached property.
/// </summary>
public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached(
"ToolTip", typeof(string), typeof(AcceleratorService), new PropertyMetadata(string.Empty, OnToolTipPropertyChanged));

/// <summary>
/// Gets the value of the AcceleratorService.ToolTip XAML attached property for a <see cref="UIElement"/>.
/// </summary>
/// <param name="element">The element from which the property value is read.</param>
/// <returns>The string tooltip content.</returns>
public static string GetToolTip(UIElement element)
{
return (string)element.GetValue(ToolTipProperty);
}

/// <summary>
/// Sets the value of the AcceleratorService.ToolTip XAML attached property.
/// </summary>
/// <param name="element">The element to set tooltip content on.</param>
/// <param name="value">The string to set as tooltip content.</param>
public static void SetToolTip(UIElement element, string value)
{
element.SetValue(ToolTipProperty, value);
}

private static void OnToolTipPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element && e.NewValue is string value)
{
string toolTipString = value;

if (DeviceInfoHelper.IsKeyboardPresent)
{
var keyboardAccelerators = element.KeyboardAccelerators;
if (keyboardAccelerators.Count > 0)
{
var keyboardAccelerator = keyboardAccelerators[0];
if (keyboardAccelerator.IsEnabled)
{
string keyboardAcceleratorText = keyboardAccelerator.ToDisplayName();
if (!string.IsNullOrEmpty(keyboardAcceleratorText))
{
// For Hebrew, we enclose the accelerator text in left-to-right (LTR) embedding marks
// to ensure modifiers appear before any translated key.
// Example: הסר+Ctrl+Alt ["Clear"+Ctrl+Alt] becomes Ctrl+Alt+הסר [Ctrl+Alt+"Clear"]
//
// In right-to-left (RTL) languages, the system automatically adjusts the tooltip format for proper readability.
// The default format "{value} ({keyboardAcceleratorText})" is reversed to "({keyboardAcceleratorText}) {value}".
toolTipString = _isHebrew
? $"{value} \u202A({keyboardAcceleratorText})\u202C"
: $"{value} ({keyboardAcceleratorText})";
}
}
}
}

ToolTipService.SetToolTip(element, toolTipString);
}
}
}
24 changes: 20 additions & 4 deletions Screenbox/Helpers/DeviceInfoHelper.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
using Windows.System.Profile;
using Windows.Devices.Input;
using Windows.System.Profile;

namespace Screenbox.Helpers;

/// <summary>
/// Provides <see langword="static"/> helper methods to get information about the system.
/// </summary>
public static partial class DeviceInfoHelper
public static class DeviceInfoHelper
{
private static readonly KeyboardCapabilities _keyboardCapabilities = new();
private static readonly TouchCapabilities _touchCapabilities = new();

public static readonly string DeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily;

/// <summary>
/// Gets whether the current device is a desktop/laptop/tablet.
/// </summary>
/// <returns><see langword="true"/> if the device is a desktop; otherwise, <see langword="false"/>.</returns>
public static bool IsDesktop => DeviceFamily == "Windows.Desktop";
public static readonly bool IsDesktop = DeviceFamily == "Windows.Desktop";

/// <summary>
/// Gets whether the current device is an Xbox.
/// </summary>
/// <returns><see langword="true"/> if the device is an Xbox; otherwise, <see langword="false"/>.</returns>
public static bool IsXbox => DeviceFamily == "Windows.Xbox";
public static readonly bool IsXbox = (DeviceFamily == "Windows.Xbox") || (DeviceFamily == "Windows.XBoxSRA") || (DeviceFamily == "Windows.XBoxERA");
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The device family names contain inconsistent capitalization. 'XBoxSRA' and 'XBoxERA' should likely be 'XboxSRA' and 'XboxERA' to match the standard Xbox naming convention used in 'Windows.Xbox'.

Suggested change
public static readonly bool IsXbox = (DeviceFamily == "Windows.Xbox") || (DeviceFamily == "Windows.XBoxSRA") || (DeviceFamily == "Windows.XBoxERA");
public static readonly bool IsXbox = (DeviceFamily == "Windows.Xbox") || (DeviceFamily == "Windows.XboxSRA") || (DeviceFamily == "Windows.XboxERA");

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/// <summary>
/// Gets whether the current device has a physical keyboard connected.
/// </summary>
/// <returns><see langword="true"/> if a keyboard is detected; otherwise, <see langword="false"/>.</returns>
public static bool IsKeyboardPresent => _keyboardCapabilities.KeyboardPresent != 0;

/// <summary>
/// Gets whether the current device has a touch digitizer.
/// </summary>
/// <returns><see langword="true"/> if touch input is supported; otherwise, <see langword="false"/>.</returns>
public static bool IsTouchPresent => _touchCapabilities.TouchPresent != 0;
}
Loading