refactor<*>: rewrite all codes...

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

View file

@ -0,0 +1,196 @@
<UserControl x:Class="SourceGit.Views.Widgets.CommitChanges"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converters:PureFileName x:Key="PureFileName"/>
<converters:PureFolderName x:Key="PureFolderName"/>
<Style x:Key="Style.DataGridRow.Changes" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
<EventSetter Event="ContextMenuOpening" Handler="OnDataGridContextMenuOpening"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<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"
BorderBrush="{StaticResource Brush.Border2}"
BorderThickness="1"/>
<Path
Grid.Column="0"
Width="14" Height="14"
Fill="{StaticResource Brush.FG2}"
Data="{StaticResource Icon.Search}"
IsHitTestVisible="False"/>
<controls:TextEdit
Grid.Column="1"
Height="24"
Margin="0"
Placeholder="{StaticResource Text.CommitViewer.Changes.Search}"
BorderThickness="0"
TextChanged="SearchFilterChanged"/>
<controls:ChangeDisplaySwitcher
Grid.Column="2"
x:Name="modeSwitcher"
Margin="4,0,0,0" Width="18" Height="18"
Mode="{Binding Source={x:Static models:Preference.Instance}, Path=Window.ChangeInCommitInfo, Mode=TwoWay}"
ModeChanged="OnDisplayModeChanged"/>
</Grid>
<Border
Grid.Row="1"
BorderBrush="{StaticResource Brush.Border2}"
BorderThickness="1"
Background="{StaticResource Brush.Contents}">
<Grid>
<controls:Tree
x:Name="modeTree"
FontFamily="Consolas"
SelectionChanged="OnTreeSelectionChanged">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="OnTreeContextMenuOpening"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<controls:ChangeStatusIcon
Grid.Column="0"
Width="14" Height="14"
IsLocalChange="False"
Change="{Binding Change}"/>
<Path
Grid.Column="0"
x:Name="IconFolder"
Width="14" Height="14"
Fill="Goldenrod"
Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock
Grid.Column="1"
Text="{Binding Path, Converter={StaticResource PureFileName}}"
Margin="4,0,0,0"
FontSize="11"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="False">
<Setter TargetName="IconFolder" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsExpanded}" Value="True">
<Setter TargetName="IconFolder" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
<DataGrid
x:Name="modeList"
RowHeight="24"
SelectionMode="Single"
SelectionUnit="FullRow"
SelectionChanged="OnListSelectionChanged"
RowStyle="{StaticResource Style.DataGridRow.Changes}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type models:Change}">
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="False" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid
x:Name="modeGrid"
RowHeight="24"
SelectionMode="Single"
SelectionUnit="FullRow"
SelectionChanged="OnGridSelectionChanged"
RowStyle="{StaticResource Style.DataGridRow.Changes}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type models:Change}">
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="False" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="4,0,0,0" Text="{Binding Path, Converter={StaticResource PureFolderName}}" Foreground="{StaticResource Brush.FG2}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Grid>
<GridSplitter
Grid.Column="1"
Width="1"
HorizontalAlignment="Center" VerticalAlignment="Stretch"
Background="Transparent"/>
<widgets:DiffViewer
Grid.Column="2"
x:Name="diffViewer"
Margin="4,0,0,0"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,310 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 显示提交中的变更列表
/// </summary>
public partial class CommitChanges : UserControl {
private string repo = null;
private List<Models.Commit> range = null;
private List<Models.Change> cachedChanges = new List<Models.Change>();
private string filter = null;
public class ChangeNode {
public string Path { get; set; } = "";
public Models.Change Change { get; set; } = null;
public bool IsExpanded { get; set; } = false;
public bool IsFolder => Change == null;
public List<ChangeNode> Children { get; set; } = new List<ChangeNode>();
}
public CommitChanges() {
InitializeComponent();
}
public void SetData(string repo, List<Models.Commit> range, List<Models.Change> changes) {
this.repo = repo;
this.range = range;
this.cachedChanges = changes;
UpdateVisible();
}
public void UpdateVisible() {
Task.Run(() => {
// 筛选出可见的列表
List<Models.Change> visible;
if (string.IsNullOrEmpty(filter)) {
visible = cachedChanges;
} else {
visible = cachedChanges.Where(x => x.Path.ToUpper().Contains(filter)).ToList();
}
// 排序
visible.Sort((l, r) => l.Path.CompareTo(r.Path));
// 生成树节点
var nodes = new List<ChangeNode>();
var folders = new Dictionary<string, ChangeNode>();
var expanded = visible.Count <= 50;
foreach (var c in visible) {
var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new ChangeNode() {
Path = c.Path,
Change = c,
IsExpanded = false
});
} else {
ChangeNode 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 ChangeNode() {
Path = folder,
Change = null,
IsExpanded = expanded
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new ChangeNode() {
Path = folder,
Change = null,
IsExpanded = expanded
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new ChangeNode() {
Path = c.Path,
Change = c,
IsExpanded = false
});
}
}
folders.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
modeTree.ItemsSource = nodes;
modeList.ItemsSource = visible;
modeGrid.ItemsSource = visible;
UpdateMode();
});
});
}
private void SortFileNodes(List<ChangeNode> nodes) {
nodes.Sort((l, r) => {
if (l.IsFolder == r.IsFolder) {
return l.Path.CompareTo(r.Path);
} else {
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes) {
if (node.Children.Count > 1) SortFileNodes(node.Children);
}
}
private void UpdateMode() {
var mode = modeSwitcher.Mode;
if (modeTree != null) {
if (mode == Models.Change.DisplayMode.Tree) {
modeTree.Visibility = Visibility.Visible;
} else {
modeTree.Visibility = Visibility.Collapsed;
}
}
if (modeList != null) {
if (mode == Models.Change.DisplayMode.List) {
modeList.Visibility = Visibility.Visible;
modeList.Columns[1].Width = DataGridLength.SizeToCells;
modeList.Columns[1].Width = DataGridLength.Auto;
} else {
modeList.Visibility = Visibility.Collapsed;
}
}
if (modeGrid != null) {
if (mode == Models.Change.DisplayMode.Grid) {
modeGrid.Visibility = Visibility.Visible;
modeGrid.Columns[1].Width = DataGridLength.SizeToCells;
modeGrid.Columns[1].Width = DataGridLength.Auto;
modeGrid.Columns[2].Width = DataGridLength.SizeToCells;
modeGrid.Columns[2].Width = DataGridLength.Auto;
} else {
modeGrid.Visibility = Visibility.Collapsed;
}
}
}
private void OpenChangeDiff(Models.Change change) {
var revisions = new string[] { "", "" };
if (range.Count == 2) {
revisions[0] = range[0].SHA;
revisions[1] = range[1].SHA;
} else {
revisions[0] = $"{range[0].SHA}^";
revisions[1] = range[0].SHA;
if (range[0].Parents.Count == 0) revisions[0] = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
}
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = revisions,
Path = change.Path,
OrgPath = change.OriginalPath
});
}
private void OpenChangeContextMenu(Models.Change change) {
var menu = new ContextMenu();
var path = change.Path;
if (change.Index != Models.Change.Status.Deleted) {
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (o, ev) => {
var viewer = new Views.Histories(repo, path);
viewer.Show();
ev.Handled = true;
};
var blame = new MenuItem();
blame.Header = App.Text("Blame");
blame.Visibility = range.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
blame.Click += (obj, ev) => {
var viewer = new Blame(repo, path, range[0].SHA);
viewer.Show();
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Click += (o, ev) => {
var full = Path.GetFullPath(repo + "\\" + path);
Process.Start("explorer", $"/select,{full}");
ev.Handled = true;
};
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");
saveAs.Visibility = range.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => {
var full = Path.Combine(saveTo, Path.GetFileName(path));
new Commands.SaveRevisionFile(repo, path, range[0].SHA, full).Exec();
});
ev.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(blame);
menu.Items.Add(explore);
menu.Items.Add(saveAs);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
menu.IsOpen = true;
}
private void OnDisplayModeChanged(object sender, RoutedEventArgs e) {
UpdateMode();
}
private void SearchFilterChanged(object sender, TextChangedEventArgs e) {
var edit = sender as Controls.TextEdit;
filter = edit.Text.ToUpper();
UpdateVisible();
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void OnTreeSelectionChanged(object sender, RoutedEventArgs e) {
if (Models.Preference.Instance.Window.ChangeInCommitInfo != Models.Change.DisplayMode.Tree) return;
diffViewer.Reset();
if (modeTree.Selected.Count == 0) return;
var change = (modeTree.Selected[0] as ChangeNode).Change;
if (change == null) return;
OpenChangeDiff(change);
}
private void OnListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Models.Preference.Instance.Window.ChangeInCommitInfo != Models.Change.DisplayMode.List) return;
diffViewer.Reset();
var change = (sender as DataGrid).SelectedItem as Models.Change;
if (change == null) return;
OpenChangeDiff(change);
}
private void OnGridSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Models.Preference.Instance.Window.ChangeInCommitInfo != Models.Change.DisplayMode.Grid) return;
diffViewer.Reset();
var change = (sender as DataGrid).SelectedItem as Models.Change;
if (change == null) return;
OpenChangeDiff(change);
}
private void OnTreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as Controls.TreeItem;
if (item == null) return;
var node = item.DataContext as ChangeNode;
if (node == null || node.IsFolder) return;
OpenChangeContextMenu(node.Change);
e.Handled = true;
}
private void OnDataGridContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.Item as Models.Change;
if (change == null) return;
OpenChangeContextMenu(change);
e.Handled = true;
}
}
}

View file

@ -0,0 +1,344 @@
<UserControl x:Class="SourceGit.Views.Widgets.CommitDetail"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="Style.DataGridRow.TextPreview" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
</Style>
<converters:PureFileName x:Key="PureFileName"/>
</UserControl.Resources>
<TabControl>
<TabItem Header="{StaticResource Text.CommitViewer.Info}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Author & Committer -->
<Grid Grid.Row="0" Margin="0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Author Avatar -->
<controls:Avatar
Grid.Column="0"
x:Name="avatarAuthor"
Width="64" Height="64"
HorizontalAlignment="Right"/>
<!-- Author Info -->
<StackPanel Grid.Column="1" Margin="16,0,8,0" Orientation="Vertical">
<TextBlock Text="{StaticResource Text.CommitViewer.Info.Author}" Foreground="{StaticResource Brush.FG2}"/>
<StackPanel Orientation="Horizontal" Margin="0,12,0,8">
<controls:TextEdit x:Name="txtAuthorName" IsReadOnly="True" BorderThickness="0"/>
<controls:TextEdit x:Name="txtAuthorEmail" IsReadOnly="True" BorderThickness="0" Foreground="{StaticResource Brush.FG2}" Margin="4,0,0,0"/>
</StackPanel>
<controls:TextEdit x:Name="txtAuthorTime" IsReadOnly="True" BorderThickness="0" Foreground="{StaticResource Brush.FG2}" FontSize="10"/>
</StackPanel>
<!-- Committer Avatar -->
<controls:Avatar
Grid.Column="2"
x:Name="avatarCommitter"
Width="64" Height="64"
HorizontalAlignment="Right"/>
<!-- Committer Info -->
<StackPanel x:Name="committerInfoPanel" Grid.Column="3" Margin="16,0,8,0" Orientation="Vertical">
<TextBlock Text="{StaticResource Text.CommitViewer.Info.Committer}" Foreground="{StaticResource Brush.FG2}"/>
<StackPanel Orientation="Horizontal" Margin="0,12,0,6">
<controls:TextEdit x:Name="txtCommitterName" IsReadOnly="True" BorderThickness="0"/>
<controls:TextEdit x:Name="txtCommitterEmail" IsReadOnly="True" BorderThickness="0" Foreground="{StaticResource Brush.FG2}" Margin="4,0,0,0"/>
</StackPanel>
<controls:TextEdit x:Name="txtCommitterTime" IsReadOnly="True" BorderThickness="0" Foreground="{StaticResource Brush.FG2}" FontSize="10"/>
</StackPanel>
</Grid>
<!-- Line -->
<Rectangle Grid.Row="1" Height="1" Margin="8" Fill="{StaticResource Brush.Border2}" VerticalAlignment="Center"/>
<!-- Base Information -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" x:Name="rowParents"/>
<RowDefinition Height="Auto" x:Name="rowRefs"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SHA -->
<TextBlock
Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
Text="{StaticResource Text.CommitViewer.Info.SHA}"
Foreground="{StaticResource Brush.FG2}"/>
<controls:TextEdit
Grid.Row="0" Grid.Column="1"
Height="24"
x:Name="txtSHA"
IsReadOnly="True"
BorderThickness="0"
FontFamily="Consolas"
Margin="11,0,0,0"/>
<!-- PARENTS -->
<TextBlock
Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right"
Text="{StaticResource Text.CommitViewer.Info.Parents}"
Foreground="{StaticResource Brush.FG2}"/>
<ItemsControl Grid.Row="1" Grid.Column="1" x:Name="listParents" Height="24" Margin="13,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Margin="0,0,8,0" FontFamily="Consolas">
<Hyperlink RequestNavigate="OnNavigateParent" NavigateUri="{Binding .}" ToolTip="{StaticResource Text.Goto}">
<Run Text="{Binding .}"/>
</Hyperlink>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- REFS -->
<TextBlock
Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right"
Text="{StaticResource Text.CommitViewer.Info.Refs}"
Foreground="{StaticResource Brush.FG2}"/>
<ItemsControl Grid.Row="2" Grid.Column="1" x:Name="listRefs" Height="24" Margin="11,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type models:Decorator}">
<StackPanel Orientation="Horizontal" Height="16" Margin="2,0">
<Border Background="{StaticResource Brush.Decorator}">
<Path x:Name="Icon" Margin="4,0" Width="8" Height="8" Data="{StaticResource Icon.Branch}"/>
</Border>
<Border x:Name="Color" Background="#FFFFB835">
<TextBlock Text="{Binding Name}" FontSize="11" Margin="4,0" Foreground="Black"/>
</Border>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.CurrentBranchHead}">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.RemoteBranchHead}">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.Tag}">
<Setter TargetName="Color" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Message -->
<TextBlock
Grid.Row="3" Grid.Column="0"
Margin="0,4,0,0"
HorizontalAlignment="Right" VerticalAlignment="Top"
Text="{StaticResource Text.CommitViewer.Info.Message}"
Foreground="{StaticResource Brush.FG2}"/>
<controls:TextEdit
Grid.Row="3" Grid.Column="1"
x:Name="txtMessage"
IsReadOnly="true"
FontFamily="Consolas"
BorderThickness="0"
TextWrapping="Wrap"
Margin="11,5,16,0"
VerticalAlignment="Top"/>
</Grid>
<!-- Line -->
<Rectangle Grid.Row="3" Height="1" Margin="8" Fill="{StaticResource Brush.Border2}" VerticalAlignment="Center"/>
<!-- Change List -->
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="0,4,0,0"
HorizontalAlignment="Right" VerticalAlignment="Top"
Text="{StaticResource Text.CommitViewer.Info.Changed}"
Foreground="{StaticResource Brush.FG2}"/>
<DataGrid
Grid.Column="1"
x:Name="changeList"
RowHeight="24"
Margin="11,0,0,2">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow.TextPreview}">
<EventSetter Event="ContextMenuOpening" Handler="OnChangeListContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="False" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
</TabItem>
<!-- Change Details -->
<TabItem Header="{StaticResource Text.CommitViewer.Changes}">
<widgets:CommitChanges x:Name="changeContainer"/>
</TabItem>
<!-- Revision Files -->
<TabItem Header="{StaticResource Text.CommitViewer.Files}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.Contents}" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1">
<controls:Tree
x:Name="treeFiles"
FontFamily="Consolas"
SelectionChanged="OnFilesSelectionChanged"
ContextMenuOpening="OnFilesContextMenuOpening">
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="Icon" Width="14" Height="14" Data="{StaticResource Icon.File}"/>
<TextBlock Margin="6,0,0,0" FontSize="11" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="Goldenrod"/>
<Setter TargetName="Icon" Property="Opacity" Value="1"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Border>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1" Margin="2,0">
<Grid>
<Grid x:Name="layerTextPreview" Visibility="Collapsed" SizeChanged="OnTextPreviewSizeChanged">
<DataGrid
x:Name="txtPreviewData"
FontFamily="Consolas"
RowHeight="16"
RowStyle="{StaticResource Style.DataGridRow.TextPreview}"
FrozenColumnCount="1"
ContextMenuOpening="OnTextPreviewContextMenuOpening"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Number}" ElementStyle="{StaticResource Style.TextBlock.LineNumber}"/>
<DataGridTextColumn Binding="{Binding Data}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
</DataGrid.Columns>
</DataGrid>
<Rectangle x:Name="txtPreviewSplitter" Width="1" Fill="{StaticResource Brush.Border2}" HorizontalAlignment="Left"/>
</Grid>
<ScrollViewer
x:Name="layerImagePreview"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Visibility="Collapsed">
<Image
x:Name="imgPreviewData"
Width="Auto" Height="Auto"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</ScrollViewer>
<StackPanel
x:Name="layerRevisionPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path x:Name="iconRevisionPreview" Width="64" Height="64" Data="{StaticResource Icon.Submodule}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock x:Name="txtRevisionPreview" Margin="0,16,0,0" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
<StackPanel
x:Name="layerBinaryPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path Width="64" Height="64" Data="{StaticResource Icon.Error}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0" Text="{StaticResource Text.BinaryNotSupported}" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</TabItem>
</TabControl>
</UserControl>

View file

@ -0,0 +1,416 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 提交详情
/// </summary>
public partial class CommitDetail : UserControl {
private string repo = null;
private Models.Commit commit = null;
private Commands.Cancellable cancelToken = new Commands.Cancellable();
/// <summary>
/// 文件列表树节点
/// </summary>
public class FileNode {
public Models.ObjectType Type { get; set; } = Models.ObjectType.None;
public string Path { get; set; } = "";
public string SHA { get; set; } = null;
public bool IsFolder => Type == Models.ObjectType.None;
public List<FileNode> Children { get; set; } = new List<FileNode>();
}
public CommitDetail() {
InitializeComponent();
}
public void SetData(string repo, Models.Commit commit) {
cancelToken.IsCancelRequested = true;
cancelToken = new Commands.Cancellable();
this.repo = repo;
this.commit = commit;
UpdateInformation(commit);
UpdateChanges();
UpdateRevisionFiles();
}
#region DATA
private void UpdateInformation(Models.Commit commit) {
txtSHA.Text = commit.SHA;
txtMessage.Text = (commit.Subject + "\n\n" + commit.Message.Trim()).Trim();
avatarAuthor.Email = commit.Author.Email;
avatarAuthor.FallbackLabel = commit.Author.Name;
txtAuthorName.Text = commit.Author.Name;
txtAuthorEmail.Text = commit.Author.Email;
txtAuthorTime.Text = commit.Author.Time;
avatarCommitter.Email = commit.Committer.Email;
avatarCommitter.FallbackLabel = commit.Committer.Name;
txtCommitterName.Text = commit.Committer.Name;
txtCommitterEmail.Text = commit.Committer.Email;
txtCommitterTime.Text = commit.Committer.Time;
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
avatarCommitter.Visibility = Visibility.Hidden;
committerInfoPanel.Visibility = Visibility.Hidden;
} else {
avatarCommitter.Visibility = Visibility.Visible;
committerInfoPanel.Visibility = Visibility.Visible;
}
if (commit.Parents.Count == 0) {
rowParents.Height = new GridLength(0);
} else {
rowParents.Height = GridLength.Auto;
var shortPIDs = new List<string>();
foreach (var p in commit.Parents) shortPIDs.Add(p.Substring(0, 10));
listParents.ItemsSource = shortPIDs;
}
if (!commit.HasDecorators) {
rowRefs.Height = new GridLength(0);
} else {
rowRefs.Height = GridLength.Auto;
listRefs.ItemsSource = commit.Decorators;
}
}
private void UpdateChanges() {
var cmd = new Commands.CommitChanges(repo, commit.SHA) { Token = cancelToken };
Task.Run(() => {
var changes = cmd.Result();
if (cmd.Token.IsCancelRequested) return;
Dispatcher.Invoke(() => {
changeList.ItemsSource = changes;
changeContainer.SetData(repo, new List<Models.Commit>() { commit }, changes);
});
});
}
private void SortFileNodes(List<FileNode> nodes) {
nodes.Sort((l, r) => {
if (l.IsFolder == r.IsFolder) {
return l.Path.CompareTo(r.Path);
} else {
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes) {
if (node.Children.Count > 1) SortFileNodes(node.Children);
}
}
private void UpdateRevisionFiles() {
var cmd = new Commands.RevisionObjects(repo, commit.SHA) { Token = cancelToken };
Task.Run(() => {
var objects = cmd.Result();
if (cmd.Token.IsCancelRequested) return;
var nodes = new List<FileNode>();
var folders = new Dictionary<string, FileNode>();
foreach (var obj in objects) {
var sepIdx = obj.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
} else {
FileNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
}
obj.Path = null;
}
folders.Clear();
objects.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
treeFiles.ItemsSource = nodes;
GC.Collect();
});
});
}
#endregion
#region INFORMATION
private void OnNavigateParent(object sender, RequestNavigateEventArgs e) {
Models.Watcher.Get(repo)?.NavigateTo(e.Uri.OriginalString);
}
private void OnChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
var row = sender as DataGridRow;
if (row == null) return;
var change = row.DataContext as Models.Change;
if (change == null) return;
var menu = new ContextMenu();
FillContextMenu(menu, change.Path, change.Index == Models.Change.Status.Deleted, true);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region REVISION_FILES
private bool IsImageFile(string path) {
return path.EndsWith(".png") ||
path.EndsWith(".jpg") ||
path.EndsWith(".jpeg") ||
path.EndsWith(".ico") ||
path.EndsWith(".bmp") ||
path.EndsWith(".tiff") ||
path.EndsWith(".gif");
}
private void LayoutTextPreview(List<Models.TextLine> lines) {
var font = new FontFamily("Consolas");
var maxLineNumber = $"{lines.Count + 1}";
var formatted = new FormattedText(
maxLineNumber,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
var offset = formatted.Width + 16;
if (lines.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.ItemsSource = lines;
txtPreviewData.Columns[0].Width = new DataGridLength(formatted.Width + 16, DataGridLengthUnitType.Pixel);
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewSplitter.Margin = new Thickness(formatted.Width + 15, 0, 0, 0);
}
private void OnTextPreviewSizeChanged(object sender, SizeChangedEventArgs e) {
if (txtPreviewData == null) return;
var offset = txtPreviewData.NonFrozenColumnsViewportHorizontalOffset;
if (txtPreviewData.Items.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewData.UpdateLayout();
}
private void OnTextPreviewContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copyIcon = new System.Windows.Shapes.Path();
copyIcon.Data = FindResource("Icon.Copy") as Geometry;
copyIcon.Width = 10;
var copy = new MenuItem();
copy.Header = "Copy";
copy.Icon = copyIcon;
copy.Click += (o, ev) => {
var items = grid.SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var line = item as Models.TextLine;
if (line == null) continue;
builder.Append(line.Data);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void OnFilesSelectionChanged(object sender, RoutedEventArgs e) {
layerTextPreview.Visibility = Visibility.Collapsed;
layerImagePreview.Visibility = Visibility.Collapsed;
layerRevisionPreview.Visibility = Visibility.Collapsed;
layerBinaryPreview.Visibility = Visibility.Collapsed;
txtPreviewData.ItemsSource = null;
if (treeFiles.Selected.Count == 0) return;
var node = treeFiles.Selected[0] as FileNode;
switch (node.Type) {
case Models.ObjectType.Blob:
if (IsImageFile(node.Path)) {
var tmp = Path.GetTempFileName();
new Commands.SaveRevisionFile(repo, node.Path, commit.SHA, tmp).Exec();
layerImagePreview.Visibility = Visibility.Visible;
imgPreviewData.Source = new BitmapImage(new Uri(tmp, UriKind.Absolute));
} else if (new Commands.IsLFSFiltered(repo, node.Path).Result()) {
var lfs = new Commands.QueryLFSObject(repo, commit.SHA, node.Path).Result();
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.LFS") as Geometry;
txtRevisionPreview.Text = "LFS SIZE: " + App.Text("Bytes", lfs.Size);
} else if (new Commands.IsBinaryFile(repo, commit.SHA, node.Path).Result()) {
layerBinaryPreview.Visibility = Visibility.Visible;
} else {
layerTextPreview.Visibility = Visibility.Visible;
Task.Run(() => {
var lines = new Commands.QueryFileContent(repo, commit.SHA, node.Path).Result();
Dispatcher.Invoke(() => LayoutTextPreview(lines));
});
}
break;
case Models.ObjectType.Tag:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tag") as Geometry;
txtRevisionPreview.Text = "TAG: " + node.SHA;
break;
case Models.ObjectType.Commit:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Submodule") as Geometry;
txtRevisionPreview.Text = "SUBMODULE: " + node.SHA;
break;
case Models.ObjectType.Tree:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tree") as Geometry;
txtRevisionPreview.Text = "TREE: " + node.SHA;
break;
default:
return;
}
}
private void OnFilesContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = treeFiles.FindItem(e.OriginalSource as DependencyObject);
if (item == null) return;
var node = item.DataContext as FileNode;
if (node == null || node.IsFolder) return;
var menu = new ContextMenu();
FillContextMenu(menu, node.Path, false, node.Type == Models.ObjectType.Blob);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region COMMON
private void FillContextMenu(ContextMenu menu, string path, bool isDeleted, bool canSave) {
if (!isDeleted) {
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (o, ev) => {
var viewer = new Views.Histories(repo, path);
viewer.Show();
ev.Handled = true;
};
var blame = new MenuItem();
blame.Header = App.Text("Blame");
blame.Click += (obj, ev) => {
var viewer = new Blame(repo, path, commit.SHA);
viewer.Show();
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Click += (o, ev) => {
var full = Path.GetFullPath(repo + "\\" + path);
Process.Start("explorer", $"/select,{full}");
ev.Handled = true;
};
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");
saveAs.IsEnabled = canSave;
saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => {
var full = Path.Combine(saveTo, Path.GetFileName(path));
new Commands.SaveRevisionFile(repo, path, commit.SHA, full).Exec();
});
ev.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(blame);
menu.Items.Add(explore);
menu.Items.Add(saveAs);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
};
menu.Items.Add(copyPath);
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,478 @@
<UserControl x:Class="SourceGit.Views.Widgets.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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Toolbar -->
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="6,0">
<Button Click="Explore" Margin="4,0,0,0" ToolTip="{StaticResource Text.Dashboard.Explore.Tip}" BorderThickness="0">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="14" Data="{StaticResource Icon.Folder.Open}"/>
<Label Content="{StaticResource Text.Dashboard.Explore}"/>
</StackPanel>
</Button>
<Button Click="Terminal" Margin="4,0,0,0" ToolTip="{StaticResource Text.Dashboard.Terminal.Tip}" BorderThickness="0">
<StackPanel Orientation="Horizontal">
<Path Width="13" Height="13" Data="{StaticResource Icon.Terminal}"/>
<Label Content="{StaticResource Text.Dashboard.Terminal}"/>
</StackPanel>
</Button>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Click="OpenFetch" Width="72" BorderThickness="0">
<StackPanel Orientation="Horizontal" Margin="6,0">
<Path Width="16" Height="16" Data="{StaticResource Icon.Fetch}"/>
<Label Content="{StaticResource Text.Fetch}" Margin="4,0,0,0"/>
</StackPanel>
</Button>
<Button Click="OpenPull" Width="72" BorderThickness="0">
<StackPanel Orientation="Horizontal" Margin="6,0">
<Path Width="15" Height="15" Data="{StaticResource Icon.Pull}"/>
<Label Content="{StaticResource Text.Pull}" Margin="4,0,0,0"/>
</StackPanel>
</Button>
<Button Click="OpenPush" Width="72" BorderThickness="0">
<StackPanel Orientation="Horizontal" Margin="6,0">
<Path Width="15" Height="15" Data="{StaticResource Icon.Push}"/>
<Label Content="{StaticResource Text.Push}" Margin="4,0,0,0"/>
</StackPanel>
</Button>
<Button Click="OpenStash" Width="72" BorderThickness="0">
<StackPanel Orientation="Horizontal" Margin="6,0">
<Path Width="14" Height="14" Data="{StaticResource Icon.SaveStash}"/>
<Label Content="{StaticResource Text.Stash}" Margin="4,0,0,0"/>
</StackPanel>
</Button>
<Button Click="OpenApply" Width="72" BorderThickness="0">
<StackPanel Orientation="Horizontal" Margin="6,0">
<Path Width="13" Height="13" Margin="0,2,0,0" Data="{StaticResource Icon.Apply}"/>
<Label Content="{StaticResource Text.Apply}" Margin="4,0,0,0"/>
</StackPanel>
</Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Click="OpenSearch" Margin="4,0,0,0" BorderThickness="0" ToolTip="{StaticResource Text.Dashboard.Search.Tip}">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16" Data="{StaticResource Icon.Search}"/>
<Label Content="{StaticResource Text.Dashboard.Search}"/>
</StackPanel>
</Button>
<Button Click="OpenConfigure" Margin="4,0,0,0" BorderThickness="0" ToolTip="{StaticResource Text.Dashboard.Configure.Tip}">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16" Data="{StaticResource Icon.Setting}"/>
<Label Content="{StaticResource Text.Configure}"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- Main -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left -->
<Grid x:Name="leftPanel" Grid.Column="0" FocusManager.IsFocusScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
</Grid.Resources>
<!-- Workspace -->
<TextBlock Grid.Row="0" Margin="8,0,0,0" Text="{StaticResource Text.Dashboard.Workspace}" FontWeight="DemiBold" Foreground="{StaticResource Brush.FG2}"/>
<ListView Grid.Row="1" x:Name="workspace" SelectionMode="Single" SelectionChanged="OnPageSelectionChanged">
<ListViewItem IsSelected="True">
<StackPanel Margin="16,0,0,0" Height="28" Orientation="Horizontal">
<Path Width="16" Height="16" Data="{StaticResource Icon.Histories}"/>
<TextBlock Margin="8,0,0,0" Text="{StaticResource Text.Histories}"/>
</StackPanel>
</ListViewItem>
<ListViewItem>
<Grid Margin="16,0,0,0" Height="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="16" Height="16" Data="{StaticResource Icon.WorkingCopy}"/>
<TextBlock Grid.Column="1" Margin="8,0,0,0" Text="{StaticResource Text.WorkingCopy}"/>
<controls:Badge Grid.Column="2" Margin="4,0" x:Name="badgeLocalChanges"/>
</Grid>
</ListViewItem>
<ListViewItem>
<Grid Margin="16,0,0,0" Height="28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="16" Height="16" Data="{StaticResource Icon.Stashes}"/>
<TextBlock Grid.Column="1" Margin="8,0,0,0" Text="{StaticResource Text.Stashes}"/>
<controls:Badge Grid.Column="2" Margin="4,0" x:Name="badgeStashes"/>
</Grid>
</ListViewItem>
</ListView>
<!-- Local Branches -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="8,0,0,0" Text="{StaticResource Text.Dashboard.LocalBranches}" FontWeight="DemiBold" Foreground="{StaticResource Brush.FG2}"/>
<controls:IconButton Grid.Column="1" Click="OpenGitFlowPanel" Width="14" Height="14" Margin="8,0" Icon="{StaticResource Icon.Flow}" ToolTip="{StaticResource Text.GitFlow}"/>
<controls:IconButton Grid.Column="2" Click="OpenNewBranch" Width="14" Height="14" Margin="0,0,2,0" Icon="{StaticResource Icon.Branch.Add}" ToolTip="{StaticResource Text.Dashboard.NewBranch}"/>
</Grid>
<controls:Tree
Grid.Row="3"
x:Name="localBranchTree"
FontFamily="Consolas"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
LostFocus="OnTreeLostFocus"
SelectionChanged="OnTreeSelectionChanged">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="MouseDoubleClick" Handler="OnTreeDoubleClick"/>
<EventSetter Event="ContextMenuOpening" Handler="OnTreeContextMenuOpening"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" x:Name="Icon" Width="10" Height="10" Data="{StaticResource Icon.Branch}"/>
<TextBlock Grid.Column="1" x:Name="Name" Text="{Binding Name}" Margin="4,0"/>
<controls:Badge Grid.Column="2" Margin="4,0" Label="{Binding UpstreamTrackStatus}"/>
<ToggleButton
Grid.Column="3"
x:Name="Filter"
Margin="4,0"
IsChecked="{Binding IsFiltered}"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="{StaticResource Text.Filter}"
Checked="OnFilterChanged" Unchecked="OnFilterChanged"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Name" Property="FontWeight" Value="ExtraBold"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}">
<Setter TargetName="Filter" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}"/>
<Condition Binding="{Binding IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}"/>
<Condition Binding="{Binding IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
<!-- REMOTES -->
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="8,0,0,0" Text="{StaticResource Text.Dashboard.Remotes}" FontWeight="DemiBold" Foreground="{StaticResource Brush.FG2}"/>
<controls:IconButton Grid.Column="1" Click="OpenAddRemote" Width="14" Height="14" Margin="0,0,4,0" Icon="{StaticResource Icon.Remote.Add}" ToolTip="{StaticResource Text.Dashboard.Remotes.Add}"/>
</Grid>
<controls:Tree
Grid.Row="5"
x:Name="remoteBranchTree"
FontFamily="Consolas"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
LostFocus="OnTreeLostFocus"
SelectionChanged="OnTreeSelectionChanged">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<EventSetter Event="ContextMenuOpening" Handler="OnTreeContextMenuOpening"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" x:Name="Icon" Width="10" Height="10" Data="{StaticResource Icon.Branch}"/>
<TextBlock Grid.Column="1" x:Name="Name" Text="{Binding Name}" Margin="4,0"/>
<ToggleButton
Grid.Column="2"
x:Name="Filter"
Margin="4,0"
IsChecked="{Binding IsFiltered}"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="{StaticResource Text.Filter}"
Checked="OnFilterChanged" Unchecked="OnFilterChanged"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
<Setter TargetName="Name" Property="FontWeight" Value="ExtraBold"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}">
<Setter TargetName="Filter" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Remote}">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
<Setter TargetName="Filter" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}"/>
<Condition Binding="{Binding IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="{x:Static widgets:Dashboard+BranchNodeType.Folder}"/>
<Condition Binding="{Binding IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
<!-- TAGS -->
<ToggleButton
Grid.Row="6"
x:Name="tglTags"
Style="{StaticResource Style.ToggleButton.Expender}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="8,0,0,0" Text="{StaticResource Text.Dashboard.Tags}" FontWeight="DemiBold" Foreground="{StaticResource Brush.FG2}"/>
<TextBlock Grid.Column="1" x:Name="txtTagCount" FontWeight="DemiBold" Margin="4,0,0,0" Foreground="{StaticResource Brush.FG2}"/>
<controls:IconButton Grid.Column="2" Click="OpenNewTag" Width="14" Height="14" Margin="0,0,4,0" Icon="{StaticResource Icon.Tag.Add}" ToolTip="{StaticResource Text.Dashboard.Tags.Add}"/>
</Grid>
</ToggleButton>
<DataGrid
Grid.Row="7"
x:Name="tagList"
RowHeight="24"
Height="200"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Visibility="{Binding ElementName=tglTags, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
SelectionMode="Single"
SelectionUnit="FullRow"
LostFocus="OnTagsLostFocus"
SelectionChanged="OnTagSelectionChanged"
ContextMenuOpening="OnTagContextMenuOpening">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" Height="10" Margin="16,0,8,0" Data="{StaticResource Icon.Tag}"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"/>
<ToggleButton
Grid.Column="2"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
Margin="0,0,4,0"
Style="{StaticResource Style.ToggleButton.Filter}"
ToolTip="{StaticResource Text.Filter}"
Checked="OnFilterChanged" Unchecked="OnFilterChanged"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- SUBMODULES -->
<ToggleButton
Grid.Row="8"
x:Name="tglSubmodules"
Style="{StaticResource Style.ToggleButton.Expender}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="8,0,0,0" Text="{StaticResource Text.Dashboard.Submodules}" FontWeight="DemiBold" Foreground="{StaticResource Brush.FG2}"/>
<TextBlock Grid.Column="1" x:Name="txtSubmoduleCount" FontWeight="DemiBold" Margin="4,0,0,0" Foreground="{StaticResource Brush.FG2}"/>
<controls:IconButton Grid.Column="2" Click="OpenAddSubmodule" Width="14" Height="14" Margin="8,0" Icon="{StaticResource Icon.Submodule}" ToolTip="{StaticResource Text.Dashboard.Remotes.Add}"/>
<Button Grid.Column="3" Click="UpdateSubmodules" Background="Transparent" Margin="0,0,4,0" BorderThickness="0" ToolTip="{StaticResource Text.Dashboard.Submodules.Update}">
<controls:Loading x:Name="iconUpdateSubmodule" Width="14" Height="14" />
</Button>
</Grid>
</ToggleButton>
<DataGrid
Grid.Row="9"
x:Name="submoduleList"
RowHeight="24"
Height="100"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Visibility="{Binding ElementName=tglSubmodules, Path=IsChecked, Converter={StaticResource BoolToCollapsed}}"
SelectionMode="Single"
SelectionUnit="FullRow"
ContextMenuOpening="OnSubmoduleContextMenuOpening"
MouseDoubleClick="OnSubmoduleMouseDoubleClick">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Width="10" Height="10" Margin="16,0,8,0" Data="{StaticResource Icon.Submodule}"/>
<TextBlock Grid.Column="1" Text="{Binding}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Splitter -->
<GridSplitter
Grid.Column="1"
Width="1"
HorizontalAlignment="Center" VerticalAlignment="Stretch"
Background="{StaticResource Brush.Border0}"/>
<!-- Right -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="mergeNavigator" Height="24" Background="{StaticResource Brush.Conflict}" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
x:Name="txtConflictTip"
Margin="4,0"
FontWeight="DemiBold"
Foreground="{StaticResource Brush.FG3}"/>
<Button
Grid.Column="1"
x:Name="btnResolve"
Click="GotoResolve"
Width="80"
Margin="2"
Content="{StaticResource Text.Dashboard.Resolve}"
Background="{StaticResource Brush.Window}"
BorderBrush="{StaticResource Brush.FG1}"/>
<Button
Grid.Column="2"
x:Name="btnContinue"
Click="ContinueMerge"
Width="80"
Margin="2"
Content="{StaticResource Text.Dashboard.Continue}"
Background="{StaticResource Brush.Accent1}"
BorderBrush="{StaticResource Brush.FG1}"/>
<Button
Grid.Column="3"
Click="AbortMerge"
Width="80"
Margin="2,2,4,2"
Content="{StaticResource Text.Dashboard.Abort}"
Background="{StaticResource Brush.Window}"
BorderBrush="{StaticResource Brush.FG1}"/>
</Grid>
<controls:PageContainer Grid.Row="1" x:Name="pages"/>
</Grid>
</Grid>
<!-- Popup -->
<widgets:PopupPanel x:Name="popup" Grid.Row="1"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,982 @@
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;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 仓库操作主界面
/// </summary>
public partial class Dashboard : UserControl, Controls.IPopupContainer {
private Models.Repository repo = null;
private List<BranchNode> localBranches = new List<BranchNode>();
private List<BranchNode> remoteBranches = new List<BranchNode>();
/// <summary>
/// 节点类型
/// </summary>
public enum BranchNodeType {
Remote,
Branch,
Folder,
}
/// <summary>
/// 分支节点
/// </summary>
public class BranchNode {
public string Name { get; set; } = "";
public bool IsExpanded { get; set; } = false;
public bool IsFiltered { get; set; } = false;
public BranchNodeType Type { get; set; } = BranchNodeType.Folder;
public object Data { get; set; } = null;
public List<BranchNode> Children { get; set; } = new List<BranchNode>();
public string UpstreamTrackStatus {
get { return Type == BranchNodeType.Branch ? (Data as Models.Branch).UpstreamTrackStatus : ""; }
}
public bool IsCurrent {
get { return Type == BranchNodeType.Branch ? (Data as Models.Branch).IsCurrent : false; }
}
}
public Dashboard(Models.Repository repo) {
this.repo = repo;
InitializeComponent();
InitPages();
UpdateBraches();
UpdateWorkingCopy();
UpdateStashes();
UpdateTags();
UpdateSubmodules();
var watcher = Models.Watcher.Get(repo.Path);
watcher.Navigate += NavigateTo;
watcher.BranchChanged += UpdateBraches;
watcher.WorkingCopyChanged += UpdateWorkingCopy;
watcher.StashChanged += UpdateStashes;
watcher.TagChanged += UpdateTags;
watcher.SubmoduleChanged += UpdateSubmodules;
}
#region POPUP
public void Show(Controls.PopupWidget widget) {
popup.Show(widget);
}
public void ShowAndStart(Controls.PopupWidget widget) {
popup.ShowAndStart(widget);
}
public void UpdateProgress(string message) {
popup.UpdateProgress(message);
}
#endregion
#region DATA
private void NavigateTo(string commitId) {
workspace.SelectedIndex = 0;
(pages.Get("histories") as Histories).NavigateTo(commitId);
}
private void BackupBranchExpandState(Dictionary<string, bool> states, List<BranchNode> nodes, string prefix) {
foreach (var node in nodes) {
if (node.Type != BranchNodeType.Branch) {
var id = prefix + node.Name + "/";
states[id] = node.IsExpanded;
BackupBranchExpandState(states, node.Children, id + "/");
}
}
}
private void MakeBranchNode(Models.Branch branch, List<BranchNode> roots, Dictionary<string, BranchNode> folders, Dictionary<string, bool> states, string prefix) {
var subs = branch.Name.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (subs.Length == 1) {
var node = new BranchNode() {
Name = subs[0],
IsExpanded = false,
IsFiltered = repo.Filters.Contains(branch.FullName),
Type = BranchNodeType.Branch,
Data = branch,
};
roots.Add(node);
return;
}
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 = states.ContainsKey(path) ? states[path] : false,
Type = BranchNodeType.Folder,
};
roots.Add(lastFolder);
folders.Add(path, lastFolder);
} else {
var folder = new BranchNode() {
Name = subs[i],
IsExpanded = states.ContainsKey(path) ? states[path] : false,
Type = BranchNodeType.Folder,
};
folders.Add(path, folder);
lastFolder.Children.Add(folder);
lastFolder = folder;
}
}
BranchNode last = new BranchNode() {
Name = subs.Last(),
IsExpanded = false,
IsFiltered = repo.Filters.Contains(branch.FullName),
Type = BranchNodeType.Branch,
Data = branch,
};
lastFolder.Children.Add(last);
}
private void SortBranches(List<BranchNode> nodes) {
nodes.Sort((l, r) => {
if (l.Type == r.Type) {
return l.Name.CompareTo(r.Name);
} else {
return (int)(l.Type) - (int)(r.Type);
}
});
foreach (var node in nodes) SortBranches(node.Children);
}
private void UpdateBraches() {
Task.Run(() => {
repo.Branches = new Commands.Branches(repo.Path).Result();
repo.Remotes = new Commands.Remotes(repo.Path).Result();
var states = new Dictionary<string, bool>();
BackupBranchExpandState(states, localBranches, "locals/");
BackupBranchExpandState(states, remoteBranches, "remotes/");
var folders = new Dictionary<string, BranchNode>();
localBranches = new List<BranchNode>();
remoteBranches = new List<BranchNode>();
foreach (var r in repo.Remotes) {
var fullName = $"remotes/{r.Name}";
var node = new BranchNode() {
Name = r.Name,
IsExpanded = states.ContainsKey(fullName) ? states[fullName] : false,
Type = BranchNodeType.Remote,
Data = r,
};
remoteBranches.Add(node);
folders.Add(fullName, node);
}
foreach (var b in repo.Branches) {
if (b.IsLocal) {
MakeBranchNode(b, localBranches, folders, states, "locals/");
} else {
var r = remoteBranches.Find(x => x.Name == b.Remote);
if (r != null) MakeBranchNode(b, r.Children, folders, states, "remotes/");
}
}
SortBranches(localBranches);
SortBranches(remoteBranches);
Dispatcher.Invoke(() => {
localBranchTree.ItemsSource = localBranches;
remoteBranchTree.ItemsSource = remoteBranches;
});
});
}
private void UpdateWorkingCopy() {
Task.Run(() => {
var changes = new Commands.LocalChanges(repo.Path).Result();
Dispatcher.Invoke(() => {
badgeLocalChanges.Label = $"{changes.Count}";
(pages.Get("working_copy") as WorkingCopy).SetData(changes);
UpdateMergeBar(changes);
});
});
}
private void UpdateStashes() {
Task.Run(() => {
var stashes = new Commands.Stashes(repo.Path).Result();
Dispatcher.Invoke(() => {
badgeStashes.Label = $"{stashes.Count}";
(pages.Get("stashes") as Stashes).SetData(stashes);
});
});
}
private void UpdateTags() {
Task.Run(() => {
var tags = new Commands.Tags(repo.Path).Result();
foreach (var t in tags) t.IsFiltered = repo.Filters.Contains(t.Name);
Dispatcher.Invoke(() => {
txtTagCount.Text = $"({tags.Count})";
tagList.ItemsSource = tags;
});
});
}
private void UpdateSubmodules() {
Task.Run(() => {
var submodules = new Commands.Submodules(repo.Path).Result();
Dispatcher.Invoke(() => {
txtSubmoduleCount.Text = $"({submodules.Count})";
submoduleList.ItemsSource = submodules;
});
});
}
#endregion
#region TOOLBAR_COMMANDS
private void Explore(object sender, RoutedEventArgs e) {
Process.Start("explorer", repo.Path);
e.Handled = true;
}
private void Terminal(object sender, RoutedEventArgs e) {
var bash = Path.Combine(Models.Preference.Instance.Git.Path, "..", "bash.exe");
if (!File.Exists(bash)) {
Models.Exception.Raise(App.Text("MissingBash"));
return;
}
var start = new ProcessStartInfo();
start.WorkingDirectory = repo.Path;
start.FileName = bash;
Process.Start(start);
e.Handled = true;
}
private void OpenFetch(object sender, RoutedEventArgs e) {
new Popups.Fetch(repo, null).Show();
e.Handled = true;
}
private void OpenPull(object sender, RoutedEventArgs e) {
new Popups.Pull(repo, null).Show();
e.Handled = true;
}
private void OpenPush(object sender, RoutedEventArgs e) {
new Popups.Push(repo, null).Show();
e.Handled = true;
}
private void OpenStash(object sender, RoutedEventArgs e) {
new Popups.Stash(repo.Path, null).Show();
e.Handled = true;
}
private void OpenApply(object sender, RoutedEventArgs e) {
new Popups.Apply(repo.Path).Show();
e.Handled = true;
}
private void OpenSearch(object sender, RoutedEventArgs e) {
if (popup.IsLocked) return;
popup.Close();
workspace.SelectedIndex = 0;
(pages.Get("histories") as Histories).ToggleSearch();
}
private void OpenConfigure(object sender, RoutedEventArgs e) {
new Popups.Configure(repo.Path).Show();
e.Handled = true;
}
#endregion
#region PAGES
private void InitPages() {
pages.Add("histories", new Histories(repo));
pages.Add("working_copy", new WorkingCopy(repo));
pages.Add("stashes", new Stashes(repo.Path));
pages.Goto("histories");
}
private void OnPageSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (pages == null) return;
switch (workspace.SelectedIndex) {
case 0: pages.Goto("histories"); break;
case 1: pages.Goto("working_copy"); break;
case 2: pages.Goto("stashes"); break;
}
if (mergeNavigator.Visibility == Visibility.Visible) {
btnResolve.Visibility = workspace.SelectedIndex == 1 ? Visibility.Collapsed : Visibility.Visible;
}
}
#endregion
#region BRANCHES
private void OpenGitFlowPanel(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.GitFlow.IsEnabled) {
var startFeature = new MenuItem();
startFeature.Header = App.Text("GitFlow.StartFeature");
startFeature.Click += (o, e) => {
new Popups.GitFlowStart(repo, Models.GitFlowBranchType.Feature).Show();
e.Handled = true;
};
var startRelease = new MenuItem();
startRelease.Header = App.Text("GitFlow.StartRelease");
startRelease.Click += (o, e) => {
new Popups.GitFlowStart(repo, Models.GitFlowBranchType.Release).Show();
e.Handled = true;
};
var startHotfix = new MenuItem();
startHotfix.Header = App.Text("GitFlow.StartHotfix");
startHotfix.Click += (o, e) => {
new Popups.GitFlowStart(repo, Models.GitFlowBranchType.Hotfix).Show();
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 = App.Text("GitFlow.Init");
init.Click += (o, e) => {
new Popups.InitGitFlow(repo).Show();
e.Handled = true;
};
button.ContextMenu.Items.Add(init);
}
button.ContextMenu.IsOpen = true;
ev.Handled = true;
}
private void OpenNewBranch(object sender, RoutedEventArgs e) {
new Popups.CreateBranch(repo, repo.Branches.Find(x => x.IsCurrent)).Show();
e.Handled = true;
}
private void OpenAddRemote(object sender, RoutedEventArgs e) {
new Popups.Remote(repo, null).Show();
e.Handled = true;
}
private void OnTreeLostFocus(object sender, RoutedEventArgs e) {
var tree = sender as Controls.Tree;
var child = FocusManager.GetFocusedElement(leftPanel);
if (child != null && tree.IsAncestorOf(child as DependencyObject)) return;
tree.UnselectAll();
}
private void OnTreeSelectionChanged(object sender, RoutedEventArgs e) {
var tree = sender as Controls.Tree;
if (tree.Selected.Count == 0) return;
var node = tree.Selected[0] as BranchNode;
if (node.Type == BranchNodeType.Branch) NavigateTo((node.Data as Models.Branch).Head);
}
private async void OnTreeDoubleClick(object sender, MouseButtonEventArgs e) {
var item = sender as Controls.TreeItem;
if (item == null) return;
var node = item.DataContext as BranchNode;
if (node == null || node.Type != BranchNodeType.Branch) return;
var branch = node.Data as Models.Branch;
if (!branch.IsLocal || branch.IsCurrent) return;
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Checkout(repo.Path).Branch(branch.Name));
Models.Watcher.SetEnabled(repo.Path, true);
}
private void OnTreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = sender as Controls.TreeItem;
if (item == null) return;
var node = item.DataContext as BranchNode;
if (node == null || node.Type == BranchNodeType.Folder) return;
var menu = new ContextMenu();
if (node.Type == BranchNodeType.Remote) {
FillRemoteContextMenu(menu, node.Data as Models.Remote);
} else {
var branch = node.Data as Models.Branch;
if (branch.IsLocal) {
FillLocalBranchContextMenu(menu, branch);
} else {
FillRemoteBranchContextMenu(menu, branch);
}
}
menu.IsOpen = true;
e.Handled = true;
}
private void FillLocalBranchContextMenu(ContextMenu menu, Models.Branch branch) {
var push = new MenuItem();
push.Header = App.Text("BranchCM.Push", branch.Name);
push.Click += (o, e) => {
new Popups.Push(repo, branch).Show();
e.Handled = true;
};
if (branch.IsCurrent) {
var discard = new MenuItem();
discard.Header = App.Text("BranchCM.DiscardAll");
discard.Click += (o, e) => {
new Popups.Discard(repo.Path, null).Show();
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 = App.Text("BranchCM.FastForward", upstream);
fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
fastForward.Click += (o, e) => {
new Popups.Merge(repo.Path, upstream, branch.Name).ShowAndStart();
e.Handled = true;
};
var pull = new MenuItem();
pull.Header = App.Text("BranchCM.Pull", upstream);
pull.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
pull.Click += (o, e) => {
new Popups.Pull(repo, null).Show();
e.Handled = true;
};
menu.Items.Add(fastForward);
menu.Items.Add(pull);
}
menu.Items.Add(push);
} else {
var current = repo.Branches.Find(x => x.IsCurrent);
var checkout = new MenuItem();
checkout.Header = App.Text("BranchCM.Checkout", branch.Name);
checkout.Click += async (o, e) => {
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Checkout(repo.Path).Branch(branch.Name));
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
};
menu.Items.Add(checkout);
menu.Items.Add(new Separator());
menu.Items.Add(push);
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name);
merge.Click += (o, e) => {
new Popups.Merge(repo.Path, branch.Name, current.Name).Show();
e.Handled = true;
};
var rebase = new MenuItem();
rebase.Header = App.Text("BranchCM.Rebase", current.Name, branch.Name);
rebase.Click += (o, e) => {
new Popups.Rebase(repo.Path, current.Name, branch).Show();
e.Handled = true;
};
menu.Items.Add(merge);
menu.Items.Add(rebase);
}
var type = repo.GitFlow.GetBranchType(branch.Name);
if (type != Models.GitFlowBranchType.None) {
var flowIcon = new System.Windows.Shapes.Path();
flowIcon.Data = FindResource("Icon.Flow") as Geometry;
flowIcon.Width = 10;
var finish = new MenuItem();
finish.Header = App.Text("BranchCM.Finish", branch.Name);
finish.Icon = flowIcon;
finish.Click += (o, e) => {
new Popups.GitFlowFinish(repo, branch.Name, type).Show();
e.Handled = true;
};
menu.Items.Add(new Separator());
menu.Items.Add(finish);
}
var rename = new MenuItem();
rename.Header = App.Text("BranchCM.Rename", branch.Name);
rename.Click += (o, e) => {
new Popups.RenameBranch(repo, branch.Name).Show();
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", branch.Name);
delete.IsEnabled = !branch.IsCurrent;
delete.Click += (o, e) => {
new Popups.DeleteBranch(repo.Path, branch.Name).Show();
e.Handled = true;
};
var createBranch = new MenuItem();
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (o, e) => {
new Popups.CreateBranch(repo, branch).Show();
e.Handled = true;
};
var createTag = new MenuItem();
createTag.Header = App.Text("CreateTag");
createTag.Click += (o, e) => {
new Popups.CreateTag(repo, branch).Show();
e.Handled = true;
};
menu.Items.Add(new Separator());
menu.Items.Add(rename);
menu.Items.Add(delete);
menu.Items.Add(new Separator());
menu.Items.Add(createBranch);
menu.Items.Add(createTag);
menu.Items.Add(new Separator());
var remoteBranches = repo.Branches.Where(x => !x.IsLocal).ToList();
if (remoteBranches.Count > 0) {
var trackingIcon = new System.Windows.Shapes.Path();
trackingIcon.Data = FindResource("Icon.Branch") as Geometry;
trackingIcon.VerticalAlignment = VerticalAlignment.Bottom;
trackingIcon.Width = 10;
var currentTrackingIcon = new System.Windows.Shapes.Path();
currentTrackingIcon.Data = FindResource("Icon.Check") as Geometry;
currentTrackingIcon.VerticalAlignment = VerticalAlignment.Center;
currentTrackingIcon.Width = 10;
var tracking = new MenuItem();
tracking.Header = App.Text("BranchCM.Tracking");
tracking.Icon = trackingIcon;
foreach (var b in remoteBranches) {
var upstream = b.FullName.Replace("refs/remotes/", "");
var target = new MenuItem();
target.Header = upstream;
if (branch.Upstream == b.FullName) target.Icon = currentTrackingIcon;
target.Click += (o, e) => {
new Commands.Branch(repo.Path, branch.Name).SetUpstream(upstream);
UpdateBraches();
e.Handled = true;
};
tracking.Items.Add(target);
}
menu.Items.Add(tracking);
menu.Items.Add(new Separator());
}
var copy = new MenuItem();
copy.Header = App.Text("BranchCM.CopyName");
copy.Click += (o, e) => {
Clipboard.SetText(branch.Name);
e.Handled = true;
};
menu.Items.Add(copy);
}
private void FillRemoteContextMenu(ContextMenu menu, Models.Remote remote) {
var fetch = new MenuItem();
fetch.Header = App.Text("RemoteCM.Fetch", remote.Name);
fetch.Click += (o, e) => {
new Popups.Fetch(repo, remote.Name).Show();
e.Handled = true;
};
var edit = new MenuItem();
edit.Header = App.Text("RemoteCM.Edit", remote.Name);
edit.Click += (o, e) => {
new Popups.Remote(repo, remote).Show();
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("RemoteCM.Delete", remote.Name);
delete.Click += (o, e) => {
new Popups.DeleteRemote(repo.Path, remote.Name).Show();
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("RemoteCM.CopyURL");
copy.Click += (o, e) => {
Clipboard.SetText(remote.URL);
e.Handled = true;
};
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);
}
private void FillRemoteBranchContextMenu(ContextMenu menu, Models.Branch branch) {
var current = repo.Branches.Find(x => x.IsCurrent);
var checkout = new MenuItem();
checkout.Header = App.Text("BranchCM.Checkout", branch.Name);
checkout.Click += async (o, e) => {
foreach (var b in repo.Branches) {
if (b.IsLocal && b.Upstream == branch.FullName) {
if (b.IsCurrent) return;
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Checkout(repo.Path).Branch(b.Name));
Models.Watcher.SetEnabled(repo.Path, true);
return;
}
}
new Popups.CreateBranch(repo, branch).Show();
e.Handled = true;
};
var pull = new MenuItem();
pull.Header = App.Text("BranchCM.PullInto", branch.Name, current.Name);
pull.Click += (o, e) => {
new Popups.Pull(repo, branch).Show();
e.Handled = true;
};
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name);
merge.Click += (o, e) => {
new Popups.Merge(repo.Path, $"{branch.Remote}/{branch.Name}", current.Name).Show();
e.Handled = true;
};
var rebase = new MenuItem();
rebase.Header = App.Text("BranchCM.Rebase", current.Name, branch.Name);
rebase.Click += (o, e) => {
new Popups.Rebase(repo.Path, current.Name, branch).Show();
e.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", branch.Name);
delete.Click += (o, e) => {
new Popups.DeleteBranch(repo.Path, branch.Name, branch.Remote).Show();
e.Handled = true;
};
var createBranch = new MenuItem();
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (o, e) => {
new Popups.CreateBranch(repo, branch).Show();
e.Handled = true;
};
var createTag = new MenuItem();
createTag.Header = App.Text("CreateTag");
createTag.Click += (o, e) => {
new Popups.CreateTag(repo, branch).Show();
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("BranchCM.CopyName");
copy.Click += (o, e) => {
Clipboard.SetText(branch.Remote + "/" + branch.Name);
e.Handled = true;
};
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);
}
#endregion
#region TAGS
private void OpenNewTag(object sender, RoutedEventArgs e) {
new Popups.CreateTag(repo, repo.Branches.Find(x => x.IsCurrent)).Show();
e.Handled = true;
}
private void OnTagsLostFocus(object sender, RoutedEventArgs e) {
tagList.SelectedItem = null;
}
private void OnTagSelectionChanged(object sender, SelectionChangedEventArgs e) {
var tag = tagList.SelectedItem as Models.Tag;
if (tag != null) NavigateTo(tag.SHA);
}
private void OnTagContextMenuOpening(object sender, ContextMenuEventArgs e) {
var tag = tagList.SelectedItem as Models.Tag;
if (tag == null) return;
var createBranch = new MenuItem();
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (o, ev) => {
new Popups.CreateBranch(repo, tag).Show();
ev.Handled = true;
};
var pushTag = new MenuItem();
pushTag.Header = App.Text("TagCM.Push", tag.Name);
pushTag.Click += (o, ev) => {
new Popups.PushTag(repo, tag.Name).Show();
ev.Handled = true;
};
var deleteTag = new MenuItem();
deleteTag.Header = App.Text("TagCM.Delete", tag.Name);
deleteTag.Click += (o, ev) => {
new Popups.DeleteTag(repo.Path, tag.Name).Show();
ev.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("TagCM.Copy");
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 SUBMODULES
private void OpenAddSubmodule(object sender, RoutedEventArgs e) {
new Popups.AddSubmodule(repo.Path).Show();
e.Handled = true;
}
private async void UpdateSubmodules(object sender, RoutedEventArgs e) {
iconUpdateSubmodule.IsAnimating = true;
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Submodule(repo.Path).Update());
Models.Watcher.SetEnabled(repo.Path, true);
iconUpdateSubmodule.IsAnimating = false;
e.Handled = true;
}
private void OnSubmoduleContextMenuOpening(object sender, ContextMenuEventArgs e) {
var submodule = submoduleList.SelectedItem as string;
if (submodule == null) return;
var copy = new MenuItem();
copy.Header = App.Text("Submodule.CopyPath");
copy.Click += (o, ev) => {
Clipboard.SetText(submodule);
ev.Handled = true;
};
var rm = new MenuItem();
rm.Header = App.Text("Submodule.Remove");
rm.Click += (o, ev) => {
new Popups.DeleteSubmodule(repo.Path, submodule).Show();
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(copy);
menu.Items.Add(rm);
menu.IsOpen = true;
e.Handled = true;
}
private void OnSubmoduleMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var submodule = submoduleList.SelectedItem as string;
if (submodule == null) return;
var hitted = (e.OriginalSource as FrameworkElement).DataContext as string;
if (hitted == null || hitted != submodule) return;
var sub = new Models.Repository();
sub.Path = Path.Combine(repo.Path, submodule);
sub.GitDir = new Commands.QueryGitDir(sub.Path).Result();
sub.Name = repo.Name + " : " + Path.GetFileName(submodule);
Models.Watcher.Open(sub);
e.Handled = true;
}
#endregion
#region FILTERS
private void OnFilterChanged(object sender, RoutedEventArgs e) {
var toggle = sender as ToggleButton;
if (toggle == null) return;
var filter = "";
var changed = false;
if (toggle.DataContext is BranchNode) {
var branch = (toggle.DataContext as BranchNode).Data as Models.Branch;
if (branch == null) return;
filter = branch.FullName;
} else if (toggle.DataContext is Models.Tag) {
filter = (toggle.DataContext as Models.Tag).Name;
}
if (toggle.IsChecked == true) {
if (!repo.Filters.Contains(filter)) {
repo.Filters.Add(filter);
changed = true;
}
} else {
if (repo.Filters.Contains(filter)) {
repo.Filters.Remove(filter);
changed = true;
}
}
if (changed) (pages.Get("histories") as Histories).UpdateCommits();
}
#endregion
#region MERGE_BAR
private void UpdateMergeBar(List<Models.Change> changes) {
if (File.Exists(Path.Combine(repo.GitDir, "CHERRY_PICK_HEAD"))) {
txtConflictTip.Text = App.Text("Conflict.CherryPick");
} else if (File.Exists(Path.Combine(repo.GitDir, "REBASE_HEAD"))) {
txtConflictTip.Text = App.Text("Conflict.Rebase");
} else if (File.Exists(Path.Combine(repo.GitDir, "REVERT_HEAD"))) {
txtConflictTip.Text = App.Text("Conflict.Revert");
} else if (File.Exists(Path.Combine(repo.GitDir, "MERGE_HEAD"))) {
txtConflictTip.Text = App.Text("Conflict.Merge");
} else {
mergeNavigator.Visibility = Visibility.Collapsed;
var rebaseTempFolder = Path.Combine(repo.GitDir, "rebase-apply");
if (Directory.Exists(rebaseTempFolder)) Directory.Delete(rebaseTempFolder);
return;
}
mergeNavigator.Visibility = Visibility.Visible;
btnResolve.Visibility = workspace.SelectedIndex == 1 ? Visibility.Collapsed : Visibility.Visible;
btnContinue.Visibility = changes.Find(x => x.IsConflit) == null ? Visibility.Visible : Visibility.Collapsed;
(pages.Get("working_copy") as WorkingCopy).TryLoadMergeMessage();
}
private void GotoResolve(object sender, RoutedEventArgs e) {
workspace.SelectedIndex = 1;
e.Handled = true;
}
private async void ContinueMerge(object sender, RoutedEventArgs e) {
var cherryPickMerge = Path.Combine(repo.GitDir, "CHERRY_PICK_HEAD");
var rebaseMerge = Path.Combine(repo.GitDir, "REBASE_HEAD");
var revertMerge = Path.Combine(repo.GitDir, "REVERT_HEAD");
var otherMerge = Path.Combine(repo.GitDir, "MERGE_HEAD");
var mode = "";
if (File.Exists(cherryPickMerge)) {
mode = "cherry-pick";
} else if (File.Exists(rebaseMerge)) {
mode = "rebase";
} else if (File.Exists(revertMerge)) {
mode = "revert";
} else if (File.Exists(otherMerge)) {
mode = "merge";
} else {
UpdateWorkingCopy();
return;
}
var cmd = new Commands.Command();
cmd.Cwd = repo.Path;
cmd.Args = $"-c core.editor=true {mode} --continue";
Models.Watcher.SetEnabled(repo.Path, false);
var succ = await Task.Run(() => cmd.Exec());
Models.Watcher.SetEnabled(repo.Path, true);
if (succ) {
(pages.Get("working_copy") as WorkingCopy).ClearMessage();
if (mode == "rebase") {
var rebaseTempFolder = Path.Combine(repo.GitDir, "rebase-apply");
if (Directory.Exists(rebaseTempFolder)) Directory.Delete(rebaseTempFolder);
}
}
}
private async void AbortMerge(object sender, RoutedEventArgs e) {
var cmd = new Commands.Command();
cmd.Cwd = repo.Path;
if (File.Exists(Path.Combine(repo.GitDir, "CHERRY_PICK_HEAD"))) {
cmd.Args = "cherry-pick --abort";
} else if (File.Exists(Path.Combine(repo.GitDir, "REBASE_HEAD"))) {
cmd.Args = "rebase --abort";
} else if (File.Exists(Path.Combine(repo.GitDir, "REVERT_HEAD"))) {
cmd.Args = "revert --abort";
} else if (File.Exists(Path.Combine(repo.GitDir, "MERGE_HEAD"))) {
cmd.Args = "merge --abort";
} else {
UpdateWorkingCopy();
return;
}
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => cmd.Exec());
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,139 @@
<UserControl x:Class="SourceGit.Views.Widgets.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"
xmlns:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:models="clr-namespace:SourceGit.Models"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="Style.DataGridRow.DiffViewer" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnTextDiffBringIntoView"/>
</Style>
</UserControl.Resources>
<Border BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Tool Bar -->
<Border x:Name="toolbar" Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<Grid Margin="8,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" x:Name="orgFileNamePanel" Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="txtOrgFileName" Margin="4,0,0,0" FontFamily="Consolas"/>
<TextBlock Margin="8,0" Text="→" FontFamily="Consolas"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="txtFileName" Margin="4,0" FontFamily="Consolas"/>
<controls:Loading x:Name="loading" Width="10" Height="10" Visibility="Collapsed"/>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="toolbarOptions" Orientation="Horizontal">
<controls:IconButton
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.Down}"
ToolTip="{StaticResource Text.Diff.Next}"
Click="GotoNextChange"/>
<controls:IconButton
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.Up}"
ToolTip="{StaticResource Text.Diff.Prev}"
Click="GotoPrevChange"/>
<ToggleButton
Width="14" Height="14"
Margin="4,0,0,0"
Style="{StaticResource Style.ToggleButton.SplitDirection}"
Foreground="{StaticResource Brush.FG1}"
ToolTip="{StaticResource Text.Diff.Mode}"
IsChecked="{Binding Source={x:Static models:Preference.Instance}, Path=Window.UseCombinedDiff, Mode=TwoWay}"
Checked="OnDiffViewModeChanged" Unchecked="OnDiffViewModeChanged"/>
</StackPanel>
</Grid>
</Border>
<Grid x:Name="textDiff" Grid.Row="1" SizeChanged="OnTextDiffSizeChanged">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
<Border x:Name="sizeChange" Grid.Row="1">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" TextElement.FontFamily="Consolas">
<TextBlock
x:Name="txtSizeChangeTitle"
Text="{StaticResource Text.Diff.Binary}"
Margin="0,0,0,32"
FontSize="18" FontWeight="UltraBold"
Foreground="{StaticResource Brush.FG2}"
HorizontalAlignment="Center"/>
<Path
x:Name="iconSizeChange"
Width="64" Height="64"
Data="{StaticResource Icon.Binary}"
Fill="{StaticResource Brush.FG2}"/>
<Grid Margin="0,16,0,0" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{StaticResource Text.Diff.Binary.Old}" Foreground="{StaticResource Brush.FG2}" TextElement.FontSize="18" TextElement.FontWeight="UltraBold"/>
<TextBlock Grid.Row="0" Grid.Column="2" x:Name="txtOldSize" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right" TextElement.FontSize="18" TextElement.FontWeight="UltraBold"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="{StaticResource Text.Diff.Binary.New}" Foreground="{StaticResource Brush.FG2}" TextElement.FontSize="18" TextElement.FontWeight="UltraBold"/>
<TextBlock Grid.Row="1" Grid.Column="2" x:Name="txtNewSize" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right" TextElement.FontSize="18" TextElement.FontWeight="UltraBold"/>
</Grid>
</StackPanel>
</Border>
<Border x:Name="noChange" Grid.Row="1">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Path Width="64" Height="64" Data="{StaticResource Icon.Check}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock
Margin="0,16,0,0"
Text="{StaticResource Text.Diff.NoChange}"
FontSize="18" FontWeight="UltraBold"
Foreground="{StaticResource Brush.FG2}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="mask" Grid.Row="0" Grid.RowSpan="2">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Path Width="64" Height="64" Data="{StaticResource Icon.Diff}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock
Margin="0,16,0,0"
Text="{StaticResource Text.Diff.Welcome}"
FontSize="18" FontWeight="UltraBold"
Foreground="{StaticResource Brush.FG2}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
</UserControl>

View file

@ -0,0 +1,621 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
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.Shapes;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 变更对比视图
/// </summary>
public partial class DiffViewer : UserControl {
private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
private static readonly Brush BG_NORMAL = Brushes.Transparent;
public class Option {
public string[] RevisionRange = new string[] { };
public string Path = "";
public string OrgPath = null;
public string ExtraArgs = "";
}
public class Block {
public string Content { get; set; }
public Models.TextChanges.LineMode Mode { get; set; }
public Brush BG { get; set; }
public Brush FG { get; set; }
public FontStyle Style { get; set; }
public string OldLine { get; set; }
public string NewLine { get; set; }
public bool IsContent {
get {
return Mode == Models.TextChanges.LineMode.Added
|| Mode == Models.TextChanges.LineMode.Deleted
|| Mode == Models.TextChanges.LineMode.Normal;
}
}
public bool IsDifference {
get {
return Mode == Models.TextChanges.LineMode.Added
|| Mode == Models.TextChanges.LineMode.Deleted
|| Mode == Models.TextChanges.LineMode.None;
}
}
}
private string repo = null;
private Option opt = null;
private List<Models.TextChanges.Line> cachedTextChanges = null;
private List<DataGrid> editors = new List<DataGrid>();
private List<Rectangle> splitters = new List<Rectangle>();
public DiffViewer() {
InitializeComponent();
Reset();
}
public void Reset() {
mask.Visibility = Visibility.Visible;
toolbar.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed;
ClearEditor();
ClearCache();
}
public void Reload() {
if (repo == null || opt == null) {
Reset();
return;
}
Diff(repo, opt, false);
}
public void Diff(string repo, Option opt, bool clearEditor = true) {
mask.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed;
toolbar.Visibility = Visibility.Visible;
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
SetTitle(opt.Path, opt.OrgPath);
if (clearEditor) ClearEditor();
ClearCache();
this.repo = repo;
this.opt = opt;
Task.Run(() => {
var args = $"{opt.ExtraArgs} ";
if (opt.RevisionRange.Length > 0) args += $"{opt.RevisionRange[0]} ";
if (opt.RevisionRange.Length > 1) args += $"{opt.RevisionRange[1]} ";
args += "-- ";
if (!string.IsNullOrEmpty(opt.OrgPath)) args += $"\"{opt.OrgPath}\" ";
args += $"\"{opt.Path}\"";
var isLFSObject = new Commands.IsLFSFiltered(repo, opt.Path).Result();
if (isLFSObject) {
var lc = new Commands.QueryLFSObjectChange(repo, args).Result();
if (lc.IsValid) {
SetLFSChange(lc);
} else {
SetSame();
}
return;
}
var rs = new Commands.Diff(repo, args).Result();
if (rs.IsBinary) {
var fsc = new Commands.QueryFileSizeChange(repo, opt.RevisionRange, opt.Path, opt.OrgPath).Result();
SetSizeChange(fsc);
} else if (rs.Lines.Count > 0) {
cachedTextChanges = rs.Lines;
SetTextChange();
} else {
SetSame();
}
});
}
#region LAYOUT_DATA
private void SetTitle(string file, string orgFile) {
txtFileName.Text = file;
if (!string.IsNullOrEmpty(orgFile) && orgFile != "/dev/null") {
orgFileNamePanel.Visibility = Visibility.Visible;
txtOrgFileName.Text = orgFile;
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed;
}
}
private void SetTextChange() {
if (cachedTextChanges == null) return;
if (Models.Preference.Instance.Window.UseCombinedDiff) {
MakeCombinedViewer();
} else {
MakeSideBySideViewer();
}
}
private void SetSizeChange(Models.FileSizeChange fsc) {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Visible;
txtSizeChangeTitle.Text = App.Text("Diff.Binary");
iconSizeChange.Data = FindResource("Icon.Binary") as Geometry;
txtOldSize.Text = App.Text("Bytes", fsc.OldSize);
txtNewSize.Text = App.Text("Bytes", fsc.NewSize);
});
}
private void SetLFSChange(Models.LFSChange lc) {
Dispatcher.Invoke(() => {
var oldSize = lc.Old == null ? 0 : lc.Old.Size;
var newSize = lc.New == null ? 0 : lc.New.Size;
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Visible;
txtSizeChangeTitle.Text = App.Text("Diff.LFS");
iconSizeChange.Data = FindResource("Icon.LFS") as Geometry;
txtNewSize.Text = App.Text("Bytes", newSize);
txtOldSize.Text = App.Text("Bytes", oldSize);
});
}
private void SetSame() {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Visible;
});
}
private void MakeCombinedViewer() {
var fgCommon = FindResource("Brush.FG1") as Brush;
var fgIndicator = FindResource("Brush.FG2") as Brush;
var lastOldLine = "";
var lastNewLine = "";
var blocks = new List<Block>();
foreach (var line in cachedTextChanges) {
var block = new Block();
block.Content = line.Content;
block.Mode = line.Mode;
block.BG = GetLineBackground(line);
block.FG = block.IsContent ? fgCommon : fgIndicator;
block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic;
block.OldLine = line.OldLine;
block.NewLine = line.NewLine;
if (line.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine;
blocks.Add(block);
}
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Visible;
var createEditor = editors.Count == 0;
var lineNumberWidth = CalcLineNumberColWidth(lastOldLine, lastNewLine);
var minWidth = textDiff.ActualWidth - lineNumberWidth * 2;
if (textDiff.ActualHeight < cachedTextChanges.Count * 16) minWidth -= 8;
DataGrid editor;
if (createEditor) {
editor = CreateTextEditor(new string[] { "OldLine", "NewLine" });
editor.SetValue(Grid.ColumnProperty, 0);
editor.SetValue(Grid.ColumnSpanProperty, 2);
editors.Add(editor);
textDiff.Children.Add(editor);
AddSplitter(0, Math.Floor(lineNumberWidth));
AddSplitter(0, Math.Floor(lineNumberWidth) * 2);
} else {
editor = editors[0];
splitters[0].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
splitters[1].Margin = new Thickness(Math.Floor(lineNumberWidth) * 2, 0, 0, 0);
}
editor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
editor.Columns[1].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
editor.Columns[2].MinWidth = minWidth;
editor.ItemsSource = blocks;
});
}
private void MakeSideBySideViewer() {
var fgCommon = FindResource("Brush.FG1") as Brush;
var fgIndicator = FindResource("Brush.FG2") as Brush;
var lastOldLine = "";
var lastNewLine = "";
var oldSideBlocks = new List<Block>();
var newSideBlocks = new List<Block>();
foreach (var line in cachedTextChanges) {
var block = new Block();
block.Content = line.Content;
block.Mode = line.Mode;
block.BG = GetLineBackground(line);
block.FG = block.IsContent ? fgCommon : fgIndicator;
block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic;
block.OldLine = line.OldLine;
block.NewLine = line.NewLine;
if (line.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine;
switch (line.Mode) {
case Models.TextChanges.LineMode.Added:
newSideBlocks.Add(block);
break;
case Models.TextChanges.LineMode.Deleted:
oldSideBlocks.Add(block);
break;
default:
FillEmptyLines(oldSideBlocks, newSideBlocks);
oldSideBlocks.Add(block);
newSideBlocks.Add(block);
break;
}
}
FillEmptyLines(oldSideBlocks, newSideBlocks);
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Visible;
var createEditor = editors.Count == 0;
var lineNumberWidth = CalcLineNumberColWidth(lastOldLine, lastNewLine);
var minWidth = textDiff.ActualWidth / 2 - lineNumberWidth;
if (textDiff.ActualHeight < newSideBlocks.Count * 16) minWidth -= 8;
DataGrid oldEditor, newEditor;
if (createEditor) {
oldEditor = CreateTextEditor(new string[] { "OldLine" });
oldEditor.SetValue(Grid.ColumnProperty, 0);
oldEditor.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnTextDiffSyncScroll));
newEditor = CreateTextEditor(new string[] { "NewLine" });
newEditor.SetValue(Grid.ColumnProperty, 1);
newEditor.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnTextDiffSyncScroll));
editors.Add(oldEditor);
editors.Add(newEditor);
textDiff.Children.Add(oldEditor);
textDiff.Children.Add(newEditor);
AddSplitter(0, Math.Floor(lineNumberWidth));
AddSplitter(1, 0);
AddSplitter(1, Math.Floor(lineNumberWidth));
} else {
oldEditor = editors[0];
newEditor = editors[1];
splitters[0].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
splitters[2].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
}
oldEditor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
oldEditor.Columns[1].MinWidth = minWidth;
oldEditor.ItemsSource = oldSideBlocks;
newEditor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
newEditor.Columns[1].MinWidth = minWidth;
newEditor.ItemsSource = newSideBlocks;
});
}
private Brush GetLineBackground(Models.TextChanges.Line line) {
switch (line.Mode) {
case Models.TextChanges.LineMode.Added:
return BG_ADDED;
case Models.TextChanges.LineMode.Deleted:
return BG_DELETED;
default:
return BG_NORMAL;
}
}
private void FillEmptyLines(List<Block> old, List<Block> cur) {
if (old.Count < cur.Count) {
int diff = cur.Count - old.Count;
for (int i = 0; i < diff; i++) {
var empty = new Block();
empty.Content = "";
empty.Mode = Models.TextChanges.LineMode.None;
empty.BG = BG_EMPTY;
empty.FG = Brushes.Transparent;
empty.Style = FontStyles.Normal;
empty.OldLine = "";
empty.NewLine = "";
old.Add(empty);
}
} else if (old.Count > cur.Count) {
int diff = old.Count - cur.Count;
for (int i = 0; i < diff; i++) {
var empty = new Block();
empty.Content = "";
empty.Mode = Models.TextChanges.LineMode.None;
empty.BG = BG_EMPTY;
empty.FG = Brushes.Transparent;
empty.Style = FontStyles.Normal;
empty.OldLine = "";
empty.NewLine = "";
cur.Add(empty);
}
}
}
private void AddSplitter(int column, double offset) {
var split = new Rectangle();
split.Width = 1;
split.Fill = FindResource("Brush.Border2") as Brush;
split.HorizontalAlignment = HorizontalAlignment.Left;
split.Margin = new Thickness(offset, 0, 0, 0);
split.SetValue(Grid.ColumnProperty, column);
textDiff.Children.Add(split);
splitters.Add(split);
}
private DataGrid CreateTextEditor(string[] lineNumbers) {
var grid = new DataGrid();
grid.RowHeight = 16.0;
grid.FrozenColumnCount = lineNumbers.Length;
grid.ContextMenuOpening += OnTextDiffContextMenuOpening;
grid.RowStyle = FindResource("Style.DataGridRow.DiffViewer") as Style;
grid.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, (o, e) => {
var items = (o as DataGrid).SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var block = item as Block;
if (block == null) continue;
if (!block.IsContent) continue;
builder.Append(block.Content);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
}));
foreach (var number in lineNumbers) {
var colLineNumber = new DataGridTextColumn();
colLineNumber.IsReadOnly = true;
colLineNumber.Binding = new Binding(number);
colLineNumber.ElementStyle = FindResource("Style.TextBlock.LineNumber") as Style;
grid.Columns.Add(colLineNumber);
}
var borderContent = new FrameworkElementFactory(typeof(Border));
borderContent.SetBinding(Border.BackgroundProperty, new Binding("BG"));
var textContent = new FrameworkElementFactory(typeof(TextBlock));
textContent.SetBinding(TextBlock.TextProperty, new Binding("Content"));
textContent.SetBinding(TextBlock.ForegroundProperty, new Binding("FG"));
textContent.SetBinding(TextBlock.FontStyleProperty, new Binding("Style"));
textContent.SetValue(TextBlock.BackgroundProperty, Brushes.Transparent);
textContent.SetValue(TextBlock.FontSizeProperty, 12.0);
textContent.SetValue(TextBlock.MarginProperty, new Thickness(0));
textContent.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0));
var visualTree = new FrameworkElementFactory(typeof(Grid));
visualTree.AppendChild(borderContent);
visualTree.AppendChild(textContent);
var colContent = new DataGridTemplateColumn();
colContent.CellTemplate = new DataTemplate();
colContent.CellTemplate.VisualTree = visualTree;
colContent.Width = DataGridLength.SizeToCells;
grid.Columns.Add(colContent);
return grid;
}
private double CalcLineNumberColWidth(string oldLine, string newLine) {
var number = oldLine;
if (newLine.Length > oldLine.Length) number = newLine;
var formatted = new FormattedText(
number,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
return formatted.Width + 16;
}
private void ClearCache() {
repo = null;
opt = null;
cachedTextChanges = null;
}
private void ClearEditor() {
editors.Clear();
splitters.Clear();
textDiff.Children.Clear();
}
private T GetVisualChild<T>(DependencyObject parent) where T : Visual {
T child = null;
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++) {
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null) {
child = GetVisualChild<T>(v);
}
if (child != null) {
break;
}
}
return child;
}
#endregion
#region EVENTS
private void OnDiffViewModeChanged(object sender, RoutedEventArgs e) {
if (editors.Count > 0) {
ClearEditor();
SetTextChange();
}
}
private void OnTextDiffSizeChanged(object sender, SizeChangedEventArgs e) {
if (editors.Count == 0) return;
var total = textDiff.ActualWidth / editors.Count;
for (int i = 0; i < editors.Count; i++) {
var editor = editors[i];
var minWidth = total - editor.NonFrozenColumnsViewportHorizontalOffset;
if (editor.Items.Count * 16 > textDiff.ActualHeight) minWidth -= 8;
var lastColumn = editor.Columns.Count - 1;
editor.Columns[lastColumn].MinWidth = minWidth;
editor.Columns[lastColumn].Width = DataGridLength.SizeToCells;
editor.UpdateLayout();
}
}
private void OnTextDiffContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copyIcon = new Path();
copyIcon.Data = FindResource("Icon.Copy") as Geometry;
copyIcon.Width = 10;
var copy = new MenuItem();
copy.Header = App.Text("Diff.Copy");
copy.Icon = copyIcon;
copy.Click += (o, ev) => {
var items = grid.SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var block = item as Block;
if (block == null) continue;
if (!block.IsContent) continue;
builder.Append(block.Content);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void OnTextDiffSyncScroll(object sender, ScrollChangedEventArgs e) {
foreach (var editor in editors) {
var scroller = GetVisualChild<ScrollViewer>(editor);
if (scroller == null) continue;
if (e.VerticalChange != 0 && scroller.VerticalOffset != e.VerticalOffset) {
scroller.ScrollToVerticalOffset(e.VerticalOffset);
}
if (e.HorizontalChange != 0 && scroller.HorizontalOffset != e.HorizontalOffset) {
scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
private void OnTextDiffBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void GotoPrevChange(object sender, RoutedEventArgs e) {
if (editors.Count == 0) return;
var grid = editors[0];
var scroller = GetVisualChild<ScrollViewer>(grid);
if (scroller == null) return;
var firstVisible = (int)scroller.VerticalOffset;
var firstModeEnded = false;
var first = grid.Items[firstVisible] as Block;
for (int i = firstVisible - 1; i >= 0; i--) {
var next = grid.Items[i] as Block;
if (next.IsDifference) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break;
}
} else {
firstModeEnded = true;
}
}
}
private void GotoNextChange(object sender, RoutedEventArgs e) {
if (editors.Count == 0) return;
var grid = editors[0];
var scroller = GetVisualChild<ScrollViewer>(grid);
if (scroller == null) return;
var firstVisible = (int)scroller.VerticalOffset;
var firstModeEnded = false;
var first = grid.Items[firstVisible] as Block;
for (int i = firstVisible + 1; i < grid.Items.Count; i++) {
var next = grid.Items[i] as Block;
if (next.IsDifference) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break;
}
} else {
firstModeEnded = true;
}
}
}
#endregion
}
}

View file

@ -0,0 +1,45 @@
<UserControl x:Class="SourceGit.Views.Widgets.Exceptions"
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"
mc:Ignorable="d"
Width="Auto" Height="Auto">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ElementName=me, Path=Messages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Height="Auto" Width="300" Margin="8">
<Border Background="{StaticResource Brush.Window}" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="1">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" Opacity=".5"/>
</Border.Effect>
</Border>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{StaticResource Text.Launcher.Error}" FontWeight="Bold"/>
<TextBlock Grid.Row="1" Margin="0,8" Text="{Binding}" TextWrapping="Wrap"/>
<Button
Grid.Row="2"
Width="60" Height="25"
Margin="4,0"
Click="Dismiss"
Content="{StaticResource Text.Close}"
Background="{StaticResource Brush.Accent1}"
BorderBrush="{StaticResource Brush.FG1}"
BorderThickness="1"
HorizontalAlignment="Right"/>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>

View file

@ -0,0 +1,24 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 错误提示面板
/// </summary>
public partial class Exceptions : UserControl {
public ObservableCollection<string> Messages { get; set; }
public Exceptions() {
Messages = new ObservableCollection<string>();
Models.Exception.Handler = e => Dispatcher.Invoke(() => Messages.Add(e));
InitializeComponent();
}
private void Dismiss(object sender, RoutedEventArgs e) {
var data = (sender as Button).DataContext as string;
Messages.Remove(data);
}
}
}

View file

@ -0,0 +1,212 @@
<UserControl x:Class="SourceGit.Views.Widgets.Histories"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="layout">
<Grid.Resources>
<converters:BoolToCollapsed x:Key="BoolToCollapsed"/>
</Grid.Resources>
<Border x:Name="commitListPanel" Background="{StaticResource Brush.Contents}">
<Grid ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- SearchBar -->
<Grid Grid.Row="0" x:Name="searchBar" Margin="0,-32,0,0">
<controls:TextEdit
x:Name="txtSearch"
Margin="4" Padding="0,0,22,0"
Height="24"
Placeholder="{StaticResource Text.Histories.Search}"
PreviewKeyDown="OnSearchPreviewKeyDown"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="8,0">
<controls:IconButton
Click="ClearSearch"
Width="14" Height="14"
Icon="{StaticResource Icon.Clear}"
Foreground="{StaticResource Brush.FG2}"
ToolTip="{StaticResource Text.Histories.SearchClear}"/>
<controls:IconButton
Click="HideSearch"
Width="14" Height="14" Margin="6,0,0,0"
Icon="{StaticResource Icon.Up}"
Foreground="{StaticResource Brush.FG2}"
ToolTip="{StaticResource Text.Close}"/>
</StackPanel>
</Grid>
<!-- Commit DataGrid -->
<DataGrid
Grid.Row="1"
x:Name="commitList"
RowHeight="24"
SelectionUnit="FullRow"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.ScrollChanged="OnCommitListScrolled"
SelectionChanged="OnCommitSelectionChanged"
KeyUp="OnCommitListKeyUp">
<DataGrid.Columns>
<DataGridTemplateColumn x:Name="graphColumn" Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="{Binding Margin}">
<ItemsControl ItemsSource="{Binding Decorators}" Visibility="{Binding HasDecorators, Converter={StaticResource BoolToCollapsed}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type models:Decorator}">
<StackPanel Orientation="Horizontal" Height="16" Margin="2,0">
<Border Background="{StaticResource Brush.Decorator}">
<Path x:Name="Icon" Margin="4,0" Width="8" Height="8" Data="{StaticResource Icon.Branch}"/>
</Border>
<Border x:Name="Color" Background="#FFFFB835">
<TextBlock Text="{Binding Name}" FontSize="11" Margin="4,0" Foreground="Black"/>
</Border>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.CurrentBranchHead}">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Check}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.RemoteBranchHead}">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Remote}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Static models:DecoratorType.Tag}">
<Setter TargetName="Color" Property="Background" Value="#FF02C302"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Tag}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding Subject}" Margin="2,0,0,0"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="32" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:Avatar
Width="16" Height="16"
HorizontalAlignment="Center" VerticalAlignment="Center"
Email="{Binding Committer.Email}"
FallbackLabel="{Binding Committer.Name}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Committer.Name}" Margin="0,0,8,0"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="64" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding ShortSHA}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="128" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Committer.Time}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="OnCommitContextMenuOpening"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMerged}" Value="False">
<Setter Property="Opacity" Value=".5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
<!-- Commit Graph -->
<controls:CommitGraph Grid.Row="1" x:Name="graph" Width="{Binding ElementName=graphColumn, Path=ActualWidth}" HorizontalAlignment="Left"/>
<!-- Loading Tip -->
<controls:Loading Grid.Row="1" Width="48" Height="48" x:Name="loading" Visibility="Collapsed" Opacity=".4"/>
</Grid>
</Border>
<GridSplitter x:Name="splitter" Background="{StaticResource Brush.Border0}"/>
<Grid x:Name="inspector">
<!-- Commit Detail -->
<widgets:CommitDetail x:Name="commitDetail"/>
<!-- Differents Between Two Revisions -->
<widgets:RevisionCompare x:Name="revisionCompare"/>
<!-- Mask -->
<Border x:Name="mask" Background="{StaticResource Brush.Window}">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".25">
<Path Width="128" Height="128" Data="{StaticResource Icon.Detail}"/>
<TextBlock x:Name="txtCounter" Visibility="Hidden" FontFamily="Consolas" Margin="0,16,0,0" FontSize="24" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Right Top Button -->
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,6,0,0" Orientation="Horizontal">
<ToggleButton
Style="{StaticResource Style.ToggleButton.SplitDirection}"
Foreground="{StaticResource Brush.FG2}"
Width="14" Height="14"
ToolTip="{StaticResource Text.Histories.DisplayMode}"
IsChecked="{Binding Source={x:Static models:Preference.Instance}, Path=Window.MoveCommitInfoRight, Mode=TwoWay}"
Checked="ChangeOrientation" Unchecked="ChangeOrientation"/>
<controls:IconButton
Margin="8,0"
Width="16" Height="16"
ToolTip="{StaticResource Text.Histories.Guide}"
Icon="{StaticResource Icon.Help}"
Opacity=".4"
Click="OpenGuide"/>
<Popup x:Name="popupGuide" IsOpen="False" StaysOpen="False" Placement="Bottom">
<Border Background="{StaticResource Brush.Popup}" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<StackPanel Orientation="Vertical" TextElement.FontFamily="Consolas" Margin="8">
<Label Content="{StaticResource Text.Histories.Guide}" FontWeight="Bold" FontSize="14" Margin="0,0,0,8"/>
<Label Content="{StaticResource Text.Histories.Guide_1}"/>
<Label Content="{StaticResource Text.Histories.Guide_2}"/>
<Label Content="{StaticResource Text.Histories.Guide_3}"/>
<Label Content="{StaticResource Text.Histories.Guide_4}"/>
</StackPanel>
</Border>
</Popup>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,599 @@
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.Shapes;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 历史记录
/// </summary>
public partial class Histories : UserControl {
private Models.Repository repo = null;
private List<Models.Commit> cachedCommits = new List<Models.Commit>();
private bool searching = false;
public Histories(Models.Repository repo) {
this.repo = repo;
InitializeComponent();
ChangeOrientation(null, null);
UpdateCommits();
var watcher = Models.Watcher.Get(repo.Path);
watcher.BranchChanged += UpdateCommits;
}
#region DATA
public void NavigateTo(string commit) {
if (string.IsNullOrEmpty(commit)) return;
foreach (var item in commitList.ItemsSource) {
var c = item as Models.Commit;
if (c.SHA.Contains(commit)) {
commitList.SelectedItem = c;
commitList.ScrollIntoView(c);
break;
}
}
}
public void UpdateCommits() {
Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
});
Task.Run(() => {
var limits = "-20000 ";
if (repo.Filters.Count > 0) {
limits += string.Join(" ", repo.Filters);
} else {
limits += "--branches --remotes --tags";
}
cachedCommits = new Commands.Commits(repo.Path, limits).Result();
UpdateVisibleCommits();
});
}
private void UpdateVisibleCommits(string filter = null) {
var visible = new List<Models.Commit>();
searching = false;
if (string.IsNullOrEmpty(filter)) {
visible = cachedCommits;
} else {
searching = true;
foreach (var c in cachedCommits) {
#if NET48
if (c.SHA.Contains(filter)
|| c.Subject.Contains(filter)
|| c.Message.Contains(filter)
|| c.Author.Name.Contains(filter)
|| c.Committer.Name.Contains(filter)) {
visible.Add(c);
}
#else
if (c.SHA.Contains(filter, StringComparison.Ordinal)
|| c.Subject.Contains(filter, StringComparison.Ordinal)
|| c.Message.Contains(filter, StringComparison.Ordinal)
|| c.Author.Name.Contains(filter, StringComparison.Ordinal)
|| c.Committer.Name.Contains(filter, StringComparison.Ordinal)) {
visible.Add(c);
}
#endif
}
}
Dispatcher.Invoke(() => {
loading.IsAnimating = false;
loading.Visibility = Visibility.Collapsed;
graph.SetData(visible, searching);
commitList.ItemsSource = visible;
});
}
#endregion
#region LAYOUT
private void ChangeOrientation(object sender, RoutedEventArgs e) {
if (layout == null || commitListPanel == null || inspector == null || splitter == null) return;
layout.RowDefinitions.Clear();
layout.ColumnDefinitions.Clear();
if (Models.Preference.Instance.Window.MoveCommitInfoRight) {
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star), MinWidth = 200 });
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1) });
layout.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star), MinWidth = 200 });
splitter.HorizontalAlignment = HorizontalAlignment.Center;
splitter.VerticalAlignment = VerticalAlignment.Stretch;
splitter.Width = 1;
splitter.Height = double.NaN;
Grid.SetRow(commitListPanel, 0);
Grid.SetRow(splitter, 0);
Grid.SetRow(inspector, 0);
Grid.SetColumn(commitListPanel, 0);
Grid.SetColumn(splitter, 1);
Grid.SetColumn(inspector, 2);
} else {
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star), MinHeight = 100 });
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1) });
layout.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star), MinHeight = 100 });
splitter.HorizontalAlignment = HorizontalAlignment.Stretch;
splitter.VerticalAlignment = VerticalAlignment.Center;
splitter.Width = double.NaN;
splitter.Height = 1;
Grid.SetRow(commitListPanel, 0);
Grid.SetRow(splitter, 1);
Grid.SetRow(inspector, 2);
Grid.SetColumn(commitListPanel, 0);
Grid.SetColumn(splitter, 0);
Grid.SetColumn(inspector, 0);
}
layout.InvalidateArrange();
}
#endregion
#region SEARCH_BAR
public void ToggleSearch() {
if (searchBar.Margin.Top == 0) {
if (searchBar.Margin.Top != 0) return;
if (searching) {
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
txtSearch.Text = "";
Task.Run(() => UpdateVisibleCommits());
}
ThicknessAnimation anim = new ThicknessAnimation();
anim.From = new Thickness(0);
anim.To = new Thickness(0, -32, 0, 0);
anim.Duration = TimeSpan.FromSeconds(.3);
searchBar.BeginAnimation(MarginProperty, anim);
} else {
ThicknessAnimation anim = new ThicknessAnimation();
anim.From = new Thickness(0, -32, 0, 0);
anim.To = new Thickness(0);
anim.Duration = TimeSpan.FromSeconds(.3);
searchBar.BeginAnimation(MarginProperty, anim);
txtSearch.Focus();
}
}
private void ClearSearch(object sender, RoutedEventArgs e) {
txtSearch.Text = "";
}
private void HideSearch(object sender, RoutedEventArgs e) {
if (searching) {
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
txtSearch.Text = "";
Task.Run(() => UpdateVisibleCommits());
}
ThicknessAnimation anim = new ThicknessAnimation();
anim.From = new Thickness(0);
anim.To = new Thickness(0, -32, 0, 0);
anim.Duration = TimeSpan.FromSeconds(.3);
searchBar.BeginAnimation(MarginProperty, anim);
}
private void OnSearchPreviewKeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Enter) {
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
var filter = txtSearch.Text;
Task.Run(() => UpdateVisibleCommits(filter));
} else if (e.Key == Key.Escape) {
ToggleSearch();
}
}
#endregion
#region COMMIT_LIST
private void OnCommitListScrolled(object sender, ScrollChangedEventArgs e) {
graph.SetOffset(e.VerticalOffset * commitList.RowHeight);
}
private void OnCommitListKeyUp(object sender, KeyEventArgs e) {
if (e.Key == Key.Up || e.Key == Key.Down) {
OnCommitSelectionChanged(sender, null);
e.Handled = true;
}
}
private void OnCommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Keyboard.IsKeyDown(Key.Up) || Keyboard.IsKeyDown(Key.Down)) return;
mask.Visibility = Visibility.Collapsed;
commitDetail.Visibility = Visibility.Collapsed;
revisionCompare.Visibility = Visibility.Collapsed;
var selected = commitList.SelectedItems;
if (selected.Count == 1) {
commitDetail.SetData(repo.Path, selected[0] as Models.Commit);
commitDetail.Visibility = Visibility.Visible;
} else if (selected.Count == 2) {
revisionCompare.SetData(repo.Path, selected[0] as Models.Commit, selected[1] as Models.Commit);
revisionCompare.Visibility = Visibility.Visible;
} else if (selected.Count > 2) {
mask.Visibility = Visibility.Visible;
txtCounter.Visibility = Visibility.Visible;
txtCounter.Text = App.Text("Histories.Selected", selected.Count);
} else {
mask.Visibility = Visibility.Visible;
txtCounter.Visibility = Visibility.Hidden;
}
}
private void OnCommitContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var row = sender as DataGridRow;
if (row == null) return;
var commit = row.DataContext as Models.Commit;
if (commit == null) return;
commitList.SelectedItem = commit;
var current = repo.Branches.Find(x => x.IsCurrent);
var merged = commit.IsMerged;
var menu = new ContextMenu();
var tags = new List<string>();
// Decorators
if (commit.HasDecorators) {
foreach (var d in commit.Decorators) {
if (d.Type == Models.DecoratorType.CurrentBranchHead) {
FillCurrentBranchMenu(menu, current);
} else if (d.Type == Models.DecoratorType.LocalBranchHead) {
FillOtherLocalBranchMenu(menu, repo.Branches.Find(x => x.IsLocal && x.Name == d.Name), current, merged);
} else if (d.Type == Models.DecoratorType.RemoteBranchHead) {
FillRemoteBranchMenu(menu, repo.Branches.Find(x => !x.IsLocal && d.Name == $"{x.Remote}/{x.Name}"), current, merged);
} else if (d.Type == Models.DecoratorType.Tag) {
tags.Add(d.Name);
}
}
if (menu.Items.Count > 0) menu.Items.Add(new Separator());
}
// Tags
if (tags.Count > 0) {
foreach (var tag in tags) FillTagMenu(menu, tag);
menu.Items.Add(new Separator());
}
if (current.Head != commit.SHA) {
var reset = new MenuItem();
reset.Header = App.Text("CommitCM.Reset", current.Name);
reset.Click += (o, e) => {
new Popups.Reset(repo.Path, current.Name, commit).Show();
e.Handled = true;
};
menu.Items.Add(reset);
if (!merged) {
var rebase = new MenuItem();
rebase.Header = App.Text("CommitCM.Rebase", current.Name);
rebase.Click += (o, e) => {
new Popups.Rebase(repo.Path, current.Name, commit).Show();
e.Handled = true;
};
menu.Items.Add(rebase);
var cherryPick = new MenuItem();
cherryPick.Header = App.Text("CommitCM.CherryPick");
cherryPick.Click += (o, e) => {
new Popups.CherryPick(repo.Path, commit).Show();
e.Handled = true;
};
menu.Items.Add(cherryPick);
} else {
var revert = new MenuItem();
revert.Header = App.Text("CommitCM.Revert");
revert.Click += (o, e) => {
new Popups.Revert(repo.Path, commit).Show();
e.Handled = true;
};
menu.Items.Add(revert);
}
menu.Items.Add(new Separator());
}
var createBranch = new MenuItem();
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (o, e) => {
new Popups.CreateBranch(repo, commit).Show();
e.Handled = true;
};
menu.Items.Add(createBranch);
var createTag = new MenuItem();
createTag.Header = App.Text("CreateTag");
createTag.Click += (o, e) => {
new Popups.CreateTag(repo, commit).Show();
e.Handled = true;
};
menu.Items.Add(createTag);
menu.Items.Add(new Separator());
var saveToPatch = new MenuItem();
saveToPatch.Header = App.Text("CommitCM.SaveAsPatch");
saveToPatch.Click += (o, e) => {
FolderBrowser.Open(null, "Save patch to ...", saveTo => {
new Commands.FormatPatch(repo.Path, commit.SHA, saveTo).Exec();
});
};
menu.Items.Add(saveToPatch);
menu.Items.Add(new Separator());
var copySHA = new MenuItem();
copySHA.Header = App.Text("CommitCM.CopySHA");
copySHA.Click += (o, e) => {
Clipboard.SetText(commit.SHA);
e.Handled = true;
};
menu.Items.Add(copySHA);
var copyInfo = new MenuItem();
copyInfo.Header = App.Text("CommitCM.CopyInfo");
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;
}
private void FillCurrentBranchMenu(ContextMenu menu, Models.Branch current) {
var icon = new Path();
icon.Data = FindResource("Icon.Branch") as Geometry;
icon.VerticalAlignment = VerticalAlignment.Bottom;
icon.Width = 10;
icon.Height = 10;
var dirty = !string.IsNullOrEmpty(current.UpstreamTrackStatus);
var submenu = new MenuItem();
submenu.Header = current.Name;
submenu.Icon = icon;
if (!string.IsNullOrEmpty(current.Upstream)) {
var upstream = current.Upstream.Substring(13);
var fastForward = new MenuItem();
fastForward.Header = App.Text("BranchCM.FastForward", upstream);
fastForward.IsEnabled = dirty;
fastForward.Click += (o, e) => {
new Popups.Merge(repo.Path, upstream, current.Name).ShowAndStart();
e.Handled = true;
};
submenu.Items.Add(fastForward);
var pull = new MenuItem();
pull.Header = App.Text("BranchCM.Pull", upstream);
pull.IsEnabled = dirty;
pull.Click += (o, e) => {
new Popups.Pull(repo, null).Show();
e.Handled = true;
};
submenu.Items.Add(pull);
}
var push = new MenuItem();
push.Header = App.Text("BranchCM.Push", current.Name);
push.IsEnabled = dirty;
push.Click += (o, e) => {
new Popups.Push(repo, current).Show();
e.Handled = true;
};
submenu.Items.Add(push);
submenu.Items.Add(new Separator());
var type = repo.GitFlow.GetBranchType(current.Name);
if (type != Models.GitFlowBranchType.None) {
var flowIcon = new Path();
flowIcon.Data = FindResource("Icon.Flow") as Geometry;
flowIcon.Width = 10;
flowIcon.Height = 10;
var finish = new MenuItem();
finish.Header = App.Text("BranchCM.Finish", current.Name);
finish.Icon = flowIcon;
finish.Click += (o, e) => {
new Popups.GitFlowFinish(repo, current.Name, type).Show();
e.Handled = true;
};
submenu.Items.Add(finish);
submenu.Items.Add(new Separator());
}
var rename = new MenuItem();
rename.Header = App.Text("BranchCM.Rename", current.Name);
rename.Click += (o, e) => {
new Popups.RenameBranch(repo, current.Name).Show();
e.Handled = true;
};
submenu.Items.Add(rename);
menu.Items.Add(submenu);
}
private void FillOtherLocalBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
var icon = new Path();
icon.Data = FindResource("Icon.Branch") as Geometry;
icon.VerticalAlignment = VerticalAlignment.Bottom;
icon.Width = 10;
icon.Height = 10;
var submenu = new MenuItem();
submenu.Header = branch.Name;
submenu.Icon = icon;
var checkout = new MenuItem();
checkout.Header = App.Text("BranchCM.Checkout", branch.Name);
checkout.Click += async (o, e) => {
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Checkout(repo.Path).Branch(branch.Name));
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
};
submenu.Items.Add(checkout);
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name);
merge.IsEnabled = !merged;
merge.Click += (o, e) => {
new Popups.Merge(repo.Path, branch.Name, current.Name).Show();
e.Handled = true;
};
submenu.Items.Add(merge);
submenu.Items.Add(new Separator());
var type = repo.GitFlow.GetBranchType(branch.Name);
if (type != Models.GitFlowBranchType.None) {
var flowIcon = new Path();
flowIcon.Data = FindResource("Icon.Flow") as Geometry;
flowIcon.Width = 10;
flowIcon.Height = 10;
var finish = new MenuItem();
finish.Header = App.Text("BranchCM.Finish", branch.Name);
finish.Icon = flowIcon;
finish.Click += (o, e) => {
new Popups.GitFlowFinish(repo, branch.Name, type).Show();
e.Handled = true;
};
submenu.Items.Add(finish);
submenu.Items.Add(new Separator());
}
var rename = new MenuItem();
rename.Header = App.Text("BranchCM.Rename", branch.Name);
rename.Click += (o, e) => {
new Popups.RenameBranch(repo, branch.Name).Show();
e.Handled = true;
};
submenu.Items.Add(rename);
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", branch.Name);
delete.Click += (o, e) => {
new Popups.DeleteBranch(repo.Path, branch.Name).Show();
e.Handled = true;
};
submenu.Items.Add(delete);
menu.Items.Add(submenu);
}
private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
var name = $"{branch.Remote}/{branch.Name}";
var icon = new Path();
icon.Data = FindResource("Icon.Branch") as Geometry;
icon.VerticalAlignment = VerticalAlignment.Bottom;
icon.Width = 10;
icon.Height = 10;
var submenu = new MenuItem();
submenu.Header = name;
submenu.Icon = icon;
var checkout = new MenuItem();
checkout.Header = App.Text("BranchCM.Checkout", name);
checkout.Click += async (o, e) => {
foreach (var b in repo.Branches) {
if (b.IsLocal && b.Upstream == branch.FullName) {
if (b.IsCurrent) return;
Models.Watcher.SetEnabled(repo.Path, false);
await Task.Run(() => new Commands.Checkout(repo.Path).Branch(b.Name));
Models.Watcher.SetEnabled(repo.Path, true);
return;
}
}
new Popups.CreateBranch(repo, branch).Show();
e.Handled = true;
};
submenu.Items.Add(checkout);
var merge = new MenuItem();
merge.Header = App.Text("BranchCM.Merge", name, current.Name);
merge.IsEnabled = !merged;
merge.Click += (o, e) => {
new Popups.Merge(repo.Path, name, current.Name).Show();
e.Handled = true;
};
submenu.Items.Add(merge);
submenu.Items.Add(new Separator());
var delete = new MenuItem();
delete.Header = App.Text("BranchCM.Delete", name);
delete.Click += (o, e) => {
new Popups.DeleteBranch(repo.Path, branch.Name, branch.Remote).Show();
e.Handled = true;
};
submenu.Items.Add(delete);
menu.Items.Add(submenu);
}
private void FillTagMenu(ContextMenu menu, string tag) {
var icon = new Path();
icon.Data = FindResource("Icon.Tag") as Geometry;
icon.Width = 10;
icon.Height = 10;
var submenu = new MenuItem();
submenu.Header = tag;
submenu.Icon = icon;
submenu.MinWidth = 200;
var push = new MenuItem();
push.Header = App.Text("TagCM.Push", tag);
push.Click += (o, e) => {
new Popups.PushTag(repo, tag).Show();
e.Handled = true;
};
submenu.Items.Add(push);
var delete = new MenuItem();
delete.Header = App.Text("TagCM.Delete", tag);
delete.Click += (o, e) => {
new Popups.DeleteTag(repo.Path, tag).Show();
e.Handled = true;
};
submenu.Items.Add(delete);
menu.Items.Add(submenu);
}
#endregion
#region GUIDE
private void OpenGuide(object sender, RoutedEventArgs e) {
popupGuide.IsOpen = !popupGuide.IsOpen;
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,116 @@
<UserControl x:Class="SourceGit.Views.Widgets.PageTabBar"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="28" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Left Scroller -->
<controls:IconButton
Grid.Column="0"
x:Name="leftScroller"
Click="ScrollLeft"
Width="18" Padding="5"
HoverBackground="{StaticResource Brush.Accent1}"
BorderBrush="{StaticResource Brush.Border0}"
BorderThickness="0,0,1,0"
Icon="{StaticResource Icon.ScrollLeft}"
WindowChrome.IsHitTestVisibleInChrome="True"
Visibility="Collapsed"/>
<!-- Tabs -->
<ScrollViewer
Grid.Column="1"
x:Name="scroller"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Horizontal" SizeChanged="CalcScrollerVisibilty">
<ListBox
x:Name="container"
ItemsSource="{Binding ElementName=me, Path=Tabs}"
WindowChrome.IsHitTestVisibleInChrome="True"
SelectionChanged="SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="AllowDrop" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="Border" Background="Transparent" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,0,1,0">
<StackPanel Margin="8,0" x:Name="Contents" Orientation="Horizontal" Opacity=".5">
<ContentPresenter VerticalAlignment="Center" Content="{Binding Control}"/>
<controls:IconButton
Click="CloseTab"
Width="16" Height="16"
Margin="4,0,0,0" Padding="4"
ToolTip="{StaticResource Text.Close}"
Icon="{StaticResource Icon.Close}"
HoverBackground="{StaticResource Brush.NewPageHover}"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource Brush.Window}"/>
<Setter TargetName="Contents" Property="Opacity" Value="1"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Contents" Property="Opacity" Value=".85"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<EventSetter Event="MouseMove" Handler="OnMouseMove"/>
<EventSetter Event="Drop" Handler="OnDrop"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<controls:IconButton
Width="20" Height="20"
Margin="4,0" Padding="4"
Icon="{StaticResource Icon.NewTab}"
HoverBackground="{StaticResource Brush.NewPageHover}"
ToolTip="{StaticResource Text.PageSwitcher.New}"
Click="NewTab"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</StackPanel>
</ScrollViewer>
<!-- Right Scroller -->
<controls:IconButton
Grid.Column="2"
x:Name="rightScroller"
Click="ScrollRight"
Width="18" Padding="5"
HoverBackground="{StaticResource Brush.Accent1}"
BorderBrush="{StaticResource Brush.Border0}"
BorderThickness="1,0"
Icon="{StaticResource Icon.ScrollRight}"
WindowChrome.IsHitTestVisibleInChrome="True"
Visibility="Collapsed"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,197 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 主窗体标题栏的标签页容器控件
/// </summary>
public partial class PageTabBar : UserControl {
/// <summary>
/// 标签数据
/// </summary>
public class Tab {
public string Id { get; set; }
public UserControl Control { get; set; }
public Tab(string id, UserControl ctrl) { Id = id; Control = ctrl; }
}
/// <summary>
/// 标签相关事件参数
/// </summary>
public class TabEventArgs : RoutedEventArgs {
public string TabId { get; set; }
public TabEventArgs(RoutedEvent e, object o, string id) : base(e, o) { TabId = id; }
}
public static readonly RoutedEvent TabAddEvent = EventManager.RegisterRoutedEvent(
"TabAdd",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(PageTabBar));
public event RoutedEventHandler TabAdd {
add { AddHandler(TabAddEvent, value); }
remove { RemoveHandler(TabAddEvent, value); }
}
public static readonly RoutedEvent TabSelectedEvent = EventManager.RegisterRoutedEvent(
"TabSelected",
RoutingStrategy.Bubble,
typeof(EventHandler<TabEventArgs>),
typeof(PageTabBar));
public event RoutedEventHandler TabSelected {
add { AddHandler(TabSelectedEvent, value); }
remove { RemoveHandler(TabSelectedEvent, value); }
}
public static readonly RoutedEvent TabClosedEvent = EventManager.RegisterRoutedEvent(
"TabClosed",
RoutingStrategy.Bubble,
typeof(EventHandler<TabEventArgs>),
typeof(PageTabBar));
public event RoutedEventHandler TabClosed {
add { AddHandler(TabClosedEvent, value); }
remove { RemoveHandler(TabClosedEvent, value); }
}
public ObservableCollection<Tab> Tabs {
get;
private set;
}
public string Current {
get { return (container.SelectedItem as Tab).Id; }
}
public PageTabBar() {
Tabs = new ObservableCollection<Tab>();
InitializeComponent();
}
public void Add(string id, UserControl element) {
var tab = new Tab(id, element);
Tabs.Add(tab);
container.SelectedItem = tab;
}
public void Replace(string oldId, string newId, UserControl element) {
var tab = null as Tab;
var curTab = container.SelectedItem as Tab;
foreach (var one in Tabs) {
if (one.Id == oldId) {
tab = one;
break;
}
}
if (tab == null) return;
var idx = Tabs.IndexOf(tab);
Tabs.RemoveAt(idx);
RaiseEvent(new TabEventArgs(TabClosedEvent, this, tab.Id));
var replaced = new Tab(newId, element);
Tabs.Insert(idx, replaced);
if (curTab.Id == oldId) container.SelectedItem = replaced;
}
public bool Goto(string id) {
foreach (var tab in Tabs) {
if (tab.Id == id) {
container.SelectedItem = tab;
return true;
}
}
return false;
}
private void CalcScrollerVisibilty(object sender, SizeChangedEventArgs e) {
if ((sender as StackPanel).ActualWidth > scroller.ActualWidth) {
leftScroller.Visibility = Visibility.Visible;
rightScroller.Visibility = Visibility.Visible;
} else {
leftScroller.Visibility = Visibility.Collapsed;
rightScroller.Visibility = Visibility.Collapsed;
}
}
private void NewTab(object sender, RoutedEventArgs e) {
RaiseEvent(new RoutedEventArgs(TabAddEvent));
}
private void ScrollLeft(object sender, RoutedEventArgs e) {
scroller.LineLeft();
}
private void ScrollRight(object sender, RoutedEventArgs e) {
scroller.LineRight();
}
private void SelectionChanged(object sender, SelectionChangedEventArgs e) {
var tab = container.SelectedItem as Tab;
if (tab == null) return;
RaiseEvent(new TabEventArgs(TabSelectedEvent, this, tab.Id));
}
private void CloseTab(object sender, RoutedEventArgs e) {
var btn = (sender as Button);
var tab = btn.DataContext as Tab;
if (tab == null) return;
var curTab = container.SelectedItem as Tab;
if (curTab != null && tab.Id == curTab.Id) {
if (Tabs.Count > 1) {
var idx = Tabs.IndexOf(tab);
Tabs.Remove(tab);
var next = Tabs[idx % Tabs.Count];
container.SelectedItem = next;
RaiseEvent(new TabEventArgs(TabSelectedEvent, this, next.Id));
} else {
Application.Current.Shutdown();
}
} else {
Tabs.Remove(tab);
}
RaiseEvent(new TabEventArgs(TabClosedEvent, this, tab.Id));
}
private void OnMouseMove(object sender, MouseEventArgs e) {
var item = sender as ListBoxItem;
if (item == null) return;
if (Mouse.LeftButton == MouseButtonState.Pressed) {
var dragging = new Controls.DragDropAdorner(item);
DragDrop.DoDragDrop(item, item.DataContext, DragDropEffects.Move);
dragging.Remove();
}
}
private void OnDrop(object sender, DragEventArgs e) {
var tabSrc = e.Data.GetData(typeof(Tab)) as Tab;
if (tabSrc == null) return;
var dst = e.Source as FrameworkElement;
if (dst == null) return;
var tabDst = dst.DataContext as Tab;
if (tabSrc.Id == tabDst.Id) return;
int dstIdx = Tabs.IndexOf(tabDst);
Tabs.Remove(tabSrc);
Tabs.Insert(dstIdx, tabSrc);
container.SelectedItem = tabSrc;
e.Handled = true;
}
}
}

View file

@ -0,0 +1,31 @@
<UserControl x:Class="SourceGit.Views.Widgets.PageTabItem"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel
Background="Transparent"
Orientation="Horizontal"
ToolTip="{Binding Tip, ElementName=me}"
ContextMenuOpening="OnContextMenuOpening">
<controls:Bookmark
Grid.Column="0"
x:Name="ctrlBookmark"
Width="14" Height="14"
IsNewPage="{Binding IsWelcomePage, ElementName=me}"
Color="{Binding Bookmark, ElementName=me}"
HideOnZero="False"/>
<TextBlock
Grid.Column="1"
Margin="4,0"
FontFamily="Consolas"
FontWeight="Bold"
Text="{Binding Title, ElementName=me}"/>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,78 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 主界面标题栏中的页面标签
/// </summary>
public partial class PageTabItem : UserControl {
public string Title { get; private set; }
public bool IsWelcomePage { get; private set; }
public int Bookmark { get; private set; }
public string Tip { get; private set; }
public PageTabItem(string title, bool isWelcomePage, int bookmark, string tip) {
Title = title;
IsWelcomePage = isWelcomePage;
Bookmark = bookmark;
Tip = tip;
InitializeComponent();
}
private void OnContextMenuOpening(object sender, ContextMenuEventArgs ev) {
if (IsWelcomePage) return;
var refresh = new MenuItem();
refresh.Header = App.Text("RepoCM.Refresh");
refresh.Click += (o, e) => {
Models.Watcher.Get(Tip)?.Refresh();
e.Handled = true;
};
var iconBookmark = FindResource("Icon.Bookmark") as Geometry;
var bookmark = new MenuItem();
bookmark.Header = App.Text("RepoCM.Bookmark");
for (int i = 0; i < Controls.Bookmark.COLORS.Length; i++) {
var icon = new System.Windows.Shapes.Path();
icon.Data = iconBookmark;
icon.Fill = Controls.Bookmark.COLORS[i];
icon.Width = 8;
var mark = new MenuItem();
mark.Icon = icon;
mark.Header = $"{i}";
var refIdx = i;
mark.Click += (o, e) => {
var repo = Models.Preference.Instance.FindRepository(Tip);
if (repo == null) return;
repo.Bookmark = refIdx;
Bookmark = refIdx;
ctrlBookmark.GetBindingExpression(Controls.Bookmark.ColorProperty).UpdateTarget();
e.Handled = true;
};
bookmark.Items.Add(mark);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("RepoCM.CopyPath");
copyPath.Click += (o, e) => {
Clipboard.SetText(Tip);
e.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(refresh);
menu.Items.Add(bookmark);
menu.Items.Add(copyPath);
menu.IsOpen = true;
ev.Handled = true;
}
}
}

View file

@ -0,0 +1,46 @@
<UserControl x:Class="SourceGit.Views.Widgets.PopupPanel"
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:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d" Visibility="Collapsed">
<Grid ClipToBounds="True">
<!-- Background to close -->
<Border Background="Transparent" MouseLeftButtonDown="Cancel"/>
<!-- Popup panel -->
<Border
Background="{StaticResource Brush.Popup}"
BorderBrush="{StaticResource Brush.Border0}"
BorderThickness="1,0,1,1"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="500"
Height="Auto">
<Grid>
<!-- Custom panel -->
<Border x:Name="body">
<StackPanel Margin="8" Orientation="Vertical">
<TextBlock Margin="8,8,0,18" x:Name="txtTitle" FontSize="18" FontWeight="DemiBold"/>
<ContentControl x:Name="container"/>
<StackPanel Margin="0,16,0,0" Height="32" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Click="Sure" Width="80" Content="SURE" BorderBrush="{StaticResource Brush.FG1}" Background="{StaticResource Brush.Accent1}"/>
<Button Click="Cancel" Width="80" Margin="8,0,0,0" Content="CANCEL"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Progress mask -->
<Border x:Name="mask" Visibility="Collapsed" Background="{StaticResource Brush.Popup}" Opacity=".9">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:Loading x:Name="processing" Width="48" Height="48"/>
<TextBlock x:Name="txtMsg" Margin="0,16,0,0"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,107 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 统一的下拉弹出窗体面板
/// </summary>
public partial class PopupPanel : UserControl {
private Controls.PopupWidget view = null;
private bool locked = false;
public bool IsLocked {
get { return locked; }
}
public PopupPanel() {
InitializeComponent();
}
public void Show(Controls.PopupWidget widget) {
if (locked) return;
view = widget;
txtTitle.Text = widget.GetTitle();
Visibility = Visibility.Hidden;
container.Content = view;
body.Margin = new Thickness(0, 0, 0, 0);
body.UpdateLayout();
var gone = new Thickness(0, -body.ActualHeight, 0, 0);
body.Margin = gone;
ThicknessAnimation anim = new ThicknessAnimation();
anim.Duration = TimeSpan.FromMilliseconds(150);
anim.From = gone;
anim.To = new Thickness(0);
Visibility = Visibility.Visible;
body.BeginAnimation(MarginProperty, anim);
}
public void ShowAndStart(Controls.PopupWidget widget) {
if (locked) return;
Show(widget);
Sure(null, null);
}
public void UpdateProgress(string message) {
Dispatcher.Invoke(() => txtMsg.Text = message);
}
public void Close() {
if (Visibility != Visibility.Visible) return;
ThicknessAnimation anim = new ThicknessAnimation();
anim.Duration = TimeSpan.FromMilliseconds(150);
anim.From = new Thickness(0);
anim.To = new Thickness(0, -body.ActualHeight, 0, 0);
anim.Completed += (obj, ev) => {
Visibility = Visibility.Collapsed;
container.Content = null;
view = null;
locked = false;
mask.Visibility = Visibility.Collapsed;
processing.IsAnimating = false;
txtMsg.Text = "";
};
body.BeginAnimation(MarginProperty, anim);
}
private async void Sure(object sender, RoutedEventArgs e) {
if (Visibility != Visibility.Visible) return;
if (view == null) {
Close();
return;
}
if (locked) return;
locked = true;
mask.Visibility = Visibility.Visible;
processing.IsAnimating = true;
var task = view.Start();
if (task != null) {
var close = await task;
if (close) {
Close();
return;
}
}
locked = false;
mask.Visibility = Visibility.Collapsed;
processing.IsAnimating = false;
txtMsg.Text = "";
}
private void Cancel(object sender, RoutedEventArgs e) {
if (locked) return;
Close();
}
}
}

View file

@ -0,0 +1,107 @@
<UserControl x:Class="SourceGit.Views.Widgets.RevisionCompare"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="4,8,4,4">
<Grid.RowDefinitions>
<RowDefinition Height="44"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="360"/>
<ColumnDefinition Width="48"/>
<ColumnDefinition Width="360"/>
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="{StaticResource Brush.Border2}"
BorderThickness="1"
Background="{StaticResource Brush.Contents}"
CornerRadius="4">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<controls:Avatar Grid.Column="0" Width="32" Height="32" x:Name="avatarStart"/>
<Grid Grid.Column="1" Margin="8,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" TextBlock.FontSize="11" TextBlock.FontFamily="Consolas">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="txtStartSHA" Foreground="DarkOrange"/>
<TextBlock Grid.Column="1" x:Name="txtStartTime" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
<TextBlock Grid.Row="1" x:Name="txtStartSubject" VerticalAlignment="Bottom"/>
</Grid>
</Grid>
</Border>
<Path
Grid.Column="1"
Width="16" Height="16"
Fill="{StaticResource Brush.FG2}"
Data="{StaticResource Icon.Down}"
RenderTransformOrigin=".5,.5">
<Path.RenderTransform>
<RotateTransform Angle="270"/>
</Path.RenderTransform>
</Path>
<Border
Grid.Column="2"
BorderBrush="{StaticResource Brush.Border2}"
BorderThickness="1"
Background="{StaticResource Brush.Contents}"
CornerRadius="4">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<controls:Avatar Grid.Column="0" Width="32" Height="32" x:Name="avatarEnd"/>
<Grid Grid.Column="1" Margin="8,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" TextBlock.FontSize="11" TextBlock.FontFamily="Consolas">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="txtEndSHA" Foreground="DarkOrange"/>
<TextBlock Grid.Column="1" x:Name="txtEndTime" Foreground="{StaticResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
<TextBlock Grid.Row="1" x:Name="txtEndSubject" VerticalAlignment="Bottom"/>
</Grid>
</Grid>
</Border>
</Grid>
<widgets:CommitChanges Grid.Row="1" x:Name="changesContainer" Margin="0,8,0,0"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 展示两个提交之间的变更
/// </summary>
public partial class RevisionCompare : UserControl {
public RevisionCompare() {
InitializeComponent();
}
public void SetData(string repo, Models.Commit start, Models.Commit end) {
avatarStart.Email = start.Committer.Email;
avatarStart.FallbackLabel = start.Committer.Name;
txtStartSHA.Text = start.ShortSHA;
txtStartTime.Text = start.Committer.Time;
txtStartSubject.Text = start.Subject;
avatarEnd.Email = end.Committer.Email;
avatarEnd.FallbackLabel = end.Committer.Name;
txtEndSHA.Text = end.ShortSHA;
txtEndTime.Text = end.Committer.Time;
txtEndSubject.Text = end.Subject;
Task.Run(() => {
var changes = new Commands.CommitRangeChanges(repo, start.SHA, end.SHA).Result();
Dispatcher.Invoke(() => {
changesContainer.SetData(repo, new List<Models.Commit>() { start, end }, changes);
});
});
}
}
}

View file

@ -0,0 +1,129 @@
<UserControl x:Class="SourceGit.Views.Widgets.Stashes"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
xmlns:models="clr-namespace:SourceGit.Models"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Stashes List Group -->
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="6,0,0,0"
Text="{StaticResource Text.Stashes.Stashes}"
Foreground="{StaticResource Brush.FG2}"
FontWeight="Bold"/>
<controls:Loading
x:Name="waiting"
Width="12" Height="12"
Margin="8,0,0,0"
Visibility="Collapsed"/>
</StackPanel>
</Border>
<!-- Stashes List -->
<ListView
Grid.Row="1"
x:Name="stashList"
Background="{StaticResource Brush.Contents}"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionChanged="OnStashSelectionChanged">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type models:Stash}">
<Border BorderBrush="{StaticResource Brush.Border3}" BorderThickness="0,0,0,1" Background="Transparent" Padding="6" ContextMenuOpening="OnStashContextMenuOpening">
<StackPanel Orientation="Vertical" TextElement.FontFamily="Consolas">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding SHA}" Foreground="{StaticResource Brush.FG2}" FontSize="11"/>
<TextBlock Grid.Column="1" Text="{Binding Author.Time}" Foreground="{StaticResource Brush.FG2}" FontSize="11"/>
</Grid>
<TextBlock Text="{Binding Message}" Margin="0,8,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Change List Group -->
<Border Grid.Row="2" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="6,0,0,0"
Text="{StaticResource Text.Stashes.Changes}"
Foreground="{StaticResource Brush.FG2}"
FontWeight="Bold"/>
<TextBlock
Grid.Column="1"
Margin="0,0,4,0"
Text="{StaticResource Text.Stashes.Changes.Tip}"
Foreground="{StaticResource Brush.FG2}"
FontFamily="Consolas"
FontSize="10"/>
</Grid>
</Border>
<!-- Changed Files -->
<DataGrid
Grid.Row="3"
x:Name="changeList"
Background="{StaticResource Brush.Contents}"
RowHeight="24"
SelectionMode="Single"
SelectionUnit="FullRow"
SelectionChanged="OnChangeSelectionChanged">
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="False" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Splitter -->
<GridSplitter Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Width="1" Background="{StaticResource Brush.Border0}"/>
<!-- Right -->
<widgets:DiffViewer Grid.Column="2" x:Name="diffViewer" Margin="4"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 贮藏管理
/// </summary>
public partial class Stashes : UserControl {
private string repo = null;
private string selected = null;
public Stashes(string repo) {
this.repo = repo;
InitializeComponent();
}
public void SetData(List<Models.Stash> data) {
stashList.ItemsSource = data;
changeList.ItemsSource = null;
}
private async void OnStashSelectionChanged(object sender, SelectionChangedEventArgs e) {
changeList.ItemsSource = null;
selected = null;
var stash = stashList.SelectedItem as Models.Stash;
if (stash == null) return;
selected = stash.SHA;
diffViewer.Reset();
var changes = await Task.Run(() => new Commands.StashChanges(repo, selected).Result());
changeList.ItemsSource = changes;
}
private void OnChangeSelectionChanged(object sender, SelectionChangedEventArgs e) {
var change = changeList.SelectedItem as Models.Change;
if (change == null) return;
diffViewer.Diff(repo, new DiffViewer.Option() {
RevisionRange = new string[] { selected + "^", selected },
Path = change.Path,
OrgPath = change.OriginalPath
});
}
private void OnStashContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var stash = (sender as Border).DataContext as Models.Stash;
if (stash == null) return;
var apply = new MenuItem();
apply.Header = App.Text("StashCM.Apply");
apply.Click += (o, e) => Start(() => new Commands.Stash(repo).Apply(stash.Name));
var pop = new MenuItem();
pop.Header = App.Text("StashCM.Pop");
pop.Click += (o, e) => Start(() => new Commands.Stash(repo).Pop(stash.Name));
var delete = new MenuItem();
delete.Header = App.Text("StashCM.Drop");
delete.Click += (o, e) => Start(() => new Commands.Stash(repo).Drop(stash.Name));
var menu = new ContextMenu();
menu.Items.Add(apply);
menu.Items.Add(pop);
menu.Items.Add(delete);
menu.IsOpen = true;
ev.Handled = true;
}
private async void Start(Func<bool> job) {
waiting.Visibility = Visibility.Visible;
waiting.IsAnimating = true;
Models.Watcher.SetEnabled(repo, false);
await Task.Run(job);
Models.Watcher.SetEnabled(repo, true);
waiting.Visibility = Visibility.Collapsed;
waiting.IsAnimating = false;
}
}
}

View file

@ -0,0 +1,186 @@
<UserControl x:Class="SourceGit.Views.Widgets.Welcome"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="800">
<Grid Background="Transparent" AllowDrop="True" DragEnter="OnPageDragEnter" DragLeave="OnPageDragLeave" Drop="OnPageDrop">
<Grid Width="420">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical" Width="420" HorizontalAlignment="Center" TextElement.FontFamily="Consolas">
<!-- Logo -->
<Path
Margin="0,48,0,0"
Width="100" Height="100"
Data="{StaticResource Icon.Git}"
Fill="{StaticResource Brush.Logo}"/>
<!-- Welcome -->
<TextBlock
Margin="0,16"
HorizontalAlignment="Center"
Text="{StaticResource Text.Welcome.Title}"
FontSize="28"
FontWeight="ExtraBold"
Foreground="{StaticResource Brush.FG2}"/>
<!-- Options -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Click="OnOpenClicked" Height="28">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16" Data="{StaticResource Icon.Folder.Open}"/>
<TextBlock Margin="12,0,0,0" Text="{StaticResource Text.Welcome.OpenOrInit}"/>
</StackPanel>
</Button>
<Button Grid.Column="2" Click="OnCloneClicked" Height="28">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16" Data="{StaticResource Icon.Pull}"/>
<TextBlock Margin="12,0,0,0" Text="{StaticResource Text.Welcome.Clone}"/>
</StackPanel>
</Button>
</Grid>
<!-- Horizontal line -->
<Rectangle Height="1" Margin="0,36,0,8" Fill="{StaticResource Brush.Border1}"/>
<!-- Labels -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{StaticResource Text.Welcome.Repositories}"
FontSize="18" FontWeight="ExtraBold"
Foreground="{StaticResource Brush.FG2}"/>
<TextBlock
Grid.Column="2"
Text="{StaticResource Text.Welcome.DragDrop}"
FontSize="14"
Foreground="{StaticResource Brush.FG2}"/>
</Grid>
</StackPanel>
<!-- Drop Area -->
<Rectangle
Grid.Row="1"
x:Name="dropArea"
Margin="0,2"
Stroke="{StaticResource Brush.Border1}"
StrokeThickness="2"
StrokeDashArray="4,4"
SnapsToDevicePixels="True"
Visibility="Hidden"/>
<!-- Tree -->
<controls:Tree
Grid.Row="1"
x:Name="tree"
Margin="2,4"
AllowDrop="True"
TextElement.FontSize="14"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ContextMenuOpening="OnTreeContextMenuOpening"
MouseMove="OnTreeMouseMove"
DragOver="OnTreeDragOver"
Drop="OnTreeDrop">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="Expanded" Handler="OnTreeNodeStatusChange"/>
<EventSetter Event="Collapsed" Handler="OnTreeNodeStatusChange"/>
<EventSetter Event="MouseDoubleClick" Handler="OnTreeNodeDoubleClick"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Border Height="32">
<Grid IsHitTestVisible="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="22"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Margin="2,0,0,0" x:Name="Icon" Width="16" Height="16" Data="{StaticResource Icon.Git}"/>
<StackPanel Grid.Column="1" x:Name="Contents" Orientation="Horizontal">
<TextBlock Margin="8,0" Text="{Binding Name}"/>
<TextBlock x:Name="Path" Text="{Binding Id}" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
<controls:TextEdit
Grid.Column="1"
x:Name="Editor"
Height="20"
Margin="4,0,0,0"
Text="{Binding Name}"
FontSize="12"
Loaded="RenameStart"
KeyDown="RenameKeyDown"
LostFocus="RenameEnd"
IsHitTestVisible="True"
Visibility="Collapsed"/>
<controls:Bookmark
Grid.Column="2"
Width="14" Height="14"
Color="{Binding Bookmark}"
IsNewPage="False"
HideOnZero="True"/>
</Grid>
</Border>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsGroup}" Value="True">
<Setter TargetName="Path" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsGroup}" Value="True"/>
<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 IsGroup}" Value="True"/>
<Condition Binding="{Binding IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter TargetName="Editor" Property="Visibility" Value="Visible"/>
<Setter TargetName="Contents" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Grid>
<!-- Popup -->
<widgets:PopupPanel x:Name="popup"/>
</Grid>
</UserControl>

View file

@ -0,0 +1,386 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 新标签页
/// </summary>
public partial class Welcome : UserControl, Controls.IPopupContainer {
/// <summary>
/// 树节点数据
/// </summary>
public class Node {
public string Id { get; set; }
public string ParentId { get; set; }
public string Name { get; set; }
public bool IsGroup { get; set; }
public bool IsEditing { get; set; }
public bool IsExpanded { get; set; }
public int Bookmark { get; set; }
public List<Node> Children { get; set; }
}
public Welcome() {
InitializeComponent();
UpdateTree();
}
#region POPUP_CONTAINER
public void Show(Controls.PopupWidget widget) {
popup.Show(widget);
}
public void ShowAndStart(Controls.PopupWidget widget) {
popup.ShowAndStart(widget);
}
public void UpdateProgress(string message) {
popup.UpdateProgress(message);
}
#endregion
#region FUNC_EVENTS
private void OnOpenClicked(object sender, RoutedEventArgs e) {
FolderBrowser.Open(null, App.Text("Welcome.OpenOrInitDialog"), CheckAndOpen);
}
private void OnCloneClicked(object sender, RoutedEventArgs e) {
if (MakeSureReady()) new Popups.Clone().Show();
}
private void OnTreeNodeStatusChange(object sender, RoutedEventArgs e) {
var node = (sender as Controls.TreeItem).DataContext as Node;
if (node != null) {
var group = Models.Preference.Instance.FindGroup(node.Id);
group.IsExpanded = node.IsExpanded;
e.Handled = true;
}
}
private void OnTreeNodeDoubleClick(object sender, MouseButtonEventArgs e) {
var node = (sender as Controls.TreeItem).DataContext as Node;
if (node != null && !node.IsGroup) {
CheckAndOpen(node.Id);
e.Handled = true;
}
}
private void OnTreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = tree.FindItem(e.OriginalSource as DependencyObject);
if (item == null) {
var addFolder = new MenuItem();
addFolder.Header = App.Text("Welcome.NewFolder");
addFolder.Click += (o, ev) => {
var group = Models.Preference.Instance.AddGroup("New Group", "");
UpdateTree(group.Id);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(addFolder);
menu.IsOpen = true;
e.Handled = true;
} else {
var node = item.DataContext as Node;
if (node == null) return;
var menu = new ContextMenu();
if (!node.IsGroup) {
var open = new MenuItem();
open.Header = App.Text("RepoCM.Open");
open.Click += (o, ev) => {
CheckAndOpen(node.Id);
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = App.Text("RepoCM.Explore");
explore.Click += (o, ev) => {
Process.Start("explorer", node.Id);
ev.Handled = true;
};
var iconBookmark = FindResource("Icon.Bookmark") as Geometry;
var bookmark = new MenuItem();
bookmark.Header = App.Text("RepoCM.Bookmark");
for (int i = 0; i < Controls.Bookmark.COLORS.Length; i++) {
var icon = new System.Windows.Shapes.Path();
icon.Data = iconBookmark;
icon.Fill = Controls.Bookmark.COLORS[i];
icon.Width = 8;
var mark = new MenuItem();
mark.Icon = icon;
mark.Header = $"{i}";
var refIdx = i;
mark.Click += (o, ev) => {
var repo = Models.Preference.Instance.FindRepository(node.Id);
if (repo != null) {
repo.Bookmark = refIdx;
UpdateTree();
}
ev.Handled = true;
};
bookmark.Items.Add(mark);
}
menu.Items.Add(open);
menu.Items.Add(explore);
menu.Items.Add(bookmark);
} else {
var addSubFolder = new MenuItem();
addSubFolder.Header = App.Text("Welcome.NewSubFolder");
addSubFolder.Click += (o, ev) => {
var parent = Models.Preference.Instance.FindGroup(node.Id);
if (parent != null) parent.IsExpanded = true;
var group = Models.Preference.Instance.AddGroup("New Group", node.Id);
UpdateTree(group.Id);
ev.Handled = true;
};
menu.Items.Add(addSubFolder);
}
var rename = new MenuItem();
rename.Header = App.Text("Welcome.Rename");
rename.Click += (o, ev) => {
UpdateTree(node.Id);
ev.Handled = true;
};
var delete = new MenuItem();
delete.Header = App.Text("Welcome.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 DRAP_DROP_EVENTS
private void OnPageDragEnter(object sender, DragEventArgs e) {
if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent(typeof(Node))) {
dropArea.Visibility = Visibility.Visible;
}
}
private void OnPageDragLeave(object sender, DragEventArgs e) {
dropArea.Visibility = Visibility.Hidden;
}
private void OnPageDrop(object sender, DragEventArgs e) {
dropArea.Visibility = Visibility.Hidden;
}
private void OnTreeMouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton != MouseButtonState.Pressed) return;
var item = tree.FindItem(e.OriginalSource as DependencyObject);
if (item == null) return;
var adorner = new Controls.DragDropAdorner(item);
DragDrop.DoDragDrop(item, item.DataContext, DragDropEffects.Move);
adorner.Remove();
}
private void OnTreeDragOver(object sender, DragEventArgs e) {
if (!e.Data.GetDataPresent(DataFormats.FileDrop) && !e.Data.GetDataPresent(typeof(Node))) return;
var item = tree.FindItem(e.OriginalSource as DependencyObject);
if (item == null) return;
var node = item.DataContext as Node;
if (node.IsGroup && !item.IsExpanded) item.IsExpanded = true;
e.Handled = true;
}
private void OnTreeDrop(object sender, DragEventArgs e) {
bool rebuild = false;
dropArea.Visibility = Visibility.Hidden;
var parent = "";
var to = tree.FindItem(e.OriginalSource as DependencyObject);
if (to != null) {
var dst = to.DataContext as Node;
parent = dst.IsGroup ? dst.Id : dst.ParentId;
}
if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
if (!MakeSureReady()) return;
var paths = e.Data.GetData(DataFormats.FileDrop) as string[];
foreach (var path in paths) {
var dir = new Commands.QueryGitDir(path).Result();
if (dir != null) {
var root = new Commands.GetRepositoryRootPath(path).Result();
Models.Preference.Instance.AddRepository(root, dir, parent);
rebuild = true;
}
}
} else if (e.Data.GetDataPresent(typeof(Node))) {
var src = e.Data.GetData(typeof(Node)) as Node;
if (src.IsGroup) {
if (!Models.Preference.Instance.IsSubGroup(src.Id, parent)) {
Models.Preference.Instance.FindGroup(src.Id).Parent = parent;
rebuild = true;
}
} else {
Models.Preference.Instance.FindRepository(src.Id).GroupId = parent;
rebuild = true;
}
}
if (rebuild) UpdateTree();
e.Handled = true;
}
#endregion
#region DATA
private void UpdateTree(string editingNodeId = null) {
var groupNodes = new Dictionary<string, Node>();
var nodes = new List<Node>();
foreach (var group in Models.Preference.Instance.Groups) {
Node node = new Node() {
Id = group.Id,
ParentId = group.Parent,
Name = group.Name,
IsGroup = true,
IsEditing = group.Id == editingNodeId,
IsExpanded = group.IsExpanded,
Bookmark = 0,
Children = new List<Node>(),
};
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 Models.Preference.Instance.Repositories) {
Node node = new Node() {
Id = repo.Path,
ParentId = repo.GroupId,
Name = repo.Name,
IsGroup = false,
IsEditing = repo.Path == editingNodeId,
IsExpanded = false,
Bookmark = repo.Bookmark,
Children = new List<Node>(),
};
if (groupNodes.ContainsKey(repo.GroupId)) {
groupNodes[repo.GroupId].Children.Add(node);
} else {
nodes.Add(node);
}
}
tree.ItemsSource = nodes;
}
private void DeleteNode(Node node) {
if (node.IsGroup) {
Models.Preference.Instance.RemoveGroup(node.Id);
} else {
Models.Preference.Instance.RemoveRepository(node.Id);
}
UpdateTree();
}
private bool MakeSureReady() {
if (!Models.Preference.Instance.IsReady) {
Models.Exception.Raise(App.Text("NotConfigured"));
return false;
}
return true;
}
private void CheckAndOpen(string path) {
if (!MakeSureReady()) return;
if (!Directory.Exists(path)) {
Models.Exception.Raise(App.Text("PathNotFound", path));
return;
}
var root = new Commands.GetRepositoryRootPath(path).Result();
if (root == null) {
new Popups.Init(path).Show();
return;
}
var gitDir = new Commands.QueryGitDir(root).Result();
var repo = Models.Preference.Instance.AddRepository(root, gitDir, "");
Models.Watcher.Open(repo);
}
#endregion
#region RENAME_NODES
private void RenameStart(object sender, RoutedEventArgs e) {
var edit = sender as Controls.TextEdit;
if (edit == null || !edit.IsVisible) return;
edit.SelectAll();
edit.Focus();
}
private void RenameKeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Escape) {
UpdateTree();
e.Handled = true;
} else if (e.Key == Key.Enter) {
RenameEnd(sender, e);
e.Handled = true;
}
}
private void RenameEnd(object sender, RoutedEventArgs e) {
var edit = sender as Controls.TextEdit;
if (edit == null) return;
if (string.IsNullOrWhiteSpace(edit.Text)) {
UpdateTree();
e.Handled = false;
return;
}
var node = edit.DataContext as Node;
if (node != null) {
if (node.IsGroup) {
Models.Preference.Instance.RenameGroup(node.Id, edit.Text);
} else {
Models.Preference.Instance.RenameRepository(node.Id, edit.Text);
}
UpdateTree();
e.Handled = false;
}
}
#endregion
}
}

View file

@ -0,0 +1,249 @@
<UserControl x:Class="SourceGit.Views.Widgets.WorkingCopy"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converter="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
xmlns:validations="clr-namespace:SourceGit.Views.Validations"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converter:BoolToCollapsed x:Key="BoolToCollapsed"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
<RowDefinition Height="26"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Unstaged Toolbar -->
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<controls:ChangeDisplaySwitcher
Grid.Column="0"
x:Name="unstagedMode"
Width="14" Height="14"
Margin="4,0,0,0"
Mode="{Binding Source={x:Static models:Preference.Instance}, Path=Window.ChangeInUnstaged, Mode=TwoWay}"/>
<TextBlock
Grid.Column="1"
Margin="6,0"
Text="{StaticResource Text.WorkingCopy.Unstaged}"
Foreground="{StaticResource Brush.FG2}"
FontWeight="Bold"/>
<controls:Loading
Grid.Column="2"
Width="12" Height="12"
x:Name="iconStaging"
IsAnimating="{Binding ElementName=unstagedContainer, Path=IsStaging}"
Visibility="{Binding ElementName=unstagedContainer, Path=IsStaging, Converter={StaticResource BoolToCollapsed}}"/>
<controls:IconButton
Grid.Column="4"
Click="StageSelected"
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.Down}"
ToolTip="{StaticResource Text.WorkingCopy.Unstaged.Stage}"/>
<controls:IconButton
Grid.Column="5"
Click="StageAll"
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.DoubleDown}"
ToolTip="{StaticResource Text.WorkingCopy.Unstaged.StageAll}"/>
</Grid>
</Border>
<!-- Unstaged Changes -->
<Border Grid.Row="1" Background="{StaticResource Brush.Contents}">
<widgets:WorkingCopyChanges
x:Name="unstagedContainer"
IsUnstaged="True"
Mode="{Binding ElementName=unstagedMode, Path=Mode}"
DiffTargetChanged="OnDiffTargetChanged"/>
</Border>
<!-- Staged Toolbar -->
<Border Grid.Row="2" BorderBrush="{StaticResource Brush.Border0}" BorderThickness="0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<controls:ChangeDisplaySwitcher
Grid.Column="0"
x:Name="stagedMode"
Width="14" Height="14"
Margin="4,0,0,0"
Mode="{Binding Source={x:Static models:Preference.Instance}, Path=Window.ChangeInStaged, Mode=TwoWay}"/>
<TextBlock
Grid.Column="1"
Margin="6,0"
Text="{StaticResource Text.WorkingCopy.Staged}"
Foreground="{StaticResource Brush.FG2}"
FontWeight="Bold"/>
<controls:IconButton
Grid.Column="2"
Click="UnstageSelected"
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.Up}"
ToolTip="{StaticResource Text.WorkingCopy.Staged.Unstage}"/>
<controls:IconButton
Grid.Column="3"
Click="UnstageAll"
Width="14" Height="14"
Margin="4,0"
Icon="{StaticResource Icon.DoubleUp}"
ToolTip="{StaticResource Text.WorkingCopy.Staged.UnstageAll}"/>
</Grid>
</Border>
<!-- Staged Changes -->
<Border Grid.Row="3" Background="{StaticResource Brush.Contents}">
<widgets:WorkingCopyChanges
x:Name="stagedContainer"
IsUnstaged="False"
Mode="{Binding ElementName=stagedMode, Path=Mode}"
DiffTargetChanged="OnDiffTargetChanged"/>
</Border>
</Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border0}"/>
<!-- Right -->
<Grid Grid.Column="2" Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Diff viewer -->
<widgets:DiffViewer Grid.Row="0" x:Name="diffViewer"/>
<!-- Merge Option Panel -->
<Grid Grid.Row="0" x:Name="mergePanel" Background="{StaticResource Brush.Window}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path
Width="64" Height="64"
Data="{StaticResource Icon.Conflict}"
Fill="{StaticResource Brush.FG2}"/>
<TextBlock
Margin="0,16,0,28"
FontSize="20" FontWeight="DemiBold"
Text="{StaticResource Text.WorkingCopy.Conflicts}"
Foreground="{StaticResource Brush.FG2}"
HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Click="UseTheirs" Content="{StaticResource Text.WorkingCopy.UseTheirs}" Height="24" Padding="8,0"/>
<Button Click="UseMine" Content="{StaticResource Text.WorkingCopy.UseMine}" Height="24" Margin="8,0" Padding="8,0"/>
<Button Click="UseMergeTool" Content="{StaticResource Text.WorkingCopy.OpenMerger}" Height="24" Padding="8,0"/>
</StackPanel>
</StackPanel>
</Grid>
<!-- Commit Message -->
<controls:TextEdit
Grid.Row="1"
x:Name="txtCommitMessage"
Height="64"
Margin="0,4" Padding="1"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="Wrap"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Placeholder="{StaticResource Text.WorkingCopy.CommitMessageTip}"
PlaceholderBaseline="Top">
<TextBox.Text>
<Binding ElementName="me" Path="CommitMessage" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validations:CommitMessage/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</controls:TextEdit>
<!-- Commit Options -->
<Grid Grid.Row="2" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<controls:IconButton
Grid.Column="0"
Width="14" Height="14"
Click="OpenCommitMessageRecorder"
ToolTip="{StaticResource Text.WorkingCopy.MessageHistories}"
Icon="{StaticResource Icon.List}"
Opacity=".5"/>
<CheckBox
Grid.Column="1"
x:Name="chkAmend"
Margin="8,0,0,0"
HorizontalAlignment="Left"
Content="{StaticResource Text.WorkingCopy.Amend}"
Checked="StartAmend" Unchecked="EndAmend"/>
<controls:Loading
Grid.Column="3"
x:Name="iconCommitting"
Width="18" Height="18"
Margin="0,0,8,0"
Visibility="Collapsed"/>
<Button
Grid.Column="4"
Height="26"
Padding="8,0"
Click="Commit"
Background="{StaticResource Brush.Accent1}"
BorderBrush="{StaticResource Brush.FG1}"
Content="{StaticResource Text.WorkingCopy.Commit}"/>
<Button
Grid.Column="5"
x:Name="btnCommitAndPush"
Height="26"
Padding="8,0"
Click="CommitAndPush"
Content="{StaticResource Text.WorkingCopy.CommitAndPush}"
Margin="8,0,0,0"/>
</Grid>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,340 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 工作区
/// </summary>
public partial class WorkingCopy : UserControl {
private Models.Repository repo = null;
public string CommitMessage { get; set; }
public WorkingCopy(Models.Repository repo) {
this.repo = repo;
InitializeComponent();
unstagedContainer.SetRepository(repo.Path);
stagedContainer.SetRepository(repo.Path);
}
public void SetData(List<Models.Change> changes) {
List<Models.Change> unstagedChanges = new List<Models.Change>();
List<Models.Change> stagedChanges = new List<Models.Change>();
foreach (var c in changes) {
if (c.IsAddedToIndex) {
stagedChanges.Add(c);
}
if (c.WorkTree != Models.Change.Status.None) {
unstagedChanges.Add(c);
}
}
unstagedContainer.SetData(unstagedChanges);
stagedContainer.SetData(stagedChanges);
var current = repo.Branches.Find(x => x.IsCurrent);
if (current != null && !string.IsNullOrEmpty(current.Upstream) && chkAmend.IsChecked != true) {
btnCommitAndPush.Visibility = Visibility.Visible;
} else {
btnCommitAndPush.Visibility = Visibility.Collapsed;
}
var diffTarget = unstagedContainer.DiffTarget;
if (diffTarget == null) diffTarget = stagedContainer.DiffTarget;
if (diffTarget == null) {
mergePanel.Visibility = Visibility.Collapsed;
diffViewer.Reset();
} else if (diffTarget.IsConflit) {
mergePanel.Visibility = Visibility.Visible;
diffViewer.Reset();
} else {
mergePanel.Visibility = Visibility.Collapsed;
diffViewer.Reload();
}
}
public void TryLoadMergeMessage() {
if (string.IsNullOrEmpty(txtCommitMessage.Text)) {
var mergeMsgFile = Path.Combine(repo.GitDir, "MERGE_MSG");
if (!File.Exists(mergeMsgFile)) return;
var content = File.ReadAllText(mergeMsgFile);
txtCommitMessage.Text = content;
}
}
public void ClearMessage() {
txtCommitMessage.Text = "";
Validation.ClearInvalid(txtCommitMessage.GetBindingExpression(TextBox.TextProperty));
}
#region STAGE_UNSTAGE
private void StageSelected(object sender, RoutedEventArgs e) {
unstagedContainer.StageSelected();
}
private void StageAll(object sender, RoutedEventArgs e) {
unstagedContainer.StageAll();
}
private void UnstageSelected(object sender, RoutedEventArgs e) {
stagedContainer.UnstageSelected();
}
private void UnstageAll(object sender, RoutedEventArgs e) {
stagedContainer.UnstageAll();
}
private void OnDiffTargetChanged(object sender, WorkingCopyChanges.DiffTargetChangedEventArgs e) {
var container = sender as WorkingCopyChanges;
if (container == null) return;
if (e.Target == null) {
if (e.HasOthers) {
mergePanel.Visibility = Visibility.Collapsed;
diffViewer.Reset();
}
return;
}
mergePanel.Visibility = Visibility.Collapsed;
diffViewer.Reset();
var change = e.Target;
if (change.IsConflit) {
mergePanel.Visibility = Visibility.Visible;
return;
}
if (change.IsAddedToIndex) {
unstagedContainer.UnselectAll();
diffViewer.Diff(repo.Path, new DiffViewer.Option() {
ExtraArgs = "--cached",
Path = change.Path,
OrgPath = change.OriginalPath
});
} else {
stagedContainer.UnselectAll();
switch (change.WorkTree) {
case Models.Change.Status.Added:
case Models.Change.Status.Untracked:
diffViewer.Diff(repo.Path, new DiffViewer.Option() {
ExtraArgs = "--no-index",
Path = change.Path,
OrgPath = "/dev/null"
});
break;
default:
diffViewer.Diff(repo.Path, new DiffViewer.Option() {
Path = change.Path,
OrgPath = change.OriginalPath
});
break;
}
}
}
#endregion
#region MERGE
private async void UseTheirs(object sender, RoutedEventArgs e) {
var change = unstagedContainer.DiffTarget;
if (change == null || !change.IsConflit) return;
Models.Watcher.SetEnabled(repo.Path, false);
var succ = await Task.Run(() => new Commands.Checkout(repo.Path).File(change.Path, true));
if (succ) {
await Task.Run(() => new Commands.Add(repo.Path, new List<string>() { change.Path }).Exec());
}
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
}
private async void UseMine(object sender, RoutedEventArgs e) {
var change = unstagedContainer.DiffTarget;
if (change == null || !change.IsConflit) return;
Models.Watcher.SetEnabled(repo.Path, false);
var succ = await Task.Run(() => new Commands.Checkout(repo.Path).File(change.Path, false));
if (succ) {
await Task.Run(() => new Commands.Add(repo.Path, new List<string>() { change.Path }).Exec());
}
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
}
private async void UseMergeTool(object sender, RoutedEventArgs e) {
var mergeType = Models.Preference.Instance.MergeTool.Type;
var mergeExe = Models.Preference.Instance.MergeTool.Path;
var merger = Models.MergeTool.Supported.Find(x => x.Type == mergeType);
if (merger == null || merger.Type == 0 || !File.Exists(mergeExe)) {
Models.Exception.Raise("Invalid merge tool in preference setting!");
return;
}
var change = unstagedContainer.DiffTarget;
if (change == null || !change.IsConflit) return;
var cmd = new Commands.Command();
cmd.Cwd = repo.Path;
cmd.Args = $"-c mergetool.sourcegit.cmd=\"\\\"{mergeExe}\\\" {merger.Cmd}\" ";
cmd.Args += "-c mergetool.writeToTemp=true -c mergetool.keepBackup=false -c mergetool.trustExitCode=true ";
cmd.Args += $"mergetool --tool=sourcegit {change.Path}";
await Task.Run(() => cmd.Exec());
e.Handled = true;
}
#endregion
#region COMMIT
private void OpenCommitMessageRecorder(object sender, RoutedEventArgs e) {
var anchor = sender as Button;
if (anchor.ContextMenu == null) {
anchor.ContextMenu = new ContextMenu();
anchor.ContextMenu.PlacementTarget = anchor;
anchor.ContextMenu.Placement = PlacementMode.Top;
anchor.ContextMenu.VerticalOffset = 0;
anchor.ContextMenu.StaysOpen = false;
anchor.ContextMenu.Focusable = true;
anchor.ContextMenu.MaxWidth = 500;
} else {
anchor.ContextMenu.Items.Clear();
}
if (repo.CommitMessages.Count == 0) {
var tip = new MenuItem();
tip.Header = App.Text("WorkingCopy.NoCommitHistories");
tip.IsEnabled = false;
anchor.ContextMenu.Items.Add(tip);
} else {
var tip = new MenuItem();
tip.Header = App.Text("WorkingCopy.HasCommitHistories");
tip.IsEnabled = false;
anchor.ContextMenu.Items.Add(tip);
anchor.ContextMenu.Items.Add(new Separator());
foreach (var one in repo.CommitMessages) {
var dump = one;
var item = new MenuItem();
item.Header = dump;
item.Padding = new Thickness(0);
item.Click += (o, ev) => {
txtCommitMessage.Text = dump;
ev.Handled = true;
};
anchor.ContextMenu.Items.Add(item);
}
}
anchor.ContextMenu.IsOpen = true;
e.Handled = true;
}
private void StartAmend(object sender, RoutedEventArgs e) {
var commits = new Commands.Commits(repo.Path, "-n 1", false).Result();
if (commits.Count == 0) {
Models.Exception.Raise("No commits to amend!");
chkAmend.IsChecked = false;
return;
}
txtCommitMessage.Text = commits[0].Subject;
btnCommitAndPush.Visibility = Visibility.Collapsed;
e.Handled = true;
}
private void EndAmend(object sender, RoutedEventArgs e) {
if (!IsLoaded) return;
var current = repo.Branches.Find(x => x.IsCurrent);
if (current != null && !string.IsNullOrEmpty(current.Upstream)) {
btnCommitAndPush.Visibility = Visibility.Visible;
} else {
btnCommitAndPush.Visibility = Visibility.Collapsed;
}
e.Handled = true;
}
private async void Commit(object sender, RoutedEventArgs e) {
var changes = await Task.Run(() => new Commands.LocalChanges(repo.Path).Result());
var conflict = changes.Find(x => x.IsConflit);
if (conflict != null) {
Models.Exception.Raise("You have unsolved conflicts in your working copy!");
return;
}
if (stagedContainer.Changes.Count == 0) {
Models.Exception.Raise("No files added to commit!");
return;
}
txtCommitMessage.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtCommitMessage)) return;
repo.PushCommitMessage(CommitMessage);
iconCommitting.Visibility = Visibility.Visible;
iconCommitting.IsAnimating = true;
Models.Watcher.SetEnabled(repo.Path, false);
var amend = chkAmend.IsChecked == true;
var succ = await Task.Run(() => new Commands.Commit(repo.Path, CommitMessage, amend).Exec());
if (succ) ClearMessage();
iconCommitting.IsAnimating = false;
iconCommitting.Visibility = Visibility.Collapsed;
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
}
private async void CommitAndPush(object sender, RoutedEventArgs e) {
var changes = await Task.Run(() => new Commands.LocalChanges(repo.Path).Result());
var conflict = changes.Find(x => x.IsConflit);
if (conflict != null) {
Models.Exception.Raise("You have unsolved conflicts in your working copy!");
return;
}
if (stagedContainer.Changes.Count == 0) {
Models.Exception.Raise("No files added to commit!");
return;
}
txtCommitMessage.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtCommitMessage)) return;
repo.PushCommitMessage(CommitMessage);
iconCommitting.Visibility = Visibility.Visible;
iconCommitting.IsAnimating = true;
Models.Watcher.SetEnabled(repo.Path, false);
var succ = await Task.Run(() => new Commands.Commit(repo.Path, CommitMessage, false).Exec());
if (succ) {
new Popups.Push(repo, repo.Branches.Find(x => x.IsCurrent)).ShowAndStart();
ClearMessage();
}
iconCommitting.IsAnimating = false;
iconCommitting.Visibility = Visibility.Collapsed;
Models.Watcher.SetEnabled(repo.Path, true);
e.Handled = true;
}
#endregion
}
}

View file

@ -0,0 +1,151 @@
<UserControl x:Class="SourceGit.Views.Widgets.WorkingCopyChanges"
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:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converters:PureFileName x:Key="PureFileName"/>
<converters:PureFolderName x:Key="PureFolderName"/>
<Style x:Key="Style.DataGridRow.Change" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
<EventSetter Event="ContextMenuOpening" Handler="OnDataGridContextMenuOpening"/>
</Style>
</UserControl.Resources>
<Grid>
<controls:Tree
x:Name="modeTree"
FontFamily="Consolas"
MultiSelection="True"
ItemsSource="{Binding ElementName=me, Path=Nodes}"
SelectionChanged="OnTreeSelectionChanged"
Visibility="Visible">
<TreeView.Resources>
<RoutedUICommand x:Key="SelectWholeTreeCommand" Text="SelectWholeTree"/>
</TreeView.Resources>
<TreeView.InputBindings>
<KeyBinding Key="A" Modifiers="Ctrl" Command="{StaticResource SelectWholeTreeCommand}"/>
</TreeView.InputBindings>
<TreeView.CommandBindings>
<CommandBinding Command="{StaticResource SelectWholeTreeCommand}" Executed="SelectWholeTree"/>
</TreeView.CommandBindings>
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="OnTreeContextMenuOpening"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<controls:ChangeStatusIcon
Grid.Column="0"
Width="14" Height="14"
IsLocalChange="{Binding ElementName=me, Path=IsUnstaged}"
Change="{Binding Change}"/>
<Path
Grid.Column="0"
x:Name="IconFolder"
Width="14" Height="14"
Fill="Goldenrod"
Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock
Grid.Column="1"
Text="{Binding Path, Converter={StaticResource PureFileName}}"
Margin="4,0,0,0"
FontSize="11"/>
</Grid>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="False">
<Setter TargetName="IconFolder" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsExpanded}" Value="True">
<Setter TargetName="IconFolder" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
<DataGrid
x:Name="modeList"
RowHeight="24"
SelectionMode="Extended"
SelectionUnit="FullRow"
SelectionChanged="OnListSelectionChanged"
SizeChanged="OnListSizeChanged"
ItemsSource="{Binding ElementName=me, Path=Changes}"
RowStyle="{StaticResource Style.DataGridRow.Change}"
Visibility="Collapsed">
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type models:Change}">
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="{Binding ElementName=me, Path=IsUnstaged}" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True" Width="SizeToCells">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid
x:Name="modeGrid"
RowHeight="24"
SelectionMode="Extended"
SelectionUnit="FullRow"
SelectionChanged="OnGridSelectionChanged"
SizeChanged="OnGridSizeChanged"
ItemsSource="{Binding ElementName=me, Path=Changes}"
RowStyle="{StaticResource Style.DataGridRow.Change}"
Visibility="Collapsed">
<DataGrid.Columns>
<DataGridTemplateColumn Width="22" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:ChangeStatusIcon Width="14" Height="14" IsLocalChange="{Binding ElementName=me, Path=IsUnstaged}" Change="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True" Width="SizeToCells">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="2,0,0,0" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="True" Width="SizeToCells">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock FontFamily="Consolas" Margin="8,0,0,0" Text="{Binding Path, Converter={StaticResource PureFolderName}}" Foreground="{StaticResource Brush.FG2}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View file

@ -0,0 +1,859 @@
using Microsoft.Win32;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 工作区变更
/// </summary>
public partial class WorkingCopyChanges : UserControl {
public static readonly DependencyProperty IsUnstagedProperty = DependencyProperty.Register(
"IsUnstaged",
typeof(bool),
typeof(WorkingCopyChanges),
new PropertyMetadata(false));
public bool IsUnstaged {
get { return (bool)GetValue(IsUnstagedProperty); }
set { SetValue(IsUnstagedProperty, value); }
}
public static readonly DependencyProperty IsStagingProperty = DependencyProperty.Register(
"IsStaging",
typeof(bool),
typeof(WorkingCopyChanges),
new PropertyMetadata(false));
public bool IsStaging {
get { return (bool)GetValue(IsStagingProperty); }
set { SetValue(IsStagingProperty, value); }
}
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
"Mode",
typeof(Models.Change.DisplayMode),
typeof(WorkingCopyChanges),
new PropertyMetadata(Models.Change.DisplayMode.Tree, OnModeChanged));
public Models.Change.DisplayMode Mode {
get { return (Models.Change.DisplayMode)GetValue(ModeProperty); }
set { SetValue(ModeProperty, value); }
}
public static readonly RoutedEvent DiffTargetChangedEvent = EventManager.RegisterRoutedEvent(
"DiffTargetChanged",
RoutingStrategy.Bubble,
typeof(EventHandler<DiffTargetChangedEventArgs>),
typeof(WorkingCopyChanges));
public class DiffTargetChangedEventArgs : RoutedEventArgs {
public Models.Change Target { get; set; }
public bool HasOthers { get; set; }
public DiffTargetChangedEventArgs(RoutedEvent re, object src, Models.Change c, bool hasOthers) : base(re, src) {
Target = c;
HasOthers = hasOthers;
}
}
public event RoutedEventHandler DiffTargetChanged {
add { AddHandler(DiffTargetChangedEvent, value); }
remove { RemoveHandler(DiffTargetChangedEvent, value); }
}
public class ChangeNode {
public string Path { get; set; } = "";
public Models.Change Change { get; set; } = null;
public bool IsExpanded { get; set; } = false;
public bool IsFolder => Change == null;
public ObservableCollection<ChangeNode> Children { get; set; } = new ObservableCollection<ChangeNode>();
}
public ObservableCollection<Models.Change> Changes {
get;
set;
}
public ObservableCollection<ChangeNode> Nodes {
get;
set;
}
public Models.Change DiffTarget {
get;
private set;
}
private string repo = null;
private bool isLoadingData = false;
public WorkingCopyChanges() {
Changes = new ObservableCollection<Models.Change>();
Nodes = new ObservableCollection<ChangeNode>();
DiffTarget = null;
InitializeComponent();
}
#region PUBLIC_METHODS
public void SetRepository(string repo) {
this.repo = repo;
}
public void UnselectAll() {
switch (Mode) {
case Models.Change.DisplayMode.Tree:
modeTree.UnselectAll();
break;
case Models.Change.DisplayMode.List:
modeList.SelectedItems.Clear();
break;
case Models.Change.DisplayMode.Grid:
modeGrid.SelectedItems.Clear();
break;
}
}
public void StageSelected() {
var files = new List<string>();
switch (Mode) {
case Models.Change.DisplayMode.Tree:
var changes = new List<Models.Change>();
foreach (var node in modeTree.Selected) GetChangesFromNode(node as ChangeNode, changes);
foreach (var c in changes) files.Add(c.Path);
break;
case Models.Change.DisplayMode.List:
foreach (var c in modeList.SelectedItems) files.Add((c as Models.Change).Path);
break;
case Models.Change.DisplayMode.Grid:
foreach (var c in modeGrid.SelectedItems) files.Add((c as Models.Change).Path);
break;
}
if (files.Count > 0) DoStage(files);
}
public void StageAll() {
DoStage(null);
}
public void UnstageSelected() {
var files = new List<string>();
switch (Mode) {
case Models.Change.DisplayMode.Tree:
var changes = new List<Models.Change>();
foreach (var node in modeTree.Selected) GetChangesFromNode(node as ChangeNode, changes);
foreach (var c in changes) files.Add(c.Path);
break;
case Models.Change.DisplayMode.List:
foreach (var c in modeList.SelectedItems) files.Add((c as Models.Change).Path);
break;
case Models.Change.DisplayMode.Grid:
foreach (var c in modeGrid.SelectedItems) files.Add((c as Models.Change).Path);
break;
}
if (files.Count > 0) DoUnstage(files);
}
public void UnstageAll() {
DoUnstage(null);
}
public void SetData(List<Models.Change> changes) {
isLoadingData = true;
var oldSet = new Dictionary<string, Models.Change>();
var newSet = new Dictionary<string, Models.Change>();
foreach (var c in changes) newSet.Add(c.Path, c);
for (int i = Changes.Count - 1; i >= 0; i--) {
var old = Changes[i];
if (!newSet.ContainsKey(old.Path)) {
Changes.RemoveAt(i);
RemoveTreeNode(Nodes, old);
continue;
}
var cur = newSet[old.Path];
if (cur.Index != old.Index || cur.WorkTree != old.WorkTree) {
Changes.RemoveAt(i);
RemoveTreeNode(Nodes, old);
continue;
}
oldSet.Add(old.Path, old);
}
var isDefaultExpand = changes.Count <= 50;
foreach (var c in changes) {
if (oldSet.ContainsKey(c.Path)) continue;
bool added = false;
for (int i = 0; i < Changes.Count; i++) {
if (c.Path.CompareTo(Changes[i].Path) < 0) {
Changes.Insert(i, c);
added = true;
break;
}
}
if (!added) Changes.Add(c);
#if NET48
int sepIdx = c.Path.IndexOf("/", StringComparison.Ordinal);
#else
int sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
#endif
if (sepIdx < 0) {
GetOrAddTreeNode(Nodes, c.Path, c, false);
} else {
ObservableCollection<ChangeNode> last = Nodes;
do {
var path = c.Path.Substring(0, sepIdx);
last = GetOrAddTreeNode(last, path, null, isDefaultExpand).Children;
sepIdx = c.Path.IndexOf('/', sepIdx + 1);
} while (sepIdx > 0);
GetOrAddTreeNode(last, c.Path, c, false);
}
}
isLoadingData = false;
}
private ChangeNode GetOrAddTreeNode(ObservableCollection<ChangeNode> nodes, string path, Models.Change change, bool isExpand) {
foreach (var n in nodes) {
if (n.Path == path) return n;
}
var node = new ChangeNode();
node.Path = path;
node.Change = change;
node.IsExpanded = isExpand;
var added = false;
if (change == null) {
for (int i = 0; i < nodes.Count; i++) {
if (!nodes[i].IsFolder || nodes[i].Path.CompareTo(path) > 0) {
added = true;
nodes.Add(node);
break;
}
}
} else {
for (int i = 0; i < nodes.Count; i++) {
if (nodes[i].IsFolder) continue;
if (nodes[i].Path.CompareTo(path) > 0) {
added = true;
nodes.Add(node);
break;
}
}
}
if (!added) nodes.Add(node);
return node;
}
private bool RemoveTreeNode(ObservableCollection<ChangeNode> nodes, Models.Change change) {
for (int i = nodes.Count - 1; i >= 0; i--) {
var node = nodes[i];
if (node.Change == null) {
if (RemoveTreeNode(node.Children, change)) {
if (node.Children.Count == 0) nodes.RemoveAt(i);
return true;
}
} else if (node.Change.Path == change.Path) {
nodes.RemoveAt(i);
return true;
}
}
return false;
}
private void GetChangesFromNode(ChangeNode node, List<Models.Change> changes) {
if (node.Change != null) {
var idx = changes.FindIndex(x => x.Path == node.Change.Path);
if (idx < 0) changes.Add(node.Change);
} else {
foreach (var sub in node.Children) GetChangesFromNode(sub, changes);
}
}
#endregion
#region UNSTAGED
private async void DoStage(List<string> files) {
IsStaging = true;
Models.Watcher.SetEnabled(repo, false);
if (files == null || files.Count == 0) {
await Task.Run(() => new Commands.Add(repo).Exec());
} else {
for (int i = 0; i < files.Count; i += 10) {
var maxCount = Math.Min(10, files.Count - i);
var step = files.GetRange(i, maxCount);
await Task.Run(() => new Commands.Add(repo, step).Exec());
}
}
Models.Watcher.SetEnabled(repo, true);
Models.Watcher.Get(repo)?.RefreshWC();
IsStaging = false;
}
private async void SaveAsPatch(string saveTo, List<Models.Change> changes) {
FileStream stream = new FileStream(saveTo, FileMode.Create);
StreamWriter writer = new StreamWriter(stream);
foreach (var c in changes) {
await Task.Run(() => new Commands.SaveChangeToStream(repo, c, writer).Exec());
}
writer.Flush();
stream.Flush();
writer.Close();
stream.Close();
}
private void OpenUnstagedContextMenuByNodes(List<ChangeNode> nodes, List<Models.Change> changes) {
var files = new List<string>();
foreach (var c in changes) files.Add(c.Path);
var menu = new ContextMenu();
if (nodes.Count == 1) {
var node = nodes[0];
var path = Path.GetFullPath(Path.Combine(repo, node.Path));
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Click += (o, e) => {
if (node.IsFolder) Process.Start("explorer", path);
else Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
var stage = new MenuItem();
stage.Header = App.Text("FileCM.Stage");
stage.Click += (o, e) => {
DoStage(files);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.Discard");
discard.Click += (o, e) => {
new Popups.Discard(repo, changes).Show();
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Click += (o, e) => {
new Popups.Stash(repo, files).Show();
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Click += (o, e) => {
var dialog = new SaveFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = App.Text("FileCM.SaveAsPatch");
dialog.InitialDirectory = repo;
if (dialog.ShowDialog() == true) {
SaveAsPatch(dialog.FileName, changes);
}
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (o, e) => {
Clipboard.SetText(node.Path);
e.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(new Separator());
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
menu.Items.Add(new Separator());
if (node.Change != null) {
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (o, e) => {
var viewer = new Views.Histories(repo, node.Path);
viewer.Show();
e.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(new Separator());
}
menu.Items.Add(copyPath);
} else {
var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageMulti", changes.Count);
stage.Click += (o, e) => {
DoStage(files);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
discard.Click += (o, e) => {
new Popups.Discard(repo, changes).Show();
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
stash.Click += (o, e) => {
new Popups.Stash(repo, files).Show();
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Click += (o, e) => {
var dialog = new SaveFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = App.Text("FileCM.SaveAsPatch");
dialog.InitialDirectory = repo;
if (dialog.ShowDialog() == true) {
SaveAsPatch(dialog.FileName, changes);
}
e.Handled = true;
};
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
}
menu.IsOpen = true;
}
private void OpenUnstagedContextMenuByChanges(List<Models.Change> changes) {
var files = new List<string>();
foreach (var c in changes) files.Add(c.Path);
var menu = new ContextMenu();
if (changes.Count == 1) {
var change = changes[0];
var path = Path.GetFullPath(Path.Combine(repo, change.Path));
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Click += (o, e) => {
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
var stage = new MenuItem();
stage.Header = App.Text("FileCM.Stage");
stage.Click += (o, e) => {
DoStage(files);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.Discard");
discard.Click += (o, e) => {
new Popups.Discard(repo, changes).Show();
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Click += (o, e) => {
new Popups.Stash(repo, files).Show();
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Click += (o, e) => {
var dialog = new SaveFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = App.Text("FileCM.SaveAsPatch");
dialog.InitialDirectory = repo;
if (dialog.ShowDialog() == true) {
SaveAsPatch(dialog.FileName, changes);
}
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (o, e) => {
Clipboard.SetText(change.Path);
e.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(new Separator());
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
menu.Items.Add(new Separator());
if (change != null) {
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (o, e) => {
var viewer = new Views.Histories(repo, change.Path);
viewer.Show();
e.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(new Separator());
}
menu.Items.Add(copyPath);
} else {
var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageMulti", changes.Count);
stage.Click += (o, e) => {
DoStage(files);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
discard.Click += (o, e) => {
new Popups.Discard(repo, changes).Show();
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
stash.Click += (o, e) => {
new Popups.Stash(repo, files).Show();
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Click += (o, e) => {
var dialog = new SaveFileDialog();
dialog.Filter = "Patch File|*.patch";
dialog.Title = App.Text("FileCM.SaveAsPatch");
dialog.InitialDirectory = repo;
if (dialog.ShowDialog() == true) {
SaveAsPatch(dialog.FileName, changes);
}
e.Handled = true;
};
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
}
menu.IsOpen = true;
}
#endregion
#region STAGED
private async void DoUnstage(List<string> files) {
Models.Watcher.SetEnabled(repo, false);
if (files == null || files.Count == 0) {
await Task.Run(() => new Commands.Reset(repo).Exec());
} else {
for (int i = 0; i < files.Count; i += 10) {
var maxCount = Math.Min(10, files.Count - i);
var step = files.GetRange(i, maxCount);
await Task.Run(() => new Commands.Reset(repo, step).Exec());
}
}
Models.Watcher.SetEnabled(repo, true);
Models.Watcher.Get(repo)?.RefreshWC();
}
private void OpenStagedContextMenuByNodes(List<ChangeNode> nodes, List<Models.Change> changes) {
var files = new List<string>();
foreach (var c in changes) files.Add(c.Path);
var menu = new ContextMenu();
if (nodes.Count == 1) {
var node = nodes[0];
var path = Path.GetFullPath(Path.Combine(repo, node.Path));
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Click += (o, e) => {
if (node.IsFolder) Process.Start(path);
else Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.Unstage");
unstage.Click += (o, e) => {
DoUnstage(files);
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (o, e) => {
Clipboard.SetText(node.Path);
e.Handled = true;
};
} else {
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.UnstageMulti", files.Count);
unstage.Click += (o, e) => {
DoUnstage(files);
e.Handled = true;
};
menu.Items.Add(unstage);
}
menu.IsOpen = true;
}
private void OpenStagedContextMenuByChanges(List<Models.Change> changes) {
var files = new List<string>();
foreach (var c in changes) files.Add(c.Path);
var menu = new ContextMenu();
if (changes.Count == 1) {
var change = changes[0];
var path = Path.GetFullPath(Path.Combine(repo, change.Path));
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Click += (o, e) => {
Process.Start("explorer", $"/select,{path}");
e.Handled = true;
};
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.Unstage");
unstage.Click += (o, e) => {
DoUnstage(files);
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (o, e) => {
Clipboard.SetText(change.Path);
e.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(new Separator());
menu.Items.Add(unstage);
menu.Items.Add(new Separator());
menu.Items.Add(copyPath);
} else {
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.UnstageMulti", files.Count);
unstage.Click += (o, e) => {
DoUnstage(files);
e.Handled = true;
};
menu.Items.Add(unstage);
}
menu.IsOpen = true;
}
#endregion
#region EVENTS
private void SelectWholeTree(object sender, ExecutedRoutedEventArgs e) {
modeTree.SelectAll();
}
private void OnTreeSelectionChanged(object sender, RoutedEventArgs e) {
if (Mode != Models.Change.DisplayMode.Tree) return;
bool hasOthers = false;
if (modeTree.Selected.Count == 0) {
DiffTarget = null;
} else if (modeTree.Selected.Count == 1) {
var node = modeTree.Selected[0] as ChangeNode;
if (node.IsFolder) {
if (DiffTarget == null) return;
DiffTarget = null;
hasOthers = true;
} else {
DiffTarget = node.Change;
}
} else {
if (DiffTarget == null) return;
DiffTarget = null;
hasOthers = true;
}
if (!isLoadingData) RaiseEvent(new DiffTargetChangedEventArgs(DiffTargetChangedEvent, this, DiffTarget, hasOthers));
}
private void OnListSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Mode != Models.Change.DisplayMode.List) return;
bool hasOthers = false;
switch (modeList.SelectedItems.Count) {
case 0:
DiffTarget = null;
break;
case 1:
DiffTarget = modeList.SelectedItems[0] as Models.Change;
break;
default:
DiffTarget = null;
hasOthers = true;
break;
}
if (!isLoadingData) RaiseEvent(new DiffTargetChangedEventArgs(DiffTargetChangedEvent, this, DiffTarget, hasOthers));
}
private void OnGridSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Mode != Models.Change.DisplayMode.Grid) return;
bool hasOthers = false;
switch (modeGrid.SelectedItems.Count) {
case 0:
DiffTarget = null;
break;
case 1:
DiffTarget = modeGrid.SelectedItems[0] as Models.Change;
break;
default:
DiffTarget = null;
hasOthers = true;
break;
}
if (!isLoadingData) RaiseEvent(new DiffTargetChangedEventArgs(DiffTargetChangedEvent, this, DiffTarget, hasOthers));
}
private void OnTreeContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var nodes = new List<ChangeNode>();
var changes = new List<Models.Change>();
foreach (var o in modeTree.Selected) {
nodes.Add(o as ChangeNode);
GetChangesFromNode(o as ChangeNode, changes);
}
if (IsUnstaged) {
OpenUnstagedContextMenuByNodes(nodes, changes);
} else {
OpenStagedContextMenuByNodes(nodes, changes);
}
ev.Handled = true;
}
private void OnDataGridContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var row = sender as DataGridRow;
if (row == null) return;
var changes = new List<Models.Change>();
if (Mode == Models.Change.DisplayMode.List) {
if (!row.IsSelected) {
modeList.SelectedItems.Clear();
modeList.SelectedItems.Add(row.DataContext);
changes.Add(row.DataContext as Models.Change);
} else {
foreach (var c in modeList.SelectedItems) changes.Add(c as Models.Change);
}
} else {
if (!row.IsSelected) {
modeGrid.SelectedItems.Clear();
modeGrid.SelectedItems.Add(row.DataContext);
changes.Add(row.DataContext as Models.Change);
} else {
foreach (var c in modeGrid.SelectedItems) changes.Add(c as Models.Change);
}
}
if (IsUnstaged) {
OpenUnstagedContextMenuByChanges(changes);
} else {
OpenStagedContextMenuByChanges(changes);
}
ev.Handled = true;
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void OnListSizeChanged(object sender, SizeChangedEventArgs e) {
if (Mode != Models.Change.DisplayMode.List) return;
int last = modeList.Columns.Count - 1;
double offset = 0;
for (int i = 0; i < last; i++) offset += modeList.Columns[i].ActualWidth;
modeList.Columns[last].MinWidth = Math.Max(modeList.ActualWidth - offset, 10);
modeList.UpdateLayout();
}
private void OnGridSizeChanged(object sender, SizeChangedEventArgs e) {
if (Mode != Models.Change.DisplayMode.Grid) return;
int last = modeGrid.Columns.Count - 1;
double offset = 0;
for (int i = 0; i < last; i++) offset += modeGrid.Columns[i].ActualWidth;
modeGrid.Columns[last].MinWidth = Math.Max(modeGrid.ActualWidth - offset, 10);
modeGrid.UpdateLayout();
}
private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var elem = d as WorkingCopyChanges;
if (elem != null) {
if (elem.modeTree != null) {
if (elem.Mode == Models.Change.DisplayMode.Tree) {
elem.modeTree.Visibility = Visibility.Visible;
} else {
elem.modeTree.Visibility = Visibility.Collapsed;
}
}
if (elem.modeList != null) {
if (elem.Mode == Models.Change.DisplayMode.List) {
elem.modeList.Visibility = Visibility.Visible;
} else {
elem.modeList.Visibility = Visibility.Collapsed;
}
}
if (elem.modeGrid != null) {
if (elem.Mode == Models.Change.DisplayMode.Grid) {
elem.modeGrid.Visibility = Visibility.Visible;
} else {
elem.modeGrid.Visibility = Visibility.Collapsed;
}
}
}
}
#endregion
}
}