Skip to content

Commit

Permalink
✨ Continue implementing the embedded monitor UI.
Browse files Browse the repository at this point in the history
This adds the ImageCropper component.
  • Loading branch information
hexawyz committed Feb 1, 2025
1 parent 8613d97 commit cb38b82
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
<lconverters:ByteArrayToBitmapImageConverter x:Key="ByteArrayToBitmapImageConverter" />
<lconverters:SharedMemoryToBitmapImageConverter x:Key="SharedMemoryToBitmapImageConverter" />
<lconverters:FileNameToBitmapImageConverter x:Key="FileNameToBitmapImageConverter" />
<lconverters:ImageToWriteableBitmapConverter x:Key="ImageToWriteableBitmapConverter" />
<lconverters:MonitorShapeToCropShapeConverter x:Key="MonitorShapeToCropShapeConverter" />

<local:RgbLightingDefaultPalette x:Key="RgbLightingDefaultPalette" />

Expand Down
2 changes: 2 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/ChangedProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ internal static class ChangedProperty
public static readonly PropertyChangedEventArgs LoadedImageName = new (nameof(LoadedImageName));
public static readonly PropertyChangedEventArgs LoadedImageData = new (nameof(LoadedImageData));
public static readonly PropertyChangedEventArgs Shape = new (nameof(Shape));
public static readonly PropertyChangedEventArgs Image = new (nameof(Image));
public static readonly PropertyChangedEventArgs ImageSize = new (nameof(ImageSize));
public static readonly PropertyChangedEventArgs DisplayWidth = new (nameof(DisplayWidth));
public static readonly PropertyChangedEventArgs DisplayHeight = new (nameof(DisplayHeight));
public static readonly PropertyChangedEventArgs HasBuiltInGraphics = new (nameof(HasBuiltInGraphics));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Exo.Settings.Ui.ViewModels;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Storage.Streams;

namespace Exo.Settings.Ui.Converters;

internal sealed class ImageToWriteableBitmapConverter : IValueConverter
{
private readonly Dictionary<string, WeakReference<WriteableBitmap>> _bitmapCache = new(StringComparer.OrdinalIgnoreCase);

public object? Convert(object value, Type targetType, object parameter, string language)
{
if (value is not ImageViewModel image) return null;

// In this case, we take the bet that the filename and the file will "always" be valid. As such, we pre-allocate a weak reference before knowing if the file will be read successfully.
WriteableBitmap? bitmapImage;
if (_bitmapCache.TryGetValue(image.FileName, out var wr))
{
if (wr.TryGetTarget(out bitmapImage)) return bitmapImage;
bitmapImage = new(image.Width, image.Height);
wr.SetTarget(bitmapImage);
}
else
{
bitmapImage = new(image.Width, image.Height);
_bitmapCache.Add(image.FileName, new(bitmapImage, false));
}

LoadImage(bitmapImage, image.FileName);
return bitmapImage;
}

private async void LoadImage(WriteableBitmap bitmapImage, string fileName)
{
try
{
using (var randomAccessStream = await FileRandomAccessStream.OpenAsync(fileName, Windows.Storage.FileAccessMode.Read, Windows.Storage.StorageOpenOptions.AllowOnlyReaders, FileOpenDisposition.OpenExisting))
{
await bitmapImage.SetSourceAsync(randomAccessStream);
}
}
catch
{
}
}

public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using CommunityToolkit.WinUI.Controls;
using Exo.Contracts.Ui.Settings;
using Microsoft.UI.Xaml.Data;

namespace Exo.Settings.Ui.Converters;

internal sealed class MonitorShapeToCropShapeConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, string language)
=> value is MonitorShape.Circle ? CropShape.Circular : CropShape.Rectangular;

public object ConvertBack(object value, Type targetType, object parameter, string language)
=> throw new NotSupportedException();
}
49 changes: 44 additions & 5 deletions src/Exo/Ui/Exo.Settings.Ui/EmbeddedMonitorSettingsControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
xmlns:lconverters="using:Exo.Settings.Ui.Converters"
xmlns:lts="using:Exo.Settings.Ui.DataTemplateSelectors"
xmlns:vm="using:Exo.Settings.Ui.ViewModels"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Expand All @@ -18,24 +19,62 @@
<DataTemplate x:DataType="vm:EmbeddedMonitorFeaturesViewModel">
<Grid HorizontalAlignment="Stretch" DataContext="{Binding EmbeddedMonitors[0]}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid HorizontalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch" Visibility="{Binding HasBuiltInGraphics, Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{StaticResource PropertyLabelColumnWidth}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="EmbeddedMonitorGraphicsLabel" Margin="{StaticResource RowLabelMargin}"></TextBlock>
<ComboBox Grid.Column="1" Margin="{StaticResource RowContentLabelMargin}" ItemsSource="{Binding SupportedGraphics}" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:EmbeddedMonitorGraphicsViewModel">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="2" Margin="{StaticResource RowContentMargin}" HorizontalAlignment="Right" Command="{Binding ResetCommand}">
<FontIcon Glyph="&#xE777;" />
</Button>
</Grid>
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{StaticResource PropertyLabelColumnWidth}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="A" Margin="{StaticResource RowLabelMargin}"></TextBlock>
<ComboBox Grid.Column="1" Margin="{StaticResource RowContentLabelMargin}" />
<TextBlock x:Uid="EmbeddedMonitorImageLabel" Margin="{StaticResource RowLabelMargin}"></TextBlock>
<ComboBox Grid.Column="1" Margin="{StaticResource RowContentLabelMargin}" ItemsSource="{Binding AvailableImages}" SelectedItem="{Binding Image, Mode=TwoWay}" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:ImageViewModel">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding FileName, Converter={StaticResource FileNameToBitmapImageConverter}}" Width="20" Height="20" Margin="0,0,6,0" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="2" Margin="{StaticResource RowContentMargin}" HorizontalAlignment="Right" Command="{Binding ResetCommand}">
<FontIcon Glyph="&#xE777;" />
</Button>
</Grid>
<Grid Grid.Row="1" MinWidth="{Binding DisplayWidth}" MinHeight="{Binding DisplayHeight}" Background="Black" BorderBrush="White">
<Image HorizontalAlignment="Center" />
<Grid Grid.Row="2" MinHeight="{Binding DisplayHeight}">
<controls:ImageCropper
CropShape="{Binding Shape, Converter={StaticResource MonitorShapeToCropShapeConverter}}"
Source="{Binding Image, Converter={StaticResource ImageToWriteableBitmapConverter}}"
ThumbPlacement="Corners"
Padding="20"
Height="{Binding ImageSize.Height}" />
</Grid>
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="0,12,0,0" HorizontalAlignment="Right">
<Button x:Uid="ResetButton" HorizontalAlignment="Right" Margin="0,0,6,0" Command="{Binding ResetCommand}" CommandParameter="{Binding}" />
<Button x:Uid="ApplyButton" Style="{StaticResource AccentButtonStyle}" HorizontalAlignment="Right" Margin="6,0,0,0" Command="{Binding ApplyCommand}" CommandParameter="{Binding}" />
</StackPanel>
</Grid>
</DataTemplate>
</lts:EmbeddedMonitorSettingTemplateSelector.SingleMonitorTemplate>
Expand Down
1 change: 1 addition & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Exo.Settings.Ui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@

<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Controls.ColorPicker" Version="8.1.240916" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.ImageCropper" Version="8.1.240916" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.1.240916" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.1.240916" />
<PackageReference Include="Grpc.Net.Client" />
Expand Down
6 changes: 6 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@
<data name="DpiPresetsLabel.Text" xml:space="preserve">
<value>DPI presets</value>
</data>
<data name="EmbeddedMonitorGraphicsLabel.Text" xml:space="preserve">
<value>Graphismes</value>
</data>
<data name="EmbeddedMonitorImageLabel.Text" xml:space="preserve">
<value>Image</value>
</data>
<data name="EmbeddedMonitorSettingsSectionHeader.Text" xml:space="preserve">
<value>Embedded Monitor</value>
</data>
Expand Down
6 changes: 6 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Strings/fr-FR/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@
<data name="DpiPresetsLabel.Text" xml:space="preserve">
<value>Préréglages PPP</value>
</data>
<data name="EmbeddedMonitorGraphicsLabel.Text" xml:space="preserve">
<value>Graphics</value>
</data>
<data name="EmbeddedMonitorImageLabel.Text" xml:space="preserve">
<value>Image</value>
</data>
<data name="EmbeddedMonitorSettingsSectionHeader.Text" xml:space="preserve">
<value>Écran intégré</value>
</data>
Expand Down
3 changes: 2 additions & 1 deletion src/Exo/Ui/Exo.Settings.Ui/ViewModels/DeviceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal class DeviceViewModel : BindableObject, IDisposable
public DeviceViewModel
(
SettingsServiceConnectionManager connectionManager,
ReadOnlyObservableCollection<ImageViewModel> availableImages,
ISettingsMetadataService metadataService,
IPowerService powerService,
IMouseService mouseService,
Expand Down Expand Up @@ -45,7 +46,7 @@ DeviceInformation deviceInformation
}
else if (featureId == WellKnownGuids.EmbeddedMonitorDeviceFeature)
{
EmbeddedMonitorFeatures ??= new(this, rasterizationScaleProvider);
EmbeddedMonitorFeatures ??= new(this, availableImages, rasterizationScaleProvider);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/Exo/Ui/Exo.Settings.Ui/ViewModels/DevicesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public event EventHandler? CanExecuteChanged
private readonly HashSet<Guid> _removedDeviceIds;

private readonly SettingsServiceConnectionManager _connectionManager;
private readonly ReadOnlyObservableCollection<ImageViewModel> _availableImages;

// Processing asynchronous status updates requires accessing the view model from the device ID.
private readonly Dictionary<Guid, DeviceViewModel> _devicesById;
Expand Down Expand Up @@ -78,6 +79,7 @@ public event EventHandler? CanExecuteChanged
public DevicesViewModel
(
SettingsServiceConnectionManager connectionManager,
ReadOnlyObservableCollection<ImageViewModel> availableImages,
ISettingsMetadataService metadataService,
IRasterizationScaleProvider rasterizationScaleProvider,
ICommand navigateCommand
Expand All @@ -86,6 +88,7 @@ ICommand navigateCommand
_devices = new();
_removedDeviceIds = new();
_connectionManager = connectionManager;
_availableImages = availableImages;
_devicesById = new();
_pendingPowerDeviceInformations = new();
_pendingBatteryChanges = new();
Expand Down Expand Up @@ -215,7 +218,7 @@ private async Task WatchDevicesAsync(IDeviceService deviceService, IPowerService
case WatchNotificationKind.Enumeration:
case WatchNotificationKind.Addition:
{
var device = new DeviceViewModel(_connectionManager, _metadataService, powerService, mouseService, _rasterizationScaleProvider, notification.Details);
var device = new DeviceViewModel(_connectionManager, _availableImages, _metadataService, powerService, mouseService, _rasterizationScaleProvider, notification.Details);
await HandleDeviceArrivalAsync(device, cancellationToken);
_devicesById.Add(notification.Details.Id, device);
_devices.Add(device);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ namespace Exo.Settings.Ui.ViewModels;
internal sealed class EmbeddedMonitorFeaturesViewModel : BindableObject, IDisposable
{
private readonly DeviceViewModel _device;
private readonly ReadOnlyObservableCollection<ImageViewModel> _availableImages;
private readonly IRasterizationScaleProvider _rasterizationScaleProvider;
private readonly ObservableCollection<EmbeddedMonitorViewModel> _embeddedMonitors;
private readonly ReadOnlyObservableCollection<EmbeddedMonitorViewModel> _readOnlyEmbeddedMonitors;
private readonly Dictionary<Guid, EmbeddedMonitorViewModel> _embeddedMonitorById;
private bool _isExpanded;
private readonly PropertyChangedEventHandler _onRasterizationScaleProviderPropertyChanged;

public EmbeddedMonitorFeaturesViewModel(DeviceViewModel device, IRasterizationScaleProvider rasterizationScaleProvider)
public EmbeddedMonitorFeaturesViewModel(DeviceViewModel device, ReadOnlyObservableCollection<ImageViewModel> availableImages, IRasterizationScaleProvider rasterizationScaleProvider)
{
_device = device;
_availableImages = availableImages;
_rasterizationScaleProvider = rasterizationScaleProvider;
_embeddedMonitors = new();
_embeddedMonitorById = new();
Expand All @@ -38,6 +40,7 @@ private void OnRasterizationScaleProviderPropertyChanged(object? sender, Propert
}

public ReadOnlyObservableCollection<EmbeddedMonitorViewModel> EmbeddedMonitors => _readOnlyEmbeddedMonitors;
public ReadOnlyObservableCollection<ImageViewModel> AvailableImages => _availableImages;

internal IRasterizationScaleProvider RasterizationScaleProvider => _rasterizationScaleProvider;

Expand Down Expand Up @@ -85,27 +88,35 @@ internal sealed class EmbeddedMonitorViewModel : BindableObject
private readonly Guid _monitorId;
private MonitorShape _shape;
private Size _imageSize;
private EmbeddedMonitorCapabilities _capabilities;
private readonly ObservableCollection<EmbeddedMonitorGraphicsViewModel> _supportedGraphics;
private readonly ReadOnlyObservableCollection<EmbeddedMonitorGraphicsViewModel> _readOnlySupportedGraphics;

private ImageViewModel? _image;

public EmbeddedMonitorViewModel(EmbeddedMonitorFeaturesViewModel owner, EmbeddedMonitorInformation information)
{
_owner = owner;
_monitorId = information.MonitorId;
_shape = information.Shape;
_imageSize = information.ImageSize;
_capabilities = information.Capabilities;
_supportedGraphics = new();
_readOnlySupportedGraphics = new(_supportedGraphics);
}

public Guid MonitorId => _monitorId;

public MonitorShape Shape
{
get => _shape;
set => SetValue(ref _shape, value, ChangedProperty.Shape);
private set => SetValue(ref _shape, value, ChangedProperty.Shape);
}

public Size ImageSize
{
get => _imageSize;
set
private set
{
bool widthChanged = value.Width != _imageSize.Width;
bool heightChanged = value.Height != _imageSize.Height;
Expand All @@ -121,9 +132,34 @@ public Size ImageSize
}
}

private EmbeddedMonitorCapabilities Capabilities
{
get => _capabilities;
set
{
if (value != _capabilities)
{
var changedCapabilities = value ^ _capabilities;
_capabilities = value;
if ((changedCapabilities & EmbeddedMonitorCapabilities.BuiltInGraphics) != 0) NotifyPropertyChanged(ChangedProperty.HasBuiltInGraphics);
}
}
}

public bool HasBuiltInGraphics => (_capabilities & EmbeddedMonitorCapabilities.BuiltInGraphics) != 0;

public double DisplayWidth => _imageSize.Width / _owner.RasterizationScaleProvider.RasterizationScale;
public double DisplayHeight => _imageSize.Height / _owner.RasterizationScaleProvider.RasterizationScale;

public ImageViewModel? Image
{
get => _image;
set => SetValue(ref _image, value, ChangedProperty.Image);
}

public ReadOnlyObservableCollection<ImageViewModel> AvailableImages => _owner.AvailableImages;
public ReadOnlyObservableCollection<EmbeddedMonitorGraphicsViewModel> SupportedGraphics => _readOnlySupportedGraphics;

internal void UpdateInformation(EmbeddedMonitorInformation information)
{
Shape = information.Shape;
Expand All @@ -136,3 +172,8 @@ internal void NotifyDpiChange()
NotifyPropertyChanged(ChangedProperty.DisplayHeight);
}
}

internal sealed class EmbeddedMonitorGraphicsViewModel
{
public string DisplayName => "";
}
4 changes: 4 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/ImagesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ private async Task AddImageAsync(CancellationToken cancellationToken)

internal sealed partial class ImageViewModel : ApplicableResettableBindableObject
{
private readonly UInt128 _id;
private string _name;
private readonly string _fileName;
private readonly ushort _width;
Expand All @@ -291,6 +292,7 @@ internal sealed partial class ImageViewModel : ApplicableResettableBindableObjec

public ImageViewModel(ImageInformation information)
{
_id = information.ImageId;
_name = information.ImageName;
_fileName = information.FileName;
_width = information.Width;
Expand All @@ -299,6 +301,8 @@ public ImageViewModel(ImageInformation information)
_isAnimated = information.IsAnimated;
}

public UInt128 Id => _id;

public string Name
{
get => _name;
Expand Down
4 changes: 2 additions & 2 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ ISettingsMetadataService metadataService
_metadataService = metadataService;
_goBackCommand = new(this);
_navigateCommand = new(this);
_devicesViewModel = new(ConnectionManager, _metadataService, rasterizationScaleProvider, _navigateCommand);
_imagesViewModel = new(ConnectionManager, fileOpenDialog);
_devicesViewModel = new(ConnectionManager, _imagesViewModel.Images, _metadataService, rasterizationScaleProvider, _navigateCommand);
_batteryDevicesViewModel = new(_devicesViewModel);
_lightingViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_imagesViewModel = new(ConnectionManager, fileOpenDialog);
_sensorsViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_coolingViewModel = new(ConnectionManager, _devicesViewModel, _sensorsViewModel, _metadataService);
_programmingViewModel = new(ConnectionManager);
Expand Down

0 comments on commit cb38b82

Please sign in to comment.