Merge pull request #242 from irihitech/tree
Add TreeDataGrid default themes
This commit is contained in:
commit
437e9ea42b
@ -28,6 +28,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Semi.Avalonia.Demo.Android"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semi.Avalonia.Demo.Drm", "demo\Semi.Avalonia.Demo.Drm\Semi.Avalonia.Demo.Drm.csproj", "{86D93406-412A-4429-93B2-92AAD0407784}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semi.Avalonia.TreeDataGrid", "src\Semi.Avalonia.TreeDataGrid\Semi.Avalonia.TreeDataGrid.csproj", "{398D2998-0835-41F5-99A3-608CAB8051E2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semi.Avalonia.TreeDataGrid.Demo", "demo\Semi.Avalonia.TreeDataGrid.Demo\Semi.Avalonia.TreeDataGrid.Demo.csproj", "{6178B545-4BB6-458C-A27C-EE11F3885D38}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -67,6 +71,14 @@ Global
|
||||
{86D93406-412A-4429-93B2-92AAD0407784}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{86D93406-412A-4429-93B2-92AAD0407784}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{86D93406-412A-4429-93B2-92AAD0407784}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{398D2998-0835-41F5-99A3-608CAB8051E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{398D2998-0835-41F5-99A3-608CAB8051E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{398D2998-0835-41F5-99A3-608CAB8051E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{398D2998-0835-41F5-99A3-608CAB8051E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6178B545-4BB6-458C-A27C-EE11F3885D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6178B545-4BB6-458C-A27C-EE11F3885D38}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6178B545-4BB6-458C-A27C-EE11F3885D38}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6178B545-4BB6-458C-A27C-EE11F3885D38}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -77,6 +89,7 @@ Global
|
||||
{D789AEDB-EBDF-4450-8E8E-B4A03FB257B0} = {43091528-9509-43CB-A003-9C5C11E96DD6}
|
||||
{0C81FC1C-5D2D-478A-9876-923A0C85EC2F} = {43091528-9509-43CB-A003-9C5C11E96DD6}
|
||||
{86D93406-412A-4429-93B2-92AAD0407784} = {43091528-9509-43CB-A003-9C5C11E96DD6}
|
||||
{6178B545-4BB6-458C-A27C-EE11F3885D38} = {43091528-9509-43CB-A003-9C5C11E96DD6}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7CA41ED3-2CED-40CC-AA21-28C3B42B1E86}
|
||||
|
12
demo/Semi.Avalonia.TreeDataGrid.Demo/App.axaml
Normal file
12
demo/Semi.Avalonia.TreeDataGrid.Demo/App.axaml
Normal file
@ -0,0 +1,12 @@
|
||||
<Application
|
||||
x:Class="Semi.Avalonia.TreeDataGrid.Demo.App"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="avares://Semi.Avalonia/Themes/Index.axaml" />
|
||||
<StyleInclude Source="avares://Semi.Avalonia.TreeDataGrid/Index.axaml" />
|
||||
</Application.Styles>
|
||||
</Application>
|
23
demo/Semi.Avalonia.TreeDataGrid.Demo/App.axaml.cs
Normal file
23
demo/Semi.Avalonia.TreeDataGrid.Demo/App.axaml.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo.Converters;
|
||||
|
||||
public class FileIconConverter: IMultiValueConverter
|
||||
{
|
||||
[Content]
|
||||
public Dictionary<string, PathGeometry> Items { get; set; } = new Dictionary<string, PathGeometry>();
|
||||
|
||||
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (values[0] is bool isDirectory && values[1] is bool isOpen)
|
||||
{
|
||||
if (!isDirectory)
|
||||
{
|
||||
return Items["file"];
|
||||
}
|
||||
return isOpen ? Items["folderOpen"] : Items["folderClosed"];
|
||||
}
|
||||
return AvaloniaProperty.UnsetValue;
|
||||
}
|
||||
}
|
138
demo/Semi.Avalonia.TreeDataGrid.Demo/MainWindow.axaml
Normal file
138
demo/Semi.Avalonia.TreeDataGrid.Demo/MainWindow.axaml
Normal file
@ -0,0 +1,138 @@
|
||||
<Window
|
||||
x:Class="Semi.Avalonia.TreeDataGrid.Demo.MainWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:Semi.Avalonia.TreeDataGrid.Demo.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Semi.Avalonia.TreeDataGrid.Demo.ViewModels;assembly=Semi.Avalonia.TreeDataGrid.Demo"
|
||||
Title="Semi.Avalonia.TreeDataGrid.Demo"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:DataType="vm:MainViewModel"
|
||||
mc:Ignorable="d">
|
||||
<Window.Resources>
|
||||
<converters:FileIconConverter x:Key="FileIconConverter">
|
||||
<PathGeometry x:Key="file">M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z</PathGeometry>
|
||||
<PathGeometry x:Key="folderOpen">M6.1,10L4,18V8H21A2,2 0 0,0 19,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H19C19.9,20 20.7,19.4 20.9,18.5L23.2,10H6.1M19,18H6L7.6,12H20.6L19,18Z</PathGeometry>
|
||||
<PathGeometry x:Key="folderClosed">M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z</PathGeometry>
|
||||
</converters:FileIconConverter>
|
||||
</Window.Resources>
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Click="Button_OnClick"
|
||||
Content="Theme" />
|
||||
<TabControl Grid.Row="1">
|
||||
<TabItem Header="Songs">
|
||||
<TreeDataGrid
|
||||
AutoDragDropRows="True"
|
||||
DataContext="{Binding SongsContext}"
|
||||
Source="{Binding Songs}">
|
||||
<TreeDataGrid.Resources>
|
||||
<DataTemplate x:Key="AlbumCell" DataType="vm:SongViewModel">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Text="{Binding Album}" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="AlbumEditCell" DataType="vm:SongViewModel">
|
||||
<ComboBox
|
||||
VerticalAlignment="Center"
|
||||
Classes="Small"
|
||||
ItemsSource="{x:Static vm:Song.Albums}"
|
||||
SelectedItem="{Binding Album}" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="CommentsCell" DataType="vm:SongViewModel">
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding CountOfComment}" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="CommentsEditCell" DataType="vm:SongViewModel">
|
||||
<NumericUpDown
|
||||
Width="100"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Classes="Small"
|
||||
Value="{Binding CountOfComment}" />
|
||||
</DataTemplate>
|
||||
</TreeDataGrid.Resources>
|
||||
<TreeDataGrid.Styles>
|
||||
<Style Selector="TreeDataGrid TreeDataGridRow:nth-last-child(2n)">
|
||||
<Setter Property="Background" Value="#20808080" />
|
||||
</Style>
|
||||
</TreeDataGrid.Styles>
|
||||
</TreeDataGrid>
|
||||
</TabItem>
|
||||
<TabItem Header="Files">
|
||||
<Grid DataContext="{Binding FilesContext}" RowDefinitions="Auto, *">
|
||||
<DockPanel Margin="0,4" DockPanel.Dock="Top">
|
||||
<ComboBox
|
||||
DockPanel.Dock="Left"
|
||||
ItemsSource="{Binding Drives}"
|
||||
SelectedItem="{Binding SelectedDrive}" />
|
||||
<TextBox
|
||||
Margin="4,0,0,0"
|
||||
VerticalContentAlignment="Center"
|
||||
KeyDown="SelectedPath_KeyDown"
|
||||
Text="{Binding SelectedPath, Mode=OneWay}" />
|
||||
</DockPanel>
|
||||
<TreeDataGrid
|
||||
Name="fileViewer"
|
||||
Grid.Row="1"
|
||||
Source="{Binding Source}">
|
||||
<TreeDataGrid.Resources>
|
||||
|
||||
<!-- Template for Name column cells -->
|
||||
<DataTemplate x:Key="FileNameCell" DataType="vm:FileNodeViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center">
|
||||
<PathIcon.Data>
|
||||
<MultiBinding Converter="{StaticResource FileIconConverter}">
|
||||
<Binding Path="IsDirectory" />
|
||||
<Binding Path="IsExpanded" />
|
||||
</MultiBinding>
|
||||
</PathIcon.Data>
|
||||
</PathIcon>
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Edit template for Name column cells -->
|
||||
<DataTemplate x:Key="FileNameEditCell" DataType="vm:FileNodeViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Margin="0,0,4,0" VerticalAlignment="Center">
|
||||
<Image.Source>
|
||||
<MultiBinding Converter="{StaticResource FileIconConverter}">
|
||||
<Binding Path="IsDirectory" />
|
||||
<Binding Path="IsExpanded" />
|
||||
</MultiBinding>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<TextBox
|
||||
VerticalAlignment="Center"
|
||||
Classes="Small"
|
||||
Text="{Binding Name}">
|
||||
<TextBox.Styles>
|
||||
<Style Selector="DataValidationErrors">
|
||||
<Setter Property="Theme" Value="{DynamicResource TooltipDataValidationErrors}" />
|
||||
</Style>
|
||||
</TextBox.Styles>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</TreeDataGrid.Resources>
|
||||
<TreeDataGrid.Styles>
|
||||
<Style Selector="TreeDataGrid TreeDataGridRow:nth-child(2n)">
|
||||
<Setter Property="Background" Value="#20808080" />
|
||||
</Style>
|
||||
</TreeDataGrid.Styles>
|
||||
</TreeDataGrid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Window>
|
36
demo/Semi.Avalonia.TreeDataGrid.Demo/MainWindow.axaml.cs
Normal file
36
demo/Semi.Avalonia.TreeDataGrid.Demo/MainWindow.axaml.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using Semi.Avalonia.TreeDataGrid.Demo.ViewModels;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContext = new MainViewModel();
|
||||
}
|
||||
|
||||
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app is not null)
|
||||
{
|
||||
var theme = app.ActualThemeVariant;
|
||||
app.RequestedThemeVariant = theme == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectedPath_KeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
var vm = (MainViewModel)DataContext!;
|
||||
vm.FilesContext.SelectedPath = ((TextBox)sender!).Text;
|
||||
}
|
||||
}
|
||||
}
|
21
demo/Semi.Avalonia.TreeDataGrid.Demo/Program.cs
Normal file
21
demo/Semi.Avalonia.TreeDataGrid.Demo/Program.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo;
|
||||
|
||||
class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Semi.Avalonia.TreeDataGrid\Semi.Avalonia.TreeDataGrid.csproj" />
|
||||
<ProjectReference Include="..\..\src\Semi.Avalonia\Semi.Avalonia.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Models.TreeDataGrid;
|
||||
using Avalonia.Controls.Selection;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo.ViewModels;
|
||||
|
||||
public class FilesPageViewModel: ObservableObject
|
||||
{
|
||||
public IList<string> Drives { get; }
|
||||
private string _selectedDrive;
|
||||
private string? _selectedPath;
|
||||
private FileNodeViewModel? _root;
|
||||
public string SelectedDrive
|
||||
{
|
||||
get => _selectedDrive;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedDrive, value);
|
||||
_root = new FileNodeViewModel(_selectedDrive, isDirectory: true, isRoot: true);
|
||||
Source.Items = new[] { _root };
|
||||
}
|
||||
}
|
||||
|
||||
public string? SelectedPath
|
||||
{
|
||||
get => _selectedPath;
|
||||
set => SetSelectedPath(value);
|
||||
}
|
||||
|
||||
public HierarchicalTreeDataGridSource<FileNodeViewModel> Source { get; }
|
||||
|
||||
public FilesPageViewModel()
|
||||
{
|
||||
Drives= DriveInfo.GetDrives().Select(x => x.Name).ToList();
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
_selectedDrive = "C:\\";
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedDrive = Drives.FirstOrDefault() ?? "/";
|
||||
}
|
||||
|
||||
Source = new HierarchicalTreeDataGridSource<FileNodeViewModel>(Array.Empty<FileNodeViewModel>())
|
||||
{
|
||||
Columns =
|
||||
{
|
||||
new CheckBoxColumn<FileNodeViewModel>(
|
||||
null,
|
||||
x => x.IsChecked,
|
||||
(o, v) => o.IsChecked = v,
|
||||
options: new()
|
||||
{
|
||||
CanUserResizeColumn = false,
|
||||
}),
|
||||
new HierarchicalExpanderColumn<FileNodeViewModel>(
|
||||
new TemplateColumn<FileNodeViewModel>(
|
||||
"Name",
|
||||
"FileNameCell",
|
||||
"FileNameEditCell",
|
||||
new GridLength(1, GridUnitType.Star),
|
||||
new()
|
||||
{
|
||||
CompareAscending = FileNodeViewModel.SortAscending(x => x.Name),
|
||||
CompareDescending = FileNodeViewModel.SortDescending(x => x.Name),
|
||||
IsTextSearchEnabled = true,
|
||||
TextSearchValueSelector = x => x.Name
|
||||
}),
|
||||
x => x.Children,
|
||||
x => x.HasChildren,
|
||||
x => x.IsExpanded),
|
||||
new TextColumn<FileNodeViewModel, long?>(
|
||||
"Size",
|
||||
x => x.Size,
|
||||
options: new()
|
||||
{
|
||||
CompareAscending = FileNodeViewModel.SortAscending(x => x.Size),
|
||||
CompareDescending = FileNodeViewModel.SortDescending(x => x.Size),
|
||||
}),
|
||||
new TextColumn<FileNodeViewModel, DateTimeOffset?>(
|
||||
"Modified",
|
||||
x => x.Modified,
|
||||
options: new()
|
||||
{
|
||||
CompareAscending = FileNodeViewModel.SortAscending(x => x.Modified),
|
||||
CompareDescending = FileNodeViewModel.SortDescending(x => x.Modified),
|
||||
}),
|
||||
}
|
||||
};
|
||||
Source.RowSelection!.SingleSelect = false;
|
||||
Source.RowSelection.SelectionChanged += SelectionChanged;
|
||||
}
|
||||
|
||||
private void SelectionChanged(object? sender, TreeSelectionModelSelectionChangedEventArgs<FileNodeViewModel> e)
|
||||
{
|
||||
var selectedPath = Source.RowSelection?.SelectedItem?.Path;
|
||||
this.SetProperty(ref _selectedPath, selectedPath, nameof(SelectedPath));
|
||||
|
||||
foreach (var i in e.DeselectedItems)
|
||||
System.Diagnostics.Trace.WriteLine($"Deselected '{i?.Path}'");
|
||||
foreach (var i in e.SelectedItems)
|
||||
System.Diagnostics.Trace.WriteLine($"Selected '{i?.Path}'");
|
||||
}
|
||||
|
||||
private void SetSelectedPath(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
Source.RowSelection!.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var path = value;
|
||||
var components = new Stack<string>();
|
||||
DirectoryInfo? d = null;
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var f = new FileInfo(path);
|
||||
components.Push(f.Name);
|
||||
d = f.Directory;
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
d = new DirectoryInfo(path);
|
||||
}
|
||||
|
||||
while (d is not null)
|
||||
{
|
||||
components.Push(d.Name);
|
||||
d = d.Parent;
|
||||
}
|
||||
|
||||
var index = IndexPath.Unselected;
|
||||
|
||||
if (components.Count > 0)
|
||||
{
|
||||
var drive = components.Pop();
|
||||
var driveIndex = Drives.FindIndex(x => string.Equals(x, drive, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (driveIndex >= 0)
|
||||
SelectedDrive = Drives[driveIndex];
|
||||
|
||||
FileNodeViewModel? node = _root;
|
||||
index = new IndexPath(0);
|
||||
|
||||
while (node is not null && components.Count > 0)
|
||||
{
|
||||
node.IsExpanded = true;
|
||||
|
||||
var component = components.Pop();
|
||||
var i = node.Children.FindIndex(x => string.Equals(x.Name, component, StringComparison.OrdinalIgnoreCase));
|
||||
node = i >= 0 ? node.Children[i] : null;
|
||||
index = i >= 0 ? index.Append(i) : default;
|
||||
}
|
||||
}
|
||||
|
||||
Source.RowSelection!.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
public class FileNodeViewModel: ObservableObject, IEditableObject
|
||||
{
|
||||
private string _path;
|
||||
private string _name;
|
||||
private string? _undoName;
|
||||
private long? _size;
|
||||
private DateTimeOffset? _modified;
|
||||
private FileSystemWatcher? _watcher;
|
||||
private ObservableCollection<FileNodeViewModel>? _children;
|
||||
private bool _hasChildren = true;
|
||||
private bool _isExpanded;
|
||||
|
||||
public FileNodeViewModel( string path, bool isDirectory, bool isRoot = false)
|
||||
{
|
||||
_path = path;
|
||||
_name = isRoot ? path : System.IO.Path.GetFileName(Path);
|
||||
_isExpanded = isRoot;
|
||||
IsDirectory = isDirectory;
|
||||
HasChildren = isDirectory;
|
||||
|
||||
if (!isDirectory)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
Size = info.Length;
|
||||
Modified = info.LastWriteTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => _path;
|
||||
private set => SetProperty(ref _path, value);
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
private set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
public long? Size
|
||||
{
|
||||
get => _size;
|
||||
private set => SetProperty(ref _size, value);
|
||||
}
|
||||
|
||||
public DateTimeOffset? Modified
|
||||
{
|
||||
get => _modified;
|
||||
private set => SetProperty(ref _modified, value);
|
||||
}
|
||||
|
||||
public bool HasChildren
|
||||
{
|
||||
get => _hasChildren;
|
||||
private set => SetProperty(ref _hasChildren, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
public bool IsChecked { get; set; }
|
||||
public bool IsDirectory { get; }
|
||||
public IReadOnlyList<FileNodeViewModel> Children => _children ??= LoadChildren();
|
||||
|
||||
private ObservableCollection<FileNodeViewModel> LoadChildren()
|
||||
{
|
||||
if (!IsDirectory)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var options = new EnumerationOptions { IgnoreInaccessible = true };
|
||||
var result = new ObservableCollection<FileNodeViewModel>();
|
||||
|
||||
foreach (var d in Directory.EnumerateDirectories(Path, "*", options))
|
||||
{
|
||||
result.Add(new FileNodeViewModel(d, true));
|
||||
}
|
||||
|
||||
foreach (var f in Directory.EnumerateFiles(Path, "*", options))
|
||||
{
|
||||
result.Add(new FileNodeViewModel(f, false));
|
||||
}
|
||||
|
||||
_watcher = new FileSystemWatcher
|
||||
{
|
||||
Path = Path,
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite,
|
||||
};
|
||||
|
||||
_watcher.Changed += OnChanged;
|
||||
_watcher.Created += OnCreated;
|
||||
_watcher.Deleted += OnDeleted;
|
||||
_watcher.Renamed += OnRenamed;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
|
||||
if (result.Count == 0)
|
||||
HasChildren = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Comparison<FileNodeViewModel?> SortAscending<T>(Func<FileNodeViewModel, T> selector)
|
||||
{
|
||||
return (x, y) =>
|
||||
{
|
||||
if (x is null && y is null)
|
||||
return 0;
|
||||
else if (x is null)
|
||||
return -1;
|
||||
else if (y is null)
|
||||
return 1;
|
||||
if (x.IsDirectory == y.IsDirectory)
|
||||
return Comparer<T>.Default.Compare(selector(x), selector(y));
|
||||
else if (x.IsDirectory)
|
||||
return -1;
|
||||
else
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
public static Comparison<FileNodeViewModel?> SortDescending<T>(Func<FileNodeViewModel, T> selector)
|
||||
{
|
||||
return (x, y) =>
|
||||
{
|
||||
if (x is null && y is null)
|
||||
return 0;
|
||||
else if (x is null)
|
||||
return 1;
|
||||
else if (y is null)
|
||||
return -1;
|
||||
if (x.IsDirectory == y.IsDirectory)
|
||||
return Comparer<T>.Default.Compare(selector(y), selector(x));
|
||||
else if (x.IsDirectory)
|
||||
return -1;
|
||||
else
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
void IEditableObject.BeginEdit() => _undoName = _name;
|
||||
void IEditableObject.CancelEdit() => _name = _undoName!;
|
||||
void IEditableObject.EndEdit() => _undoName = null;
|
||||
|
||||
private void OnChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (e.ChangeType == WatcherChangeTypes.Changed && File.Exists(e.FullPath))
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
foreach (var child in _children!)
|
||||
{
|
||||
if (child.Path == e.FullPath)
|
||||
{
|
||||
if (!child.IsDirectory)
|
||||
{
|
||||
var info = new FileInfo(e.FullPath);
|
||||
child.Size = info.Length;
|
||||
child.Modified = info.LastWriteTimeUtc;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCreated(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var node = new FileNodeViewModel(
|
||||
e.FullPath,
|
||||
File.GetAttributes(e.FullPath).HasFlag(FileAttributes.Directory));
|
||||
_children!.Add(node);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDeleted(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
for (var i = 0; i < _children!.Count; ++i)
|
||||
{
|
||||
if (_children[i].Path == e.FullPath)
|
||||
{
|
||||
_children.RemoveAt(i);
|
||||
System.Diagnostics.Debug.WriteLine($"Removed {e.FullPath}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnRenamed(object sender, RenamedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
foreach (var child in _children!)
|
||||
{
|
||||
if (child.Path == e.OldFullPath)
|
||||
{
|
||||
child.Path = e.FullPath;
|
||||
child.Name = e.Name ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ListExtensions
|
||||
{
|
||||
public static int FindIndex<T>(this IEnumerable<T> source, Func<T, bool> predicate)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var item in source)
|
||||
{
|
||||
if (predicate(item))
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo.ViewModels;
|
||||
|
||||
public class MainViewModel: ObservableObject
|
||||
{
|
||||
public SongsPageViewModel SongsContext { get; } = new();
|
||||
public FilesPageViewModel FilesContext { get; } = new();
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Models.TreeDataGrid;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Semi.Avalonia.TreeDataGrid.Demo.ViewModels;
|
||||
|
||||
public class SongsPageViewModel: ObservableObject
|
||||
{
|
||||
private readonly ObservableCollection<SongViewModel> _songs;
|
||||
|
||||
public FlatTreeDataGridSource<SongViewModel> Songs { get; }
|
||||
|
||||
public SongsPageViewModel()
|
||||
{
|
||||
_songs = new ObservableCollection<SongViewModel>(Song.Songs.Select(a => new SongViewModel()
|
||||
{
|
||||
Title = a.Title, Artist = a.Artist, Album = a.Album, CountOfComment = a.CountOfComment,
|
||||
IsSelected = false
|
||||
}));
|
||||
|
||||
Songs = new FlatTreeDataGridSource<SongViewModel>(_songs)
|
||||
{
|
||||
Columns =
|
||||
{
|
||||
new CheckBoxColumn<SongViewModel>("IsSelected", a => a.IsSelected,
|
||||
(model, b) => { model.IsSelected = b; }, new GridLength(108, GridUnitType.Pixel)),
|
||||
new TextColumn<SongViewModel, string>("Title", a => a.Title, (o, a) => o.Title = a,
|
||||
new GridLength(6, GridUnitType.Star)),
|
||||
new TextColumn<SongViewModel, string>("Artist", a => a.Artist, (o, a) => o.Artist = a,
|
||||
new GridLength(6, GridUnitType.Star)),
|
||||
new TemplateColumn<SongViewModel>("Album", "AlbumCell", "AlbumEditCell",
|
||||
new GridLength(6, GridUnitType.Star)),
|
||||
new TemplateColumn<SongViewModel>("Comments", "CommentsCell", "CommentsEditCell",
|
||||
new GridLength(6, GridUnitType.Star)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class Song
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Artist { get; set; }
|
||||
public TimeSpan? Duration { get; set; }
|
||||
public string? Album { get; set; }
|
||||
public int? CountOfComment { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
public Song(string title, string artist, int m, int s, string album, int countOfComment, int netEaseId)
|
||||
{
|
||||
Title = title;
|
||||
Artist = artist;
|
||||
Duration = new TimeSpan(0, m, s);
|
||||
Album = album;
|
||||
CountOfComment = countOfComment;
|
||||
Url = $"https://music.163.com/song?id={netEaseId}";
|
||||
|
||||
}
|
||||
|
||||
public static List<string> Albums { get; set; } = new List<string>()
|
||||
{
|
||||
"A.S.I.A",
|
||||
"饕餮人间",
|
||||
"七步咙咚呛",
|
||||
"大惊小怪",
|
||||
"The ONE",
|
||||
"以梦为马 (壮志骄阳版)",
|
||||
"emo了",
|
||||
"一眼万年",
|
||||
"冲刺吧",
|
||||
"爱的赏味期限",
|
||||
"COSMIC ANTHEM / 手紙",
|
||||
"世界晚安",
|
||||
"明年也要好好长大",
|
||||
"320万年前",
|
||||
"W.O.R.L.D.",
|
||||
};
|
||||
|
||||
public static List<Song> Songs { get; set; } = new List<Song>()
|
||||
{
|
||||
new("好肚有肚(feat.李玲玉)", "熊猫堂ProducePandas", 2, 50, "A.S.I.A", 730, 1487039339),
|
||||
new("荒诞秀", "熊猫堂ProducePandas", 3, 15, "A.S.I.A", 639, 1487037601),
|
||||
new("长大", "熊猫堂ProducePandas", 4, 6, "A.S.I.A", 1114, 1487037690),
|
||||
new("招财猫(feat.纪粹希(G-Tracy))", "熊猫堂ProducePandas", 3, 37, "A.S.I.A", 361, 1487039632),
|
||||
new("千转", "熊猫堂ProducePandas", 4, 0, "A.S.I.A", 1115, 1477312398),
|
||||
new("辣辣辣", "熊猫堂ProducePandas", 3, 24, "A.S.I.A", 1873, 1465043716),
|
||||
new("碎碎念", "熊猫堂ProducePandas", 3, 25, "A.S.I.A", 676, 1474142064),
|
||||
new("盘他", "熊猫堂ProducePandas", 2, 16, "A.S.I.A", 365, 1481652786),
|
||||
new("Na Na Na", "熊猫堂ProducePandas", 3, 26, "A.S.I.A", 312, 1469022662),
|
||||
new("Indigo", "熊猫堂ProducePandas", 3, 15, "A.S.I.A", 137, 1487039517),
|
||||
new("饕餮人间", "熊猫堂ProducePandas", 3, 20, "饕餮人间", 1295, 1499584605),
|
||||
new("七步咙咚呛", "熊猫堂ProducePandas", 3, 10, "七步咙咚呛", 175, 1809095152),
|
||||
new("大惊小怪", "熊猫堂ProducePandas", 3, 32, "大惊小怪", 10420, 1847477425),
|
||||
new("工具人", "熊猫堂ProducePandas", 2, 46, "大惊小怪", 1135, 1847476499),
|
||||
new("以梦为马", "熊猫堂ProducePandas", 4, 19, "大惊小怪", 18361, 1836034373),
|
||||
new("以梦为马(Piano Version)", "熊猫堂ProducePandas", 3, 4, "大惊小怪", 570, 1847477423),
|
||||
new("The ONE", "熊猫堂ProducePandas", 2, 58, "The ONE", 1508, 1864329424),
|
||||
new("The ONE(日文版)", "熊猫堂ProducePandas", 2, 57, "The ONE", 385, 1864329429),
|
||||
new("以梦为马 (壮志骄阳版)", "熊猫堂ProducePandas", 4, 19, "以梦为马 (壮志骄阳版)", 161, 1865138896),
|
||||
new("New Horse", "熊猫堂ProducePandas", 2, 30, "emo了", 643, 1887021307),
|
||||
new("不例外", "熊猫堂ProducePandas", 3, 31, "emo了", 1818, 1887022665),
|
||||
new("满意", "熊猫堂ProducePandas", 4, 32, "emo了", 1081, 1882433472),
|
||||
new("就算与全世界为敌也要跟你在一起", "熊猫堂ProducePandas", 3, 32, "emo了", 2119, 1881759960),
|
||||
new("The ONE", "熊猫堂ProducePandas", 2, 58, "emo了", 67, 1887022648),
|
||||
new("口香糖", "熊猫堂ProducePandas", 3, 10, "emo了", 2181, 1885502254),
|
||||
new("Suuuuuuper Mario", "熊猫堂ProducePandas", 3, 32, "emo了", 1010, 1887021318),
|
||||
new("饕餮人间", "熊猫堂ProducePandas", 3, 22, "emo了", 109, 1887021320),
|
||||
new("以梦为马 (壮志骄阳版)", "熊猫堂ProducePandas", 4, 21, "emo了", 34, 1887022666),
|
||||
new("The ONE(日文版)", "熊猫堂ProducePandas", 2, 57, "emo了", 27, 1887022646),
|
||||
new("满意(DJheap九天版)", "熊猫堂ProducePandas", 4, 31, "emo了", 31, 1901605941),
|
||||
new("一眼万年", "熊猫堂ProducePandas", 3, 54, "一眼万年", 20, 1922599361),
|
||||
new("冲刺", "熊猫堂ProducePandas", 3, 49, "冲刺吧", 1006, 1932878194),
|
||||
new("滴答滴", "熊猫堂ProducePandas", 2, 30, "爱的赏味期限", 86, 1957515790),
|
||||
new("热带季风", "熊猫堂ProducePandas", 2, 45, "爱的赏味期限", 212, 1957514964),
|
||||
new("渣", "熊猫堂ProducePandas", 3, 28, "爱的赏味期限", 22, 1957514965),
|
||||
new("独特", "熊猫堂ProducePandas", 3, 33, "爱的赏味期限", 62, 1957514966),
|
||||
new("雨后", "熊猫堂ProducePandas", 4, 15, "爱的赏味期限", 23, 1957514967),
|
||||
new("然后然后", "熊猫堂ProducePandas", 3, 50, "爱的赏味期限", 108, 1957514968),
|
||||
new("丢", "熊猫堂ProducePandas", 3, 26, "爱的赏味期限", 30, 1957515792),
|
||||
new("热带疾风(FACEVOID桃心连哥 Remix)", "熊猫堂ProducePandas", 3, 23, "爱的赏味期限", 55, 1957515793),
|
||||
new("COSMIC ANTHEM -Japanese Ver.-", "熊猫堂ProducePandas", 3, 11, "COSMIC ANTHEM / 手紙", 0, 1977171493),
|
||||
new("手紙 (「長大-You Raise Me Up-」-Japanese Ver.-)", "熊猫堂ProducePandas", 4, 11, "COSMIC ANTHEM / 手紙", 0,
|
||||
1977171494),
|
||||
new("COSMIC ANTHEM -Chinese Ver.-", "熊猫堂ProducePandas", 3, 31, "COSMIC ANTHEM / 手紙", 0, 1977172202),
|
||||
new("世界晚安", "熊猫堂ProducePandas", 2, 59, "世界晚安", 652, 1985063377),
|
||||
new("世界晚安(泰文版)", "熊猫堂ProducePandas", 2, 59, "世界晚安", 134, 1987842504),
|
||||
new("世界晚安(钢琴版)", "熊猫堂ProducePandas", 3, 2, "世界晚安", 76, 1990475933),
|
||||
new("世界晚安(泰文钢琴版)", "熊猫堂ProducePandas", 3, 2, "世界晚安", 29, 1990475934),
|
||||
new("世界晚安(DJ沈念版)", "熊猫堂ProducePandas", 3, 9, "世界晚安", 34, 2014263184),
|
||||
new("世界晚安(钢琴配乐)", "熊猫堂ProducePandas", 2, 59, "世界晚安", 11, 2014263185),
|
||||
new("明年也要好好长大", "熊猫堂ProducePandas", 3, 12, "明年也要好好长大", 0, 2010515162),
|
||||
new("320万年前(DJ沈念版)", "熊猫堂ProducePandas", 3, 21, "320万年前", 8, 2055888636),
|
||||
new("320万年前", "熊猫堂ProducePandas", 3, 7, "W.O.R.L.D.", 329, 2049770469),
|
||||
new("隐德来希", "熊猫堂ProducePandas", 3, 3, "W.O.R.L.D.", 594, 2061317924),
|
||||
new("孔明", "熊猫堂ProducePandas", 3, 59, "W.O.R.L.D.", 91, 2063175274),
|
||||
new("锦鲤卟噜噜", "熊猫堂ProducePandas", 3, 5, "W.O.R.L.D.", 67, 2059208262),
|
||||
new("指鹿为马", "熊猫堂ProducePandas", 3, 12, "W.O.R.L.D.", 74, 2063175272),
|
||||
new("热带季风Remix", "熊猫堂ProducePandas", 3, 22, "W.O.R.L.D.", 23, 2063173319),
|
||||
new("加州梦境", "熊猫堂ProducePandas", 2, 56, "W.O.R.L.D.", 1662, 2063173324),
|
||||
new("渐进自由", "熊猫堂ProducePandas", 4, 19, "W.O.R.L.D.", 124, 2063173321),
|
||||
new("世界所有的烂漫", "熊猫堂ProducePandas", 3, 30, "W.O.R.L.D.", 335, 2053388775),
|
||||
};
|
||||
}
|
||||
|
||||
public class SongViewModel: ObservableObject
|
||||
{
|
||||
private string? _title;
|
||||
private string? _artist;
|
||||
private string? _album;
|
||||
private int? _countOfComment;
|
||||
private bool? _isSelected;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
public string? Artist
|
||||
{
|
||||
get => _artist;
|
||||
set => SetProperty(ref _artist, value);
|
||||
}
|
||||
public string? Album
|
||||
{
|
||||
get => _album;
|
||||
set => SetProperty(ref _album, value);
|
||||
}
|
||||
public int? CountOfComment
|
||||
{
|
||||
get => _countOfComment;
|
||||
set => SetProperty(ref _countOfComment, value);
|
||||
}
|
||||
public bool? IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
}
|
18
demo/Semi.Avalonia.TreeDataGrid.Demo/app.manifest
Normal file
18
demo/Semi.Avalonia.TreeDataGrid.Demo/app.manifest
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="Semi.Avalonia.TreeDataGrid.Demo.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
12
src/Semi.Avalonia.TreeDataGrid/Dark.axaml
Normal file
12
src/Semi.Avalonia.TreeDataGrid/Dark.axaml
Normal file
@ -0,0 +1,12 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Add Resources Here -->
|
||||
<SolidColorBrush x:Key="TreeDataGridGridLinesBrush" Opacity="0.08" Color="White" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverBackground" Opacity="0.16" Color="White" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedBackground" Opacity="0.20" Color="White" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverForeground" Opacity="0.8" Color="#F9F9F9" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedForeground" Opacity="1" Color="#F9F9F9" />
|
||||
<SolidColorBrush x:Key="TreeDataGridCellSelectedBackground" Opacity="0.2" Color="#FF54A9FF" />
|
||||
<SolidColorBrush x:Key="TreeDataGridColumnHeaderForeground" Opacity="0.6" Color="#F9F9F9" />
|
||||
</ResourceDictionary>
|
15
src/Semi.Avalonia.TreeDataGrid/Index.axaml
Normal file
15
src/Semi.Avalonia.TreeDataGrid/Index.axaml
Normal file
@ -0,0 +1,15 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Add Resources Here -->
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<MergeResourceInclude x:Key="Default" Source="avares://Semi.Avalonia.TreeDataGrid/Light.axaml" />
|
||||
<MergeResourceInclude x:Key="Dark" Source="avares://Semi.Avalonia.TreeDataGrid/Dark.axaml" />
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="avares://Semi.Avalonia.TreeDataGrid/TreeDataGrid.axaml" />
|
||||
<ResourceInclude Source="avares://Semi.Avalonia.TreeDataGrid/Shared.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
12
src/Semi.Avalonia.TreeDataGrid/Light.axaml
Normal file
12
src/Semi.Avalonia.TreeDataGrid/Light.axaml
Normal file
@ -0,0 +1,12 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Add Resources Here -->
|
||||
<SolidColorBrush x:Key="TreeDataGridGridLinesBrush" Opacity="0.08" Color="#1C1F23" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverBackground" Opacity="0.09" Color="#2E3238" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedBackground" Opacity="0.13" Color="#2E3238" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPointerOverForeground" Opacity="0.8" Color="#1C1F23" />
|
||||
<SolidColorBrush x:Key="TreeDataGridHeaderPressedForeground" Opacity="1" Color="#1C1F23" />
|
||||
<SolidColorBrush x:Key="TreeDataGridCellSelectedBackground" Color="#EAF5FF" />
|
||||
<SolidColorBrush x:Key="TreeDataGridColumnHeaderForeground" Opacity="0.62" Color="#1C1F23" />
|
||||
</ResourceDictionary>
|
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Controls.TreeDataGrid" Version="11.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
8
src/Semi.Avalonia.TreeDataGrid/Shared.axaml
Normal file
8
src/Semi.Avalonia.TreeDataGrid/Shared.axaml
Normal file
@ -0,0 +1,8 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Add Resources Here -->
|
||||
<StreamGeometry x:Key="TreeDataGridSortIconDescendingPath">
|
||||
M17.549 15.659L12.753 21.139C12.6591 21.2464 12.5434 21.3325 12.4135 21.3915C12.2836 21.4505 12.1427 21.481 12 21.481C11.8574 21.481 11.7164 21.4505 11.5865 21.3915C11.4566 21.3325 11.3409 21.2464 11.247 21.139L6.45101 15.659C5.88501 15.011 6.34501 14 7.20401 14H16.796C17.656 14 18.115 15.012 17.549 15.659Z
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="TreeDataGridSortIconAscendingPath">M6.45096 8.34102L11.247 2.86102C11.3408 2.75361 11.4566 2.66753 11.5865 2.60854C11.7163 2.54956 11.8573 2.51904 12 2.51904C12.1426 2.51904 12.2836 2.54956 12.4135 2.60854C12.5433 2.66753 12.6591 2.75361 12.753 2.86102L17.549 8.34102C18.115 8.98802 17.655 10 16.796 10H7.20396C6.34396 10 5.88496 8.98802 6.45096 8.34102Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="TreeDataGridItemCollapsedChevronPathData">M9.65618 3.44015L18.6322 11.2454C19.0906 11.644 19.0906 12.356 18.6322 12.7546L9.65618 20.5598C9.00895 21.1226 8 20.6629 8 19.8052V4.19475C8 3.33705 9.00895 2.87734 9.65618 3.44015Z</StreamGeometry>
|
||||
</ResourceDictionary>
|
342
src/Semi.Avalonia.TreeDataGrid/TreeDataGrid.axaml
Normal file
342
src/Semi.Avalonia.TreeDataGrid/TreeDataGrid.axaml
Normal file
@ -0,0 +1,342 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls.TreeDataGrid">
|
||||
<!-- Add Resources Here -->
|
||||
<Design.PreviewWith>
|
||||
<StackPanel Margin="20">
|
||||
<TreeDataGridColumnHeader Header="123" />
|
||||
</StackPanel>
|
||||
</Design.PreviewWith>
|
||||
<ControlTheme x:Key="{x:Type TreeDataGrid}" TargetType="TreeDataGrid">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="TreeDataGrid">
|
||||
<Border
|
||||
x:Name="RootBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DockPanel>
|
||||
<ScrollViewer
|
||||
Name="PART_HeaderScrollViewer"
|
||||
DockPanel.Dock="Top"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
IsVisible="{TemplateBinding ShowColumnHeaders}"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<Border x:Name="ColumnHeadersPresenterBorder">
|
||||
<TreeDataGridColumnHeadersPresenter
|
||||
Name="PART_ColumnHeadersPresenter"
|
||||
ElementFactory="{TemplateBinding ElementFactory}"
|
||||
Items="{TemplateBinding Columns}" />
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="PART_ScrollViewer" HorizontalScrollBarVisibility="Auto">
|
||||
<TreeDataGridRowsPresenter
|
||||
Name="PART_RowsPresenter"
|
||||
Columns="{TemplateBinding Columns}"
|
||||
ElementFactory="{TemplateBinding ElementFactory}"
|
||||
Items="{TemplateBinding Rows}" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^/template/ Border#ColumnHeadersPresenterBorder">
|
||||
<Setter Property="BorderThickness" Value="0 0 0 1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource TreeDataGridGridLinesBrush}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridColumnHeader}" TargetType="TreeDataGridColumnHeader">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Padding" Value="8 2 0 2" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TreeDataGridColumnHeaderForeground}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="TreeDataGridColumnHeader">
|
||||
<Panel>
|
||||
<Border
|
||||
Name="DataGridBorder"
|
||||
Margin="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
<DockPanel VerticalAlignment="Stretch">
|
||||
<Panel DockPanel.Dock="Right" TabIndex="2">
|
||||
<Rectangle
|
||||
Width="1"
|
||||
HorizontalAlignment="Right"
|
||||
Fill="{DynamicResource TreeDataGridGridLinesBrush}" />
|
||||
<Thumb
|
||||
Name="PART_Resizer"
|
||||
Width="5"
|
||||
Background="Transparent"
|
||||
Cursor="SizeWestEast"
|
||||
DockPanel.Dock="Right"
|
||||
IsVisible="{TemplateBinding CanUserResize}">
|
||||
<Thumb.Template>
|
||||
<ControlTemplate>
|
||||
<Border VerticalAlignment="Stretch" Background="{TemplateBinding Background}" />
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Panel>
|
||||
<PathIcon
|
||||
Name="SortIcon"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
TabIndex="1" />
|
||||
<ContentPresenter
|
||||
Name="PART_ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
TabIndex="0">
|
||||
<ContentPresenter.DataTemplates>
|
||||
<DataTemplate DataType="x:String">
|
||||
<TextBlock Text="{Binding}" TextTrimming="CharacterEllipsis" />
|
||||
</DataTemplate>
|
||||
</ContentPresenter.DataTemplates>
|
||||
</ContentPresenter>
|
||||
</DockPanel>
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Border#DataGridBorder">
|
||||
<Setter Property="Background" Value="{DynamicResource TreeDataGridHeaderPointerOverBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource TreeDataGridHeaderPointerOverBorderBrush}" />
|
||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource TreeDataGridHeaderPointerOverForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pressed /template/ Border#DataGridBorder">
|
||||
<Setter Property="Background" Value="{DynamicResource TreeDataGridHeaderPressedBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource TreeDataGridHeaderPressedBorderBrush}" />
|
||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource TreeDataGridHeaderPressedForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SortDirection=Ascending] /template/ PathIcon#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{DynamicResource TreeDataGridSortIconAscendingPath}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SortDirection=Descending] /template/ PathIcon#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{DynamicResource TreeDataGridSortIconDescendingPath}" />
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridRow}" TargetType="TreeDataGridRow">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel>
|
||||
<Border
|
||||
Name="RowBorder"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
<TreeDataGridCellsPresenter
|
||||
Name="PART_CellsPresenter"
|
||||
ElementFactory="{TemplateBinding ElementFactory}"
|
||||
Items="{TemplateBinding Columns}"
|
||||
Rows="{TemplateBinding Rows}" />
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:selected /template/ Border#RowBorder">
|
||||
<Setter Property="Background" Value="{DynamicResource TreeDataGridCellSelectedBackground}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridCheckBoxCell}" TargetType="TreeDataGridCheckBoxCell">
|
||||
<Setter Property="Padding" Value="4 2" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
x:Name="CellBorder"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<CheckBox
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{TemplateBinding Value,
|
||||
Mode=TwoWay}"
|
||||
IsEnabled="{Binding !IsReadOnly, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
IsThreeState="{TemplateBinding IsThreeState}"
|
||||
Theme="{DynamicResource SimpleCheckBox}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="TreeDataGridExpandCollapseChevron" TargetType="ToggleButton">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Width" Value="8" />
|
||||
<Setter Property="Height" Value="8" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TreeDataGridColumnHeaderForeground}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent">
|
||||
<PathIcon
|
||||
x:Name="ChevronPath"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Data="{DynamicResource TreeDataGridItemCollapsedChevronPathData}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:checked /template/ PathIcon#ChevronPath">
|
||||
<Setter Property="PathIcon.RenderTransform" Value="rotate(90deg)" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridExpanderCell}" TargetType="TreeDataGridExpanderCell">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
x:Name="CellBorder"
|
||||
Padding="{TemplateBinding Indent,
|
||||
Converter={x:Static conv:IndentConverter.Instance}}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DockPanel>
|
||||
<Border
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="4,0"
|
||||
DockPanel.Dock="Left">
|
||||
<ToggleButton
|
||||
Focusable="False"
|
||||
IsChecked="{TemplateBinding IsExpanded,
|
||||
Mode=TwoWay}"
|
||||
IsVisible="{TemplateBinding ShowExpander}"
|
||||
Theme="{StaticResource TreeDataGridExpandCollapseChevron}" />
|
||||
</Border>
|
||||
<Decorator Name="PART_Content" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridTextCell}" TargetType="TreeDataGridTextCell">
|
||||
<Setter Property="Padding" Value="8 4" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
x:Name="CellBorder"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Text="{TemplateBinding Value}"
|
||||
TextTrimming="{TemplateBinding TextTrimming}"
|
||||
TextWrapping="{TemplateBinding TextWrapping}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:editing">
|
||||
<Setter Property="Padding" Value="4 2" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
x:Name="CellBorder"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<TextBox
|
||||
Name="PART_Edit"
|
||||
Classes="Small"
|
||||
Text="{TemplateBinding Value,
|
||||
Mode=TwoWay}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:editing /template/ TextBox#PART_Edit">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="MinHeight" Value="25" />
|
||||
<Setter Property="Padding" Value="10,3,6,3" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:editing /template/ TextBox#PART_Edit DataValidationErrors">
|
||||
<Setter Property="Theme" Value="{DynamicResource TooltipDataValidationErrors}" />
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type TreeDataGridTemplateCell}" TargetType="TreeDataGridTemplateCell">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter
|
||||
Name="PART_ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:editing">
|
||||
<Setter Property="Padding" Value="4 2" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter
|
||||
Name="PART_EditingContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding EditingTemplate}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
Loading…
x
Reference in New Issue
Block a user