112 changed files with 9346 additions and 993 deletions
@ -0,0 +1,59 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui"> |
|||
<StackPanel Orientation="Vertical" Gap="4"> |
|||
<TextBlock Classes="h1">AutoCompleteBox</TextBlock> |
|||
<TextBlock Classes="h2">A control into which the user can input text</TextBlock> |
|||
|
|||
<StackPanel Orientation="Horizontal" |
|||
Margin="0,16,0,0" |
|||
HorizontalAlignment="Center" |
|||
Gap="8"> |
|||
<StackPanel Orientation="Vertical"> |
|||
<TextBlock Text="MinimumPrefixLength: 1"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
MinimumPrefixLength="1"/> |
|||
<TextBlock Text="MinimumPrefixLength: 3"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
MinimumPrefixLength="3"/> |
|||
<TextBlock Text="MinimumPopulateDelay: 1 Second"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
MinimumPopulateDelay="1"/> |
|||
<TextBlock Text="MaxDropDownHeight: 60"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
MaxDropDownHeight="60"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
Watermark="Watermark"/> |
|||
<TextBlock Text="Disabled"/> |
|||
<AutoCompleteBox Width="200" |
|||
IsEnabled="False"/> |
|||
</StackPanel> |
|||
|
|||
|
|||
<StackPanel Orientation="Vertical"> |
|||
|
|||
<TextBlock Text="ValueMemeberSelector"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
ValueMemberSelector="Capital"/> |
|||
<TextBlock Text="ValueMemberBinding"/> |
|||
<AutoCompleteBox Width="200" |
|||
Margin="0,0,0,8" |
|||
ValueMemberBinding="{Binding Capital}"/> |
|||
<TextBlock Text="Multi-Binding"/> |
|||
<AutoCompleteBox Name="MultiBindingBox" |
|||
Width="200" |
|||
Margin="0,0,0,8" |
|||
FilterMode="Contains"/> |
|||
<TextBlock Text="Async Populate"/> |
|||
<AutoCompleteBox Name="AsyncBox" |
|||
Width="200" |
|||
Margin="0,0,0,8" |
|||
FilterMode="None"/> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,143 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Markup; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Markup.Xaml.Data; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class AutoCompleteBoxPage : UserControl |
|||
{ |
|||
public class StateData |
|||
{ |
|||
public string Name { get; private set; } |
|||
public string Abbreviation { get; private set; } |
|||
public string Capital { get; private set; } |
|||
|
|||
public StateData(string name, string abbreviatoin, string capital) |
|||
{ |
|||
Name = name; |
|||
Abbreviation = abbreviatoin; |
|||
Capital = capital; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return Name; |
|||
} |
|||
} |
|||
|
|||
private StateData[] BuildAllStates() |
|||
{ |
|||
return new StateData[] |
|||
{ |
|||
new StateData("Alabama","AL","Montgomery"), |
|||
new StateData("Alaska","AK","Juneau"), |
|||
new StateData("Arizona","AZ","Phoenix"), |
|||
new StateData("Arkansas","AR","Little Rock"), |
|||
new StateData("California","CA","Sacramento"), |
|||
new StateData("Colorado","CO","Denver"), |
|||
new StateData("Connecticut","CT","Hartford"), |
|||
new StateData("Delaware","DE","Dover"), |
|||
new StateData("Florida","FL","Tallahassee"), |
|||
new StateData("Georgia","GA","Atlanta"), |
|||
new StateData("Hawaii","HI","Honolulu"), |
|||
new StateData("Idaho","ID","Boise"), |
|||
new StateData("Illinois","IL","Springfield"), |
|||
new StateData("Indiana","IN","Indianapolis"), |
|||
new StateData("Iowa","IA","Des Moines"), |
|||
new StateData("Kansas","KS","Topeka"), |
|||
new StateData("Kentucky","KY","Frankfort"), |
|||
new StateData("Louisiana","LA","Baton Rouge"), |
|||
new StateData("Maine","ME","Augusta"), |
|||
new StateData("Maryland","MD","Annapolis"), |
|||
new StateData("Massachusetts","MA","Boston"), |
|||
new StateData("Michigan","MI","Lansing"), |
|||
new StateData("Minnesota","MN","St. Paul"), |
|||
new StateData("Mississippi","MS","Jackson"), |
|||
new StateData("Missouri","MO","Jefferson City"), |
|||
new StateData("Montana","MT","Helena"), |
|||
new StateData("Nebraska","NE","Lincoln"), |
|||
new StateData("Nevada","NV","Carson City"), |
|||
new StateData("New Hampshire","NH","Concord"), |
|||
new StateData("New Jersey","NJ","Trenton"), |
|||
new StateData("New Mexico","NM","Santa Fe"), |
|||
new StateData("New York","NY","Albany"), |
|||
new StateData("North Carolina","NC","Raleigh"), |
|||
new StateData("North Dakota","ND","Bismarck"), |
|||
new StateData("Ohio","OH","Columbus"), |
|||
new StateData("Oklahoma","OK","Oklahoma City"), |
|||
new StateData("Oregon","OR","Salem"), |
|||
new StateData("Pennsylvania","PA","Harrisburg"), |
|||
new StateData("Rhode Island","RI","Providence"), |
|||
new StateData("South Carolina","SC","Columbia"), |
|||
new StateData("South Dakota","SD","Pierre"), |
|||
new StateData("Tennessee","TN","Nashville"), |
|||
new StateData("Texas","TX","Austin"), |
|||
new StateData("Utah","UT","Salt Lake City"), |
|||
new StateData("Vermont","VT","Montpelier"), |
|||
new StateData("Virginia","VA","Richmond"), |
|||
new StateData("Washington","WA","Olympia"), |
|||
new StateData("West Virginia","WV","Charleston"), |
|||
new StateData("Wisconsin","WI","Madison"), |
|||
new StateData("Wyoming","WY","Cheyenne"), |
|||
}; |
|||
} |
|||
public StateData[] States { get; private set; } |
|||
|
|||
public AutoCompleteBoxPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
|
|||
States = BuildAllStates(); |
|||
|
|||
foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) |
|||
{ |
|||
box.Items = States; |
|||
} |
|||
|
|||
var converter = new FuncMultiValueConverter<string, string>(parts => |
|||
{ |
|||
return String.Format("{0} ({1})", parts.ToArray()); |
|||
}); |
|||
var binding = new MultiBinding { Converter = converter }; |
|||
binding.Bindings.Add(new Binding("Name")); |
|||
binding.Bindings.Add(new Binding("Abbreviation")); |
|||
|
|||
var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox"); |
|||
multibindingBox.ValueMemberBinding = binding; |
|||
|
|||
var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox"); |
|||
asyncBox.AsyncPopulator = PopulateAsync; |
|||
} |
|||
private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox() |
|||
{ |
|||
return |
|||
this.GetLogicalDescendants() |
|||
.OfType<AutoCompleteBox>(); |
|||
} |
|||
|
|||
private bool StringContains(string str, string query) |
|||
{ |
|||
return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; |
|||
} |
|||
private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken) |
|||
{ |
|||
await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); |
|||
|
|||
return |
|||
States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) |
|||
.ToList(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui"> |
|||
<StackPanel Orientation="Vertical" Gap="4"> |
|||
<TextBlock Classes="h1">Drag+Drop</TextBlock> |
|||
<TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock> |
|||
|
|||
<StackPanel Orientation="Horizontal" |
|||
Margin="0,16,0,0" |
|||
HorizontalAlignment="Center" |
|||
Gap="16"> |
|||
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe"> |
|||
<TextBlock Name="DragState">Drag Me</TextBlock> |
|||
</Border> |
|||
<Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16" |
|||
DragDrop.AllowDrop="True"> |
|||
<TextBlock Name="DropState">Drop some text or files here</TextBlock> |
|||
</Border> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,71 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Markup.Xaml; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class DragAndDropPage : UserControl |
|||
{ |
|||
private TextBlock _DropState; |
|||
private TextBlock _DragState; |
|||
private Border _DragMe; |
|||
private int DragCount = 0; |
|||
|
|||
public DragAndDropPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
|
|||
_DragMe.PointerPressed += DoDrag; |
|||
|
|||
AddHandler(DragDrop.DropEvent, Drop); |
|||
AddHandler(DragDrop.DragOverEvent, DragOver); |
|||
} |
|||
|
|||
private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) |
|||
{ |
|||
DataObject dragData = new DataObject(); |
|||
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); |
|||
|
|||
var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy); |
|||
switch(result) |
|||
{ |
|||
case DragDropEffects.Copy: |
|||
_DragState.Text = "The text was copied"; break; |
|||
case DragDropEffects.Link: |
|||
_DragState.Text = "The text was linked"; break; |
|||
case DragDropEffects.None: |
|||
_DragState.Text = "The drag operation was canceled"; break; |
|||
} |
|||
} |
|||
|
|||
private void DragOver(object sender, DragEventArgs e) |
|||
{ |
|||
// Only allow Copy or Link as Drop Operations.
|
|||
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); |
|||
|
|||
// Only allow if the dragged data contains text or filenames.
|
|||
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) |
|||
e.DragEffects = DragDropEffects.None; |
|||
} |
|||
|
|||
private void Drop(object sender, DragEventArgs e) |
|||
{ |
|||
if (e.Data.Contains(DataFormats.Text)) |
|||
_DropState.Text = e.Data.GetText(); |
|||
else if (e.Data.Contains(DataFormats.FileNames)) |
|||
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
|
|||
_DropState = this.Find<TextBlock>("DropState"); |
|||
_DragState = this.Find<TextBlock>("DragState"); |
|||
_DragMe = this.Find<Border>("DragMe"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<StackPanel Orientation="Vertical" Gap="4"> |
|||
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock> |
|||
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock> |
|||
|
|||
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock> |
|||
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto"> |
|||
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35"> |
|||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock> |
|||
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock> |
|||
<CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock> |
|||
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock> |
|||
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
</Grid> |
|||
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35"> |
|||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock> |
|||
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}" |
|||
VerticalAlignment="Center" Margin="2"> |
|||
<DropDown.ItemTemplate> |
|||
<DataTemplate> |
|||
<StackPanel Orientation="Horizontal" Gap="2"> |
|||
<TextBlock Text="{Binding Name}"/> |
|||
<TextBlock Text="-"/> |
|||
<TextBlock Text="{Binding Value}"/> |
|||
</StackPanel> |
|||
</DataTemplate> |
|||
</DropDown.ItemTemplate> |
|||
</DropDown> |
|||
|
|||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock> |
|||
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}" |
|||
VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock> |
|||
<DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}" |
|||
VerticalAlignment="Center" Margin="2"/> |
|||
|
|||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock> |
|||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" /> |
|||
|
|||
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock> |
|||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" /> |
|||
</Grid> |
|||
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120"> |
|||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock> |
|||
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}" |
|||
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
|||
|
|||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock> |
|||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}" |
|||
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
|||
|
|||
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock> |
|||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center" |
|||
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
|||
|
|||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock> |
|||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center" |
|||
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
|||
|
|||
</Grid> |
|||
</Grid> |
|||
|
|||
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10"> |
|||
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock> |
|||
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" |
|||
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100" |
|||
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,94 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Markup.Xaml; |
|||
using ReactiveUI; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class NumericUpDownPage : UserControl |
|||
{ |
|||
public NumericUpDownPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
var viewModel = new NumbersPageViewModel(); |
|||
DataContext = viewModel; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
} |
|||
|
|||
public class NumbersPageViewModel : ReactiveObject |
|||
{ |
|||
private IList<FormatObject> _formats; |
|||
private FormatObject _selectedFormat; |
|||
private IList<Location> _spinnerLocations; |
|||
|
|||
public NumbersPageViewModel() |
|||
{ |
|||
SelectedFormat = Formats.FirstOrDefault(); |
|||
} |
|||
|
|||
public IList<FormatObject> Formats |
|||
{ |
|||
get |
|||
{ |
|||
return _formats ?? (_formats = new List<FormatObject>() |
|||
{ |
|||
new FormatObject() {Name = "Currency", Value = "C2"}, |
|||
new FormatObject() {Name = "Fixed point", Value = "F2"}, |
|||
new FormatObject() {Name = "General", Value = "G"}, |
|||
new FormatObject() {Name = "Number", Value = "N"}, |
|||
new FormatObject() {Name = "Percent", Value = "P"}, |
|||
new FormatObject() {Name = "Degrees", Value = "{0:N2} °"}, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public IList<Location> SpinnerLocations |
|||
{ |
|||
get |
|||
{ |
|||
if (_spinnerLocations == null) |
|||
{ |
|||
_spinnerLocations = new List<Location>(); |
|||
foreach (Location value in Enum.GetValues(typeof(Location))) |
|||
{ |
|||
_spinnerLocations.Add(value); |
|||
} |
|||
} |
|||
return _spinnerLocations ; |
|||
} |
|||
} |
|||
|
|||
public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>() |
|||
{ |
|||
new CultureInfo("en-US"), |
|||
new CultureInfo("en-GB"), |
|||
new CultureInfo("fr-FR"), |
|||
new CultureInfo("ar-DZ"), |
|||
new CultureInfo("zh-CN"), |
|||
new CultureInfo("cs-CZ") |
|||
}; |
|||
|
|||
public FormatObject SelectedFormat |
|||
{ |
|||
get { return _selectedFormat; } |
|||
set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); } |
|||
} |
|||
} |
|||
|
|||
public class FormatObject |
|||
{ |
|||
public string Value { get; set; } |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
// General Information about an assembly is controlled through the following
|
|||
// set of attributes. Change these attribute values to modify the information
|
|||
// associated with an assembly.
|
|||
[assembly: AssemblyTitle("ControlCatalog")] |
|||
[assembly: AssemblyDescription("")] |
|||
[assembly: AssemblyConfiguration("")] |
|||
[assembly: AssemblyCompany("")] |
|||
[assembly: AssemblyProduct("ControlCatalog")] |
|||
[assembly: AssemblyCopyright("Copyright © 2015")] |
|||
[assembly: AssemblyTrademark("")] |
|||
[assembly: AssemblyCulture("")] |
|||
|
|||
// Setting ComVisible to false makes the types in this assembly not visible
|
|||
// to COM components. If you need to access a type in this assembly from
|
|||
// COM, set the ComVisible attribute to true on that type.
|
|||
[assembly: ComVisible(false)] |
|||
|
|||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|||
[assembly: Guid("61bec86c-f307-4295-b5b8-9428610d7d55")] |
|||
|
|||
// Version information for an assembly consists of the following four values:
|
|||
//
|
|||
// Major Version
|
|||
// Minor Version
|
|||
// Build Number
|
|||
// Revision
|
|||
//
|
|||
// You can specify all the values or you can default the Build and Revision Numbers
|
|||
// by using the '*' as shown below:
|
|||
// [assembly: AssemblyVersion("1.0.*")]
|
|||
[assembly: AssemblyVersion("1.0.0.0")] |
|||
[assembly: AssemblyFileVersion("1.0.0.0")] |
|||
@ -0,0 +1,205 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using static System.Char; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
public struct StringTokenizer : IDisposable |
|||
{ |
|||
private const char DefaultSeparatorChar = ','; |
|||
|
|||
private readonly string _s; |
|||
private readonly int _length; |
|||
private readonly char _separator; |
|||
private readonly string _exceptionMessage; |
|||
private readonly IFormatProvider _formatProvider; |
|||
private int _index; |
|||
private int _tokenIndex; |
|||
private int _tokenLength; |
|||
|
|||
public StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null) |
|||
: this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage) |
|||
{ |
|||
_formatProvider = formatProvider; |
|||
} |
|||
|
|||
public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null) |
|||
{ |
|||
_s = s ?? throw new ArgumentNullException(nameof(s)); |
|||
_length = s?.Length ?? 0; |
|||
_separator = separator; |
|||
_exceptionMessage = exceptionMessage; |
|||
_formatProvider = CultureInfo.InvariantCulture; |
|||
_index = 0; |
|||
_tokenIndex = -1; |
|||
_tokenLength = 0; |
|||
|
|||
while (_index < _length && IsWhiteSpace(_s, _index)) |
|||
{ |
|||
_index++; |
|||
} |
|||
} |
|||
|
|||
public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_index != _length) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
} |
|||
|
|||
public bool TryReadInt32(out Int32 result, char? separator = null) |
|||
{ |
|||
var success = TryReadString(out var stringResult, separator); |
|||
result = success ? int.Parse(stringResult, _formatProvider) : 0; |
|||
return success; |
|||
} |
|||
|
|||
public int ReadInt32(char? separator = null) |
|||
{ |
|||
if (!TryReadInt32(out var result, separator)) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public bool TryReadDouble(out double result, char? separator = null) |
|||
{ |
|||
var success = TryReadString(out var stringResult, separator); |
|||
result = success ? double.Parse(stringResult, _formatProvider) : 0; |
|||
return success; |
|||
} |
|||
|
|||
public double ReadDouble(char? separator = null) |
|||
{ |
|||
if (!TryReadDouble(out var result, separator)) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public bool TryReadString(out string result, char? separator = null) |
|||
{ |
|||
var success = TryReadToken(separator ?? _separator); |
|||
result = CurrentToken; |
|||
return success; |
|||
} |
|||
|
|||
public string ReadString(char? separator = null) |
|||
{ |
|||
if (!TryReadString(out var result, separator)) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private bool TryReadToken(char separator) |
|||
{ |
|||
_tokenIndex = -1; |
|||
|
|||
if (_index >= _length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var c = _s[_index]; |
|||
|
|||
var index = _index; |
|||
var length = 0; |
|||
|
|||
while (_index < _length) |
|||
{ |
|||
c = _s[_index]; |
|||
|
|||
if (IsWhiteSpace(c) || c == separator) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
_index++; |
|||
length++; |
|||
} |
|||
|
|||
SkipToNextToken(separator); |
|||
|
|||
_tokenIndex = index; |
|||
_tokenLength = length; |
|||
|
|||
if (_tokenLength < 1) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private void SkipToNextToken(char separator) |
|||
{ |
|||
if (_index < _length) |
|||
{ |
|||
var c = _s[_index]; |
|||
|
|||
if (c != separator && !IsWhiteSpace(c)) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
|
|||
var length = 0; |
|||
|
|||
while (_index < _length) |
|||
{ |
|||
c = _s[_index]; |
|||
|
|||
if (c == separator) |
|||
{ |
|||
length++; |
|||
_index++; |
|||
|
|||
if (length > 1) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (!IsWhiteSpace(c)) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
_index++; |
|||
} |
|||
} |
|||
|
|||
if (length > 0 && _index >= _length) |
|||
{ |
|||
throw GetFormatException(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private FormatException GetFormatException() => |
|||
_exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException(); |
|||
|
|||
private static char GetSeparatorFromFormatProvider(IFormatProvider provider) |
|||
{ |
|||
var c = DefaultSeparatorChar; |
|||
|
|||
var formatInfo = NumberFormatInfo.GetInstance(provider); |
|||
if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0]) |
|||
{ |
|||
c = ';'; |
|||
} |
|||
|
|||
return c; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,998 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Data; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
|
|||
/// </summary>
|
|||
public class NumericUpDown : TemplatedControl |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="AllowSpin"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> AllowSpinProperty = |
|||
ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ButtonSpinnerLocation"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty = |
|||
ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ShowButtonSpinner"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> ShowButtonSpinnerProperty = |
|||
ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ClipValueToMinMax"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty = |
|||
AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax), |
|||
updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CultureInfo"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty = |
|||
AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo, |
|||
(o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FormatString"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string> FormatStringProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Increment"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> IncrementProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="IsReadOnly"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsReadOnlyProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly)); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Maximum"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> MaximumProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Minimum"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> MinimumProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ParsingNumberStyle"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty = |
|||
AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle), |
|||
updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Text"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<NumericUpDown, string> TextProperty = |
|||
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Value"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<NumericUpDown, double> ValueProperty = |
|||
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value, |
|||
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Watermark"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string> WatermarkProperty = |
|||
AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark)); |
|||
|
|||
private IDisposable _textBoxTextChangedSubscription; |
|||
|
|||
private double _value; |
|||
private string _text; |
|||
private bool _internalValueSet; |
|||
private bool _clipValueToMinMax; |
|||
private bool _isSyncingTextAndValueProperties; |
|||
private bool _isTextChangedFromUI; |
|||
private CultureInfo _cultureInfo; |
|||
private NumberStyles _parsingNumberStyle = NumberStyles.Any; |
|||
|
|||
/// <summary>
|
|||
/// Gets the Spinner template part.
|
|||
/// </summary>
|
|||
private Spinner Spinner { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the TextBox template part.
|
|||
/// </summary>
|
|||
private TextBox TextBox { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
|
|||
/// </summary>
|
|||
public bool AllowSpin |
|||
{ |
|||
get { return GetValue(AllowSpinProperty); } |
|||
set { SetValue(AllowSpinProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets current location of the <see cref="ButtonSpinner"/>.
|
|||
/// </summary>
|
|||
public Location ButtonSpinnerLocation |
|||
{ |
|||
get { return GetValue(ButtonSpinnerLocationProperty); } |
|||
set { SetValue(ButtonSpinnerLocationProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the spin buttons should be shown.
|
|||
/// </summary>
|
|||
public bool ShowButtonSpinner |
|||
{ |
|||
get { return GetValue(ShowButtonSpinnerProperty); } |
|||
set { SetValue(ShowButtonSpinnerProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets if the value should be clipped when minimum/maximum is reached.
|
|||
/// </summary>
|
|||
public bool ClipValueToMinMax |
|||
{ |
|||
get { return _clipValueToMinMax; } |
|||
set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current CultureInfo.
|
|||
/// </summary>
|
|||
public CultureInfo CultureInfo |
|||
{ |
|||
get { return _cultureInfo; } |
|||
set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the display format of the <see cref="Value"/>.
|
|||
/// </summary>
|
|||
public string FormatString |
|||
{ |
|||
get { return GetValue(FormatStringProperty); } |
|||
set { SetValue(FormatStringProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the amount in which to increment the <see cref="Value"/>.
|
|||
/// </summary>
|
|||
public double Increment |
|||
{ |
|||
get { return GetValue(IncrementProperty); } |
|||
set { SetValue(IncrementProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets if the control is read only.
|
|||
/// </summary>
|
|||
public bool IsReadOnly |
|||
{ |
|||
get { return GetValue(IsReadOnlyProperty); } |
|||
set { SetValue(IsReadOnlyProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum allowed value.
|
|||
/// </summary>
|
|||
public double Maximum |
|||
{ |
|||
get { return GetValue(MaximumProperty); } |
|||
set { SetValue(MaximumProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the minimum allowed value.
|
|||
/// </summary>
|
|||
public double Minimum |
|||
{ |
|||
get { return GetValue(MinimumProperty); } |
|||
set { SetValue(MinimumProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
|
|||
/// </summary>
|
|||
public NumberStyles ParsingNumberStyle |
|||
{ |
|||
get { return _parsingNumberStyle; } |
|||
set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the formatted string representation of the value.
|
|||
/// </summary>
|
|||
public string Text |
|||
{ |
|||
get { return _text; } |
|||
set { SetAndRaise(TextProperty, ref _text, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value.
|
|||
/// </summary>
|
|||
public double Value |
|||
{ |
|||
get { return _value; } |
|||
set |
|||
{ |
|||
value = OnCoerceValue(value); |
|||
SetAndRaise(ValueProperty, ref _value, value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
|
|||
/// </summary>
|
|||
public string Watermark |
|||
{ |
|||
get { return GetValue(WatermarkProperty); } |
|||
set { SetValue(WatermarkProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes new instance of <see cref="NumericUpDown"/> class.
|
|||
/// </summary>
|
|||
public NumericUpDown() |
|||
{ |
|||
Initialized += (sender, e) => |
|||
{ |
|||
if (!_internalValueSet && IsInitialized) |
|||
{ |
|||
SyncTextAndValueProperties(false, null, true); |
|||
} |
|||
|
|||
SetValidSpinDirection(); |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes static members of the <see cref="NumericUpDown"/> class.
|
|||
/// </summary>
|
|||
static NumericUpDown() |
|||
{ |
|||
CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); |
|||
FormatStringProperty.Changed.Subscribe(FormatStringChanged); |
|||
IncrementProperty.Changed.Subscribe(IncrementChanged); |
|||
IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); |
|||
MaximumProperty.Changed.Subscribe(OnMaximumChanged); |
|||
MinimumProperty.Changed.Subscribe(OnMinimumChanged); |
|||
TextProperty.Changed.Subscribe(OnTextChanged); |
|||
ValueProperty.Changed.Subscribe(OnValueChanged); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) |
|||
{ |
|||
if (TextBox != null) |
|||
{ |
|||
TextBox.PointerPressed -= TextBoxOnPointerPressed; |
|||
_textBoxTextChangedSubscription?.Dispose(); |
|||
} |
|||
TextBox = e.NameScope.Find<TextBox>("PART_TextBox"); |
|||
if (TextBox != null) |
|||
{ |
|||
TextBox.Text = Text; |
|||
TextBox.PointerPressed += TextBoxOnPointerPressed; |
|||
_textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); |
|||
} |
|||
|
|||
if (Spinner != null) |
|||
{ |
|||
Spinner.Spin -= OnSpinnerSpin; |
|||
} |
|||
|
|||
Spinner = e.NameScope.Find<Spinner>("PART_Spinner"); |
|||
|
|||
if (Spinner != null) |
|||
{ |
|||
Spinner.Spin += OnSpinnerSpin; |
|||
} |
|||
|
|||
SetValidSpinDirection(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void OnKeyDown(KeyEventArgs e) |
|||
{ |
|||
switch (e.Key) |
|||
{ |
|||
case Key.Enter: |
|||
var commitSuccess = CommitInput(); |
|||
e.Handled = !commitSuccess; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="CultureInfo"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SyncTextAndValueProperties(false, null); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="FormatString"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnFormatStringChanged(string oldValue, string newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SyncTextAndValueProperties(false, null); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Increment"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnIncrementChanged(double oldValue, double newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SetValidSpinDirection(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="IsReadOnly"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) |
|||
{ |
|||
SetValidSpinDirection(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Maximum"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnMaximumChanged(double oldValue, double newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SetValidSpinDirection(); |
|||
} |
|||
if (ClipValueToMinMax) |
|||
{ |
|||
Value = MathUtilities.Clamp(Value, Minimum, Maximum); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Minimum"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnMinimumChanged(double oldValue, double newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SetValidSpinDirection(); |
|||
} |
|||
if (ClipValueToMinMax) |
|||
{ |
|||
Value = MathUtilities.Clamp(Value, Minimum, Maximum); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Text"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnTextChanged(string oldValue, string newValue) |
|||
{ |
|||
if (IsInitialized) |
|||
{ |
|||
SyncTextAndValueProperties(true, Text); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Value"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void OnValueChanged(double oldValue, double newValue) |
|||
{ |
|||
if (!_internalValueSet && IsInitialized) |
|||
{ |
|||
SyncTextAndValueProperties(false, null, true); |
|||
} |
|||
|
|||
SetValidSpinDirection(); |
|||
|
|||
RaiseValueChangedEvent(oldValue, newValue); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Increment"/> property has to be coerced.
|
|||
/// </summary>
|
|||
/// <param name="baseValue">The value.</param>
|
|||
protected virtual double OnCoerceIncrement(double baseValue) |
|||
{ |
|||
return baseValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Maximum"/> property has to be coerced.
|
|||
/// </summary>
|
|||
/// <param name="baseValue">The value.</param>
|
|||
protected virtual double OnCoerceMaximum(double baseValue) |
|||
{ |
|||
return Math.Max(baseValue, Minimum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Minimum"/> property has to be coerced.
|
|||
/// </summary>
|
|||
/// <param name="baseValue">The value.</param>
|
|||
protected virtual double OnCoerceMinimum(double baseValue) |
|||
{ |
|||
return Math.Min(baseValue, Maximum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Value"/> property has to be coerced.
|
|||
/// </summary>
|
|||
/// <param name="baseValue">The value.</param>
|
|||
protected virtual double OnCoerceValue(double baseValue) |
|||
{ |
|||
return baseValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the OnSpin event when spinning is initiated by the end-user.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
protected virtual void OnSpin(SpinEventArgs e) |
|||
{ |
|||
if (e == null) |
|||
{ |
|||
throw new ArgumentNullException("e"); |
|||
} |
|||
|
|||
var handler = Spinned; |
|||
handler?.Invoke(this, e); |
|||
|
|||
if (e.Direction == SpinDirection.Increase) |
|||
{ |
|||
DoIncrement(); |
|||
} |
|||
else |
|||
{ |
|||
DoDecrement(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the <see cref="ValueChanged"/> event.
|
|||
/// </summary>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) |
|||
{ |
|||
var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); |
|||
RaiseEvent(e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts the formatted text to a value.
|
|||
/// </summary>
|
|||
private double ConvertTextToValue(string text) |
|||
{ |
|||
double result = 0; |
|||
|
|||
if (string.IsNullOrEmpty(text)) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
// Since the conversion from Value to text using a FormartString may not be parsable,
|
|||
// we verify that the already existing text is not the exact same value.
|
|||
var currentValueText = ConvertValueToText(); |
|||
if (Equals(currentValueText, text)) |
|||
{ |
|||
return Value; |
|||
} |
|||
|
|||
result = ConvertTextToValueCore(currentValueText, text); |
|||
|
|||
if (ClipValueToMinMax) |
|||
{ |
|||
return MathUtilities.Clamp(result, Minimum, Maximum); |
|||
} |
|||
|
|||
ValidateMinMax(result); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts the value to formatted text.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
private string ConvertValueToText() |
|||
{ |
|||
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
|
|||
if (FormatString.Contains("{0")) |
|||
{ |
|||
return string.Format(CultureInfo, FormatString, Value); |
|||
} |
|||
|
|||
return Value.ToString(FormatString, CultureInfo); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by OnSpin when the spin direction is SpinDirection.Increase.
|
|||
/// </summary>
|
|||
private void OnIncrement() |
|||
{ |
|||
var result = Value + Increment; |
|||
Value = MathUtilities.Clamp(result, Minimum, Maximum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called by OnSpin when the spin direction is SpinDirection.Descrease.
|
|||
/// </summary>
|
|||
private void OnDecrement() |
|||
{ |
|||
var result = Value - Increment; |
|||
Value = MathUtilities.Clamp(result, Minimum, Maximum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the valid spin directions.
|
|||
/// </summary>
|
|||
private void SetValidSpinDirection() |
|||
{ |
|||
var validDirections = ValidSpinDirections.None; |
|||
|
|||
// Zero increment always prevents spin.
|
|||
if (Increment != 0 && !IsReadOnly) |
|||
{ |
|||
if (Value < Maximum) |
|||
{ |
|||
validDirections = validDirections | ValidSpinDirections.Increase; |
|||
} |
|||
|
|||
if (Value > Minimum) |
|||
{ |
|||
validDirections = validDirections | ValidSpinDirections.Decrease; |
|||
} |
|||
} |
|||
|
|||
if (Spinner != null) |
|||
{ |
|||
Spinner.ValidSpinDirection = validDirections; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="CultureInfo"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (CultureInfo)e.OldValue; |
|||
var newValue = (CultureInfo)e.NewValue; |
|||
upDown.OnCultureInfoChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Increment"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (double)e.OldValue; |
|||
var newValue = (double)e.NewValue; |
|||
upDown.OnIncrementChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="FormatString"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (string)e.OldValue; |
|||
var newValue = (string)e.NewValue; |
|||
upDown.OnFormatStringChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="IsReadOnly"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (bool)e.OldValue; |
|||
var newValue = (bool)e.NewValue; |
|||
upDown.OnIsReadOnlyChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Maximum"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (double)e.OldValue; |
|||
var newValue = (double)e.NewValue; |
|||
upDown.OnMaximumChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Minimum"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (double)e.OldValue; |
|||
var newValue = (double)e.NewValue; |
|||
upDown.OnMinimumChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Text"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (string)e.OldValue; |
|||
var newValue = (string)e.NewValue; |
|||
upDown.OnTextChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the <see cref="Value"/> property value changed.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Sender is NumericUpDown upDown) |
|||
{ |
|||
var oldValue = (double)e.OldValue; |
|||
var newValue = (double)e.NewValue; |
|||
upDown.OnValueChanged(oldValue, newValue); |
|||
} |
|||
} |
|||
|
|||
private void SetValueInternal(double value) |
|||
{ |
|||
_internalValueSet = true; |
|||
try |
|||
{ |
|||
Value = value; |
|||
} |
|||
finally |
|||
{ |
|||
_internalValueSet = false; |
|||
} |
|||
} |
|||
|
|||
private static double OnCoerceMaximum(NumericUpDown upDown, double value) |
|||
{ |
|||
return upDown.OnCoerceMaximum(value); |
|||
} |
|||
|
|||
private static double OnCoerceMinimum(NumericUpDown upDown, double value) |
|||
{ |
|||
return upDown.OnCoerceMinimum(value); |
|||
} |
|||
|
|||
private static double OnCoerceIncrement(NumericUpDown upDown, double value) |
|||
{ |
|||
return upDown.OnCoerceIncrement(value); |
|||
} |
|||
|
|||
private void TextBoxOnTextChanged() |
|||
{ |
|||
try |
|||
{ |
|||
_isTextChangedFromUI = true; |
|||
if (TextBox != null) |
|||
{ |
|||
Text = TextBox.Text; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_isTextChangedFromUI = false; |
|||
} |
|||
} |
|||
|
|||
private void OnSpinnerSpin(object sender, SpinEventArgs e) |
|||
{ |
|||
if (AllowSpin && !IsReadOnly) |
|||
{ |
|||
var spin = !e.UsingMouseWheel; |
|||
spin |= ((TextBox != null) && TextBox.IsFocused); |
|||
|
|||
if (spin) |
|||
{ |
|||
e.Handled = true; |
|||
OnSpin(e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void DoDecrement() |
|||
{ |
|||
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) |
|||
{ |
|||
OnDecrement(); |
|||
} |
|||
} |
|||
|
|||
private void DoIncrement() |
|||
{ |
|||
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) |
|||
{ |
|||
OnIncrement(); |
|||
} |
|||
} |
|||
|
|||
public event EventHandler<SpinEventArgs> Spinned; |
|||
|
|||
private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) |
|||
{ |
|||
if (e.Device.Captured != Spinner) |
|||
{ |
|||
Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ValueChanged"/> event.
|
|||
/// </summary>
|
|||
public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent = |
|||
RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble); |
|||
|
|||
/// <summary>
|
|||
/// Raised when the <see cref="Value"/> changes.
|
|||
/// </summary>
|
|||
public event EventHandler<SpinEventArgs> ValueChanged |
|||
{ |
|||
add { AddHandler(ValueChangedEvent, value); } |
|||
remove { RemoveHandler(ValueChangedEvent, value); } |
|||
} |
|||
|
|||
private bool CommitInput() |
|||
{ |
|||
return SyncTextAndValueProperties(true, Text); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
|
|||
/// </summary>
|
|||
/// <param name="updateValueFromText">If value should be updated from text.</param>
|
|||
/// <param name="text">The text.</param>
|
|||
private bool SyncTextAndValueProperties(bool updateValueFromText, string text) |
|||
{ |
|||
return SyncTextAndValueProperties(updateValueFromText, text, false); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
|
|||
/// </summary>
|
|||
/// <param name="updateValueFromText">If value should be updated from text.</param>
|
|||
/// <param name="text">The text.</param>
|
|||
/// <param name="forceTextUpdate">Force text update.</param>
|
|||
private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) |
|||
{ |
|||
if (_isSyncingTextAndValueProperties) |
|||
return true; |
|||
|
|||
_isSyncingTextAndValueProperties = true; |
|||
var parsedTextIsValid = true; |
|||
try |
|||
{ |
|||
if (updateValueFromText) |
|||
{ |
|||
if (!string.IsNullOrEmpty(text)) |
|||
{ |
|||
try |
|||
{ |
|||
var newValue = ConvertTextToValue(text); |
|||
if (!Equals(newValue, Value)) |
|||
{ |
|||
SetValueInternal(newValue); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
parsedTextIsValid = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Do not touch the ongoing text input from user.
|
|||
if (!_isTextChangedFromUI) |
|||
{ |
|||
var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text); |
|||
if (!keepEmpty) |
|||
{ |
|||
var newText = ConvertValueToText(); |
|||
if (!Equals(Text, newText)) |
|||
{ |
|||
Text = newText; |
|||
} |
|||
} |
|||
|
|||
// Sync Text and textBox
|
|||
if (TextBox != null) |
|||
{ |
|||
TextBox.Text = Text; |
|||
} |
|||
} |
|||
|
|||
if (_isTextChangedFromUI && !parsedTextIsValid) |
|||
{ |
|||
// Text input was made from the user and the text
|
|||
// repesents an invalid value. Disable the spinner in this case.
|
|||
if (Spinner != null) |
|||
{ |
|||
Spinner.ValidSpinDirection = ValidSpinDirections.None; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
SetValidSpinDirection(); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_isSyncingTextAndValueProperties = false; |
|||
} |
|||
return parsedTextIsValid; |
|||
} |
|||
|
|||
private double ConvertTextToValueCore(string currentValueText, string text) |
|||
{ |
|||
double result; |
|||
|
|||
if (IsPercent(FormatString)) |
|||
{ |
|||
result = decimal.ToDouble(ParsePercent(text, CultureInfo)); |
|||
} |
|||
else |
|||
{ |
|||
// Problem while converting new text
|
|||
if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) |
|||
{ |
|||
var shouldThrow = true; |
|||
|
|||
// Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
|
|||
if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) |
|||
{ |
|||
// extract non-digit characters
|
|||
var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); |
|||
var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); |
|||
// same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
|
|||
if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) |
|||
{ |
|||
foreach (var character in textSpecialCharacters) |
|||
{ |
|||
text = text.Replace(character.ToString(), string.Empty); |
|||
} |
|||
// if without the special characters, parsing is good, do not throw
|
|||
if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) |
|||
{ |
|||
shouldThrow = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (shouldThrow) |
|||
{ |
|||
throw new InvalidDataException("Input string was not in a correct format."); |
|||
} |
|||
} |
|||
result = outputValue; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private void ValidateMinMax(double value) |
|||
{ |
|||
if (value < Minimum) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); |
|||
} |
|||
else if (value > Maximum) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parse percent format text
|
|||
/// </summary>
|
|||
/// <param name="text">Text to parse.</param>
|
|||
/// <param name="cultureInfo">The culture info.</param>
|
|||
private static decimal ParsePercent(string text, IFormatProvider cultureInfo) |
|||
{ |
|||
var info = NumberFormatInfo.GetInstance(cultureInfo); |
|||
text = text.Replace(info.PercentSymbol, null); |
|||
var result = decimal.Parse(text, NumberStyles.Any, info); |
|||
result = result / 100; |
|||
return result; |
|||
} |
|||
|
|||
|
|||
private bool IsPercent(string stringToTest) |
|||
{ |
|||
var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); |
|||
if (PIndex >= 0) |
|||
{ |
|||
//stringToTest contains a "P" between 2 "'", it's considered as text, not percent
|
|||
var isText = stringToTest.Substring(0, PIndex).Contains("'") |
|||
&& stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); |
|||
|
|||
return !isText; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class NumericUpDownValueChangedEventArgs : RoutedEventArgs |
|||
{ |
|||
public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) |
|||
{ |
|||
OldValue = oldValue; |
|||
NewValue = newValue; |
|||
} |
|||
|
|||
public double OldValue { get; } |
|||
public double NewValue { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,210 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using System.Reactive.Subjects; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
class InProcessDragSource : IPlatformDragSource |
|||
{ |
|||
private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton; |
|||
private readonly IDragDropDevice _dragDrop; |
|||
private readonly IInputManager _inputManager; |
|||
private readonly Subject<DragDropEffects> _result = new Subject<DragDropEffects>(); |
|||
|
|||
private DragDropEffects _allowedEffects; |
|||
private IDataObject _draggedData; |
|||
private IInputElement _lastRoot; |
|||
private Point _lastPosition; |
|||
private StandardCursorType _lastCursorType; |
|||
private object _originalCursor; |
|||
private InputModifiers? _initialInputModifiers; |
|||
|
|||
public InProcessDragSource() |
|||
{ |
|||
_inputManager = AvaloniaLocator.Current.GetService<IInputManager>(); |
|||
_dragDrop = AvaloniaLocator.Current.GetService<IDragDropDevice>(); |
|||
} |
|||
|
|||
public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects) |
|||
{ |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
if (_draggedData == null) |
|||
{ |
|||
_draggedData = data; |
|||
_lastRoot = null; |
|||
_lastPosition = default(Point); |
|||
_allowedEffects = allowedEffects; |
|||
|
|||
using (_inputManager.PreProcess.OfType<RawMouseEventArgs>().Subscribe(ProcessMouseEvents)) |
|||
{ |
|||
using (_inputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(ProcessKeyEvents)) |
|||
{ |
|||
var effect = await _result.FirstAsync(); |
|||
return effect; |
|||
} |
|||
} |
|||
} |
|||
return DragDropEffects.None; |
|||
} |
|||
|
|||
|
|||
private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, InputModifiers modifiers) |
|||
{ |
|||
_lastPosition = pt; |
|||
|
|||
RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); |
|||
var tl = root.GetSelfAndVisualAncestors().OfType<TopLevel>().FirstOrDefault(); |
|||
tl.PlatformImpl.Input(rawEvent); |
|||
|
|||
var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers); |
|||
UpdateCursor(root, effect); |
|||
return effect; |
|||
} |
|||
|
|||
private DragDropEffects GetPreferredEffect(DragDropEffects effect, InputModifiers modifiers) |
|||
{ |
|||
if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None) |
|||
return effect; // No need to check for the modifiers.
|
|||
if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(InputModifiers.Alt)) |
|||
return DragDropEffects.Link; |
|||
if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(InputModifiers.Control)) |
|||
return DragDropEffects.Copy; |
|||
return DragDropEffects.Move; |
|||
} |
|||
|
|||
private StandardCursorType GetCursorForDropEffect(DragDropEffects effects) |
|||
{ |
|||
if (effects.HasFlag(DragDropEffects.Copy)) |
|||
return StandardCursorType.DragCopy; |
|||
if (effects.HasFlag(DragDropEffects.Move)) |
|||
return StandardCursorType.DragMove; |
|||
if (effects.HasFlag(DragDropEffects.Link)) |
|||
return StandardCursorType.DragLink; |
|||
return StandardCursorType.No; |
|||
} |
|||
|
|||
private void UpdateCursor(IInputElement root, DragDropEffects effect) |
|||
{ |
|||
if (_lastRoot != root) |
|||
{ |
|||
if (_lastRoot is InputElement ieLast) |
|||
{ |
|||
if (_originalCursor == AvaloniaProperty.UnsetValue) |
|||
ieLast.ClearValue(InputElement.CursorProperty); |
|||
else |
|||
ieLast.Cursor = _originalCursor as Cursor; |
|||
} |
|||
|
|||
if (root is InputElement ieNew) |
|||
{ |
|||
if (!ieNew.IsSet(InputElement.CursorProperty)) |
|||
_originalCursor = AvaloniaProperty.UnsetValue; |
|||
else |
|||
_originalCursor = root.Cursor; |
|||
} |
|||
else |
|||
_originalCursor = null; |
|||
|
|||
_lastCursorType = StandardCursorType.Arrow; |
|||
_lastRoot = root; |
|||
} |
|||
|
|||
if (root is InputElement ie) |
|||
{ |
|||
var ct = GetCursorForDropEffect(effect); |
|||
if (ct != _lastCursorType) |
|||
{ |
|||
_lastCursorType = ct; |
|||
ie.Cursor = new Cursor(ct); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void CancelDragging() |
|||
{ |
|||
if (_lastRoot != null) |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, InputModifiers.None); |
|||
UpdateCursor(null, DragDropEffects.None); |
|||
_result.OnNext(DragDropEffects.None); |
|||
} |
|||
|
|||
private void ProcessKeyEvents(RawKeyEventArgs e) |
|||
{ |
|||
if (e.Type == RawKeyEventType.KeyDown && e.Key == Key.Escape) |
|||
{ |
|||
if (_lastRoot != null) |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, e.Modifiers); |
|||
UpdateCursor(null, DragDropEffects.None); |
|||
_result.OnNext(DragDropEffects.None); |
|||
e.Handled = true; |
|||
} |
|||
else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt) |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers); |
|||
} |
|||
|
|||
private void ProcessMouseEvents(RawMouseEventArgs e) |
|||
{ |
|||
if (!_initialInputModifiers.HasValue) |
|||
_initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; |
|||
|
|||
|
|||
void CheckDraggingAccepted(InputModifiers changedMouseButton) |
|||
{ |
|||
if (_initialInputModifiers.Value.HasFlag(changedMouseButton)) |
|||
{ |
|||
var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers); |
|||
UpdateCursor(null, DragDropEffects.None); |
|||
_result.OnNext(result); |
|||
} |
|||
else |
|||
CancelDragging(); |
|||
e.Handled = true; |
|||
} |
|||
|
|||
switch (e.Type) |
|||
{ |
|||
case RawMouseEventType.LeftButtonDown: |
|||
case RawMouseEventType.RightButtonDown: |
|||
case RawMouseEventType.MiddleButtonDown: |
|||
case RawMouseEventType.NonClientLeftButtonDown: |
|||
CancelDragging(); |
|||
e.Handled = true; |
|||
return; |
|||
case RawMouseEventType.LeaveWindow: |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break; |
|||
case RawMouseEventType.LeftButtonUp: |
|||
CheckDraggingAccepted(InputModifiers.LeftMouseButton); break; |
|||
case RawMouseEventType.MiddleButtonUp: |
|||
CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break; |
|||
case RawMouseEventType.RightButtonUp: |
|||
CheckDraggingAccepted(InputModifiers.RightMouseButton); break; |
|||
case RawMouseEventType.Move: |
|||
var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS; |
|||
if (_initialInputModifiers.Value != mods) |
|||
{ |
|||
CancelDragging(); |
|||
e.Handled = true; |
|||
return; |
|||
} |
|||
|
|||
if (e.Root != _lastRoot) |
|||
{ |
|||
if (_lastRoot != null) |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), e.InputModifiers); |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragEnter, e.Root, e.Position, e.InputModifiers); |
|||
} |
|||
else |
|||
RaiseEventAndUpdateCursor(RawDragEventType.DragOver, e.Root, e.Position, e.InputModifiers); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,279 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
internal class BorderRenderHelper |
|||
{ |
|||
private bool _useComplexRendering; |
|||
private StreamGeometry _backgroundGeometryCache; |
|||
private StreamGeometry _borderGeometryCache; |
|||
|
|||
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) |
|||
{ |
|||
if (borderThickness.IsUniform && cornerRadius.IsUniform) |
|||
{ |
|||
_backgroundGeometryCache = null; |
|||
_borderGeometryCache = null; |
|||
_useComplexRendering = false; |
|||
} |
|||
else |
|||
{ |
|||
_useComplexRendering = true; |
|||
|
|||
var boundRect = new Rect(finalSize); |
|||
var innerRect = boundRect.Deflate(borderThickness); |
|||
var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false); |
|||
|
|||
StreamGeometry backgroundGeometry = null; |
|||
|
|||
if (innerRect.Width != 0 && innerRect.Height != 0) |
|||
{ |
|||
backgroundGeometry = new StreamGeometry(); |
|||
|
|||
using (var ctx = backgroundGeometry.Open()) |
|||
{ |
|||
CreateGeometry(ctx, innerRect, innerCoordinates); |
|||
} |
|||
|
|||
_backgroundGeometryCache = backgroundGeometry; |
|||
} |
|||
else |
|||
{ |
|||
_backgroundGeometryCache = null; |
|||
} |
|||
|
|||
if (boundRect.Width != 0 && innerRect.Height != 0) |
|||
{ |
|||
var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true); |
|||
var borderGeometry = new StreamGeometry(); |
|||
|
|||
using (var ctx = borderGeometry.Open()) |
|||
{ |
|||
CreateGeometry(ctx, boundRect, outerCoordinates); |
|||
|
|||
if (backgroundGeometry != null) |
|||
{ |
|||
CreateGeometry(ctx, innerRect, innerCoordinates); |
|||
} |
|||
} |
|||
|
|||
_borderGeometryCache = borderGeometry; |
|||
} |
|||
else |
|||
{ |
|||
_borderGeometryCache = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush) |
|||
{ |
|||
if (_useComplexRendering) |
|||
{ |
|||
var backgroundGeometry = _backgroundGeometryCache; |
|||
if (backgroundGeometry != null) |
|||
{ |
|||
context.DrawGeometry(background, null, backgroundGeometry); |
|||
} |
|||
|
|||
var borderGeometry = _borderGeometryCache; |
|||
if (borderGeometry != null) |
|||
{ |
|||
context.DrawGeometry(borderBrush, null, borderGeometry); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var borderThickness = borders.Left; |
|||
var cornerRadius = (float)radii.TopLeft; |
|||
var rect = new Rect(size); |
|||
|
|||
if (background != null) |
|||
{ |
|||
context.FillRectangle(background, rect.Deflate(borders), cornerRadius); |
|||
} |
|||
|
|||
if (borderBrush != null && borderThickness > 0) |
|||
{ |
|||
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates) |
|||
{ |
|||
var topLeft = new Point(borderCoordinates.LeftTop, 0); |
|||
var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0); |
|||
var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight); |
|||
var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight); |
|||
var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height); |
|||
var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height); |
|||
var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft); |
|||
var leftTop = new Point(0, borderCoordinates.TopLeft); |
|||
|
|||
|
|||
if (topLeft.X > topRight.X) |
|||
{ |
|||
var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width; |
|||
topLeft = new Point(scaledX, topLeft.Y); |
|||
topRight = new Point(scaledX, topRight.Y); |
|||
} |
|||
|
|||
if (rightTop.Y > rightBottom.Y) |
|||
{ |
|||
var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height; |
|||
rightTop = new Point(rightTop.X, scaledY); |
|||
rightBottom = new Point(rightBottom.X, scaledY); |
|||
} |
|||
|
|||
if (bottomRight.X < bottomLeft.X) |
|||
{ |
|||
var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width; |
|||
bottomRight = new Point(scaledX, bottomRight.Y); |
|||
bottomLeft = new Point(scaledX, bottomLeft.Y); |
|||
} |
|||
|
|||
if (leftBottom.Y < leftTop.Y) |
|||
{ |
|||
var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height; |
|||
leftBottom = new Point(leftBottom.X, scaledY); |
|||
leftTop = new Point(leftTop.X, scaledY); |
|||
} |
|||
|
|||
var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y); |
|||
topLeft += offset; |
|||
topRight += offset; |
|||
rightTop += offset; |
|||
rightBottom += offset; |
|||
bottomRight += offset; |
|||
bottomLeft += offset; |
|||
leftBottom += offset; |
|||
leftTop += offset; |
|||
|
|||
context.BeginFigure(topLeft, true); |
|||
|
|||
//Top
|
|||
context.LineTo(topRight); |
|||
|
|||
//TopRight corner
|
|||
var radiusX = boundRect.TopRight.X - topRight.X; |
|||
var radiusY = rightTop.Y - boundRect.TopRight.Y; |
|||
if (radiusX != 0 || radiusY != 0) |
|||
{ |
|||
context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise); |
|||
} |
|||
|
|||
//Right
|
|||
context.LineTo(rightBottom); |
|||
|
|||
//BottomRight corner
|
|||
radiusX = boundRect.BottomRight.X - bottomRight.X; |
|||
radiusY = boundRect.BottomRight.Y - rightBottom.Y; |
|||
if (radiusX != 0 || radiusY != 0) |
|||
{ |
|||
context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
|||
} |
|||
|
|||
//Bottom
|
|||
context.LineTo(bottomLeft); |
|||
|
|||
//BottomLeft corner
|
|||
radiusX = bottomLeft.X - boundRect.BottomLeft.X; |
|||
radiusY = boundRect.BottomLeft.Y - leftBottom.Y; |
|||
if (radiusX != 0 || radiusY != 0) |
|||
{ |
|||
context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
|||
} |
|||
|
|||
//Left
|
|||
context.LineTo(leftTop); |
|||
|
|||
//TopLeft corner
|
|||
radiusX = topLeft.X - boundRect.TopLeft.X; |
|||
radiusY = leftTop.Y - boundRect.TopLeft.Y; |
|||
|
|||
if (radiusX != 0 || radiusY != 0) |
|||
{ |
|||
context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
|||
} |
|||
|
|||
context.EndFigure(true); |
|||
} |
|||
|
|||
private struct BorderCoordinates |
|||
{ |
|||
internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter) |
|||
{ |
|||
var left = 0.5 * borderThickness.Left; |
|||
var top = 0.5 * borderThickness.Top; |
|||
var right = 0.5 * borderThickness.Right; |
|||
var bottom = 0.5 * borderThickness.Bottom; |
|||
|
|||
if (isOuter) |
|||
{ |
|||
if (cornerRadius.TopLeft == 0) |
|||
{ |
|||
LeftTop = TopLeft = 0.0; |
|||
} |
|||
else |
|||
{ |
|||
LeftTop = cornerRadius.TopLeft + left; |
|||
TopLeft = cornerRadius.TopLeft + top; |
|||
} |
|||
if (cornerRadius.TopRight == 0) |
|||
{ |
|||
TopRight = RightTop = 0; |
|||
} |
|||
else |
|||
{ |
|||
TopRight = cornerRadius.TopRight + top; |
|||
RightTop = cornerRadius.TopRight + right; |
|||
} |
|||
if (cornerRadius.BottomRight == 0) |
|||
{ |
|||
RightBottom = BottomRight = 0; |
|||
} |
|||
else |
|||
{ |
|||
RightBottom = cornerRadius.BottomRight + right; |
|||
BottomRight = cornerRadius.BottomRight + bottom; |
|||
} |
|||
if (cornerRadius.BottomLeft == 0) |
|||
{ |
|||
BottomLeft = LeftBottom = 0; |
|||
} |
|||
else |
|||
{ |
|||
BottomLeft = cornerRadius.BottomLeft + bottom; |
|||
LeftBottom = cornerRadius.BottomLeft + left; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
LeftTop = Math.Max(0, cornerRadius.TopLeft - left); |
|||
TopLeft = Math.Max(0, cornerRadius.TopLeft - top); |
|||
TopRight = Math.Max(0, cornerRadius.TopRight - top); |
|||
RightTop = Math.Max(0, cornerRadius.TopRight - right); |
|||
RightBottom = Math.Max(0, cornerRadius.BottomRight - right); |
|||
BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom); |
|||
BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom); |
|||
LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left); |
|||
} |
|||
} |
|||
|
|||
internal readonly double LeftTop; |
|||
internal readonly double TopLeft; |
|||
internal readonly double TopRight; |
|||
internal readonly double RightTop; |
|||
internal readonly double RightBottom; |
|||
internal readonly double BottomRight; |
|||
internal readonly double BottomLeft; |
|||
internal readonly double LeftBottom; |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
// (c) Copyright Microsoft Corporation.
|
|||
// This source is subject to the Microsoft Public License (Ms-PL).
|
|||
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
|
|||
// All other rights reserved.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Defines an item collection, selection members, and key handling for the
|
|||
/// selection adapter contained in the drop-down portion of an
|
|||
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
|
|||
/// </summary>
|
|||
public interface ISelectionAdapter |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the selected item.
|
|||
/// </summary>
|
|||
/// <value>The currently selected item.</value>
|
|||
object SelectedItem { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Occurs when the
|
|||
/// <see cref="P:Avalonia.Controls.Utils.ISelectionAdapter.SelectedItem" />
|
|||
/// property value changes.
|
|||
/// </summary>
|
|||
event EventHandler<SelectionChangedEventArgs> SelectionChanged; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a collection that is used to generate content for the
|
|||
/// selection adapter.
|
|||
/// </summary>
|
|||
/// <value>The collection that is used to generate content for the
|
|||
/// selection adapter.</value>
|
|||
IEnumerable Items { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Occurs when a selected item is not cancelled and is committed as the
|
|||
/// selected item.
|
|||
/// </summary>
|
|||
event EventHandler<RoutedEventArgs> Commit; |
|||
|
|||
/// <summary>
|
|||
/// Occurs when a selection has been canceled.
|
|||
/// </summary>
|
|||
event EventHandler<RoutedEventArgs> Cancel; |
|||
|
|||
/// <summary>
|
|||
/// Provides handling for the
|
|||
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
|
|||
/// when a key is pressed while the drop-down portion of the
|
|||
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
|
|||
/// </summary>
|
|||
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
|
|||
/// that contains data about the
|
|||
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
|
|||
void HandleKeyDown(KeyEventArgs e); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,342 @@ |
|||
// (c) Copyright Microsoft Corporation.
|
|||
// This source is subject to the Microsoft Public License (Ms-PL).
|
|||
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
|
|||
// All other rights reserved.
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Input; |
|||
using Avalonia.LogicalTree; |
|||
using System.Collections; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the selection adapter contained in the drop-down portion of
|
|||
/// an <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
|
|||
/// </summary>
|
|||
public class SelectingItemsControlSelectionAdapter : ISelectionAdapter |
|||
{ |
|||
/// <summary>
|
|||
/// The SelectingItemsControl instance.
|
|||
/// </summary>
|
|||
private SelectingItemsControl _selector; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the selection change event
|
|||
/// should not be fired.
|
|||
/// </summary>
|
|||
private bool IgnoringSelectionChanged { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the underlying
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.
|
|||
/// </summary>
|
|||
/// <value>The underlying
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.</value>
|
|||
public SelectingItemsControl SelectorControl |
|||
{ |
|||
get { return _selector; } |
|||
|
|||
set |
|||
{ |
|||
if (_selector != null) |
|||
{ |
|||
_selector.SelectionChanged -= OnSelectionChanged; |
|||
_selector.PointerReleased -= OnSelectorPointerReleased; |
|||
} |
|||
|
|||
_selector = value; |
|||
|
|||
if (_selector != null) |
|||
{ |
|||
_selector.SelectionChanged += OnSelectionChanged; |
|||
_selector.PointerReleased += OnSelectorPointerReleased; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Occurs when the
|
|||
/// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
|
|||
/// property value changes.
|
|||
/// </summary>
|
|||
public event EventHandler<SelectionChangedEventArgs> SelectionChanged; |
|||
|
|||
/// <summary>
|
|||
/// Occurs when an item is selected and is committed to the underlying
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.
|
|||
/// </summary>
|
|||
public event EventHandler<RoutedEventArgs> Commit; |
|||
|
|||
/// <summary>
|
|||
/// Occurs when a selection is canceled before it is committed.
|
|||
/// </summary>
|
|||
public event EventHandler<RoutedEventArgs> Cancel; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the
|
|||
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
|
|||
/// class.
|
|||
/// </summary>
|
|||
public SelectingItemsControlSelectionAdapter() |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the
|
|||
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
|
|||
/// class with the specified
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.
|
|||
/// </summary>
|
|||
/// <param name="selector">The
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
|
|||
/// to wrap as a
|
|||
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.</param>
|
|||
public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector) |
|||
{ |
|||
SelectorControl = selector; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the selected item of the selection adapter.
|
|||
/// </summary>
|
|||
/// <value>The selected item of the underlying selection adapter.</value>
|
|||
public object SelectedItem |
|||
{ |
|||
get |
|||
{ |
|||
return SelectorControl?.SelectedItem; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
IgnoringSelectionChanged = true; |
|||
if (SelectorControl != null) |
|||
{ |
|||
SelectorControl.SelectedItem = value; |
|||
} |
|||
|
|||
// Attempt to reset the scroll viewer's position
|
|||
if (value == null) |
|||
{ |
|||
ResetScrollViewer(); |
|||
} |
|||
|
|||
IgnoringSelectionChanged = false; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a collection that is used to generate the content of
|
|||
/// the selection adapter.
|
|||
/// </summary>
|
|||
/// <value>The collection used to generate content for the selection
|
|||
/// adapter.</value>
|
|||
public IEnumerable Items |
|||
{ |
|||
get |
|||
{ |
|||
return SelectorControl?.Items; |
|||
} |
|||
set |
|||
{ |
|||
if (SelectorControl != null) |
|||
{ |
|||
SelectorControl.Items = value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If the control contains a ScrollViewer, this will reset the viewer
|
|||
/// to be scrolled to the top.
|
|||
/// </summary>
|
|||
private void ResetScrollViewer() |
|||
{ |
|||
if (SelectorControl != null) |
|||
{ |
|||
ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault(); |
|||
if (sv != null) |
|||
{ |
|||
sv.Offset = new Vector(0, 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles the mouse left button up event on the selector control.
|
|||
/// </summary>
|
|||
/// <param name="sender">The source object.</param>
|
|||
/// <param name="e">The event data.</param>
|
|||
private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) |
|||
{ |
|||
if (e.MouseButton == MouseButton.Left) |
|||
{ |
|||
OnCommit(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles the SelectionChanged event on the SelectingItemsControl control.
|
|||
/// </summary>
|
|||
/// <param name="sender">The source object.</param>
|
|||
/// <param name="e">The selection changed event data.</param>
|
|||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) |
|||
{ |
|||
if (IgnoringSelectionChanged) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
SelectionChanged?.Invoke(sender, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Increments the
|
|||
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
|
|||
/// property of the underlying
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.
|
|||
/// </summary>
|
|||
protected void SelectedIndexIncrement() |
|||
{ |
|||
if (SelectorControl != null) |
|||
{ |
|||
SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Decrements the
|
|||
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
|
|||
/// property of the underlying
|
|||
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
|||
/// control.
|
|||
/// </summary>
|
|||
protected void SelectedIndexDecrement() |
|||
{ |
|||
if (SelectorControl != null) |
|||
{ |
|||
int index = SelectorControl.SelectedIndex; |
|||
if (index >= 0) |
|||
{ |
|||
SelectorControl.SelectedIndex--; |
|||
} |
|||
else if (index == -1) |
|||
{ |
|||
SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides handling for the
|
|||
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
|
|||
/// when a key is pressed while the drop-down portion of the
|
|||
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
|
|||
/// </summary>
|
|||
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
|
|||
/// that contains data about the
|
|||
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
|
|||
public void HandleKeyDown(KeyEventArgs e) |
|||
{ |
|||
switch (e.Key) |
|||
{ |
|||
case Key.Enter: |
|||
OnCommit(); |
|||
e.Handled = true; |
|||
break; |
|||
|
|||
case Key.Up: |
|||
SelectedIndexDecrement(); |
|||
e.Handled = true; |
|||
break; |
|||
|
|||
case Key.Down: |
|||
if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None) |
|||
{ |
|||
SelectedIndexIncrement(); |
|||
e.Handled = true; |
|||
} |
|||
break; |
|||
|
|||
case Key.Escape: |
|||
OnCancel(); |
|||
e.Handled = true; |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the
|
|||
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
|
|||
/// event.
|
|||
/// </summary>
|
|||
protected virtual void OnCommit() |
|||
{ |
|||
OnCommit(this, new RoutedEventArgs()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Fires the Commit event.
|
|||
/// </summary>
|
|||
/// <param name="sender">The source object.</param>
|
|||
/// <param name="e">The event data.</param>
|
|||
private void OnCommit(object sender, RoutedEventArgs e) |
|||
{ |
|||
Commit?.Invoke(sender, e); |
|||
|
|||
AfterAdapterAction(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the
|
|||
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
|
|||
/// event.
|
|||
/// </summary>
|
|||
protected virtual void OnCancel() |
|||
{ |
|||
OnCancel(this, new RoutedEventArgs()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Fires the Cancel event.
|
|||
/// </summary>
|
|||
/// <param name="sender">The source object.</param>
|
|||
/// <param name="e">The event data.</param>
|
|||
private void OnCancel(object sender, RoutedEventArgs e) |
|||
{ |
|||
Cancel?.Invoke(sender, e); |
|||
|
|||
AfterAdapterAction(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Change the selection after the actions are complete.
|
|||
/// </summary>
|
|||
private void AfterAdapterAction() |
|||
{ |
|||
IgnoringSelectionChanged = true; |
|||
if (SelectorControl != null) |
|||
{ |
|||
SelectorControl.SelectedItem = null; |
|||
SelectorControl.SelectedIndex = -1; |
|||
} |
|||
IgnoringSelectionChanged = false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace Avalonia.Input |
|||
{ |
|||
public static class DataFormats |
|||
{ |
|||
/// <summary>
|
|||
/// Dataformat for plaintext
|
|||
/// </summary>
|
|||
public static string Text = nameof(Text); |
|||
|
|||
/// <summary>
|
|||
/// Dataformat for one or more filenames
|
|||
/// </summary>
|
|||
public static string FileNames = nameof(FileNames); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class DataObject : IDataObject |
|||
{ |
|||
private readonly Dictionary<string, object> _items = new Dictionary<string, object>(); |
|||
|
|||
public bool Contains(string dataFormat) |
|||
{ |
|||
return _items.ContainsKey(dataFormat); |
|||
} |
|||
|
|||
public object Get(string dataFormat) |
|||
{ |
|||
if (_items.ContainsKey(dataFormat)) |
|||
return _items[dataFormat]; |
|||
return null; |
|||
} |
|||
|
|||
public IEnumerable<string> GetDataFormats() |
|||
{ |
|||
return _items.Keys; |
|||
} |
|||
|
|||
public IEnumerable<string> GetFileNames() |
|||
{ |
|||
return Get(DataFormats.FileNames) as IEnumerable<string>; |
|||
} |
|||
|
|||
public string GetText() |
|||
{ |
|||
return Get(DataFormats.Text) as string; |
|||
} |
|||
|
|||
public void Set(string dataFormat, object value) |
|||
{ |
|||
_items[dataFormat] = value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public static class DragDrop |
|||
{ |
|||
/// <summary>
|
|||
/// Event which is raised, when a drag-and-drop operation enters the element.
|
|||
/// </summary>
|
|||
public static RoutedEvent<DragEventArgs> DragEnterEvent = RoutedEvent.Register<DragEventArgs>("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop)); |
|||
/// <summary>
|
|||
/// Event which is raised, when a drag-and-drop operation leaves the element.
|
|||
/// </summary>
|
|||
public static RoutedEvent<RoutedEventArgs> DragLeaveEvent = RoutedEvent.Register<RoutedEventArgs>("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop)); |
|||
/// <summary>
|
|||
/// Event which is raised, when a drag-and-drop operation is updated while over the element.
|
|||
/// </summary>
|
|||
public static RoutedEvent<DragEventArgs> DragOverEvent = RoutedEvent.Register<DragEventArgs>("DragOver", RoutingStrategies.Bubble, typeof(DragDrop)); |
|||
/// <summary>
|
|||
/// Event which is raised, when a drag-and-drop operation should complete over the element.
|
|||
/// </summary>
|
|||
public static RoutedEvent<DragEventArgs> DropEvent = RoutedEvent.Register<DragEventArgs>("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); |
|||
|
|||
public static AvaloniaProperty<bool> AllowDropProperty = AvaloniaProperty.RegisterAttached<Interactive, bool>("AllowDrop", typeof(DragDrop), inherits: true); |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation.
|
|||
/// </summary>
|
|||
public static bool GetAllowDrop(Interactive interactive) |
|||
{ |
|||
return interactive.GetValue(AllowDropProperty); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets a value indicating whether the given interactive can be used as the target of a drag-and-drop operation.
|
|||
/// </summary>
|
|||
public static void SetAllowDrop(Interactive interactive, bool value) |
|||
{ |
|||
interactive.SetValue(AllowDropProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts a dragging operation with the given <see cref="IDataObject"/> and returns the applied drop effect from the target.
|
|||
/// <seealso cref="DataObject"/>
|
|||
/// </summary>
|
|||
public static Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects) |
|||
{ |
|||
var src = AvaloniaLocator.Current.GetService<IPlatformDragSource>(); |
|||
return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.VisualTree; |
|||
using System.Linq; |
|||
using Avalonia.Input.Raw; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class DragDropDevice : IDragDropDevice |
|||
{ |
|||
public static readonly DragDropDevice Instance = new DragDropDevice(); |
|||
|
|||
private Interactive _lastTarget = null; |
|||
|
|||
private Interactive GetTarget(IInputElement root, Point local) |
|||
{ |
|||
var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType<Interactive>()?.FirstOrDefault(); |
|||
if (target != null && DragDrop.GetAllowDrop(target)) |
|||
return target; |
|||
return null; |
|||
} |
|||
|
|||
private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data) |
|||
{ |
|||
if (target == null) |
|||
return DragDropEffects.None; |
|||
var args = new DragEventArgs(routedEvent, data) |
|||
{ |
|||
RoutedEvent = routedEvent, |
|||
DragEffects = operation |
|||
}; |
|||
target.RaiseEvent(args); |
|||
return args.DragEffects; |
|||
} |
|||
|
|||
private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) |
|||
{ |
|||
_lastTarget = GetTarget(inputRoot, point); |
|||
return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data); |
|||
} |
|||
|
|||
private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) |
|||
{ |
|||
var target = GetTarget(inputRoot, point); |
|||
|
|||
if (target == _lastTarget) |
|||
return RaiseDragEvent(target, DragDrop.DragOverEvent, effects, data); |
|||
|
|||
try |
|||
{ |
|||
if (_lastTarget != null) |
|||
_lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); |
|||
return RaiseDragEvent(target, DragDrop.DragEnterEvent, effects, data); |
|||
} |
|||
finally |
|||
{ |
|||
_lastTarget = target; |
|||
} |
|||
} |
|||
|
|||
private void DragLeave(IInputElement inputRoot) |
|||
{ |
|||
if (_lastTarget == null) |
|||
return; |
|||
try |
|||
{ |
|||
_lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); |
|||
} |
|||
finally |
|||
{ |
|||
_lastTarget = null; |
|||
} |
|||
} |
|||
|
|||
private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) |
|||
{ |
|||
try |
|||
{ |
|||
return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, effects, data); |
|||
} |
|||
finally |
|||
{ |
|||
_lastTarget = null; |
|||
} |
|||
} |
|||
|
|||
public void ProcessRawEvent(RawInputEventArgs e) |
|||
{ |
|||
if (!e.Handled && e is RawDragEvent margs) |
|||
ProcessRawEvent(margs); |
|||
} |
|||
|
|||
private void ProcessRawEvent(RawDragEvent e) |
|||
{ |
|||
switch (e.Type) |
|||
{ |
|||
case RawDragEventType.DragEnter: |
|||
e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects); |
|||
break; |
|||
case RawDragEventType.DragOver: |
|||
e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects); |
|||
break; |
|||
case RawDragEventType.DragLeave: |
|||
DragLeave(e.InputRoot); |
|||
break; |
|||
case RawDragEventType.Drop: |
|||
e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
[Flags] |
|||
public enum DragDropEffects |
|||
{ |
|||
None = 0, |
|||
Copy = 1, |
|||
Move = 2, |
|||
Link = 4, |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class DragEventArgs : RoutedEventArgs |
|||
{ |
|||
public DragDropEffects DragEffects { get; set; } |
|||
|
|||
public IDataObject Data { get; private set; } |
|||
|
|||
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data) |
|||
: base(routedEvent) |
|||
{ |
|||
this.Data = data; |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
/// <summary>
|
|||
/// Interface to access information about the data of a drag-and-drop operation.
|
|||
/// </summary>
|
|||
public interface IDataObject |
|||
{ |
|||
/// <summary>
|
|||
/// Lists all formats which are present in the DataObject.
|
|||
/// <seealso cref="DataFormats"/>
|
|||
/// </summary>
|
|||
IEnumerable<string> GetDataFormats(); |
|||
|
|||
/// <summary>
|
|||
/// Checks wether a given DataFormat is present in this object
|
|||
/// <seealso cref="DataFormats"/>
|
|||
/// </summary>
|
|||
bool Contains(string dataFormat); |
|||
|
|||
/// <summary>
|
|||
/// Returns the dragged text if the DataObject contains any text.
|
|||
/// <seealso cref="DataFormats.Text"/>
|
|||
/// </summary>
|
|||
string GetText(); |
|||
|
|||
/// <summary>
|
|||
/// Returns a list of filenames if the DataObject contains filenames.
|
|||
/// <seealso cref="DataFormats.FileNames"/>
|
|||
/// </summary>
|
|||
IEnumerable<string> GetFileNames(); |
|||
|
|||
/// <summary>
|
|||
/// Tries to get the data of the given DataFormat.
|
|||
/// </summary>
|
|||
object Get(string dataFormat); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input.Platform |
|||
{ |
|||
public interface IPlatformDragSource |
|||
{ |
|||
Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Input.Raw |
|||
{ |
|||
public interface IDragDropDevice : IInputDevice |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
|
|||
namespace Avalonia.Input.Raw |
|||
{ |
|||
public class RawDragEvent : RawInputEventArgs |
|||
{ |
|||
public IInputElement InputRoot { get; } |
|||
public Point Location { get; } |
|||
public IDataObject Data { get; } |
|||
public DragDropEffects Effects { get; set; } |
|||
public RawDragEventType Type { get; } |
|||
|
|||
public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, |
|||
IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) |
|||
:base(inputDevice, 0) |
|||
{ |
|||
Type = type; |
|||
InputRoot = inputRoot; |
|||
Location = location; |
|||
Data = data; |
|||
Effects = effects; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
namespace Avalonia.Input.Raw |
|||
{ |
|||
public enum RawDragEventType |
|||
{ |
|||
DragEnter, |
|||
DragOver, |
|||
DragLeave, |
|||
Drop |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui"> |
|||
<Style Selector="AutoCompleteBox"> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> |
|||
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> |
|||
<Setter Property="Padding" Value="4"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Panel> |
|||
<TextBox Name="PART_TextBox" |
|||
Background="{TemplateBinding Background}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
Padding="{TemplateBinding Padding}" |
|||
Watermark="{TemplateBinding Watermark}" |
|||
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" /> |
|||
|
|||
<Popup Name="PART_Popup" |
|||
MinWidth="{TemplateBinding Bounds.Width}" |
|||
MaxHeight="{TemplateBinding MaxDropDownHeight}" |
|||
PlacementTarget="{TemplateBinding}" |
|||
StaysOpen="False"> |
|||
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" |
|||
BorderThickness="1"> |
|||
<ListBox Name="PART_SelectingItemsControl" |
|||
BorderThickness="0" |
|||
Background="{TemplateBinding Background}" |
|||
Foreground="{TemplateBinding Foreground}" |
|||
ItemTemplate="{TemplateBinding ItemTemplate}" |
|||
MemberSelector="{TemplateBinding ValueMemberSelector}" |
|||
ScrollViewer.HorizontalScrollBarVisibility="Auto" |
|||
ScrollViewer.VerticalScrollBarVisibility="Auto" /> |
|||
</Border> |
|||
</Popup> |
|||
</Panel> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="AutoCompleteBox ListBoxItem:pointerover"> |
|||
<Setter Property="Background" Value="#ffd0d0d0"/> |
|||
</Style> |
|||
</Styles> |
|||
@ -0,0 +1,41 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui"> |
|||
<Style Selector="NumericUpDown"> |
|||
<Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/> |
|||
<Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> |
|||
<Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" /> |
|||
<Setter Property="TemplatedControl.Foreground" Value="{DynamicResource ThemeForegroundBrush}" /> |
|||
<Setter Property="TemplatedControl.Template"> |
|||
<ControlTemplate> |
|||
<ButtonSpinner Name="PART_Spinner" |
|||
Background="{TemplateBinding Background}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
HorizontalContentAlignment="Stretch" |
|||
VerticalContentAlignment="Stretch" |
|||
AllowSpin="{TemplateBinding AllowSpin}" |
|||
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}" |
|||
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}"> |
|||
<TextBox Name="PART_TextBox" |
|||
BorderThickness="0" |
|||
Background="Transparent" |
|||
ContextMenu="{TemplateBinding ContextMenu}" |
|||
FontFamily="{TemplateBinding FontFamily}" |
|||
FontSize="{TemplateBinding FontSize}" |
|||
FontStyle="{TemplateBinding FontStyle}" |
|||
FontWeight="{TemplateBinding FontWeight}" |
|||
Foreground="{TemplateBinding Foreground}" |
|||
Watermark="{TemplateBinding Watermark}" |
|||
IsReadOnly="{TemplateBinding IsReadOnly}" |
|||
Text="{TemplateBinding Text}" |
|||
Padding="{TemplateBinding Padding}" |
|||
TextAlignment="Left" |
|||
Margin="1" |
|||
MinWidth="20" |
|||
AcceptsReturn="False" |
|||
TextWrapping="NoWrap"> |
|||
</TextBox> |
|||
</ButtonSpinner> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
</Styles> |
|||
@ -0,0 +1,97 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public struct CornerRadius |
|||
{ |
|||
public CornerRadius(double uniformRadius) |
|||
{ |
|||
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius; |
|||
|
|||
} |
|||
public CornerRadius(double top, double bottom) |
|||
{ |
|||
TopLeft = TopRight = top; |
|||
BottomLeft = BottomRight = bottom; |
|||
} |
|||
public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft) |
|||
{ |
|||
TopLeft = topLeft; |
|||
TopRight = topRight; |
|||
BottomRight = bottomRight; |
|||
BottomLeft = bottomLeft; |
|||
} |
|||
|
|||
public double TopLeft { get; } |
|||
public double TopRight { get; } |
|||
public double BottomRight { get; } |
|||
public double BottomLeft { get; } |
|||
public bool IsEmpty => TopLeft.Equals(0) && IsUniform; |
|||
public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight); |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is CornerRadius) |
|||
{ |
|||
return this == (CornerRadius)obj; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode(); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}"; |
|||
} |
|||
|
|||
public static CornerRadius Parse(string s, CultureInfo culture) |
|||
{ |
|||
var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) |
|||
.Select(x => x.Trim()) |
|||
.ToList(); |
|||
|
|||
switch (parts.Count) |
|||
{ |
|||
case 1: |
|||
var uniform = double.Parse(parts[0], culture); |
|||
return new CornerRadius(uniform); |
|||
case 2: |
|||
var top = double.Parse(parts[0], culture); |
|||
var bottom = double.Parse(parts[1], culture); |
|||
return new CornerRadius(top, bottom); |
|||
case 4: |
|||
var topLeft = double.Parse(parts[0], culture); |
|||
var topRight = double.Parse(parts[1], culture); |
|||
var bottomRight = double.Parse(parts[2], culture); |
|||
var bottomLeft = double.Parse(parts[3], culture); |
|||
return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); |
|||
default: |
|||
{ |
|||
throw new FormatException("Invalid CornerRadius."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static bool operator ==(CornerRadius cr1, CornerRadius cr2) |
|||
{ |
|||
return cr1.TopLeft.Equals(cr2.TopLeft) |
|||
&& cr1.TopRight.Equals(cr2.TopRight) |
|||
&& cr1.BottomRight.Equals(cr2.BottomRight) |
|||
&& cr1.BottomLeft.Equals(cr2.BottomLeft); |
|||
} |
|||
|
|||
public static bool operator !=(CornerRadius cr1, CornerRadius cr2) |
|||
{ |
|||
return !(cr1 == cr2); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,224 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
internal static class KnownColors |
|||
{ |
|||
private static readonly IReadOnlyDictionary<string, KnownColor> _knownColorNames; |
|||
private static readonly IReadOnlyDictionary<uint, string> _knownColors; |
|||
private static readonly Dictionary<KnownColor, ISolidColorBrush> _knownBrushes; |
|||
|
|||
static KnownColors() |
|||
{ |
|||
var knownColorNames = new Dictionary<string, KnownColor>(StringComparer.OrdinalIgnoreCase); |
|||
var knownColors = new Dictionary<uint, string>(); |
|||
|
|||
foreach (var field in typeof(KnownColor).GetRuntimeFields()) |
|||
{ |
|||
if (field.FieldType != typeof(KnownColor)) continue; |
|||
var knownColor = (KnownColor)field.GetValue(null); |
|||
if (knownColor == KnownColor.None) continue; |
|||
|
|||
knownColorNames.Add(field.Name, knownColor); |
|||
|
|||
// some known colors have the same value, so use the first
|
|||
if (!knownColors.ContainsKey((uint)knownColor)) |
|||
{ |
|||
knownColors.Add((uint)knownColor, field.Name); |
|||
} |
|||
} |
|||
|
|||
_knownColorNames = knownColorNames; |
|||
_knownColors = knownColors; |
|||
_knownBrushes = new Dictionary<KnownColor, ISolidColorBrush>(); |
|||
} |
|||
|
|||
public static ISolidColorBrush GetKnownBrush(string s) |
|||
{ |
|||
var color = GetKnownColor(s); |
|||
return color != KnownColor.None ? color.ToBrush() : null; |
|||
} |
|||
|
|||
public static KnownColor GetKnownColor(string s) |
|||
{ |
|||
if (_knownColorNames.TryGetValue(s, out var color)) |
|||
{ |
|||
return color; |
|||
} |
|||
|
|||
return KnownColor.None; |
|||
} |
|||
|
|||
public static string GetKnownColorName(uint rgb) |
|||
{ |
|||
return _knownColors.TryGetValue(rgb, out var name) ? name : null; |
|||
} |
|||
|
|||
public static Color ToColor(this KnownColor color) |
|||
{ |
|||
return Color.FromUInt32((uint)color); |
|||
} |
|||
|
|||
public static ISolidColorBrush ToBrush(this KnownColor color) |
|||
{ |
|||
lock (_knownBrushes) |
|||
{ |
|||
if (!_knownBrushes.TryGetValue(color, out var brush)) |
|||
{ |
|||
brush = new Immutable.ImmutableSolidColorBrush(color.ToColor()); |
|||
_knownBrushes.Add(color, brush); |
|||
} |
|||
|
|||
return brush; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal enum KnownColor : uint |
|||
{ |
|||
None, |
|||
AliceBlue = 0xfff0f8ff, |
|||
AntiqueWhite = 0xfffaebd7, |
|||
Aqua = 0xff00ffff, |
|||
Aquamarine = 0xff7fffd4, |
|||
Azure = 0xfff0ffff, |
|||
Beige = 0xfff5f5dc, |
|||
Bisque = 0xffffe4c4, |
|||
Black = 0xff000000, |
|||
BlanchedAlmond = 0xffffebcd, |
|||
Blue = 0xff0000ff, |
|||
BlueViolet = 0xff8a2be2, |
|||
Brown = 0xffa52a2a, |
|||
BurlyWood = 0xffdeb887, |
|||
CadetBlue = 0xff5f9ea0, |
|||
Chartreuse = 0xff7fff00, |
|||
Chocolate = 0xffd2691e, |
|||
Coral = 0xffff7f50, |
|||
CornflowerBlue = 0xff6495ed, |
|||
Cornsilk = 0xfffff8dc, |
|||
Crimson = 0xffdc143c, |
|||
Cyan = 0xff00ffff, |
|||
DarkBlue = 0xff00008b, |
|||
DarkCyan = 0xff008b8b, |
|||
DarkGoldenrod = 0xffb8860b, |
|||
DarkGray = 0xffa9a9a9, |
|||
DarkGreen = 0xff006400, |
|||
DarkKhaki = 0xffbdb76b, |
|||
DarkMagenta = 0xff8b008b, |
|||
DarkOliveGreen = 0xff556b2f, |
|||
DarkOrange = 0xffff8c00, |
|||
DarkOrchid = 0xff9932cc, |
|||
DarkRed = 0xff8b0000, |
|||
DarkSalmon = 0xffe9967a, |
|||
DarkSeaGreen = 0xff8fbc8f, |
|||
DarkSlateBlue = 0xff483d8b, |
|||
DarkSlateGray = 0xff2f4f4f, |
|||
DarkTurquoise = 0xff00ced1, |
|||
DarkViolet = 0xff9400d3, |
|||
DeepPink = 0xffff1493, |
|||
DeepSkyBlue = 0xff00bfff, |
|||
DimGray = 0xff696969, |
|||
DodgerBlue = 0xff1e90ff, |
|||
Firebrick = 0xffb22222, |
|||
FloralWhite = 0xfffffaf0, |
|||
ForestGreen = 0xff228b22, |
|||
Fuchsia = 0xffff00ff, |
|||
Gainsboro = 0xffdcdcdc, |
|||
GhostWhite = 0xfff8f8ff, |
|||
Gold = 0xffffd700, |
|||
Goldenrod = 0xffdaa520, |
|||
Gray = 0xff808080, |
|||
Green = 0xff008000, |
|||
GreenYellow = 0xffadff2f, |
|||
Honeydew = 0xfff0fff0, |
|||
HotPink = 0xffff69b4, |
|||
IndianRed = 0xffcd5c5c, |
|||
Indigo = 0xff4b0082, |
|||
Ivory = 0xfffffff0, |
|||
Khaki = 0xfff0e68c, |
|||
Lavender = 0xffe6e6fa, |
|||
LavenderBlush = 0xfffff0f5, |
|||
LawnGreen = 0xff7cfc00, |
|||
LemonChiffon = 0xfffffacd, |
|||
LightBlue = 0xffadd8e6, |
|||
LightCoral = 0xfff08080, |
|||
LightCyan = 0xffe0ffff, |
|||
LightGoldenrodYellow = 0xfffafad2, |
|||
LightGreen = 0xff90ee90, |
|||
LightGray = 0xffd3d3d3, |
|||
LightPink = 0xffffb6c1, |
|||
LightSalmon = 0xffffa07a, |
|||
LightSeaGreen = 0xff20b2aa, |
|||
LightSkyBlue = 0xff87cefa, |
|||
LightSlateGray = 0xff778899, |
|||
LightSteelBlue = 0xffb0c4de, |
|||
LightYellow = 0xffffffe0, |
|||
Lime = 0xff00ff00, |
|||
LimeGreen = 0xff32cd32, |
|||
Linen = 0xfffaf0e6, |
|||
Magenta = 0xffff00ff, |
|||
Maroon = 0xff800000, |
|||
MediumAquamarine = 0xff66cdaa, |
|||
MediumBlue = 0xff0000cd, |
|||
MediumOrchid = 0xffba55d3, |
|||
MediumPurple = 0xff9370db, |
|||
MediumSeaGreen = 0xff3cb371, |
|||
MediumSlateBlue = 0xff7b68ee, |
|||
MediumSpringGreen = 0xff00fa9a, |
|||
MediumTurquoise = 0xff48d1cc, |
|||
MediumVioletRed = 0xffc71585, |
|||
MidnightBlue = 0xff191970, |
|||
MintCream = 0xfff5fffa, |
|||
MistyRose = 0xffffe4e1, |
|||
Moccasin = 0xffffe4b5, |
|||
NavajoWhite = 0xffffdead, |
|||
Navy = 0xff000080, |
|||
OldLace = 0xfffdf5e6, |
|||
Olive = 0xff808000, |
|||
OliveDrab = 0xff6b8e23, |
|||
Orange = 0xffffa500, |
|||
OrangeRed = 0xffff4500, |
|||
Orchid = 0xffda70d6, |
|||
PaleGoldenrod = 0xffeee8aa, |
|||
PaleGreen = 0xff98fb98, |
|||
PaleTurquoise = 0xffafeeee, |
|||
PaleVioletRed = 0xffdb7093, |
|||
PapayaWhip = 0xffffefd5, |
|||
PeachPuff = 0xffffdab9, |
|||
Peru = 0xffcd853f, |
|||
Pink = 0xffffc0cb, |
|||
Plum = 0xffdda0dd, |
|||
PowderBlue = 0xffb0e0e6, |
|||
Purple = 0xff800080, |
|||
Red = 0xffff0000, |
|||
RosyBrown = 0xffbc8f8f, |
|||
RoyalBlue = 0xff4169e1, |
|||
SaddleBrown = 0xff8b4513, |
|||
Salmon = 0xfffa8072, |
|||
SandyBrown = 0xfff4a460, |
|||
SeaGreen = 0xff2e8b57, |
|||
SeaShell = 0xfffff5ee, |
|||
Sienna = 0xffa0522d, |
|||
Silver = 0xffc0c0c0, |
|||
SkyBlue = 0xff87ceeb, |
|||
SlateBlue = 0xff6a5acd, |
|||
SlateGray = 0xff708090, |
|||
Snow = 0xfffffafa, |
|||
SpringGreen = 0xff00ff7f, |
|||
SteelBlue = 0xff4682b4, |
|||
Tan = 0xffd2b48c, |
|||
Teal = 0xff008080, |
|||
Thistle = 0xffd8bfd8, |
|||
Tomato = 0xffff6347, |
|||
Transparent = 0x00ffffff, |
|||
Turquoise = 0xff40e0d0, |
|||
Violet = 0xffee82ee, |
|||
Wheat = 0xfff5deb3, |
|||
White = 0xffffffff, |
|||
WhiteSmoke = 0xfff5f5f5, |
|||
Yellow = 0xffffff00, |
|||
YellowGreen = 0xff9acd32 |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
|
|||
namespace Avalonia.Markup.Xaml.Converters |
|||
{ |
|||
public class CornerRadiusTypeConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|||
{ |
|||
return CornerRadius.Parse((string)value, culture); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Data; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using System.Reactive.Linq; |
|||
using System.Reactive.Subjects; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Serialization.Formatters.Binary; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Input.Raw; |
|||
using MonoMac; |
|||
using MonoMac.AppKit; |
|||
using MonoMac.CoreGraphics; |
|||
using MonoMac.Foundation; |
|||
using MonoMac.OpenGL; |
|||
|
|||
namespace Avalonia.MonoMac |
|||
{ |
|||
public class DragSource : NSDraggingSource, IPlatformDragSource |
|||
{ |
|||
private const string NSPasteboardTypeString = "public.utf8-plain-text"; |
|||
private const string NSPasteboardTypeFileUrl = "public.file-url"; |
|||
|
|||
private readonly Subject<DragDropEffects> _result = new Subject<DragDropEffects>(); |
|||
private readonly IInputManager _inputManager; |
|||
private DragDropEffects _allowedEffects; |
|||
|
|||
public override bool IgnoreModifierKeysWhileDragging => false; |
|||
|
|||
public DragSource() |
|||
{ |
|||
_inputManager = AvaloniaLocator.Current.GetService<IInputManager>(); |
|||
} |
|||
|
|||
private string DataFormatToUTI(string s) |
|||
{ |
|||
if (s == DataFormats.FileNames) |
|||
return NSPasteboardTypeFileUrl; |
|||
if (s == DataFormats.Text) |
|||
return NSPasteboardTypeString; |
|||
return s; |
|||
} |
|||
|
|||
private NSDraggingItem CreateDraggingItem(string format, object data) |
|||
{ |
|||
var pasteboardItem = new NSPasteboardItem(); |
|||
NSData nsData; |
|||
if (data is string s) |
|||
{ |
|||
if (format == DataFormats.FileNames) |
|||
s = new Uri(s).AbsoluteUri; // Ensure file uris...
|
|||
nsData = NSData.FromString(s); |
|||
} |
|||
else if (data is Stream strm) |
|||
nsData = NSData.FromStream(strm); |
|||
else if (data is byte[] bytes) |
|||
nsData = NSData.FromArray(bytes); |
|||
else |
|||
{ |
|||
BinaryFormatter bf = new BinaryFormatter(); |
|||
using (var ms = new MemoryStream()) |
|||
{ |
|||
bf.Serialize(ms, data); |
|||
ms.Position = 0; |
|||
nsData = NSData.FromStream(ms); |
|||
} |
|||
} |
|||
pasteboardItem.SetDataForType(nsData, DataFormatToUTI(format)); |
|||
|
|||
NSPasteboardWriting writing = new NSPasteboardWriting(pasteboardItem.Handle); |
|||
|
|||
return new NSDraggingItem(writing); |
|||
} |
|||
|
|||
public IEnumerable<NSDraggingItem> CreateDraggingItems(string format, object data) |
|||
{ |
|||
if (format == DataFormats.FileNames && data is IEnumerable<string> files) |
|||
{ |
|||
foreach (var file in files) |
|||
yield return CreateDraggingItem(format, file); |
|||
|
|||
yield break; |
|||
} |
|||
|
|||
yield return CreateDraggingItem(format, data); |
|||
} |
|||
|
|||
|
|||
public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects) |
|||
{ |
|||
// We need the TopLevelImpl + a mouse location so we just wait for the next event.
|
|||
var mouseEv = await _inputManager.PreProcess.OfType<RawMouseEventArgs>().FirstAsync(); |
|||
var view = ((mouseEv.Root as TopLevel)?.PlatformImpl as TopLevelImpl)?.View; |
|||
if (view == null) |
|||
return DragDropEffects.None; |
|||
|
|||
// Prepare the source event:
|
|||
var pt = view.TranslateLocalPoint(mouseEv.Position).ToMonoMacPoint(); |
|||
var ev = NSEvent.MouseEvent(NSEventType.LeftMouseDown, pt, 0, 0, 0, null, 0, 0, 0); |
|||
|
|||
_allowedEffects = allowedEffects; |
|||
var items = data.GetDataFormats().SelectMany(fmt => CreateDraggingItems(fmt, data.Get(fmt))).ToArray(); |
|||
view.BeginDraggingSession(items ,ev, this); |
|||
|
|||
return await _result; |
|||
} |
|||
|
|||
public override NSDragOperation DraggingSourceOperationMaskForLocal(bool flag) |
|||
{ |
|||
return DraggingInfo.ConvertDragOperation(_allowedEffects); |
|||
} |
|||
|
|||
public override void DraggedImageEndedAtOperation(NSImage image, CGPoint screenPoint, NSDragOperation operation) |
|||
{ |
|||
_result.OnNext(DraggingInfo.ConvertDragOperation(operation)); |
|||
_result.OnCompleted(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Input; |
|||
using MonoMac.AppKit; |
|||
using MonoMac.Foundation; |
|||
|
|||
namespace Avalonia.MonoMac |
|||
{ |
|||
class DraggingInfo : IDataObject |
|||
{ |
|||
private readonly NSDraggingInfo _info; |
|||
|
|||
public DraggingInfo(NSDraggingInfo info) |
|||
{ |
|||
_info = info; |
|||
} |
|||
|
|||
internal static NSDragOperation ConvertDragOperation(DragDropEffects d) |
|||
{ |
|||
NSDragOperation result = NSDragOperation.None; |
|||
if (d.HasFlag(DragDropEffects.Copy)) |
|||
result |= NSDragOperation.Copy; |
|||
if (d.HasFlag(DragDropEffects.Link)) |
|||
result |= NSDragOperation.Link; |
|||
if (d.HasFlag(DragDropEffects.Move)) |
|||
result |= NSDragOperation.Move; |
|||
return result; |
|||
} |
|||
|
|||
internal static DragDropEffects ConvertDragOperation(NSDragOperation d) |
|||
{ |
|||
DragDropEffects result = DragDropEffects.None; |
|||
if (d.HasFlag(NSDragOperation.Copy)) |
|||
result |= DragDropEffects.Copy; |
|||
if (d.HasFlag(NSDragOperation.Link)) |
|||
result |= DragDropEffects.Link; |
|||
if (d.HasFlag(NSDragOperation.Move)) |
|||
result |= DragDropEffects.Move; |
|||
return result; |
|||
} |
|||
|
|||
public Point Location => new Point(_info.DraggingLocation.X, _info.DraggingLocation.Y); |
|||
|
|||
public IEnumerable<string> GetDataFormats() |
|||
{ |
|||
return _info.DraggingPasteboard.Types.Select(NSTypeToWellknownType); |
|||
} |
|||
|
|||
private string NSTypeToWellknownType(string type) |
|||
{ |
|||
if (type == NSPasteboard.NSStringType) |
|||
return DataFormats.Text; |
|||
if (type == NSPasteboard.NSFilenamesType) |
|||
return DataFormats.FileNames; |
|||
return type; |
|||
} |
|||
|
|||
public string GetText() |
|||
{ |
|||
return _info.DraggingPasteboard.GetStringForType(NSPasteboard.NSStringType); |
|||
} |
|||
|
|||
public IEnumerable<string> GetFileNames() |
|||
{ |
|||
using(var fileNames = (NSArray)_info.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType)) |
|||
{ |
|||
if (fileNames != null) |
|||
return NSArray.StringArrayFromHandle(fileNames.Handle); |
|||
} |
|||
|
|||
return Enumerable.Empty<string>(); |
|||
} |
|||
|
|||
public bool Contains(string dataFormat) |
|||
{ |
|||
return GetDataFormats().Any(f => f == dataFormat); |
|||
} |
|||
|
|||
public object Get(string dataFormat) |
|||
{ |
|||
if (dataFormat == DataFormats.Text) |
|||
return GetText(); |
|||
if (dataFormat == DataFormats.FileNames) |
|||
return GetFileNames(); |
|||
|
|||
return _info.DraggingPasteboard.GetDataForType(dataFormat).ToArray(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Avalonia.Input; |
|||
using Avalonia.Win32.Interop; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
static class ClipboardFormats |
|||
{ |
|||
private const int MAX_FORMAT_NAME_LENGTH = 260; |
|||
|
|||
class ClipboardFormat |
|||
{ |
|||
public short Format { get; private set; } |
|||
public string Name { get; private set; } |
|||
public short[] Synthesized { get; private set; } |
|||
|
|||
public ClipboardFormat(string name, short format, params short[] synthesized) |
|||
{ |
|||
Format = format; |
|||
Name = name; |
|||
Synthesized = synthesized; |
|||
} |
|||
} |
|||
|
|||
private static readonly List<ClipboardFormat> FormatList = new List<ClipboardFormat>() |
|||
{ |
|||
new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT), |
|||
new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), |
|||
}; |
|||
|
|||
|
|||
private static string QueryFormatName(short format) |
|||
{ |
|||
StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); |
|||
if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) |
|||
return sb.ToString(); |
|||
return null; |
|||
} |
|||
|
|||
public static string GetFormat(short format) |
|||
{ |
|||
lock (FormatList) |
|||
{ |
|||
var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0); |
|||
if (pd == null) |
|||
{ |
|||
string name = QueryFormatName(format); |
|||
if (string.IsNullOrEmpty(name)) |
|||
name = string.Format("Unknown_Format_{0}", format); |
|||
pd = new ClipboardFormat(name, format); |
|||
FormatList.Add(pd); |
|||
} |
|||
return pd.Name; |
|||
} |
|||
} |
|||
|
|||
public static short GetFormat(string format) |
|||
{ |
|||
lock (FormatList) |
|||
{ |
|||
var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format)); |
|||
if (pd == null) |
|||
{ |
|||
int id = UnmanagedMethods.RegisterClipboardFormat(format); |
|||
if (id == 0) |
|||
throw new Win32Exception(); |
|||
pd = new ClipboardFormat(format, (short)id); |
|||
FormatList.Add(pd); |
|||
} |
|||
return pd.Format; |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,361 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.InteropServices.ComTypes; |
|||
using System.Text; |
|||
using Avalonia.Input; |
|||
using Avalonia.Win32.Interop; |
|||
using IDataObject = Avalonia.Input.IDataObject; |
|||
using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; |
|||
using System.IO; |
|||
using System.Runtime.Serialization.Formatters.Binary; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class DataObject : IDataObject, IOleDataObject |
|||
{ |
|||
// Compatibility with WinForms + WPF...
|
|||
internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); |
|||
|
|||
class FormatEnumerator : IEnumFORMATETC |
|||
{ |
|||
private FORMATETC[] _formats; |
|||
private int _current; |
|||
|
|||
private FormatEnumerator(FORMATETC[] formats, int current) |
|||
{ |
|||
_formats = formats; |
|||
_current = current; |
|||
} |
|||
|
|||
public FormatEnumerator(IDataObject dataobj) |
|||
{ |
|||
_formats = dataobj.GetDataFormats().Select(ConvertToFormatEtc).ToArray(); |
|||
_current = 0; |
|||
} |
|||
|
|||
private FORMATETC ConvertToFormatEtc(string aFormatName) |
|||
{ |
|||
FORMATETC result = default(FORMATETC); |
|||
result.cfFormat = ClipboardFormats.GetFormat(aFormatName); |
|||
result.dwAspect = DVASPECT.DVASPECT_CONTENT; |
|||
result.ptd = IntPtr.Zero; |
|||
result.lindex = -1; |
|||
result.tymed = TYMED.TYMED_HGLOBAL; |
|||
return result; |
|||
} |
|||
|
|||
public void Clone(out IEnumFORMATETC newEnum) |
|||
{ |
|||
newEnum = new FormatEnumerator(_formats, _current); |
|||
} |
|||
|
|||
public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched) |
|||
{ |
|||
if (rgelt == null) |
|||
return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG); |
|||
|
|||
int i = 0; |
|||
while (i < celt && _current < _formats.Length) |
|||
{ |
|||
rgelt[i] = _formats[_current]; |
|||
_current++; |
|||
i++; |
|||
} |
|||
if (pceltFetched != null) |
|||
pceltFetched[0] = i; |
|||
|
|||
if (i != celt) |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
|
|||
public int Reset() |
|||
{ |
|||
_current = 0; |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
|
|||
public int Skip(int celt) |
|||
{ |
|||
_current += Math.Min(celt, int.MaxValue - _current); |
|||
if (_current >= _formats.Length) |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
} |
|||
|
|||
private const int DV_E_TYMED = unchecked((int)0x80040069); |
|||
private const int DV_E_DVASPECT = unchecked((int)0x8004006B); |
|||
private const int DV_E_FORMATETC = unchecked((int)0x80040064); |
|||
private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003); |
|||
private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070); |
|||
private const int GMEM_ZEROINIT = 0x0040; |
|||
private const int GMEM_MOVEABLE = 0x0002; |
|||
|
|||
|
|||
IDataObject _wrapped; |
|||
|
|||
public DataObject(IDataObject wrapped) |
|||
{ |
|||
_wrapped = wrapped; |
|||
} |
|||
|
|||
#region IDataObject
|
|||
bool IDataObject.Contains(string dataFormat) |
|||
{ |
|||
return _wrapped.Contains(dataFormat); |
|||
} |
|||
|
|||
IEnumerable<string> IDataObject.GetDataFormats() |
|||
{ |
|||
return _wrapped.GetDataFormats(); |
|||
} |
|||
|
|||
IEnumerable<string> IDataObject.GetFileNames() |
|||
{ |
|||
return _wrapped.GetFileNames(); |
|||
} |
|||
|
|||
string IDataObject.GetText() |
|||
{ |
|||
return _wrapped.GetText(); |
|||
} |
|||
|
|||
object IDataObject.Get(string dataFormat) |
|||
{ |
|||
return _wrapped.Get(dataFormat); |
|||
} |
|||
#endregion
|
|||
|
|||
#region IOleDataObject
|
|||
|
|||
int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection); |
|||
connection = 0; |
|||
return OLE_E_ADVISENOTSUPPORTED; |
|||
} |
|||
|
|||
void IOleDataObject.DUnadvise(int connection) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
ole.DUnadvise(connection); |
|||
Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED); |
|||
} |
|||
|
|||
int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
return ole.EnumDAdvise(out enumAdvise); |
|||
|
|||
enumAdvise = null; |
|||
return OLE_E_ADVISENOTSUPPORTED; |
|||
} |
|||
|
|||
IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
return ole.EnumFormatEtc(direction); |
|||
if (direction == DATADIR.DATADIR_GET) |
|||
return new FormatEnumerator(_wrapped); |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut); |
|||
|
|||
formatOut = new FORMATETC(); |
|||
formatOut.ptd = IntPtr.Zero; |
|||
return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL); |
|||
} |
|||
|
|||
void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
{ |
|||
ole.GetData(ref format, out medium); |
|||
return; |
|||
} |
|||
if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) |
|||
Marshal.ThrowExceptionForHR(DV_E_TYMED); |
|||
|
|||
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) |
|||
Marshal.ThrowExceptionForHR(DV_E_DVASPECT); |
|||
|
|||
string fmt = ClipboardFormats.GetFormat(format.cfFormat); |
|||
if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) |
|||
Marshal.ThrowExceptionForHR(DV_E_FORMATETC); |
|||
|
|||
medium = default(STGMEDIUM); |
|||
medium.tymed = TYMED.TYMED_HGLOBAL; |
|||
int result = WriteDataToHGlobal(fmt, ref medium.unionmember); |
|||
Marshal.ThrowExceptionForHR(result); |
|||
} |
|||
|
|||
void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
{ |
|||
ole.GetDataHere(ref format, ref medium); |
|||
return; |
|||
} |
|||
|
|||
if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) |
|||
Marshal.ThrowExceptionForHR(DV_E_TYMED); |
|||
|
|||
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) |
|||
Marshal.ThrowExceptionForHR(DV_E_DVASPECT); |
|||
|
|||
string fmt = ClipboardFormats.GetFormat(format.cfFormat); |
|||
if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) |
|||
Marshal.ThrowExceptionForHR(DV_E_FORMATETC); |
|||
|
|||
if (medium.unionmember == IntPtr.Zero) |
|||
Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL); |
|||
|
|||
int result = WriteDataToHGlobal(fmt, ref medium.unionmember); |
|||
Marshal.ThrowExceptionForHR(result); |
|||
} |
|||
|
|||
int IOleDataObject.QueryGetData(ref FORMATETC format) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
return ole.QueryGetData(ref format); |
|||
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) |
|||
return DV_E_DVASPECT; |
|||
if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) |
|||
return DV_E_TYMED; |
|||
|
|||
string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); |
|||
if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat)) |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
return DV_E_FORMATETC; |
|||
} |
|||
|
|||
void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release) |
|||
{ |
|||
if (_wrapped is IOleDataObject ole) |
|||
{ |
|||
ole.SetData(ref formatIn, ref medium, release); |
|||
return; |
|||
} |
|||
Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); |
|||
} |
|||
|
|||
private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) |
|||
{ |
|||
object data = _wrapped.Get(dataFormat); |
|||
if (dataFormat == DataFormats.Text || data is string) |
|||
return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data)); |
|||
if (dataFormat == DataFormats.FileNames && data is IEnumerable<string> files) |
|||
return WriteFileListToHGlobal(ref hGlobal, files); |
|||
if (data is Stream stream) |
|||
{ |
|||
byte[] buffer = new byte[stream.Length - stream.Position]; |
|||
stream.Read(buffer, 0, buffer.Length); |
|||
return WriteBytesToHGlobal(ref hGlobal, buffer); |
|||
} |
|||
if (data is IEnumerable<byte> bytes) |
|||
{ |
|||
var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray(); |
|||
return WriteBytesToHGlobal(ref hGlobal, byteArr); |
|||
} |
|||
return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data)); |
|||
} |
|||
|
|||
private byte[] SerializeObject(object data) |
|||
{ |
|||
using (var ms = new MemoryStream()) |
|||
{ |
|||
ms.Write(SerializedObjectGUID, 0, SerializedObjectGUID.Length); |
|||
BinaryFormatter binaryFormatter = new BinaryFormatter(); |
|||
binaryFormatter.Serialize(ms, data); |
|||
return ms.ToArray(); |
|||
} |
|||
} |
|||
|
|||
private int WriteBytesToHGlobal(ref IntPtr hGlobal, byte[] data) |
|||
{ |
|||
int required = data.Length; |
|||
if (hGlobal == IntPtr.Zero) |
|||
hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); |
|||
|
|||
long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); |
|||
if (required > available) |
|||
return STG_E_MEDIUMFULL; |
|||
|
|||
IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); |
|||
try |
|||
{ |
|||
Marshal.Copy(data, 0, ptr, data.Length); |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.GlobalUnlock(hGlobal); |
|||
} |
|||
} |
|||
|
|||
private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable<string> files) |
|||
{ |
|||
if (!files?.Any() ?? false) |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
|
|||
char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray(); |
|||
_DROPFILES df = new _DROPFILES(); |
|||
df.pFiles = Marshal.SizeOf<_DROPFILES>(); |
|||
df.fWide = true; |
|||
|
|||
int required = (filesStr.Length * sizeof(char)) + Marshal.SizeOf<_DROPFILES>(); |
|||
if (hGlobal == IntPtr.Zero) |
|||
hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); |
|||
|
|||
long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); |
|||
if (required > available) |
|||
return STG_E_MEDIUMFULL; |
|||
|
|||
IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); |
|||
try |
|||
{ |
|||
Marshal.StructureToPtr(df, ptr, false); |
|||
|
|||
Marshal.Copy(filesStr, 0, ptr + Marshal.SizeOf<_DROPFILES>(), filesStr.Length); |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.GlobalUnlock(hGlobal); |
|||
} |
|||
} |
|||
|
|||
private int WriteStringToHGlobal(ref IntPtr hGlobal, string data) |
|||
{ |
|||
int required = (data.Length + 1) * sizeof(char); |
|||
if (hGlobal == IntPtr.Zero) |
|||
hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, required); |
|||
|
|||
long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); |
|||
if (required > available) |
|||
return STG_E_MEDIUMFULL; |
|||
|
|||
IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); |
|||
try |
|||
{ |
|||
char[] chars = (data + '\0').ToCharArray(); |
|||
Marshal.Copy(chars, 0, ptr, chars.Length); |
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.GlobalUnlock(hGlobal); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Win32.Interop; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class DragSource : IPlatformDragSource |
|||
{ |
|||
public Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects) |
|||
{ |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
OleDragSource src = new OleDragSource(); |
|||
DataObject dataObject = new DataObject(data); |
|||
int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); |
|||
|
|||
int[] finalEffect = new int[1]; |
|||
UnmanagedMethods.DoDragDrop(dataObject, src, allowed, finalEffect); |
|||
|
|||
return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect[0]));} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Win32.Interop; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class OleContext |
|||
{ |
|||
private static OleContext fCurrent; |
|||
|
|||
internal static OleContext Current |
|||
{ |
|||
get |
|||
{ |
|||
if (!IsValidOleThread()) |
|||
return null; |
|||
|
|||
if (fCurrent == null) |
|||
fCurrent = new OleContext(); |
|||
return fCurrent; |
|||
} |
|||
} |
|||
|
|||
|
|||
private OleContext() |
|||
{ |
|||
if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK) |
|||
throw new SystemException("Failed to initialize OLE"); |
|||
} |
|||
|
|||
private static bool IsValidOleThread() |
|||
{ |
|||
return Dispatcher.UIThread.CheckAccess() && |
|||
Thread.CurrentThread.GetApartmentState() == ApartmentState.STA; |
|||
} |
|||
|
|||
internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) |
|||
{ |
|||
if (hwnd?.HandleDescriptor != "HWND" || target == null) |
|||
return false; |
|||
|
|||
return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.InteropServices.ComTypes; |
|||
using System.Runtime.Serialization.Formatters.Binary; |
|||
using System.Text; |
|||
using Avalonia.Input; |
|||
using Avalonia.Win32.Interop; |
|||
using IDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class OleDataObject : Avalonia.Input.IDataObject |
|||
{ |
|||
private IDataObject _wrapped; |
|||
|
|||
public OleDataObject(IDataObject wrapped) |
|||
{ |
|||
_wrapped = wrapped; |
|||
} |
|||
|
|||
public bool Contains(string dataFormat) |
|||
{ |
|||
return GetDataFormatsCore().Any(df => StringComparer.OrdinalIgnoreCase.Equals(df, dataFormat)); |
|||
} |
|||
|
|||
public IEnumerable<string> GetDataFormats() |
|||
{ |
|||
return GetDataFormatsCore().Distinct(); |
|||
} |
|||
|
|||
public string GetText() |
|||
{ |
|||
return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string; |
|||
} |
|||
|
|||
public IEnumerable<string> GetFileNames() |
|||
{ |
|||
return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable<string>; |
|||
} |
|||
|
|||
public object Get(string dataFormat) |
|||
{ |
|||
return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); |
|||
} |
|||
|
|||
private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) |
|||
{ |
|||
FORMATETC formatEtc = new FORMATETC(); |
|||
formatEtc.cfFormat = ClipboardFormats.GetFormat(format); |
|||
formatEtc.dwAspect = aspect; |
|||
formatEtc.lindex = -1; |
|||
formatEtc.tymed = TYMED.TYMED_HGLOBAL; |
|||
if (_wrapped.QueryGetData(ref formatEtc) == 0) |
|||
{ |
|||
_wrapped.GetData(ref formatEtc, out STGMEDIUM medium); |
|||
try |
|||
{ |
|||
if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL) |
|||
{ |
|||
if (format == DataFormats.Text) |
|||
return ReadStringFromHGlobal(medium.unionmember); |
|||
if (format == DataFormats.FileNames) |
|||
return ReadFileNamesFromHGlobal(medium.unionmember); |
|||
|
|||
byte[] data = ReadBytesFromHGlobal(medium.unionmember); |
|||
|
|||
if (IsSerializedObject(data)) |
|||
{ |
|||
using (var ms = new MemoryStream(data)) |
|||
{ |
|||
ms.Position = DataObject.SerializedObjectGUID.Length; |
|||
BinaryFormatter binaryFormatter = new BinaryFormatter(); |
|||
return binaryFormatter.Deserialize(ms); |
|||
} |
|||
} |
|||
return data; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.ReleaseStgMedium(ref medium); |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private bool IsSerializedObject(byte[] data) |
|||
{ |
|||
if (data.Length < DataObject.SerializedObjectGUID.Length) |
|||
return false; |
|||
for (int i = 0; i < DataObject.SerializedObjectGUID.Length; i++) |
|||
if (data[i] != DataObject.SerializedObjectGUID[i]) |
|||
return false; |
|||
return true; |
|||
} |
|||
|
|||
private static IEnumerable<string> ReadFileNamesFromHGlobal(IntPtr hGlobal) |
|||
{ |
|||
List<string> files = new List<string>(); |
|||
int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0); |
|||
if (fileCount > 0) |
|||
{ |
|||
for (int i = 0; i < fileCount; i++) |
|||
{ |
|||
int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0); |
|||
StringBuilder sb = new StringBuilder(pathLen+1); |
|||
|
|||
if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen) |
|||
{ |
|||
files.Add(sb.ToString()); |
|||
} |
|||
} |
|||
} |
|||
return files; |
|||
} |
|||
|
|||
private static string ReadStringFromHGlobal(IntPtr hGlobal) |
|||
{ |
|||
IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); |
|||
try |
|||
{ |
|||
return Marshal.PtrToStringAuto(ptr); |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.GlobalUnlock(hGlobal); |
|||
} |
|||
} |
|||
|
|||
private static byte[] ReadBytesFromHGlobal(IntPtr hGlobal) |
|||
{ |
|||
IntPtr source = UnmanagedMethods.GlobalLock(hGlobal); |
|||
try |
|||
{ |
|||
int size = (int)UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); |
|||
byte[] data = new byte[size]; |
|||
Marshal.Copy(source, data, 0, size); |
|||
return data; |
|||
} |
|||
finally |
|||
{ |
|||
UnmanagedMethods.GlobalUnlock(hGlobal); |
|||
} |
|||
} |
|||
|
|||
private IEnumerable<string> GetDataFormatsCore() |
|||
{ |
|||
var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET); |
|||
if (enumFormat != null) |
|||
{ |
|||
enumFormat.Reset(); |
|||
FORMATETC[] formats = new FORMATETC[1]; |
|||
int[] fetched = { 1 }; |
|||
while (fetched[0] > 0) |
|||
{ |
|||
fetched[0] = 0; |
|||
if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0) |
|||
{ |
|||
if (formats[0].ptd != IntPtr.Zero) |
|||
Marshal.FreeCoTaskMem(formats[0].ptd); |
|||
|
|||
yield return ClipboardFormats.GetFormat(formats[0].cfFormat); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Win32.Interop; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class OleDragSource : IDropSource |
|||
{ |
|||
private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102; |
|||
private const int DRAGDROP_S_DROP = 0x00040100; |
|||
private const int DRAGDROP_S_CANCEL = 0x00040101; |
|||
|
|||
private const int KEYSTATE_LEFTMB = 1; |
|||
private const int KEYSTATE_MIDDLEMB = 16; |
|||
private const int KEYSTATE_RIGHTMB = 2; |
|||
private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB }; |
|||
|
|||
public int QueryContinueDrag(int fEscapePressed, int grfKeyState) |
|||
{ |
|||
if (fEscapePressed != 0) |
|||
return DRAGDROP_S_CANCEL; |
|||
|
|||
int pressedMouseButtons = MOUSE_BUTTONS.Where(mb => (grfKeyState & mb) == mb).Count(); |
|||
|
|||
if (pressedMouseButtons >= 2) |
|||
return DRAGDROP_S_CANCEL; |
|||
if (pressedMouseButtons == 0) |
|||
return DRAGDROP_S_DROP; |
|||
|
|||
return unchecked((int)UnmanagedMethods.HRESULT.S_OK); |
|||
} |
|||
|
|||
public int GiveFeedback(int dwEffect) |
|||
{ |
|||
return DRAGDROP_S_USEDEFAULTCURSORS; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Win32.Interop; |
|||
using IDataObject = Avalonia.Input.IDataObject; |
|||
using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; |
|||
|
|||
namespace Avalonia.Win32 |
|||
{ |
|||
class OleDropTarget : IDropTarget |
|||
{ |
|||
private readonly IInputElement _target; |
|||
private readonly ITopLevelImpl _tl; |
|||
private readonly IDragDropDevice _dragDevice; |
|||
|
|||
private IDataObject _currentDrag = null; |
|||
|
|||
public OleDropTarget(ITopLevelImpl tl, IInputElement target) |
|||
{ |
|||
_dragDevice = AvaloniaLocator.Current.GetService<IDragDropDevice>(); |
|||
_tl = tl; |
|||
_target = target; |
|||
} |
|||
|
|||
public static DropEffect ConvertDropEffect(DragDropEffects operation) |
|||
{ |
|||
DropEffect result = DropEffect.None; |
|||
if (operation.HasFlag(DragDropEffects.Copy)) |
|||
result |= DropEffect.Copy; |
|||
if (operation.HasFlag(DragDropEffects.Move)) |
|||
result |= DropEffect.Move; |
|||
if (operation.HasFlag(DragDropEffects.Link)) |
|||
result |= DropEffect.Link; |
|||
return result; |
|||
} |
|||
|
|||
public static DragDropEffects ConvertDropEffect(DropEffect effect) |
|||
{ |
|||
DragDropEffects result = DragDropEffects.None; |
|||
if (effect.HasFlag(DropEffect.Copy)) |
|||
result |= DragDropEffects.Copy; |
|||
if (effect.HasFlag(DropEffect.Move)) |
|||
result |= DragDropEffects.Move; |
|||
if (effect.HasFlag(DropEffect.Link)) |
|||
result |= DragDropEffects.Link; |
|||
return result; |
|||
} |
|||
|
|||
UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) |
|||
{ |
|||
var dispatch = _tl?.Input; |
|||
if (dispatch == null) |
|||
{ |
|||
pdwEffect = DropEffect.None; |
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
_currentDrag = pDataObj as IDataObject; |
|||
if (_currentDrag == null) |
|||
_currentDrag = new OleDataObject(pDataObj); |
|||
var args = new RawDragEvent( |
|||
_dragDevice, |
|||
RawDragEventType.DragEnter, |
|||
_target, |
|||
GetDragLocation(pt), |
|||
_currentDrag, |
|||
ConvertDropEffect(pdwEffect) |
|||
); |
|||
dispatch(args); |
|||
pdwEffect = ConvertDropEffect(args.Effects); |
|||
|
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
|
|||
UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect) |
|||
{ |
|||
var dispatch = _tl?.Input; |
|||
if (dispatch == null) |
|||
{ |
|||
pdwEffect = DropEffect.None; |
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
|
|||
var args = new RawDragEvent( |
|||
_dragDevice, |
|||
RawDragEventType.DragOver, |
|||
_target, |
|||
GetDragLocation(pt), |
|||
_currentDrag, |
|||
ConvertDropEffect(pdwEffect) |
|||
); |
|||
dispatch(args); |
|||
pdwEffect = ConvertDropEffect(args.Effects); |
|||
|
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
|
|||
UnmanagedMethods.HRESULT IDropTarget.DragLeave() |
|||
{ |
|||
try |
|||
{ |
|||
_tl?.Input(new RawDragEvent( |
|||
_dragDevice, |
|||
RawDragEventType.DragLeave, |
|||
_target, |
|||
default(Point), |
|||
null, |
|||
DragDropEffects.None |
|||
)); |
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
finally |
|||
{ |
|||
_currentDrag = null; |
|||
} |
|||
} |
|||
|
|||
UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) |
|||
{ |
|||
try |
|||
{ |
|||
var dispatch = _tl?.Input; |
|||
if (dispatch == null) |
|||
{ |
|||
pdwEffect = DropEffect.None; |
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
|
|||
_currentDrag = pDataObj as IDataObject; |
|||
if (_currentDrag == null) |
|||
_currentDrag= new OleDataObject(pDataObj); |
|||
|
|||
var args = new RawDragEvent( |
|||
_dragDevice, |
|||
RawDragEventType.Drop, |
|||
_target, |
|||
GetDragLocation(pt), |
|||
_currentDrag, |
|||
ConvertDropEffect(pdwEffect) |
|||
); |
|||
dispatch(args); |
|||
pdwEffect = ConvertDropEffect(args.Effects); |
|||
|
|||
return UnmanagedMethods.HRESULT.S_OK; |
|||
} |
|||
finally |
|||
{ |
|||
_currentDrag = null; |
|||
} |
|||
} |
|||
|
|||
private Point GetDragLocation(long dragPoint) |
|||
{ |
|||
int x = (int)dragPoint; |
|||
int y = (int)(dragPoint >> 32); |
|||
|
|||
Point screenPt = new Point(x, y); |
|||
return _target.PointToClient(screenPt); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue