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