refactor<*>: rewrite all codes...

This commit is contained in:
leo 2021-04-29 20:05:55 +08:00
parent 89ff8aa744
commit 30ab8ae954
342 changed files with 17208 additions and 19633 deletions

View file

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SourceGit.Views.Controls {
/// <summary>
/// 头像控件
/// </summary>
public class Avatar : Image {
/// <summary>
/// 显示FallbackLabel时的背景色
/// </summary>
private static readonly Brush[] BACKGROUND_BRUSHES = new Brush[] {
new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90),
new LinearGradientBrush(Colors.DodgerBlue, Colors.LightSkyBlue, 90),
new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90),
new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90),
new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90),
};
/// <summary>
/// 头像资源本地缓存路径
/// </summary>
public static readonly string CACHE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"avatars");
/// <summary>
/// 邮件属性定义
/// </summary>
public static readonly DependencyProperty EmailProperty = DependencyProperty.Register(
"Email",
typeof(string),
typeof(Avatar),
new PropertyMetadata(null, OnEmailChanged));
/// <summary>
/// 邮件属性
/// </summary>
public string Email {
get { return (string)GetValue(EmailProperty); }
set { SetValue(EmailProperty, value); }
}
/// <summary>
/// 下载头像失败时显示的Label属性定义
/// </summary>
public static readonly DependencyProperty FallbackLabelProperty = DependencyProperty.Register(
"FallbackLabel",
typeof(string),
typeof(Avatar),
new PropertyMetadata("?"));
/// <summary>
/// 下载头像失败时显示的Label属性
/// </summary>
public string FallbackLabel {
get { return (string)GetValue(FallbackLabelProperty); }
set { SetValue(FallbackLabelProperty, value); }
}
private static Dictionary<string, List<Avatar>> requesting = new Dictionary<string, List<Avatar>>();
private static Dictionary<string, BitmapImage> loaded = new Dictionary<string, BitmapImage>();
private static Task loader = null;
/// <summary>
/// 渲染实现
/// </summary>
/// <param name="dc"></param>
protected override void OnRender(DrawingContext dc) {
base.OnRender(dc);
if (Source == null) {
var placeholder = FallbackLabel.Length > 0 ? FallbackLabel.Substring(0, 1) : "?";
var formatted = new FormattedText(
placeholder,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily("Consolas"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
Width * 0.65,
Brushes.White,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
var corner = Math.Max(2, Width / 16);
var offsetX = (double)0;
if (HorizontalAlignment == HorizontalAlignment.Right) {
offsetX = -Width * 0.5;
} else if (HorizontalAlignment == HorizontalAlignment.Left) {
offsetX = Width * 0.5;
}
var chars = placeholder.ToCharArray();
var sum = 0;
foreach (var ch in chars) sum += Math.Abs(ch);
Brush brush = BACKGROUND_BRUSHES[sum % BACKGROUND_BRUSHES.Length];
dc.DrawRoundedRectangle(brush, null, new Rect(-Width * 0.5 + offsetX, -Height * 0.5, Width, Height), corner, corner);
dc.DrawText(formatted, new Point(formatted.Width * -0.5 + offsetX, formatted.Height * -0.5));
}
}
/// <summary>
/// 邮件变化时触发
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnEmailChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Avatar a = d as Avatar;
if (a == null) return;
var oldEmail = e.OldValue as string;
if (!string.IsNullOrEmpty(oldEmail) && requesting.ContainsKey(oldEmail)) {
if (requesting[oldEmail].Count <= 1) {
requesting.Remove(oldEmail);
} else {
requesting[oldEmail].Remove(a);
}
}
a.Source = null;
a.InvalidateVisual();
var email = e.NewValue as string;
if (string.IsNullOrEmpty(email)) return;
if (loaded.ContainsKey(email)) {
a.Source = loaded[email];
return;
}
if (requesting.ContainsKey(email)) {
requesting[email].Add(a);
return;
}
byte[] hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(email.ToLower().Trim()));
string md5 = "";
for (int i = 0; i < hash.Length; i++) md5 += hash[i].ToString("x2");
md5 = md5.ToLower();
string filePath = Path.Combine(CACHE_PATH, md5);
if (File.Exists(filePath)) {
var img = new BitmapImage(new Uri(filePath));
loaded.Add(email, img);
a.Source = img;
return;
}
requesting.Add(email, new List<Avatar>());
requesting[email].Add(a);
Action job = () => {
try {
HttpWebRequest req = WebRequest.CreateHttp(Models.Preference.Instance.General.AvatarServer + md5 + "?d=404");
req.Timeout = 2000;
req.Method = "GET";
HttpWebResponse rsp = req.GetResponse() as HttpWebResponse;
if (rsp.StatusCode == HttpStatusCode.OK) {
using (Stream reader = rsp.GetResponseStream())
using (FileStream writer = File.OpenWrite(filePath)) {
reader.CopyTo(writer);
}
a.Dispatcher.Invoke(() => {
var img = new BitmapImage(new Uri(filePath));
loaded.Add(email, img);
if (requesting.ContainsKey(email)) {
foreach (var one in requesting[email]) one.Source = img;
}
});
} else {
if (!loaded.ContainsKey(email)) loaded.Add(email, null);
}
} catch {
if (!loaded.ContainsKey(email)) loaded.Add(email, null);
}
requesting.Remove(email);
};
if (loader != null && !loader.IsCompleted) {
loader = loader.ContinueWith(t => { job(); });
} else {
loader = Task.Run(job);
}
}
}
}

View file

@ -0,0 +1,52 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 徽章
/// </summary>
public class Badge : Border {
private TextBlock label = null;
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
"Label",
typeof(string),
typeof(Border),
new PropertyMetadata("", OnLabelChanged));
public string Label {
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
public Badge() {
Width = double.NaN;
Height = 18;
CornerRadius = new CornerRadius(9);
VerticalAlignment = VerticalAlignment.Center;
Background = FindResource("Brush.Badge") as Brush;
Visibility = Visibility.Collapsed;
label = new TextBlock();
label.FontSize = 10;
label.HorizontalAlignment = HorizontalAlignment.Center;
label.Margin = new Thickness(9, 0, 9, 0);
Child = label;
}
private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Badge badge = d as Badge;
if (badge != null) {
var text = e.NewValue as string;
if (string.IsNullOrEmpty(text) || text == "0") {
badge.Visibility = Visibility.Collapsed;
} else {
badge.label.Text = text;
badge.Visibility = Visibility.Visible;
}
}
}
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SourceGit.Views.Controls {
/// <summary>
/// 标签页图标
/// </summary>
public class Bookmark : Border {
private Path icon = null;
public static readonly Brush[] COLORS = new Brush[] {
Brushes.Transparent,
Brushes.White,
Brushes.Red,
Brushes.Orange,
Brushes.Yellow,
Brushes.ForestGreen,
Brushes.Purple,
Brushes.DeepSkyBlue,
Brushes.Magenta,
};
public static readonly DependencyProperty ColorProperty =
DependencyProperty.Register("Color", typeof(int), typeof(Bookmark), new PropertyMetadata(0, UpdateBookmark));
public int Color {
get { return (int)GetValue(ColorProperty); }
set { SetValue(ColorProperty, value); }
}
public static readonly DependencyProperty IsNewPageProperty =
DependencyProperty.Register("IsNewPage", typeof(bool), typeof(Bookmark), new PropertyMetadata(false, UpdateBookmark));
public bool IsNewPage {
get { return (bool)GetValue(IsNewPageProperty); }
set { SetValue(IsNewPageProperty, value); }
}
public static readonly DependencyProperty HideOnZeroProperty =
DependencyProperty.Register("HideOnZero", typeof(bool), typeof(Bookmark), new PropertyMetadata(false, UpdateBookmark));
public bool HideOnZero {
get { return (bool)GetValue(HideOnZeroProperty); }
set { SetValue(HideOnZeroProperty, value); }
}
public Bookmark() {
icon = new Path();
Child = icon;
UpdateBookmark(this, new DependencyPropertyChangedEventArgs());
}
private static void UpdateBookmark(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var mark = d as Bookmark;
if (mark == null) return;
if (mark.HideOnZero && mark.Color == 0) {
mark.Visibility = Visibility.Collapsed;
return;
}
if (!mark.IsNewPage) {
if (mark.Color == 0) {
mark.icon.Fill = mark.FindResource("Brush.FG1") as Brush;
mark.icon.Data = mark.FindResource("Icon.Git") as Geometry;
} else {
mark.icon.Fill = COLORS[mark.Color % COLORS.Length];
mark.icon.Data = mark.FindResource("Icon.Bookmark") as Geometry;
}
} else {
mark.icon.Fill = mark.FindResource("Brush.FG1") as Brush;
mark.icon.Data = mark.FindResource("Icon.NewPage") as Geometry;
}
mark.Visibility = Visibility.Visible;
}
}
}

View file

@ -0,0 +1,105 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SourceGit.Views.Controls {
/// <summary>
/// 用于切换变更显示模式的按钮
/// </summary>
public class ChangeDisplaySwitcher : Button {
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
"Mode",
typeof(Models.Change.DisplayMode),
typeof(ChangeDisplaySwitcher),
new PropertyMetadata(Models.Change.DisplayMode.Tree, OnModeChanged));
public Models.Change.DisplayMode Mode {
get { return (Models.Change.DisplayMode)GetValue(ModeProperty); }
set { SetValue(ModeProperty, value); }
}
public static readonly RoutedEvent ModeChangedEvent = EventManager.RegisterRoutedEvent(
"ModeChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ChangeDisplaySwitcher));
public event RoutedEventHandler ModeChanged {
add { AddHandler(ModeChangedEvent, value); }
remove { RemoveHandler(ModeChangedEvent, value); }
}
private Path icon = null;
public ChangeDisplaySwitcher() {
icon = new Path();
icon.Fill = FindResource("Brush.FG2") as Brush;
icon.Data = FindResource("Icon.Tree") as Geometry;
Content = icon;
Style = FindResource("Style.Button") as Style;
BorderThickness = new Thickness(0);
ToolTip = App.Text("ChangeDisplayMode");
Click += OnClicked;
}
private void OnClicked(object sender, RoutedEventArgs e) {
if (ContextMenu != null) {
ContextMenu.IsOpen = true;
e.Handled = true;
return;
}
var menu = new ContextMenu();
menu.Placement = PlacementMode.Bottom;
menu.PlacementTarget = this;
menu.StaysOpen = false;
menu.Focusable = true;
FillMenu(menu, "ChangeDisplayMode.Tree", "Icon.Tree", Models.Change.DisplayMode.Tree);
FillMenu(menu, "ChangeDisplayMode.List", "Icon.List", Models.Change.DisplayMode.List);
FillMenu(menu, "ChangeDisplayMode.Grid", "Icon.Grid", Models.Change.DisplayMode.Grid);
ContextMenu = menu;
ContextMenu.IsOpen = true;
e.Handled = true;
}
private void FillMenu(ContextMenu menu, string header, string icon, Models.Change.DisplayMode useMode) {
var iconMode = new Path();
iconMode.Width = 12;
iconMode.Height = 12;
iconMode.Fill = FindResource("Brush.FG2") as Brush;
iconMode.Data = FindResource(icon) as Geometry;
var item = new MenuItem();
item.Icon = iconMode;
item.Header = App.Text(header);
item.Click += (o, e) => Mode = useMode;
menu.Items.Add(item);
}
private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var elem = d as ChangeDisplaySwitcher;
if (elem != null) {
switch (elem.Mode) {
case Models.Change.DisplayMode.Tree:
elem.icon.Data = elem.FindResource("Icon.Tree") as Geometry;
break;
case Models.Change.DisplayMode.List:
elem.icon.Data = elem.FindResource("Icon.List") as Geometry;
break;
case Models.Change.DisplayMode.Grid:
elem.icon.Data = elem.FindResource("Icon.Grid") as Geometry;
break;
}
elem.RaiseEvent(new RoutedEventArgs(ModeChangedEvent));
}
}
}
}

View file

@ -0,0 +1,111 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 变更状态图标
/// </summary>
class ChangeStatusIcon : FrameworkElement {
public static readonly DependencyProperty ChangeProperty = DependencyProperty.Register(
"Change",
typeof(Models.Change),
typeof(ChangeStatusIcon),
new PropertyMetadata(null, ForceDirty));
public Models.Change Change {
get { return (Models.Change)GetValue(ChangeProperty); }
set { SetValue(ChangeProperty, value); }
}
public static readonly DependencyProperty IsLocalChangeProperty = DependencyProperty.Register(
"IsLocalChange",
typeof(bool),
typeof(ChangeStatusIcon),
new PropertyMetadata(false, ForceDirty));
public bool IsLocalChange {
get { return (bool)GetValue(IsLocalChangeProperty); }
set { SetValue(IsLocalChangeProperty, value); }
}
private Brush background;
private FormattedText label;
public ChangeStatusIcon() {
HorizontalAlignment = HorizontalAlignment.Center;
VerticalAlignment = VerticalAlignment.Center;
}
protected override void OnRender(DrawingContext dc) {
if (background == null || label == null) return;
var corner = Math.Max(2, Width / 16);
dc.DrawRoundedRectangle(background, null, new Rect(0, 0, Width, Height), corner, corner);
dc.DrawText(label, new Point((Width - label.Width) * 0.5, 0));
}
private static void ForceDirty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var icon = d as ChangeStatusIcon;
if (icon == null) return;
if (icon.Change == null) {
icon.background = null;
icon.label = null;
return;
}
string txt;
if (icon.IsLocalChange) {
if (icon.Change.IsConflit) {
icon.background = Brushes.OrangeRed;
txt = "!";
} else {
icon.background = GetBackground(icon.Change.WorkTree);
txt = GetLabel(icon.Change.WorkTree);
}
} else {
icon.background = GetBackground(icon.Change.Index);
txt = GetLabel(icon.Change.Index);
}
icon.label = new FormattedText(
txt,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily("Consolas"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
icon.Width * 0.8,
new SolidColorBrush(Color.FromRgb(241,241,241)),
VisualTreeHelper.GetDpi(icon).PixelsPerDip);
icon.InvalidateVisual();
}
private static Brush GetBackground(Models.Change.Status status) {
switch (status) {
case Models.Change.Status.Modified: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
case Models.Change.Status.Added: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90);
case Models.Change.Status.Deleted: return new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90);
case Models.Change.Status.Renamed: return new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90);
case Models.Change.Status.Copied: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
case Models.Change.Status.Unmerged: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90);
case Models.Change.Status.Untracked: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90);
default: return Brushes.Transparent;
}
}
private static string GetLabel(Models.Change.Status status) {
switch (status) {
case Models.Change.Status.Modified: return "±";
case Models.Change.Status.Added: return "✚";
case Models.Change.Status.Deleted: return "▬";
case Models.Change.Status.Renamed: return "➔";
case Models.Change.Status.Copied: return "❏";
case Models.Change.Status.Unmerged: return "U";
case Models.Change.Status.Untracked: return "?";
default: return "?";
}
}
}
}

View file

@ -0,0 +1,312 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 提交线路图
/// </summary>
public class CommitGraph : FrameworkElement {
public static readonly Brush[] COLORS = new Brush[] {
Brushes.Orange,
Brushes.ForestGreen,
Brushes.Gold,
Brushes.Magenta,
Brushes.Red,
Brushes.Gray,
Brushes.Turquoise,
Brushes.Olive,
};
public static readonly double UNIT_WIDTH = 12;
public static readonly double HALF_WIDTH = 6;
public static readonly double UNIT_HEIGHT = 24;
public static readonly double HALF_HEIGHT = 12;
public class Line {
public List<Point> Points = new List<Point>();
public int Color = 0;
}
public class LineHelper {
public string Next;
public bool IsMerged;
public double LastX;
public double LastY;
public Line Line;
public LineHelper(string next, bool isMerged, int color, Point start) {
Next = next;
IsMerged = isMerged;
LastX = start.X;
LastY = start.Y;
Line = new Line();
Line.Color = color % COLORS.Length;
Line.Points.Add(start);
}
public void Add(double x, double y, bool isEnd = false) {
if (x > LastX) {
Line.Points.Add(new Point(LastX, LastY));
Line.Points.Add(new Point(x, y - HALF_HEIGHT));
} else if (x < LastX) {
Line.Points.Add(new Point(LastX, LastY + HALF_HEIGHT));
Line.Points.Add(new Point(x, y));
}
LastX = x;
LastY = y;
if (isEnd) {
var last = Line.Points.Last();
if (LastX != last.X || LastY != last.Y) Line.Points.Add(new Point(LastX, LastY));
}
}
}
public class Link {
public Point Start;
public Point Control;
public Point End;
public int Color;
}
public class Dot {
public Point Center;
public int Color;
}
public class Data {
public List<Line> Lines = new List<Line>();
public List<Link> Links = new List<Link>();
public List<Dot> Dots = new List<Dot>();
}
private Data data = null;
private double startY = 0;
public CommitGraph() {
IsHitTestVisible = false;
ClipToBounds = true;
}
public void SetOffset(double offset) {
startY = offset;
InvalidateVisual();
}
public void SetData(List<Models.Commit> commits, bool isSearchResult = false) {
if (isSearchResult) {
foreach (var c in commits) c.Margin = new Thickness(0);
data = null;
return;
}
var temp = new Data();
var unsolved = new List<LineHelper>();
var mapUnsolved = new Dictionary<string, LineHelper>();
var ended = new List<LineHelper>();
var offsetY = -HALF_HEIGHT;
var colorIdx = 0;
foreach (var commit in commits) {
var major = null as LineHelper;
var isMerged = commit.IsMerged;
var oldCount = unsolved.Count;
// 更新Y坐标
offsetY += UNIT_HEIGHT;
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved) {
if (l.Next == commit.SHA) {
if (major == null) {
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0) {
major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major);
} else {
major.Next = "ENDED";
ended.Add(l);
}
major.Add(offsetX, offsetY);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY);
}
}
// 处理本提交为非当前分支HEAD的情况创建新依赖线路
if (major == null && commit.Parents.Count > 0) {
offsetX += UNIT_WIDTH;
major = new LineHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY));
unsolved.Add(major);
colorIdx++;
}
// 确定本提交的点的位置
Point position = new Point(offsetX, offsetY);
if (major != null) {
major.IsMerged = isMerged;
position.X = major.LastX;
position.Y = offsetY;
temp.Dots.Add(new Dot() { Center = position, Color = major.Line.Color });
} else {
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
}
// 处理本提交的其他依赖
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (mapUnsolved.ContainsKey(parent)) {
var l = mapUnsolved[parent];
var link = new Link();
link.Start = position;
link.End = new Point(l.LastX, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Color = l.Line.Color;
temp.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
unsolved.Add(new LineHelper(commit.Parents[j], isMerged, colorIdx, position));
colorIdx++;
}
}
// 处理已终止的线
foreach (var l in ended) {
l.Add(position.X, position.Y, true);
temp.Lines.Add(l.Line);
unsolved.Remove(l);
}
// 加入本次提交
commit.IsMerged = isMerged;
commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH), 0, 0, 0);
// 清理
ended.Clear();
mapUnsolved.Clear();
}
// 处理尚未终结的线
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
var endY = (commits.Count - 0.5) * UNIT_HEIGHT;
if (path.Line.Points.Count == 1 && path.Line.Points[0].Y == endY) continue;
path.Add((i + 0.5) * UNIT_WIDTH, endY, true);
temp.Lines.Add(path.Line);
}
unsolved.Clear();
// 排序
temp.Lines.Sort((l, h) => l.Points[0].Y.CompareTo(h.Points[0].Y));
Dispatcher.Invoke(() => {
data = temp;
InvalidateVisual();
});
}
protected override void OnRender(DrawingContext dc) {
if (data == null) return;
var top = startY;
var bottom = startY + ActualHeight;
dc.PushTransform(new TranslateTransform(0, -startY));
// 绘制曲线
foreach (var line in data.Lines) {
var last = line.Points[0];
var size = line.Points.Count;
if (line.Points[size - 1].Y < top) continue;
if (last.Y > bottom) continue;
var geo = new StreamGeometry();
var pen = new Pen(COLORS[line.Color], 2);
using (var ctx = geo.Open()) {
ctx.BeginFigure(last, false, false);
var ended = false;
for (int i = 1; i < size; i++) {
var cur = line.Points[i];
if (cur.Y > bottom) {
cur.Y = bottom;
ended = true;
}
if (cur.X > last.X) {
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur, true, false);
} else if (cur.X < last.X) {
if (i < size - 1) {
cur.Y += HALF_HEIGHT;
var midY = (last.Y + cur.Y) / 2;
var midX = (last.X + cur.X) / 2;
ctx.PolyQuadraticBezierTo(new Point[] {
new Point(last.X, midY),
new Point(midX, midY),
new Point(cur.X, midY),
cur}, true, false);
} else {
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur, true, false);
}
} else {
ctx.LineTo(cur, true, false);
}
if (ended) break;
last = cur;
}
}
geo.Freeze();
dc.DrawGeometry(null, pen, geo);
}
// 绘制合并线
foreach (var link in data.Links) {
if (link.End.Y < top) continue;
if (link.Start.Y > bottom) break;
var geo = new StreamGeometry();
var pen = new Pen(COLORS[link.Color], 2);
using (var ctx = geo.Open()) {
ctx.BeginFigure(link.Start, false, false);
ctx.QuadraticBezierTo(link.Control, link.End, true, false);
}
geo.Freeze();
dc.DrawGeometry(null, pen, geo);
}
// 绘制点
foreach (var dot in data.Dots) {
if (dot.Center.Y < top) continue;
if (dot.Center.Y > bottom) break;
dc.DrawEllipse(COLORS[dot.Color], null, dot.Center, 3, 3);
}
}
}
}

View file

@ -0,0 +1,80 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// DragDropAdorner容器
/// </summary>
public class DragDropAdornerLayer : Grid {
private static AdornerLayer host = null;
private static bool enableFeedback = false;
public DragDropAdornerLayer() {
Loaded += (o, e) => host = AdornerLayer.GetAdornerLayer(this);
PreviewGiveFeedback += OnPreviewGiveFeedback;
}
public static void Add(Adorner adorner) {
host.Add(adorner);
enableFeedback = true;
}
public static void Remove(Adorner adorner) {
host.Remove(adorner);
enableFeedback = false;
}
private static void OnPreviewGiveFeedback(object sender, GiveFeedbackEventArgs e) {
if (enableFeedback) host.Update();
e.Handled = true;
}
}
/// <summary>
/// 展示正在拖拽的视图
/// </summary>
public class DragDropAdorner : Adorner {
private Size renderSize;
private Brush renderBrush;
public struct PInPoint {
public int X;
public int Y;
public PInPoint(int x, int y) { X = x; Y = y; }
public PInPoint(double x, double y) { X = (int)x; Y = (int)y; }
}
[DllImport("user32.dll")]
static extern void GetCursorPos(ref PInPoint p);
public DragDropAdorner(FrameworkElement elem) : base(elem) {
renderSize = elem.RenderSize;
renderBrush = new VisualBrush(elem);
IsHitTestVisible = false;
DragDropAdornerLayer.Add(this);
}
public void Remove() {
DragDropAdornerLayer.Remove(this);
}
protected override void OnRender(DrawingContext dc) {
base.OnRender(dc);
PInPoint p = new PInPoint();
GetCursorPos(ref p);
Point pos = PointFromScreen(new Point(p.X, p.Y));
Rect rect = new Rect(pos.X, pos.Y, renderSize.Width, renderSize.Height);
dc.PushOpacity(1);
dc.DrawRectangle(renderBrush, null, rect);
dc.DrawRectangle(null, new Pen(Brushes.DeepSkyBlue, 2), rect);
}
}
}

View file

@ -0,0 +1,34 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 简化只有一个Icon的Button
/// </summary>
public class IconButton : Button {
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
"Icon",
typeof(Geometry),
typeof(IconButton),
new PropertyMetadata(null));
public Geometry Icon {
get { return (Geometry)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty HoverBackgroundProperty = DependencyProperty.Register(
"HoverBackground",
typeof(Brush),
typeof(IconButton),
new PropertyMetadata(Brushes.Transparent));
public Brush HoverBackground {
get { return (Brush)GetValue(HoverBackgroundProperty); }
set { SetValue(HoverBackgroundProperty, value); }
}
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SourceGit.Views.Controls {
/// <summary>
/// 加载中图标
/// </summary>
public class Loading : UserControl {
private Path icon = null;
public static readonly DependencyProperty IsAnimatingProperty = DependencyProperty.Register(
"IsAnimating",
typeof(bool),
typeof(Loading),
new PropertyMetadata(false, OnIsAnimatingChanged));
public bool IsAnimating {
get { return (bool)GetValue(IsAnimatingProperty); }
set { SetValue(IsAnimatingProperty, value); }
}
public Loading() {
icon = new Path();
icon.Data = FindResource("Icon.Loading") as Geometry;
icon.RenderTransformOrigin = new Point(.5, .5);
icon.RenderTransform = new RotateTransform(0);
icon.Width = double.NaN;
icon.Height = double.NaN;
AddChild(icon);
}
private static void OnIsAnimatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var loading = d as Loading;
if (loading == null) return;
if (loading.IsAnimating) {
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.icon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
} else {
loading.icon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
}
}
}
}

View file

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 用于方便切换子页面的组件
/// </summary>
public class PageContainer : Grid {
private Dictionary<string, UIElement> pages;
private string front;
public PageContainer() {
pages = new Dictionary<string, UIElement>();
front = null;
Loaded += OnLoaded;
}
public void Add(string id, UIElement view) {
view.Visibility = Visibility.Collapsed;
pages.Add(id, view);
Children.Add(view);
}
public UIElement Get(string id) {
if (pages.ContainsKey(id)) return pages[id];
return null;
}
public void Goto(string id) {
if (!pages.ContainsKey(id)) return;
if (!string.IsNullOrEmpty(front)) {
if (front == id) return;
pages[front].Visibility = Visibility.Collapsed;
}
front = id;
pages[front].Visibility = Visibility.Visible;
}
public void Remove(string id) {
if (!pages.ContainsKey(id)) return;
if (front == id) front = null;
Children.Remove(pages[id]);
pages.Remove(id);
}
private void OnLoaded(object sender, RoutedEventArgs e) {
foreach (var child in Children) {
var elem = child as UIElement;
var id = elem.Uid;
if (string.IsNullOrEmpty(id)) continue;
pages.Add(id, elem);
front = id;
}
if (!string.IsNullOrEmpty(front)) {
pages[front].Visibility = Visibility.Visible;
}
}
}
}

View file

@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 可显示弹出面板的容器接口
/// </summary>
public interface IPopupContainer {
void Show(PopupWidget widget);
void ShowAndStart(PopupWidget widget);
void UpdateProgress(string message);
}
/// <summary>
/// 可弹出面板
/// </summary>
public class PopupWidget : UserControl {
private static Dictionary<string, IPopupContainer> containers = new Dictionary<string, IPopupContainer>();
private static string currentContainer = null;
private IPopupContainer mine = null;
/// <summary>
/// 注册一个弹出容器
/// </summary>
/// <param name="id">页面ID</param>
/// <param name="container">容器实例</param>
public static void RegisterContainer(string id, IPopupContainer container) {
if (containers.ContainsKey(id)) containers[id] = container;
else containers.Add(id, container);
}
/// <summary>
/// 删除一个弹出容器
/// </summary>
/// <param name="id">容器ID</param>
public static void UnregisterContainer(string id) {
if (containers.ContainsKey(id)) containers.Remove(id);
}
/// <summary>
/// 设置当前的弹出容器
/// </summary>
/// <param name="id">容器ID</param>
public static void SetCurrentContainer(string id) {
currentContainer = id;
}
/// <summary>
/// 显示
/// </summary>
public void Show() {
if (string.IsNullOrEmpty(currentContainer) || !containers.ContainsKey(currentContainer)) return;
mine = containers[currentContainer];
mine.Show(this);
}
/// <summary>
/// 显示并直接点击开始
/// </summary>
public void ShowAndStart() {
if (string.IsNullOrEmpty(currentContainer) || !containers.ContainsKey(currentContainer)) return;
mine = containers[currentContainer];
mine.ShowAndStart(this);
}
/// <summary>
/// 窗体标题
/// </summary>
/// <returns>返回具体的标题</returns>
public virtual string GetTitle() {
return "TITLE";
}
/// <summary>
/// 点击确定时的回调,由程序自己
/// </summary>
/// <returns>返回一个任务任务预期返回类型为bool表示是否关闭Popup</returns>
public virtual Task<bool> Start() {
return null;
}
/// <summary>
/// 更新进度显示
/// </summary>
/// <param name="message"></param>
protected void UpdateProgress(string message) {
mine?.UpdateProgress(message);
}
}
}

View file

@ -0,0 +1,109 @@
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 扩展默认TextBox
/// </summary>
public class TextEdit : TextBox {
private bool isPlaceholderShow = false;
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(
"Placeholder",
typeof(string),
typeof(TextEdit),
new PropertyMetadata(""));
public string Placeholder {
get { return (string)GetValue(PlaceholderProperty); }
set { SetValue(PlaceholderProperty, value); }
}
public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.Register(
"PlaceholderBaseline",
typeof(AlignmentY),
typeof(TextEdit),
new PropertyMetadata(AlignmentY.Center));
public AlignmentY PlaceholderBaseline {
get { return (AlignmentY)GetValue(PlaceholderBaselineProperty); }
set { SetValue(PlaceholderBaselineProperty, value); }
}
public TextEdit() {
TextChanged += OnTextChanged;
SelectionChanged += OnSelectionChanged;
}
protected override void OnRender(DrawingContext dc) {
base.OnRender(dc);
if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder)) {
isPlaceholderShow = true;
var text = new FormattedText(
Placeholder,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretches.Normal),
FontSize,
FindResource("Brush.FG2") as Brush,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
switch (PlaceholderBaseline) {
case AlignmentY.Top:
dc.DrawText(text, new Point(4, 4));
break;
case AlignmentY.Center:
dc.DrawText(text, new Point(4, ActualHeight * .5 - text.Height * .5));
break;
default:
dc.DrawText(text, new Point(4, ActualHeight - text.Height - 4));
break;
}
} else {
isPlaceholderShow = false;
}
}
private void OnTextChanged(object sender, TextChangedEventArgs e) {
if (string.IsNullOrEmpty(Text) || isPlaceholderShow) InvalidateVisual();
}
private void OnSelectionChanged(object sender, RoutedEventArgs e) {
if (!IsFocused) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && SelectionLength > 0) {
var p = Mouse.GetPosition(this);
if (p.X <= 8) {
LineLeft();
} else if (p.X >= ActualWidth - 8) {
LineRight();
}
if (p.Y <= 8) {
LineUp();
} else if (p.Y >= ActualHeight - 8) {
LineDown();
}
} else {
var rect = GetRectFromCharacterIndex(CaretIndex);
if (rect.Left <= 0) {
ScrollToHorizontalOffset(HorizontalOffset + rect.Left);
} else if (rect.Right >= ActualWidth) {
ScrollToHorizontalOffset(HorizontalOffset + rect.Right);
}
if (rect.Top <= 0) {
ScrollToVerticalOffset(VerticalOffset + rect.Top);
} else if (rect.Bottom >= ActualHeight) {
ScrollToVerticalOffset(VerticalOffset + rect.Bottom);
}
}
}
}
}

228
src/Views/Controls/Tree.cs Normal file
View file

@ -0,0 +1,228 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 树
/// </summary>
public class Tree : TreeView {
public static readonly DependencyProperty MultiSelectionProperty = DependencyProperty.Register(
"MultiSelection",
typeof(bool),
typeof(Tree),
new PropertyMetadata(false));
public bool MultiSelection {
get { return (bool)GetValue(MultiSelectionProperty); }
set { SetValue(MultiSelectionProperty, value); }
}
public static readonly DependencyProperty IndentProperty = DependencyProperty.Register(
"Indent",
typeof(double),
typeof(TreeItem),
new PropertyMetadata(16.0));
public double Indent {
get { return (double)GetValue(IndentProperty); }
set { SetValue(IndentProperty, value); }
}
public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(
"SelectionChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(Tree));
public event RoutedEventHandler SelectionChanged {
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
public List<object> Selected {
get;
set;
}
public Tree() {
Selected = new List<object>();
PreviewMouseDown += OnPreviewMouseDown;
}
public TreeItem FindItem(DependencyObject elem) {
if (elem == null) return null;
if (elem is TreeItem) return elem as TreeItem;
if (elem is Tree) return null;
return FindItem(VisualTreeHelper.GetParent(elem));
}
public void SelectAll() {
SelectAllChildren(this);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
public void UnselectAll() {
if (Selected.Count == 0) return;
UnselectAllChildren(this);
Selected.Clear();
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
protected override DependencyObject GetContainerForItemOverride() {
return new TreeItem(0, Indent);
}
protected override bool IsItemItsOwnContainerOverride(object item) {
return item is TreeItem;
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
base.OnItemsSourceChanged(oldValue, newValue);
if (Selected.Count > 0) {
Selected.Clear();
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
}
private TreeItem FindItemByDataContext(ItemsControl control, object data) {
if (control == null) return null;
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (control.Items[i] == data) return child;
var found = FindItemByDataContext(child, data);
if (found != null) return found;
}
return null;
}
private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e) {
var hit = VisualTreeHelper.HitTest(this, e.GetPosition(this));
if (hit == null || hit.VisualHit == null) return;
var item = FindItem(hit.VisualHit);
if (item == null) return;
if (!MultiSelection) {
if (item.IsChecked) return;
AddSelected(item, true);
return;
}
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
if (item.IsChecked) {
RemoveSelected(item);
} else {
AddSelected(item, false);
}
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && Selected.Count > 0) {
var last = FindItemByDataContext(this, Selected.Last());
if (last == item) return;
var lastPos = last.PointToScreen(new Point(0, 0));
var curPos = item.PointToScreen(new Point(0, 0));
if (lastPos.Y > curPos.Y) {
SelectRange(this, item, last);
} else {
SelectRange(this, last, item);
}
AddSelected(item, false);
} else if (e.RightButton == MouseButtonState.Pressed) {
if (item.IsChecked) return;
AddSelected(item, true);
} else {
if (item.IsChecked && Selected.Count == 1) return;
AddSelected(item, true);
}
}
private void AddSelected(TreeItem item, bool removeOthers) {
if (removeOthers && Selected.Count > 0) {
UnselectAllChildren(this);
Selected.Clear();
}
item.IsChecked = true;
Selected.Add(item.DataContext);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void RemoveSelected(TreeItem item) {
item.IsChecked = false;
Selected.Remove(item.DataContext);
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void SelectAllChildren(ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
child.IsChecked = true;
Selected.Add(control.Items[i]);
SelectAllChildren(child);
}
}
private void UnselectAllChildren(ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (child.IsChecked) child.IsChecked = false;
UnselectAllChildren(child);
}
}
private int SelectRange(ItemsControl control, TreeItem from, TreeItem to, int matches = 0) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (matches == 1) {
if (child == to) return 2;
Selected.Add(control.Items[i]);
child.IsChecked = true;
if (TryEndRangeSelection(child, to)) return 2;
} else if (child == from) {
matches = 1;
if (TryEndRangeSelection(child, to)) return 2;
} else {
matches = SelectRange(child, from, to, matches);
if (matches == 2) return 2;
}
}
return matches;
}
private bool TryEndRangeSelection(ItemsControl control, TreeItem end) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeItem;
if (child == null) continue;
if (child == end) {
return true;
} else {
Selected.Add(control.Items[i]);
child.IsChecked = true;
var ended = TryEndRangeSelection(child, end);
if (ended) return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,41 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Controls {
/// <summary>
/// 树节点
/// </summary>
public class TreeItem : TreeViewItem {
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(
"IsChecked",
typeof(bool),
typeof(TreeItem),
new PropertyMetadata(false));
public bool IsChecked {
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
private int depth = 0;
private double indent = 16;
public TreeItem(int depth, double indent) {
this.depth = depth;
this.indent = indent;
Padding = new Thickness(indent * depth, 0, 0, 0);
RequestBringIntoView += (o, e) => e.Handled = true;
}
protected override DependencyObject GetContainerForItemOverride() {
return new TreeItem(depth + 1, indent);
}
protected override bool IsItemItsOwnContainerOverride(object item) {
return item is TreeItem;
}
}
}

View file

@ -0,0 +1,36 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Views.Controls {
/// <summary>
/// 主窗体Border
/// </summary>
public class WindowBorder : Border {
public WindowBorder() {
Background = FindResource("Brush.Window") as Brush;
BorderBrush = FindResource("Brush.Border0") as Brush;
BorderThickness = new Thickness(1);
Margin = new Thickness(0);
Loaded += (o, e) => {
var owner = Parent as Window;
if (owner != null) {
owner.StateChanged += (o1, e1) => {
if (owner.WindowState == WindowState.Maximized) {
BorderThickness = new Thickness(0);
Margin = new Thickness(
(SystemParameters.MaximizedPrimaryScreenWidth - SystemParameters.WorkArea.Width) / 2
);
} else {
BorderThickness = new Thickness(1);
Margin = new Thickness(0);
}
};
}
};
}
}
}