Custom input bar

This commit is contained in:
lanyi 2020-03-22 19:56:16 +01:00
parent d404cf2ca3
commit a51aa6958b
10 changed files with 586 additions and 108 deletions

184
FileSystemSuggestions.cs Normal file
View File

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
namespace HashCalculator.GUI
{
internal class FileSystemSuggestions
{
private static readonly Dictionary<string, InputEntryType> Mapping = new Dictionary<string, InputEntryType>(StringComparer.OrdinalIgnoreCase)
{
{ ".big", InputEntryType.BigFile },
{ ".xml", InputEntryType.XmlFile },
{ ".w3x", InputEntryType.XmlFile },
{ ".manifest", InputEntryType.ManifestFile },
};
private Search _search = new Search();
[SuppressMessage("Microsoft.Performance", "CA1031")]
public IEnumerable<InputEntry> ProvideFileSystemSuggestions(string? path)
{
var empty = Enumerable.Empty<InputEntry>();
if (string.IsNullOrWhiteSpace(path) || path == null)
{
return empty;
}
path = Environment.ExpandEnvironmentVariables(path);
_search.Update(path);
if (!_search.IsValidPath)
{
return empty;
}
var currentFiles = empty;
string? fileName;
string? currentFullPath;
try
{
currentFullPath = Path.GetFullPath(path);
fileName = Path.GetFileName(path);
if (File.Exists(currentFullPath))
{
var type = CheckExtension(currentFullPath);
if (type.HasValue)
{
currentFiles.Append(new InputEntry(type.Value, path, currentFullPath));
}
currentFiles.Append(new InputEntry(InputEntryType.BinaryFile, path, currentFullPath));
}
}
catch
{
return empty;
}
var directories = from directory in _search.AllDirectories
where directory.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
select new InputEntry(InputEntryType.Path, _search.GetInputStyleName(directory), directory.FullName);
var otherFiles = from file in _search.AllFiles
where file.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
where file.FullName != currentFullPath
select file;
var supportedFiles = empty;
try
{
supportedFiles = from file in otherFiles
let type = CheckExtension(file.Extension)
where type.HasValue
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
}
catch { }
var binaryFiles = empty;
try
{
binaryFiles = from file in otherFiles
select new InputEntry(InputEntryType.BinaryFile, _search.GetInputStyleName(file), file.FullName);
}
catch { }
return currentFiles.Concat(supportedFiles).Concat(directories).Concat(binaryFiles);
}
private static InputEntryType? CheckExtension(string path)
{
if (Mapping.TryGetValue(path, out var type))
{
return type;
}
return null;
}
}
internal sealed class Search
{
private string? _inputBaseDirectory;
public bool IsValidPath => _inputBaseDirectory != null;
public IEnumerable<DirectoryInfo> AllDirectories { get; private set; }
public IEnumerable<FileInfo> AllFiles { get; private set; }
public Search()
{
AllDirectories = Array.Empty<DirectoryInfo>();
AllFiles = Array.Empty<FileInfo>();
}
public string GetInputStyleName(FileSystemInfo entry)
{
if (_inputBaseDirectory == null)
{
throw new InvalidOperationException();
}
return Path.Combine(_inputBaseDirectory, entry.Name);
}
[SuppressMessage("Microsoft.Performance", "CA1031")]
public void Update(string path)
{
string? newBaseDirectory = null;
try
{
newBaseDirectory = Path.GetDirectoryName(path);
var rootDirectory = Path.GetPathRoot(path);
if(string.IsNullOrEmpty(newBaseDirectory) && !string.IsNullOrEmpty(rootDirectory))
{
var last = rootDirectory.LastOrDefault();
if (last != Path.DirectorySeparatorChar && last != Path.AltDirectorySeparatorChar)
{
rootDirectory += Path.DirectorySeparatorChar;
}
newBaseDirectory = rootDirectory;
}
}
catch
{
_inputBaseDirectory = null;
}
if (newBaseDirectory == null)
{
return;
}
if (newBaseDirectory == _inputBaseDirectory)
{
return;
}
_inputBaseDirectory = newBaseDirectory;
Update();
}
[SuppressMessage("Microsoft.Performance", "CA1031")]
private void Update()
{
AllDirectories = Enumerable.Empty<DirectoryInfo>();
AllFiles = Enumerable.Empty<FileInfo>();
try
{
var actualPath = _inputBaseDirectory!.Length == 0
? "."
: _inputBaseDirectory;
var directory = new DirectoryInfo(actualPath);
if (directory.Exists)
{
AllDirectories = directory.EnumerateDirectories();
AllFiles = directory.EnumerateFiles();
}
}
catch { }
}
}
}

View File

@ -2,14 +2,25 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net461</TargetFramework> <TargetFramework>net472</TargetFramework>
<RootNamespace>HashCalculator.GUI</RootNamespace> <RootNamespace>HashCalculator.GUI</RootNamespace>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<Platforms>AnyCPU;x86</Platforms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNetKit.Wpf.AutoCompleteComboBox" Version="1.2.0" /> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Reference Include="TechnologyAssembler.Core">
<HintPath>TechnologyAssembler.Core.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>

115
InputBar.xaml Normal file
View File

@ -0,0 +1,115 @@
<UserControl
x:Class="HashCalculator.GUI.InputBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HashCalculator.GUI"
mc:Ignorable="d"
x:ClassModifier="internal"
x:Name="_this"
d:DesignHeight="100" d:DesignWidth="800"
>
<UserControl.Resources>
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<Style
x:Key="BlankListBoxContainerStyle"
TargetType="{x:Type ListBoxItem}"
>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border
Name="Border"
Padding="3, 2"
SnapsToDevicePixels="true"
>
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger
Property="IsSelected"
Value="true"
>
<Setter
TargetName="Border"
Property="Background"
Value="DarkSlateGray"
/>
<Setter
Property="Foreground"
Value="White"
/>
</Trigger>
<Trigger
Property="IsMouseOver"
Value="true"
>
<Setter
TargetName="Border"
Property="Background"
Value="DarkSlateGray"
/>
<Setter
Property="Foreground"
Value="White"
/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<!--TextChanged="OnTextChanged"-->
<Grid>
<TextBox
x:Name="TextBox"
Text="{Binding Path=Text,
RelativeSource={RelativeSource AncestorType=local:InputBar},
UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnPreviewKeyDown"
Padding="2, 0"
VerticalContentAlignment="Center"
TextChanged="OnTextChanged"
LostFocus="OnLostFocus"
/>
<TextBlock
Text="{Binding Path=HintText,
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
IsHitTestVisible="False"
Visibility="{Binding ElementName=TextBox, Path=Text, Converter={StaticResource NullToVisibilityConverter}}"
Padding="5, 0"
VerticalAlignment="Center"
/>
</Grid>
<Popup
x:Name="DropDown"
StaysOpen="False"
MaxHeight="400"
Width="{Binding ElementName=TextBox, Path=ActualWidth}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
>
<ListBox
x:Name="ListBox"
ItemContainerStyle="{StaticResource ResourceKey=BlankListBoxContainerStyle}"
ItemsSource="{Binding Path=Collection,
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
SelectionMode="Single"
SelectionChanged="OnSelectionChanged"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Text}" FontWeight="Bold"></TextBlock>
<TextBlock Text="{Binding Type}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Popup>
</Grid>
</UserControl>

150
InputBar.xaml.cs Normal file
View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
namespace HashCalculator.GUI
{
/// <summary>
/// InputBar.xaml 的交互逻辑
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal partial class InputBar : UserControl
{
public static readonly DependencyProperty HintTextProperty =
DependencyProperty.Register(nameof(HintText), typeof(string), typeof(InputBar), new FrameworkPropertyMetadata
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public string? HintText
{
get => GetValue(HintTextProperty) as string;
set => SetValue(HintTextProperty, value);
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(InputBar), new FrameworkPropertyMetadata
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public string? Text
{
get => GetValue(TextProperty) as string;
set => SetValue(TextProperty, value);
}
public static readonly DependencyProperty CollectionProperty =
DependencyProperty.Register(nameof(Collection), typeof(IEnumerable<InputEntry>), typeof(InputBar), new FrameworkPropertyMetadata
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
CoerceValueCallback = (d, baseObject) =>
{
var value = baseObject as IEnumerable<InputEntry>;
if (value?.Count() > 100)
{
value = value
.Take(99)
.Append(new InputEntry(InputEntryType.Path, $"Other {value.Count() - 50} items...", string.Empty));
}
return value;
}
});
public IEnumerable<InputEntry>? Collection
{
get => GetValue(CollectionProperty) as IEnumerable<InputEntry>;
set => SetValue(CollectionProperty, value);
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(InputEntry), typeof(InputBar), new FrameworkPropertyMetadata
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public InputEntry? SelectedItem
{
get => GetValue(SelectedItemProperty) as InputEntry;
set => SetValue(SelectedItemProperty, value);
}
public event TextChangedEventHandler? TextChanged;
public InputBar()
{
InitializeComponent();
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Down:
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex + 1);
ListBox.ScrollIntoView(ListBox.SelectedItem);
DropDown.IsOpen = true;
break;
case Key.Up:
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex - 1);
ListBox.ScrollIntoView(ListBox.SelectedItem);
break;
case Key.Escape:
case Key.Enter:
DropDown.IsOpen = false;
break;
default:
DropDown.IsOpen = true;
break;
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = (InputEntry)ListBox.SelectedItem;
if (SelectedItem != null)
{
Text = SelectedItem.ToString();
TextBox.Select(Text.Length, 0);
}
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (Text != SelectedItem?.ToString())
{
TextChanged?.Invoke(this, e);
}
}
private void OnLostFocus(object sender, RoutedEventArgs e)
{
DropDown.IsOpen = false;
}
private int NormalizeIndex(int rawIndex)
{
if (Collection == null)
{
return -1;
}
return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1);
}
}
}

39
InputEntry.cs Normal file
View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using TechnologyAssembler.Core.IO;
namespace HashCalculator.GUI
{
internal enum InputEntryType
{
Text,
BigFile,
ManifestFile,
XmlFile,
BinaryFile,
Path,
}
internal sealed class InputEntry
{
public InputEntryType Type { get; }
public string Value { get; }
public string Text { get; }
public InputEntry(InputEntryType type, string text, string value)
{
Type = type;
Text = text;
Value = value;
}
public override string ToString()
{
return Text;
}
}
}

View File

@ -3,10 +3,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dotNetKitControls="clr-namespace:DotNetKit.Windows.Controls;assembly=DotNetKit.Wpf.AutoCompleteComboBox"
xmlns:l="clr-namespace:HashCalculator.GUI" xmlns:l="clr-namespace:HashCalculator.GUI"
mc:Ignorable="d" mc:Ignorable="d"
Title="Sage FastHash 哈希计算器" Height="600" Width="600"> Title="Sage FastHash 哈希计算器" Height="600" Width="600">
<Window.Resources>
<l:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</Window.Resources>
<Grid Margin="10, 10"> <Grid Margin="10, 10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition/> <RowDefinition/>
@ -18,36 +20,35 @@
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="92"/> <ColumnDefinition Width="92"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<dotNetKitControls:AutoCompleteComboBox <Grid>
SelectedValuePath="Id" <!--<l:InputBox
TextSearch.TextPath="Name" HintText="Select some big file"
Text="输入任意字符串来计算它的哈希值也可以输入big/manifest/xml文件的路径" Collection="{Binding Items}"
ItemsSource="{Binding Items}" TextChanged="OnTextChanged"
SelectedItem="{Binding SelectedItem}" >
SelectedValue="{Binding SelectedValue}" </l:InputBox>-->
Grid.Column="0" Margin="0" <l:InputBar
HintText="Select some big file"
Text="{Binding Text}"
Collection="{Binding Items}"
TextChanged="OnTextChanged"
/> />
</Grid>
<Button x:Name="button" Content="浏览文件" <Button x:Name="button" Content="浏览文件"
Grid.Column="1" Margin="0" Grid.Column="1" Margin="0"
/> />
</Grid> </Grid>
<dotNetKitControls:AutoCompleteComboBox <l:InputBar
SelectedValuePath="Id" HintText="输入big文件里包含的manifest文件的路径"
TextSearch.TextPath="Name" Collection="{Binding Items}"
Text="输入big文件里包含的manifest文件的路径"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}" SelectedItem="{Binding SelectedItem}"
SelectedValue="{Binding SelectedValue}"
Margin="0,10,0,0" Margin="0,10,0,0"
Height="25" VerticalAlignment="Top" Height="25" VerticalAlignment="Top"
/> />
<dotNetKitControls:AutoCompleteComboBox <l:InputBar
SelectedValuePath="Id" HintText="过滤Asset ID可选"
TextSearch.TextPath="Name" Collection="{Binding Items}"
Text="过滤Asset ID可选"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}" SelectedItem="{Binding SelectedItem}"
SelectedValue="{Binding SelectedValue}"
Margin="0,10,0,0" Margin="0,10,0,0"
Height="25" VerticalAlignment="Top" Height="25" VerticalAlignment="Top"
/> />

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
@ -23,19 +23,40 @@ namespace HashCalculator.GUI
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
private readonly ViewModel _model = new ViewModel();
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
DataContext = new ViewModel(); DataContext = _model;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is InputBar)
{
var inputValue = _model.Text;
if(inputValue != null)
{
_model.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
}
}
} }
} }
internal sealed class ViewModel : INotifyPropertyChanged internal class ViewModel : INotifyPropertyChanged
{ {
#region INotifyPropertyChanged #region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private void Notify(string name)
{
System.Diagnostics.Debug.WriteLine($"Updating {name}");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private void SetField<X>(ref X field, X value, [CallerMemberName] string? propertyName = null) private void SetField<X>(ref X field, X value, [CallerMemberName] string? propertyName = null)
{ {
if (propertyName == null) if (propertyName == null)
@ -50,104 +71,37 @@ namespace HashCalculator.GUI
field = value; field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); Notify(propertyName);
} }
#endregion #endregion
public IReadOnlyList<Person> Items private IEnumerable<InputEntry>? _items;
public IEnumerable<InputEntry>? Items
{ {
get { return PersonModule.All; } get => _items;
set => SetField(ref _items, value);
} }
Person selectedItem; private InputEntry? selectedItem;
public Person SelectedItem public InputEntry? SelectedItem
{ {
get { return selectedItem; } get { return selectedItem; }
set { SetField(ref selectedItem, value); } set { SetField(ref selectedItem, value); }
} }
long? selectedValue; private string? selectedValue;
public long? SelectedValue public string? SelectedValue
{ {
get { return selectedValue; } get { return selectedValue; }
set { SetField(ref selectedValue, value); } set { SetField(ref selectedValue, value); }
} }
private string? _text;
public string? Text
{
get { return _text; }
set { SetField(ref _text, value); }
}
} }
internal enum InputEntryType
{
Text,
SupportedFilePath,
BinaryFilePath,
Path,
}
internal sealed class InputEntry
{
public InputEntryType Type { get; }
public string Value { get; }
public InputEntry(InputEntryType type, string value)
{
Type = type;
Value = value;
}
public static List<InputEntry> CreateSuggestion(string value)
{
var list = new List<InputEntry>();
try
{
var path = new FileInfo(value);
path.Directory.GetFiles()
}
catch { }
}
public static bool IsSupportedFile(string value)
{
}
public static IEnumerable<InputEntry> ProvideFileSystemSuggestions(string path)
{
var empty = Enumerable.Empty<InputEntry>();
var list = empty;
if (string.IsNullOrWhiteSpace(path))
{
return list;
}
var directories = empty;
try
{
var directoryInfo = new DirectoryInfo(path);
if (directoryInfo.Exists)
{
directories = directoryInfo.GetDirectories()
.Select(info => new InputEntry(InputEntryType.Path, info.FullName));
}
}
catch { }
var files = empty;
try
{
var fileInfo = new FileInfo(path);
if (fileInfo.Exists)
{
files = files.Concat()
}
if (fileInfo.Directory.Exists)
{
}
//fileInfo.Directory.get
}
catch { }
}
}
} }

View File

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace HashCalculator.GUI
{
[ValueConversion(typeof(string), typeof(Visibility))]
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var text = value as string;
return string.IsNullOrEmpty(text) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,12 +1,14 @@
using System.Diagnostics; using System.Diagnostics;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Diagnostics.CodeAnalysis;
namespace HashCalculator.GUI namespace HashCalculator.GUI
{ {
/// <summary> /// <summary>
/// Opens <see cref="Hyperlink.NavigateUri"/> in a default system browser /// Opens <see cref="Hyperlink.NavigateUri"/> in a default system browser
/// </summary> /// </summary>
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal sealed class ShellLink : Hyperlink internal sealed class ShellLink : Hyperlink
{ {
public ShellLink() public ShellLink()

Binary file not shown.