Skip to content

Commit

Permalink
Implemnted SetDesktopSize message and added an auto resizing feature …
Browse files Browse the repository at this point in the history
…th the Avalonia VncView
  • Loading branch information
MarcusWichelmann committed Nov 2, 2020
1 parent 71f0fd7 commit f875564
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 21 deletions.
23 changes: 12 additions & 11 deletions samples/AvaloniaVncClient/Views/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
</Grid>
</Border>

<Border DockPanel.Dock="Right" BorderBrush="LightGray" BorderThickness="1,0,0,0">
<Border DockPanel.Dock="Right" BorderBrush="LightGray" BorderThickness="1,0,0,0" IsVisible="{Binding RfbConnection, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="Auto,*,Auto,*" Margin="10">
<TextBlock Grid.Row="0" Margin="0,0,0,10" FontWeight="Bold" Text="Used Message Types:" />
<ListBox Grid.Row="1" Margin="0,0,0,10" Items="{Binding RfbConnection.UsedMessageTypes}" BorderThickness="0">
Expand All @@ -63,18 +63,19 @@
</Grid>
</Border>

<StackPanel Orientation="Vertical" Margin="10" Spacing="10">
<Grid ColumnDefinitions="3*,*">
<TextBox Grid.Column="0" Watermark="Host" UseFloatingWatermark="True" Text="{Binding Host}" Margin="0,0,10,0" />
<TextBox Grid.Column="1" Watermark="Port" UseFloatingWatermark="True" Text="{Binding Port}" />
<Border DockPanel.Dock="Top" BorderBrush="LightGray" BorderThickness="0,0,0,1">
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="3*,*" Margin="10">
<TextBox Grid.Row="0" Grid.Column="0" Watermark="Host" UseFloatingWatermark="True" Text="{Binding Host}" Margin="0,0,10,0" />
<TextBox Grid.Row="0" Grid.Column="1" Watermark="Port" UseFloatingWatermark="True" Text="{Binding Port}" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Name="ConnectButton" Command="{Binding ConnectCommand}" Margin="0,10,0,0" IsDefault="True" />
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Foreground="Red" Text="{Binding ErrorMessage}"
IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" Margin="0,10,0,0" />
</Grid>
</Border>

<Button Name="ConnectButton" Command="{Binding ConnectCommand}" IsDefault="True" />
<TextBlock Foreground="Red" Text="{Binding ErrorMessage}"
IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />

<vnc:VncView Connection="{Binding RfbConnection, Mode=OneWay}" />
</StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<vnc:VncView Connection="{Binding RfbConnection}" OptimalSize="{Binding $parent[ScrollViewer].Viewport}" />
</ScrollViewer>
</DockPanel>

</Window>
11 changes: 7 additions & 4 deletions src/MarcusW.VncClient.Avalonia/MarcusW.VncClient.Avalonia.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.7"/>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="all"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.3"/>
<PackageReference Include="Avalonia" Version="0.9.7" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MarcusW.VncClient\MarcusW.VncClient.csproj"/>
<ProjectReference Include="..\MarcusW.VncClient\MarcusW.VncClient.csproj" />
</ItemGroup>

<ItemGroup>
Expand All @@ -17,6 +17,9 @@
<Compile Update="VncView.KeyInput.cs">
<DependentUpon>VncView.cs</DependentUpon>
</Compile>
<Compile Update="VncView.Sizing.cs">
<DependentUpon>VncView.cs</DependentUpon>
</Compile>
</ItemGroup>

</Project>
139 changes: 139 additions & 0 deletions src/MarcusW.VncClient.Avalonia/VncView.Sizing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia;
using Avalonia.Controls;
using MarcusW.VncClient.Protocol.Implementation.MessageTypes.Outgoing;

namespace MarcusW.VncClient.Avalonia
{
public partial class VncView
{
private enum SizeSource
{
None,
OwnBounds,
OptimalSizeProperty
}

private static readonly TimeSpan ThrottleTime = TimeSpan.FromSeconds(0.5);

/// <summary>
/// Defines the <see cref="AutoResizeRemote"/> property.
/// </summary>
public static readonly DirectProperty<VncView, bool> AutoResizeRemoteProperty =
AvaloniaProperty.RegisterDirect<VncView, bool>(nameof(AutoResizeRemote), o => o.AutoResizeRemote, (o, v) => o.AutoResizeRemote = v, true);

/// <summary>
/// Defines the <see cref="OptimalSize"/> property.
/// </summary>
public static readonly DirectProperty<VncView, global::Avalonia.Size?> OptimalSizeProperty =
AvaloniaProperty.RegisterDirect<VncView, global::Avalonia.Size?>(nameof(OptimalSize), o => o.OptimalSize, (o, v) => o.OptimalSize = v);

private bool _autoResizeRemote = true;
private global::Avalonia.Size? _optimalSize = null;

private SizeSource _sizeSource = SizeSource.None;
private IDisposable _sizeSubscription = Disposable.Empty;

/// <summary>
/// Gets or sets whether the remote view should be automatically resized to fit the current <see cref="VncView"/>'s size (or the <see cref="OptimalSize"/>).
/// </summary>
public bool AutoResizeRemote
{
get => _autoResizeRemote;
set
{
if (value)
SetSizeSource(OptimalSize == null ? SizeSource.OwnBounds : SizeSource.OptimalSizeProperty);
else
SetSizeSource(SizeSource.None);

SetAndRaise(AutoResizeRemoteProperty, ref _autoResizeRemote, value);
}
}

/// <summary>
/// Gets or sets the optimal size value that is used to resize the remote view to make it fit into this optimal size.
/// If set to <see langword="null"/>, the size of this <see cref="VncView"/> itself is used as the optimal size for the remote view.
/// </summary>
/// <remarks>
/// This property is useful when this <see cref="VncView"/> is contained in e.g. a <see cref="ScrollViewer"/>.
/// In this case the view would never shrink when this property isn't used.
/// </remarks>
public global::Avalonia.Size? OptimalSize
{
get => _optimalSize;
set
{
if (AutoResizeRemote)
SetSizeSource(value == null ? SizeSource.OwnBounds : SizeSource.OptimalSizeProperty);
else
SetSizeSource(SizeSource.None);

SetAndRaise(OptimalSizeProperty, ref _optimalSize, value);
}
}

private void InitSizing()
{
SetSizeSource(SizeSource.OwnBounds);
}

private void SetSizeSource(SizeSource newSizeSource)
{
if (newSizeSource == _sizeSource)
return;
_sizeSource = newSizeSource;

// Source has changed, dispose the previous subscription
_sizeSubscription.Dispose();

// Disable resizing?
if (_sizeSource == SizeSource.None)
{
_sizeSubscription = Disposable.Empty;
return;
}

// Setup a new subscription
IObservable<global::Avalonia.Size> observable = _sizeSource == SizeSource.OptimalSizeProperty
? this.GetObservable(OptimalSizeProperty).Where(s => s.HasValue).Select(s => s!.Value)
: this.GetObservable(BoundsProperty).Select(bounds => bounds.Size);
_sizeSubscription = observable.Distinct().Throttle(ThrottleTime).Subscribe(HandleResize);
}

private void HandleResize(global::Avalonia.Size size)
{
RfbConnection? connection = Connection;
if (connection == null)
return;

if (!connection.DesktopIsResizable)
return;

connection.EnqueueMessage(new SetDesktopSizeMessage((currentSize, currentLayout) => {
var newSize = new Size((int)size.Width, (int)size.Height);
var newRectangle = new Rectangle(Position.Origin, newSize);
Screen newScreen;
if (!currentLayout.Any())
{
// Create a new layout with one screen
newScreen = new Screen(1, newRectangle, 0);
}
else
{
// If there is more than one screen, only use one because multi-monitor is not supported
Screen firstScreen = currentLayout.First();
newScreen = new Screen(firstScreen.Id, newRectangle, firstScreen.Flags);
}
return (newSize, new[] { newScreen }.ToImmutableHashSet());
}));
}
}
}
7 changes: 6 additions & 1 deletion src/MarcusW.VncClient.Avalonia/VncView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ static VncView()
/// Gets or sets the connection that is shown in this VNC view.
/// </summary>
/// <remarks>
/// Interactions with this control will be forwarded to the selected <see cref="RfbConnection"/>.
/// Interactions with this <see cref="VncView"/> will be forwarded to the selected <see cref="RfbConnection"/>.
/// In case this property is set to <see langword="null"/>, no connection will be attached to this view.
/// </remarks>
public RfbConnection? Connection
Expand Down Expand Up @@ -65,6 +65,11 @@ public RfbConnection? Connection
}
}

public VncView()
{
InitSizing();
}

/// <inheritdoc />
public virtual void RingBell()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public static IEnumerable<IMessageType> GetDefaultMessageTypes(RfbConnectionCont
yield return new EnableContinuousUpdatesMessageType();
yield return new PointerEventMessageType();
yield return new KeyEventMessageType();
yield return new SetDesktopSizeMessageType();
yield return new SetDesktopSizeMessageType(context);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
using System;
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using MarcusW.VncClient.Protocol.EncodingTypes;
using MarcusW.VncClient.Protocol.MessageTypes;

namespace MarcusW.VncClient.Protocol.Implementation.MessageTypes.Outgoing
Expand All @@ -8,6 +13,8 @@ namespace MarcusW.VncClient.Protocol.Implementation.MessageTypes.Outgoing
/// </summary>
public class SetDesktopSizeMessageType : IOutgoingMessageType
{
private readonly ProtocolState _state;

/// <inheritdoc />
public byte Id => (byte)WellKnownOutgoingMessageType.SetDesktopSize;

Expand All @@ -17,18 +24,111 @@ public class SetDesktopSizeMessageType : IOutgoingMessageType
/// <inheritdoc />
public bool IsStandardMessageType => false;

/// <summary>
/// Initializes a new instance of the <see cref="SetDesktopSizeMessageType"/>.
/// </summary>
/// <param name="context">The connection context.</param>
public SetDesktopSizeMessageType(RfbConnectionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
_state = context.GetState<ProtocolState>();
}

/// <inheritdoc />
public void WriteToTransport(IOutgoingMessage<IOutgoingMessageType> message, ITransport transport, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
if (message == null)
throw new ArgumentNullException(nameof(message));
if (transport == null)
throw new ArgumentNullException(nameof(transport));
if (!(message is SetDesktopSizeMessage setDesktopSizeMessage))
throw new ArgumentException($"Message is no {nameof(SetDesktopSizeMessage)}.", nameof(message));

cancellationToken.ThrowIfCancellationRequested();

// Execute the mutation
(Size size, IImmutableSet<Screen> layout) = setDesktopSizeMessage.MutationFunc.Invoke(_state.RemoteFramebufferSize, _state.RemoteFramebufferLayout);

// Calculate message size
int messageSize = 2 + 2 * sizeof(ushort) + 2 + layout.Count * (sizeof(uint) + 4 * sizeof(ushort) + sizeof(uint));

// Allocate buffer
Span<byte> buffer = messageSize <= 1024 ? stackalloc byte[messageSize] : new byte[messageSize];

// Message header
buffer[0] = Id;
buffer[1] = 0; // Padding

// Size
BinaryPrimitives.WriteUInt16BigEndian(buffer[2..], (ushort)size.Width);
BinaryPrimitives.WriteUInt16BigEndian(buffer[4..], (ushort)size.Height);

// Number of screens
buffer[6] = (byte)layout.Count;
buffer[7] = 0; // Padding

// Screens
var bufferPosition = 8;
foreach (Screen screen in layout)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer[bufferPosition..], screen.Id);
bufferPosition += sizeof(int);

BinaryPrimitives.WriteUInt16BigEndian(buffer[bufferPosition..], (ushort)screen.Rectangle.Position.X);
bufferPosition += sizeof(ushort);

BinaryPrimitives.WriteUInt16BigEndian(buffer[bufferPosition..], (ushort)screen.Rectangle.Position.Y);
bufferPosition += sizeof(ushort);

BinaryPrimitives.WriteUInt16BigEndian(buffer[bufferPosition..], (ushort)screen.Rectangle.Size.Width);
bufferPosition += sizeof(ushort);

BinaryPrimitives.WriteUInt16BigEndian(buffer[bufferPosition..], (ushort)screen.Rectangle.Size.Height);
bufferPosition += sizeof(ushort);

BinaryPrimitives.WriteUInt32BigEndian(buffer[bufferPosition..], screen.Flags);
bufferPosition += sizeof(int);
}

Debug.Assert(bufferPosition == messageSize, "bufferPosition == messageSize");

// Write buffer to stream
transport.Stream.Write(buffer);
}
}

/// <summary>
/// A message for updating the remote framebuffer size and layout.
/// </summary>
public class SetDesktopSizeMessage : IOutgoingMessage<SetDesktopSizeMessageType>
{
// TODO
/// <summary>
/// Represents the method that mutates a remote framebuffer size and layout.
/// </summary>
/// <param name="size">The current size.</param>
/// <param name="layout">The current layout.</param>
public delegate (Size size, IImmutableSet<Screen> layout) MutationFuncDelegate(Size size, IImmutableSet<Screen> layout);

/// <summary>
/// Gets the function that mutates the current remote framebuffer size and layout.
/// It will be called by the sender thread right before the message gets sent.
/// </summary>
public MutationFuncDelegate MutationFunc { get; }

/// <summary>
/// Initializes a new instance of the <see cref="SetDesktopSizeMessage"/>.
/// </summary>
/// <param name="mutationFunc">
/// The function that mutates the current remote framebuffer size and layout.
/// It will be called by the sender thread right before the message gets sent.
/// </param>
public SetDesktopSizeMessage(MutationFuncDelegate mutationFunc)
{
MutationFunc = mutationFunc ?? throw new ArgumentNullException(nameof(mutationFunc));
}

/// <inheritdoc />
public string? GetParametersOverview() => "TODO TODO TODO TODO TODO TODO";
public string? GetParametersOverview() => $"MutationFunc: {MutationFunc}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ public bool ServerSupportsContinuousUpdates
public bool ServerSupportsExtendedDesktopSize
{
get => _serverSupportsExtendedDesktopSizeValue.Value;
set => _serverSupportsExtendedDesktopSizeValue.Value = value;
set
{
_serverSupportsExtendedDesktopSizeValue.Value = value;
_context.ConnectionDetails.SetDesktopIsResizable(value);
}
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/MarcusW.VncClient/Protocol/RfbConnectionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ internal ConnectionDetailsAccessor(RfbConnection connection)
/// <param name="desktopName">The new desktop name.</param>
public void SetDesktopName(string? desktopName) => _connection.DesktopName = desktopName;

/// <summary>
/// Sets the value of the <seealso cref="RfbConnection.DesktopIsResizable"/> property on the <see cref="RfbConnection"/> object.
/// </summary>
/// <param name="desktopIsResizable">The new state.</param>
public void SetDesktopIsResizable(bool desktopIsResizable) => _connection.DesktopIsResizable = desktopIsResizable;

/// <summary>
/// Sets the value of the <seealso cref="RfbConnection.ContinuousUpdatesEnabled"/> property on the <see cref="RfbConnection"/> object.
/// </summary>
Expand Down
Loading

0 comments on commit f875564

Please sign in to comment.