Upgrade project style

This commit is contained in:
ONEO 2020-07-06 10:23:45 +08:00
parent afd22ca85d
commit baa6c1445a
136 changed files with 29 additions and 770 deletions

6
SourceGit/App.config Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
</configuration>

BIN
SourceGit/App.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

16
SourceGit/App.xaml Normal file
View file

@ -0,0 +1,16 @@
<Application x:Class="SourceGit.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="OnAppStartup"
Deactivated="OnAppDeactivated"
Exit="OnAppExit">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Icons.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Controls.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Themes/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

134
SourceGit/App.xaml.cs Normal file
View file

@ -0,0 +1,134 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
namespace SourceGit {
/// <summary>
/// Application.
/// </summary>
public partial class App : Application {
/// <summary>
/// Getter/Setter for Git preference.
/// </summary>
public static Git.Preference Preference {
get { return Git.Preference.Instance; }
set { Git.Preference.Instance = value; }
}
/// <summary>
/// Check if GIT has been configured.
/// </summary>
public static bool IsGitConfigured {
get {
return !string.IsNullOrEmpty(Preference.GitExecutable)
&& File.Exists(Preference.GitExecutable);
}
}
/// <summary>
/// Interactive rebase sequence file.
/// </summary>
public static string InteractiveRebaseScript {
get;
private set;
}
/// <summary>
/// TODO file for interactive rebase.
/// </summary>
public static string InteractiveRebaseTodo {
get;
private set;
}
/// <summary>
/// Error handler.
/// </summary>
public static Action<string> OnError {
get;
set;
}
/// <summary>
/// Raise error message.
/// </summary>
/// <param name="message"></param>
public static void RaiseError(string message) {
OnError?.Invoke(message);
}
/// <summary>
/// Startup event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppStartup(object sender, StartupEventArgs e) {
// Try auto configure git via registry.
if (!IsGitConfigured) {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var git = root.OpenSubKey("SOFTWARE\\GitForWindows");
if (git != null) {
Preference.GitExecutable = Path.Combine(
git.GetValue("InstallPath") as string,
"bin",
"git.exe");
}
}
// Files for interactive rebase.
InteractiveRebaseScript = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"rebase.bat");
InteractiveRebaseTodo = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"REBASE_TODO");
if (!File.Exists(InteractiveRebaseScript)) {
var folder = Path.GetDirectoryName(InteractiveRebaseScript);
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
File.WriteAllText(InteractiveRebaseScript, $"@echo off\ntype \"{InteractiveRebaseTodo}\" > .git\\rebase-merge\\git-rebase-todo");
File.WriteAllText(InteractiveRebaseTodo, "");
}
// Apply themes
if (Preference.UIUseLightTheme) {
foreach (var rs in Current.Resources.MergedDictionaries) {
if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/")) {
rs.Source = new Uri("pack://application:,,,/Resources/Themes/Light.xaml", UriKind.Absolute);
break;
}
}
}
// Show main window
var launcher = new UI.Launcher();
launcher.Show();
}
/// <summary>
/// Deactivated event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppDeactivated(object sender, EventArgs e) {
GC.Collect();
}
/// <summary>
/// Quit event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnAppExit(object sender, ExitEventArgs e) {
Git.Preference.Save();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Same as BoolToVisibilityConverter.
/// </summary>
public class BoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to brush
/// </summary>
public class FileStatusToColor : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return Brushes.Transparent;
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return Brushes.Yellow;
status = change.WorkTree;
} else {
status = change.Index;
}
if (App.Preference.UIUseLightTheme) {
switch (status) {
case Git.Change.Status.Modified: return Brushes.Goldenrod;
case Git.Change.Status.Added: return Brushes.Green;
case Git.Change.Status.Deleted: return Brushes.Red;
case Git.Change.Status.Renamed: return Brushes.Magenta;
case Git.Change.Status.Copied: return Brushes.Goldenrod;
case Git.Change.Status.Unmerged: return Brushes.Goldenrod;
case Git.Change.Status.Untracked: return Brushes.Green;
default: return Brushes.Transparent;
}
} else {
switch (status) {
case Git.Change.Status.Modified: return Brushes.DarkGoldenrod;
case Git.Change.Status.Added: return Brushes.DarkGreen;
case Git.Change.Status.Deleted: return Brushes.DarkRed;
case Git.Change.Status.Renamed: return Brushes.DarkMagenta;
case Git.Change.Status.Copied: return Brushes.DarkGoldenrod;
case Git.Change.Status.Unmerged: return Brushes.DarkGoldenrod;
case Git.Change.Status.Untracked: return Brushes.DarkGreen;
default: return Brushes.Transparent;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert file status to icon.
/// </summary>
public class FileStatusToIcon : IValueConverter {
/// <summary>
/// Is only test local changes.
/// </summary>
public bool OnlyWorkTree { get; set; } = false;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var change = value as Git.Change;
if (change == null) return "";
var status = Git.Change.Status.None;
if (OnlyWorkTree) {
if (change.IsConflit) return "X";
status = change.WorkTree;
} else {
status = change.Index;
}
switch (status) {
case Git.Change.Status.Modified: return "M";
case Git.Change.Status.Added: return "A";
case Git.Change.Status.Deleted: return "D";
case Git.Change.Status.Renamed: return "R";
case Git.Change.Status.Copied: return "C";
case Git.Change.Status.Unmerged: return "U";
case Git.Change.Status.Untracked: return "?";
default: return "?";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert indent(horizontal offset) to Margin property
/// </summary>
public class IndentToMargin : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return new Thickness((double)value, 0, 0, 0);
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return ((Thickness)value).Left;
}
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse bool converter.
/// </summary>
public class InverseBool : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return !((bool)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Inverse BoolToCollapsed.
/// </summary>
public class InverseBoolToCollapsed : IValueConverter {
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Collapsed : Visibility.Visible;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace SourceGit.Converters {
/// <summary>
/// Convert percent to double.
/// </summary>
public class PercentToDouble : IValueConverter {
/// <summary>
/// Percentage.
/// </summary>
public double Percent { get; set; }
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (double)value * Percent;
}
/// <summary>
/// Implement IValueConverter.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace SourceGit.Converters {
/// <summary>
/// Convert depth of a TreeViewItem to Margin property.
/// </summary>
public class TreeViewItemDepthToMargin : IValueConverter {
/// <summary>
/// Indent length
/// </summary>
public double Indent { get; set; } = 19;
/// <summary>
/// Implement IValueConverter.Convert
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
TreeViewItem item = value as TreeViewItem;
if (item == null) return new Thickness(0);
TreeViewItem iterator = GetParent(item);
int depth = 0;
while (iterator != null) {
depth++;
iterator = GetParent(iterator);
}
return new Thickness(Indent * depth, 0, 0, 0);
}
/// <summary>
/// Implement IValueConvert.ConvertBack
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
/// <summary>
/// Get parent item.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private TreeViewItem GetParent(TreeViewItem item) {
var parent = VisualTreeHelper.GetParent(item);
while (parent != null && !(parent is TreeView) && !(parent is TreeViewItem)) {
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
}
}

35
SourceGit/Git/Blame.cs Normal file
View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace SourceGit.Git {
/// <summary>
/// Blame
/// </summary>
public class Blame {
/// <summary>
/// Block content.
/// </summary>
public class Block {
public string CommitSHA { get; set; }
public string Author { get; set; }
public string Time { get; set; }
public string Content { get; set; }
}
/// <summary>
/// Blocks
/// </summary>
public List<Block> Blocks { get; set; } = new List<Block>();
/// <summary>
/// Is binary file?
/// </summary>
public bool IsBinary { get; set; } = false;
/// <summary>
/// Line count.
/// </summary>
public int LineCount { get; set; } = 0;
}
}

190
SourceGit/Git/Branch.cs Normal file
View file

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git branch
/// </summary>
public class Branch {
private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)";
private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)");
private static readonly Regex AHEAD = new Regex(@"ahead (\d+)");
private static readonly Regex BEHIND = new Regex(@"behind (\d+)");
/// <summary>
/// Branch type.
/// </summary>
public enum Type {
Normal,
Feature,
Release,
Hotfix,
}
/// <summary>
/// Branch name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Full name.
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Head ref
/// </summary>
public string Head { get; set; }
/// <summary>
/// Subject for head ref.
/// </summary>
public string HeadSubject { get; set; }
/// <summary>
/// Is local branch
/// </summary>
public bool IsLocal { get; set; }
/// <summary>
/// Branch type.
/// </summary>
public Type Kind { get; set; } = Type.Normal;
/// <summary>
/// Remote name. Only used for remote branch
/// </summary>
public string Remote { get; set; }
/// <summary>
/// Upstream. Only used for local branches.
/// </summary>
public string Upstream { get; set; }
/// <summary>
/// Track information for upstream. Only used for local branches.
/// </summary>
public string UpstreamTrack { get; set; }
/// <summary>
/// Is current branch. Only used for local branches.
/// </summary>
public bool IsCurrent { get; set; }
/// <summary>
/// Is this branch's HEAD same with upstream?
/// </summary>
public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack);
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load branches.
/// </summary>
/// <param name="repo"></param>
public static List<Branch> Load(Repository repo) {
var localPrefix = "refs/heads/";
var remotePrefix = "refs/remotes/";
var branches = new List<Branch>();
var remoteBranches = new List<string>();
repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => {
var match = PARSE.Match(line);
if (!match.Success) return;
var branch = new Branch();
var refname = match.Groups[1].Value;
if (refname.EndsWith("/HEAD")) return;
if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) {
branch.Name = refname.Substring(localPrefix.Length);
branch.IsLocal = true;
} else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) {
var name = refname.Substring(remotePrefix.Length);
branch.Remote = name.Substring(0, name.IndexOf('/'));
branch.Name = name;
branch.IsLocal = false;
remoteBranches.Add(refname);
}
branch.FullName = refname;
branch.Head = match.Groups[2].Value;
branch.IsCurrent = match.Groups[3].Value == "*";
branch.Upstream = match.Groups[4].Value;
branch.UpstreamTrack = ParseTrack(match.Groups[5].Value);
branch.HeadSubject = match.Groups[6].Value;
branches.Add(branch);
});
// Fixed deleted remote branch
foreach (var b in branches) {
if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) {
b.Upstream = null;
}
}
return branches;
}
/// <summary>
/// Create new branch.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
public static void Create(Repository repo, string name, string startPoint) {
var errs = repo.RunCommand($"branch {name} {startPoint}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Rename branch
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
public void Rename(Repository repo, string name) {
var errs = repo.RunCommand($"branch -M {Name} {name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Delete branch.
/// </summary>
/// <param name="repo"></param>
public void Delete(Repository repo) {
string errs = null;
if (!IsLocal) {
errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null);
} else {
errs = repo.RunCommand($"branch -D {Name}", null);
}
if (errs != null) App.RaiseError(errs);
}
private static string ParseTrack(string data) {
if (string.IsNullOrEmpty(data)) return "";
string track = "";
var ahead = AHEAD.Match(data);
if (ahead.Success) {
track += ahead.Groups[1].Value + "↑ ";
}
var behind = BEHIND.Match(data);
if (behind.Success) {
track += behind.Groups[1].Value + "↓";
}
return track.Trim();
}
}
}

147
SourceGit/Git/Change.cs Normal file
View file

@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Changed file status.
/// </summary>
public class Change {
private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$");
/// <summary>
/// Status Code
/// </summary>
public enum Status {
None,
Modified,
Added,
Deleted,
Renamed,
Copied,
Unmerged,
Untracked,
}
/// <summary>
/// Index status
/// </summary>
public Status Index { get; set; }
/// <summary>
/// Work tree status.
/// </summary>
public Status WorkTree { get; set; }
/// <summary>
/// Current file path.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Original file path before this revision.
/// </summary>
public string OriginalPath { get; set; }
/// <summary>
/// Staged(added) in index?
/// </summary>
public bool IsAddedToIndex {
get {
if (Index == Status.None || Index == Status.Untracked) return false;
return true;
}
}
/// <summary>
/// Is conflict?
/// </summary>
public bool IsConflit {
get {
if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true;
if (Index == Status.Added && WorkTree == Status.Added) return true;
if (Index == Status.Deleted && WorkTree == Status.Deleted) return true;
return false;
}
}
/// <summary>
/// Parse change for `--name-status` data.
/// </summary>
/// <param name="data">Raw data.</param>
/// <param name="fromCommit">Read from commit?</param>
/// <returns>Parsed change instance.</returns>
public static Change Parse(string data, bool fromCommit = false) {
var match = FORMAT.Match(data);
if (!match.Success) return null;
var change = new Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
if (fromCommit) {
switch (status[0]) {
case 'M': change.Set(Status.Modified); break;
case 'A': change.Set(Status.Added); break;
case 'D': change.Set(Status.Deleted); break;
case 'R': change.Set(Status.Renamed); break;
case 'C': change.Set(Status.Copied); break;
default: return null;
}
} else {
switch (status) {
case " M": change.Set(Status.None, Status.Modified); break;
case " A": change.Set(Status.None, Status.Added); break;
case " D": change.Set(Status.None, Status.Deleted); break;
case " R": change.Set(Status.None, Status.Renamed); break;
case " C": change.Set(Status.None, Status.Copied); break;
case "M": change.Set(Status.Modified, Status.None); break;
case "MM": change.Set(Status.Modified, Status.Modified); break;
case "MD": change.Set(Status.Modified, Status.Deleted); break;
case "A": change.Set(Status.Added, Status.None); break;
case "AM": change.Set(Status.Added, Status.Modified); break;
case "AD": change.Set(Status.Added, Status.Deleted); break;
case "D": change.Set(Status.Deleted, Status.None); break;
case "R": change.Set(Status.Renamed, Status.None); break;
case "RM": change.Set(Status.Renamed, Status.Modified); break;
case "RD": change.Set(Status.Renamed, Status.Deleted); break;
case "C": change.Set(Status.Copied, Status.None); break;
case "CM": change.Set(Status.Copied, Status.Modified); break;
case "CD": change.Set(Status.Copied, Status.Deleted); break;
case "DR": change.Set(Status.Deleted, Status.Renamed); break;
case "DC": change.Set(Status.Deleted, Status.Copied); break;
case "DD": change.Set(Status.Deleted, Status.Deleted); break;
case "AU": change.Set(Status.Added, Status.Unmerged); break;
case "UD": change.Set(Status.Unmerged, Status.Deleted); break;
case "UA": change.Set(Status.Unmerged, Status.Added); break;
case "DU": change.Set(Status.Deleted, Status.Unmerged); break;
case "AA": change.Set(Status.Added, Status.Added); break;
case "UU": change.Set(Status.Unmerged, Status.Unmerged); break;
case "??": change.Set(Status.Untracked, Status.Untracked); break;
default: return null;
}
}
if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2);
if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2);
return change;
}
private void Set(Status index, Status workTree = Status.None) {
Index = index;
WorkTree = workTree;
if (index == Status.Renamed || workTree == Status.Renamed) {
var idx = Path.IndexOf('\t');
if (idx >= 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 1);
} else {
idx = Path.IndexOf(" -> ");
if (idx > 0) {
OriginalPath = Path.Substring(0, idx);
Path = Path.Substring(idx + 4);
}
}
}
}
}
}

262
SourceGit/Git/Commit.cs Normal file
View file

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git commit information.
/// </summary>
public class Commit {
private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----";
private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----";
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Short SHA.
/// </summary>
public string ShortSHA => SHA.Substring(0, 8);
/// <summary>
/// Parent commit SHAs.
/// </summary>
public List<string> Parents { get; set; } = new List<string>();
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Committer.
/// </summary>
public User Committer { get; set; } = new User();
/// <summary>
/// Subject
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// Extra message.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// HEAD commit?
/// </summary>
public bool IsHEAD { get; set; } = false;
/// <summary>
/// Merged in current branch?
/// </summary>
public bool IsMerged { get; set; } = false;
/// <summary>
/// X offset in graph
/// </summary>
public double GraphOffset { get; set; } = 0;
/// <summary>
/// Has decorators.
/// </summary>
public bool HasDecorators => Decorators.Count > 0;
/// <summary>
/// Decorators.
/// </summary>
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
/// <summary>
/// Read commits.
/// </summary>
/// <param name="repo">Repository</param>
/// <param name="limit">Limitations</param>
/// <returns>Parsed commits.</returns>
public static List<Commit> Load(Repository repo, string limit) {
List<Commit> commits = new List<Commit>();
Commit current = null;
bool bSkippingGpgsig = false;
bool findHead = false;
repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => {
if (bSkippingGpgsig) {
if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false;
return;
} else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) {
bSkippingGpgsig = true;
return;
}
if (line.StartsWith("commit ", StringComparison.Ordinal)) {
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
current = new Commit();
ParseSHA(current, line.Substring("commit ".Length));
if (!findHead) findHead = current.IsHEAD;
return;
}
if (current == null) return;
if (line.StartsWith("tree ", StringComparison.Ordinal)) {
return;
} else if (line.StartsWith("parent ", StringComparison.Ordinal)) {
current.Parents.Add(line.Substring("parent ".Length));
} else if (line.StartsWith("author ", StringComparison.Ordinal)) {
current.Author.Parse(line);
} else if (line.StartsWith("committer ", StringComparison.Ordinal)) {
current.Committer.Parse(line);
} else if (string.IsNullOrEmpty(current.Subject)) {
current.Subject = line.Trim();
} else {
current.Message += (line.Trim() + "\n");
}
});
if (current != null) {
current.Message = current.Message.TrimEnd();
commits.Add(current);
}
if (!findHead && commits.Count > 0) {
var startInfo = new ProcessStartInfo();
startInfo.FileName = Preference.Instance.GitExecutable;
startInfo.Arguments = $"merge-base --is-ancestor {commits[0].SHA} HEAD";
startInfo.WorkingDirectory = repo.Path;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = false;
startInfo.RedirectStandardError = false;
var proc = new Process() { StartInfo = startInfo };
proc.Start();
proc.WaitForExit();
commits[0].IsMerged = proc.ExitCode == 0;
proc.Close();
}
return commits;
}
/// <summary>
/// Get changed file list.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
var changes = new List<Change>();
var regex = new Regex(@"^[MADRC]\d*\s*.*$");
var errs = repo.RunCommand($"show --name-status {SHA}", line => {
if (!regex.IsMatch(line)) return;
var change = Change.Parse(line, true);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Get revision files.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<string> GetFiles(Repository repo) {
var files = new List<string>();
var errs = repo.RunCommand($"ls-tree --name-only -r {SHA}", line => {
files.Add(line);
});
if (errs != null) App.RaiseError(errs);
return files;
}
/// <summary>
/// Get file content.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <returns></returns>
public string GetTextFileContent(Repository repo, string file) {
var data = new List<string>();
var isBinary = false;
var count = 0;
var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => {
if (isBinary) return;
count++;
if (data.Count >= 1000) return;
if (line.IndexOf('\0') >= 0) {
isBinary = true;
data.Clear();
data.Add("BINARY FILE PREVIEW NOT SUPPORTED!");
return;
}
data.Add(line);
});
if (!isBinary && count > 1000) {
data.Add("...");
data.Add($"Total {count} lines. Hide {count-1000} lines.");
}
if (errs != null) App.RaiseError(errs);
return string.Join("\n", data);
}
private static void ParseSHA(Commit commit, string data) {
var decoratorStart = data.IndexOf('(');
if (decoratorStart < 0) {
commit.SHA = data.Trim();
return;
}
commit.SHA = data.Substring(0, decoratorStart).Trim();
var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var sub in subs) {
var d = sub.Trim();
if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.Tag,
Name = d.Substring(15).Trim()
});
} else if (d.EndsWith("/HEAD")) {
continue;
} else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) {
commit.IsHEAD = true;
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.CurrentBranchHead,
Name = d.Substring(19).Trim()
});
} else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.LocalBranchHead,
Name = d.Substring(11).Trim()
});
} else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) {
commit.Decorators.Add(new Decorator() {
Type = DecoratorType.RemoteBranchHead,
Name = d.Substring(13).Trim()
});
}
}
}
}
}

View file

@ -0,0 +1,21 @@
namespace SourceGit.Git {
/// <summary>
/// Decorator type.
/// </summary>
public enum DecoratorType {
None,
CurrentBranchHead,
LocalBranchHead,
RemoteBranchHead,
Tag,
}
/// <summary>
/// Commit decorator.
/// </summary>
public class Decorator {
public DecoratorType Type { get; set; }
public string Name { get; set; }
}
}

202
SourceGit/Git/MergeTool.cs Normal file
View file

@ -0,0 +1,202 @@
using Microsoft.Win32;
using SourceGit.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// External merge tool
/// </summary>
public class MergeTool {
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Executable file name.
/// </summary>
public string ExecutableName { get; set; }
/// <summary>
/// Command line parameter.
/// </summary>
public string Parameter { get; set; }
/// <summary>
/// Auto finder.
/// </summary>
public Func<string> Finder { get; set; }
/// <summary>
/// Is this merge tool configured.
/// </summary>
public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName);
/// <summary>
/// Supported merge tools.
/// </summary>
public static List<MergeTool> Supported = new List<MergeTool>() {
new MergeTool("--", "", "", FindInvalid),
new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge),
new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare),
new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3),
new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge),
new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge),
new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge),
new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode),
};
/// <summary>
/// Finder for invalid merge tool.
/// </summary>
/// <returns></returns>
public static string FindInvalid() {
return "--";
}
/// <summary>
/// Find araxis merge tool install path.
/// </summary>
/// <returns></returns>
public static string FindAraxisMerge() {
var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find kdiff3.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindKDiff3() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext");
if (kdiff == null) return "";
return kdiff.GetValue("diffcommand") as string;
}
/// <summary>
/// Finder for p4merge
/// </summary>
/// <returns></returns>
public static string FindP4Merge() {
var path = @"C:\Program Files\Perforce\p4merge.exe";
if (File.Exists(path)) return path;
return "";
}
/// <summary>
/// Find BComp.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindBCompare() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare");
if (bc == null) return "";
var exec = bc.GetValue("ExePath") as string;
var dir = Path.GetDirectoryName(exec);
return $"{dir}\\BComp.exe";
}
/// <summary>
/// Find TortoiseMerge.exe by registry.
/// </summary>
/// <returns></returns>
public static string FindTortoiseMerge() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN");
if (tortoiseSVN == null) return "";
return tortoiseSVN.GetValue("TMergePath") as string;
}
/// <summary>
/// Find vsDiffMerge.exe.
/// </summary>
/// <returns></returns>
public static string FindVSMerge() {
var dir = @"C:\Program Files (x86)\Microsoft Visual Studio";
if (Directory.Exists($"{dir}\\2019")) {
dir += "\\2019";
} else if (Directory.Exists($"{dir}\\2017")) {
dir += "\\2017";
} else {
return "";
}
if (Directory.Exists($"{dir}\\Community")) {
dir += "\\Community";
} else if (Directory.Exists($"{dir}\\Enterprise")) {
dir += "\\Enterprise";
} else if (Directory.Exists($"{dir}\\Professional")) {
dir += "\\Professional";
} else {
return "";
}
return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe";
}
/// <summary>
/// Find VSCode executable file path.
/// </summary>
/// <returns></returns>
public static string FindVSCode() {
var root = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1");
if (vscode != null) {
return vscode.GetValue("DisplayIcon") as string;
}
return "";
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="name"></param>
/// <param name="exe"></param>
/// <param name="param"></param>
/// <param name="finder"></param>
public MergeTool(string name, string exe, string param, Func<string> finder) {
Name = name;
ExecutableName = exe;
Parameter = param;
Finder = finder;
}
}
}

296
SourceGit/Git/Preference.cs Normal file
View file

@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace SourceGit.Git {
/// <summary>
/// User's preference settings. Serialized to
/// </summary>
public class Preference {
/// <summary>
/// Group(Virtual folder) for watched repositories.
/// </summary>
public class Group {
/// <summary>
/// Unique ID of this group.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Parent ID.
/// </summary>
public string ParentId { get; set; }
/// <summary>
/// Cache UI IsExpended status.
/// </summary>
public bool IsExpended { get; set; }
}
#region STATICS
/// <summary>
/// Storage path for Preference.
/// </summary>
private static readonly string SAVE_PATH = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"SourceGit",
"preference.xml");
/// <summary>
/// Runtime singleton instance.
/// </summary>
private static Preference instance = null;
public static Preference Instance {
get {
if (instance == null) Load();
return instance;
}
set {
instance = value;
}
}
#endregion
#region SETTING_GIT
/// <summary>
/// Git executable file path.
/// </summary>
public string GitExecutable { get; set; }
/// <summary>
/// Default clone directory.
/// </summary>
public string GitDefaultCloneDir { get; set; }
#endregion
#region SETTING_MERGE_TOOL
/// <summary>
/// Selected merge tool.
/// </summary>
public int MergeTool { get; set; } = 0;
/// <summary>
/// Executable file path for merge tool.
/// </summary>
public string MergeExecutable { get; set; } = "--";
#endregion
#region SETTING_UI
/// <summary>
/// Main window's width
/// </summary>
public double UIMainWindowWidth { get; set; }
/// <summary>
/// Main window's height
/// </summary>
public double UIMainWindowHeight { get; set; }
/// <summary>
/// Use light color theme.
/// </summary>
public bool UIUseLightTheme { get; set; }
/// <summary>
/// Show/Hide tags' list view.
/// </summary>
public bool UIShowTags { get; set; } = true;
/// <summary>
/// Use horizontal layout for histories.
/// </summary>
public bool UIUseHorizontalLayout { get; set; }
/// <summary>
/// Use list instead of tree in unstaged view
/// </summary>
public bool UIUseListInUnstaged { get; set; }
/// <summary>
/// Use list instead of tree in staged view.
/// </summary>
public bool UIUseListInStaged { get; set; }
/// <summary>
/// Use list instead of tree in change view.
/// </summary>
public bool UIUseListInChanges { get; set; }
#endregion
#region SETTING_REPOS
/// <summary>
/// Groups for repositories.
/// </summary>
public List<Group> Groups { get; set; } = new List<Group>();
/// <summary>
/// Watched repositories.
/// </summary>
public List<Repository> Repositories { get; set; } = new List<Git.Repository>();
#endregion
#region METHODS_LOAD_SAVE
/// <summary>
/// Load preference from disk.
/// </summary>
/// <returns>Loaded preference instance.</returns>
public static void Load() {
if (!File.Exists(SAVE_PATH)) {
instance = new Preference();
return;
}
var stream = new FileStream(SAVE_PATH, FileMode.Open);
var reader = new XmlSerializer(typeof(Preference));
instance = (Preference)reader.Deserialize(stream);
stream.Close();
}
/// <summary>
/// Save current preference into disk.
/// </summary>
public static void Save() {
if (instance == null) return;
var dir = Path.GetDirectoryName(SAVE_PATH);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var stream = new FileStream(SAVE_PATH, FileMode.Create);
var writer = new XmlSerializer(typeof(Preference));
writer.Serialize(stream, instance);
stream.Flush();
stream.Close();
}
#endregion
#region METHODS_ON_GROUP
/// <summary>
/// Add new group(virtual folder).
/// </summary>
/// <param name="name">Display name.</param>
/// <param name="parentId">Parent group ID.</param>
/// <returns>Added group instance.</returns>
public Group AddGroup(string name, string parentId) {
var group = new Group() {
Name = name,
Id = Guid.NewGuid().ToString(),
ParentId = parentId,
IsExpended = false,
};
Groups.Add(group);
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
return group;
}
/// <summary>
/// Find group by ID.
/// </summary>
/// <param name="id">Unique ID</param>
/// <returns>Founded group's instance.</returns>
public Group FindGroup(string id) {
foreach (var group in Groups) {
if (group.Id == id) return group;
}
return null;
}
/// <summary>
/// Rename group.
/// </summary>
/// <param name="id">Unique ID</param>
/// <param name="newName">New name.</param>
public void RenameGroup(string id, string newName) {
foreach (var group in Groups) {
if (group.Id == id) {
group.Name = newName;
break;
}
}
Groups.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a group.
/// </summary>
/// <param name="id">Unique ID</param>
public void RemoveGroup(string id) {
int removedIdx = -1;
for (int i = 0; i < Groups.Count; i++) {
if (Groups[i].Id == id) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Groups.RemoveAt(removedIdx);
}
#endregion
#region METHODS_ON_REPOS
/// <summary>
/// Add repository.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="groupId">Group's ID</param>
/// <returns>Added repository instance.</returns>
public Repository AddRepository(string path, string groupId) {
var repo = FindRepository(path);
if (repo != null) return repo;
var dir = new DirectoryInfo(path);
repo = new Repository() {
Path = dir.FullName,
Name = dir.Name,
GroupId = groupId,
LastOpenTime = 0,
};
Repositories.Add(repo);
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
return repo;
}
/// <summary>
/// Find repository by path.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <returns>Founded repository instance.</returns>
public Repository FindRepository(string path) {
var dir = new DirectoryInfo(path);
foreach (var repo in Repositories) {
if (repo.Path == dir.FullName) return repo;
}
return null;
}
/// <summary>
/// Change a repository's display name in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
/// <param name="newName">New name</param>
public void RenameRepository(string path, string newName) {
var repo = FindRepository(path);
if (repo == null) return;
repo.Name = newName;
Repositories.Sort((l, r) => l.Name.CompareTo(r.Name));
}
/// <summary>
/// Remove a repository in RepositoryManager.
/// </summary>
/// <param name="path">Local storage path.</param>
public void RemoveRepository(string path) {
var dir = new DirectoryInfo(path);
var removedIdx = -1;
for (int i = 0; i < Repositories.Count; i++) {
if (Repositories[i].Path == dir.FullName) {
removedIdx = i;
break;
}
}
if (removedIdx >= 0) Repositories.RemoveAt(removedIdx);
}
#endregion
}
}

97
SourceGit/Git/Remote.cs Normal file
View file

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git remote
/// </summary>
public class Remote {
private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$");
/// <summary>
/// Name of this remote
/// </summary>
public string Name { get; set; }
/// <summary>
/// URL
/// </summary>
public string URL { get; set; }
/// <summary>
/// Parsing remote
/// </summary>
/// <param name="repo">Repository</param>
/// <returns></returns>
public static List<Remote> Load(Repository repo) {
var remotes = new List<Remote>();
var added = new List<string>();
repo.RunCommand("remote -v", data => {
var match = FORMAT.Match(data);
if (!match.Success) return;
var remote = new Remote() {
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (added.Contains(remote.Name)) return;
added.Add(remote.Name);
remotes.Add(remote);
});
return remotes;
}
/// <summary>
/// Add new remote
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public static void Add(Repository repo, string name, string url) {
var errs = repo.RunCommand($"remote add {name} {url}", null);
if (errs != null) {
App.RaiseError(errs);
} else {
repo.Fetch(null, true, null);
}
}
/// <summary>
/// Delete remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="remote"></param>
public static void Delete(Repository repo, string remote) {
var errs = repo.RunCommand($"remote remove {remote}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Edit remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="url"></param>
public void Edit(Repository repo, string name, string url) {
string errs = null;
if (name != Name) {
errs = repo.RunCommand($"remote rename {Name} {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
}
if (url != URL) {
errs = repo.RunCommand($"remote set-url {name} {url}", null);
if (errs != null) App.RaiseError(errs);
}
}
}
}

1058
SourceGit/Git/Repository.cs Normal file

File diff suppressed because it is too large Load diff

101
SourceGit/Git/Stash.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SourceGit.Git {
/// <summary>
/// Git stash
/// </summary>
public class Stash {
/// <summary>
/// SHA for this stash
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Author
/// </summary>
public User Author { get; set; } = new User();
/// <summary>
/// Message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Stash push.
/// </summary>
/// <param name="repo"></param>
/// <param name="includeUntracked"></param>
/// <param name="message"></param>
/// <param name="files"></param>
public static void Push(Repository repo, bool includeUntracked, string message, List<string> files) {
string specialFiles = "";
if (files.Count > 0) {
specialFiles = " --";
foreach (var f in files) specialFiles += $" \"{f}\"";
}
string args = "stash push ";
if (includeUntracked) args += "-u ";
if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" ";
var errs = repo.RunCommand(args + specialFiles, null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Get changed file list in this stash.
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public List<Change> GetChanges(Repository repo) {
List<Change> changes = new List<Change>();
var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => {
var change = Change.Parse(line);
if (change != null) changes.Add(change);
});
if (errs != null) App.RaiseError(errs);
return changes;
}
/// <summary>
/// Apply stash.
/// </summary>
/// <param name="repo"></param>
public void Apply(Repository repo) {
var errs = repo.RunCommand($"stash apply -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Pop stash
/// </summary>
/// <param name="repo"></param>
public void Pop(Repository repo) {
var errs = repo.RunCommand($"stash pop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
/// <summary>
/// Drop stash
/// </summary>
/// <param name="repo"></param>
public void Drop(Repository repo) {
var errs = repo.RunCommand($"stash drop -q {Name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

118
SourceGit/Git/Tag.cs Normal file
View file

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git tag.
/// </summary>
public class Tag {
private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)");
/// <summary>
/// SHA
/// </summary>
public string SHA { get; set; }
/// <summary>
/// Display name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Enable filter in log histories.
/// </summary>
public bool IsFiltered { get; set; }
/// <summary>
/// Load all tags
/// </summary>
/// <param name="repo"></param>
/// <returns></returns>
public static List<Tag> Load(Repository repo) {
var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags";
var tags = new List<Tag>();
repo.RunCommand(args, line => {
var match = FORMAT.Match(line);
if (!match.Success) return;
var name = match.Groups[1].Value;
var commit = match.Groups[2].Value;
var dereference = match.Groups[3].Value;
if (string.IsNullOrEmpty(dereference)) {
tags.Add(new Tag() {
Name = name,
SHA = commit,
});
} else {
tags.Add(new Tag() {
Name = name,
SHA = dereference,
});
}
});
return tags;
}
/// <summary>
/// Add new tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="startPoint"></param>
/// <param name="message"></param>
public static void Add(Repository repo, string name, string startPoint, string message) {
var args = $"tag -a {name} {startPoint} ";
if (!string.IsNullOrEmpty(message)) {
string temp = Path.GetTempFileName();
File.WriteAllText(temp, message);
args += $"-F \"{temp}\"";
} else {
args += $"-m {name}";
}
var errs = repo.RunCommand(args, null);
if (errs != null) App.RaiseError(errs);
else repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Delete tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="push"></param>
public static void Delete(Repository repo, string name, bool push) {
var errs = repo.RunCommand($"tag --delete {name}", null);
if (errs != null) {
App.RaiseError(errs);
return;
}
if (push) {
var remotes = repo.Remotes();
foreach (var r in remotes) {
repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null);
}
}
repo.OnCommitsChanged?.Invoke();
}
/// <summary>
/// Push tag to remote.
/// </summary>
/// <param name="repo"></param>
/// <param name="name"></param>
/// <param name="remote"></param>
public static void Push(Repository repo, string name, string remote) {
var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null);
if (errs != null) App.RaiseError(errs);
}
}
}

42
SourceGit/Git/User.cs Normal file
View file

@ -0,0 +1,42 @@
using System;
using System.Text.RegularExpressions;
namespace SourceGit.Git {
/// <summary>
/// Git user.
/// </summary>
public class User {
private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+");
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Email.
/// </summary>
public string Email { get; set; } = "";
/// <summary>
/// Operation time.
/// </summary>
public string Time { get; set; } = "";
/// <summary>
/// Parse user from raw string.
/// </summary>
/// <param name="data">Raw string</param>
public void Parse(string data) {
var match = FORMAT.Match(data);
if (!match.Success) return;
var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(int.Parse(match.Groups[3].Value));
Name = match.Groups[1].Value;
Email = match.Groups[2].Value;
Time = time.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
}
}

View file

@ -0,0 +1,274 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Tools to parse commit graph.
/// </summary>
public class CommitGraphMaker {
/// <summary>
/// Sizes
/// </summary>
public static readonly double UNIT_WIDTH = 12;
public static readonly double HALF_WIDTH = 6;
public static readonly double DOUBLE_WIDTH = 24;
public static readonly double UNIT_HEIGHT = 24;
public static readonly double HALF_HEIGHT = 12;
/// <summary>
/// Colors
/// </summary>
public static Brush[] Colors = new Brush[] {
Brushes.Orange,
Brushes.ForestGreen,
Brushes.Gold,
Brushes.Magenta,
Brushes.Red,
Brushes.Gray,
Brushes.Turquoise,
Brushes.Olive,
};
/// <summary>
/// Helpers to draw lines.
/// </summary>
public class LineHelper {
private double lastX = 0;
private double lastY = 0;
/// <summary>
/// Parent commit id.
/// </summary>
public string Next { get; set; }
/// <summary>
/// Is merged into this tree.
/// </summary>
public bool IsMerged { get; set; }
/// <summary>
/// Points in line
/// </summary>
public List<Point> Points { get; set; }
/// <summary>
/// Brush to draw line
/// </summary>
public Brush Brush { get; set; }
/// <summary>
/// Current horizontal offset.
/// </summary>
public double HorizontalOffset => lastX;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="nextCommitId">Parent commit id</param>
/// <param name="isMerged">Is merged in tree</param>
/// <param name="colorIdx">Color index</param>
/// <param name="startPoint">Start point</param>
public LineHelper(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) {
Next = nextCommitId;
IsMerged = isMerged;
Points = new List<Point>() { startPoint };
Brush = Colors[colorIdx % Colors.Length];
lastX = startPoint.X;
lastY = startPoint.Y;
}
/// <summary>
/// Line to.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="isEnd"></param>
public void AddPoint(double x, double y, bool isEnd = false) {
if (x > lastX) {
Points.Add(new Point(lastX, lastY));
Points.Add(new Point(x, y - HALF_HEIGHT));
} else if (x < lastX) {
Points.Add(new Point(lastX, lastY + HALF_HEIGHT));
Points.Add(new Point(x, y));
}
lastX = x;
lastY = y;
if (isEnd) {
var last = Points.Last();
if (last.X != lastX || last.Y != lastY) Points.Add(new Point(lastX, lastY));
}
}
}
/// <summary>
/// Short link between two commits.
/// </summary>
public struct ShortLink {
public Point Start;
public Point Control;
public Point End;
public Brush Brush;
}
/// <summary>
/// Dot
/// </summary>
public struct Dot {
public double X;
public double Y;
public Brush Color;
}
/// <summary>
/// Independent lines in graph
/// </summary>
public List<LineHelper> Lines { get; set; } = new List<LineHelper>();
/// <summary>
/// Short links.
/// </summary>
public List<ShortLink> Links { get; set; } = new List<ShortLink>();
/// <summary>
/// All dots.
/// </summary>
public List<Dot> Dots { get; set; } = new List<Dot>();
/// <summary>
/// Highlight commit id.
/// </summary>
public string Highlight { get; set; }
/// <summary>
/// Parse commits.
/// </summary>
/// <param name="commits"></param>
/// <returns></returns>
public static CommitGraphMaker Parse(List<Git.Commit> commits) {
CommitGraphMaker maker = new CommitGraphMaker();
List<LineHelper> unsolved = new List<LineHelper>();
List<LineHelper> ended = new List<LineHelper>();
Dictionary<string, LineHelper> currentMap = new Dictionary<string, LineHelper>();
double offsetY = -HALF_HEIGHT;
int colorIdx = 0;
for (int i = 0; i < commits.Count; i++) {
Git.Commit commit = commits[i];
LineHelper major = null;
bool isMerged = commit.IsHEAD || commit.IsMerged;
int oldCount = unsolved.Count;
// 更新Y坐标
offsetY += UNIT_HEIGHT;
// 找到当前的分支的HEAD用于默认选中
if (maker.Highlight == null && commit.IsHEAD) {
maker.Highlight = commit.SHA;
}
// 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合)
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 (!currentMap.ContainsKey(major.Next)) currentMap.Add(major.Next, major);
} else {
major.Next = "ENDED";
}
major.AddPoint(offsetX, offsetY);
} else {
ended.Add(l);
}
isMerged = isMerged || l.IsMerged;
} else {
if (!currentMap.ContainsKey(l.Next)) currentMap.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.AddPoint(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.HorizontalOffset;
position.Y = offsetY;
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = major.Brush });
} else {
maker.Dots.Add(new Dot() { X = position.X - 3, Y = position.Y - 3, Color = Brushes.Orange });
}
// 处理本提交的其他依赖
for (int j = 1; j < commit.Parents.Count; j++) {
var parent = commit.Parents[j];
if (currentMap.ContainsKey(parent)) {
var l = currentMap[parent];
var link = new ShortLink();
link.Start = position;
link.End = new Point(l.HorizontalOffset, offsetY + HALF_HEIGHT);
link.Control = new Point(link.End.X, link.Start.Y);
link.Brush = l.Brush;
maker.Links.Add(link);
} else {
offsetX += UNIT_WIDTH;
unsolved.Add(new LineHelper(commit.Parents[j], isMerged, colorIdx, position));
colorIdx++;
}
}
// 处理已终止的线
foreach (var l in ended) {
l.AddPoint(position.X, position.Y, true);
maker.Lines.Add(l);
unsolved.Remove(l);
}
// 加入本次提交
commit.IsMerged = isMerged;
commit.GraphOffset = System.Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH);
// 清理临时数据
ended.Clear();
currentMap.Clear();
}
// 处理尚未终结的线
for (int i = 0; i < unsolved.Count; i++) {
var path = unsolved[i];
path.AddPoint((i + 0.5) * UNIT_WIDTH, (commits.Count - 0.5) * UNIT_HEIGHT, true);
maker.Lines.Add(path);
}
unsolved.Clear();
// 处理默认选中异常
if (maker.Highlight == null && commits.Count > 0) {
maker.Highlight = commits[0].SHA;
}
return maker;
}
}
}

View file

@ -0,0 +1,143 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Attached properties to TextBox.
/// </summary>
public static class TextBoxHelper {
/// <summary>
/// Placeholder property
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder",
typeof(string),
typeof(TextBoxHelper),
new PropertyMetadata(string.Empty, OnPlaceholderChanged));
/// <summary>
/// Vertical alignment for placeholder.
/// </summary>
public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached(
"PlaceholderBaseline",
typeof(AlignmentY),
typeof(TextBoxHelper),
new PropertyMetadata(AlignmentY.Center));
/// <summary>
/// Property to store generated placeholder brush.
/// </summary>
public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached(
"PlaceholderBrush",
typeof(Brush),
typeof(TextBoxHelper),
new PropertyMetadata(Brushes.Transparent));
/// <summary>
/// Triggered when placeholder changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var textBox = d as TextBox;
if (textBox != null) textBox.Loaded += OnTextLoaded;
}
/// <summary>
/// Setter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholder(UIElement element, string value) {
element.SetValue(PlaceholderProperty, value);
}
/// <summary>
/// Getter for Placeholder property
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static string GetPlaceholder(UIElement element) {
return (string)element.GetValue(PlaceholderProperty);
}
/// <summary>
/// Setter for PlaceholderBaseline property
/// </summary>
/// <param name="element"></param>
/// <param name="align"></param>
public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) {
element.SetValue(PlaceholderBaselineProperty, align);
}
/// <summary>
/// Setter for PlaceholderBaseline property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static AlignmentY GetPlaceholderBaseline(UIElement element) {
return (AlignmentY)element.GetValue(PlaceholderBaselineProperty);
}
/// <summary>
/// Setter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetPlaceholderBrush(UIElement element, Brush value) {
element.SetValue(PlaceholderBrushProperty, value);
}
/// <summary>
/// Getter for PlaceholderBrush property.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static Brush GetPlaceholderBrush(UIElement element) {
return (Brush)element.GetValue(PlaceholderBrushProperty);
}
/// <summary>
/// Set placeholder as background when TextBox was loaded.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTextLoaded(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (textBox == null) return;
Label placeholder = new Label();
placeholder.Content = textBox.GetValue(PlaceholderProperty);
VisualBrush brush = new VisualBrush();
brush.AlignmentX = AlignmentX.Left;
brush.AlignmentY = GetPlaceholderBaseline(textBox);
brush.TileMode = TileMode.None;
brush.Stretch = Stretch.None;
brush.Opacity = 0.3;
brush.Visual = placeholder;
textBox.SetValue(PlaceholderBrushProperty, brush);
textBox.Background = brush;
textBox.TextChanged += OnTextChanged;
OnTextChanged(textBox, null);
}
/// <summary>
/// Dynamically hide/show placeholder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTextChanged(object sender, RoutedEventArgs e) {
var textBox = sender as TextBox;
if (string.IsNullOrEmpty(textBox.Text)) {
textBox.Background = textBox.GetValue(PlaceholderBrushProperty) as Brush;
} else {
textBox.Background = Brushes.Transparent;
}
}
}
}

View file

@ -0,0 +1,329 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Helpers {
/// <summary>
/// Helper class to enable multi-selection of TreeView
/// </summary>
public static class TreeViewHelper {
/// <summary>
/// Definition of EnableMultiSelection property.
/// </summary>
public static readonly DependencyProperty EnableMultiSelectionProperty =
DependencyProperty.RegisterAttached(
"EnableMultiSelection",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged));
/// <summary>
/// Getter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetEnableMultiSelection(DependencyObject obj) {
return (bool)obj.GetValue(EnableMultiSelectionProperty);
}
/// <summary>
/// Setter of EnableMultiSelection
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetEnableMultiSelection(DependencyObject obj, bool value) {
obj.SetValue(EnableMultiSelectionProperty, value);
}
/// <summary>
/// Definition of SelectedItems
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems",
typeof(ObservableCollection<TreeViewItem>),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Getter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static ObservableCollection<TreeViewItem> GetSelectedItems(DependencyObject obj) {
return (ObservableCollection<TreeViewItem>)obj.GetValue(SelectedItemsProperty);
}
/// <summary>
/// Setter of SelectedItems
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetSelectedItems(DependencyObject obj, ObservableCollection<TreeViewItem> value) {
obj.SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Definition of IsChecked property.
/// </summary>
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.RegisterAttached(
"IsChecked",
typeof(bool),
typeof(TreeViewHelper),
new FrameworkPropertyMetadata(false));
/// <summary>
/// Getter of IsChecked Property.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetIsChecked(DependencyObject obj) {
return (bool)obj.GetValue(IsCheckedProperty);
}
/// <summary>
/// Setter of IsChecked property
/// </summary>
/// <param name="obj"></param>
/// <param name="value"></param>
public static void SetIsChecked(DependencyObject obj, bool value) {
obj.SetValue(IsCheckedProperty, value);
}
/// <summary>
/// Definition of MultiSelectionChangedEvent
/// </summary>
public static readonly RoutedEvent MultiSelectionChangedEvent =
EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper));
/// <summary>
/// Add handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Remove handler for MultiSelectionChanged event.
/// </summary>
/// <param name="d"></param>
/// <param name="handler"></param>
public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) {
var tree = d as TreeView;
if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler);
}
/// <summary>
/// Select all items in tree.
/// </summary>
/// <param name="tree"></param>
public static void SelectWholeTree(TreeView tree) {
var selected = GetSelectedItems(tree);
selected.Clear();
SelectAll(selected, tree);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Selected one item by DataContext
/// </summary>
/// <param name="tree"></param>
/// <param name="obj"></param>
public static void SelectOneByContext(TreeView tree, object obj) {
var item = FindTreeViewItemByDataContext(tree, obj);
if (item != null) {
var selected = GetSelectedItems(tree);
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
}
/// <summary>
/// Unselect the whole tree.
/// </summary>
/// <param name="tree"></param>
public static void UnselectTree(TreeView tree) {
var selected = GetSelectedItems(tree);
if (selected.Count == 0) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Hooks when EnableMultiSelection changed.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var tree = d as TreeView;
if (tree != null && (bool)e.NewValue) {
tree.SetValue(SelectedItemsProperty, new ObservableCollection<TreeViewItem>());
tree.PreviewMouseDown += OnTreeMouseDown;
}
}
/// <summary>
/// Preview mouse button select.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnTreeMouseDown(object sender, MouseButtonEventArgs e) {
var tree = sender as TreeView;
if (tree == null) return;
var hit = VisualTreeHelper.HitTest(tree, e.GetPosition(tree));
if (hit == null || hit.VisualHit is null) return;
var item = FindTreeViewItem(hit.VisualHit as UIElement);
if (item == null) return;
var selected = GetSelectedItems(tree);
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
if (GetIsChecked(item)) {
selected.Remove(item);
item.SetValue(IsCheckedProperty, false);
} else {
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
} else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && selected.Count > 0) {
var last = 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(selected, tree, item, last);
} else {
SelectRange(selected, tree, last, item);
}
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else if (e.RightButton == MouseButtonState.Pressed) {
if (GetIsChecked(item)) return;
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
} else {
foreach (var old in selected) old.SetValue(IsCheckedProperty, false);
selected.Clear();
selected.Add(item);
item.SetValue(IsCheckedProperty, true);
}
tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent));
}
/// <summary>
/// Find TreeViewItem by child element.
/// </summary>
/// <param name="item"></param>
/// <param name="child"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItem(DependencyObject child) {
if (child == null) return null;
if (child is TreeViewItem) return child as TreeViewItem;
if (child is TreeView) return null;
return FindTreeViewItem(VisualTreeHelper.GetParent(child));
}
/// <summary>
/// Find TreeViewItem by DataContext
/// </summary>
/// <param name="control"></param>
/// <param name="obj"></param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl control, object obj) {
if (control == null) return null;
if (control.DataContext == obj) return control as TreeViewItem;
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
var found = FindTreeViewItemByDataContext(child, obj);
if (found != null) return found;
}
return null;
}
/// <summary>
/// Select all items.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
private static void SelectAll(ObservableCollection<TreeViewItem> selected, ItemsControl control) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
SelectAll(selected, child);
}
}
/// <summary>
/// Select range items between given.
/// </summary>
/// <param name="selected"></param>
/// <param name="control"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="started"></param>
private static int SelectRange(ObservableCollection<TreeViewItem> selected, ItemsControl control, TreeViewItem from, TreeViewItem to, int matches = 0) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (matches == 1) {
if (child == to) return 2;
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
if (TryEndRangeSelection(selected, child, to)) return 2;
} else if (child == from) {
matches = 1;
if (TryEndRangeSelection(selected, child, to)) return 2;
} else {
matches = SelectRange(selected, child, from, to, matches);
if (matches == 2) return 2;
}
}
return matches;
}
private static bool TryEndRangeSelection(ObservableCollection<TreeViewItem> selected, TreeViewItem control, TreeViewItem end) {
for (int i = 0; i < control.Items.Count; i++) {
var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (child == null) continue;
if (child == end) {
return true;
} else {
selected.Add(child);
child.SetValue(IsCheckedProperty, true);
var ended = TryEndRangeSelection(selected, child, end);
if (ended) return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,124 @@
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Controls;
namespace SourceGit.Helpers {
/// <summary>
/// Validate clone folder.
/// </summary>
public class CloneFolderRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badPath = "EXISTS and FULL ACCESS CONTROL needed";
var path = value as string;
return Directory.Exists(path) ? ValidationResult.ValidResult : new ValidationResult(false, badPath);
}
}
/// <summary>
/// Validate git remote URL
/// </summary>
public class RemoteUriRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var badUrl = "Remote git URL not supported";
return Git.Repository.IsValidUrl(value as string) ? ValidationResult.ValidResult : new ValidationResult(false, badUrl);
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class RemoteNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var remotes = Repo.Remotes();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Remote name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for remote. Regex: ^[\\w\\-\\.]+$");
foreach (var t in remotes) {
if (t.Name == name) {
return new ValidationResult(false, $"Remote '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate branch name.
/// </summary>
public class BranchNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public string Prefix { get; set; } = "";
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-/\.]+$");
var name = value as string;
var branches = Repo.Branches();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Branch name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for branch. Regex: ^[\\w\\-/\\.]+$");
name = Prefix + name;
foreach (var b in branches) {
if (b.Name == name) {
return new ValidationResult(false, $"Branch '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Validate tag name.
/// </summary>
public class TagNameRule : ValidationRule {
public Git.Repository Repo { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\.]+$");
var name = value as string;
var tags = Repo.Tags();
if (string.IsNullOrEmpty(name)) return new ValidationResult(false, "Tag name can NOT be null");
if (!regex.IsMatch(name)) return new ValidationResult(false, $"Bad name for tag. Regex: ^[\\w\\-\\.]+$");
foreach (var t in tags) {
if (t.Name == name) {
return new ValidationResult(false, $"Tag '{name}' already exists");
}
}
return ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for commit subject.
/// </summary>
public class CommitSubjectRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var subject = value as string;
return string.IsNullOrWhiteSpace(subject) ? new ValidationResult(false, "Commit subject can NOT be empty") : ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for patch file.
/// </summary>
public class PatchFileRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var path = value as string;
var succ = !string.IsNullOrEmpty(path) && File.Exists(path);
return !succ ? new ValidationResult(false, "Invalid path for patch file") : ValidationResult.ValidResult;
}
}
}

View file

@ -0,0 +1,3 @@
using System.Windows;
[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]

View file

@ -0,0 +1,22 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Border.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Button.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/CheckBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ComboBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ContextMenu.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/DataGrid.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/HyperLink.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Label.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ListView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Path.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/RadioButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollBar.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ScrollViewer.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TabControl.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TextBox.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/ToggleButton.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/Tooltip.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Resources/Styles/TreeView.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -0,0 +1,59 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Geometry x:Key="Icon.Git">M1004.824 466.4L557.72 19.328c-25.728-25.76-67.488-25.76-93.28 0L360.568 123.2l78.176 78.176c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96 0 14.816-3.36 28.864-9.376 41.376l127.968 127.968c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96s-42.976 96-96 96-96-42.976-96-96c0-14.816 3.36-28.864 9.376-41.376L521.496 374.624a88.837 88.837 0 0 1-9.376 3.872v266.976c37.28 13.184 64 48.704 64 90.528 0 53.024-42.976 96-96 96s-96-42.976-96-96c0-41.792 26.72-77.344 64-90.528V378.496c-37.28-13.184-64-48.704-64-90.528 0-14.816 3.36-28.864 9.376-41.376l-78.176-78.176L19.416 464.288c-25.76 25.792-25.76 67.52 0 93.28l447.136 447.072c25.728 25.76 67.488 25.76 93.28 0l444.992-444.992c25.76-25.76 25.76-67.552 0-93.28z</Geometry>
<Geometry x:Key="Icon.Minimize">F1M0,6L0,9 9,9 9,6 0,6z</Geometry>
<Geometry x:Key="Icon.Maximize">F1M0,0L0,9 9,9 9,0 0,0 0,3 8,3 8,8 1,8 1,3z</Geometry>
<Geometry x:Key="Icon.Restore">F1M0,10L0,3 3,3 3,0 10,0 10,2 4,2 4,3 7,3 7,6 6,6 6,5 1,5 1,10z M1,10L7,10 7,7 10,7 10,2 9,2 9,6 6,6 6,9 1,9z</Geometry>
<Geometry x:Key="Icon.Close">M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z</Geometry>
<Geometry x:Key="Icon.Check">M512 597.33333332m-1.26648097 0a1.26648097 1.26648097 0 1 0 2.53296194 0 1.26648097 1.26648097 0 1 0-2.53296194 0ZM809.691429 392.777143L732.16 314.514286 447.634286 599.771429 292.571429 443.977143 214.308571 521.508571l155.794286 155.794286 77.531429 77.531429 362.057143-362.057143z</Geometry>
<Geometry x:Key="Icon.Loading">M511.680999 0C233.071131 0 6.524722 222.580887 0.12872 499.655715 6.013042 257.886821 189.834154 63.960025 415.740962 63.960025c229.61649 0 415.740162 200.450718 415.740162 447.720175 0 52.958901 42.981137 95.940037 95.940038 95.940037s95.940037-42.981137 95.940037-95.940037c0-282.57539-229.104809-511.6802-511.6802-511.6802z m0 1023.3604c278.609869 0 505.156277-222.580887 511.55228-499.655715-5.884322 241.768894-189.705434 435.69569-415.612242 435.69569-229.61649 0-415.740162-200.450718-415.740163-447.720175 0-52.958901-42.981137-95.940037-95.940037-95.940038s-95.940037 42.981137-95.940037 95.940038c0 282.57539 229.104809 511.6802 511.680199 511.6802z</Geometry>
<Geometry x:Key="Icon.Search">M701.9062029 677.41589899L589.90712068 565.41681675a148.33953321 148.33953321 0 1 0-24.97646381 26.55648342L676.07895931 703.12160261z m-346.38891409-199.50786053a114.97681148 114.97681148 0 1 1 114.85527151 114.97681148A115.09835147 115.09835147 0 0 1 355.45651882 477.90803846z</Geometry>
<Geometry x:Key="Icon.Conflict">M352 64h320L960 352v320L672 960h-320L64 672v-320L352 64z m161.28 362.688L344.128 256 259.584 341.312 428.736 512l-169.152 170.688L344.128 768 513.28 597.312 682.432 768l84.544-85.312L597.824 512l169.152-170.688L682.432 256 513.28 426.688z</Geometry>
<Geometry x:Key="Icon.List">M51.2 204.8h102.4v102.4H51.2V204.8z m204.8 0h716.8v102.4H256V204.8zM51.2 460.8h102.4v102.4H51.2V460.8z m204.8 0h716.8v102.4H256V460.8z m-204.8 256h102.4v102.4H51.2v-102.4z m204.8 0h716.8v102.4H256v-102.4z</Geometry>
<Geometry x:Key="Icon.Tree">M912 737l0 150L362 887l0-100 0-50 0-150 0-150 0-150L112 287l0-150 450 0 0 150L412 287l0 150L912 437l0 150L412 587l0 150L912 737z</Geometry>
<Geometry x:Key="Icon.MoveUp">M868 545.5L536.1 163c-12.7-14.7-35.5-14.7-48.3 0L156 545.5c-4.5 5.2-0.8 13.2 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z</Geometry>
<Geometry x:Key="Icon.MoveDown">M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861c12.7 14.7 35.5 14.7 48.3 0L868 478.5c4.5-5.2 0.8-13.2-6-13.2z</Geometry>
<Geometry x:Key="Icon.StageSelected">M509.44 546.304l270.848-270.912 90.56 90.56-347.52 349.056-0.832-0.768-13.056 13.056-362.624-361.28 91.136-91.264z</Geometry>
<Geometry x:Key="Icon.StageAll">M256 224l1e-8 115.2L512 544l255.99999999-204.8 1e-8-115.2-256 204.80000001L256 224zM512 684.8l-256-204.8L256 595.2 512 800 768 595.2l0-115.2L512 684.8z</Geometry>
<Geometry x:Key="Icon.UnstageSelected">M169.5 831l342.8-341.9L855.1 831l105.3-105.3-448.1-448.1L64.2 725.7 169.5 831z</Geometry>
<Geometry x:Key="Icon.UnstageAll">M768 800V684.8L512 480 256 684.8V800l256-204.8L768 800zM512 339.2L768 544V428.8L512 224 256 428.8V544l256-204.8z</Geometry>
<Geometry x:Key="Icon.Preference">M64.2 180.3h418.2v120.6H64.2zM64.2 461.7h358.5v120.6H64.2zM64.2 723.1h418.2v120.6H64.2zM601.9 180.3h358.5v120.6H601.9zM482.4 119.9h179.2v241.3H482.4zM303.2 401.4h179.2v241.3H303.2zM482.4 662.8h179.2v241.3H482.4zM540.3 461.7h420.1v120.6H540.3zM601.9 723.1h358.5v120.6H601.9z</Geometry>
<Geometry x:Key="Icon.Info">M 38,19C 48.4934,19 57,27.5066 57,38C 57,48.4934 48.4934,57 38,57C 27.5066,57 19,48.4934 19,38C 19,27.5066 27.5066,19 38,19 Z M 33.25,33.25L 33.25,36.4167L 36.4166,36.4167L 36.4166,47.5L 33.25,47.5L 33.25,50.6667L 44.3333,50.6667L 44.3333,47.5L 41.1666,47.5L 41.1666,36.4167L 41.1666,33.25L 33.25,33.25 Z M 38.7917,25.3333C 37.48,25.3333 36.4167,26.3967 36.4167,27.7083C 36.4167,29.02 37.48,30.0833 38.7917,30.0833C 40.1033,30.0833 41.1667,29.02 41.1667,27.7083C 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333 Z</Geometry>
<Geometry x:Key="Icon.Folder">M64 864h896V288h-396.224a64 64 0 0 1-57.242667-35.376L460.224 160H64v704z m-64 32V128a32 32 0 0 1 32-32h448a32 32 0 0 1 28.624 17.690667L563.776 224H992a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H32a32 32 0 0 1-32-32z</Geometry>
<Geometry x:Key="Icon.Folder.Fill">M448 64l128 128h448v768H0V64z</Geometry>
<Geometry x:Key="Icon.Folder.Open">M832 960l192-512H192L0 960zM128 384L0 960V128h288l128 128h416v128z</Geometry>
<Geometry x:Key="Icon.Manager">M780.512477 870.01493 780.512477 512l89.502453 0-358.013907-358.01493L153.98507 512l89.503477 0 0 358.01493 179.007977 0L422.496523 735.759203c0-49.427736 40.075741-89.503477 89.503477-89.503477s89.503477 40.075741 89.503477 89.503477l0 134.255727L780.512477 870.01493z</Geometry>
<Geometry x:Key="Icon.Clone">M928 0c53.02 0 96 42.98 96 96v576c0 53.02-42.98 96-96 96H352c-53.02 0-96-42.98-96-96V96c0-53.02 42.98-96 96-96h576M352 832c-88.224 0-160-71.776-160-160V256H96c-53.02 0-96 42.98-96 96v576c0 53.02 42.98 96 96 96h576c53.02 0 96-42.98 96-96v-96H352z</Geometry>
<Geometry x:Key="Icon.Book">M384 576H320V512h64v64z m0-192H320v64h64V384z m0-128H320v64h64V256z m0-128H320v64h64V128z m512-64v768c0 35.2-28.8 64-64 64H512v128l-96-96L320 1024v-128H192c-35.2 0-64-28.8-64-64V64c0-35.2 28.8-64 64-64h640c35.2 0 64 28.8 64 64z m-64 640H192v128h128v-64h192v64h320v-128z m0-640H256v576h576V64z</Geometry>
<Geometry x:Key="Icon.Navigator">M989.866667 512L689.493333 802.133333 614.4 729.6 839.68 512 614.4 294.4 689.493333 221.866667z</Geometry>
<Geometry x:Key="Icon.File">M958.656 320H960v639.936A64 64 0 0 1 896.128 1024H191.936A63.872 63.872 0 0 1 128 959.936V64.064A64 64 0 0 1 191.936 0H640v320.96h319.616L958.656 320zM320 544c0 17.152 14.464 32 32.192 32h383.552A32.384 32.384 0 0 0 768 544c0-17.152-14.464-32-32.256-32H352.192A32.448 32.448 0 0 0 320 544z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z m0 128c0 17.152 14.464 32 32.192 32h383.552a32.384 32.384 0 0 0 32.256-32c0-17.152-14.464-32-32.256-32H352.192a32.448 32.448 0 0 0-32.192 32z</Geometry>
<Geometry x:Key="Icon.Diff">M854.2 306.6L611.3 72.9c-6-5.7-13.9-8.9-22.2-8.9H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h277l219 210.6V824c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V329.6c0-8.7-3.5-17-9.8-23zM553.4 201.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v704c0 17.7 14.3 32 32 32h512c17.7 0 32-14.3 32-32V397.3c0-8.5-3.4-16.6-9.4-22.6L553.4 201.4zM568 753c0 3.8-3.4 7-7.5 7h-225c-4.1 0-7.5-3.2-7.5-7v-42c0-3.8 3.4-7 7.5-7h225c4.1 0 7.5 3.2 7.5 7v42z m0-220c0 3.8-3.4 7-7.5 7H476v84.9c0 3.9-3.1 7.1-7 7.1h-42c-3.8 0-7-3.2-7-7.1V540h-84.5c-4.1 0-7.5-3.2-7.5-7v-42c0-3.9 3.4-7 7.5-7H420v-84.9c0-3.9 3.2-7.1 7-7.1h42c3.9 0 7 3.2 7 7.1V484h84.5c4.1 0 7.5 3.1 7.5 7v42z</Geometry>
<Geometry x:Key="Icon.Filter">M599.22969 424.769286 599.22969 657.383158 424.769286 831.844585 424.769286 424.769286 192.155415 192.155415 831.844585 192.155415Z</Geometry>
<Geometry x:Key="Icon.Vertical">M1024 1024H0V0h1024v1024z m-64-64V320H320V256h640V64H64v896h192V64h64v896z</Geometry>
<Geometry x:Key="Icon.Horizontal">M81.92 81.92v860.16h860.16V81.92H81.92z m802.304 57.856V322.56H139.776V139.776h744.448z m-744.448 240.64H322.56v503.808H139.776V380.416z m240.128 503.808V380.416h504.32v503.808H379.904z</Geometry>
<Geometry x:Key="Icon.Fetch">M1024 896v128H0V704h128v192h768V704h128v192zM576 554.688L810.688 320 896 405.312l-384 384-384-384L213.312 320 448 554.688V0h128v554.688z</Geometry>
<Geometry x:Key="Icon.Pull">M432 0h160c26.6 0 48 21.4 48 48v336h175.4c35.6 0 53.4 43 28.2 68.2L539.4 756.6c-15 15-39.6 15-54.6 0L180.2 452.2c-25.2-25.2-7.4-68.2 28.2-68.2H384V48c0-26.6 21.4-48 48-48z m592 752v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h293.4l98 98c40.2 40.2 105 40.2 145.2 0l98-98H976c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.Push">M592 768h-160c-26.6 0-48-21.4-48-48V384h-175.4c-35.6 0-53.4-43-28.2-68.2L484.6 11.4c15-15 39.6-15 54.6 0l304.4 304.4c25.2 25.2 7.4 68.2-28.2 68.2H640v336c0 26.6-21.4 48-48 48z m432-16v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h272v16c0 61.8 50.2 112 112 112h160c61.8 0 112-50.2 112-112v-16h272c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z</Geometry>
<Geometry x:Key="Icon.SaveStash">M961.3 319.6L512 577.3 62.7 319.6 512 62l449.3 257.6zM512 628.4L185.4 441.6 62.7 512 512 769.6 961.3 512l-122.7-70.4L512 628.4zM512 820.8L185.4 634 62.7 704.3 512 962l449.3-257.7L838.6 634 512 820.8z</Geometry>
<Geometry x:Key="Icon.Apply">M889.259 551.125c-39.638 0-74.112 21.163-93.483 52.608v-0.426c-16.896 30.378-64.597 27.52-81.28-0.811V319.488H445.781c-41.13-6.827-49.92-66.859-15.061-86.23h-0.384c31.445-19.37 52.608-53.845 52.608-93.482 0-60.8-49.28-110.123-110.08-110.123S262.699 78.976 262.699 139.776c0 39.637 21.162 74.112 52.608 93.483h-0.384c34.858 19.37 26.069 79.402-15.062 86.229H31.104v630.23H292.48c47.744 0 59.392-69.505 22.443-81.622h0.938c-44.757-21.376-75.904-66.688-75.904-119.595 0-73.386 59.478-132.864 132.864-132.864 73.387 0 132.864 59.478 132.864 132.864 0 52.907-31.146 98.219-75.904 119.595h0.939c-36.95 12.16-25.344 81.621 22.443 81.621h261.376V719.915c16.682-28.331 64.384-31.19 81.28-0.811v-0.384c19.37 31.445 53.845 52.608 93.482 52.608 60.8 0 110.08-49.28 110.08-110.123 0-60.8-49.322-110.08-110.122-110.08z</Geometry>
<Geometry x:Key="Icon.Terminal">M89.6 806.4h844.8V217.6H89.6v588.8zM0 128h1024v768H0V128z m242.816 577.536L192 654.72l154.304-154.368L192 346.048l50.816-50.816L448 500.352 242.816 705.536z m584.32 13.248H512V640h315.072v78.72z</Geometry>
<Geometry x:Key="Icon.Flow">M508.928 556.125091l92.904727 148.759273h124.462546l-79.639273-79.173819 49.245091-49.524363 164.584727 163.700363-164.631273 163.002182-49.152-49.617454 79.36-78.568728h-162.955636l-95.650909-153.227636 41.472-65.349818z m186.973091-394.705455l164.584727 163.700364-164.631273 163.002182-49.152-49.617455L726.109091 359.936H529.687273l-135.540364 223.976727H139.636364v-69.818182h215.133091l135.586909-223.976727h235.938909l-79.639273-79.173818 49.245091-49.524364z</Geometry>
<Geometry x:Key="Icon.Commit">M795.968 471.04A291.584 291.584 0 0 0 512 256a293.376 293.376 0 0 0-283.968 215.04H0v144h228.032A292.864 292.864 0 0 0 512 832a291.136 291.136 0 0 0 283.968-216.96H1024V471.04h-228.032M512 688A145.856 145.856 0 0 1 366.016 544 144.576 144.576 0 0 1 512 400c80 0 145.984 63.104 145.984 144A145.856 145.856 0 0 1 512 688</Geometry>
<Geometry x:Key="Icon.WorkingCopy">M0 586.459429l403.968 118.784 497.517714-409.892572-385.536 441.490286-1.609143 250.587428 154.916572-204.580571 278.601143 83.456L1170.285714 36.571429z</Geometry>
<Geometry x:Key="Icon.Histories">M24.356571 512A488.155429 488.155429 0 0 1 512 24.356571 488.155429 488.155429 0 0 1 999.643429 512 488.155429 488.155429 0 0 1 512 999.643429 488.155429 488.155429 0 0 1 24.356571 512z m446.976-325.046857v326.656L242.614857 619.227429l51.126857 110.665142 299.52-138.24V186.953143H471.332571z</Geometry>
<Geometry x:Key="Icon.Stashes">M714.624 253.648h-404.8l-57.808 57.328h520.48z m-491.568 85.984v200.624h578.336V339.632z m404.8 143.296h-28.88v-28.64H425.472v28.64h-28.912v-57.312h231.328v57.312z m-404.8 295.12h578.336V559.36H223.056z m173.504-132.704h231.328v57.328h-28.912v-28.656H425.472v28.656h-28.912v-57.328z</Geometry>
<Geometry x:Key="Icon.Branch">M868.736 144.96a144.64 144.64 0 1 0-289.408 0c0 56.064 32.64 107.008 83.456 130.624-4.928 95.552-76.608 128-201.088 174.592-52.48 19.712-110.336 41.6-159.744 74.432V276.16A144.448 144.448 0 0 0 241.664 0.192a144.64 144.64 0 0 0-144.64 144.768c0 58.24 34.688 108.288 84.352 131.2v461.184a144.32 144.32 0 0 0-84.416 131.2 144.704 144.704 0 1 0 289.472 0 144.32 144.32 0 0 0-83.52-130.688c4.992-95.488 76.672-127.936 201.152-174.592 122.368-45.952 273.792-103.168 279.744-286.784a144.64 144.64 0 0 0 84.928-131.52zM241.664 61.44a83.456 83.456 0 1 1 0 166.912 83.456 83.456 0 0 1 0-166.912z m0 890.56a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04zM724.032 228.416a83.52 83.52 0 1 1 0-167.04 83.52 83.52 0 0 1 0 167.04z</Geometry>
<Geometry x:Key="Icon.Branch.Add">M896 128h-64V64c0-35.2-28.8-64-64-64s-64 28.8-64 64v64h-64c-35.2 0-64 28.8-64 64s28.8 64 64 64h64v64c0 35.2 28.8 64 64 64s64-28.8 64-64V256h64c35.2 0 64-28.8 64-64s-28.8-64-64-64z m-203.52 307.2C672.64 480.64 628.48 512 576 512H448c-46.72 0-90.24 12.8-128 35.2V372.48C394.24 345.6 448 275.2 448 192c0-106.24-85.76-192-192-192S64 85.76 64 192c0 83.2 53.76 153.6 128 180.48v279.68c-74.24 25.6-128 96.64-128 179.84 0 106.24 85.76 192 192 192s192-85.76 192-192c0-66.56-33.92-124.8-84.48-159.36 22.4-19.84 51.84-32.64 84.48-32.64h128c121.6 0 223.36-85.12 248.96-199.04-18.56 4.48-37.12 7.04-56.96 7.04-26.24 0-51.2-5.12-75.52-12.8zM256 128c35.2 0 64 28.8 64 64s-28.8 64-64 64-64-28.8-64-64 28.8-64 64-64z m0 768c-35.2 0-64-28.8-64-64s28.8-64 64-64 64 28.8 64 64-28.8 64-64 64z</Geometry>
<Geometry x:Key="Icon.Remote">M901.802667 479.232v-1.024c0-133.461333-111.616-241.664-249.514667-241.664-105.813333 0-195.925333 63.829333-232.448 153.941333-27.989333-20.138667-62.464-32.426667-100.010667-32.426666-75.776 0-139.605333 49.152-159.744 116.053333-51.882667 36.522667-86.016 96.938667-86.016 165.205333 0 111.616 90.453333 201.728 201.728 201.728h503.466667c111.616 0 201.728-90.453333 201.728-201.728 0-65.194667-31.061333-123.221333-79.189333-160.085333z</Geometry>
<Geometry x:Key="Icon.Remote.Add">M363.789474 512h67.368421v107.789474h107.789473v67.368421h-107.789473v107.789473h-67.368421v-107.789473h-107.789474v-67.368421h107.789474v-107.789474z m297.539368-64A106.671158 106.671158 0 0 1 768 554.671158C768 613.578105 719.548632 660.210526 660.210526 660.210526h-107.789473v-53.894737h-107.789474v-107.789473h-94.31579v107.789473h-94.315789c4.311579-21.194105 22.231579-46.807579 43.560421-50.755368l-0.889263-11.560421a74.671158 74.671158 0 0 1 71.248842-74.590316 128.053895 128.053895 0 0 1 238.605474-7.437473 106.172632 106.172632 0 0 1 52.816842-13.972211z</Geometry>
<Geometry x:Key="Icon.Tag">M177.311335 156.116617c-22.478967 4.729721-32.774451 17.336854-36.251645 36.893258-10.080589 56.697303-33.399691 257.604032-13.234419 277.769304l445.342858 445.341834c23.177885 23.177885 60.757782 23.178909 83.935668 0l246.019183-246.019183c23.177885-23.177885 23.177885-60.757782 0-83.935668l-445.341834-445.341834C437.419398 120.463606 231.004211 144.82034 177.311335 156.116617zM331.22375 344.221786c-26.195615 26.195615-68.667939 26.195615-94.863555 0-26.195615-26.195615-26.195615-68.666916 0-94.863555s68.667939-26.195615 94.862531 0C357.418342 275.55487 357.419366 318.02617 331.22375 344.221786z</Geometry>
<Geometry x:Key="Icon.Tag.Add">M682.666667 536.576h-143.701334v-142.336h-142.336V283.306667H238.933333a44.032 44.032 0 0 0-40.96 40.96v170.666666a55.978667 55.978667 0 0 0 14.336 34.133334l320.512 320.512a40.96 40.96 0 0 0 57.685334 0l173.738666-173.738667a40.96 40.96 0 0 0 0-57.685333z m-341.333334-108.544a40.96 40.96 0 1 1 0-57.685333 40.96 40.96 0 0 1 0 57.685333zM649.216 284.330667V141.994667h-68.608v142.336h-142.336v68.266666h142.336v142.336h68.608v-142.336h142.336v-68.266666h-142.336z</Geometry>
</ResourceDictionary>

View file

@ -0,0 +1,12 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Border.Badge" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="9"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Height" Value="18"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource Brush.Badge}"/>
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,55 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 无边框按钮(也是默认样式) -->
<Style x:Key="Style.Button" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Opacity" Value="0.9"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 修改默认样式 -->
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button}"/>
<!-- 无边框但显示Hover -->
<Style x:Key="Style.Button.HighlightHover" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#40000000"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 有边框 -->
<Style x:Key="Style.Button.Bordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
<Style x:Key="Style.Button.AccentBordered" BasedOn="{StaticResource Style.Button}" TargetType="{x:Type Button}">
<Setter Property="Background" Value="{DynamicResource Brush.Accent1}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,38 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1" Background="Transparent">
<Path x:Name="Checked" Height="12" Width="12" Style="{DynamicResource Style.Icon}" Data="{DynamicResource Icon.Check}" Fill="{DynamicResource Brush.Accent1}"/>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="8,0,0,0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter TargetName="Checked" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,83 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTemplate x:Key="Template.ComboBox.ToggleButton" TargetType="{x:Type ToggleButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.ColumnSpan="2" CornerRadius="0" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{DynamicResource Brush.Border1}" Background="Transparent"/>
<Border Grid.Column="0" CornerRadius="0" Margin="1" Background="Transparent"/>
<Path x:Name="Arrow" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z" Fill="{DynamicResource Brush.Border1}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="Template.ComboBox.TextBox" TargetType="{x:Type TextBox}">
<Border x:Name="PART_ContentHost" Focusable="False" Background="{TemplateBinding Background}" />
</ControlTemplate>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="true" />
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="20" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton" BorderThickness="{TemplateBinding BorderThickness}" Template="{StaticResource Template.ComboBox.ToggleButton}" Grid.Column="2" Focusable="false" ClickMode="Press" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<ContentPresenter x:Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left" TextElement.Foreground="{DynamicResource Brush.FG}"/>
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" Template="{StaticResource Template.ComboBox.TextBox}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" />
<Popup x:Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Grid x:Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border x:Name="DropDownBorder" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Background="{DynamicResource Brush.BG2}"/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" TextElement.Foreground="{DynamicResource Brush.FG}"/>
</ScrollViewer>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
<Trigger SourceName="Popup" Property="AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder" Property="CornerRadius" Value="0" />
<Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Border" Padding="2" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,91 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border Background="{DynamicResource Brush.BG1}" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
<StackPanel IsItemsHost="True" Margin="1" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuItemTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" HorizontalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="{x:Static MenuItem.SubmenuHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
<Border Name="Border" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="Icon" Grid.Column="0" Margin="6,0" VerticalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter Name="HeadHost" Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
<Path Grid.Column="2" Width="8" Height="8" Style="{DynamicResource Style.Icon}" Data="M 0 0 L 0 7 L 4 3.5 Z"/>
<Popup Name="Popup" Placement="Right" HorizontalOffset="-2" IsOpen="{TemplateBinding IsSubmenuOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
<Border Name="SubmenuBorder" SnapsToDevicePixels="True" Background="{DynamicResource Brush.BG1}" BorderBrush="{DynamicResource Brush.Border1}" BorderThickness="1">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="{x:Type Separator}">
<Setter Property="Margin" Value="30,2,0,2"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Rectangle Height=".8" VerticalAlignment="Center" SnapsToDevicePixels="True" Fill="{DynamicResource Brush.Border1}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.DataGridCell" TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style x:Key="Style.DataGridRow" TargetType="{x:Type DataGridRow}">
<Setter Property="BorderThickness" Value="0"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="{x:Static SystemColors.HighlightColor}"/>
</Style.Resources>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="CanUserDeleteRows" Value="False"/>
<Setter Property="CanUserResizeRows" Value="False"/>
<Setter Property="CanUserReorderColumns" Value="False"/>
<Setter Property="CanUserResizeColumns" Value="False" />
<Setter Property="CanUserSortColumns" Value="False"/>
<Setter Property="AllowDrop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="TextElement.FontFamily" Value="Consolas"/>
<Setter Property="EnableColumnVirtualization" Value="True"/>
<Setter Property="EnableRowVirtualization" Value="True"/>
<Setter Property="RowBackground" Value="Transparent"/>
<Setter Property="HeadersVisibility" Value="None"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="CellStyle" Value="{StaticResource Style.DataGridCell}"/>
<Setter Property="RowStyle" Value="{StaticResource Style.DataGridRow}"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="TextDecorations" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Label" TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource Style.Label}"/>
<Style x:Key="Style.Label.GroupHeader" BasedOn="{StaticResource Style.Label}" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="Opacity" Value=".5"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,66 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ListViewItem.Borderless" TargetType="{x:Type ListViewItem}">
<Setter Property="Padding" Value="2"/>
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Border x:Name="Border" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true" Background="Transparent">
<ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ListView.Borderless" TargetType="{x:Type ListView}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.ListViewItem.Borderless}"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListView}">
<Border Name="Border" BorderThickness="0" Background="{TemplateBinding Background}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}">
<ItemsPresenter/>
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.Icon" TargetType="{x:Type Path}">
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Stretch" Value="Uniform"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
</Style>
<Style x:Key="Style.WindowControlIcon" TargetType="{x:Type Path}">
<Setter Property="Stretch" Value="None"/>
<Setter Property="Fill" Value="{DynamicResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
<Setter Property="RenderOptions.EdgeMode" Value="Aliased"/>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,52 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Path
x:Name="Border"
Width="14" Height="14"
Stretch="Uniform"
Fill="Transparent"
Stroke="{DynamicResource Brush.Border1}" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"/>
<Path
x:Name="Dot"
Width="10" Height="10"
Stretch="Uniform"
Fill="{DynamicResource Brush.Accent1}"
Stroke="Transparent" StrokeThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 A 180,180 180 1 1 1,1 Z"
Visibility="Collapsed"/>
</Grid>
<Grid Grid.Column="1" Margin="4,0">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" RecognizesAccessKey="True"/>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Dot" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Stroke" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,98 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ScrollBar.RepeatPage" TargetType="{x:Type RepeatButton}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border x:Name="area" Background="Transparent" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="area" Property="Background" Value="{DynamicResource Brush.FG}" />
<Setter TargetName="area" Property="Opacity" Value=".08"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ScrollBar.Thumb" TargetType="{x:Type Thumb}">
<Setter Property="Background" Value="{DynamicResource Brush.Border1}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Focusable" Value="false" />
<Setter Property="IsTabStop" Value="false" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border x:Name="Border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Opacity=".6"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="Template.ScrollBar.Horizontal" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="Template.ScrollBar.Vertical" TargetType="{x:Type ScrollBar}">
<Grid>
<Track Name="PART_Track"
Grid.Row="1"
IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource Style.ScrollBar.Thumb}"/>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource Style.ScrollBar.RepeatPage}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Height" Value="Auto" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Vertical}" />
<Setter Property="Width" Value="8" />
</Trigger>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="8" />
<Setter Property="Template" Value="{StaticResource Template.ScrollBar.Horizontal}" />
<Setter Property="Width" Value="Auto" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,54 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter
Cursor="{TemplateBinding Cursor}"
Margin="{TemplateBinding Padding}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ScrollBar x:Name="PART_VerticalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Grid.Column="1" Grid.Row="0" Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Maximum="{TemplateBinding ScrollableHeight}"
Minimum="0"
Value="{TemplateBinding VerticalOffset}"
Margin="0,-1,-1,-1"/>
<ScrollBar x:Name="PART_HorizontalScrollBar"
IsTabStop="False"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Grid.Column="0" Grid.Row="1" Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Maximum="{TemplateBinding ScrollableWidth}"
Minimum="0"
Value="{TemplateBinding HorizontalOffset}"
Margin="-1,0,-1,-1"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,62 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type TabControl}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabPanel x:Name="HeaderPanel" Grid.Row="0" IsItemsHost="True" KeyboardNavigation.TabIndex="1" Background="Transparent" />
<Border
x:Name="Border"
Grid.Row="1"
Background="Transparent"
KeyboardNavigation.TabNavigation="Local"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2">
<ContentPresenter x:Name="PART_SelectedContentHost" Margin="4" ContentSource="SelectedContent" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border x:Name="Border" Margin="0" BorderThickness="0,0,0,1.1" BorderBrush="Transparent" Opacity=".7">
<ContentPresenter
x:Name="ContentSite"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextElement.Foreground="{DynamicResource Brush.FG}"
TextElement.FontWeight="Bold"
ContentSource="Header"
Margin="8,6"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
<Setter TargetName="Border" Property="Opacity" Value="1"/>
<Setter TargetName="ContentSite" Property="TextElement.Foreground" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,114 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 错误Tooltip -->
<ControlTemplate x:Key="Template.Validation.Tooltip" TargetType="{x:Type ToolTip}">
<Border x:Name="Root" Margin="5,0,0,0" Opacity="0" Padding="0,0,20,20" RenderTransformOrigin="0,0">
<Border.RenderTransform>
<TranslateTransform x:Name="xform" X="-25" />
</Border.RenderTransform>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="OpenStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" />
<VisualTransition GeneratedDuration="0:0:0.2" To="Open">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform">
<DoubleAnimation.EasingFunction>
<BackEase Amplitude=".3" EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Duration="0:0:0.2" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Closed">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
<VisualState x:Name="Open">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="X" Storyboard.TargetName="xform" />
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<FrameworkElement.Effect>
<DropShadowEffect BlurRadius="11" ShadowDepth="6" Opacity="0.4" />
</FrameworkElement.Effect>
<Border Background="#FFDC000C" BorderThickness="1" BorderBrush="#FFBC000C">
<TextBlock Foreground="White" MaxWidth="250" Margin="8,4,8,4" TextWrapping="Wrap" Text="{Binding [0].ErrorContent}" UseLayoutRounding="false" />
</Border>
</Border>
</ControlTemplate>
<!-- 验证错误模板 -->
<ControlTemplate x:Key="Template.Validation.Error">
<AdornedElementPlaceholder x:Name="Target">
<Border BorderBrush="#FFDB000C" BorderThickness="1" x:Name="root">
<ToolTipService.ToolTip>
<ToolTip x:Name="validationTooltip"
Placement="Right"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Template="{StaticResource Template.Validation.Tooltip}"
Style="{x:Null}"/>
</ToolTipService.ToolTip>
<Grid Background="Transparent" HorizontalAlignment="Right" Height="12" Width="12" Margin="1,-4,-4,0" VerticalAlignment="Top">
<Path Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 z" Fill="#FFDC000C" Margin="1,3,0,0" />
</Grid>
</Border>
</AdornedElementPlaceholder>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.IsKeyboardFocusWithin, Mode=OneWay}" Value="True" />
<Condition Binding="{Binding ElementName=Target, Path=AdornedElement.(Validation.HasError), Mode=OneWay}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="validationTooltip" Property="IsOpen" Value="True"/>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- 修改默认 -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource Brush.FG}"/>
<Setter Property="CaretBrush" Value="{DynamicResource Brush.FG}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Border1}"/>
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource Template.Validation.Error}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Background="{x:Null}"
BorderThickness="0"
IsTabStop="False"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<Trigger Property="AcceptsReturn" Value="True">
<Setter TargetName="PART_ContentHost" Property="VerticalAlignment" Value="Top"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,111 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Style.ToggleButton.Expender" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<ContentPresenter/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Filter" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid>
<Path
x:Name="Icon"
Height="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG2}"
StrokeThickness="1"
Data="{DynamicResource Icon.Filter}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.FG2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.Orientation" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Width="18"
Height="18"
Style="{DynamicResource Style.Icon}"
Fill="{DynamicResource Brush.Border1}"
Data="{DynamicResource Icon.Horizontal}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.Vertical}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.ToggleButton.ListOrTree" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Grid Background="Transparent">
<Path
x:Name="Icon"
Height="12"
Width="12"
Style="{DynamicResource Style.Icon}"
Fill="Transparent"
Stroke="{DynamicResource Brush.FG2}"
StrokeThickness="1"
Data="{DynamicResource Icon.Tree}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{DynamicResource Icon.List}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type ToolTip}">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<Border
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border1}"
Background="{DynamicResource Brush.BG1}"
Width="Auto"
Height="Auto">
<ContentPresenter Margin="6,4" TextElement.Foreground="{DynamicResource Brush.FG}" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,201 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers">
<converters:TreeViewItemDepthToMargin x:Key="Converter.TreeViewItemIndent" Indent="19"/>
<Style x:Key="Style.TreeView.ToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="16" Height="16" Margin="1" Background="Transparent">
<Path x:Name="ExpandPath" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1,1,1,1" Fill="{DynamicResource Brush.FG}" Data="M 4 0 L 8 4 L 4 8 Z"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="ExpandPath" Value="M 0 4 L 8 4 L 4 8 Z"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.ItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Style.TreeView.MultiSelectionItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, Mode=OneWay, FallbackValue=Stretch, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, Mode=OneWay, FallbackValue=Center, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border
x:Name="BG"
Background="Transparent"
BorderThickness="0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid
Margin="{Binding Converter={StaticResource Converter.TreeViewItemIndent}, RelativeSource={x:Static RelativeSource.TemplatedParent}}"
VerticalAlignment="Stretch"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ToggleButton
Grid.Column="0"
x:Name="Expander"
Style="{StaticResource Style.TreeView.ToggleButton}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={x:Static RelativeSource.TemplatedParent}, Mode=TwoWay}"
ClickMode="Press"/>
<ContentPresenter
x:Name="PART_Header"
Grid.Column="1"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="helpers:TreeViewHelper.IsChecked" Value="True">
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent1}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="BG" Property="IsMouseOver" Value="True"/>
<Condition Property="helpers:TreeViewHelper.IsChecked" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="BG" Property="Background" Value="{DynamicResource Brush.Accent2}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="ItemContainerStyle" Value="{StaticResource Style.TreeView.ItemContainerStyle}"/>
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Standard" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeView}">
<Border Name="Border"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="0"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ScrollViewer Padding="{TemplateBinding Padding}"
CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".5"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,16 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FF252525"/>
<SolidColorBrush x:Key="Brush.BG2" Color="#FF1B1B1B"/>
<SolidColorBrush x:Key="Brush.BG3" Color="#FF202020"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FF303030"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FF505050"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF7C7C7C"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FF404040"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FFF1F1F1"/>
<SolidColorBrush x:Key="Brush.FG2" Color="#40F1F1F1"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF007ACC"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#33007ACC"/>
</ResourceDictionary>

View file

@ -0,0 +1,15 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Brush.BG1" Color="#FFEEEEF2"/>
<SolidColorBrush x:Key="Brush.BG2" Color="White"/>
<SolidColorBrush x:Key="Brush.BG3" Color="WhiteSmoke"/>
<SolidColorBrush x:Key="Brush.BG4" Color="#FFE6E7E8"/>
<SolidColorBrush x:Key="Brush.BG5" Color="#FFBDBDBD"/>
<SolidColorBrush x:Key="Brush.Border1" Color="#FF898989"/>
<SolidColorBrush x:Key="Brush.Border2" Color="#FFCFCFCF"/>
<SolidColorBrush x:Key="Brush.FG" Color="#FF1F1F1F"/>
<SolidColorBrush x:Key="Brush.FG2" Color="DarkGray"/>
<SolidColorBrush x:Key="Brush.Badge" Color="#FF8F8F8F"/>
<SolidColorBrush x:Key="Brush.Accent1" Color="#FF4295FF"/>
<SolidColorBrush x:Key="Brush.Accent2" Color="#33007ACC"/>
</ResourceDictionary>

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<LangVersion>latest</LangVersion>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>App.ico</ApplicationIcon>
<Company>Leo</Company>
<Description>OpenSource GIT client for Windows</Description>
<Copyright>Copyright © sourcegit 2020. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup>
<Resource Include="App.ico" />
</ItemGroup>
</Project>

77
SourceGit/UI/About.xaml Normal file
View file

@ -0,0 +1,77 @@
<Window x:Class="SourceGit.UI.About"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="280" Width="400"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<!-- Enable WindowChrome Feature -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32" GlassFrameThickness="0 0 0 1" ResizeBorderThickness="8" ResizeGripDirection="None" NonClientFrameEdges="None" />
</WindowChrome.WindowChrome>
<!-- Window Layout -->
<Border Background="{StaticResource Brush.BG1}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Title -->
<Label Content="ABOUT" FontWeight="Light"/>
<!-- Close Button -->
<Button Click="Quit" Width="32" Grid.Column="2" WindowChrome.IsHitTestVisibleInChrome="True">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="90"/>
<RowDefinition Height="40"/>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,6,0,0">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}" Fill="#FFF05133"/>
</StackPanel>
<Label Grid.Row="1" Content="SourceGit - OPEN SOURCE GIT CLIENT" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" FontSize="18" FontWeight="Bold"/>
<Label Grid.Row="2" Content="{Binding ElementName=me, Path=Version}" HorizontalContentAlignment="Center" FontSize="11"/>
<Label Grid.Row="3" HorizontalContentAlignment="Center" FontSize="11">
<Hyperlink RequestNavigate="OpenSource" NavigateUri="https://gitee.com/sourcegit/SourceGit.git">
<Run Text="https://gitee.com/sourcegit/SourceGit.git"/>
</Hyperlink>
</Label>
<Label Grid.Row="4" Content="Copyright © sourcegit 2020. All rights reserved." HorizontalContentAlignment="Center" FontSize="11"/>
</Grid>
</Grid>
</Border>
</Window>

View file

@ -0,0 +1,46 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// About dialog
/// </summary>
public partial class About : Window {
/// <summary>
/// Current app version
/// </summary>
public string Version {
get {
return "VERSION : " + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion;
}
}
/// <summary>
/// Constructor
/// </summary>
public About() {
InitializeComponent();
}
/// <summary>
/// Open source code link
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OpenSource(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
/// <summary>
/// Close this dialog
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
}
}

85
SourceGit/UI/Apply.xaml Normal file
View file

@ -0,0 +1,85 @@
<UserControl x:Class="SourceGit.UI.Apply"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Apply Patch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Patch File :"/>
<Grid Grid.Row="2" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
x:Name="txtPatchFile"
Height="24"
helpers:TextBoxHelper.Placeholder="Select .patch file to apply">
<TextBox.Text>
<Binding Path="PatchFile" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:PatchFileRequiredRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Click="FindPatchFile" Padding="0" BorderThickness="1" Style="{StaticResource Style.Button.Bordered}">
<Path Width="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Whitespace :"/>
<ComboBox x:Name="combWhitespaceOptions" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="20">
<Label Content="{Binding Name}" Padding="4,0"/>
<Label Content="{Binding Desc}" Foreground="{StaticResource Brush.FG2}" FontSize="11" Padding="4,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

112
SourceGit/UI/Apply.xaml.cs Normal file
View file

@ -0,0 +1,112 @@
using Microsoft.Win32;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Apply patch dialog
/// </summary>
public partial class Apply : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Whitespace option.
/// </summary>
public class WhitespaceOption {
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public WhitespaceOption(string n, string d, string a) {
Name = n;
Desc = d;
Arg = a;
}
}
/// <summary>
/// Path of file to be patched.
/// </summary>
public string PatchFile { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Apply(Git.Repository opened) {
repo = opened;
InitializeComponent();
combWhitespaceOptions.ItemsSource = new WhitespaceOption[] {
new WhitespaceOption("No Warn", "Turns off the trailing whitespace warning", "nowarn"),
new WhitespaceOption("Warn", "Outputs warnings for a few such errors, but applies", "warn"),
new WhitespaceOption("Error", "Raise errors and refuses to apply the patch", "error"),
new WhitespaceOption("Error All", "Similar to 'error', but shows more", "error-all"),
};
combWhitespaceOptions.SelectedIndex = 0;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
public static void Show(Git.Repository opened) {
PopupManager.Show(new Apply(opened));
}
/// <summary>
/// Open file browser dialog for select a file to patch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FindPatchFile(object sender, RoutedEventArgs e) {
var dialog = new OpenFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = "Select Patch File";
dialog.InitialDirectory = repo.Path;
dialog.CheckFileExists = true;
if (dialog.ShowDialog() == true) {
PatchFile = dialog.FileName;
txtPatchFile.Text = dialog.FileName;
}
}
/// <summary>
/// Start apply selected path.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtPatchFile.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtPatchFile)) return;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
var mode = combWhitespaceOptions.SelectedItem as WhitespaceOption;
await Task.Run(() => repo.Apply(PatchFile, mode.Arg));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel options.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

201
SourceGit/UI/Blame.xaml Normal file
View file

@ -0,0 +1,201 @@
<Window x:Class="SourceGit.UI.Blame"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Content -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Logo & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - BLAME" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Blame file -->
<Border Grid.Row="1" Padding="2,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="blameFile" HorizontalAlignment="Left" FontSize="11" Foreground="{StaticResource Brush.FG2}" FontFamily="Consolas"/>
<Label Grid.Column="1" HorizontalAlignment="Right" Foreground="{StaticResource Brush.FG2}" FontSize="11" Content="Use right mouse button to view commit information."/>
</Grid>
</Border>
<!-- Content -->
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" ClipToBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="lineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"
FontFamily="Consolas"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="content"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="SyncScrollChanged"
PreviewMouseWheel="MouseWheelOnContent"
SizeChanged="ContentSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas">
<FlowDocument PageWidth="0"/>
</RichTextBox>
<!-- Loading tip -->
<Path x:Name="loading" Grid.ColumnSpan="5" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<!-- Popup to show commit info -->
<Popup x:Name="popup" Grid.ColumnSpan="5" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True">
<Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG1}">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="COMMIT SHA" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="commitID"/>
<Label Grid.Row="1" Grid.Column="0" Content="AUTHOR" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="authorName"/>
<Label Grid.Row="2" Grid.Column="0" Content="MODIFY TIME" Foreground="{StaticResource Brush.FG2}"/>
<Label Grid.Row="2" Grid.Column="1" x:Name="authorTime"/>
</Grid>
</Border>
</Popup>
</Grid>
</Border>
</Grid>
</Border>
</Window>

211
SourceGit/UI/Blame.xaml.cs Normal file
View file

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Viewer to show git-blame
/// </summary>
public partial class Blame : Window {
/// <summary>
/// Background color for blocks.
/// </summary>
public static Brush[] BG = new Brush[] {
Brushes.Transparent,
new SolidColorBrush(Color.FromArgb(128, 0, 0, 0))
};
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
/// <param name="revision"></param>
public Blame(Git.Repository repo, string file, string revision) {
InitializeComponent();
double minWidth = content.ActualWidth;
// Move to center.
var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading.
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible;
// Layout content
blameFile.Content = $"{file}@{revision.Substring(0, 8)}";
Task.Run(() => {
var blame = repo.BlameFile(file, revision);
Dispatcher.Invoke(() => {
content.Document.Blocks.Clear();
if (blame.IsBinary) {
lineNumber.Text = "0";
Paragraph p = new Paragraph(new Run("BINARY FILE BLAME NOT SUPPORTED!!!"));
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
content.Document.Blocks.Add(p);
} else {
List<string> numbers = new List<string>();
for (int i = 0; i < blame.LineCount; i++) numbers.Add(i.ToString());
lineNumber.Text = string.Join("\n", numbers);
numbers.Clear();
for (int i = 0; i < blame.Blocks.Count; i++) {
var frag = blame.Blocks[i];
var idx = i;
Paragraph p = new Paragraph(new Run(frag.Content));
p.DataContext = frag;
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = BG[i % 2];
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
p.MouseRightButtonDown += (sender, ev) => {
Hyperlink link = new Hyperlink(new Run(frag.CommitSHA));
link.ToolTip = "CLICK TO GO";
link.Click += (o, e) => {
repo.OnNavigateCommit?.Invoke(frag.CommitSHA);
e.Handled = true;
};
foreach (var block in content.Document.Blocks) {
var paragraph = block as Paragraph;
if ((paragraph.DataContext as Git.Blame.Block).CommitSHA == frag.CommitSHA) {
paragraph.Background = Brushes.Green;
} else {
paragraph.Background = BG[i % 2];
}
}
commitID.Content = link;
authorName.Content = frag.Author;
authorTime.Content = frag.Time;
popup.IsOpen = true;
};
var formatter = new FormattedText(
frag.Content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(content.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
content.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) {
content.Document.PageWidth = formatter.Width + 16;
minWidth = formatter.Width;
}
content.Document.Blocks.Add(p);
}
}
// Hide loading.
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
});
});
}
/// <summary>
/// Click logo
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
/// <summary>
/// Sync scroll
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
var margin = new Thickness(4, -e.VerticalOffset, 4, 0);
lineNumber.Margin = margin;
}
}
/// <summary>
/// Mouse wheel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) {
if (e.Delta > 0) {
content.LineUp();
} else {
content.LineDown();
}
e.Handled = true;
}
/// <summary>
/// Content size changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentSizeChanged(object sender, SizeChangedEventArgs e) {
if (content.Document.PageWidth < content.ActualWidth) {
content.Document.PageWidth = content.ActualWidth;
}
}
}
}

View file

@ -0,0 +1,45 @@
<UserControl x:Class="SourceGit.UI.CherryPick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Cherry Pick"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Commit :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}" Margin="4,0"/>
<Label x:Name="desc"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkCommitChanges" IsChecked="True" Content="Commit the changes"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,54 @@
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Cherry pick commit dialog.
/// </summary>
public partial class CherryPick : UserControl {
private Git.Repository repo = null;
private string commitSHA = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="commit"></param>
public CherryPick(Git.Repository opened, Git.Commit commit) {
InitializeComponent();
repo = opened;
commitSHA = commit.SHA;
desc.Content = $"{commit.ShortSHA} {commit.Subject}";
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
PopupManager.Show(new CherryPick(repo, commit));
}
/// <summary>
/// Start pick.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
repo.CherryPick(commitSHA, chkCommitChanges.IsChecked != true);
PopupManager.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

98
SourceGit/UI/Clone.xaml Normal file
View file

@ -0,0 +1,98 @@
<UserControl x:Class="SourceGit.UI.Clone"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
Width="500" Height="192">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Clone Remote Repository"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Repository URL :"/>
<TextBox x:Name="txtUrl" Grid.Row="2" Grid.Column="1"
Height="24"
helpers:TextBoxHelper.Placeholder="Git Repository URL">
<TextBox.Text>
<Binding Path="RemoteUri" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:RemoteUriRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Parent Folder :"/>
<Grid Grid.Row="3" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
x:Name="txtParentFolder"
Height="24"
helpers:TextBoxHelper.Placeholder="Folder to contain this repository">
<TextBox.Text>
<Binding Path="ParentFolder" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:CloneFolderRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Column="1" Width="24" Height="24" Padding="0" BorderThickness="1" Click="SelectParentFolder" Style="{StaticResource Style.Button.Bordered}">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Name :"/>
<TextBox Grid.Row="4" Grid.Column="1"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Repository name. Optional."
Text="{Binding LocalName, ElementName=me, Mode=TwoWay}">
</TextBox>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="7" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

115
SourceGit/UI/Clone.xaml.cs Normal file
View file

@ -0,0 +1,115 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Clone dialog.
/// </summary>
public partial class Clone : UserControl {
/// <summary>
/// Remote repository
/// </summary>
public string RemoteUri { get; set; }
/// <summary>
/// Parent folder.
/// </summary>
public string ParentFolder { get; set; }
/// <summary>
/// Local name.
/// </summary>
public string LocalName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public Clone() {
ParentFolder = App.Preference.GitDefaultCloneDir;
InitializeComponent();
}
/// <summary>
/// Show clone dialog.
/// </summary>
public static void Show() {
PopupManager.Show(new Clone());
}
/// <summary>
/// Select parent folder.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SelectParentFolder(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Git Repository URL";
dialog.RootFolder = Environment.SpecialFolder.MyComputer;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
txtParentFolder.Text = dialog.SelectedPath;
}
}
/// <summary>
/// Start clone
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
txtUrl.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtUrl)) return;
txtParentFolder.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtParentFolder)) return;
string repoName;
if (string.IsNullOrWhiteSpace(LocalName)) {
var from = RemoteUri.LastIndexOfAny(new char[] { '\\', '/' });
if (from <= 0) return;
var name = RemoteUri.Substring(from + 1);
repoName = name.Replace(".git", "");
} else {
repoName = LocalName;
}
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
Task.Run(() => {
var repo = Git.Repository.Clone(RemoteUri, ParentFolder, repoName, msg => Dispatcher.Invoke(() => statusMsg.Content = msg));
if (repo == null) {
PopupManager.Unlock();
Dispatcher.Invoke(() => {
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
});
} else {
Dispatcher.Invoke(() => PopupManager.Close(true));
repo.Open();
}
});
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,408 @@
<UserControl x:Class="SourceGit.UI.CommitViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<TabControl>
<TabItem Header="INFORMATION">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition x:Name="committerRow" Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="16"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SHA -->
<Label Grid.Row="0" Grid.Column="0" Content="SHA" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="0" Grid.Column="1"
x:Name="SHA"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- Refs -->
<Label x:Name="lblRefs" Grid.Row="0" Grid.Column="2" Content="REFS" HorizontalAlignment="Right" Opacity=".5"/>
<ItemsControl Grid.Row="0" Grid.Column="3" x:Name="refs" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="BG" Height="16" Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.BG5}">
<Path x:Name="Icon" Width="8" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
</Border>
<Label x:Name="Name" Grid.Column="1" Content="{Binding Name}" FontSize="11" Padding="4,0" Foreground="Black"/>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.Tag}">
<Setter TargetName="BG" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.LocalBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Branch}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.RemoteBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.CurrentBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Icon" Property="Fill" Value="Orange"/>
<Setter TargetName="Name" Property="FontWeight" Value="Bold"/>
<Setter TargetName="Name" Property="Foreground" Value="{StaticResource Brush.FG}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- PARENTS -->
<Label Grid.Row="1" Grid.Column="0" Content="PARENTS" HorizontalAlignment="Right" Opacity=".5"/>
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" x:Name="parents" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Margin="0,0,8,0">
<Hyperlink
RequestNavigate="NavigateParent"
NavigateUri="{Binding .}"
ToolTip="NAVIGATE TO COMMIT">
<Run Text="{Binding .}"/>
</Hyperlink>
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- AUTHOR -->
<Label Grid.Row="2" Grid.Column="0" Content="AUTHOR" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="author"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- AUTHOR TIME -->
<Label Grid.Row="2" Grid.Column="2" Content="AUTHOR TIME" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="2" Grid.Column="3" Grid.ColumnSpan="3" x:Name="authorTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<!-- COMMITTER -->
<Label Grid.Row="3" Grid.Column="0" Content="COMMITTER" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="committer"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="11,0,0,0"/>
<!-- COMMIT TIME -->
<Label Grid.Row="3" Grid.Column="2" Content="COMMIT TIME" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="3" x:Name="committerTime"
IsReadOnly="True"
Background="Transparent"
AcceptsReturn="True"
BorderThickness="0"
Margin="8,0,0,0"/>
<Rectangle Grid.Row="4" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- SUBJECT -->
<Label Grid.Row="5" Grid.Column="0" Content="SUBJECT" HorizontalAlignment="Right" Opacity=".5"/>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="subject"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
Margin="8,0,16,0"/>
<!-- MESSAGE -->
<Label Grid.Row="6" Grid.Column="0" Content="DESCRIPTION" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".5"/>
<TextBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="3"
x:Name="message"
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
VerticalAlignment="Center"
FontSize="11"
Margin="11,8,0,0"/>
<Rectangle Grid.Row="7" Grid.ColumnSpan="4" Height="1" Margin="8,0" Fill="{StaticResource Brush.Border2}"/>
<!-- CHANGELIST -->
<Label Grid.Row="8" Grid.Column="0" Content="CHANGED" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity=".5"/>
<DataGrid
Grid.Row="8"
Grid.Column="1"
Grid.ColumnSpan="3"
x:Name="changeList1"
RowHeight="20"
Margin="11,2,0,2">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</TabItem>
<!-- CHANGES -->
<TabItem Header="CHANGES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="2,0">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
<converters:InverseBoolToCollapsed x:Key="InverseBoolToCollapsed"/>
</Grid.Resources>
<Grid Grid.Row="0" Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" Background="{StaticResource Brush.BG3}"/>
<Path Grid.Column="0" Width="14" Height="14" Fill="{StaticResource Brush.FG2}" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<TextBox Grid.Column="1" x:Name="txtChangeFilter" BorderThickness="0" helpers:TextBoxHelper.Placeholder="Search File ..." TextChanged="SearchChangeFileTextChanged"/>
<ToggleButton
Grid.Column="2"
x:Name="toggleSwitchMode"
Margin="4,0,0,0"
ToolTip="SWITCH TO LIST/TREE VIEW"
Style="{StaticResource Style.ToggleButton.ListOrTree}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIUseListInChanges, Mode=TwoWay}"/>
</Grid>
<TreeView
Grid.Row="1"
x:Name="changeTree"
FontFamily="Consolas"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource InverseBoolToCollapsed}}"
Background="{StaticResource Brush.BG2}"
SelectedItemChanged="ChangeTreeItemSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Border x:Name="status" Width="14" Height="14" Visibility="Collapsed" Background="{Binding Change, Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="0,0,4,0">
<TextBlock Text="{Binding Change, Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="10" RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="4,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="status" Property="Visibility" Value="Visible"/>
<Setter TargetName="icon" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<DataGrid
Grid.Row="1"
x:Name="changeList2"
Visibility="{Binding ElementName=toggleSwitchMode, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
RowHeight="24"
SelectionChanged="ChangeListSelectionChanged"
SelectionMode="Single"
SelectionUnit="FullRow"
Background="{StaticResource Brush.BG2}">
<DataGrid.Resources>
<converters:FileStatusToColor x:Key="StatusColorConverter"/>
<converters:FileStatusToIcon x:Key="StatusIconConverter"/>
<Style x:Key="Style.DataGridText.VerticalCenter" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Width="14" Height="14" x:Name="status" Background="{Binding ., Converter={StaticResource StatusColorConverter}}" CornerRadius="2" Margin="2,0,4,0">
<TextBlock Text="{Binding ., Converter={StaticResource StatusIconConverter}}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" FontSize="8"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Path}" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" ElementStyle="{StaticResource Style.DataGridText.VerticalCenter}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="ChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<local:DiffViewer Grid.Column="2" x:Name="diffViewer" Background="{StaticResource Brush.BG3}"/>
</Grid>
</TabItem>
<!-- FILE TREE -->
<TabItem Header="FILES">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="2" Background="{StaticResource Brush.BG2}">
<TreeView x:Name="fileTree" SelectedItemChanged="FileTreeItemSelected" FontFamily="Consolas" PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="icon" Width="14" Style="{StaticResource Style.Icon}" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Foreground="{StaticResource Brush.FG}" TextAlignment="Center" VerticalAlignment="Center" Margin="6,0,0,0" FontSize="11"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFile}" Value="True">
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.File}"/>
<Setter TargetName="icon" Property="Fill" Value="{StaticResource Brush.FG}"/>
<Setter TargetName="icon" Property="Opacity" Value=".75"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFile}" Value="False"/>
<Condition Binding="{Binding IsNodeExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderThickness="1" Margin="2,0" BorderBrush="{StaticResource Brush.Border2}">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock FontSize="10pt"
FontFamily="Consolas"
Padding="8"
Opacity="0.8"
Background="{StaticResource Brush.BG2}"
Foreground="{StaticResource Brush.FG}"
x:Name="filePreview"/>
</ScrollViewer>
</Border>
</Grid>
</TabItem>
</TabControl>
</UserControl>

View file

@ -0,0 +1,468 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// Commit detail viewer
/// </summary>
public partial class CommitViewer : UserControl {
private Git.Repository repo = null;
private Git.Commit commit = null;
private List<Git.Change> cachedChanges = new List<Git.Change>();
private List<Git.Change> displayChanges = new List<Git.Change>();
private string changeFilter = null;
/// <summary>
/// Node for file tree.
/// </summary>
public class Node {
public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = "";
public string Name { get; set; } = "";
public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true;
public Git.Change Change { get; set; } = null;
public List<Node> Children { get; set; } = new List<Node>();
}
/// <summary>
/// Constructor.
/// </summary>
public CommitViewer() {
InitializeComponent();
}
#region DATA
public void SetData(Git.Repository opened, Git.Commit selected) {
repo = opened;
commit = selected;
SetBaseInfo(commit);
Task.Run(() => {
cachedChanges.Clear();
cachedChanges = commit.GetChanges(repo);
Dispatcher.Invoke(() => {
changeList1.ItemsSource = null;
changeList1.ItemsSource = cachedChanges;
});
LayoutChanges();
SetRevisionFiles(commit.GetFiles(repo));
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
fileTree.ItemsSource = null;
changeList1.ItemsSource = null;
changeList2.ItemsSource = null;
displayChanges.Clear();
cachedChanges.Clear();
diffViewer.Reset();
}
#endregion
#region BASE_INFO
private void SetBaseInfo(Git.Commit commit) {
var parentIds = new List<string>();
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
SHA.Text = commit.SHA;
refs.ItemsSource = commit.Decorators;
parents.ItemsSource = parentIds;
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
authorTime.Text = commit.Author.Time;
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
committerTime.Text = commit.Committer.Time;
subject.Text = commit.Subject;
message.Text = commit.Message.Trim();
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
else lblRefs.Visibility = Visibility.Visible;
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
committerRow.Height = new GridLength(0);
} else {
committerRow.Height = GridLength.Auto;
}
}
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true;
}
#endregion
#region CHANGES
private void LayoutChanges() {
displayChanges.Clear();
if (string.IsNullOrEmpty(changeFilter)) {
displayChanges.AddRange(cachedChanges);
} else {
foreach (var c in cachedChanges) {
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
}
}
List<Node> changeTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
bool isDefaultExpanded = displayChanges.Count < 50;
foreach (var c in displayChanges) {
var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = c.Path;
node.IsFile = true;
node.Name = c.Path;
node.Change = c;
node.IsNodeExpanded = isDefaultExpanded;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
changeTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = isDefaultExpanded;
changeTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = isDefaultExpanded;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = c.Path;
node.Name = c.Path.Substring(start);
node.IsFile = true;
node.Change = c;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(changeTreeSource);
Dispatcher.Invoke(() => {
changeList2.ItemsSource = null;
changeList2.ItemsSource = displayChanges;
changeTree.ItemsSource = changeTreeSource;
diffViewer.Reset();
});
}
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
changeFilter = txtChangeFilter.Text.ToUpper();
Task.Run(() => LayoutChanges());
}
private async void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
diffViewer.Reset();
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, node.FilePath, node.OriginalPath);
});
diffViewer.SetData(data, node.FilePath, node.OriginalPath);
}
private async void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var change = e.AddedItems[0] as Git.Change;
if (change == null) return;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, change.Path, change.OriginalPath);
});
diffViewer.SetData(data, change.Path, change.OriginalPath);
}
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Git.Change;
if (change == null) return;
var path = change.Path;
var menu = new ContextMenu();
if (change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, path);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, path, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
Process.Start("explorer", $"/select,{absPath}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = change.Path;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region FILES
private void SetRevisionFiles(List<string> files) {
List<Node> fileTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>();
foreach (var path in files) {
var sepIdx = path.IndexOf("/");
if (sepIdx == -1) {
Node node = new Node();
node.FilePath = path;
node.Name = path;
node.IsFile = true;
node.IsNodeExpanded = false;
fileTreeSource.Add(node);
} else {
Node lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new Node();
lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = false;
fileTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var folderNode = new Node();
folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = false;
folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode);
lastFolder = folderNode;
}
start = sepIdx + 1;
sepIdx = path.IndexOf('/', start);
}
Node node = new Node();
node.FilePath = path;
node.Name = path.Substring(start);
node.IsFile = true;
node.IsNodeExpanded = false;
lastFolder.Children.Add(node);
}
}
folders.Clear();
SortTreeNodes(fileTreeSource);
Dispatcher.Invoke(() => {
fileTree.ItemsSource = fileTreeSource;
filePreview.Text = "";
});
}
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
filePreview.Text = "";
var node = e.NewValue as Node;
if (node == null || !node.IsFile) return;
await Task.Run(() => {
var data = commit.GetTextFileContent(repo, node.FilePath);
Dispatcher.Invoke(() => filePreview.Text = data);
});
}
#endregion
#region TREE_COMMON
private void SortTreeNodes(List<Node> list) {
list.Sort((l, r) => {
if (l.IsFile) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
} else {
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
}
});
foreach (var sub in list) {
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
}
}
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
if (owner == null) return null;
if (owner is ScrollViewer) return owner as ScrollViewer;
int n = VisualTreeHelper.GetChildrenCount(owner);
for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
var deep = GetScrollViewer(child);
if (deep != null) return deep;
}
return null;
}
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView);
if (scroll == null) return;
if (e.Delta > 0) {
scroll.LineUp();
} else {
scroll.LineDown();
}
e.Handled = true;
}
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as TreeViewItem;
if (item == null) return;
var node = item.DataContext as Node;
if (node == null || !node.IsFile) return;
item.IsSelected = true;
ContextMenu menu = new ContextMenu();
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
history.Header = "File History";
history.Click += (o, ev) => {
var viewer = new FileHistories(repo, node.FilePath);
viewer.Show();
};
menu.Items.Add(history);
MenuItem blame = new MenuItem();
blame.Header = "Blame";
blame.Click += (obj, ev) => {
Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
viewer.Show();
};
menu.Items.Add(blame);
MenuItem explore = new MenuItem();
explore.Header = "Reveal in File Explorer";
explore.Click += (o, ev) => {
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
menu.Items.Add(explore);
MenuItem saveAs = new MenuItem();
saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = node.FilePath;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
var path = Path.Combine(dialog.SelectedPath, node.Name);
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
}
};
menu.Items.Add(saveAs);
}
MenuItem copyPath = new MenuItem();
copyPath.Header = "Copy Path";
copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.FilePath);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,92 @@
<UserControl x:Class="SourceGit.UI.CreateBranch"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="224" Width="500">
<UserControl.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Local Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Based On :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="basedOnType" Width="12" Style="{StaticResource Style.Icon}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="master"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Branch Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="txtName"
VerticalContentAlignment="Center"
Height="24"
helpers:TextBoxHelper.Placeholder="Enter branch name.">
<TextBox.Text>
<Binding ElementName="me" Path="BranchName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:BranchNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" Content="Local Changes :"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal">
<RadioButton Content="Stash &amp; Reapply" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me}"/>
<RadioButton Content="Discard" Margin="8,0,0,0" GroupName="LocalChanges" IsChecked="{Binding AutoStash, ElementName=me, Mode=OneWay, Converter={StaticResource InverseBool}}"/>
</StackPanel>
<CheckBox Grid.Row="5" Grid.Column="1"
x:Name="chkCheckout"
VerticalAlignment="Center"
IsChecked="True"
Content="Check out after created"/>
<Grid Grid.Row="7" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="8" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Create branch dialog
/// </summary>
public partial class CreateBranch : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// New branch name.
/// </summary>
public string BranchName {
get;
set;
}
/// <summary>
/// Auto Stash
/// </summary>
public bool AutoStash { get; set; } = false;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
public CreateBranch(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create branch based on current head.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
var current = repo.CurrentBranch();
if (current != null) Show(repo, current);
}
/// <summary>
/// Create branch base on existed one.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
var dialog = new CreateBranch(repo);
dialog.based = branch.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
if (!branch.IsLocal) dialog.txtName.Text = branch.Name.Substring(branch.Remote.Length + 1);
PopupManager.Show(dialog);
}
/// <summary>
/// Create branch based on tag.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
var dialog = new CreateBranch(repo);
dialog.based = tag.Name;
dialog.basedOnType.Data = dialog.FindResource("Icon.Tag") as Geometry;
dialog.basedOnDesc.Content = tag.Name;
PopupManager.Show(dialog);
}
/// <summary>
/// Create branch based on commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateBranch(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
PopupManager.Show(dialog);
}
/// <summary>
/// Start create branch.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
bool checkout = chkCheckout.IsChecked == true;
await Task.Run(() => {
if (checkout) {
bool stashed = false;
if (repo.LocalChanges().Count > 0 && AutoStash) {
Git.Stash.Push(repo, true, "CREATE BRANCH AUTO STASH", new List<string>());
stashed = true;
}
repo.Checkout($"-b {BranchName} {based}");
if (stashed) {
var stashes = repo.Stashes();
if (stashes.Count > 0) stashes[0].Pop(repo);
}
} else {
Git.Branch.Create(repo, BranchName, based);
}
});
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,68 @@
<UserControl x:Class="SourceGit.UI.CreateTag"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="224" d:DesignWidth="500" Width="500" Height="224">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="64"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Create Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="New Tag At :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" x:Name="basedOnType" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}"/>
<Label x:Name="basedOnDesc" VerticalAlignment="Center" Content="xxx"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Tag Name :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="tagName"
Height="24"
helpers:TextBoxHelper.Placeholder="Recommanded format v1.0.0-alpha">
<TextBox.Text>
<Binding ElementName="me" Path="TagName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:TagNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="4" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,4" Content="Tag Message :"/>
<TextBox Grid.Row="4" Grid.Column="1"
x:Name="tagMessage"
Height="56"
AcceptsReturn="True"
helpers:TextBoxHelper.Placeholder="Optional"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,92 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Create tag dialog
/// </summary>
public partial class CreateTag : UserControl {
private Git.Repository repo = null;
private string based = null;
/// <summary>
/// Tag name
/// </summary>
public string TagName { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
public CreateTag(Git.Repository opened) {
InitializeComponent();
repo = opened;
nameValidator.Repo = opened;
}
/// <summary>
/// Create tag using current branch.
/// </summary>
/// <param name="repo">Opened repository.</param>
public static void Show(Git.Repository repo) {
Show(repo, repo.Branches().First(b => b.IsCurrent));
}
/// <summary>
/// Create tag using branch
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
if (branch == null) {
App.RaiseError("Empty repository!");
return;
}
var dialog = new CreateTag(repo);
dialog.based = branch.Head;
dialog.basedOnType.Data = dialog.FindResource("Icon.Branch") as Geometry;
dialog.basedOnDesc.Content = branch.Name;
PopupManager.Show(dialog);
}
/// <summary>
/// Create tag using commit.
/// </summary>
/// <param name="repo"></param>
/// <param name="commit"></param>
public static void Show(Git.Repository repo, Git.Commit commit) {
var dialog = new CreateTag(repo);
dialog.based = commit.SHA;
dialog.basedOnType.Data = dialog.FindResource("Icon.Commit") as Geometry;
dialog.basedOnDesc.Content = $"{commit.ShortSHA} {commit.Subject}";
PopupManager.Show(dialog);
}
/// <summary>
/// Start to create tag.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start(object sender, RoutedEventArgs e) {
tagName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(tagName)) return;
Git.Tag.Add(repo, TagName, based, tagMessage.Text);
PopupManager.Close();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

450
SourceGit/UI/Dashboard.xaml Normal file
View file

@ -0,0 +1,450 @@
<UserControl x:Class="SourceGit.UI.Dashboard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:source="clr-namespace:SourceGit"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<UserControl.Resources>
<RoutedUICommand x:Key="OpenSearchBarCommand" Text="OpenSearchBar"/>
<RoutedUICommand x:Key="HideSearchBarCommand" Text="HideSearchBar"/>
</UserControl.Resources>
<UserControl.InputBindings>
<KeyBinding Key="F" Modifiers="Ctrl" Command="{StaticResource OpenSearchBarCommand}"/>
<KeyBinding Key="ESC" Command="{StaticResource HideSearchBarCommand}"/>
</UserControl.InputBindings>
<UserControl.CommandBindings>
<CommandBinding Command="{StaticResource OpenSearchBarCommand}" Executed="OpenSearchBar"/>
<CommandBinding Command="{StaticResource HideSearchBarCommand}" Executed="HideSearchBar"/>
</UserControl.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- TitleBar -->
<Grid Grid.Row="0" Panel.ZIndex="9999">
<Border Background="{StaticResource Brush.BG1}">
<Border.Effect>
<DropShadowEffect ShadowDepth="2" Direction="270" Opacity=".3"/>
</Border.Effect>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Navigation -->
<StackPanel Grid.Column="0" Margin="8,0,0,0" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Click="Close" ToolTip="Back To Welcome" Padding="0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Manager}"/>
<Label Content="Repositories"/>
</StackPanel>
</Button>
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Navigator}"/>
<Path Margin="6,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="repoName"/>
</StackPanel>
<!-- Common Git Options -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Click="OpenFetch" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Fetch}"/>
<Label Content="Fetch"/>
</StackPanel>
</Button>
<Button Click="OpenPull" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Pull}"/>
<Label Content="Pull"/>
</StackPanel>
</Button>
<Button Click="OpenPush" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Push}"/>
<Label Content="Push"/>
</StackPanel>
</Button>
<Button Click="OpenStash" Margin="2,0,0,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.SaveStash}"/>
<Label Content="Stash"/>
</StackPanel>
</Button>
<Button Click="OpenApply" Margin="4,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Apply}"/>
<Label Content="Apply"/>
</StackPanel>
</Button>
</StackPanel>
<!-- External Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Click="OpenSearch" Margin="4,0" ToolTip="Search Commit">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Search}"/>
<Label Content="Search"/>
</StackPanel>
</Button>
<Button Click="OpenExplorer" Margin="4,0" ToolTip="Open In File Browser">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder.Open}"/>
<Label Content="Explore"/>
</StackPanel>
</Button>
<Button Click="OpenTerminal" Margin="4,0" ToolTip="Open Git Bash">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Terminal}"/>
<Label Content="Terminal"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
<!-- Main body -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left panel -->
<Grid Grid.Column="0" x:Name="main" Background="{StaticResource Brush.BG4}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="Bool2Collapsed"/>
</Grid.Resources>
<!-- WORKSPACE -->
<Label Grid.Row="0" Margin="4,0,0,0" Content="WORKSPACE" Style="{StaticResource Style.Label.GroupHeader}" />
<ListView
Grid.Row="1"
x:Name="workspace"
Background="{StaticResource Brush.BG3}"
Style="{StaticResource Style.ListView.Borderless}">
<ListViewItem x:Name="historiesSwitch" Selected="SwitchHistories" IsSelected="True">
<StackPanel Margin="16,0,0,0" Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Histories}"/>
<Label Margin="4,0,0,0" Content="Histories"/>
</StackPanel>
</ListViewItem>
<ListViewItem x:Name="workingCopySwitch" Selected="SwitchWorkingCopy">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.WorkingCopy}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Commit"/>
<Border Grid.Column="2" x:Name="localChangesBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="localChangesCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
<ListViewItem x:Name="stashesSwitch" Selected="SwitchStashes">
<Grid Margin="16,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Stashes}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="Stashes"/>
<Border Grid.Column="2" x:Name="stashBadge" Style="{StaticResource Style.Border.Badge}">
<Label x:Name="stashCount" Margin="4,-2,4,-2" Content="999" FontSize="10"/>
</Border>
</Grid>
</ListViewItem>
</ListView>
<!-- LOCAL BRANCHES -->
<Grid Grid.Row="2" Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="LOCAL BRANCHES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenGitFlow" Background="Transparent" ToolTip="GIT FLOW">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Flow}"/>
</Button>
<Button Grid.Column="3" Click="OpenNewBranch" Background="Transparent" ToolTip="NEW BRANCH">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Branch.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="3"
x:Name="localBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
LostFocus="TreeLostFocus"
SelectedItemChanged="LocalBranchSelected"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="LocalBranchContextMenuOpening"/>
<EventSetter Event="MouseDoubleClick" Handler="LocalBranchMouseDoubleClick"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<Label Grid.Column="1" x:Name="name" Content="{Binding Name}" Padding="4,0,0,0"/>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<Border Style="{StaticResource Style.Border.Badge}" Visibility="{Binding TrackVisibility}">
<Label Margin="4,-2,4,-2" Content="{Binding Branch.UpstreamTrack}" FontSize="10"/>
</Border>
<ToggleButton
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</StackPanel>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter TargetName="name" Property="FontWeight" Value="ExtraBold"/>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Check}"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- REMOTES -->
<Grid Grid.Row="4" Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="REMOTES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenRemote" ToolTip="ADD REMOTE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{DynamicResource Icon.Remote.Add}"/>
</Button>
</Grid>
<TreeView
Grid.Row="5"
x:Name="remoteBranchTree"
Background="{StaticResource Brush.BG3}"
FontFamily="Consolas"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectedItemChanged="RemoteBranchSelected"
LostFocus="TreeLostFocus"
PreviewMouseWheel="TreeMouseWheel">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="RemoteContextMenuOpening"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:RemoteNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
</Grid>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:BranchNode}" ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" x:Name="icon" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<TextBlock Grid.Column="1" x:Name="name" Text="{Binding Name}" Padding="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}" ClipToBounds="True"/>
<ToggleButton
Grid.Column="2"
Visibility="{Binding FilterVisibility}"
IsChecked="{Binding IsFiltered, Mode=OneWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Branch}" Value="{x:Null}"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<!-- TAGS -->
<ToggleButton
x:Name="tagListToggle"
Grid.Row="6"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIShowTags, Mode=TwoWay}">
<Grid Margin="4,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="tagCount" Content="TAGS" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenNewTag" ToolTip="NEW TAG">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag.Add}"/>
</Button>
</Grid>
</ToggleButton>
<ListView
Grid.Row="7"
x:Name="tagList"
Visibility="{Binding ElementName=tagListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
Height="200"
LostFocus="TagLostFocus"
SelectionChanged="TagSelectionChanged"
ContextMenuOpening="TagContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Style="{StaticResource Style.ListView.Borderless}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type git:Tag}">
<Grid Margin="16, 0, 0, 0" Height="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}"/>
<Label Grid.Column="1" Margin="4,0,0,0" Content="{Binding Name}" Padding="4,0,0,0"/>
<ToggleButton
Grid.Column="2"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
Checked="FilterChanged"
Unchecked="FilterChanged"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="FILTER"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<!-- Right -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Abort panel -->
<Grid x:Name="abortPanel" Grid.Row="0" Background="LightGoldenrodYellow" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="txtMergeProcessing" FontWeight="DemiBold" Foreground="{StaticResource Brush.BG4}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="btnResolve" Click="Resolve" Content="RESOLVE" Margin="4">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.Bordered}">
<Setter Property="Background" Value="{StaticResource Brush.BG1}"/>
<Setter Property="Margin" Value="2"/>
</Style>
</Button.Style>
</Button>
<Button x:Name="btnContinue" Click="Continue" Content="CONTINUE" Style="{StaticResource Style.Button.AccentBordered}" Margin="4"/>
<Button Grid.Column="3" Click="Abort" Content="ABORT" Style="{StaticResource Style.Button.Bordered}" Foreground="{StaticResource Brush.BG1}" Margin="4"/>
</StackPanel>
</Grid>
<!-- Others -->
<local:Histories Grid.Row="1" x:Name="histories" Visibility="Visible"/>
<local:WorkingCopy Grid.Row="1" x:Name="commits" Visibility="Collapsed"/>
<local:Stashes Grid.Row="1" x:Name="stashes" Visibility="Collapsed"/>
</Grid>
</Grid>
<!-- Popups -->
<local:PopupManager Grid.Row="1"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,984 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace SourceGit.UI {
/// <summary>
/// Branch node in tree.
/// </summary>
public class BranchNode {
public string Name { get; set; }
public Git.Branch Branch { get; set; }
public bool IsExpanded { get; set; }
public bool IsCurrent => Branch != null ? Branch.IsCurrent : false;
public bool IsFiltered => Branch != null ? Branch.IsFiltered : false;
public string Track => Branch != null ? Branch.UpstreamTrack : "";
public Visibility FilterVisibility => Branch == null ? Visibility.Collapsed : Visibility.Visible;
public Visibility TrackVisibility => (Branch != null && !Branch.IsSameWithUpstream) ? Visibility.Visible : Visibility.Collapsed;
public List<BranchNode> Children { get; set; }
}
/// <summary>
/// Remote node in tree.
/// </summary>
public class RemoteNode {
public string Name { get; set; }
public bool IsExpanded { get; set; }
public List<BranchNode> Children { get; set; }
}
/// <summary>
/// Dashboard for opened repository.
/// </summary>
public partial class Dashboard : UserControl {
private Git.Repository repo = null;
private List<BranchNode> cachedLocalBranches = new List<BranchNode>();
private List<RemoteNode> cachedRemotes = new List<RemoteNode>();
private string abortCommand = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo">Opened repository.</param>
public Dashboard(Git.Repository opened) {
opened.OnWorkingCopyChanged = UpdateLocalChanges;
opened.OnTagChanged = UpdateTags;
opened.OnStashChanged = UpdateStashes;
opened.OnBranchChanged = () => UpdateBranches(false);
opened.OnCommitsChanged = UpdateHistories;
opened.OnNavigateCommit = commit => {
Dispatcher.Invoke(() => {
workspace.SelectedItem = historiesSwitch;
histories.Navigate(commit);
});
};
InitializeComponent();
repo = opened;
repoName.Content = repo.Name;
histories.Repo = opened;
commits.Repo = opened;
UpdateBranches();
UpdateHistories();
UpdateLocalChanges();
UpdateStashes();
UpdateTags();
}
#region DATA_UPDATE
private void UpdateHistories() {
Dispatcher.Invoke(() => {
histories.SetLoadingEnabled(true);
});
Task.Run(() => {
var args = "-5000 ";
if (repo.LogFilters.Count > 0) {
args = args + string.Join(" ", repo.LogFilters);
} else {
args = args + "--branches --remotes --tags";
}
var commits = repo.Commits(args);
histories.SetCommits(commits);
});
}
private void UpdateLocalChanges() {
Task.Run(() => {
var changes = repo.LocalChanges();
var conflicts = commits.SetData(changes);
Dispatcher.Invoke(() => {
localChangesBadge.Visibility = changes.Count == 0 ? Visibility.Collapsed : Visibility.Visible;
localChangesCount.Content = changes.Count;
btnContinue.Visibility = conflicts ? Visibility.Collapsed : Visibility.Visible;
DetectMergeState();
});
});
}
private void UpdateStashes() {
Task.Run(() => {
var data = repo.Stashes();
Dispatcher.Invoke(() => {
stashBadge.Visibility = data.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
stashCount.Content = data.Count;
stashes.SetData(repo, data);
});
});
}
private void BackupBranchNodeExpandState(Dictionary<string, bool> states, List<BranchNode> nodes, string prefix) {
foreach (var node in nodes) {
var path = prefix + "/" + node.Name;
states.Add(path, node.IsExpanded);
BackupBranchNodeExpandState(states, node.Children, path);
}
}
private void MakeBranchNode(Git.Branch branch, List<BranchNode> collection, Dictionary<string, BranchNode> folders, Dictionary<string, bool> expandStates, string prefix) {
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (!branch.IsLocal) {
if (subs.Length < 2) return;
subs = subs.Skip(1).ToArray();
}
branch.IsFiltered = repo.LogFilters.Contains(branch.FullName);
if (subs.Length == 1) {
var node = new BranchNode() {
Name = subs[0],
Branch = branch,
Children = new List<BranchNode>(),
};
collection.Add(node);
} else {
BranchNode lastFolder = null;
string path = prefix;
for (int i = 0; i < subs.Length - 1; i++) {
path = path + "/" + subs[i];
if (folders.ContainsKey(path)) {
lastFolder = folders[path];
} else if (lastFolder == null) {
lastFolder = new BranchNode() {
Name = subs[i],
IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false,
Children = new List<BranchNode>(),
};
collection.Add(lastFolder);
folders.Add(path, lastFolder);
} else {
var folder = new BranchNode() {
Name = subs[i],
IsExpanded = expandStates.ContainsKey(path) ? expandStates[path] : false,
Children = new List<BranchNode>(),
};
lastFolder.Children.Add(folder);
folders.Add(path, folder);
lastFolder = folder;
}
}
BranchNode node = new BranchNode();
node.Name = subs[subs.Length - 1];
node.Branch = branch;
node.Children = new List<BranchNode>();
lastFolder.Children.Add(node);
}
}
private void SortBranchNodes(List<BranchNode> collection) {
collection.Sort((l, r) => {
if (l.Branch != null) {
return r.Branch != null ? l.Branch.Name.CompareTo(r.Branch.Name) : -1;
} else {
return r.Branch == null ? l.Name.CompareTo(r.Name) : 1;
}
});
foreach (var sub in collection) {
if (sub.Children.Count > 0) SortBranchNodes(sub.Children);
}
}
private void UpdateBranches(bool force = true) {
Task.Run(() => {
var branches = repo.Branches(force);
var localBranchNodes = new List<BranchNode>();
var remoteNodes = new List<RemoteNode>();
var remoteMap = new Dictionary<string, RemoteNode>();
var folders = new Dictionary<string, BranchNode>();
var states = new Dictionary<string, bool>();
BackupBranchNodeExpandState(states, cachedLocalBranches, "locals");
foreach (var r in cachedRemotes) {
var prefix = $"remotes/{r.Name}";
states.Add(prefix, r.IsExpanded);
BackupBranchNodeExpandState(states, r.Children, prefix);
}
foreach (var b in branches) {
if (b.IsLocal) {
MakeBranchNode(b, localBranchNodes, folders, states, "locals");
} else {
RemoteNode remote = null;
if (!remoteMap.ContainsKey(b.Remote)) {
var key = "remotes/" + b.Remote;
remote = new RemoteNode() {
Name = b.Remote,
IsExpanded = states.ContainsKey(key) ? states[key] : false,
Children = new List<BranchNode>(),
};
remoteNodes.Add(remote);
remoteMap.Add(b.Remote, remote);
} else {
remote = remoteMap[b.Remote];
}
MakeBranchNode(b, remote.Children, folders, states, "remotes");
}
}
SortBranchNodes(localBranchNodes);
foreach (var r in remoteNodes) SortBranchNodes(r.Children);
cachedLocalBranches = localBranchNodes;
cachedRemotes = remoteNodes;
Dispatcher.Invoke(() => {
localBranchTree.ItemsSource = localBranchNodes;
remoteBranchTree.ItemsSource = remoteNodes;
});
});
}
private void UpdateTags() {
Task.Run(() => {
var tags = repo.Tags(true);
foreach (var t in tags) t.IsFiltered = repo.LogFilters.Contains(t.Name);
Dispatcher.Invoke(() => {
tagCount.Content = $"TAGS ({tags.Count})";
tagList.ItemsSource = tags;
});
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
localBranchTree.ItemsSource = null;
remoteBranchTree.ItemsSource = null;
tagList.ItemsSource = null;
cachedLocalBranches.Clear();
cachedRemotes.Clear();
}
#endregion
#region TOOLBAR
private void Close(object sender, RoutedEventArgs e) {
if (PopupManager.IsLocked()) return;
PopupManager.Close();
cachedLocalBranches.Clear();
cachedRemotes.Clear();
repo.Close();
}
private void OpenFetch(object sender, RoutedEventArgs e) {
Fetch.Show(repo);
}
private void OpenPull(object sender, RoutedEventArgs e) {
Pull.Show(repo);
}
private void OpenPush(object sender, RoutedEventArgs e) {
Push.Show(repo);
}
private void OpenStash(object sender, RoutedEventArgs e) {
Stash.Show(repo, new List<string>());
}
private void OpenApply(object sender, RoutedEventArgs e) {
Apply.Show(repo);
}
private void OpenSearch(object sender, RoutedEventArgs e) {
workspace.SelectedItem = historiesSwitch;
if (histories.searchBar.Margin.Top == 0) {
histories.HideSearchBar();
} else {
histories.OpenSearchBar();
}
}
private void OpenExplorer(object sender, RoutedEventArgs e) {
Process.Start(repo.Path);
}
private void OpenTerminal(object sender, RoutedEventArgs e) {
var bash = Path.Combine(App.Preference.GitExecutable, "..", "bash.exe");
if (!File.Exists(bash)) {
App.RaiseError("Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe");
return;
}
var start = new ProcessStartInfo();
start.WorkingDirectory = repo.Path;
start.FileName = bash;
Process.Start(start);
}
#endregion
#region HOT_KEYS
public void OpenSearchBar(object sender, ExecutedRoutedEventArgs e) {
workspace.SelectedItem = historiesSwitch;
histories.OpenSearchBar();
}
public void HideSearchBar(object sender, ExecutedRoutedEventArgs e) {
if (histories.Visibility == Visibility.Visible) {
histories.HideSearchBar();
}
}
#endregion
#region MERGE_ABORTS
public void DetectMergeState() {
var gitDir = Path.Combine(repo.Path, ".git");
var cherryPickMerge = Path.Combine(gitDir, "CHERRY_PICK_HEAD");
var rebaseMerge = Path.Combine(gitDir, "REBASE_HEAD");
var revertMerge = Path.Combine(gitDir, "REVERT_HEAD");
var otherMerge = Path.Combine(gitDir, "MERGE_HEAD");
if (File.Exists(cherryPickMerge)) {
abortCommand = "cherry-pick";
txtMergeProcessing.Content = "Cherry-Pick merge request detected! Press 'Abort' to restore original HEAD";
} else if (File.Exists(rebaseMerge)) {
abortCommand = "rebase";
txtMergeProcessing.Content = "Rebase merge request detected! Press 'Abort' to restore original HEAD";
} else if (File.Exists(revertMerge)) {
abortCommand = "revert";
txtMergeProcessing.Content = "Revert merge request detected! Press 'Abort' to restore original HEAD";
} else if (File.Exists(otherMerge)) {
abortCommand = "merge";
txtMergeProcessing.Content = "Merge request detected! Press 'Abort' to restore original HEAD";
} else {
abortCommand = null;
}
if (abortCommand != null) {
abortPanel.Visibility = Visibility.Visible;
if (commits.Visibility == Visibility.Visible) {
btnResolve.Visibility = Visibility.Collapsed;
} else {
btnResolve.Visibility = Visibility.Visible;
}
commits.LoadMergeMessage();
} else {
abortPanel.Visibility = Visibility.Collapsed;
}
}
private void Resolve(object sender, RoutedEventArgs e) {
workspace.SelectedItem = workingCopySwitch;
}
private async void Continue(object sender, RoutedEventArgs e) {
if (abortCommand == null) return;
await Task.Run(() => {
repo.SetWatcherEnabled(false);
var errs = repo.RunCommand($"-c core.editor=true {abortCommand} --continue", null);
repo.AssertCommand(errs);
});
commits.ClearMessage();
}
private async void Abort(object sender, RoutedEventArgs e) {
if (abortCommand == null) return;
await Task.Run(() => {
repo.SetWatcherEnabled(false);
var errs = repo.RunCommand($"{abortCommand} --abort", null);
repo.AssertCommand(errs);
});
commits.ClearMessage();
}
#endregion
#region WORKSPACE
private void SwitchWorkingCopy(object sender, RoutedEventArgs e) {
if (commits == null || histories == null || stashes == null) return;
commits.Visibility = Visibility.Visible;
histories.Visibility = Visibility.Collapsed;
stashes.Visibility = Visibility.Collapsed;
if (abortPanel.Visibility == Visibility.Visible) {
btnResolve.Visibility = Visibility.Collapsed;
}
}
private void SwitchHistories(object sender, RoutedEventArgs e) {
if (commits == null || histories == null || stashes == null) return;
commits.Visibility = Visibility.Collapsed;
histories.Visibility = Visibility.Visible;
stashes.Visibility = Visibility.Collapsed;
if (abortPanel.Visibility == Visibility.Visible) {
btnResolve.Visibility = Visibility.Visible;
}
}
private void SwitchStashes(object sender, RoutedEventArgs e) {
if (commits == null || histories == null || stashes == null) return;
commits.Visibility = Visibility.Collapsed;
histories.Visibility = Visibility.Collapsed;
stashes.Visibility = Visibility.Visible;
if (abortPanel.Visibility == Visibility.Visible) {
btnResolve.Visibility = Visibility.Visible;
}
}
#endregion
#region LOCAL_BRANCHES
private void OpenNewBranch(object sender, RoutedEventArgs e) {
CreateBranch.Show(repo);
}
private void OpenGitFlow(object sender, RoutedEventArgs ev) {
var button = sender as Button;
if (button.ContextMenu == null) {
button.ContextMenu = new ContextMenu();
button.ContextMenu.PlacementTarget = button;
button.ContextMenu.Placement = PlacementMode.Bottom;
button.ContextMenu.StaysOpen = false;
button.ContextMenu.Focusable = true;
} else {
button.ContextMenu.Items.Clear();
}
if (repo.IsGitFlowEnabled()) {
var startFeature = new MenuItem();
startFeature.Header = "Start Feature ...";
startFeature.Click += (o, e) => {
GitFlowStartBranch.Show(repo, Git.Branch.Type.Feature);
e.Handled = true;
};
var startRelease = new MenuItem();
startRelease.Header = "Start Release ...";
startRelease.Click += (o, e) => {
GitFlowStartBranch.Show(repo, Git.Branch.Type.Release);
e.Handled = true;
};
var startHotfix = new MenuItem();
startHotfix.Header = "Start Hotfix ...";
startHotfix.Click += (o, e) => {
GitFlowStartBranch.Show(repo, Git.Branch.Type.Hotfix);
e.Handled = true;
};
button.ContextMenu.Items.Add(startFeature);
button.ContextMenu.Items.Add(startRelease);
button.ContextMenu.Items.Add(startHotfix);
} else {
var init = new MenuItem();
init.Header = "Initialize Git-Flow";
init.Click += (o, e) => {
GitFlowSetup.Show(repo);
e.Handled = true;
};
button.ContextMenu.Items.Add(init);
}
button.ContextMenu.IsOpen = true;
ev.Handled = true;
}
private void LocalBranchSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
var node = e.NewValue as BranchNode;
if (node == null || node.Branch == null) return;
repo.OnNavigateCommit?.Invoke(node.Branch.Head);
}
private void LocalBranchMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var node = (sender as TreeViewItem).DataContext as BranchNode;
if (node == null || node.Branch == null) return;
Task.Run(() => repo.Checkout(node.Branch.Name));
}
private void LocalBranchContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var node = (sender as TreeViewItem).DataContext as BranchNode;
if (node == null || node.Branch == null) return;
var menu = new ContextMenu();
var branch = node.Branch;
var push = new MenuItem();
push.Header = $"Push '{branch.Name}'";
push.Click += (o, e) => {
Push.Show(repo, branch);
e.Handled = true;
};
if (branch.IsCurrent) {
var discard = new MenuItem();
discard.Header = "Discard all changes";
discard.Click += (o, e) => {
Discard.Show(repo, null);
e.Handled = true;
};
menu.Items.Add(discard);
menu.Items.Add(new Separator());
if (!string.IsNullOrEmpty(branch.Upstream)) {
var upstream = branch.Upstream.Substring(13);
var fastForward = new MenuItem();
fastForward.Header = $"Fast-Forward to '{upstream}'";
fastForward.Click += (o, e) => {
Merge.StartDirectly(repo, upstream, branch.Name);
e.Handled = true;
};
var pull = new MenuItem();
pull.Header = $"Pull '{upstream}'";
pull.Click += (o, e) => {
Pull.Show(repo);
e.Handled = true;
};
menu.Items.Add(fastForward);
menu.Items.Add(pull);
}
menu.Items.Add(push);
} else {
var current = repo.CurrentBranch();
var checkout = new MenuItem();
checkout.Header = $"Checkout {branch.Name}";
checkout.Click += (o, e) => {
Task.Run(() => repo.Checkout(node.Branch.Name));
e.Handled = true;
};
menu.Items.Add(checkout);
menu.Items.Add(new Separator());
menu.Items.Add(push);
var merge = new MenuItem();
merge.Header = $"Merge '{branch.Name}' into '{current.Name}'";
merge.Click += (o, e) => {
Merge.Show(repo, branch.Name, current.Name);
e.Handled = true;
};
menu.Items.Add(merge);
var rebase = new MenuItem();
rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'";
rebase.Click += (o, e) => {
Rebase.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(rebase);
}
if (branch.Kind != Git.Branch.Type.Normal) {
menu.Items.Add(new Separator());
var icon = new System.Windows.Shapes.Path();
icon.Style = FindResource("Style.Icon") as Style;
icon.Data = FindResource("Icon.Flow") as Geometry;
icon.Width = 10;
var finish = new MenuItem();
finish.Header = $"Git Flow - Finish '{branch.Name}'";
finish.Icon = icon;
finish.Click += (o, e) => {
GitFlowFinishBranch.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(finish);
}
var rename = new MenuItem();
rename.Header = $"Rename '{branch.Name}'";
rename.Click += (o, e) => {
RenameBranch.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(new Separator());
menu.Items.Add(rename);
var delete = new MenuItem();
delete.Header = $"Delete '{branch.Name}'";
delete.IsEnabled = !branch.IsCurrent;
delete.Click += (o, e) => {
DeleteBranch.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(delete);
menu.Items.Add(new Separator());
var createBranch = new MenuItem();
createBranch.Header = "Create Branch";
createBranch.Click += (o, e) => {
CreateBranch.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(createBranch);
var createTag = new MenuItem();
createTag.Header = "Create Tag";
createTag.Click += (o, e) => {
CreateTag.Show(repo, branch);
e.Handled = true;
};
menu.Items.Add(createTag);
menu.Items.Add(new Separator());
var copy = new MenuItem();
copy.Header = "Copy Branch Name";
copy.Click += (o, e) => {
Clipboard.SetText(branch.Name);
e.Handled = true;
};
menu.Items.Add(copy);
menu.IsOpen = true;
ev.Handled = true;
}
#endregion
#region REMOTE_BRANCHES
private void OpenRemote(object sender, RoutedEventArgs e) {
Remote.Show(repo);
}
private void OpenRemoteContextMenu(RemoteNode node) {
var fetch = new MenuItem();
fetch.Header = $"Fetch '{node.Name}'";
fetch.Click += (o, e) => {
Fetch.Show(repo, node.Name);
e.Handled = true;
};
var edit = new MenuItem();
edit.Header = $"Edit '{node.Name}'";
edit.Click += (o, e) => {
var remotes = repo.Remotes();
var found = remotes.Find(r => r.Name == node.Name);
if (found != null) Remote.Show(repo, found);
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = $"Delete '{node.Name}'";
delete.Click += (o, e) => {
DeleteRemote.Show(repo, node.Name);
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = "Copy Remote URL";
copy.Click += (o, e) => {
var remotes = repo.Remotes();
var found = remotes.Find(r => r.Name == node.Name);
if (found != null) Clipboard.SetText(found.URL);
e.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(fetch);
menu.Items.Add(new Separator());
menu.Items.Add(edit);
menu.Items.Add(delete);
menu.Items.Add(new Separator());
menu.Items.Add(copy);
menu.IsOpen = true;
}
private void OpenRemoteBranchContextMenu(BranchNode node) {
var branch = node.Branch;
var current = repo.CurrentBranch();
if (current == null) return;
var checkout = new MenuItem();
checkout.Header = $"Checkout '{branch.Name}'";
checkout.Click += (o, e) => {
var branches = repo.Branches();
var tracked = null as Git.Branch;
var upstream = $"refs/remotes/{branch.Name}";
foreach (var b in branches) {
if (b.IsLocal && b.Upstream == upstream) {
tracked = b;
break;
}
}
if (tracked == null) {
CreateBranch.Show(repo, branch);
} else if (!tracked.IsCurrent) {
Task.Run(() => repo.Checkout(tracked.Name));
}
e.Handled = true;
};
var pull = new MenuItem();
pull.Header = $"Pull '{branch.Name}' into '{current.Name}'";
pull.Click += (o, e) => {
Pull.Show(repo, branch.Name);
e.Handled = true;
};
var merge = new MenuItem();
merge.Header = $"Merge '{branch.Name}' into '{current.Name}'";
merge.Click += (o, e) => {
Merge.Show(repo, branch.Name, current.Name);
e.Handled = true;
};
var rebase = new MenuItem();
rebase.Header = $"Rebase '{current.Name}' on '{branch.Name}'";
rebase.Click += (o, e) => {
Rebase.Show(repo, branch);
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = $"Delete '{branch.Name}'";
delete.Click += (o, e) => {
DeleteBranch.Show(repo, branch);
e.Handled = true;
};
var createBranch = new MenuItem();
createBranch.Header = "Create New Branch";
createBranch.Click += (o, e) => {
CreateBranch.Show(repo, branch);
e.Handled = true;
};
var createTag = new MenuItem();
createTag.Header = "Create New Tag";
createTag.Click += (o, e) => {
CreateTag.Show(repo, branch);
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = "Copy Branch Name";
copy.Click += (o, e) => {
Clipboard.SetText(branch.Name);
e.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(checkout);
menu.Items.Add(new Separator());
menu.Items.Add(pull);
menu.Items.Add(merge);
menu.Items.Add(rebase);
menu.Items.Add(new Separator());
menu.Items.Add(delete);
menu.Items.Add(new Separator());
menu.Items.Add(createBranch);
menu.Items.Add(createTag);
menu.Items.Add(new Separator());
menu.Items.Add(copy);
menu.IsOpen = true;
}
private void RemoteBranchSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
var node = e.NewValue as BranchNode;
if (node == null || node.Branch == null) return;
repo.OnNavigateCommit?.Invoke(node.Branch.Head);
}
private void RemoteContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var remoteNode = (sender as TreeViewItem).DataContext as RemoteNode;
if (remoteNode != null) {
OpenRemoteContextMenu(remoteNode);
ev.Handled = true;
return;
}
var branchNode = (sender as TreeViewItem).DataContext as BranchNode;
if (branchNode != null && branchNode.Branch != null) {
OpenRemoteBranchContextMenu(branchNode);
ev.Handled = true;
return;
}
}
#endregion
#region TAGS
private void OpenNewTag(object sender, RoutedEventArgs e) {
CreateTag.Show(repo);
}
private void TagLostFocus(object sender, RoutedEventArgs e) {
(sender as ListView).UnselectAll();
}
private void TagSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count == 1) {
var item = e.AddedItems[0] as Git.Tag;
repo.OnNavigateCommit?.Invoke(item.SHA);
}
}
private void TagContextMenuOpening(object sender, ContextMenuEventArgs e) {
var tag = (sender as ListView).SelectedItem as Git.Tag;
if (tag == null) return;
var createBranch = new MenuItem();
createBranch.Header = "Create New Branch";
createBranch.Click += (o, ev) => {
CreateBranch.Show(repo, tag);
ev.Handled = true;
};
var pushTag = new MenuItem();
pushTag.Header = $"Push '{tag.Name}'";
pushTag.Click += (o, ev) => {
PushTag.Show(repo, tag);
ev.Handled = true;
};
var deleteTag = new MenuItem();
deleteTag.Header = $"Delete '{tag.Name}'";
deleteTag.Click += (o, ev) => {
DeleteTag.Show(repo, tag);
ev.Handled = true;
};
var copy = new MenuItem();
copy.Header = "Copy Name";
copy.Click += (o, ev) => {
Clipboard.SetText(tag.Name);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(createBranch);
menu.Items.Add(new Separator());
menu.Items.Add(pushTag);
menu.Items.Add(deleteTag);
menu.Items.Add(new Separator());
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region TREES
private TreeViewItem FindTreeViewItem(ItemsControl item, BranchNode node) {
if (item == null) return null;
var data = item.DataContext as BranchNode;
if (data == node) return item as TreeViewItem;
for (int i = 0; i < item.Items.Count; i++) {
var childContainer = item.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
var child = FindTreeViewItem(childContainer, node);
if (child != null) return child;
}
return null;
}
private void TreeLostFocus(object sender, RoutedEventArgs e) {
var tree = sender as TreeView;
var remote = tree.SelectedItem as RemoteNode;
if (remote != null) {
var remoteItem = tree.ItemContainerGenerator.ContainerFromItem(remote) as TreeViewItem;
if (remoteItem != null) remoteItem.IsSelected = false;
return;
}
var node = tree.SelectedItem as BranchNode;
if (node == null) return;
var item = FindTreeViewItem(tree, node);
if (item != null) item.IsSelected = false;
}
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
if (owner == null) return null;
if (owner is ScrollViewer) return owner as ScrollViewer;
int n = VisualTreeHelper.GetChildrenCount(owner);
for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
var deep = GetScrollViewer(child);
if (deep != null) return deep;
}
return null;
}
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView);
if (scroll == null) return;
if (e.Delta > 0) {
scroll.LineUp();
} else {
scroll.LineDown();
}
e.Handled = true;
}
#endregion
#region FILETER
private void FilterChanged(object sender, RoutedEventArgs e) {
var toggle = sender as ToggleButton;
if (toggle == null) return;
if (toggle.DataContext is BranchNode) {
var branch = (toggle.DataContext as BranchNode).Branch;
if (branch == null) return;
if (toggle.IsChecked == true) {
if (!repo.LogFilters.Contains(branch.FullName)) {
repo.LogFilters.Add(branch.FullName);
}
if (!string.IsNullOrEmpty(branch.Upstream) && !repo.LogFilters.Contains(branch.Upstream)) {
repo.LogFilters.Add(branch.Upstream);
UpdateBranches(false);
}
} else {
repo.LogFilters.Remove(branch.FullName);
if (!string.IsNullOrEmpty(branch.Upstream)) {
repo.LogFilters.Remove(branch.Upstream);
UpdateBranches(false);
}
}
}
if (toggle.DataContext is Git.Tag) {
var tag = toggle.DataContext as Git.Tag;
if (toggle.IsChecked == true) {
if (!repo.LogFilters.Contains(tag.Name)) {
repo.LogFilters.Add(tag.Name);
}
} else {
repo.LogFilters.Remove(tag.Name);
}
}
UpdateHistories();
}
#endregion
}
}

View file

@ -0,0 +1,51 @@
<UserControl x:Class="SourceGit.UI.DeleteBranch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Branch"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Branch :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}" Margin="4,0"/>
<Label x:Name="branchName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete branch
/// </summary>
public partial class DeleteBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch branch = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository.</param>
/// <param name="target">Branch to be deleted.</param>
public DeleteBranch(Git.Repository opened, Git.Branch target) {
InitializeComponent();
repo = opened;
branch = target;
branchName.Content = target.Name;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="opened"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository opened, Git.Branch branch) {
PopupManager.Show(new DeleteBranch(opened, branch));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => branch.Delete(repo));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,50 @@
<UserControl x:Class="SourceGit.UI.DeleteRemote"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Remote"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}" Margin="4,0"/>
<Label x:Name="remoteName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to delete a remote
/// </summary>
public partial class DeleteRemote : UserControl {
private Git.Repository repo = null;
private string remote = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
/// <param name="target">Remote to be deleted</param>
public DeleteRemote(Git.Repository opened, string target) {
InitializeComponent();
repo = opened;
remote = target;
remoteName.Content = target;
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="remote"></param>
public static void Show(Git.Repository opened, string remote) {
PopupManager.Show(new DeleteRemote(opened, remote));
}
/// <summary>
/// Delete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => Git.Remote.Delete(repo, remote));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,53 @@
<UserControl x:Class="SourceGit.UI.DeleteTag"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Delete Tag"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Tag :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Tag}" Margin="4,0"/>
<Label x:Name="tagName"/>
</StackPanel>
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="chkWithRemote" Content="Delete from remote repositories"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Delete tag dialog.
/// </summary>
public partial class DeleteTag : UserControl {
private Git.Repository repo = null;
private Git.Tag tag = null;
/// <summary>
/// Constructor
/// </summary>
/// <param name="repo">Opened repo</param>
/// <param name="tag">Delete tag</param>
public DeleteTag(Git.Repository repo, Git.Tag tag) {
this.repo = repo;
this.tag = tag;
InitializeComponent();
tagName.Content = tag.Name;
}
/// <summary>
/// Display this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="tag"></param>
public static void Show(Git.Repository repo, Git.Tag tag) {
PopupManager.Show(new DeleteTag(repo, tag));
}
/// <summary>
/// Start request.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
var push = chkWithRemote.IsChecked == true;
await Task.Run(() => Git.Tag.Delete(repo, tag.Name, push));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,139 @@
<UserControl x:Class="SourceGit.UI.DiffViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
FontFamily="Consolas">
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
<StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="orgFileName" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
<TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
</StackPanel>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
</StackPanel>
</Border>
<Grid Grid.Row="1" ClipToBounds="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="leftLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
BorderThickness="0"
Background="Transparent"
IsReadOnly="True"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="leftText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="LeftSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
</Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="rightLineNumber"
Grid.Column="0"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Margin="4,0,4,0"
FontSize="13"
HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="rightText"
Grid.Column="2"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="RightSizeChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
</Grid>
</Grid>
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/>
<Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
</UserControl>

View file

@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Viewer for git diff
/// </summary>
public partial class DiffViewer : UserControl {
private double minWidth = 0;
/// <summary>
/// Line mode.
/// </summary>
public enum LineMode {
Normal,
Indicator,
Empty,
Added,
Deleted,
}
/// <summary>
/// Constructor
/// </summary>
public DiffViewer() {
InitializeComponent();
Reset();
}
/// <summary>
///
/// </summary>
/// <param name="lines"></param>
/// <param name="file"></param>
/// <param name="orgFile"></param>
public void SetData(List<string> lines, string file, string orgFile = null) {
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
fileName.Text = file;
if (!string.IsNullOrEmpty(orgFile)) {
orgFileNamePanel.Visibility = Visibility.Visible;
orgFileName.Text = orgFile;
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed;
}
leftText.Document.Blocks.Clear();
rightText.Document.Blocks.Clear();
leftLineNumber.Text = "";
rightLineNumber.Text = "";
Regex regex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
bool started = false;
List<Paragraph> leftData = new List<Paragraph>();
List<Paragraph> rightData = new List<Paragraph>();
List<string> leftNumbers = new List<string>();
List<string> rightNumbers = new List<string>();
int leftLine = 0;
int rightLine = 0;
bool bLastLeft = true;
foreach (var line in lines) {
if (!started) {
var match = regex.Match(line);
if (!match.Success) continue;
MakeParagraph(leftData, line, LineMode.Indicator);
MakeParagraph(rightData, line, LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
leftLine = int.Parse(match.Groups[1].Value);
rightLine = int.Parse(match.Groups[2].Value);
started = true;
continue;
}
if (line[0] == '-') {
MakeParagraph(leftData, line.Substring(1), LineMode.Deleted);
leftNumbers.Add(leftLine.ToString());
leftLine++;
bLastLeft = true;
} else if (line[0] == '+') {
MakeParagraph(rightData, line.Substring(1), LineMode.Added);
rightNumbers.Add(rightLine.ToString());
rightLine++;
bLastLeft = false;
} else if (line[0] == '\\') {
if (bLastLeft) {
MakeParagraph(leftData, line.Substring(1), LineMode.Indicator);
leftNumbers.Add("");
} else {
MakeParagraph(rightData, line.Substring(1), LineMode.Indicator);
rightNumbers.Add("");
}
} else {
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
bLastLeft = true;
var match = regex.Match(line);
if (match.Success) {
MakeParagraph(leftData, line, LineMode.Indicator);
MakeParagraph(rightData, line, LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
leftLine = int.Parse(match.Groups[1].Value);
rightLine = int.Parse(match.Groups[2].Value);
} else {
var data = line.Substring(1);
MakeParagraph(leftData, data, LineMode.Normal);
MakeParagraph(rightData, data, LineMode.Normal);
leftNumbers.Add(leftLine.ToString());
rightNumbers.Add(rightLine.ToString());
leftLine++;
rightLine++;
}
}
}
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
if (leftData.Count == 0) {
MakeParagraph(leftData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
MakeParagraph(rightData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
leftNumbers.Add("");
rightNumbers.Add("");
}
leftLineNumber.Text = string.Join("\n", leftNumbers);
rightLineNumber.Text = string.Join("\n", rightNumbers);
leftText.Document.PageWidth = minWidth + 16;
rightText.Document.PageWidth = minWidth + 16;
leftText.Document.Blocks.AddRange(leftData);
rightText.Document.Blocks.AddRange(rightData);
leftText.ScrollToHome();
mask.Visibility = Visibility.Collapsed;
}
/// <summary>
/// Reset data.
/// </summary>
public void Reset() {
mask.Visibility = Visibility.Visible;
}
/// <summary>
/// Make paragraph.
/// </summary>
/// <param name="collection"></param>
/// <param name="content"></param>
/// <param name="mode"></param>
private void MakeParagraph(List<Paragraph> collection, string content, LineMode mode) {
Paragraph p = new Paragraph(new Run(content));
p.Margin = new Thickness(0);
p.Padding = new Thickness();
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
switch (mode) {
case LineMode.Normal:
break;
case LineMode.Indicator:
p.Foreground = Brushes.Gray;
p.FontStyle = FontStyles.Italic;
break;
case LineMode.Empty:
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
break;
case LineMode.Added:
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
break;
case LineMode.Deleted:
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
break;
}
var formatter = new FormattedText(
content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
leftText.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) minWidth = formatter.Width;
collection.Add(p);
}
/// <summary>
/// Fit both side with empty lines.
/// </summary>
/// <param name="left"></param>
/// <param name="leftNumbers"></param>
/// <param name="right"></param>
/// <param name="rightNumbers"></param>
private void FitBothSide(List<Paragraph> left, List<string> leftNumbers, List<Paragraph> right, List<string> rightNumbers) {
int leftCount = left.Count;
int rightCount = right.Count;
int diff = 0;
List<Paragraph> fitContent = null;
List<string> fitNumber = null;
if (leftCount > rightCount) {
diff = leftCount - rightCount;
fitContent = right;
fitNumber = rightNumbers;
} else if (rightCount > leftCount) {
diff = rightCount - leftCount;
fitContent = left;
fitNumber = leftNumbers;
}
for (int i = 0; i < diff; i++) {
MakeParagraph(fitContent, "", LineMode.Empty);
fitNumber.Add("");
}
}
/// <summary>
/// Sync scroll both sides.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
if (leftText.VerticalOffset != e.VerticalOffset) {
leftText.ScrollToVerticalOffset(e.VerticalOffset);
}
if (rightText.VerticalOffset != e.VerticalOffset) {
rightText.ScrollToVerticalOffset(e.VerticalOffset);
}
leftLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
rightLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
} else {
if (leftText.HorizontalOffset != e.HorizontalOffset) {
leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
if (rightText.HorizontalOffset != e.HorizontalOffset) {
rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
/// <summary>
/// Scroll using mouse wheel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
var text = sender as RichTextBox;
if (text == null) return;
if (e.Delta > 0) {
text.LineUp();
} else {
text.LineDown();
}
e.Handled = true;
}
private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
if (leftText.Document.PageWidth < leftText.ActualWidth) {
leftText.Document.PageWidth = leftText.ActualWidth;
}
}
private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
if (rightText.Document.PageWidth < rightText.ActualWidth) {
rightText.Document.PageWidth = rightText.ActualWidth;
}
}
}
}

55
SourceGit/UI/Discard.xaml Normal file
View file

@ -0,0 +1,55 @@
<UserControl x:Class="SourceGit.UI.Discard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Confirm To Discard Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Changes :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path x:Name="icon" Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}" Fill="{StaticResource Brush.FG2}" Margin="4,0"/>
<Label x:Name="txtPath"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="1" Content="You can't undo this action!!!" Foreground="{StaticResource Brush.FG2}"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm to discard changes dialog.
/// </summary>
public partial class Discard : UserControl {
private Git.Repository repo = null;
private List<Git.Change> changes = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public Discard(Git.Repository opened, List<Git.Change> targets) {
repo = opened;
changes = targets;
InitializeComponent();
if (changes == null || changes.Count == 0) {
txtPath.Content = "All local changes in working copy.";
icon.Data = FindResource("Icon.Folder") as Geometry;
} else if (changes.Count == 1) {
txtPath.Content = changes[0].Path;
} else {
txtPath.Content = $"Total {changes.Count} changes ...";
}
}
/// <summary>
/// Show this dialog
/// </summary>
/// <param name="opened"></param>
/// <param name="targets"></param>
public static void Show(Git.Repository opened, List<Git.Change> targets) {
PopupManager.Show(new Discard(opened, targets));
}
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.Discard(changes));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

78
SourceGit/UI/Fetch.xaml Normal file
View file

@ -0,0 +1,78 @@
<UserControl x:Class="SourceGit.UI.Fetch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<converters:InverseBool x:Key="InverseBool"/>
</Grid.Resources>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Fetch Remote Changes"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Remote :"/>
<ComboBox x:Name="combRemotes" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" IsEnabled="{Binding ElementName=chkFetchAll, Path=IsChecked, Converter={StaticResource InverseBool}}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type git:Remote}">
<StackPanel Orientation="Horizontal" Height="20">
<Path Margin="4,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Remote}"/>
<Label Content="{Binding Name}" Padding="8,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Grid.Row="3" Grid.Column="1"
x:Name="chkFetchAll"
IsChecked="True"
Content="Fetch all remotes"/>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkPrune"
IsChecked="True"
Content="Prune remote dead branches"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Start" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="7" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<Label x:Name="statusMsg" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Fetch dialog.
/// </summary>
public partial class Fetch : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened">Opened repository</param>
/// <param name="preferRemote">Prefer selected remote.</param>
public Fetch(Git.Repository opened, string preferRemote) {
repo = opened;
InitializeComponent();
Task.Run(() => {
var remotes = repo.Remotes();
Dispatcher.Invoke(() => {
combRemotes.ItemsSource = remotes;
if (preferRemote != null) {
combRemotes.SelectedIndex = remotes.FindIndex(r => r.Name == preferRemote);
chkFetchAll.IsChecked = false;
} else {
combRemotes.SelectedIndex = 0;
chkFetchAll.IsChecked = true;
}
});
});
}
/// <summary>
/// Show fetch dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="preferRemote"></param>
public static void Show(Git.Repository repo, string preferRemote = null) {
PopupManager.Show(new Fetch(repo, preferRemote));
}
/// <summary>
/// Start fetch
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Start(object sender, RoutedEventArgs e) {
bool prune = chkPrune.IsChecked == true;
PopupManager.Lock();
status.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
if (chkFetchAll.IsChecked == true) {
await Task.Run(() => repo.Fetch(null, prune, msg => Dispatcher.Invoke(() => statusMsg.Content = msg)));
} else {
var remote = combRemotes.SelectedItem as Git.Remote;
await Task.Run(() => repo.Fetch(remote, prune, msg => Dispatcher.Invoke(() => statusMsg.Content = msg)));
}
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,169 @@
<Window x:Class="SourceGit.UI.FileHistories"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
mc:Ignorable="d"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Layout Window -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LOGO & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - FILE HISTORIES" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Body -->
<Border Grid.Row="1" ClipToBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid
x:Name="commitList"
Margin="2,0,0,0"
Grid.Column="0"
Background="{StaticResource Brush.BG2}"
BorderThickness="0"
SelectionMode="Single"
SelectionChanged="CommitSelectionChanged">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource Brush.BG4}" BorderThickness="0,0,0,1" Padding="4">
<StackPanel Orientation="Vertical" Margin="2" MaxWidth="290">
<Grid TextBlock.FontSize="11" TextBlock.Foreground="{StaticResource Brush.FG2}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="72"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Padding="0">
<Hyperlink RequestNavigate="NavigateToCommit" NavigateUri="{Binding SHA}" Foreground="DarkOrange" ToolTip="GOTO COMMIT">
<Run Text="{Binding ShortSHA, Mode=OneWay}"/>
</Hyperlink>
</Label>
<TextBlock Grid.Column="1" Text="{Binding Author.Name}"/>
<TextBlock Grid.Column="2" Text="{Binding Author.Time}"/>
</Grid>
<TextBlock MaxWidth="280" Foreground="{StaticResource Brush.FG}" Text="{Binding Subject}" TextAlignment="Left" Padding="0" Margin="0,8,2,0"/>
</StackPanel>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Loading tip -->
<Path x:Name="loading" Grid.Column="0" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.BG4}"/>
<local:DiffViewer x:Name="diff" Grid.Column="2" Margin="2,0"/>
</Grid>
</Border>
</Grid>
</Border>
</Window>

View file

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
namespace SourceGit.UI {
/// <summary>
/// File histories panel.
/// </summary>
public partial class FileHistories : Window {
private Git.Repository repo = null;
private string file = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="file"></param>
public FileHistories(Git.Repository repo, string file) {
this.repo = repo;
this.file = file;
InitializeComponent();
// Move to center
var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible;
// Load commits
Task.Run(() => {
var commits = repo.Commits($"-n 10000 -- \"{file}\"");
Dispatcher.Invoke(() => {
commitList.ItemsSource = commits;
commitList.SelectedIndex = 0;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
});
});
}
/// <summary>
/// Logo click
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
/// <summary>
/// Commit selection change event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return;
var commit = e.AddedItems[0] as Git.Commit;
var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
List<string> data = new List<string>();
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, file);
});
diff.SetData(data, $"{file} @ {commit.ShortSHA}");
}
/// <summary>
/// Navigate to given string
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NavigateToCommit(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true;
}
}
}

View file

@ -0,0 +1,50 @@
<UserControl x:Class="SourceGit.UI.GitFlowFinishBranch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" x:Name="txtTitle" FontWeight="DemiBold" FontSize="18"/>
<Label Grid.Row="2" Grid.Column="0" x:Name="txtBranchType" HorizontalAlignment="Right"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}" Margin="4,0"/>
<Label x:Name="txtBranchName"/>
</StackPanel>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Confirm finish git-flow branch dialog
/// </summary>
public partial class GitFlowFinishBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch branch = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public GitFlowFinishBranch(Git.Repository repo, Git.Branch branch) {
this.repo = repo;
this.branch = branch;
InitializeComponent();
switch (branch.Kind) {
case Git.Branch.Type.Feature:
txtTitle.Content = "Git Flow - Finish Feature";
txtBranchType.Content = "Feature :";
break;
case Git.Branch.Type.Release:
txtTitle.Content = "Git Flow - Finish Release";
txtBranchType.Content = "Release :";
break;
case Git.Branch.Type.Hotfix:
txtTitle.Content = "Git Flow - Finish Hotfix";
txtBranchType.Content = "Hotfix :";
break;
default:
PopupManager.Close();
return;
}
txtBranchName.Content = branch.Name;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="repo"></param>
/// <param name="branch"></param>
public static void Show(Git.Repository repo, Git.Branch branch) {
PopupManager.Show(new GitFlowFinishBranch(repo, branch));
}
/// <summary>
/// Do finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.FinishGitFlowBranch(branch));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,68 @@
<UserControl x:Class="SourceGit.UI.GitFlowSetup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="304" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Git Flow - Initialize"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Production Branch :"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtMaster" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="master" Height="24" Text="master"/>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Development Branch :"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="txtDevelop" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="develop" Height="24" Text="develop"/>
<Label Grid.Row="5" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Feature Prefix :"/>
<TextBox Grid.Row="5" Grid.Column="1" x:Name="txtFeature" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="feature/" Height="24" Text="feature/"/>
<Label Grid.Row="6" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Release Prefix :"/>
<TextBox Grid.Row="6" Grid.Column="1" x:Name="txtRelease" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="release/" Height="24" Text="release/"/>
<Label Grid.Row="7" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Hotfix Prefix :"/>
<TextBox Grid.Row="7" Grid.Column="1" x:Name="txtHotfix" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="hotfix/" Height="24" Text="hotfix/"/>
<Label Grid.Row="8" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Content="Version Tag Prefix :"/>
<TextBox Grid.Row="8" Grid.Column="1" x:Name="txtVersion" Padding="2,0" TextChanged="ValidateNames" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="Optional." Height="24" Text=""/>
<Grid Grid.Row="10" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="txtValidation" Foreground="Red"/>
<Button Grid.Column="1" x:Name="btnSure" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="11" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,134 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Dialog to initialize git flow.
/// </summary>
public partial class GitFlowSetup : UserControl {
private Git.Repository repo = null;
private Regex regex = new Regex(@"^[\w\-/\.]+$");
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
public GitFlowSetup(Git.Repository opened) {
repo = opened;
InitializeComponent();
}
/// <summary>
/// Open this dialog.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
PopupManager.Show(new GitFlowSetup(repo));
}
/// <summary>
/// Start to initialize git-flow.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
var master = txtMaster.Text;
var dev = txtDevelop.Text;
var feature = txtFeature.Text;
var release = txtRelease.Text;
var hotfix = txtHotfix.Text;
var version = txtVersion.Text;
await Task.Run(() => repo.EnableGitFlow(master, dev, feature, release, hotfix, version));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
/// <summary>
/// Validate input names.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ValidateNames(object sender, TextChangedEventArgs e) {
if (!IsLoaded) return;
var master = txtMaster.Text;
var dev = txtDevelop.Text;
var feature = txtFeature.Text;
var release = txtRelease.Text;
var hotfix = txtHotfix.Text;
if (!ValidateBranch("Production", master)) return;
if (!ValidateBranch("Development", dev)) return;
if (dev == master) {
txtValidation.Content = "Development branch is same with production!";
btnSure.IsEnabled = false;
return;
}
if (!ValidatePrefix("Feature", feature)) return;
if (!ValidatePrefix("Release", release)) return;
if (!ValidatePrefix("Hotfix", hotfix)) return;
txtValidation.Content = "";
btnSure.IsEnabled = true;
}
private bool ValidateBranch(string type, string name) {
if (string.IsNullOrEmpty(name)) {
txtValidation.Content = $"{type} branch name can't be empty";
btnSure.IsEnabled = false;
return false;
}
if (!regex.IsMatch(name)) {
txtValidation.Content = $"{type} branch name contains invalid characters.";
btnSure.IsEnabled = false;
return false;
}
return true;
}
private bool ValidatePrefix(string type, string prefix) {
if (string.IsNullOrEmpty(prefix)) {
txtValidation.Content = $"{type} prefix is required!";
btnSure.IsEnabled = false;
return false;
}
if (!regex.IsMatch(prefix)) {
txtValidation.Content = $"{type} prefix contains invalid characters.";
btnSure.IsEnabled = false;
return false;
}
return true;
}
}
}

View file

@ -0,0 +1,57 @@
<UserControl x:Class="SourceGit.UI.GitFlowStartBranch"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="128" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" x:Name="txtTitle" FontWeight="DemiBold" FontSize="18"/>
<Label Grid.Row="2" Grid.Column="0" x:Name="txtPrefix" HorizontalAlignment="Right" FontSize="16" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtName" VerticalContentAlignment="Center" helpers:TextBoxHelper.Placeholder="Enter name" Height="24">
<TextBox.Text>
<Binding ElementName="me" Path="SubName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<helpers:BranchNameRule x:Name="nameValidator"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Grid Grid.Row="4" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,103 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// Start git-flow branch dialog.
/// </summary>
public partial class GitFlowStartBranch : UserControl {
private Git.Repository repo = null;
private Git.Branch.Type type = Git.Branch.Type.Feature;
/// <summary>
/// Sub-name for this git-flow branch.
/// </summary>
public string SubName {
get;
set;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="repo"></param>
/// <param name="type"></param>
public GitFlowStartBranch(Git.Repository repo, Git.Branch.Type type) {
this.repo = repo;
this.type = type;
InitializeComponent();
nameValidator.Repo = repo;
switch (type) {
case Git.Branch.Type.Feature:
var featurePrefix = repo.GetFeaturePrefix();
txtTitle.Content = "Git Flow - Start Feature";
txtPrefix.Content = featurePrefix;
nameValidator.Prefix = featurePrefix;
break;
case Git.Branch.Type.Release:
var releasePrefix = repo.GetReleasePrefix();
txtTitle.Content = "Git Flow - Start Release";
txtPrefix.Content = releasePrefix;
nameValidator.Prefix = releasePrefix;
break;
case Git.Branch.Type.Hotfix:
var hotfixPrefix = repo.GetHotfixPrefix();
txtTitle.Content = "Git Flow - Start Hotfix";
txtPrefix.Content = hotfixPrefix;
nameValidator.Prefix = hotfixPrefix;
break;
default:
PopupManager.Close();
return;
}
}
/// <summary>
/// Display this dialog
/// </summary>
/// <param name="repo"></param>
/// <param name="type"></param>
public static void Show(Git.Repository repo, Git.Branch.Type type) {
PopupManager.Show(new GitFlowStartBranch(repo, type));
}
/// <summary>
/// Start git-flow branch
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtName)) return;
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => repo.StartGitFlowBranch(type, SubName));
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
}
/// <summary>
/// Cancel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

187
SourceGit/UI/Histories.xaml Normal file
View file

@ -0,0 +1,187 @@
<UserControl x:Class="SourceGit.UI.Histories"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
xmlns:sourcegit="clr-namespace:SourceGit"
xmlns:converters="clr-namespace:SourceGit.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Unloaded="Cleanup">
<Grid x:Name="layout">
<!-- List Panel (SearchBar + DataGrid) -->
<Grid x:Name="commitListPanel" Background="{StaticResource Brush.BG2}" ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Loading Tip -->
<Path x:Name="loading" Grid.RowSpan="2" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
<!-- SearchBar -->
<Grid x:Name="searchBar" Margin="0,-32,0,0" Grid.Row="0">
<TextBox x:Name="txtSearch" Margin="4" Height="24" Padding="0,0,16,0" helpers:TextBoxHelper.Placeholder="SEARCH SHA/SUBJECT/AUTHOR. PRESS ENTER TO SEARCH, ESC TO QUIT" PreviewKeyDown="PreviewSearchKeyDown"/>
<Button HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Style="{StaticResource Style.Button.HighlightHover}" ToolTip="CLEAR" Click="ClearSearch">
<Path Width="10" Height="10" Fill="{StaticResource Brush.Border1}" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</Grid>
<!-- Commit DataGrid -->
<DataGrid
Grid.Row="1"
x:Name="commitList"
RowHeight="{x:Static helpers:CommitGraphMaker.UNIT_HEIGHT}"
ScrollViewer.ScrollChanged="CommitListScrolled"
SelectionChanged="CommitSelectChanged"
SelectionUnit="FullRow">
<DataGrid.Resources>
<converters:IndentToMargin x:Key="CommitTitleMargin"/>
<Style x:Key="Style.DataGridText" TargetType="{x:Type TextBlock}">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="16,0"/>
</Style>
<Style x:Key="Style.DataGridText.NoPadding" TargetType="{x:Type TextBlock}">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="{Binding GraphOffset, Converter={StaticResource CommitTitleMargin}}">
<ItemsControl x:Name="Decorator" ItemsSource="{Binding Decorators}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type git:Decorator}">
<Border x:Name="BG" Height="16" Margin="2,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.BG5}">
<Path x:Name="Icon" Width="8" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
</Border>
<TextBlock x:Name="Name" Grid.Column="1" Text="{Binding Name}" FontSize="11" Padding="4,0" Foreground="Black" VerticalAlignment="Center" TextWrapping="NoWrap"/>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.Tag}">
<Setter TargetName="BG" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.LocalBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Branch}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.RemoteBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static git:DecoratorType.CurrentBranchHead}">
<Setter TargetName="BG" Property="Background" Value="#FFFFB835"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Icon" Property="Fill" Value="Orange"/>
<Setter TargetName="Name" Property="FontWeight" Value="Bold"/>
<Setter TargetName="Name" Property="Foreground" Value="{StaticResource Brush.FG}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding Subject}" VerticalAlignment="Center" Margin="2,0,0,0"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding HasDecorators}" Value="False">
<Setter TargetName="Decorator" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="100" IsReadOnly="True" Binding="{Binding Committer.Name}" ElementStyle="{StaticResource Style.DataGridText}"/>
<DataGridTextColumn Width="84" IsReadOnly="True" Binding="{Binding ShortSHA}" ElementStyle="{StaticResource Style.DataGridText}"/>
<DataGridTextColumn Width="128" IsReadOnly="True" Binding="{Binding Committer.Time}" ElementStyle="{StaticResource Style.DataGridText.NoPadding}"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="CommitContextMenuOpening"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMerged}" Value="False">
<Setter Property="Opacity" Value=".4"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
<!-- Commit Graph -->
<Border Grid.Row="1" Margin="0,0,310,0" ClipToBounds="True" IsHitTestVisible="False">
<Canvas x:Name="commitGraph"/>
</Border>
</Grid>
<!-- Split -->
<GridSplitter x:Name="splitter" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.BG1}"/>
<!-- Detail for selected commit -->
<Grid x:Name="commitDetailPanel" Background="{StaticResource Brush.BG1}">
<!-- Selected commit detail -->
<local:CommitViewer x:Name="commitViewer"/>
<!-- Mask for select multi rows in commit list -->
<Border x:Name="mask4MultiSelection" Background="{StaticResource Brush.BG1}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="160" Height="160" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="txtTotalSelected" Margin="0,16,0,0" FontSize="24" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<!-- SWITCH LAYOUT -->
<ToggleButton
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,4,8,0"
Style="{StaticResource Style.ToggleButton.Orientation}"
ToolTip="Toggle Horizontal/Vertical Layout"
IsChecked="{Binding Source={x:Static sourcegit:App.Preference}, Path=UIUseHorizontalLayout, Mode=TwoWay}"
Checked="ChangeOrientation" Unchecked="ChangeOrientation"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,649 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SourceGit.UI {
/// <summary>
/// Commit histories viewer
/// </summary>
public partial class Histories : UserControl {
/// <summary>
/// Current opened repository.
/// </summary>
public Git.Repository Repo { get; set; }
/// <summary>
/// Cached commits.
/// </summary>
private List<Git.Commit> cachedCommits = new List<Git.Commit>();
/// <summary>
/// Is in search mode?
/// </summary>
private bool isSearchMode = false;
/// <summary>
/// Regex to test search input.
/// </summary>
private Regex commitRegex = new Regex(@"^[0-9a-f]{6,40}$", RegexOptions.None);
/// <summary>
/// Constructor
/// </summary>
public Histories() {
InitializeComponent();
ChangeOrientation(null, null);
}
/// <summary>
/// Navigate to given commit.
/// </summary>
/// <param name="commit"></param>
public void Navigate(string commit) {
if (string.IsNullOrEmpty(commit)) return;
foreach (var item in commitList.ItemsSource) {
var c = item as Git.Commit;
if (c.SHA.StartsWith(commit)) {
commitList.SelectedItem = c;
commitList.ScrollIntoView(c);
return;
}
}
}
/// <summary>
/// Loading tips.
/// </summary>
/// <param name="enabled"></param>
public void SetLoadingEnabled(bool enabled) {
if (enabled) {
loading.Visibility = Visibility.Visible;
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
} else {
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed;
}
}
#region DATA
public void SetCommits(List<Git.Commit> commits) {
cachedCommits = commits;
if (isSearchMode) return;
var maker = Helpers.CommitGraphMaker.Parse(commits);
Dispatcher.Invoke(() => {
commitGraph.Children.Clear();
isSearchMode = false;
txtSearch.Text = "";
// Draw all lines.
foreach (var path in maker.Lines) {
var size = path.Points.Count;
var geo = new StreamGeometry();
var last = path.Points[0];
using (var ctx = geo.Open()) {
ctx.BeginFigure(last, false, false);
for (int i = 1; i < size; i++) {
var cur = path.Points[i];
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 += Helpers.CommitGraphMaker.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);
}
last = cur;
}
}
geo.Freeze();
var p = new Path();
p.Data = geo;
p.Stroke = path.Brush;
p.StrokeThickness = 2;
commitGraph.Children.Add(p);
}
maker.Lines.Clear();
// Draw short links
foreach (var link in maker.Links) {
var geo = new StreamGeometry();
using (var ctx = geo.Open()) {
ctx.BeginFigure(link.Start, false, false);
ctx.QuadraticBezierTo(link.Control, link.End, true, false);
}
geo.Freeze();
var p = new Path();
p.Data = geo;
p.Stroke = link.Brush;
p.StrokeThickness = 2;
commitGraph.Children.Add(p);
}
maker.Links.Clear();
// Draw points.
foreach (var dot in maker.Dots) {
var ellipse = new Ellipse();
ellipse.Height = 6;
ellipse.Width = 6;
ellipse.Fill = dot.Color;
ellipse.SetValue(Canvas.LeftProperty, dot.X);
ellipse.SetValue(Canvas.TopProperty, dot.Y);
commitGraph.Children.Add(ellipse);
}
maker.Dots.Clear();
commitList.ItemsSource = new List<Git.Commit>(cachedCommits);
Navigate(maker.Highlight);
SetLoadingEnabled(false);
});
}
public void SetSearchResult(List<Git.Commit> commits) {
isSearchMode = true;
foreach (var c in commits) c.GraphOffset = 0;
Dispatcher.Invoke(() => {
commitGraph.Children.Clear();
commitList.ItemsSource = new List<Git.Commit>(commits);
SetLoadingEnabled(false);
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
commitGraph.Children.Clear();
commitList.ItemsSource = null;
cachedCommits.Clear();
}
#endregion
#region SEARCH_BAR
public void OpenSearchBar() {
if (searchBar.Margin.Top == 0) return;
ThicknessAnimation anim = new ThicknessAnimation();
anim.From = new Thickness(0, -32, 0, 0);
anim.To = new Thickness(0);
anim.Duration = TimeSpan.FromSeconds(.3);
searchBar.BeginAnimation(Grid.MarginProperty, anim);
txtSearch.Focus();
}
public void HideSearchBar() {
if (searchBar.Margin.Top != 0) return;
ClearSearch(null, null);
ThicknessAnimation anim = new ThicknessAnimation();
anim.From = new Thickness(0);
anim.To = new Thickness(0, -32, 0, 0);
anim.Duration = TimeSpan.FromSeconds(.3);
searchBar.BeginAnimation(Grid.MarginProperty, anim);
}
private void ClearSearch(object sender, RoutedEventArgs e) {
txtSearch.Text = "";
if (isSearchMode) {
isSearchMode = false;
SetLoadingEnabled(true);
Task.Run(() => SetCommits(cachedCommits));
}
}
private void PreviewSearchKeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Enter) {
string search = txtSearch.Text;
if (string.IsNullOrEmpty(search)) {
ClearSearch(sender, e);
} else if (commitRegex.IsMatch(search)) {
SetLoadingEnabled(true);
Task.Run(() => {
var commits = Repo.Commits($"search -n 1");
SetSearchResult(commits);
});
} else {
SetLoadingEnabled(true);
Task.Run(() => {
List<Git.Commit> found = new List<Git.Commit>();
foreach (var commit in cachedCommits) {
if (commit.Subject.IndexOf(search) >= 0 ||
(commit.Author != null && commit.Author.Name == search) ||
(commit.Committer != null && commit.Committer.Name == search) ||
commit.Message.IndexOf(search) >= 0) {
found.Add(commit);
}
}
SetSearchResult(found);
});
}
}
}
#endregion
#region COMMIT_DATAGRID_AND_GRAPH
private void CommitListScrolled(object sender, ScrollChangedEventArgs e) {
commitGraph.Margin = new Thickness(0, -e.VerticalOffset * Helpers.CommitGraphMaker.UNIT_HEIGHT, 0, 0);
}
private void CommitSelectChanged(object sender, SelectionChangedEventArgs e) {
mask4MultiSelection.Visibility = Visibility.Collapsed;
var selected = commitList.SelectedItems;
if (selected.Count == 1) {
var commit = selected[0] as Git.Commit;
if (commit != null) commitViewer.SetData(Repo, commit);
} else if (selected.Count > 1) {
mask4MultiSelection.Visibility = Visibility.Visible;
txtTotalSelected.Content = $"SELECTED {selected.Count} COMMITS";
}
}
private MenuItem GetCurrentBranchContextMenu(Git.Branch branch) {
var icon = new Path();
icon.Style = FindResource("Style.Icon") as Style;
icon.Data = FindResource("Icon.Branch") as Geometry;
icon.VerticalAlignment = VerticalAlignment.Bottom;
icon.Width = 10;
var submenu = new MenuItem();
submenu.Header = branch.Name;
submenu.Icon = icon;
if (!string.IsNullOrEmpty(branch.Upstream)) {
var upstream = branch.Upstream.Substring(13);
var fastForward = new MenuItem();
fastForward.Header = $"Fast-Forward to '{upstream}'";
fastForward.Click += (o, e) => {
Merge.StartDirectly(Repo, upstream, branch.Name);
e.Handled = true;
};
submenu.Items.Add(fastForward);
var pull = new MenuItem();
pull.Header = $"Pull '{upstream}' ...";
pull.Click += (o, e) => {
Pull.Show(Repo);
e.Handled = true;
};
submenu.Items.Add(pull);
}
var push = new MenuItem();
push.Header = $"Push '{branch.Name}' ...";
push.Click += (o, e) => {
Push.Show(Repo, branch);
e.Handled = true;
};
submenu.Items.Add(push);
submenu.Items.Add(new Separator());
if (branch.Kind != Git.Branch.Type.Normal) {
var flowIcon = new Path();
flowIcon.Style = FindResource("Style.Icon") as Style;
flowIcon.Data = FindResource("Icon.Flow") as Geometry;
flowIcon.Width = 10;
var finish = new MenuItem();
finish.Header = $"Git Flow - Finish '{branch.Name}'";
finish.Icon = flowIcon;
finish.Click += (o, e) => {
GitFlowFinishBranch.Show(Repo, branch);
e.Handled = true;
};
submenu.Items.Add(finish);
submenu.Items.Add(new Separator());
}
var rename = new MenuItem();
rename.Header = "Rename ...";
rename.Click += (o, e) => {
RenameBranch.Show(Repo, branch);
e.Handled = true;
};
submenu.Items.Add(rename);
return submenu;
}
private MenuItem GetOtherBranchContextMenu(Git.Branch current, Git.Branch branch, bool merged) {
var icon = new Path();
icon.Style = FindResource("Style.Icon") as Style;
icon.Data = FindResource("Icon.Branch") as Geometry;
icon.VerticalAlignment = VerticalAlignment.Bottom;
icon.Width = 10;
var submenu = new MenuItem();
submenu.Header = branch.Name;
submenu.Icon = icon;
var checkout = new MenuItem();
checkout.Header = $"Checkout '{branch.Name}'";
checkout.Click += (o, e) => {
if (branch.IsLocal) {
Task.Run(() => Repo.Checkout(branch.Name));
} else {
var upstream = $"refs/remotes/{branch.Name}";
var tracked = Repo.Branches().Find(b => b.IsLocal && b.Upstream == upstream);
if (tracked == null) {
CreateBranch.Show(Repo, branch);
} else if (!tracked.IsCurrent) {
Task.Run(() => Repo.Checkout(tracked.Name));
}
}
e.Handled = true;
};
submenu.Items.Add(checkout);
var merge = new MenuItem();
merge.Header = $"Merge into '{current.Name}' ...";
merge.IsEnabled = !merged;
merge.Click += (o, e) => {
Merge.Show(Repo, branch.Name, current.Name);
e.Handled = true;
};
submenu.Items.Add(merge);
submenu.Items.Add(new Separator());
if (branch.Kind != Git.Branch.Type.Normal) {
var flowIcon = new Path();
flowIcon.Style = FindResource("Style.Icon") as Style;
flowIcon.Data = FindResource("Icon.Flow") as Geometry;
flowIcon.Width = 10;
var finish = new MenuItem();
finish.Header = $"Git Flow - Finish '{branch.Name}'";
finish.Icon = flowIcon;
finish.Click += (o, e) => {
GitFlowFinishBranch.Show(Repo, branch);
e.Handled = true;
};
submenu.Items.Add(finish);
submenu.Items.Add(new Separator());
}
var rename = new MenuItem();
rename.Header = "Rename ...";
rename.Visibility = branch.IsLocal ? Visibility.Visible : Visibility.Collapsed;
rename.Click += (o, e) => {
RenameBranch.Show(Repo, current);
e.Handled = true;
};
submenu.Items.Add(rename);
var delete = new MenuItem();
delete.Header = "Delete ...";
delete.Click += (o, e) => {
DeleteBranch.Show(Repo, branch);
};
submenu.Items.Add(delete);
return submenu;
}
private MenuItem GetTagContextMenu(Git.Tag tag) {
var icon = new Path();
icon.Style = FindResource("Style.Icon") as Style;
icon.Data = FindResource("Icon.Tag") as Geometry;
icon.Width = 10;
var submenu = new MenuItem();
submenu.Header = tag.Name;
submenu.Icon = icon;
submenu.MinWidth = 200;
var push = new MenuItem();
push.Header = "Push ...";
push.Click += (o, e) => {
PushTag.Show(Repo, tag);
e.Handled = true;
};
submenu.Items.Add(push);
var delete = new MenuItem();
delete.Header = "Delete ...";
delete.Click += (o, e) => {
DeleteTag.Show(Repo, tag);
e.Handled = true;
};
submenu.Items.Add(delete);
return submenu;
}
private void CommitContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var row = sender as DataGridRow;
if (row == null) return;
var commit = row.DataContext as Git.Commit;
if (commit == null) return;
commitList.SelectedItem = commit;
var current = Repo.CurrentBranch();
if (current == null) return;
var menu = new ContextMenu();
menu.MinWidth = 200;
// Decorators.
{
var localBranchContextMenus = new List<MenuItem>();
var remoteBranchContextMenus = new List<MenuItem>();
var tagContextMenus = new List<MenuItem>();
foreach (var d in commit.Decorators) {
if (d.Type == Git.DecoratorType.CurrentBranchHead) {
menu.Items.Add(GetCurrentBranchContextMenu(current));
} else if (d.Type == Git.DecoratorType.LocalBranchHead) {
var branch = Repo.Branches().Find(b => b.Name == d.Name);
if (branch != null) {
localBranchContextMenus.Add(GetOtherBranchContextMenu(current, branch, commit.IsMerged));
}
} else if (d.Type == Git.DecoratorType.RemoteBranchHead) {
var branch = Repo.Branches().Find(b => b.Name == d.Name);
if (branch != null) {
remoteBranchContextMenus.Add(GetOtherBranchContextMenu(current, branch, commit.IsMerged));
}
} else if (d.Type == Git.DecoratorType.Tag) {
var tag = Repo.Tags().Find(t => t.Name == d.Name);
if (tag != null) tagContextMenus.Add(GetTagContextMenu(tag));
}
}
foreach (var m in localBranchContextMenus) menu.Items.Add(m);
foreach (var m in remoteBranchContextMenus) menu.Items.Add(m);
if (menu.Items.Count > 0) menu.Items.Add(new Separator());
if (tagContextMenus.Count > 0) {
foreach (var m in tagContextMenus) menu.Items.Add(m);
menu.Items.Add(new Separator());
}
}
// Reset
var reset = new MenuItem();
reset.Header = $"Reset '{current.Name}' To Here";
reset.Visibility = commit.IsHEAD ? Visibility.Collapsed : Visibility.Visible;
reset.Click += (o, e) => {
Reset.Show(Repo, commit);
e.Handled = true;
};
menu.Items.Add(reset);
// Rebase or interactive rebase
var rebase = new MenuItem();
rebase.Header = commit.IsMerged ? $"Interactive Rebase '{current.Name}' From Here" : $"Rebase '{current.Name}' To Here";
rebase.Visibility = commit.IsHEAD ? Visibility.Collapsed : Visibility.Visible;
rebase.Click += (o, e) => {
if (commit.IsMerged) {
if (Repo.LocalChanges().Count > 0) {
App.RaiseError("You have local changes!!!");
e.Handled = true;
return;
}
var dialog = new InteractiveRebase(Repo, commit);
dialog.Owner = App.Current.MainWindow;
dialog.ShowDialog();
} else {
Rebase.Show(Repo, commit);
}
e.Handled = true;
};
menu.Items.Add(rebase);
// Cherry-Pick
var cherryPick = new MenuItem();
cherryPick.Header = "Cherry-Pick This Commit";
cherryPick.Visibility = commit.IsMerged ? Visibility.Collapsed : Visibility.Visible;
cherryPick.Click += (o, e) => {
CherryPick.Show(Repo, commit);
e.Handled = true;
};
menu.Items.Add(cherryPick);
// Revert commit
var revert = new MenuItem();
revert.Header = "Revert commit";
revert.Visibility = !commit.IsMerged ? Visibility.Collapsed : Visibility.Visible;
revert.Click += (o, e) => {
Revert.Show(Repo, commit);
e.Handled = true;
};
menu.Items.Add(revert);
menu.Items.Add(new Separator());
// Common
var createBranch = new MenuItem();
createBranch.Header = "Create Branch";
createBranch.Click += (o, e) => {
CreateBranch.Show(Repo, commit);
e.Handled = true;
};
menu.Items.Add(createBranch);
var createTag = new MenuItem();
createTag.Header = "Create Tag";
createTag.Click += (o, e) => {
CreateTag.Show(Repo, commit);
e.Handled = true;
};
menu.Items.Add(createTag);
menu.Items.Add(new Separator());
// Save as patch
var patch = new MenuItem();
patch.Header = "Save As Patch";
patch.Click += (o, e) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
Repo.RunCommand($"format-patch {commit.SHA} -1 -o \"{dialog.SelectedPath}\"", null);
}
};
menu.Items.Add(patch);
menu.Items.Add(new Separator());
// Copy SHA
var copySHA = new MenuItem();
copySHA.Header = "Copy Commit SHA";
copySHA.Click += (o, e) => {
Clipboard.SetText(commit.SHA);
};
menu.Items.Add(copySHA);
// Copy info
var copyInfo = new MenuItem();
copyInfo.Header = "Copy Commit Info";
copyInfo.Click += (o, e) => {
Clipboard.SetText(string.Format(
"SHA: {0}\nTITLE: {1}\nAUTHOR: {2} <{3}>\nTIME: {4}",
commit.SHA, commit.Subject, commit.Committer.Name, commit.Committer.Email, commit.Committer.Time));
};
menu.Items.Add(copyInfo);
menu.IsOpen = true;
ev.Handled = true;
}
#endregion
#region LAYOUT
private void ChangeOrientation(object sender, RoutedEventArgs e) {
if (commitDetailPanel == null || splitter == null || commitListPanel == null) return;
layout.RowDefinitions.Clear();
layout.ColumnDefinitions.Clear();
if (App.Preference.UIUseHorizontalLayout) {
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star), MinWidth = 200 });
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(2) });
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star), MinWidth = 200 });
Grid.SetRow(commitListPanel, 0);
Grid.SetRow(splitter, 0);
Grid.SetRow(commitDetailPanel, 0);
Grid.SetColumn(commitListPanel, 0);
Grid.SetColumn(splitter, 1);
Grid.SetColumn(commitDetailPanel, 2);
} else {
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star), MinHeight = 100 });
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(2) });
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star), MinHeight = 100 });
Grid.SetRow(commitListPanel, 0);
Grid.SetRow(splitter, 1);
Grid.SetRow(commitDetailPanel, 2);
Grid.SetColumn(commitListPanel, 0);
Grid.SetColumn(splitter, 0);
Grid.SetColumn(commitDetailPanel, 0);
}
layout.InvalidateVisual();
}
#endregion
}
}

53
SourceGit/UI/Init.xaml Normal file
View file

@ -0,0 +1,53 @@
<UserControl x:Class="SourceGit.UI.Init"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="500" Height="160" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Initialize Repository"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="Path :"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}" Margin="4,0"/>
<Label x:Name="txtPath"/>
</StackPanel>
<Label Grid.Row="3" Grid.Column="1" Content="Invalid repository detected. Run `git init` under this path?" Foreground="{StaticResource Brush.FG2}"/>
<Grid Grid.Row="5" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="6" Grid.ColumnSpan="2" x:Name="status" Visibility="Collapsed" Background="{StaticResource Brush.BG1}" Opacity=".9">
<Path x:Name="statusIcon" Width="48" Height="48" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
</Grid>
</Grid>
</UserControl>

73
SourceGit/UI/Init.xaml.cs Normal file
View file

@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SourceGit.UI {
/// <summary>
/// `git init` confirm panel.
/// </summary>
public partial class Init : UserControl {
private string workingDir = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="path"></param>
public Init(string path) {
workingDir = path;
InitializeComponent();
txtPath.Content = path;
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="path"></param>
public static void Show(string path) {
PopupManager.Show(new Init(path));
}
/// <summary>
/// Do `git init`
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Sure(object sender, RoutedEventArgs e) {
PopupManager.Lock();
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
status.Visibility = Visibility.Visible;
await Task.Run(() => {
var errs = Git.Repository.RunCommand(workingDir, "init -q", null);
if (errs != null) {
App.RaiseError(errs);
} else {
App.Preference.AddRepository(workingDir, "");
}
});
status.Visibility = Visibility.Collapsed;
statusIcon.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
PopupManager.Close(true);
var repo = App.Preference.FindRepository(workingDir);
if (repo != null) repo.Open();
}
/// <summary>
/// Cancel.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
}
}

View file

@ -0,0 +1,256 @@
<Window x:Class="SourceGit.UI.InteractiveRebase"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
Height="600" Width="800">
<!-- Enable WindowChrome -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Content -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
<RowDefinition Height="1"/>
<RowDefinition Height="*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Logo & TITLE -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Image
Source="pack://application:,,,/Resources/App.png"
Width="20" Height="20" Margin="6,-1,2,0"
VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT - INTERACTIVE REBASE" FontWeight="Light"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Commit List -->
<ListView Grid.Row="1"
x:Name="commitList"
Background="Transparent"
Style="{StaticResource Style.ListView.Borderless}"
ItemsSource="{Binding ElementName=me, Path=Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionChanged="CommitSelectionChanged"
BorderThickness="0">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:InteractiveRebaseItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="128"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ComboBox
Grid.Column="0"
ItemsSource="{Binding Source={x:Static local:InteractiveRebaseModeInfo.Supported}}"
SelectedIndex="{Binding Path=Mode, Mode=TwoWay}"
BorderThickness="0">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:InteractiveRebaseModeInfo}">
<Grid Height="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="12" Height="12" Margin="4,0,0,0" Fill="{Binding Theme}" Style="{StaticResource Style.Icon}" Data="M 0,0 A 180,180 180 1 1 1,1 Z"/>
<Label Grid.Column="1" Content="{Binding Title}" Padding="4,0"/>
<Label Grid.Column="2" Content="{Binding Desc}" Foreground="{StaticResource Brush.FG2}" FontSize="11" Padding="4,0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Grid Grid.Column="1">
<ContentControl x:Name="MessageEditorAnchor" MouseDoubleClick="PopupMessageEditor">
<TextBlock Text="{Binding Subject, Mode=TwoWay}" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
</ContentControl>
<Popup x:Name="MessageEditor" Placement="Bottom" IsOpen="{Binding IsEditorOpened}" VerticalOffset="1" Height="150" Width="400" PlacementTarget="{Binding ElementName=MessageEditorAnchor}">
<Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG4}">
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border1}" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="1"/>
<RowDefinition Height="79"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Text="{Binding EditSubject, Mode=TwoWay}" Height="32" Padding="2,0" helpers:TextBoxHelper.Placeholder="Enter commit subject" BorderThickness="0"/>
<Rectangle Grid.Row="1" Height="1" Fill="{StaticResource Brush.FG}" Opacity="0.1"/>
<TextBox Grid.Row="2" Text="{Binding EditMessage, Mode=TwoWay}" TextChanged="CommitMessageChanged" Height="79" ScrollViewer.VerticalScrollBarVisibility="Auto" Padding="2" helpers:TextBoxHelper.Placeholder="Enter commit description. Optional" helpers:TextBoxHelper.PlaceholderBaseline="Top" BorderThickness="0" AcceptsReturn="True" AcceptsTab="True" TextWrapping="Wrap"/>
</Grid>
</Border>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Click="ApplyMessageEdit" Height="24" Style="{StaticResource Style.Button.AccentBordered}" BorderThickness="1" Content="APPLY" Margin="8,0"/>
<Button Click="HideMessageEditor" Height="24" Style="{StaticResource Style.Button.Bordered}" BorderThickness="1" Content="CANCEL"/>
</StackPanel>
</Grid>
</Border>
</Popup>
</Grid>
<TextBlock
Grid.Column="2"
Foreground="{StaticResource Brush.FG}"
VerticalAlignment="Center"
Text="{Binding Commit.Committer.Name}"/>
<TextBlock
Grid.Column="3"
Foreground="{StaticResource Brush.FG}"
VerticalAlignment="Center"
Text="{Binding Commit.Committer.Time}"/>
<StackPanel
Grid.Column="4"
Orientation="Horizontal"
Margin="4,0">
<Button Click="MoveUp" ToolTip="MOVE UP">
<Path Width="12" Height="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.MoveUp}"/>
</Button>
<Button Click="MoveDown" ToolTip="MOVE DOWN" Margin="4,0,0,0">
<Path Width="12" Height="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.MoveDown}"/>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Loading Tip -->
<Grid Grid.Row="1" IsHitTestVisible="False">
<!-- Loading tip -->
<Path x:Name="loading" Grid.ColumnSpan="5" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5" Visibility="Hidden">
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
<Path.Style>
<Style BasedOn="{StaticResource Style.Icon}" TargetType="{x:Type Path}">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="48"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Fill" Value="{StaticResource Brush.FG2}"/>
</Style>
</Path.Style>
</Path>
</Grid>
<!-- Splitter -->
<GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
<!-- Commit Detail -->
<local:CommitViewer x:Name="commitViewer" Grid.Row="3" Background="{StaticResource Brush.BG4}"/>
<!-- Options Bar -->
<Grid Grid.Row="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="4"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="4,0,24,0">
<Label Grid.Column="0" Content="Rebase :"/>
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Branch}"/>
<Label x:Name="branch"/>
<Label Grid.Column="2" Content="On :" Margin="8,0,0,0"/>
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Commit}"/>
<Label x:Name="on"/>
</StackPanel>
<Button Grid.Column="1" Height="26" Click="Start" Style="{StaticResource Style.Button.AccentBordered}" Content="REBASE"/>
<Button Grid.Column="3" Height="26" Click="Cancel" Style="{StaticResource Style.Button.Bordered}" Content="CANCEL"/>
</Grid>
</Grid>
</Border>
</Window>

View file

@ -0,0 +1,299 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.UI {
/// <summary>
/// Rebase mode.
/// </summary>
public enum InteractiveRebaseMode {
Pick,
Reword,
Squash,
Fixup,
Drop,
}
/// <summary>
/// Rebase mode information to display in UI.
/// </summary>
public class InteractiveRebaseModeInfo {
public InteractiveRebaseMode Mode { get; set; }
public string Title { get; set; }
public string Desc { get; set; }
public Brush Theme { get; set; }
public InteractiveRebaseModeInfo(InteractiveRebaseMode mode, string title, string desc, Brush brush) {
Mode = mode;
Title = title;
Desc = desc;
Theme = brush;
}
public static List<InteractiveRebaseModeInfo> Supported = new List<InteractiveRebaseModeInfo>() {
new InteractiveRebaseModeInfo(InteractiveRebaseMode.Pick, "Pick", "Use this commit", Brushes.Green),
new InteractiveRebaseModeInfo(InteractiveRebaseMode.Reword, "Reword", "Edit the commit message", Brushes.Yellow),
new InteractiveRebaseModeInfo(InteractiveRebaseMode.Squash, "Squash", "Meld into previous commit", App.Preference.UIUseLightTheme ? Brushes.Gray : Brushes.White),
new InteractiveRebaseModeInfo(InteractiveRebaseMode.Fixup, "Fixup", "Like 'Squash' but discard log message", App.Preference.UIUseLightTheme ? Brushes.Gray : Brushes.White),
new InteractiveRebaseModeInfo(InteractiveRebaseMode.Drop, "Drop", "Remove commit", Brushes.Red),
};
}
/// <summary>
/// Rebase item.
/// </summary>
public class InteractiveRebaseItem : INotifyPropertyChanged {
private InteractiveRebaseMode mode = InteractiveRebaseMode.Pick;
private bool isEditorOpened = false;
private string editSubject = null;
private string editMsg = null;
public event PropertyChangedEventHandler PropertyChanged;
public Git.Commit Commit { get; set; }
public int Mode {
get { return (int)mode; }
set {
if (value != (int)mode) {
mode = (InteractiveRebaseMode)value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Mode"));
}
}
}
public bool IsEditorOpened {
get { return isEditorOpened; }
set {
if (value != isEditorOpened) {
isEditorOpened = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("IsEditorOpened"));
}
}
}
public string Subject {
get { return Commit.Subject; }
set {
if (value != Commit.Subject) {
Commit.Subject = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Subject"));
}
}
}
public string EditSubject {
get { return editSubject; }
set {
if (value != editMsg) {
editSubject = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("EditSubject"));
}
}
}
public string EditMessage {
get { return editMsg; }
set {
if (value != editMsg) {
editMsg = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("EditMessage"));
}
}
}
}
/// <summary>
/// Interactive rebase panel.
/// </summary>
public partial class InteractiveRebase : Window {
private Git.Repository repo = null;
private string from = null;
/// <summary>
/// Edit commit list.
/// </summary>
public ObservableCollection<InteractiveRebaseItem> Items {
get;
set;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
/// <param name="start"></param>
public InteractiveRebase(Git.Repository opened, Git.Commit start) {
repo = opened;
Items = new ObservableCollection<InteractiveRebaseItem>();
from = start.ShortSHA;
InitializeComponent();
branch.Content = opened.CurrentBranch().Name;
on.Content = $"{start.ShortSHA} {start.Subject}";
Task.Run(() => {
var commits = repo.Commits($"{start.SHA}..HEAD");
commits.Add(start);
Dispatcher.Invoke(() => {
Items.Clear();
foreach (var c in commits) Items.Add(new InteractiveRebaseItem() { Commit = c });
});
});
}
#region WINDOW_COMMANDS
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
#endregion
private void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
foreach (var obj in e.RemovedItems) {
var item = obj as InteractiveRebaseItem;
if (item != null) item.IsEditorOpened = false;
}
if (e.AddedItems.Count == 1) {
var item = e.AddedItems[0] as InteractiveRebaseItem;
if (item != null) commitViewer.SetData(repo, item.Commit);
}
}
private void PopupMessageEditor(object sender, MouseButtonEventArgs e) {
var item = (sender as Control).DataContext as InteractiveRebaseItem;
if (item == null) return;
item.EditSubject = item.Commit.Subject;
item.EditMessage = item.Commit.Message;
item.IsEditorOpened = true;
}
private void HideMessageEditor(object sender, RoutedEventArgs e) {
var item = (sender as Button).DataContext as InteractiveRebaseItem;
if (item == null) return;
item.IsEditorOpened = false;
}
private void ApplyMessageEdit(object sender, RoutedEventArgs e) {
var item = (sender as Button).DataContext as InteractiveRebaseItem;
if (item == null) return;
item.Subject = item.EditSubject;
item.Commit.Message = item.EditMessage;
item.Mode = (int)InteractiveRebaseMode.Reword;
item.IsEditorOpened = false;
}
private void CommitMessageChanged(object sender, TextChangedEventArgs e) {
(sender as TextBox).ScrollToEnd();
}
private void MoveUp(object sender, RoutedEventArgs e) {
var item = (sender as Button).DataContext as InteractiveRebaseItem;
if (item == null) return;
var idx = -1;
for (int i = 0; i < Items.Count; i++) {
if (Items[i].Commit.SHA == item.Commit.SHA) {
idx = i;
break;
}
}
if (idx > 0) {
Items.RemoveAt(idx);
Items.Insert(idx - 1, item);
}
}
private void MoveDown(object sender, RoutedEventArgs e) {
var item = (sender as Button).DataContext as InteractiveRebaseItem;
if (item == null) return;
var idx = -1;
for (int i = 0; i < Items.Count; i++) {
if (Items[i].Commit.SHA == item.Commit.SHA) {
idx = i;
break;
}
}
if (idx < Items.Count - 1) {
Items.RemoveAt(idx);
Items.Insert(idx + 1, item);
}
}
private void Start(object sender, RoutedEventArgs e) {
var stream = new FileStream(App.InteractiveRebaseTodo, FileMode.Create);
var writer = new StreamWriter(stream);
for (int i = Items.Count - 1; i >= 0; i--) {
var item = Items[i];
switch ((InteractiveRebaseMode)item.Mode) {
case InteractiveRebaseMode.Pick:
writer.WriteLine($"pick {item.Commit.ShortSHA} {item.Subject}");
break;
case InteractiveRebaseMode.Reword:
writer.WriteLine($"reword {item.Commit.ShortSHA} {item.Subject}");
break;
case InteractiveRebaseMode.Squash:
writer.WriteLine($"squash {item.Commit.ShortSHA} {item.Subject}");
break;
case InteractiveRebaseMode.Fixup:
writer.WriteLine($"fixup {item.Commit.ShortSHA} {item.Subject}");
break;
case InteractiveRebaseMode.Drop:
writer.WriteLine($"drop {item.Commit.ShortSHA} {item.Subject}");
break;
}
}
writer.Flush();
stream.Flush();
writer.Close();
stream.Close();
repo.SetWatcherEnabled(false);
var errs = repo.RunCommand($"-c sequence.editor=\"\\\"{App.InteractiveRebaseScript}\\\"\" rebase -i {from}^", null);
repo.AssertCommand(errs);
Close();
}
private void Cancel(object sender, RoutedEventArgs e) {
Close();
}
}
}

149
SourceGit/UI/Launcher.xaml Normal file
View file

@ -0,0 +1,149 @@
<Window x:Class="SourceGit.UI.Launcher"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:source="clr-namespace:SourceGit"
mc:Ignorable="d"
MinWidth="800" MinHeight="600"
Title="Source Git"
Width="{Binding Source={x:Static source:App.Preference}, Path=UIMainWindowWidth, Mode=TwoWay}"
Height="{Binding Source={x:Static source:App.Preference}, Path=UIMainWindowHeight, Mode=TwoWay}"
WindowStartupLocation="CenterScreen">
<!-- Enable WindowChrome Feature -->
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="32"/>
</WindowChrome.WindowChrome>
<!-- Window Layout -->
<Border Background="{StaticResource Brush.BG1}">
<!-- Fix Maximize BUG -->
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Margin" Value="6"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Margin" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Titlebar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.BG4}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Logo & Title -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Path
Width="20" Height="20" Margin="6,-1,2,0"
Style="{StaticResource Style.Icon}"
Data="{StaticResource Icon.Git}"
Fill="#FFF05133"
WindowChrome.IsHitTestVisibleInChrome="True"
MouseLeftButtonDown="LogoMouseButtonDown"/>
<Label Content="SOURCE GIT" FontWeight="Light"/>
</StackPanel>
<!-- Commands -->
<StackPanel Grid.Column="2" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Click="ShowPreference" Width="24" ToolTip="PREFERENCE">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Preference}"/>
</Button>
<Rectangle Width="1" Height="18" Margin="6,0" VerticalAlignment="Center" Fill="{StaticResource Brush.Border1}"/>
<Button Click="ShowAbout" Width="24" ToolTip="ABOUT">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Info}"/>
</Button>
<Rectangle Width="1" Height="18" Margin="6,0" VerticalAlignment="Center" Fill="{StaticResource Brush.Border1}"/>
<Button Click="Minimize" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path Style="{StaticResource Style.WindowControlIcon}" Data="{StaticResource Icon.Minimize}"/>
</Button>
<Button Click="MaximizeOrRestore" Width="32" Style="{StaticResource Style.Button.HighlightHover}">
<Path>
<Path.Style>
<Style TargetType="{x:Type Path}" BasedOn="{StaticResource Style.WindowControlIcon}">
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Maximized">
<Setter Property="Data" Value="{StaticResource Icon.Restore}"/>
</DataTrigger>
<DataTrigger Binding="{Binding WindowState, ElementName=me}" Value="Normal">
<Setter Property="Data" Value="{StaticResource Icon.Maximize}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button Click="Quit" Width="32">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style.Button.HighlightHover}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Close}"/>
</Button>
</StackPanel>
</Grid>
<!-- Body -->
<ContentControl Grid.Row="1" x:Name="body"/>
<!-- Alerts -->
<ScrollViewer Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Top" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl Margin="4" Width="300" Height="Auto" ItemsSource="{Binding Alerts, ElementName=me}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="item" Height="Auto" Width="300" Margin="0,8,0,0">
<Border Background="{StaticResource Brush.BG1}">
<Border.Effect>
<DropShadowEffect BlurRadius="8" ShadowDepth="2" Direction="270"/>
</Border.Effect>
</Border>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="{Binding Title}" FontWeight="DemiBold"/>
<TextBlock Grid.Row="1" Margin="6,8" Text="{Binding Message}" Foreground="{StaticResource Brush.FG}" TextWrapping="Wrap"/>
<Button
Grid.Row="2"
Margin="4,0"
Click="RemoveAlert"
Content="CLOSE"
Style="{StaticResource Style.Button.AccentBordered}"
BorderBrush="{StaticResource Brush.FG}"
VerticalAlignment="Center"
HorizontalAlignment="Right"/>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</Window>

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SourceGit.UI {
/// <summary>
/// Main window for this app.
/// </summary>
public partial class Launcher : Window {
/// <summary>
/// Alert data.
/// </summary>
public class Alert {
public string Title { get; set; }
public string Message { get; set; }
}
/// <summary>
/// Alerts.
/// </summary>
public ObservableCollection<Alert> Alerts { get; set; } = new ObservableCollection<Alert>();
/// <summary>
/// Constructor
/// </summary>
public Launcher() {
Git.Repository.OnOpen = ShowDashboard;
Git.Repository.OnClose = ShowManager;
App.OnError = msg => ShowAlert(new Alert() { Title = "ERROR", Message = msg });
InitializeComponent();
ShowManager();
}
#region LAYOUT_CONTENT
/// <summary>
/// Show manager.
/// </summary>
private void ShowManager() {
Dispatcher.Invoke(() => {
body.Content = new Manager();
});
}
/// <summary>
/// Show dashboard.
/// </summary>
/// <param name="repo"></param>
private void ShowDashboard(Git.Repository repo) {
Dispatcher.Invoke(() => {
if (body.Content is Dashboard) return;
body.Content = new Dashboard(repo);
});
}
/// <summary>
/// Open preference dialog.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ShowPreference(object sender, RoutedEventArgs e) {
Preference.Show();
}
/// <summary>
/// Open about dialog.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ShowAbout(object sender, RoutedEventArgs e) {
var about = new About();
about.Owner = this;
about.ShowDialog();
}
/// <summary>
/// Show alert.
/// </summary>
/// <param name="alert"></param>
private void ShowAlert(Alert alert) {
Dispatcher.Invoke(() => Alerts.Add(alert));
}
/// <summary>
/// Remove an alert.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void RemoveAlert(object sender, RoutedEventArgs e) {
var alert = (sender as Button).DataContext as Alert;
Alerts.Remove(alert);
}
#endregion
#region WINDOW_COMMANDS
/// <summary>
/// Minimize
/// </summary>
private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this);
}
/// <summary>
/// Maximize/Restore
/// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this);
} else {
SystemCommands.RestoreWindow(this);
}
}
/// <summary>
/// Quit
/// </summary>
private void Quit(object sender, RoutedEventArgs e) {
App.Current.Shutdown();
}
/// <summary>
/// Show system menu when user click logo.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement;
if (element == null) return;
var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos);
}
#endregion
}
}

231
SourceGit/UI/Manager.xaml Normal file
View file

@ -0,0 +1,231 @@
<UserControl
x:Class="SourceGit.UI.Manager"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SourceGit.UI"
xmlns:git="clr-namespace:SourceGit.Git"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Toolbar -->
<Grid Grid.Row="0" Panel.ZIndex="9999">
<Border Background="{StaticResource Brush.BG1}">
<Border.Effect>
<DropShadowEffect ShadowDepth="2" Direction="270" Opacity=".3"/>
</Border.Effect>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Navigation -->
<StackPanel Grid.Column="0" Margin="8,0,0,0" Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Manager}"/>
<Label Content="Repositories"/>
</StackPanel>
<!-- Options -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Click="OpenOrAddRepo" Style="{StaticResource Style.Button}">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder.Open}"/>
<Label Content="Open Folder"/>
</StackPanel>
</Button>
<Button Click="CloneRepo" Style="{StaticResource Style.Button}" Margin="8,0,0,0">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Clone}"/>
<Label Content="Clone"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
<!-- Main Body -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="360"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left panel -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Recent Opened Repositories -->
<Label Grid.Row="0" Margin="8,8,0,0" Content="RECENTLY OPENED" Style="{StaticResource Style.Label.GroupHeader}"/>
<ListView
x:Name="recentOpened"
Grid.Row="1"
Height="Auto"
Margin="0,4"
Background="Transparent"
BorderThickness="0"
Style="{StaticResource Style.ListView.Borderless}"
ItemContainerStyle="{StaticResource Style.ListViewItem.Borderless}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
GotFocus="RecentsGotFocus"
SelectionChanged="RecentsSelectionChanged"
MouseDoubleClick="RecentsMouseDoubleClick">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type git:Repository}">
<StackPanel Orientation="Horizontal" Height="24">
<Path Height="12" Margin="16,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label Margin="4,0,0,0" Content="{Binding Name}"/>
<Label FontSize="11" Content="{Binding Path}" Opacity=".5"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Repositories' tree -->
<Label Grid.Row="2" Margin="8,8,0,0" Content="REPOSITORIES" Style="{StaticResource Style.Label.GroupHeader}"/>
<TreeView
x:Name="repositories"
Grid.Row="3"
Margin="0, 4"
AllowDrop="True"
ContextMenuOpening="TreeContextMenuOpening"
Drop="TreeDrop"
GotFocus="TreeGotFocus"
MouseMove="TreeMouseMove">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpended, Mode=TwoWay}"/>
<EventSetter Event="Selected" Handler="TreeNodeSelected"/>
<EventSetter Event="DragOver" Handler="TreeNodeDragOver"/>
<EventSetter Event="Drop" Handler="TreeNodeDrop"/>
<EventSetter Event="Expanded" Handler="TreeNodeIsExpandedChanged"/>
<EventSetter Event="Collapsed" Handler="TreeNodeIsExpandedChanged"/>
<EventSetter Event="KeyDown" Handler="TreeNodeKeyDown"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeNodeContextMenuOpening"/>
<EventSetter Event="MouseDoubleClick" Handler="TreeNodeDoubleClick"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path x:Name="icon" Grid.Column="0" Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder.Fill}"/>
<Label x:Name="name" Grid.Column="1" Margin="4,0,0,0" Content="{Binding Name}" />
<TextBox x:Name="editName" Grid.Column="1" Margin="4,0,0,0" Text="{Binding Name}" Loaded="TreeNodeRenameStart" KeyDown="TreeNodeRenameKeyDown" LostFocus="TreeNodeRenameEnd"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsExpended}" Value="True">
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsRepo}" Value="True">
<Setter TargetName="icon" Property="Data" Value="{StaticResource Icon.Git}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter TargetName="name" Property="Visibility" Value="Hidden"/>
<Setter TargetName="editName" Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsEditing}" Value="False">
<Setter TargetName="name" Property="Visibility" Value="Visible"/>
<Setter TargetName="editName" Property="Visibility" Value="Hidden"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="Transparent"/>
<!-- Right Panel -->
<Grid Grid.Column="2" Background="{StaticResource Brush.BG3}">
<!-- Brief -->
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Name & Path & OpenButton -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="repoName" FontSize="20" FontWeight="Bold"/>
<Label Grid.Column="1" x:Name="repoPath" FontSize="20" FontWeight="Light" Opacity="0.5" VerticalAlignment="Center"/>
<Button Grid.Column="3" Click="OpenRepo" ToolTip="Open">
<Path Width="20" Height="20" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Folder}"/>
</Button>
</Grid>
<!-- Status of selected repository -->
<Label Grid.Row="1" Content="STATUS" FontSize="16" Margin="0,16,0,4" FontWeight="Bold" Opacity=".8"/>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<Label Content="Local Changes :" Opacity=".5" FontWeight="Bold"/>
<Label x:Name="localChanges" Margin="2,0,0,0"/>
<Label Content="Total Commits :" Opacity=".5" FontWeight="Bold"/>
<Label x:Name="totalCommits" Margin="2,0,0,0"/>
<Label Content="Last Commit :" Opacity=".5" FontWeight="Bold"/>
<Border Background="{StaticResource Brush.BG4}" Height="18" CornerRadius="4" VerticalAlignment="Center">
<Label x:Name="lastCommitId" Foreground="{StaticResource Brush.FG2}" FontFamily="Consolas" Padding="4,0" VerticalAlignment="Center"/>
</Border>
<Label x:Name="lastCommit" Margin="2,0,0,0"/>
</StackPanel>
<!-- README.md -->
<Label Grid.Row="3" Content="README" FontSize="16" Margin="0,16,0,4" FontWeight="Bold" Opacity=".8"/>
<Border Grid.Row="4" Margin="6,0,0,0" BorderBrush="{StaticResource Brush.BG4}" BorderThickness="1">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock FontSize="10pt"
FontFamily="Consolas"
Padding="8"
Opacity="0.8"
Background="{StaticResource Brush.BG2}"
Foreground="{StaticResource Brush.FG}"
x:Name="readme"/>
</ScrollViewer>
</Border>
</Grid>
<!-- Mask -->
<Border x:Name="briefMask" Background="{StaticResource Brush.BG3}" IsHitTestVisible="False">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="160" Height="160" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Book}"/>
<Label Margin="0,8,0,0" Content="DRAG &amp; DROP FOLDER TO LEFT" FontSize="24" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Grid>
<!-- Popups -->
<local:PopupManager Grid.Row="1"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,495 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SourceGit.UI {
/// <summary>
/// Repository manager.
/// </summary>
public partial class Manager : UserControl {
private TreeViewItem selectedTreeViewItem = null;
/// <summary>
/// Used to build tree
/// </summary>
public class Node {
public string Id { get; set; }
public string ParentId { get; set; }
public string Name { get; set; }
public bool IsRepo { get; set; }
public bool IsExpended { get; set; }
public bool IsEditing { get; set; }
public List<Node> Children { get; set; } = new List<Node>();
}
/// <summary>
/// Constructor.
/// </summary>
public Manager() {
Loaded += async (o, e) => {
await Task.Delay(500);
GC.Collect();
};
InitializeComponent();
UpdateRecentOpened();
UpdateTree();
}
#region TOOLBAR
/// <summary>
/// Open or add local repository.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OpenOrAddRepo(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Open or init local repository";
dialog.RootFolder = Environment.SpecialFolder.MyComputer;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
CheckAndOpenRepo(dialog.SelectedPath);
}
}
/// <summary>
/// Clone remote repository.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CloneRepo(object sender, RoutedEventArgs e) {
if (MakeSureReady()) Clone.Show();
}
#endregion
#region EVENT_RECENT_LISTVIEW
private void RecentsGotFocus(object sender, RoutedEventArgs e) {
if (selectedTreeViewItem != null) selectedTreeViewItem.IsSelected = false;
selectedTreeViewItem = null;
e.Handled = true;
}
private void RecentsSelectionChanged(object sender, SelectionChangedEventArgs e) {
var recent = recentOpened.SelectedItem as Git.Repository;
if (recent != null) ShowBrief(recent);
e.Handled = true;
}
private void RecentsMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var list = sender as ListView;
var recent = list.SelectedItem as Git.Repository;
if (recent != null) {
CheckAndOpenRepo(recent.Path);
e.Handled = true;
}
}
#endregion
#region EVENT_TREEVIEW
private void TreeGotFocus(object sender, RoutedEventArgs e) {
recentOpened.SelectedItems.Clear();
e.Handled = true;
}
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var addFolder = new MenuItem();
addFolder.Header = "Add Folder";
addFolder.Click += (o, ev) => {
var group = App.Preference.AddGroup("New Group", "");
UpdateTree(group.Id);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(addFolder);
menu.IsOpen = true;
e.Handled = true;
}
private void TreeMouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton != MouseButtonState.Pressed) return;
if (selectedTreeViewItem == null) return;
var node = selectedTreeViewItem.DataContext as Node;
if (node == null || !node.IsRepo) return;
DragDrop.DoDragDrop(repositories, selectedTreeViewItem, DragDropEffects.Move);
e.Handled = true;
}
private void TreeDrop(object sender, DragEventArgs e) {
bool needRebuild = false;
if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
if (!MakeSureReady()) return;
string[] paths = e.Data.GetData(DataFormats.FileDrop) as string[];
string group = "";
var node = (sender as TreeViewItem)?.DataContext as Node;
if (node != null) group = node.IsRepo ? node.ParentId : node.Id;
foreach (var path in paths) {
FileInfo info = new FileInfo(path);
if (info.Attributes == FileAttributes.Directory && Git.Repository.IsValid(path)) {
App.Preference.AddRepository(path, group);
needRebuild = true;
}
}
} else if (e.Data.GetDataPresent(typeof(TreeViewItem))) {
var item = e.Data.GetData(typeof(TreeViewItem)) as TreeViewItem;
var node = item.DataContext as Node;
if (node == null || !node.IsRepo) return;
var group = "";
var to = (sender as TreeViewItem)?.DataContext as Node;
if (to != null) group = to.IsRepo ? to.ParentId : to.Id;
App.Preference.FindRepository(node.Id).GroupId = group;
needRebuild = true;
}
if (needRebuild) UpdateTree();
e.Handled = true;
}
#endregion
#region EVENT_TREEVIEWITEM
private void TreeNodeSelected(object sender, RoutedEventArgs e) {
selectedTreeViewItem = sender as TreeViewItem;
var node = selectedTreeViewItem.DataContext as Node;
if (node.IsRepo) {
ShowBrief(App.Preference.FindRepository(node.Id));
} else {
HideBrief();
}
e.Handled = true;
}
private void TreeNodeDoubleClick(object sender, MouseButtonEventArgs e) {
var node = (sender as TreeViewItem).DataContext as Node;
if (node != null && node.IsRepo) {
CheckAndOpenRepo(node.Id);
e.Handled = true;
}
}
private void TreeNodeDragOver(object sender, DragEventArgs e) {
var item = sender as TreeViewItem;
var node = item.DataContext as Node;
if (node != null && !node.IsRepo) item.IsExpanded = true;
e.Handled = true;
}
private void TreeNodeDrop(object sender, DragEventArgs e) {
TreeDrop(sender, e);
}
private void TreeNodeIsExpandedChanged(object sender, RoutedEventArgs e) {
var item = sender as TreeViewItem;
var node = item.DataContext as Node;
if (node != null && !node.IsRepo) {
var group = App.Preference.FindGroup(node.Id);
group.IsExpended = item.IsExpanded;
e.Handled = true;
}
}
private void TreeNodeKeyDown(object sender, KeyEventArgs e) {
if (e.Key != Key.Delete) return;
var node = (sender as TreeViewItem).DataContext as Node;
if (node != null) DeleteNode(node);
e.Handled = true;
}
private void TreeNodeRenameStart(object sender, RoutedEventArgs e) {
var text = sender as TextBox;
if (text.IsVisible) {
text.SelectAll();
text.Focus();
}
e.Handled = true;
}
private void TreeNodeRenameKeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Escape) {
UpdateTree();
e.Handled = true;
} else if (e.Key == Key.Enter) {
TreeNodeRenameEnd(sender, e);
e.Handled = true;
}
}
private void TreeNodeRenameEnd(object sender, RoutedEventArgs e) {
var text = sender as TextBox;
if (string.IsNullOrWhiteSpace(text.Text)) {
UpdateTree();
e.Handled = false;
return;
}
var node = text.DataContext as Node;
if (node != null) {
if (node.IsRepo) {
App.Preference.RenameRepository(node.Id, text.Text);
} else {
App.Preference.RenameGroup(node.Id, text.Text);
}
UpdateRecentOpened();
UpdateTree();
e.Handled = true;
}
}
private void TreeNodeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as TreeViewItem;
var node = item.DataContext as Node;
var menu = new ContextMenu();
if (node.IsRepo) {
var open = new MenuItem();
open.Header = "Open";
open.Click += (o, ev) => {
CheckAndOpenRepo(node.Id);
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = "Open Container Folder";
explore.Click += (o, ev) => {
Process.Start(node.Id);
ev.Handled = true;
};
menu.Items.Add(open);
menu.Items.Add(explore);
} else {
var addSubFolder = new MenuItem();
addSubFolder.Header = "Add Sub-Folder";
addSubFolder.Click += (o, ev) => {
var parent = App.Preference.FindGroup(node.Id);
if (parent != null) parent.IsExpended = true;
var group = App.Preference.AddGroup("New Group", node.Id);
UpdateTree(group.Id);
ev.Handled = true;
};
menu.Items.Add(addSubFolder);
}
var rename = new MenuItem();
rename.Header = "Rename";
rename.Click += (o, ev) => {
UpdateTree(node.Id);
ev.Handled = true;
};
var delete = new MenuItem();
delete.Header = "Delete";
delete.Click += (o, ev) => {
DeleteNode(node);
ev.Handled = true;
};
menu.Items.Add(rename);
menu.Items.Add(delete);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region EVENT_BRIEF
private void ShowBrief(Git.Repository repo) {
if (repo == null || !Git.Repository.IsValid(repo.Path)) {
if (Directory.Exists(repo.Path)) {
Init.Show(repo.Path);
} else {
App.RaiseError("Path is NOT valid git repository or has been removed.");
App.Preference.RemoveRepository(repo.Path);
UpdateRecentOpened();
UpdateTree();
}
return;
}
briefMask.Visibility = Visibility.Hidden;
repoName.Content = repo.Name;
repoPath.Content = repo.Path;
Task.Run(() => {
var changes = repo.LocalChanges();
var count = changes.Count;
Dispatcher.Invoke(() => localChanges.Content = count);
});
Task.Run(() => {
var count = repo.TotalCommits();
Dispatcher.Invoke(() => totalCommits.Content = count);
});
Task.Run(() => {
var commits = repo.Commits("-n 1");
Dispatcher.Invoke(() => {
if (commits.Count > 0) {
var c = commits[0];
lastCommitId.Content = c.ShortSHA;
lastCommit.Content = c.Subject;
} else {
lastCommitId.Content = "---";
lastCommit.Content = "";
}
});
});
if (File.Exists(repo.Path + "/README.md")) {
readme.Text = File.ReadAllText(repo.Path + "/README.md");
} else {
readme.Text = "";
}
}
private void HideBrief() {
briefMask.Visibility = Visibility.Visible;
}
private void OpenRepo(object sender, RoutedEventArgs e) {
CheckAndOpenRepo(repoPath.Content as string);
}
#endregion
#region PRIVATES
/// <summary>
/// Make sure git is configured.
/// </summary>
/// <returns></returns>
private bool MakeSureReady() {
if (!App.IsGitConfigured) {
App.RaiseError("Git has NOT been configured.\nPlease to go [Preference] and configure it first.");
return false;
}
return true;
}
/// <summary>
/// Check and open repository
/// </summary>
/// <param name="path"></param>
private void CheckAndOpenRepo(string path) {
if (!MakeSureReady()) return;
if (!Git.Repository.IsValid(path)) {
if (Directory.Exists(path)) {
Init.Show(path);
return;
}
App.RaiseError($"Path[{path}] not exists!");
return;
}
var repo = App.Preference.AddRepository(path, "");
repo.Open();
}
/// <summary>
/// Update recent opened repositories.
/// </summary>
private void UpdateRecentOpened() {
var sorted = App.Preference.Repositories.OrderByDescending(a => a.LastOpenTime).ToList();
var top5 = new List<Git.Repository>();
for (int i = 0; i < sorted.Count && i < 5; i++) {
if (sorted[i].LastOpenTime <= 0) break;
top5.Add(sorted[i]);
}
recentOpened.ItemsSource = top5;
}
/// <summary>
/// Update tree items.
/// </summary>
/// <param name="editingNodeId"></param>
private void UpdateTree(string editingNodeId = null) {
var groupNodes = new Dictionary<string, Node>();
var nodes = new List<Node>();
foreach (var group in App.Preference.Groups) {
Node node = new Node() {
Id = group.Id,
ParentId = group.ParentId,
Name = group.Name,
IsRepo = false,
IsExpended = group.IsExpended,
IsEditing = group.Id == editingNodeId,
};
groupNodes.Add(node.Id, node);
}
nodes.Clear();
foreach (var kv in groupNodes) {
if (groupNodes.ContainsKey(kv.Value.ParentId)) {
groupNodes[kv.Value.ParentId].Children.Add(kv.Value);
} else {
nodes.Add(kv.Value);
}
}
foreach (var repo in App.Preference.Repositories) {
Node node = new Node() {
Id = repo.Path,
ParentId = repo.GroupId,
Name = repo.Name,
IsRepo = true,
IsExpended = false,
IsEditing = repo.Path == editingNodeId,
};
if (groupNodes.ContainsKey(repo.GroupId)) {
groupNodes[repo.GroupId].Children.Add(node);
} else {
nodes.Add(node);
}
}
repositories.ItemsSource = nodes;
}
/// <summary>
/// Delete tree node.
/// </summary>
/// <param name="node"></param>
private void DeleteNode(Node node) {
if (node.IsRepo) {
App.Preference.RemoveRepository(node.Id);
UpdateRecentOpened();
} else {
App.Preference.RemoveGroup(node.Id);
}
UpdateTree();
}
#endregion
}
}

Some files were not shown because too many files have changed in this diff Show more