Merge pull request #13 from irihitech/11-timeline

11 timeline
This commit is contained in:
Dong Bin
2023-04-24 22:56:13 +08:00
committed by GitHub
14 changed files with 577 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<UserControl
x:Class="Ursa.Demo.Pages.TimelineDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:viewModels="clr-namespace:Ursa.Demo.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="False"
x:DataType="viewModels:TimelineDemoViewModel"
mc:Ignorable="d">
<UserControl.Resources>
<u:TimelineFormatConverter x:Key="FormatConverter" />
</UserControl.Resources>
<StackPanel>
<u:Timeline>
<u:TimelineItem
Content="Start"
ItemType="Warning"
Time="2022-01-01" />
<u:TimelineItem
Content="In between"
ItemType="Ongoing"
Time="2022-01-02" />
<u:TimelineItem
Content="Finished"
ItemType="Error"
Time="2022-01-03" />
<u:TimelineItem
Content="Finished"
IconForeground="Yellow"
ItemType="Default"
Time="2022-01-03" />
</u:Timeline>
<u:Timeline HorizontalAlignment="Left" ItemsSource="{Binding Items}">
<u:Timeline.ItemTemplate>
<DataTemplate x:DataType="viewModels:TimelineItemViewModel">
<u:TimelineItem
Content="{Binding Content}"
ItemType="{Binding ItemType}"
Time="{Binding Time}"
TimeFormat="{Binding TimeFormat}">
<u:TimelineItem.ContentTemplate>
<DataTemplate>
<TextBlock
MaxWidth="100"
Text="{Binding}"
TextWrapping="Wrap" />
</DataTemplate>
</u:TimelineItem.ContentTemplate>
</u:TimelineItem>
</DataTemplate>
</u:Timeline.ItemTemplate>
</u:Timeline>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Demo.ViewModels;
namespace Ursa.Demo.Pages;
public partial class TimelineDemo : UserControl
{
public TimelineDemo()
{
InitializeComponent();
this.DataContext = new TimelineDemoViewModel();
}
}

View File

@@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,115 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Ursa.Controls;
namespace Ursa.Demo.ViewModels;
public class TimelineDemoViewModel: ObservableObject
{
public TimelineItemViewModel[] Items { get; } =
{
new()
{
Time = DateTime.Now,
TimeFormat = "yyyy-MM-dd HH:mm:ss",
Description = "Item 1",
Content = "First",
ItemType = TimelineItemType.Success,
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 2",
Content = "Content 2",
ItemType = TimelineItemType.Success,
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 3",
Content = "Content 3",
ItemType = TimelineItemType.Ongoing,
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 4",
Content = "Content 4"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 5",
Content = "Content 5"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 6",
Content = "Content 6"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 7",
Content = "Content 71231"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 8",
Content = "Content 8123123"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 9",
Content = "Content 9123123"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 10",
Content = "Content 1231231231231231231230"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 11",
Content = "Content 11231231"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 12",
Content = "Content 12123123123123"
},
new()
{
Time = DateTime.Now,
TimeFormat = "HH:mm:ss",
Description = "Item 13",
Content = "Last"
}
};
}
public class TimelineItemViewModel: ObservableObject
{
public DateTime Time { get; set; }
public string? TimeFormat { get; set; }
public string? Description { get; set; }
public string? Content { get; set; }
public TimelineItemType ItemType { get; set; }
}

View File

@@ -35,6 +35,9 @@
<TabItem Header="IPv4Box">
<pages:IPv4BoxDemo />
</TabItem>
<TabItem Header="Timeline">
<pages:TimelineDemo />
</TabItem>
</TabControl>
</Grid>

View File

@@ -0,0 +1,116 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters"
xmlns:u="https://irihi.tech/ursa">
<Design.PreviewWith>
<StackPanel Width="100" Spacing="20">
<u:Timeline>
<u:TimelineItem Content="Hello" Time="2022-01-01" />
<u:TimelineItem Content="World" Time="2022-02-01" />
<u:TimelineItem Content="!" Time="2022-03-01" />
<u:TimelineItem />
</u:Timeline>
</StackPanel>
</Design.PreviewWith>
<!-- Add Resources Here -->
<u:TimelineFormatConverter x:Key="FormatConverter" />
<ControlTheme x:Key="{x:Type u:Timeline}" TargetType="u:Timeline">
<Setter Property="Template">
<ControlTemplate TargetType="u:Timeline">
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</ControlTemplate>
</Setter>
</ControlTheme>
<converters:TimelineItemTypeToIconForegroundConverter
x:Key="ForegroundConverter"
DefaultBrush="{DynamicResource DefaultTimelineIconForeground}"
ErrorBrush="{DynamicResource ErrorTimelineIconForeground}"
OngoingBrush="{DynamicResource OngoingTimelineIconForeground}"
SuccessBrush="{DynamicResource SuccessTimelineIconForeground}"
WarningBrush="{DynamicResource WarningTimelineIconForeground}" />
<ControlTheme x:Key="{x:Type u:TimelineItem}" TargetType="u:TimelineItem">
<Setter Property="u:TimelineItem.Template">
<ControlTemplate TargetType="u:TimelineItem">
<Grid ColumnDefinitions="Auto, *" RowDefinitions="*, Auto, *">
<Grid
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
RowDefinitions="Auto, Auto, *">
<Rectangle
Grid.Row="0"
Grid.Column="0"
Width="1"
Height="8"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Classes="start"
Fill="LightGray" />
<Panel Grid.Row="1">
<Ellipse
Width="8"
Height="8"
Margin="2"
HorizontalAlignment="Center"
VerticalAlignment="Top">
<Ellipse.Fill>
<MultiBinding Converter="{StaticResource ForegroundConverter}">
<Binding Path="ItemType" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="IconForeground" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</Ellipse.Fill>
</Ellipse>
</Panel>
<Rectangle
Grid.Row="2"
Grid.Column="0"
Width="1"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Classes="end"
Fill="LightGray" />
</Grid>
<Rectangle
Grid.Row="2"
Grid.Column="0"
Width="1"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Classes="end"
Fill="LightGray" />
<ContentPresenter
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Foreground="Gray">
<ContentPresenter.Content>
<MultiBinding Converter="{StaticResource FormatConverter}">
<Binding Path="Time" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="TimeFormat" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</ContentPresenter.Content>
</ContentPresenter>
<ContentPresenter
Name="content"
Grid.Row="1"
Grid.Column="1"
Margin="0,0,0,16"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:first /template/ Rectangle.start">
<Setter Property="Rectangle.Fill" Value="Transparent" />
</Style>
<Style Selector="^:last /template/ Rectangle.end">
<Setter Property="Rectangle.Fill" Value="Transparent" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -5,5 +5,6 @@
<ResourceInclude Source="Banner.axaml" />
<ResourceInclude Source="Divider.axaml" />
<ResourceInclude Source="IPv4Box.axaml" />
<ResourceInclude Source="Timeline.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,82 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Ursa.Controls;
namespace Ursa.Themes.Semi.Converters;
public class TimelineItemTypeToIconForegroundConverter: AvaloniaObject, IMultiValueConverter
{
public static readonly StyledProperty<IBrush> DefaultBrushProperty = AvaloniaProperty.Register<TimelineItemTypeToIconForegroundConverter, IBrush>(
nameof(DefaultBrush));
public IBrush DefaultBrush
{
get => GetValue(DefaultBrushProperty);
set => SetValue(DefaultBrushProperty, value);
}
public static readonly StyledProperty<IBrush> OngoingBrushProperty = AvaloniaProperty.Register<TimelineItemTypeToIconForegroundConverter, IBrush>(
nameof(OngoingBrush));
public IBrush OngoingBrush
{
get => GetValue(OngoingBrushProperty);
set => SetValue(OngoingBrushProperty, value);
}
public static readonly StyledProperty<IBrush> SuccessBrushProperty = AvaloniaProperty.Register<TimelineItemTypeToIconForegroundConverter, IBrush>(
nameof(SuccessBrush));
public IBrush SuccessBrush
{
get => GetValue(SuccessBrushProperty);
set => SetValue(SuccessBrushProperty, value);
}
public static readonly StyledProperty<IBrush> WarningBrushProperty = AvaloniaProperty.Register<TimelineItemTypeToIconForegroundConverter, IBrush>(
nameof(WarningBrush));
public IBrush WarningBrush
{
get => GetValue(WarningBrushProperty);
set => SetValue(WarningBrushProperty, value);
}
public static readonly StyledProperty<IBrush> ErrorBrushProperty = AvaloniaProperty.Register<TimelineItemTypeToIconForegroundConverter, IBrush>(
nameof(ErrorBrush));
public IBrush ErrorBrush
{
get => GetValue(ErrorBrushProperty);
set => SetValue(ErrorBrushProperty, value);
}
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values[0] is TimelineItemType type)
{
switch (type)
{
case TimelineItemType.Error:
return ErrorBrush;
case TimelineItemType.Warning:
return WarningBrush;
case TimelineItemType.Success:
return SuccessBrush;
case TimelineItemType.Ongoing:
return OngoingBrush;
case TimelineItemType.Default:
if (values[1] is IBrush brush)
{
return brush;
}
return DefaultBrush;
}
}
return AvaloniaProperty.UnsetValue;
}
}

View File

@@ -0,0 +1,8 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<SolidColorBrush x:Key="DefaultTimelineIconForeground" Opacity="0.13" Color="#FF2E3238" />
<SolidColorBrush x:Key="OngoingTimelineIconForeground" Color="#FF0077FA" />
<SolidColorBrush x:Key="SuccessTimelineIconForeground" Color="#FF3BB346" />
<SolidColorBrush x:Key="WarningTimelineIconForeground" Color="#FFFC8800" />
<SolidColorBrush x:Key="ErrorTimelineIconForeground" Color="#FFF93920" />
</ResourceDictionary>

View File

@@ -5,5 +5,6 @@
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
<MergeResourceInclude Source="IPv4Box.axaml" />
<MergeResourceInclude Source="Timeline.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,56 @@
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.ComTypes;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
namespace Ursa.Controls;
public class Timeline: ItemsControl
{
public static readonly StyledProperty<IDataTemplate?> ItemDescriptionTemplateProperty = AvaloniaProperty.Register<Timeline, IDataTemplate?>(
nameof(ItemDescriptionTemplate));
public IDataTemplate? ItemDescriptionTemplate
{
get => GetValue(ItemDescriptionTemplateProperty);
set => SetValue(ItemDescriptionTemplateProperty, value);
}
public Timeline()
{
ItemsView.CollectionChanged+=ItemsViewOnCollectionChanged;
}
private void ItemsViewOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RefreshTimelineItems();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
RefreshTimelineItems();
}
private void RefreshTimelineItems()
{
for (int i = 0; i < this.LogicalChildren.Count; i++)
{
if (this.LogicalChildren[i] is TimelineItem t)
{
t.SetIndex(i == 0, i == this.LogicalChildren.Count - 1);
}
else if (this.LogicalChildren[i] is ContentPresenter { Child: TimelineItem t2 })
{
t2.SetIndex(i == 0, i == this.LogicalChildren.Count - 1);
}
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace Ursa.Controls;
public class TimelineFormatConverter: IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values.Count> 1 && values[0] is DateTime date && values[1] is string s)
{
return date.ToString(s, culture);
}
return AvaloniaProperty.UnsetValue;
}
}

View File

@@ -0,0 +1,94 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Media;
namespace Ursa.Controls;
[PseudoClasses(PC_First, PC_Last, PC_Default, PC_Ongoing, PC_Success, PC_Warning, PC_Error)]
public class TimelineItem: ContentControl
{
private const string PC_First = ":first";
private const string PC_Last = ":last";
private const string PC_Default = ":default";
private const string PC_Ongoing = ":ongoing";
private const string PC_Success = ":success";
private const string PC_Warning = ":warning";
private const string PC_Error = ":error";
private static readonly IReadOnlyDictionary<TimelineItemType, string> _itemTypeMapping = new Dictionary<TimelineItemType, string>
{
{TimelineItemType.Default, PC_Default},
{TimelineItemType.Ongoing, PC_Ongoing},
{TimelineItemType.Success, PC_Success},
{TimelineItemType.Warning, PC_Warning},
{TimelineItemType.Error, PC_Error},
};
public static readonly StyledProperty<IBrush> IconForegroundProperty =
AvaloniaProperty.Register<TimelineItem, IBrush>(nameof(IconForeground));
public IBrush IconForeground
{
get => GetValue(IconForegroundProperty);
set => SetValue(IconForegroundProperty, value);
}
public static readonly StyledProperty<DateTime> TimeProperty = AvaloniaProperty.Register<TimelineItem, DateTime>(
nameof(Time));
public DateTime Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public static readonly StyledProperty<string?> TimeFormatProperty = AvaloniaProperty.Register<TimelineItem, string?>(
nameof(TimeFormat), defaultValue:CultureInfo.CurrentUICulture.DateTimeFormat.ShortDatePattern);
public string? TimeFormat
{
get => GetValue(TimeFormatProperty);
set => SetValue(TimeFormatProperty, value);
}
public static readonly StyledProperty<IDataTemplate> DescriptionTemplateProperty = AvaloniaProperty.Register<TimelineItem, IDataTemplate>(
nameof(DescriptionTemplate));
public IDataTemplate DescriptionTemplate
{
get => GetValue(DescriptionTemplateProperty);
set => SetValue(DescriptionTemplateProperty, value);
}
public static readonly StyledProperty<TimelineItemType> ItemTypeProperty = AvaloniaProperty.Register<TimelineItem, TimelineItemType>(
nameof(ItemType));
public TimelineItemType ItemType
{
get => GetValue(ItemTypeProperty);
set => SetValue(ItemTypeProperty, value);
}
internal void SetIndex(bool isFirst, bool isLast)
{
PseudoClasses.Set(PC_First, isFirst);
PseudoClasses.Set(PC_Last, isLast);
}
static TimelineItem()
{
ItemTypeProperty.Changed.AddClassHandler<TimelineItem>((o, e) => { o.OnItemTypeChanged(e); });
}
private void OnItemTypeChanged(AvaloniaPropertyChangedEventArgs args)
{
var oldValue = args.GetOldValue<TimelineItemType>();
var newValue = args.GetNewValue<TimelineItemType>();
PseudoClasses.Set(_itemTypeMapping[oldValue], false);
PseudoClasses.Set(_itemTypeMapping[newValue], true);
}
}

View File

@@ -0,0 +1,10 @@
namespace Ursa.Controls;
public enum TimelineItemType
{
Default,
Ongoing,
Success,
Warning,
Error,
}