mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-30 08:34:59 +00:00
refactor<*>: rewrite all codes...
This commit is contained in:
parent
89ff8aa744
commit
30ab8ae954
342 changed files with 17208 additions and 19633 deletions
196
src/Views/Widgets/CommitChanges.xaml
Normal file
196
src/Views/Widgets/CommitChanges.xaml
Normal 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>
|
310
src/Views/Widgets/CommitChanges.xaml.cs
Normal file
310
src/Views/Widgets/CommitChanges.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
344
src/Views/Widgets/CommitDetail.xaml
Normal file
344
src/Views/Widgets/CommitDetail.xaml
Normal 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>
|
416
src/Views/Widgets/CommitDetail.xaml.cs
Normal file
416
src/Views/Widgets/CommitDetail.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
478
src/Views/Widgets/Dashboard.xaml
Normal file
478
src/Views/Widgets/Dashboard.xaml
Normal 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>
|
982
src/Views/Widgets/Dashboard.xaml.cs
Normal file
982
src/Views/Widgets/Dashboard.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
139
src/Views/Widgets/DiffViewer.xaml
Normal file
139
src/Views/Widgets/DiffViewer.xaml
Normal 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>
|
621
src/Views/Widgets/DiffViewer.xaml.cs
Normal file
621
src/Views/Widgets/DiffViewer.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
45
src/Views/Widgets/Exceptions.xaml
Normal file
45
src/Views/Widgets/Exceptions.xaml
Normal 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>
|
24
src/Views/Widgets/Exceptions.xaml.cs
Normal file
24
src/Views/Widgets/Exceptions.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
212
src/Views/Widgets/Histories.xaml
Normal file
212
src/Views/Widgets/Histories.xaml
Normal 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>
|
599
src/Views/Widgets/Histories.xaml.cs
Normal file
599
src/Views/Widgets/Histories.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
116
src/Views/Widgets/PageTabBar.xaml
Normal file
116
src/Views/Widgets/PageTabBar.xaml
Normal 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>
|
197
src/Views/Widgets/PageTabBar.xaml.cs
Normal file
197
src/Views/Widgets/PageTabBar.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
31
src/Views/Widgets/PageTabItem.xaml
Normal file
31
src/Views/Widgets/PageTabItem.xaml
Normal 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>
|
78
src/Views/Widgets/PageTabItem.xaml.cs
Normal file
78
src/Views/Widgets/PageTabItem.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
46
src/Views/Widgets/PopupPanel.xaml
Normal file
46
src/Views/Widgets/PopupPanel.xaml
Normal 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>
|
107
src/Views/Widgets/PopupPanel.xaml.cs
Normal file
107
src/Views/Widgets/PopupPanel.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
107
src/Views/Widgets/RevisionCompare.xaml
Normal file
107
src/Views/Widgets/RevisionCompare.xaml
Normal 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>
|
36
src/Views/Widgets/RevisionCompare.xaml.cs
Normal file
36
src/Views/Widgets/RevisionCompare.xaml.cs
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
129
src/Views/Widgets/Stashes.xaml
Normal file
129
src/Views/Widgets/Stashes.xaml
Normal 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>
|
85
src/Views/Widgets/Stashes.xaml.cs
Normal file
85
src/Views/Widgets/Stashes.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
186
src/Views/Widgets/Welcome.xaml
Normal file
186
src/Views/Widgets/Welcome.xaml
Normal 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>
|
386
src/Views/Widgets/Welcome.xaml.cs
Normal file
386
src/Views/Widgets/Welcome.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
249
src/Views/Widgets/WorkingCopy.xaml
Normal file
249
src/Views/Widgets/WorkingCopy.xaml
Normal 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>
|
340
src/Views/Widgets/WorkingCopy.xaml.cs
Normal file
340
src/Views/Widgets/WorkingCopy.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
151
src/Views/Widgets/WorkingCopyChanges.xaml
Normal file
151
src/Views/Widgets/WorkingCopyChanges.xaml
Normal 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>
|
859
src/Views/Widgets/WorkingCopyChanges.xaml.cs
Normal file
859
src/Views/Widgets/WorkingCopyChanges.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue