mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-25 14:15:00 +00:00
refactor<*>: rewrite all codes...
This commit is contained in:
parent
89ff8aa744
commit
30ab8ae954
342 changed files with 17208 additions and 19633 deletions
202
src/Views/Controls/Avatar.cs
Normal file
202
src/Views/Controls/Avatar.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
src/Views/Controls/Badge.cs
Normal file
52
src/Views/Controls/Badge.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
src/Views/Controls/Bookmark.cs
Normal file
86
src/Views/Controls/Bookmark.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
105
src/Views/Controls/ChangeDisplaySwitcher.cs
Normal file
105
src/Views/Controls/ChangeDisplaySwitcher.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/Views/Controls/ChangeStatusIcon.cs
Normal file
111
src/Views/Controls/ChangeStatusIcon.cs
Normal 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 "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
312
src/Views/Controls/CommitGraph.cs
Normal file
312
src/Views/Controls/CommitGraph.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
src/Views/Controls/DragDropAdorner.cs
Normal file
80
src/Views/Controls/DragDropAdorner.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
34
src/Views/Controls/IconButton.cs
Normal file
34
src/Views/Controls/IconButton.cs
Normal 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); }
|
||||
}
|
||||
}
|
||||
}
|
51
src/Views/Controls/Loading.cs
Normal file
51
src/Views/Controls/Loading.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
src/Views/Controls/PageContainer.cs
Normal file
66
src/Views/Controls/PageContainer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
src/Views/Controls/PopupWidget.cs
Normal file
92
src/Views/Controls/PopupWidget.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
109
src/Views/Controls/TextEdit.cs
Normal file
109
src/Views/Controls/TextEdit.cs
Normal 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
228
src/Views/Controls/Tree.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
41
src/Views/Controls/TreeItem.cs
Normal file
41
src/Views/Controls/TreeItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
36
src/Views/Controls/WindowBorder.cs
Normal file
36
src/Views/Controls/WindowBorder.cs
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue