Modified the triggering method and parameter passing of the Command;changedSelectedPathtoSelectedPathsTextand supported two-way conversion withSelectedPaths;modified the parsing method ofFileFilterso that it can override theNameparameter of theFilePickerFileTypeconstructor and can define multipleFilePickerFileTypeinstances;added support forFilePickerFileTypesinFileFilter;and added a new PathPicker stylePathPickerForMultipleText.
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
<StackPanel>
|
||||
<HeaderedContentControl Theme="{DynamicResource GroupBox}"
|
||||
Header="Functionality and Usage"
|
||||
Content="PathPicker aggregates file selection functionality and provides a Command property. When a button is pressed or Enter is pressed while the focus is on PathPicker, the Command is triggered and a Task <IReadOnlyList> is passed to the Command.">
|
||||
Content="PathPicker aggregates a file selector and provides a Command property. The Command is triggered solely after opening the file selector and selecting a file, whereupon the Command receives an IReadOnlyList<string> parameter.">
|
||||
</HeaderedContentControl>
|
||||
<u:Form LabelAlignment="Left" LabelPosition="Left" LabelWidth="*" HorizontalAlignment="Stretch">
|
||||
<TextBox Name="Title" u:FormItem.Label="Title"></TextBox>
|
||||
@@ -24,7 +24,7 @@
|
||||
Watermark="D:\Win7 Help\win-x64">
|
||||
</TextBox>
|
||||
<TextBox Name="FileFilter" u:FormItem.Label="FileFilter"
|
||||
Watermark="*.txt,*.json or *.json">
|
||||
Watermark="[Name,Pattern] like this [123,*.exe,*.pdb] or [All][ImageAll][11,*.txt]">
|
||||
</TextBox>
|
||||
<TextBox Name="DefaultFileExtension" u:FormItem.Label="DefaultFileExtension"
|
||||
Watermark="json">
|
||||
@@ -46,7 +46,20 @@
|
||||
DefaultFileExtension="{Binding #DefaultFileExtension.Text}"
|
||||
AllowMultiple="{Binding #AllowMultiple.IsChecked}"
|
||||
UsePickerType="{Binding #UsePickerType.Value}"
|
||||
SelectedPath="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPathsText="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPaths="{Binding Paths,Mode=OneWayToSource}">
|
||||
</u:PathPicker>
|
||||
</HeaderedContentControl>
|
||||
<HeaderedContentControl Header="PathPickerForMultipleText">
|
||||
<u:PathPicker Title="{Binding #Title.Text}"
|
||||
Theme="{DynamicResource PathPickerForMultipleText}"
|
||||
SuggestedFileName="{Binding #SuggestedFileName.Text}"
|
||||
SuggestedStartPath="{Binding #SuggestedStartPath.Text}"
|
||||
FileFilter="{Binding #FileFilter.Text}"
|
||||
DefaultFileExtension="{Binding #DefaultFileExtension.Text}"
|
||||
AllowMultiple="{Binding #AllowMultiple.IsChecked}"
|
||||
UsePickerType="{Binding #UsePickerType.Value}"
|
||||
SelectedPathsText="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPaths="{Binding Paths,Mode=OneWayToSource}">
|
||||
</u:PathPicker>
|
||||
</HeaderedContentControl>
|
||||
@@ -59,11 +72,11 @@
|
||||
DefaultFileExtension="{Binding #DefaultFileExtension.Text}"
|
||||
AllowMultiple="{Binding #AllowMultiple.IsChecked}"
|
||||
UsePickerType="{Binding #UsePickerType.Value}"
|
||||
SelectedPath="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPathsText="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPaths="{Binding Paths,Mode=OneWayToSource}">
|
||||
</u:PathPicker>
|
||||
</HeaderedContentControl>
|
||||
<HeaderedContentControl Header="PathPickerForList">
|
||||
<HeaderedContentControl Header="PathPickerForListView">
|
||||
<u:PathPicker Theme="{DynamicResource PathPickerForList}"
|
||||
Title="{Binding #Title.Text}"
|
||||
SuggestedFileName="{Binding #SuggestedFileName.Text}"
|
||||
@@ -72,7 +85,7 @@
|
||||
DefaultFileExtension="{Binding #DefaultFileExtension.Text}"
|
||||
AllowMultiple="{Binding #AllowMultiple.IsChecked}"
|
||||
UsePickerType="{Binding #UsePickerType.Value}"
|
||||
SelectedPath="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPathsText="{Binding Path,Mode=OneWayToSource}"
|
||||
SelectedPaths="{Binding Paths,Mode=OneWayToSource}">
|
||||
</u:PathPicker>
|
||||
</HeaderedContentControl>
|
||||
@@ -80,7 +93,7 @@
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">
|
||||
<StackPanel Spacing="1">
|
||||
<HeaderedContentControl Header="SelectedPath">
|
||||
<HeaderedContentControl Header="SelectedPathsText">
|
||||
<TextBox Name="SelectedPath" u:FormItem.Label="SelectedPath" IsReadOnly="True"
|
||||
Text="{Binding Path}">
|
||||
</TextBox>
|
||||
|
||||
@@ -9,31 +9,50 @@
|
||||
<Button Name="PART_Button"
|
||||
DockPanel.Dock="Right"
|
||||
Content="{TemplateBinding Title}"
|
||||
Command="{TemplateBinding Command}"
|
||||
Margin="1,0,0,0">
|
||||
</Button>
|
||||
<TextBox DockPanel.Dock="Left"
|
||||
Text="{TemplateBinding SelectedPath,Mode=TwoWay}">
|
||||
Text="{TemplateBinding SelectedPathsText,Mode=TwoWay}">
|
||||
</TextBox>
|
||||
</DockPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="PathPickerForMultipleText" TargetType="ursa:PathPicker">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<DockPanel HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}">
|
||||
<Button Name="PART_Button"
|
||||
DockPanel.Dock="Top"
|
||||
HorizontalAlignment="Stretch"
|
||||
Content="{TemplateBinding Title}"
|
||||
Margin="0,0,0,1">
|
||||
</Button>
|
||||
<TextBox DockPanel.Dock="Bottom"
|
||||
Text="{TemplateBinding SelectedPathsText,Mode=TwoWay}"
|
||||
AcceptsReturn="True">
|
||||
</TextBox>
|
||||
</DockPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
|
||||
<ControlTheme x:Key="PathPickerOnlyButton" TargetType="ursa:PathPicker">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Button Name="PART_Button"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Content="{TemplateBinding Title}"
|
||||
Command="{TemplateBinding Command}">
|
||||
Content="{TemplateBinding Title}">
|
||||
</Button>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="PathPickerForList" TargetType="ursa:PathPicker">
|
||||
<ControlTheme x:Key="PathPickerForListView" TargetType="ursa:PathPicker">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Expander HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
@@ -41,8 +60,7 @@
|
||||
<Expander.Header>
|
||||
<Button Name="PART_Button"
|
||||
HorizontalAlignment="Stretch"
|
||||
Content="{TemplateBinding Title}"
|
||||
Command="{TemplateBinding Command}">
|
||||
Content="{TemplateBinding Title}">
|
||||
<Button.Theme>
|
||||
<ControlTheme TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows.Input;
|
||||
using System.Text;
|
||||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
@@ -6,21 +7,17 @@ using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Logging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Irihi.Avalonia.Shared.Common;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(Name = "PART_Button", Type = typeof(Button))]
|
||||
public class PathPicker : TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<string?> SelectedPathProperty =
|
||||
AvaloniaProperty.Register<PathPicker, string?>(
|
||||
nameof(SelectedPath), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true,
|
||||
validate: x => string.IsNullOrWhiteSpace(x) || File.Exists(x) || Directory.Exists(x));
|
||||
|
||||
|
||||
public static readonly StyledProperty<string> SuggestedStartPathProperty =
|
||||
AvaloniaProperty.Register<PathPicker, string>(
|
||||
nameof(SuggestedStartPath), string.Empty);
|
||||
@@ -53,24 +50,20 @@ public class PathPicker : TemplatedControl
|
||||
public static readonly StyledProperty<bool> AllowMultipleProperty = AvaloniaProperty.Register<PathPicker, bool>(
|
||||
nameof(AllowMultiple));
|
||||
|
||||
public static readonly StyledProperty<string?> SelectedPathsTextProperty =
|
||||
AvaloniaProperty.Register<PathPicker, string?>(
|
||||
nameof(SelectedPathsText), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public string? SelectedPathsText
|
||||
{
|
||||
get => GetValue(SelectedPathsTextProperty);
|
||||
set => SetValue(SelectedPathsTextProperty, value);
|
||||
}
|
||||
|
||||
private Button? _button;
|
||||
|
||||
private IReadOnlyList<string> _selectedPaths = [];
|
||||
|
||||
public PathPicker()
|
||||
{
|
||||
KeyBindings.Add(new KeyBinding
|
||||
{
|
||||
Command = new IRIHI_CommandBase(() =>
|
||||
{
|
||||
if (!SelectedPathProperty.ValidateValue!.Invoke(SelectedPath)) return;
|
||||
SelectedPaths = string.IsNullOrWhiteSpace(SelectedPath) ? Array.Empty<string>() : [SelectedPath!];
|
||||
Command?.Execute(Task.FromResult(SelectedPaths));
|
||||
}),
|
||||
Gesture = new KeyGesture(Key.Enter)
|
||||
});
|
||||
}
|
||||
|
||||
public bool AllowMultiple
|
||||
{
|
||||
get => GetValue(AllowMultipleProperty);
|
||||
@@ -126,86 +119,161 @@ public class PathPicker : TemplatedControl
|
||||
set => SetValue(SuggestedStartPathProperty, value);
|
||||
}
|
||||
|
||||
public string? SelectedPath
|
||||
{
|
||||
get => GetValue(SelectedPathProperty);
|
||||
set => SetValue(SelectedPathProperty, value);
|
||||
}
|
||||
private bool _twoConvertLock;
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (_twoConvertLock) return;
|
||||
if (change.Property == SelectedPathsProperty)
|
||||
SelectedPath = SelectedPaths.Count > 0 ? SelectedPaths[0] : string.Empty;
|
||||
{
|
||||
_twoConvertLock = true;
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.Append(SelectedPaths.FirstOrDefault());
|
||||
foreach (var item in SelectedPaths.Skip(1))
|
||||
{
|
||||
stringBuilder.AppendLine(item);
|
||||
}
|
||||
|
||||
SelectedPathsText = stringBuilder.ToString();
|
||||
_twoConvertLock = false;
|
||||
}
|
||||
|
||||
if (change.Property == SelectedPathsTextProperty)
|
||||
{
|
||||
_twoConvertLock = true;
|
||||
string[] separatedStrings = ["\r", "\n", "\r\n"];
|
||||
// var list = SelectedPathsText?.Split(separatedStrings, StringSplitOptions.RemoveEmptyEntries)
|
||||
// .Select(RemoveNewLine).ToArray()
|
||||
// ?? [];
|
||||
// if (list.Length == SelectedPaths.Count)
|
||||
// {
|
||||
// if (SelectedPaths.Select(x => list.Any(y => x == y)).All(x => x is false))
|
||||
// }
|
||||
|
||||
SelectedPaths = SelectedPathsText?.Split(separatedStrings, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(RemoveNewLine).ToArray()
|
||||
?? [];
|
||||
_twoConvertLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
Button.ClickEvent.RemoveHandler(LaunchPicker, _button);
|
||||
_button = e.NameScope.Find<Button>("PART_Button");
|
||||
_button!.Click += LaunchPicker;
|
||||
Button.ClickEvent.AddHandler(LaunchPicker, _button);
|
||||
}
|
||||
|
||||
private void LaunchPicker(object? sender, RoutedEventArgs e)
|
||||
|
||||
private static string RemoveNewLine(string str)
|
||||
{
|
||||
if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storageProvider) return;
|
||||
return str.Replace("\r", "")
|
||||
.Replace("\n", "")
|
||||
.Replace("\r\n", "")
|
||||
.Replace(Environment.NewLine, "");
|
||||
}
|
||||
|
||||
Task<IReadOnlyList<string>> task = Task.Run(async () =>
|
||||
/**
|
||||
* FilePickerFileTypeName,Pattern,Pattern,Pattern...
|
||||
*/
|
||||
private static FilePickerFileType ParseFilePickerType(string str)
|
||||
{
|
||||
return str switch
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
nameof(FilePickerFileTypes.All) => FilePickerFileTypes.All,
|
||||
nameof(FilePickerFileTypes.Pdf) => FilePickerFileTypes.Pdf,
|
||||
nameof(FilePickerFileTypes.ImageAll) => FilePickerFileTypes.ImageAll,
|
||||
nameof(FilePickerFileTypes.ImageJpg) => FilePickerFileTypes.ImageJpg,
|
||||
nameof(FilePickerFileTypes.ImagePng) => FilePickerFileTypes.ImagePng,
|
||||
nameof(FilePickerFileTypes.ImageWebp) => FilePickerFileTypes.ImageWebp,
|
||||
nameof(FilePickerFileTypes.TextPlain) => FilePickerFileTypes.TextPlain,
|
||||
_ => parse()
|
||||
};
|
||||
|
||||
FilePickerFileType parse()
|
||||
{
|
||||
var list = str.Split(',');
|
||||
return new FilePickerFileType(list.First())
|
||||
{
|
||||
switch (UsePickerType)
|
||||
{
|
||||
case UsePickerTypes.OpenFile:
|
||||
FilePickerOpenOptions filePickerOpenOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
AllowMultiple = AllowMultiple,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
FileTypeFilter = FileFilter?.Split(',')
|
||||
.Select(x => new FilePickerFileType(x) { Patterns = [x] }).ToList()
|
||||
};
|
||||
var resFiles = await storageProvider.OpenFilePickerAsync(filePickerOpenOptions);
|
||||
SelectedPaths = resFiles.Select(x => x.TryGetLocalPath()).ToArray()!;
|
||||
break;
|
||||
case UsePickerTypes.SaveFile:
|
||||
FilePickerSaveOptions filePickerSaveOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
SuggestedFileName = SuggestedFileName,
|
||||
FileTypeChoices = FileFilter?.Split(',')
|
||||
.Select(x => new FilePickerFileType(x) { Patterns = [x] }).ToList(),
|
||||
DefaultExtension = DefaultFileExtension
|
||||
};
|
||||
Patterns = list.Skip(1).Select(x => x.Trim()).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var path = (await storageProvider.SaveFilePickerAsync(filePickerSaveOptions))
|
||||
?.TryGetLocalPath();
|
||||
SelectedPaths = string.IsNullOrEmpty(path)
|
||||
? Array.Empty<string>()
|
||||
: [path!];
|
||||
break;
|
||||
case UsePickerTypes.OpenFolder:
|
||||
FolderPickerOpenOptions folderPickerOpenOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
AllowMultiple = AllowMultiple,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
SuggestedFileName = SuggestedFileName
|
||||
};
|
||||
var resFolder = await storageProvider.OpenFolderPickerAsync(folderPickerOpenOptions);
|
||||
SelectedPaths = resFolder.Select(x => x.TryGetLocalPath()).ToArray()!;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
});
|
||||
/**
|
||||
* [ParseFilePickerTypeStr][ParseFilePickerTypeStr]...
|
||||
*/
|
||||
private static IReadOnlyList<FilePickerFileType>? ParseFileTypes(string str)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str)) return null;
|
||||
string[] separatedStrings = ["[", "][", "]"];
|
||||
var list = RemoveNewLine(str)
|
||||
.Replace(" ", string.Empty)
|
||||
.Split(separatedStrings, StringSplitOptions.RemoveEmptyEntries);
|
||||
return list.Select(ParseFilePickerType).ToArray();
|
||||
}
|
||||
|
||||
return await Dispatcher.UIThread.InvokeAsync(() => SelectedPaths);
|
||||
});
|
||||
_button!.CommandParameter = task;
|
||||
private async void LaunchPicker(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storageProvider) return;
|
||||
|
||||
switch (UsePickerType)
|
||||
{
|
||||
case UsePickerTypes.OpenFile:
|
||||
FilePickerOpenOptions filePickerOpenOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
AllowMultiple = AllowMultiple,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
FileTypeFilter = ParseFileTypes(FileFilter)
|
||||
};
|
||||
var resFiles = await storageProvider.OpenFilePickerAsync(filePickerOpenOptions);
|
||||
SelectedPaths = resFiles.Select(x => x.TryGetLocalPath()).ToArray()!;
|
||||
break;
|
||||
case UsePickerTypes.SaveFile:
|
||||
FilePickerSaveOptions filePickerSaveOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
SuggestedFileName = SuggestedFileName,
|
||||
FileTypeChoices = ParseFileTypes(FileFilter),
|
||||
DefaultExtension = DefaultFileExtension
|
||||
};
|
||||
|
||||
var path = (await storageProvider.SaveFilePickerAsync(filePickerSaveOptions))
|
||||
?.TryGetLocalPath();
|
||||
SelectedPaths = string.IsNullOrEmpty(path)
|
||||
? Array.Empty<string>()
|
||||
: [path!];
|
||||
break;
|
||||
case UsePickerTypes.OpenFolder:
|
||||
FolderPickerOpenOptions folderPickerOpenOptions = new()
|
||||
{
|
||||
Title = Title,
|
||||
AllowMultiple = AllowMultiple,
|
||||
SuggestedStartLocation =
|
||||
await storageProvider.TryGetFolderFromPathAsync(SuggestedStartPath),
|
||||
SuggestedFileName = SuggestedFileName
|
||||
};
|
||||
var resFolder = await storageProvider.OpenFolderPickerAsync(folderPickerOpenOptions);
|
||||
SelectedPaths = resFolder.Select(x => x.TryGetLocalPath()).ToArray()!;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (SelectedPaths.Count != 0)
|
||||
Command?.Execute(SelectedPaths);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.TryGet(LogEventLevel.Error, LogArea.Control)?.Log(this, $"{exception}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user