Skip to content

Commit

Permalink
⚒️ Continue with implementing embedded monitors in the UI.
Browse files Browse the repository at this point in the history
Among other things, split the UI components in multiple parts to make it easier to address ImageCropper quirks.
Will probably still have to fork the component in the end.
  • Loading branch information
hexawyz committed Feb 1, 2025
1 parent babc050 commit a8abb06
Show file tree
Hide file tree
Showing 18 changed files with 394 additions and 199 deletions.
49 changes: 48 additions & 1 deletion src/Exo/Service/Exo.Service.Core/EmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,28 @@ public async ValueTask<bool> SetBuiltInGraphicsAsync(Guid graphicsId, Cancellati
return false;
}

public async ValueTask<bool> SetImageAsync(UInt128 imageId, Rectangle region, CancellationToken cancellationToken)
{
if (imageId != _currentImageId && (region.Left != _currentRegionLeft || region.Top != _currentRegionTop || region.Width != _currentRegionWidth || region.Height != _currentRegionHeight))
{
if (_monitor is not null)
{
// TODO: Implement generation in the image storage.
//await _monitor.SetImageAsync();
}

_currentGraphics = default;
_currentImageId = imageId;
_currentRegionLeft = (ushort)region.Left;
_currentRegionTop = (ushort)region.Top;
_currentRegionWidth = (ushort)region.Width;
_currentRegionHeight = (ushort)region.Height;

return true;
}
return false;
}

public async ValueTask RestoreConfigurationAsync(ImageStorageService imageStorageService, CancellationToken cancellationToken)
{
if (_currentGraphics != default)
Expand Down Expand Up @@ -630,7 +652,32 @@ public async ValueTask SetBuiltInGraphicsAsync(Guid deviceId, Guid monitorId, Gu
{
if (!deviceState.EmbeddedMonitors.TryGetValue(monitorId, out var monitorState)) throw new InvalidOperationException("Embedded monitor not found.");

if (!(await monitorState.SetBuiltInGraphicsAsync(graphicsId, cancellationToken).ConfigureAwait(false))) return;
if (!await monitorState.SetBuiltInGraphicsAsync(graphicsId, cancellationToken).ConfigureAwait(false)) return;

configuration = monitorState.CreatePersistedConfiguration();
}

await PersistConfigurationAsync(deviceState.EmbeddedMonitorConfigurationContainer, monitorId, configuration, cancellationToken).ConfigureAwait(false);
}

public async ValueTask SetImageAsync(Guid deviceId, Guid monitorId, UInt128 imageId, Rectangle imageRegion, CancellationToken cancellationToken)
{
if (!_embeddedMonitorDeviceStates.TryGetValue(deviceId, out var deviceState)) throw new InvalidOperationException("Device not found.");
if ((uint)imageRegion.Left > ushort.MaxValue ||
(uint)imageRegion.Top > ushort.MaxValue ||
(uint)imageRegion.Width > ushort.MaxValue ||
(uint)imageRegion.Height > ushort.MaxValue)
{
throw new ArgumentException("Invalid crop region.");
}

PersistedMonitorConfiguration configuration;

using (await deviceState.Lock.WaitAsync(cancellationToken).ConfigureAwait(false))
{
if (!deviceState.EmbeddedMonitors.TryGetValue(monitorId, out var monitorState)) throw new InvalidOperationException("Embedded monitor not found.");

if (!await monitorState.SetImageAsync(imageId, imageRegion, cancellationToken).ConfigureAwait(false)) return;

configuration = monitorState.CreatePersistedConfiguration();
}
Expand Down
3 changes: 3 additions & 0 deletions src/Exo/Service/Exo.Service.Grpc/GrpcConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
using GrpcImageInformation = Exo.Contracts.Ui.Settings.ImageInformation;
using GrpcImageFormat = Exo.Contracts.Ui.Settings.ImageFormat;
using GrpcSize = Exo.Contracts.Ui.Settings.Size;
using GrpcRectangle = Exo.Contracts.Ui.Settings.Rectangle;
using VendorIdSource = DeviceTools.VendorIdSource;
using Exo.Contracts.Ui.Settings.Cooling;
using Exo.Cooling.Configuration;
Expand Down Expand Up @@ -180,6 +181,8 @@ public static GrpcMonitorShape ToGrpc(this MonitorShape shape)
};

public static GrpcSize ToGrpc(this Size size) => new() { Width = size.Width, Height = size.Height };
public static GrpcRectangle ToGrpc(this Rectangle rectangle) => new() { Left = rectangle.Left, Top = rectangle.Top, Width = rectangle.Width, Height = rectangle.Height };
public static Rectangle FromGrpc(this GrpcRectangle rectangle) => new() { Left = rectangle.Left, Top = rectangle.Top, Width = rectangle.Width, Height = rectangle.Height };

public static GrpcImageInformation ToGrpc(this ImageInformation imageInformation)
=> new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public async ValueTask SetBuiltInGraphicsAsync(EmbeddedMonitorSetBuiltInGraphics

public async ValueTask SetImageAsync(EmbeddedMonitorSetImageRequest request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
await _embeddedMonitorService.SetImageAsync(request.DeviceId, request.MonitorId, request.ImageId, request.CropRegion.FromGrpc(), cancellationToken).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Runtime.Serialization;

namespace Exo.Contracts.Ui.Settings;

[DataContract]
public sealed class EmbeddedMonitorSetBuiltInGraphicsRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public Guid GraphicsId { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Runtime.Serialization;

namespace Exo.Contracts.Ui.Settings;

[DataContract]
public sealed class EmbeddedMonitorSetImageRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public UInt128 ImageId { get; init; }
[DataMember(Order = 4)]
public Rectangle CropRegion { get; init; }
}
25 changes: 0 additions & 25 deletions src/Exo/Ui/Exo.Contracts.Ui.Settings/IEmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Runtime.Serialization;
using System.ServiceModel;

namespace Exo.Contracts.Ui.Settings;
Expand All @@ -19,27 +18,3 @@ public interface IEmbeddedMonitorService
[OperationContract(Name = "SetImage")]
ValueTask SetImageAsync(EmbeddedMonitorSetImageRequest request, CancellationToken cancellationToken);
}

[DataContract]
public sealed class EmbeddedMonitorSetBuiltInGraphicsRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public Guid GraphicsId { get; init; }
}

[DataContract]
public sealed class EmbeddedMonitorSetImageRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public UInt128 ImageId { get; init; }
[DataMember(Order = 4)]
public Rectangle CropZone { get; init; }
}
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 @@ -71,6 +71,8 @@ internal static class ChangedProperty
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 AspectRatio = new (nameof(AspectRatio));
public static readonly PropertyChangedEventArgs CropRectangle = new (nameof(CropRectangle));
public static readonly PropertyChangedEventArgs HasBuiltInGraphics = new (nameof(HasBuiltInGraphics));
public static readonly PropertyChangedEventArgs CurrentGraphics = new (nameof(CurrentGraphics));
}
2 changes: 1 addition & 1 deletion src/Exo/Ui/Exo.Settings.Ui/DevicePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<TextBlock x:Uid="EmbeddedMonitorSettingsSectionHeader" Margin="{StaticResource TitleTextIconMargin}" />
</StackPanel>
</Expander.Header>
<local:EmbeddedMonitorSettingsControl DataContext="{Binding EmbeddedMonitorFeatures, Mode=OneWay}" />
<local:EmbeddedMonitorDeviceSettingsControl DataContext="{Binding EmbeddedMonitorFeatures, Mode=OneWay}" />
</Expander>
<Expander
Margin="{StaticResource VerticalItemSpacing}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Exo.Settings.Ui.EmbeddedMonitorDeviceSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exo.Settings.Ui"
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"
d:DataContext="{d:DesignInstance vm:EmbeddedMonitorFeaturesViewModel, IsDesignTimeCreatable=False}">
<ContentControl Content="{Binding}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<ContentControl.ContentTemplateSelector>
<lts:EmbeddedMonitorSettingTemplateSelector>
<lts:EmbeddedMonitorSettingTemplateSelector.SingleMonitorTemplate>
<DataTemplate x:DataType="vm:EmbeddedMonitorFeaturesViewModel">
<local:EmbeddedMonitorSettingControl Monitor="{Binding EmbeddedMonitors[0]}" />
</DataTemplate>
</lts:EmbeddedMonitorSettingTemplateSelector.SingleMonitorTemplate>
<lts:EmbeddedMonitorSettingTemplateSelector.MonitorMatrixTemplate>
<DataTemplate x:DataType="vm:EmbeddedMonitorFeaturesViewModel">
<Grid>
<!-- TODO: Create a Control to display items in a fixed grid -->
<ItemsView ItemsSource="{Binding EmbeddedMonitors}">
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="vm:EmbeddedMonitorViewModel">
<ItemContainer>
<Grid Width="{Binding DisplayWidth}" Height="{Binding DisplayHeight}" Background="Black" Margin="3">
<Image></Image>
</Grid>
</ItemContainer>
</DataTemplate>
</ItemsView.ItemTemplate>
<ItemsView.Layout>
<UniformGridLayout MaximumRowsOrColumns="8" />
</ItemsView.Layout>
</ItemsView>
</Grid>
</DataTemplate>
</lts:EmbeddedMonitorSettingTemplateSelector.MonitorMatrixTemplate>
<lts:EmbeddedMonitorSettingTemplateSelector.MultiMonitorTemplate>
<DataTemplate x:DataType="vm:EmbeddedMonitorFeaturesViewModel">
<Grid></Grid>
</DataTemplate>
</lts:EmbeddedMonitorSettingTemplateSelector.MultiMonitorTemplate>
</lts:EmbeddedMonitorSettingTemplateSelector>
</ContentControl.ContentTemplateSelector>
</ContentControl>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml.Controls;

namespace Exo.Settings.Ui;

internal sealed partial class EmbeddedMonitorDeviceSettingsControl : UserControl
{
public EmbeddedMonitorDeviceSettingsControl()
{
InitializeComponent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Exo.Settings.Ui.EmbeddedMonitorImageSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exo.Settings.Ui"
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">

<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{StaticResource PropertyLabelColumnWidth}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<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" MinHeight="{Binding DisplayHeight}">
<controls:ImageCropper
x:Name="ImageCropper"
CropShape="{Binding Shape, Converter={StaticResource MonitorShapeToCropShapeConverter}}"
Source="{Binding Image, Converter={StaticResource ImageToWriteableBitmapConverter}}"
ThumbPlacement="Corners"
Padding="20"
Height="{Binding ImageSize.Height}" />
</Grid>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.ComponentModel;
using Exo.Settings.Ui.ViewModels;
using Exo.Ui;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace Exo.Settings.Ui;

internal sealed partial class EmbeddedMonitorImageSettingsControl : UserControl
{
public EmbeddedMonitorImageGraphicsViewModel? ImageGraphics
{
get => (EmbeddedMonitorImageGraphicsViewModel)GetValue(ImageGraphicsProperty);
set => SetValue(ImageGraphicsProperty, value);
}

public static readonly DependencyProperty ImageGraphicsProperty = DependencyProperty.Register
(
nameof(ImageGraphics),
typeof(EmbeddedMonitorImageGraphicsViewModel),
typeof(EmbeddedMonitorImageSettingsControl),
new PropertyMetadata(null, (d, e) => ((EmbeddedMonitorImageSettingsControl)d).OnPropertyChanged(e))
);

public EmbeddedMonitorImageSettingsControl()
{
InitializeComponent();
}

private void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == ImageGraphicsProperty)
{
if (e.OldValue is EmbeddedMonitorImageGraphicsViewModel oldValue) oldValue.PropertyChanged -= OnImageGraphicsPropertyChanged;
if (e.NewValue is EmbeddedMonitorImageGraphicsViewModel newValue) newValue.PropertyChanged += OnImageGraphicsPropertyChanged;
UpdateCroppedRegion();
}
}

private void OnImageGraphicsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (BindableObject.Equals(e, ChangedProperty.CropRectangle))
{
UpdateCroppedRegion();
}
}

private void UpdateCroppedRegion()
{
if (ImageGraphics is { } imageGraphics)
{
var rectangle = imageGraphics.CropRectangle;

ImageCropper.TrySetCroppedRegion(new(rectangle.Left, rectangle.Top, rectangle.Width, rectangle.Height));
}
}
}
Loading

0 comments on commit a8abb06

Please sign in to comment.