diff --git a/.gitignore b/.gitignore index ed1d7c43..0b5677c5 100644 --- a/.gitignore +++ b/.gitignore @@ -604,3 +604,4 @@ build/*.zip build/*.tar.gz build/*.deb build/*.rpm +build/*.AppImage diff --git a/README.md b/README.md index f79c774b..2ea6ebec 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ You can download the latest stable from [Releases](https://github.com/sourcegit- This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs. -| OS | PATH | -| --- | --- | -| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` | -| Linux | `${HOME}/.config/SourceGit` | -| macOS | `${HOME}/Library/Application Support/SourceGit` | +| OS | PATH | +|---------|-------------------------------------------------| +| Windows | `C:\Users\USER_NAME\AppData\Roaming\SourceGit` | +| Linux | `${HOME}/.config/SourceGit` | +| macOS | `${HOME}/Library/Application Support/SourceGit` | For **Windows** users: @@ -74,13 +74,13 @@ For **Linux** users: This app supports open repository in external tools listed in the table below. -| Tool | Windows | macOS | Linux | Environment Variable | -| --- | --- | --- | --- | --- | -| Visual Studio Code | YES | YES | YES | VSCODE_PATH | -| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS_PATH | -| VSCodium | YES | YES | YES | VSCODIUM_PATH | -| JetBrains Fleet | YES | YES | YES | FLEET_PATH | -| Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH | +| Tool | Windows | macOS | Linux | Environment Variable | +|-------------------------------|---------|-------|-------|----------------------| +| Visual Studio Code | YES | YES | YES | VSCODE_PATH | +| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS_PATH | +| VSCodium | YES | YES | YES | VSCODIUM_PATH | +| JetBrains Fleet | YES | YES | YES | FLEET_PATH | +| Sublime Text | YES | YES | YES | SUBLIME_TEXT_PATH | * You can set the given environment variable for special tool if it can NOT be found by this app automatically. * Installing `JetBrains Toolbox` will help this app to find other JetBrains tools installed on your device. diff --git a/VERSION b/VERSION index d8101a48..3a580d88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.20 \ No newline at end of file +8.21 \ No newline at end of file diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 901a9b5b..ffc74262 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -1,13 +1,67 @@ -using System.Collections.Generic; +using System; +using System.Text.Json; using System.Text.Json.Serialization; +using Avalonia.Controls; +using Avalonia.Media; + namespace SourceGit { - [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)] - [JsonSerializable(typeof(List))] + public class ColorConverter : JsonConverter + { + public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Color.Parse(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } + + public class FontFamilyConverter : JsonConverter + { + public override FontFamily Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var name = reader.GetString(); + return new FontFamily(name); + } + + public override void Write(Utf8JsonWriter writer, FontFamily value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } + + public class GridLengthConverter : JsonConverter + { + public override GridLength Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var size = reader.GetDouble(); + return new GridLength(size, GridUnitType.Pixel); + } + + public override void Write(Utf8JsonWriter writer, GridLength value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.Value); + } + } + + [JsonSourceGenerationOptions( + WriteIndented = true, + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + Converters = [ + typeof(ColorConverter), + typeof(FontFamilyConverter), + typeof(GridLengthConverter), + ] + )] + [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] - [JsonSerializable(typeof(Models.CustomColorSchema))] [JsonSerializable(typeof(ViewModels.Preference))] [JsonSerializable(typeof(ViewModels.RepositorySettings))] internal partial class JsonCodeGen : JsonSerializerContext { } diff --git a/src/App.axaml b/src/App.axaml index d73ea45d..6e485f3a 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -20,7 +20,6 @@ - diff --git a/src/App.axaml.cs b/src/App.axaml.cs index f989a325..67bd3aad 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -44,34 +44,29 @@ namespace SourceGit [STAThread] public static void Main(string[] args) { + AppDomain.CurrentDomain.UnhandledException += (_, e) => + { + LogException(e.ExceptionObject as Exception); + }; + + TaskScheduler.UnobservedTaskException += (_, e) => + { + LogException(e.Exception); + e.SetObserved(); + }; + try { - if (args.Length > 1 && args[0].Equals("--rebase-editor", StringComparison.Ordinal)) - Environment.Exit(Models.InteractiveRebaseEditor.Process(args[1])); + if (TryLaunchedAsRebaseTodoEditor(args, out int exitTodo)) + Environment.Exit(exitTodo); + else if (TryLaunchedAsRebaseMessageEditor(args, out int exitMessage)) + Environment.Exit(exitMessage); else BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception ex) { - var builder = new StringBuilder(); - builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); - builder.Append("----------------------------\n"); - builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); - builder.Append($"OS: {Environment.OSVersion.ToString()}\n"); - builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); - builder.Append($"Source: {ex.Source}\n"); - builder.Append($"---------------------------\n\n"); - builder.Append(ex.StackTrace); - while (ex.InnerException != null) - { - ex = ex.InnerException; - builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n"); - builder.Append(ex.StackTrace); - } - - var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); - File.WriteAllText(file, builder.ToString()); + LogException(ex); } } @@ -94,20 +89,32 @@ namespace SourceGit public static readonly SimpleCommand OpenPreferenceCommand = new SimpleCommand(() => { + var toplevel = GetTopLevel() as Window; + if (toplevel == null) + return; + var dialog = new Views.Preference(); - dialog.ShowDialog(GetTopLevel() as Window); + dialog.ShowDialog(toplevel); }); public static readonly SimpleCommand OpenHotkeysCommand = new SimpleCommand(() => { + var toplevel = GetTopLevel() as Window; + if (toplevel == null) + return; + var dialog = new Views.Hotkeys(); - dialog.ShowDialog(GetTopLevel() as Window); + dialog.ShowDialog(toplevel); }); public static readonly SimpleCommand OpenAboutCommand = new SimpleCommand(() => { + var toplevel = GetTopLevel() as Window; + if (toplevel == null) + return; + var dialog = new Views.About(); - dialog.ShowDialog(GetTopLevel() as Window); + dialog.ShowDialog(toplevel); }); public static readonly SimpleCommand CheckForUpdateCommand = new SimpleCommand(() => @@ -115,7 +122,7 @@ namespace SourceGit Check4Update(true); }); - public static readonly SimpleCommand QuitCommand = new SimpleCommand(Quit); + public static readonly SimpleCommand QuitCommand = new SimpleCommand(() => Quit(0)); public static void RaiseException(string context, string message) { @@ -132,7 +139,7 @@ namespace SourceGit public static void SetLocale(string localeKey) { var app = Current as App; - var targetLocale = app.Resources[localeKey] as ResourceDictionary; + var targetLocale = app?.Resources[localeKey] as ResourceDictionary; if (targetLocale == null || targetLocale == app._activeLocale) return; @@ -143,9 +150,11 @@ namespace SourceGit app._activeLocale = targetLocale; } - public static void SetTheme(string theme, string colorsFile) + public static void SetTheme(string theme, string themeOverridesFile) { var app = Current as App; + if (app == null) + return; if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) app.RequestedThemeVariant = ThemeVariant.Light; @@ -154,63 +163,59 @@ namespace SourceGit else app.RequestedThemeVariant = ThemeVariant.Default; - if (app._colorOverrides != null) + if (app._themeOverrides != null) { - app.Resources.MergedDictionaries.Remove(app._colorOverrides); - app._colorOverrides = null; + app.Resources.MergedDictionaries.Remove(app._themeOverrides); + app._themeOverrides = null; } - Models.CommitGraph.SetDefaultPens(); - - if (!string.IsNullOrEmpty(colorsFile) && File.Exists(colorsFile)) + if (!string.IsNullOrEmpty(themeOverridesFile) && File.Exists(themeOverridesFile)) { try { var resDic = new ResourceDictionary(); - - var schema = JsonSerializer.Deserialize(File.ReadAllText(colorsFile), JsonCodeGen.Default.CustomColorSchema); - foreach (var kv in schema.Basic) + var overrides = JsonSerializer.Deserialize(File.ReadAllText(themeOverridesFile), JsonCodeGen.Default.ThemeOverrides); + foreach (var kv in overrides.BasicColors) { if (kv.Key.Equals("SystemAccentColor", StringComparison.Ordinal)) - resDic["SystemAccentColor"] = Color.Parse(kv.Value); + resDic["SystemAccentColor"] = kv.Value; else - resDic[$"Color.{kv.Key}"] = Color.Parse(kv.Value); + resDic[$"Color.{kv.Key}"] = kv.Value; } - - if (schema.Graph.Count > 0) - { - var penColors = new List(); - - foreach (var c in schema.Graph) - penColors.Add(Color.Parse(c)); - - Models.CommitGraph.SetPenColors(penColors); - } + if (overrides.GraphColors.Count > 0) + Models.CommitGraph.SetPens(overrides.GraphColors, overrides.GraphPenThickness); + else + Models.CommitGraph.SetDefaultPens(overrides.GraphPenThickness); app.Resources.MergedDictionaries.Add(resDic); - app._colorOverrides = resDic; + app._themeOverrides = resDic; } catch { + // ignore } } + else + { + Models.CommitGraph.SetDefaultPens(); + } } public static async void CopyText(string data) { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - if (desktop.MainWindow.Clipboard is { } clipbord) + if (desktop.MainWindow?.Clipboard is { } clipbord) await clipbord.SetTextAsync(data); } } public static async Task GetClipboardTextAsync() { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - if (desktop.MainWindow.Clipboard is { } clipboard) + if (desktop.MainWindow?.Clipboard is { } clipboard) { return await clipboard.GetTextAsync(); } @@ -220,7 +225,7 @@ namespace SourceGit public static string Text(string key, params object[] args) { - var fmt = Current.FindResource($"Text.{key}") as string; + var fmt = Current?.FindResource($"Text.{key}") as string; if (string.IsNullOrWhiteSpace(fmt)) return $"Text.{key}"; @@ -236,16 +241,21 @@ namespace SourceGit icon.Width = 12; icon.Height = 12; icon.Stretch = Stretch.Uniform; - icon.Data = Current.FindResource(key) as StreamGeometry; + + var geo = Current?.FindResource(key) as StreamGeometry; + if (geo != null) + icon.Data = geo; + return icon; } public static TopLevel GetTopLevel() { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { return desktop.MainWindow; } + return null; } @@ -305,12 +315,16 @@ namespace SourceGit return null; } - public static void Quit() + public static void Quit(int exitCode) { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow.Close(); - desktop.Shutdown(); + desktop.MainWindow?.Close(); + desktop.Shutdown(exitCode); + } + else + { + Environment.Exit(exitCode); } } @@ -321,7 +335,7 @@ namespace SourceGit var pref = ViewModels.Preference.Instance; SetLocale(pref.Locale); - SetTheme(pref.Theme, pref.ColorOverrides); + SetTheme(pref.Theme, pref.ThemeOverrides); } public override void OnFrameworkInitializationCompleted() @@ -329,34 +343,52 @@ namespace SourceGit if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { BindingPlugins.DataValidators.RemoveAt(0); - Native.OS.SetupEnternalTools(); - _launcher = new ViewModels.Launcher(); - desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + if (TryLaunchedAsCoreEditor(desktop)) + return; - var pref = ViewModels.Preference.Instance; - if (pref.ShouldCheck4UpdateOnStartup) - { - pref.Save(); - Check4Update(); - } + if (TryLaunchedAsAskpass(desktop)) + return; + + TryLaunchedAsNormal(desktop); + } + } + + private static void LogException(Exception ex) + { + if (ex == null) + return; + + var builder = new StringBuilder(); + builder.Append($"Crash::: {ex.GetType().FullName}: {ex.Message}\n\n"); + builder.Append("----------------------------\n"); + builder.Append($"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n"); + builder.Append($"OS: {Environment.OSVersion.ToString()}\n"); + builder.Append($"Framework: {AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\n"); + builder.Append($"Source: {ex.Source}\n"); + builder.Append($"---------------------------\n\n"); + builder.Append(ex.StackTrace); + while (ex.InnerException != null) + { + ex = ex.InnerException; + builder.Append($"\n\nInnerException::: {ex.GetType().FullName}: {ex.Message}\n"); + builder.Append(ex.StackTrace); } - base.OnFrameworkInitializationCompleted(); + var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + var file = Path.Combine(Native.OS.DataDir, $"crash_{time}.log"); + File.WriteAllText(file, builder.ToString()); } private static void ShowSelfUpdateResult(object data) { Dispatcher.UIThread.Post(() => { - if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: not null } desktop) { var dialog = new Views.SelfUpdate() { - DataContext = new ViewModels.SelfUpdate - { - Data = data - } + DataContext = new ViewModels.SelfUpdate() { Data = data } }; dialog.Show(desktop.MainWindow); @@ -364,8 +396,137 @@ namespace SourceGit }); } + private static bool TryLaunchedAsRebaseTodoEditor(string[] args, out int exitCode) + { + exitCode = -1; + + if (args.Length <= 1 || !args[0].Equals("--rebase-todo-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + var filename = Path.GetFileName(file); + if (!filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) + return true; + + var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)!); + if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) + return true; + + var jobsFile = Path.Combine(dirInfo.Parent!.FullName, "sourcegit_rebase_jobs.json"); + if (!File.Exists(jobsFile)) + return true; + + var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + var lines = new List(); + foreach (var job in collection.Jobs) + { + switch (job.Action) + { + case Models.InteractiveRebaseAction.Pick: + lines.Add($"p {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Edit: + lines.Add($"e {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Reword: + lines.Add($"r {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Squash: + lines.Add($"s {job.SHA}"); + break; + case Models.InteractiveRebaseAction.Fixup: + lines.Add($"f {job.SHA}"); + break; + default: + lines.Add($"d {job.SHA}"); + break; + } + } + + File.WriteAllLines(file, lines); + + exitCode = 0; + return true; + } + + private static bool TryLaunchedAsRebaseMessageEditor(string[] args, out int exitCode) + { + exitCode = -1; + + if (args.Length <= 1 || !args[0].Equals("--rebase-message-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + var filename = Path.GetFileName(file); + if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) + return true; + + var jobsFile = Path.Combine(Path.GetDirectoryName(file)!, "sourcegit_rebase_jobs.json"); + if (!File.Exists(jobsFile)) + return true; + + var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection); + var doneFile = Path.Combine(Path.GetDirectoryName(file)!, "rebase-merge", "done"); + if (!File.Exists(doneFile)) + return true; + + var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + if (done.Length > collection.Jobs.Count) + return true; + + var job = collection.Jobs[done.Length - 1]; + File.WriteAllText(file, job.Message); + + exitCode = 0; + return true; + } + + private bool TryLaunchedAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal)) + return false; + + var file = args[1]; + if (!File.Exists(file)) + desktop.Shutdown(-1); + else + desktop.MainWindow = new Views.StandaloneCommitMessageEditor(file); + + return true; + } + + private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) + { + var args = desktop.Args; + if (args == null || args.Length != 1 || !args[0].StartsWith("Enter passphrase", StringComparison.Ordinal)) + return false; + + desktop.MainWindow = new Views.Askpass(args[0]); + return true; + } + + private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop) + { + Native.OS.SetupEnternalTools(); + + string startupRepo = null; + if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) + startupRepo = desktop.Args[0]; + + _launcher = new ViewModels.Launcher(startupRepo); + desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; + + var pref = ViewModels.Preference.Instance; + if (pref.ShouldCheck4UpdateOnStartup) + { + pref.Save(); + Check4Update(); + } + } + private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; - private ResourceDictionary _colorOverrides = null; + private ResourceDictionary _themeOverrides = null; } } diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 660a5daa..890b54ee 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -25,14 +25,12 @@ var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; + if (string.IsNullOrEmpty(upstream)) - { cmd.Args = $"branch {name} --unset-upstream"; - } else - { cmd.Args = $"branch {name} -u {upstream}"; - } + return cmd.Exec(); } @@ -50,18 +48,8 @@ var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - cmd.Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - cmd.Args = "-c credential.helper=manager "; - } - - cmd.Args += $"push {remote} --delete {name}"; + cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + cmd.Args = $"push {remote} --delete {name}"; return cmd.Exec(); } } diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index 80e0df50..683b8846 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -11,21 +11,14 @@ namespace SourceGit.Commands Context = ctx; WorkingDirectory = path; TraitErrorAsOutput = true; - - if (string.IsNullOrEmpty(sshKey)) - { - Args = "-c credential.helper=manager "; - } - else - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - - Args += "clone --progress --verbose --recurse-submodules "; + SSHKey = sshKey; + Args = "clone --progress --verbose --recurse-submodules "; if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} "; + Args += $"{url} "; + if (!string.IsNullOrEmpty(localName)) Args += localName; diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 00f36c85..48ef1825 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -19,12 +19,20 @@ namespace SourceGit.Commands { public bool IsSuccess { get; set; } public string StdOut { get; set; } - public string StdErr { get; set; } + } + + public enum EditorType + { + None, + CoreEditor, + RebaseEditor, } public string Context { get; set; } = string.Empty; public CancelToken Cancel { get; set; } = null; public string WorkingDirectory { get; set; } = null; + public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode + public string SSHKey { get; set; } = string.Empty; public string Args { get; set; } = string.Empty; public bool RaiseError { get; set; } = true; public bool TraitErrorAsOutput { get; set; } = false; @@ -33,7 +41,7 @@ namespace SourceGit.Commands { var start = new ProcessStartInfo(); start.FileName = Native.OS.GitExecutable; - start.Arguments = "--no-pager -c core.quotepath=off " + Args; + start.Arguments = "--no-pager -c core.quotepath=off "; start.UseShellExecute = false; start.CreateNoWindow = true; start.RedirectStandardOutput = true; @@ -41,10 +49,41 @@ namespace SourceGit.Commands start.StandardOutputEncoding = Encoding.UTF8; start.StandardErrorEncoding = Encoding.UTF8; + // Force using this app as SSH askpass program + var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; + if (!OperatingSystem.IsLinux()) + start.Environment.Add("DISPLAY", "required"); + start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` + start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); + + // If an SSH private key was provided, sets the environment. + if (!string.IsNullOrEmpty(SSHKey)) + start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}'"); + else + start.Arguments += "-c credential.helper=manager "; + // Force using en_US.UTF-8 locale to avoid GCM crash if (OperatingSystem.IsLinux()) start.Environment.Add("LANG", "en_US.UTF-8"); + // Force using this app as git editor. + switch (Editor) + { + case EditorType.CoreEditor: + start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" "; + break; + case EditorType.RebaseEditor: + start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true "; + break; + default: + start.Arguments += "-c core.editor=true "; + break; + } + + // Append command args + start.Arguments += Args; + + // Working directory if (!string.IsNullOrEmpty(WorkingDirectory)) start.WorkingDirectory = WorkingDirectory; @@ -94,7 +133,7 @@ namespace SourceGit.Commands return; if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal)) return; - if (_progressRegex().IsMatch(e.Data)) + if (REG_PROGRESS().IsMatch(e.Data)) return; errs.Add(e.Data); }; @@ -159,20 +198,18 @@ namespace SourceGit.Commands { proc.Start(); } - catch (Exception e) + catch { return new ReadToEndResult() { IsSuccess = false, StdOut = string.Empty, - StdErr = e.Message, }; } var rs = new ReadToEndResult() { StdOut = proc.StandardOutput.ReadToEnd(), - StdErr = proc.StandardError.ReadToEnd(), }; proc.WaitForExit(); @@ -185,6 +222,6 @@ namespace SourceGit.Commands protected virtual void OnReadline(string line) { } [GeneratedRegex(@"\d+%")] - private static partial Regex _progressRegex(); + private static partial Regex REG_PROGRESS(); } } diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs index 492d00c7..6232fc17 100644 --- a/src/Commands/Commit.cs +++ b/src/Commands/Commit.cs @@ -11,6 +11,7 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; + TraitErrorAsOutput = true; Args = $"commit --file=\"{file}\""; if (autoStage) Args += " --all"; diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index a9efb36c..860cd34a 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; namespace SourceGit.Commands @@ -18,7 +19,7 @@ namespace SourceGit.Commands public List Result() { Exec(); - _changes.Sort((l, r) => l.Path.CompareTo(r.Path)); + _changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal)); return _changes; } diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index e1016f1e..67c17ce9 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -23,7 +23,7 @@ namespace SourceGit.Commands var rs = new Dictionary(); if (output.IsSuccess) { - var lines = output.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + var lines = output.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var idx = line.IndexOf('=', StringComparison.Ordinal); @@ -31,14 +31,7 @@ namespace SourceGit.Commands { var key = line.Substring(0, idx).Trim(); var val = line.Substring(idx + 1).Trim(); - if (rs.ContainsKey(key)) - { - rs[key] = val; - } - else - { - rs.Add(key, val); - } + rs[key] = val; } } } diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index ca1d83d6..df5a1c2b 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -13,18 +13,9 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; TraitErrorAsOutput = true; + SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + Args = "fetch --progress --verbose "; - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += "fetch --progress --verbose "; if (prune) Args += "--prune "; @@ -44,18 +35,8 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; TraitErrorAsOutput = true; - - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; + SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + Args = $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}"; } protected override void OnReadline(string line) @@ -144,14 +125,7 @@ namespace SourceGit.Commands lock (_lock) { - if (_jobs.ContainsKey(repo)) - { - _jobs[repo] = job; - } - else - { - _jobs.Add(repo, job); - } + _jobs[repo] = job; } } diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index b115844d..5e05ed83 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -64,7 +64,7 @@ namespace SourceGit.Commands return init.Exec(); } - public static string Prefix(string repo, string type) + public static string GetPrefix(string repo, string type) { return new Config(repo).Get($"gitflow.prefix.{type}"); } diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index f8f42ae2..c9ab7b41 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -82,7 +82,7 @@ namespace SourceGit.Commands var rs = cmd.ReadToEnd(); if (rs.IsSuccess) { - var lines = rs.StdOut.Split(new char[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var match = REG_LOCK().Match(line); diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs index 43418825..a4efa4b6 100644 --- a/src/Commands/Pull.cs +++ b/src/Commands/Pull.cs @@ -10,18 +10,9 @@ namespace SourceGit.Commands WorkingDirectory = repo; Context = repo; TraitErrorAsOutput = true; + SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + Args = "pull --verbose --progress --tags "; - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += "pull --verbose --progress --tags "; if (useRebase) Args += "--rebase "; if (noTags) diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index 0aac37a5..31a69eb9 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -6,22 +6,13 @@ namespace SourceGit.Commands { public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action onProgress) { + _outputHandler = onProgress; + WorkingDirectory = repo; Context = repo; TraitErrorAsOutput = true; - _outputHandler = onProgress; - - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += "push --progress --verbose "; + SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + Args = "push --progress --verbose "; if (withTags) Args += "--tags "; @@ -37,20 +28,12 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; + SSHKey = new Config(repo).Get($"remote.{remote}.sshkey"); + Args = "push "; - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += "push "; if (isDelete) Args += "--delete "; + Args += $"{remote} refs/tags/{tag}"; } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 401f1682..66a1ee28 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; namespace SourceGit.Commands { @@ -9,10 +10,27 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits; + Args = "log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits; _findFirstMerged = needFindHead; } + public QueryCommits(string repo, int maxCount, string messageFilter) + { + var argsBuilder = new StringBuilder(); + var words = messageFilter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var word in words) + { + var escaped = word.Trim().Replace("\"", "\\\"", StringComparison.Ordinal); + argsBuilder.Append($"--grep=\"{escaped}\" "); + } + argsBuilder.Append("--all-match"); + + WorkingDirectory = repo; + Context = repo; + Args = $"log -{maxCount} --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + argsBuilder.ToString(); + _findFirstMerged = false; + } + public List Result() { var rs = ReadToEnd(); @@ -53,8 +71,6 @@ namespace SourceGit.Commands _current.Subject = line; nextPartIdx = -1; break; - default: - break; } nextPartIdx++; @@ -97,6 +113,9 @@ namespace SourceGit.Commands foreach (var sub in subs) { var d = sub.Trim(); + if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + continue; + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { _current.Decorators.Add(new Models.Decorator() @@ -105,10 +124,6 @@ namespace SourceGit.Commands Name = d.Substring(15), }); } - else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) - { - continue; - } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { _current.IsMerged = true; @@ -159,10 +174,10 @@ namespace SourceGit.Commands private void MarkFirstMerged() { - Args = $"log --since=\"{_commits[_commits.Count - 1].CommitterTimeStr}\" --format=\"%H\""; + Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\""; var rs = ReadToEnd(); - var shas = rs.StdOut.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + var shas = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); if (shas.Length == 0) return; diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 2f1e89b8..a9f777b1 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -52,7 +52,7 @@ namespace SourceGit.Commands change.Set(Models.ChangeState.None, Models.ChangeState.Copied); break; case "M": - change.Set(Models.ChangeState.Modified, Models.ChangeState.None); + change.Set(Models.ChangeState.Modified); break; case "MM": change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified); @@ -61,7 +61,7 @@ namespace SourceGit.Commands change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted); break; case "A": - change.Set(Models.ChangeState.Added, Models.ChangeState.None); + change.Set(Models.ChangeState.Added); break; case "AM": change.Set(Models.ChangeState.Added, Models.ChangeState.Modified); @@ -70,10 +70,10 @@ namespace SourceGit.Commands change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted); break; case "D": - change.Set(Models.ChangeState.Deleted, Models.ChangeState.None); + change.Set(Models.ChangeState.Deleted); break; case "R": - change.Set(Models.ChangeState.Renamed, Models.ChangeState.None); + change.Set(Models.ChangeState.Renamed); break; case "RM": change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified); @@ -82,7 +82,7 @@ namespace SourceGit.Commands change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted); break; case "C": - change.Set(Models.ChangeState.Copied, Models.ChangeState.None); + change.Set(Models.ChangeState.Copied); break; case "CM": change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified); diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index a3b13929..f65b6ce1 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -47,6 +47,9 @@ namespace SourceGit.Commands foreach (var sub in subs) { var d = sub.Trim(); + if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + continue; + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { decorators.Add(new Models.Decorator() @@ -55,10 +58,6 @@ namespace SourceGit.Commands Name = d.Substring(15), }); } - else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) - { - continue; - } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { isHeadOfCurrent = true; diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 0b5b747f..f17b7896 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -20,7 +20,7 @@ namespace SourceGit.Commands protected override void OnReadline(string line) { - var subs = line.Split(new char[] { '$' }, StringSplitOptions.RemoveEmptyEntries); + var subs = line.Split('$', StringSplitOptions.RemoveEmptyEntries); if (subs.Length == 2) { _loaded.Add(new Models.Tag() diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs index 2576d0e6..2ec50f3c 100644 --- a/src/Commands/Rebase.cs +++ b/src/Commands/Rebase.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace SourceGit.Commands +namespace SourceGit.Commands { public class Rebase : Command { @@ -19,12 +17,10 @@ namespace SourceGit.Commands { public InteractiveRebase(string repo, string basedOn) { - var exec = Process.GetCurrentProcess().MainModule.FileName; - var editor = $"\\\"{exec}\\\" --rebase-editor"; - WorkingDirectory = repo; Context = repo; - Args = $"-c core.editor=\"{editor}\" -c sequence.editor=\"{editor}\" -c rebase.abbreviateCommands=true rebase -i --autosquash {basedOn}"; + Editor = EditorType.RebaseEditor; + Args = $"rebase -i --autosquash {basedOn}"; } } } diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 85f5a4fb..b9f33367 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -27,11 +27,8 @@ namespace SourceGit.Commands return; var dateStr = line.Substring(0, dateEndIdx); - var date = 0.0; - if (!double.TryParse(dateStr, out date)) - return; - - _statistics.AddCommit(line.Substring(dateEndIdx + 1), date); + if (double.TryParse(dateStr, out var date)) + _statistics.AddCommit(line.Substring(dateEndIdx + 1), date); } private readonly Models.Statistics _statistics = null; diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index a0c57df7..7516b1e3 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; namespace SourceGit.Commands { - public partial class Worktree : Command + public class Worktree : Command { - [GeneratedRegex(@"^(\w)\s(\d+)$")] - private static partial Regex REG_AHEAD_BEHIND(); - public Worktree(string repo) { WorkingDirectory = repo; @@ -24,7 +20,7 @@ namespace SourceGit.Commands var last = null as Models.Worktree; if (rs.IsSuccess) { - var lines = rs.StdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.StartsWith("worktree ", StringComparison.Ordinal)) @@ -34,27 +30,23 @@ namespace SourceGit.Commands } else if (line.StartsWith("bare", StringComparison.Ordinal)) { - last.IsBare = true; + last!.IsBare = true; } else if (line.StartsWith("HEAD ", StringComparison.Ordinal)) { - last.Head = line.Substring(5).Trim(); + last!.Head = line.Substring(5).Trim(); } else if (line.StartsWith("branch ", StringComparison.Ordinal)) { - last.Branch = line.Substring(7).Trim(); + last!.Branch = line.Substring(7).Trim(); } else if (line.StartsWith("detached", StringComparison.Ordinal)) { - last.IsDetached = true; + last!.IsDetached = true; } else if (line.StartsWith("locked", StringComparison.Ordinal)) { - last.IsLocked = true; - } - else if (line.StartsWith("prunable", StringComparison.Ordinal)) - { - last.IsPrunable = true; + last!.IsLocked = true; } } } diff --git a/src/Converters/ChangeViewModeConverters.cs b/src/Converters/ChangeViewModeConverters.cs index a5b07bca..59a5652c 100644 --- a/src/Converters/ChangeViewModeConverters.cs +++ b/src/Converters/ChangeViewModeConverters.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls; +using Avalonia; +using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Media; @@ -12,11 +13,11 @@ namespace SourceGit.Converters switch (v) { case Models.ChangeViewMode.List: - return App.Current?.FindResource("Icons.List") as StreamGeometry; + return Application.Current?.FindResource("Icons.List") as StreamGeometry; case Models.ChangeViewMode.Grid: - return App.Current?.FindResource("Icons.Grid") as StreamGeometry; + return Application.Current?.FindResource("Icons.Grid") as StreamGeometry; default: - return App.Current?.FindResource("Icons.Tree") as StreamGeometry; + return Application.Current?.FindResource("Icons.Tree") as StreamGeometry; } }); } diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 137f6c9b..765ff6ef 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -22,7 +22,7 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsSubjectLengthBad = new FuncValueConverter(v => v > ViewModels.Preference.Instance.SubjectGuideLength); - + public static readonly FuncValueConverter IsSubjectLengthGood = new FuncValueConverter(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 42eaa164..a3e3bbba 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -18,7 +18,7 @@ namespace SourceGit.Converters public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return (value as Models.Locale).Key; + return (value as Models.Locale)?.Key; } } @@ -29,18 +29,21 @@ namespace SourceGit.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var theme = (string)value; + if (string.IsNullOrEmpty(theme)) + return ThemeVariant.Default; + if (theme.Equals("Light", StringComparison.OrdinalIgnoreCase)) return ThemeVariant.Light; - else if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) + + if (theme.Equals("Dark", StringComparison.OrdinalIgnoreCase)) return ThemeVariant.Dark; - else - return ThemeVariant.Default; + + return ThemeVariant.Default; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - var theme = (ThemeVariant)value; - return theme.Key; + return (value as ThemeVariant)?.Key; } } diff --git a/src/Models/AvatarManager.cs b/src/Models/AvatarManager.cs index 50d890e1..ed39bcf7 100644 --- a/src/Models/AvatarManager.cs +++ b/src/Models/AvatarManager.cs @@ -75,7 +75,10 @@ namespace SourceGit.Models } } } - catch { } + catch + { + // ignored + } lock (_synclock) { @@ -84,10 +87,7 @@ namespace SourceGit.Models Dispatcher.UIThread.InvokeAsync(() => { - if (_resources.ContainsKey(md5)) - _resources[md5] = img; - else - _resources.Add(md5, img); + _resources[md5] = img; NotifyResourceChanged(md5); }); } @@ -134,7 +134,10 @@ namespace SourceGit.Models return img; } } - catch { } + catch + { + // ignore + } } } @@ -156,7 +159,7 @@ namespace SourceGit.Models } private static readonly object _synclock = new object(); - private static readonly string _storePath = string.Empty; + private static readonly string _storePath; private static readonly List _avatars = new List(); private static readonly Dictionary _resources = new Dictionary(); private static readonly HashSet _requesting = new HashSet(); diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 0a95eac3..30c2c499 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -19,13 +19,11 @@ namespace SourceGit.Models public bool IsMerged { get; set; } = false; public Thickness Margin { get; set; } = new Thickness(0); - public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd"); - public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToString("yyyy/MM/dd"); - public bool IsCommitterVisible => Author != Committer || AuthorTime != CommitterTime; + public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index bc0ea8e1..6f371594 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -61,8 +61,13 @@ namespace SourceGit.Models } else if (x < LastX) { - if (y > LastY + halfHeight) - Add(new Point(LastX, LastY + halfHeight)); + var testY = LastY + halfHeight; + if (y > testY) + Add(new Point(LastX, testY)); + + if (!isEnd) + y += halfHeight; + Add(new Point(x, y)); } else if (isEnd) @@ -108,17 +113,17 @@ namespace SourceGit.Models private set; } = new List(); - public static void SetDefaultPens() + public static void SetDefaultPens(double thickness = 2) { - SetPenColors(_defaultPenColors); + SetPens(_defaultPenColors, thickness); } - public static void SetPenColors(List colors) + public static void SetPens(List colors, double thickness) { Pens.Clear(); foreach (var c in colors) - Pens.Add(new Pen(c.ToUInt32(), 2)); + Pens.Add(new Pen(c.ToUInt32(), thickness)); _penCount = colors.Count; } @@ -216,13 +221,17 @@ namespace SourceGit.Models var parent = commit.Parents[j]; if (mapUnsolved.TryGetValue(parent, out var value)) { + // Try to change the merge state of linked graph var l = value; - var link = new Link(); + if (isMerged) + l.IsMerged = true; + var link = new Link(); link.Start = position; link.End = new Point(l.LastX, offsetY + HALF_HEIGHT); link.Control = new Point(link.End.X, link.Start.Y); link.Color = l.Path.Color; + temp.Links.Add(link); } else @@ -259,8 +268,9 @@ namespace SourceGit.Models var path = unsolved[i]; var endY = (commits.Count - 0.5) * UNIT_HEIGHT; - if (path.Path.Points.Count == 1 && path.Path.Points[0].Y == endY) + if (path.Path.Points.Count == 1 && Math.Abs(path.Path.Points[0].Y - endY) < 0.0001) continue; + path.Add((i + 0.5) * UNIT_WIDTH, endY + HALF_HEIGHT, HALF_HEIGHT, true); } unsolved.Clear(); diff --git a/src/Models/CustomColorSchema.cs b/src/Models/CustomColorSchema.cs deleted file mode 100644 index 4266b98e..00000000 --- a/src/Models/CustomColorSchema.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace SourceGit.Models -{ - public class CustomColorSchema - { - public Dictionary Basic { get; set; } = new Dictionary(); - public List Graph { get; set; } = new List(); - } -} diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index e98edffb..f5bec602 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -108,8 +108,8 @@ namespace SourceGit.Models private readonly Change _workingCopyChange = null; private readonly bool _isUnstaged = false; + private readonly string _path; private readonly string _orgPath = string.Empty; - private readonly string _path = string.Empty; private readonly string _extra = string.Empty; private readonly List _revisions = new List(); } diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index 98351cd6..dc164f59 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -12,9 +12,9 @@ namespace SourceGit.Models { public class ExternalTool { - public string Name { get; private set; } = string.Empty; - public string Executable { get; private set; } = string.Empty; - public string OpenCmdArgs { get; private set; } = string.Empty; + public string Name { get; private set; } + public string Executable { get; private set; } + public string OpenCmdArgs { get; private set; } public Bitmap IconImage { get; private set; } = null; public ExternalTool(string name, string icon, string executable, string openCmdArgs) @@ -25,10 +25,14 @@ namespace SourceGit.Models try { - var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{icon}.png", UriKind.RelativeOrAbsolute)); + var asset = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{icon}.png", + UriKind.RelativeOrAbsolute)); IconImage = new Bitmap(asset); } - catch { } + catch + { + // ignore + } } public void Open(string repo) diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs new file mode 100644 index 00000000..0980587a --- /dev/null +++ b/src/Models/InteractiveRebase.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SourceGit.Models +{ + public enum InteractiveRebaseAction + { + Pick, + Edit, + Reword, + Squash, + Fixup, + Drop, + } + + public class InteractiveRebaseJob + { + public string SHA { get; set; } = string.Empty; + public InteractiveRebaseAction Action { get; set; } = InteractiveRebaseAction.Pick; + public string Message { get; set; } = string.Empty; + } + + public class InteractiveRebaseJobCollection + { + public List Jobs { get; set; } = new List(); + } +} diff --git a/src/Models/InteractiveRebaseEditor.cs b/src/Models/InteractiveRebaseEditor.cs deleted file mode 100644 index 911258d4..00000000 --- a/src/Models/InteractiveRebaseEditor.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; - -namespace SourceGit.Models -{ - public enum InteractiveRebaseAction - { - Pick, - Edit, - Reword, - Squash, - Fixup, - Drop, - } - - public class InteractiveRebaseJob - { - public string SHA { get; set; } = string.Empty; - public InteractiveRebaseAction Action { get; set; } = InteractiveRebaseAction.Pick; - public string Message { get; set; } = string.Empty; - } - - public static class InteractiveRebaseEditor - { - public static int Process(string file) - { - try - { - var filename = Path.GetFileName(file); - if (filename.Equals("git-rebase-todo", StringComparison.OrdinalIgnoreCase)) - { - var dirInfo = new DirectoryInfo(Path.GetDirectoryName(file)); - if (!dirInfo.Exists || !dirInfo.Name.Equals("rebase-merge", StringComparison.Ordinal)) - return -1; - - var jobsFile = Path.Combine(dirInfo.Parent.FullName, "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return -1; - - var jobs = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.ListInteractiveRebaseJob); - var lines = new List(); - foreach (var job in jobs) - { - switch (job.Action) - { - case InteractiveRebaseAction.Pick: - lines.Add($"p {job.SHA}"); - break; - case InteractiveRebaseAction.Edit: - lines.Add($"e {job.SHA}"); - break; - case InteractiveRebaseAction.Reword: - lines.Add($"r {job.SHA}"); - break; - case InteractiveRebaseAction.Squash: - lines.Add($"s {job.SHA}"); - break; - case InteractiveRebaseAction.Fixup: - lines.Add($"f {job.SHA}"); - break; - default: - lines.Add($"d {job.SHA}"); - break; - } - } - - File.WriteAllLines(file, lines); - } - else if (filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase)) - { - var jobsFile = Path.Combine(Path.GetDirectoryName(file), "sourcegit_rebase_jobs.json"); - if (!File.Exists(jobsFile)) - return 0; - - var jobs = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.ListInteractiveRebaseJob); - var doneFile = Path.Combine(Path.GetDirectoryName(file), "rebase-merge", "done"); - if (!File.Exists(doneFile)) - return -1; - - var done = File.ReadAllText(doneFile).Split(new char[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); - if (done.Length > jobs.Count) - return -1; - - var job = jobs[done.Length - 1]; - File.WriteAllText(file, job.Message); - } - - return 0; - } - catch - { - return -1; - } - } - } -} diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 29c10d51..e068deda 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -40,11 +40,15 @@ namespace SourceGit.Models public static bool IsValidURL(string url) { + if (string.IsNullOrWhiteSpace(url)) + return false; + foreach (var fmt in URL_FORMATS) { if (fmt.IsMatch(url)) return true; } + return false; } diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index 59868fcc..e2f4abb0 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -14,7 +14,6 @@ namespace SourceGit.Models public class RevisionTextFile { - public string FileName { get; set; } public string Content { get; set; } } diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index 044f2b29..0ae46c90 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -1,6 +1,7 @@ using System; using System.IO; +using Avalonia; using Avalonia.Styling; using AvaloniaEdit; @@ -14,14 +15,10 @@ namespace SourceGit.Models { public static TextMate.Installation CreateForEditor(TextEditor editor) { - if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) - { + if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) return editor.InstallTextMate(new RegistryOptions(ThemeName.DarkPlus)); - } - else - { - return editor.InstallTextMate(new RegistryOptions(ThemeName.LightPlus)); - } + + return editor.InstallTextMate(new RegistryOptions(ThemeName.LightPlus)); } public static void SetThemeByApp(TextMate.Installation installation) @@ -29,35 +26,28 @@ namespace SourceGit.Models if (installation == null) return; - var reg = installation.RegistryOptions as RegistryOptions; - if (App.Current?.ActualThemeVariant == ThemeVariant.Dark) + if (installation.RegistryOptions is RegistryOptions reg) { - installation.SetTheme(reg.LoadTheme(ThemeName.DarkPlus)); - } - else - { - installation.SetTheme(reg.LoadTheme(ThemeName.LightPlus)); + if (Application.Current?.ActualThemeVariant == ThemeVariant.Dark) + installation.SetTheme(reg.LoadTheme(ThemeName.DarkPlus)); + else + installation.SetTheme(reg.LoadTheme(ThemeName.LightPlus)); } } public static void SetGrammarByFileName(TextMate.Installation installation, string filePath) { - if (installation == null) - return; - - var ext = Path.GetExtension(filePath); - if (ext == ".h") + if (installation is { RegistryOptions: RegistryOptions reg }) { - ext = ".cpp"; - } - else if (ext == ".resx" || ext == ".plist") - { - ext = ".xml"; - } + var ext = Path.GetExtension(filePath); + if (ext == ".h") + ext = ".cpp"; + else if (ext == ".resx" || ext == ".plist") + ext = ".xml"; - var reg = installation.RegistryOptions as RegistryOptions; - installation.SetGrammar(reg.GetScopeByExtension(ext)); - GC.Collect(); + installation.SetGrammar(reg.GetScopeByExtension(ext)); + GC.Collect(); + } } } } diff --git a/src/Models/ThemeOverrides.cs b/src/Models/ThemeOverrides.cs new file mode 100644 index 00000000..e14b1f1a --- /dev/null +++ b/src/Models/ThemeOverrides.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +using Avalonia.Media; + +namespace SourceGit.Models +{ + public class ThemeOverrides + { + public Dictionary BasicColors { get; set; } = new Dictionary(); + public double GraphPenThickness { get; set; } = 2; + public List GraphColors { get; set; } = new List(); + } +} diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs deleted file mode 100644 index be9d7d7c..00000000 --- a/src/Models/TreeDataGridSelectionModel.cs +++ /dev/null @@ -1,462 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Models.TreeDataGrid; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Selection; -using Avalonia.Input; - -namespace SourceGit.Models -{ - public class TreeDataGridSelectionModel : TreeSelectionModelBase, - ITreeDataGridRowSelectionModel, - ITreeDataGridSelectionInteraction - where TModel : class - { - private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); - - private readonly ITreeDataGridSource _source; - private EventHandler _viewSelectionChanged; - private EventHandler _rowDoubleTapped; - private Point _pressedPoint = s_InvalidPoint; - private bool _raiseViewSelectionChanged; - private Func> _childrenGetter; - - public TreeDataGridSelectionModel(ITreeDataGridSource source, Func> childrenGetter) - : base(source.Items) - { - _source = source; - _childrenGetter = childrenGetter; - - SelectionChanged += (s, e) => - { - if (!IsSourceCollectionChanging) - _viewSelectionChanged?.Invoke(this, e); - else - _raiseViewSelectionChanged = true; - }; - } - - public void Select(IEnumerable items) - { - using (BatchUpdate()) - { - Clear(); - - foreach (var selected in items) - { - var idx = GetModelIndex(_source.Items, selected, IndexPath.Unselected); - if (!idx.Equals(IndexPath.Unselected)) - Select(idx); - } - } - } - - event EventHandler ITreeDataGridSelectionInteraction.SelectionChanged - { - add => _viewSelectionChanged += value; - remove => _viewSelectionChanged -= value; - } - - public event EventHandler RowDoubleTapped - { - add => _rowDoubleTapped += value; - remove => _rowDoubleTapped -= value; - } - - IEnumerable ITreeDataGridSelection.Source - { - get => Source; - set => Source = value; - } - - bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel) - { - if (rowModel is IModelIndexableRow indexable) - return IsSelected(indexable.ModelIndexPath); - return false; - } - - bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex) - { - if (rowIndex >= 0 && rowIndex < _source.Rows.Count) - { - if (_source.Rows[rowIndex] is IModelIndexableRow indexable) - return IsSelected(indexable.ModelIndexPath); - } - - return false; - } - - void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e) - { - if (sender.RowsPresenter is null) - return; - - if (!e.Handled) - { - var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); - if (e.Key == Key.A && ctrl && !SingleSelect) - { - using (BatchUpdate()) - { - Clear(); - - int num = _source.Rows.Count; - for (int i = 0; i < num; ++i) - { - var m = _source.Rows.RowIndexToModelIndex(i); - Select(m); - } - } - e.Handled = true; - } - - var direction = e.Key.ToNavigationDirection(); - var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (direction.HasValue) - { - var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(AnchorIndex); - sender.RowsPresenter.BringIntoView(anchorRowIndex); - - var anchor = sender.TryGetRow(anchorRowIndex); - if (anchor is not null && !ctrl) - { - e.Handled = TryKeyExpandCollapse(sender, direction.Value, anchor); - } - - if (!e.Handled && (!ctrl || shift)) - { - e.Handled = MoveSelection(sender, direction.Value, shift, anchor); - } - - if (!e.Handled && direction == NavigationDirection.Left - && anchor?.Rows is HierarchicalRows hierarchicalRows && anchorRowIndex > 0) - { - var newIndex = hierarchicalRows.GetParentRowIndex(AnchorIndex); - UpdateSelection(sender, newIndex, true); - FocusRow(sender, sender.RowsPresenter.BringIntoView(newIndex)); - } - - if (!e.Handled && direction == NavigationDirection.Right - && anchor?.Rows is HierarchicalRows hierarchicalRows2 && hierarchicalRows2[anchorRowIndex].IsExpanded) - { - var newIndex = anchorRowIndex + 1; - UpdateSelection(sender, newIndex, true); - sender.RowsPresenter.BringIntoView(newIndex); - } - } - } - } - - void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) - { - if (!e.Handled && - e.Pointer.Type == PointerType.Mouse && - e.Source is Control source && - sender.TryGetRow(source, out var row) && - _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) - { - if (!IsSelected(modelIndex)) - { - PointerSelect(sender, row, e); - _pressedPoint = s_InvalidPoint; - } - else - { - var point = e.GetCurrentPoint(sender); - if (point.Properties.IsRightButtonPressed) - { - _pressedPoint = s_InvalidPoint; - return; - } - - if (e.KeyModifiers == KeyModifiers.Control) - { - Deselect(modelIndex); - } - else if (e.ClickCount % 2 == 0) - { - var focus = _source.Rows[row.RowIndex]; - if (focus is IExpander expander && HasChildren(focus)) - expander.IsExpanded = !expander.IsExpanded; - else - _rowDoubleTapped?.Invoke(this, e); - - e.Handled = true; - } - else if (sender.RowSelection.Count > 1) - { - using (BatchUpdate()) - { - Clear(); - Select(modelIndex); - } - } - - _pressedPoint = s_InvalidPoint; - } - } - else - { - if (!sender.TryGetRow(e.Source as Control, out var test)) - Clear(); - - _pressedPoint = e.GetPosition(sender); - } - } - - void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e) - { - if (!e.Handled && - _pressedPoint != s_InvalidPoint && - e.Source is Control source && - sender.TryGetRow(source, out var row) && - _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) - { - if (!IsSelected(modelIndex)) - { - var p = e.GetPosition(sender); - if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3) - PointerSelect(sender, row, e); - } - } - } - - protected override void OnSourceCollectionChangeFinished() - { - if (_raiseViewSelectionChanged) - { - _viewSelectionChanged?.Invoke(this, EventArgs.Empty); - _raiseViewSelectionChanged = false; - } - } - - private void PointerSelect(TreeDataGrid sender, TreeDataGridRow row, PointerEventArgs e) - { - var point = e.GetCurrentPoint(sender); - - var commandModifiers = TopLevel.GetTopLevel(sender)?.PlatformSettings?.HotkeyConfiguration.CommandModifiers; - var toggleModifier = commandModifiers is not null && e.KeyModifiers.HasFlag(commandModifiers); - var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or - PointerUpdateKind.RightButtonReleased; - - UpdateSelection( - sender, - row.RowIndex, - select: true, - rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift), - toggleModifier: toggleModifier, - rightButton: isRightButton); - e.Handled = true; - } - - private void UpdateSelection(TreeDataGrid treeDataGrid, int rowIndex, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) - { - var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); - if (modelIndex == default) - return; - - var mode = SingleSelect ? SelectionMode.Single : SelectionMode.Multiple; - var multi = (mode & SelectionMode.Multiple) != 0; - var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); - var range = multi && rangeModifier; - - if (!select) - { - if (IsSelected(modelIndex) && !treeDataGrid.QueryCancelSelection()) - Deselect(modelIndex); - } - else if (rightButton) - { - if (IsSelected(modelIndex) == false && !treeDataGrid.QueryCancelSelection()) - SelectedIndex = modelIndex; - } - else if (range) - { - if (!treeDataGrid.QueryCancelSelection()) - { - var anchor = RangeAnchorIndex; - var i = Math.Max(_source.Rows.ModelIndexToRowIndex(anchor), 0); - var step = i < rowIndex ? 1 : -1; - - using (BatchUpdate()) - { - Clear(); - - while (true) - { - var m = _source.Rows.RowIndexToModelIndex(i); - Select(m); - anchor = m; - if (i == rowIndex) - break; - i += step; - } - } - } - } - else if (multi && toggle) - { - if (!treeDataGrid.QueryCancelSelection()) - { - if (IsSelected(modelIndex) == true) - Deselect(modelIndex); - else - Select(modelIndex); - } - } - else if (toggle) - { - if (!treeDataGrid.QueryCancelSelection()) - SelectedIndex = (SelectedIndex == modelIndex) ? -1 : modelIndex; - } - else if (SelectedIndex != modelIndex || Count > 1) - { - if (!treeDataGrid.QueryCancelSelection()) - SelectedIndex = modelIndex; - } - } - - private bool TryKeyExpandCollapse(TreeDataGrid treeDataGrid, NavigationDirection direction, TreeDataGridRow focused) - { - if (treeDataGrid.RowsPresenter is null || focused.RowIndex < 0) - return false; - - var row = _source.Rows[focused.RowIndex]; - - if (row is IExpander expander) - { - if (direction == NavigationDirection.Right && !expander.IsExpanded) - { - expander.IsExpanded = true; - return true; - } - else if (direction == NavigationDirection.Left && expander.IsExpanded) - { - expander.IsExpanded = false; - return true; - } - } - - return false; - } - - private bool MoveSelection(TreeDataGrid treeDataGrid, NavigationDirection direction, bool rangeModifier, TreeDataGridRow focused) - { - if (treeDataGrid.RowsPresenter is null || _source.Columns.Count == 0 || _source.Rows.Count == 0) - return false; - - var currentRowIndex = focused?.RowIndex ?? _source.Rows.ModelIndexToRowIndex(SelectedIndex); - int newRowIndex; - - if (direction == NavigationDirection.First || direction == NavigationDirection.Last) - { - newRowIndex = direction == NavigationDirection.First ? 0 : _source.Rows.Count - 1; - } - else - { - (var x, var y) = direction switch - { - NavigationDirection.Up => (0, -1), - NavigationDirection.Down => (0, 1), - NavigationDirection.Left => (-1, 0), - NavigationDirection.Right => (1, 0), - _ => (0, 0) - }; - - newRowIndex = Math.Max(0, Math.Min(currentRowIndex + y, _source.Rows.Count - 1)); - } - - if (newRowIndex != currentRowIndex) - UpdateSelection(treeDataGrid, newRowIndex, true, rangeModifier); - - if (newRowIndex != currentRowIndex) - { - treeDataGrid.RowsPresenter?.BringIntoView(newRowIndex); - FocusRow(treeDataGrid, treeDataGrid.TryGetRow(newRowIndex)); - return true; - } - else - { - return false; - } - } - - private static void FocusRow(TreeDataGrid owner, Control control) - { - if (!owner.TryGetRow(control, out var row) || row.CellsPresenter is null) - return; - - // Get the column index of the currently focused cell if possible: we'll try to focus the - // same column in the new row. - if (TopLevel.GetTopLevel(owner)?.FocusManager is { } focusManager && - focusManager.GetFocusedElement() is Control currentFocus && - owner.TryGetCell(currentFocus, out var currentCell) && - row.TryGetCell(currentCell.ColumnIndex) is { } newCell && - newCell.Focusable) - { - newCell.Focus(); - } - else - { - // Otherwise, just focus the first focusable cell in the row. - foreach (var cell in row.CellsPresenter.GetRealizedElements()) - { - if (cell.Focusable) - { - cell.Focus(); - break; - } - } - } - } - - protected override IEnumerable GetChildren(TModel node) - { - if (node == null) - return null; - - return _childrenGetter?.Invoke(node); - } - - private IndexPath GetModelIndex(IEnumerable collection, TModel model, IndexPath parent) - { - int i = 0; - - foreach (var item in collection) - { - var index = parent.Append(i); - if (item != null && item == model) - return index; - - var children = GetChildren(item); - if (children != null) - { - var findInChildren = GetModelIndex(children, model, index); - if (!findInChildren.Equals(IndexPath.Unselected)) - return findInChildren; - } - - i++; - } - - return IndexPath.Unselected; - } - - private bool HasChildren(IRow row) - { - var children = GetChildren(row.Model as TModel); - if (children != null) - { - foreach (var c in children) - return true; - } - - return false; - } - } -} diff --git a/src/Models/User.cs b/src/Models/User.cs index cb0d21cd..850bcf2f 100644 --- a/src/Models/User.cs +++ b/src/Models/User.cs @@ -10,6 +10,20 @@ namespace SourceGit.Models public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; + public User() + { + // Only used by User.Invalid + } + + public User(string data) + { + var nameEndIdx = data.IndexOf('±', StringComparison.Ordinal); + + Name = nameEndIdx > 0 ? data.Substring(0, nameEndIdx) : string.Empty; + Email = data.Substring(nameEndIdx + 1); + _hash = data.GetHashCode(); + } + public override bool Equals(object obj) { if (obj == null || !(obj is User)) @@ -21,21 +35,15 @@ namespace SourceGit.Models public override int GetHashCode() { - return base.GetHashCode(); + return _hash; } public static User FindOrAdd(string data) { - return _caches.GetOrAdd(data, key => - { - var nameEndIdx = key.IndexOf('±', StringComparison.Ordinal); - var name = nameEndIdx > 0 ? key.Substring(0, nameEndIdx) : string.Empty; - var email = key.Substring(nameEndIdx + 1); - - return new User() { Name = name, Email = email }; - }); + return _caches.GetOrAdd(data, key => new User(key)); } private static ConcurrentDictionary _caches = new ConcurrentDictionary(); + private readonly int _hash; } } diff --git a/src/Models/Version.cs b/src/Models/Version.cs index c16c1233..aca1f0fc 100644 --- a/src/Models/Version.cs +++ b/src/Models/Version.cs @@ -28,7 +28,7 @@ namespace SourceGit.Models var major = int.Parse(match.Groups[1].Value); var minor = int.Parse(match.Groups[2].Value); - var ver = Assembly.GetExecutingAssembly().GetName().Version; + var ver = Assembly.GetExecutingAssembly().GetName().Version!; return ver.Major < major || (ver.Major == major && ver.Minor < minor); } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index de3a42c4..2a1c4cc6 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -38,7 +38,7 @@ namespace SourceGit.Models // If this repository is a worktree repository, just watch the main repository's gitdir. var gitDirNormalized = _repo.GitDir.Replace("\\", "/"); - var worktreeIdx = gitDirNormalized.IndexOf(".git/worktrees/"); + var worktreeIdx = gitDirNormalized.IndexOf(".git/worktrees/", StringComparison.Ordinal); var repoWatchDir = _repo.GitDir; if (worktreeIdx > 0) repoWatchDir = _repo.GitDir.Substring(0, worktreeIdx + 4); diff --git a/src/Models/Worktree.cs b/src/Models/Worktree.cs index 5fbd4436..f9ba14e4 100644 --- a/src/Models/Worktree.cs +++ b/src/Models/Worktree.cs @@ -9,7 +9,6 @@ namespace SourceGit.Models public string Head { get; set; } = string.Empty; public bool IsBare { get; set; } = false; public bool IsDetached { get; set; } = false; - public bool IsPrunable { get; set; } = false; public bool IsLocked { @@ -26,7 +25,7 @@ namespace SourceGit.Models if (Branch.StartsWith("refs/heads/", System.StringComparison.Ordinal)) return $"({Branch.Substring(11)})"; - + if (Branch.StartsWith("refs/remotes/", System.StringComparison.Ordinal)) return $"({Branch.Substring(13)})"; diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index bca2f2ab..dbcd43aa 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -15,8 +15,8 @@ namespace SourceGit.Native { class Terminal { - public string FilePath { get; set; } = string.Empty; - public string OpenArgFormat { get; set; } = string.Empty; + public string FilePath { get; set; } + public string OpenArgFormat { get; set; } public Terminal(string exec, string fmt) { @@ -115,12 +115,15 @@ namespace SourceGit.Native } var proc = Process.Start(_xdgOpenPath, $"\"{file}\""); - proc.WaitForExit(); + if (proc != null) + { + proc.WaitForExit(); - if (proc.ExitCode != 0) - App.RaiseException("", $"Failed to open \"{file}\""); + if (proc.ExitCode != 0) + App.RaiseException("", $"Failed to open \"{file}\""); - proc.Close(); + proc.Close(); + } } private string FindExecutable(string filename) @@ -177,7 +180,7 @@ namespace SourceGit.Native return File.Exists(path) ? path : FindExecutable("fleet"); } - private string _xdgOpenPath = string.Empty; + private string _xdgOpenPath = null; private Terminal _terminal = null; } } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index d5b58fb3..4fc9998c 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -78,7 +78,10 @@ namespace SourceGit.Native File.WriteAllText(tmp, builder.ToString()); var proc = Process.Start("osascript", $"\"{tmp}\""); - proc.Exited += (o, e) => File.Delete(tmp); + if (proc != null) + proc.Exited += (_, _) => File.Delete(tmp); + else + File.Delete(tmp); } public void OpenWithDefaultEditor(string file) diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 451362ca..8c2a3ada 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -48,20 +48,16 @@ namespace SourceGit.Native public static Models.Shell GetShell() { if (OperatingSystem.IsWindows()) - { - return (_backend as Windows).Shell; - } - else - { - return Models.Shell.Default; - } + return (_backend as Windows)!.Shell; + + return Models.Shell.Default; } public static bool SetShell(Models.Shell shell) { if (OperatingSystem.IsWindows()) { - var windows = _backend as Windows; + var windows = (_backend as Windows)!; if (windows.Shell != shell) { windows.Shell = shell; diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index bdf4d74c..d8203d13 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -73,8 +73,8 @@ namespace SourceGit.Native v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000)) { - Window.WindowStateProperty.Changed.AddClassHandler((w, e) => ExtendWindowFrame(w)); - Window.LoadedEvent.AddClassHandler((w, e) => ExtendWindowFrame(w)); + Window.WindowStateProperty.Changed.AddClassHandler((w, _) => ExtendWindowFrame(w)); + Control.LoadedEvent.AddClassHandler((w, _) => ExtendWindowFrame(w)); } } @@ -85,9 +85,9 @@ namespace SourceGit.Native Microsoft.Win32.RegistryView.Registry64); var git = reg.OpenSubKey("SOFTWARE\\GitForWindows"); - if (git != null) + if (git != null && git.GetValue("InstallPath") is string installPath) { - return Path.Combine(git.GetValue("InstallPath") as string, "bin", "git.exe"); + return Path.Combine(installPath, "bin", "git.exe"); } var builder = new StringBuilder("git.exe", 259); @@ -137,7 +137,13 @@ namespace SourceGit.Native switch (Shell) { case Models.Shell.Default: - var binDir = Path.GetDirectoryName(OS.GitExecutable); + if (string.IsNullOrEmpty(OS.GitExecutable)) + { + App.RaiseException(workdir, $"Can NOT found bash.exe"); + return; + } + + var binDir = Path.GetDirectoryName(OS.GitExecutable)!; var bash = Path.Combine(binDir, "bash.exe"); if (!File.Exists(bash)) { @@ -175,28 +181,23 @@ namespace SourceGit.Native public void OpenInFileManager(string path, bool select) { - var fullpath = string.Empty; + string fullpath; if (File.Exists(path)) { fullpath = new FileInfo(path).FullName; - - // For security reason, we never execute a file. - // Instead, we open the folder and select it. select = true; } else { - fullpath = new DirectoryInfo(path).FullName; + fullpath = new DirectoryInfo(path!).FullName; } if (select) { - // The fullpath here may be a file or a folder. OpenFolderAndSelectFile(fullpath); } else { - // The fullpath here is always a folder. Process.Start(new ProcessStartInfo(fullpath) { UseShellExecute = true, @@ -362,7 +363,7 @@ namespace SourceGit.Native if (sublime != null) { var icon = sublime.GetValue("DisplayIcon") as string; - return Path.Combine(Path.GetDirectoryName(icon), "subl.exe"); + return Path.Combine(Path.GetDirectoryName(icon)!, "subl.exe"); } // Sublime Text 3 @@ -370,7 +371,7 @@ namespace SourceGit.Native if (sublime3 != null) { var icon = sublime3.GetValue("DisplayIcon") as string; - return Path.Combine(Path.GetDirectoryName(icon), "subl.exe"); + return Path.Combine(Path.GetDirectoryName(icon)!, "subl.exe"); } return string.Empty; diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 1af75c64..b070f132 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -50,6 +50,7 @@ M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z + M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z M40 9 15 23 15 31 9 28 9 20 34 5 24 0 0 14 0 34 25 48 25 28 49 14zM26 29 26 48 49 34 49 15z @@ -59,7 +60,7 @@ M512 0C233 0 7 223 0 500C6 258 190 64 416 64c230 0 416 200 416 448c0 53 43 96 96 96s96-43 96-96c0-283-229-512-512-512zm0 1023c279 0 505-223 512-500c-6 242-190 436-416 436c-230 0-416-200-416-448c0-53-43-96-96-96s-96 43-96 96c0 283 229 512 512 512z M976 0h-928A48 48 0 000 48v652a48 48 0 0048 48h416V928H200a48 48 0 000 96h624a48 48 0 000-96H560v-180h416a48 48 0 0048-48V48A48 48 0 00976 0zM928 652H96V96h832v556z M832 464h-68V240a128 128 0 00-128-128h-248a128 128 0 00-128 128v224H192c-18 0-32 14-32 32v384c0 18 14 32 32 32h640c18 0 32-14 32-32v-384c0-18-14-32-32-32zm-292 237v53a8 8 0 01-8 8h-40a8 8 0 01-8-8v-53a48 48 0 1156 0zm152-237H332V240a56 56 0 0156-56h248a56 56 0 0156 56v224z - M851 755q0 23-16 39l-78 78q-16 16-39 16t-39-16l-168-168-168 168q-16 16-39 16t-39-16l-78-78q-16-16-16-39t16-39l168-168-168-168q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 168 168-168q16-16 39-16t39 16l78 78q16 16 16 39t-16 39l-168 168 168 168q16 16 16 39z + M887 774 625 511l263-260c11-11 11-28 0-39l-75-75c-5-5-12-8-20-8-7 0-14 3-20 8L512 396 250 137c-5-5-12-8-20-8-7 0-14 3-20 8L136 212c-11 11-11 28 0 39l263 260L137 774c-5 5-8 12-8 20 0 7 3 14 8 20l75 75c5 5 12 8 20 8 7 0 14-3 20-8L512 626l261 262c5 5 12 8 20 8 7 0 14-3 20-8l75-75c5-5 8-12 8-20C895 786 892 779 887 774z M1024 750v110c0 50-41 91-91 91h-841A92 92 0 010 859v-110C0 699 41 658 91 658h841c50 0 91 41 91 91z M0 4 0 20 16 20 0 4M4 0 20 0 20 16 4 0z M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 55b05a01..ef8ffd06 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -36,6 +36,7 @@ Select archive file path Revision: Archive + SourceGit Askpass FILES ASSUME UNCHANGED NO FILES ASSUMED AS UNCHANGED REMOVE @@ -90,6 +91,7 @@ Parent Folder: Repository URL: CLOSE + Editor Cherry-Pick This Commit Checkout Commit Compare with HEAD @@ -275,6 +277,10 @@ Histories Switch Horizontal/Vertical Layout Switch Curve/Polyline Graph Mode + AUTHOR + GRAPH & SUBJECT + SHA + COMMIT TIME SEARCH SHA/SUBJECT/AUTHOR. PRESS ENTER TO SEARCH, ESC TO QUIT CLEAR SELECTED {0} COMMITS @@ -337,11 +343,11 @@ Paste Preference APPEARANCE - Custom Color Schema Default Font Default Font Size Monospace Font Theme + Theme Overrides GENERAL Avatar Server Check for updates on startup @@ -449,8 +455,10 @@ RESOLVE Search Commit Search By - Information File + Message + SHA + Author & Committer Search Branches & Tags Statistics SUBMODULES diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 58a72d47..20265dab 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -39,6 +39,7 @@ 选择存档文件的存放路径 指定的提交: 存档 + SourceGit Askpass 不跟踪更改的文件 没有不跟踪更改的文件 移除 @@ -93,6 +94,7 @@ 父级目录 : 远程仓库 : 关闭 + 提交信息编辑器 挑选(cherry-pick)此提交 检出此提交 与当前HEAD比较 @@ -278,6 +280,10 @@ 历史记录 切换横向/纵向显示 切换曲线/折线显示 + 作者 + 路线图与主题 + 提交指纹 + 提交时间 查询提交指纹、信息、作者。回车键开始,ESC键取消 清空 已选中 {0} 项提交 @@ -340,11 +346,11 @@ 粘贴 偏好设置 外观配置 - 自定义配色文件 缺省字体 默认字体大小 等宽字体 主题 + 主题自定义 通用配置 头像服务 启动时检测软件更新 @@ -451,8 +457,10 @@ 解决冲突 查找提交 搜索途径 - 摘要 文件 + 提交信息 + 提交指纹 + 作者及提交者 快速查找分支、标签 提交统计 子模块列表 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 73176126..70b7bbf1 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -39,6 +39,7 @@ 選擇存檔檔案的存放路徑 指定的提交: 存檔 + SourceGit Askpass 不跟蹤更改的檔案 沒有不跟蹤更改的檔案 移除 @@ -93,6 +94,7 @@ 父級目錄 : 遠端倉庫 : 關閉 + 提交資訊編輯器 挑選(cherry-pick)此提交 檢出此提交 與當前HEAD比較 @@ -278,6 +280,10 @@ 歷史記錄 切換橫向/縱向顯示 切換曲線/折線顯示 + 作者 + 路線圖與主題 + 提交指紋 + 提交時間 查詢提交指紋、資訊、作者。回車鍵開始,ESC鍵取消 清空 已選中 {0} 項提交 @@ -340,11 +346,11 @@ 貼上 偏好設定 外觀配置 - 自訂配色檔 預設字型 預設字型大小 等寬字型 主題 + 主題自訂 通用配置 頭像服務 啟動時檢測軟體更新 @@ -451,8 +457,10 @@ 解決衝突 查詢提交 查詢方式 - 摘要 檔案 + 提交資訊 + 提交指紋 + 作者及提交者 快速查找分支、標籤 提交統計 子模組列表 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 139e5bd5..346c8f1c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -169,6 +169,9 @@ + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + - + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index c9ae72b2..d59e191a 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -3,111 +3,45 @@ using System.Collections.Generic; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Models.TreeDataGrid; -using Avalonia.Controls.Templates; +using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { - public class ChangeTreeNode + public class ChangeTreeNodeToggleButton : ToggleButton { - public string FullPath { get; set; } = string.Empty; - public bool IsFolder { get; set; } = false; - public bool IsExpanded { get; set; } = false; - public Models.Change Change { get; set; } = null; - public List Children { get; set; } = new List(); + protected override Type StyleKeyOverride => typeof(ToggleButton); - public static List Build(IList changes, bool expanded) + protected override void OnPointerPressed(PointerPressedEventArgs e) { - var nodes = new List(); - var folders = new Dictionary(); - - foreach (var c in changes) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.ChangeTreeNode { IsFolder: true } node) { - var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal); - if (sepIdx == -1) - { - nodes.Add(new ChangeTreeNode() - { - FullPath = c.Path, - Change = c, - IsFolder = false, - IsExpanded = false - }); - } - else - { - ChangeTreeNode lastFolder = null; - var start = 0; - - while (sepIdx != -1) - { - var folder = c.Path.Substring(0, sepIdx); - if (folders.TryGetValue(folder, out var value)) - { - lastFolder = value; - } - else if (lastFolder == null) - { - lastFolder = new ChangeTreeNode() - { - FullPath = folder, - IsFolder = true, - IsExpanded = expanded - }; - folders.Add(folder, lastFolder); - InsertFolder(nodes, lastFolder); - } - else - { - var cur = new ChangeTreeNode() - { - FullPath = folder, - IsFolder = true, - IsExpanded = expanded - }; - folders.Add(folder, cur); - InsertFolder(lastFolder.Children, cur); - lastFolder = cur; - } - - start = sepIdx + 1; - sepIdx = c.Path.IndexOf('/', start); - } - - lastFolder.Children.Add(new ChangeTreeNode() - { - FullPath = c.Path, - Change = c, - IsFolder = false, - IsExpanded = false - }); - } + var tree = this.FindAncestorOfType(); + tree?.ToggleNodeIsExpanded(node); } - folders.Clear(); - return nodes; + e.Handled = true; } + } - private static void InsertFolder(List collection, ChangeTreeNode subFolder) + public class ChangeCollectionContainer : ListBox + { + protected override Type StyleKeyOverride => typeof(ListBox); + + protected override void OnKeyDown(KeyEventArgs e) { - for (int i = 0; i < collection.Count; i++) - { - if (!collection[i].IsFolder) - { - collection.Insert(i, subFolder); - return; - } - } - - collection.Add(subFolder); + if (e.Key != Key.Space) + base.OnKeyDown(e); } } public partial class ChangeCollectionView : UserControl { public static readonly StyledProperty IsWorkingCopyChangeProperty = - AvaloniaProperty.Register(nameof(IsWorkingCopyChange), false); + AvaloniaProperty.Register(nameof(IsWorkingCopyChange)); public bool IsWorkingCopyChange { @@ -115,13 +49,13 @@ namespace SourceGit.Views set => SetValue(IsWorkingCopyChangeProperty, value); } - public static readonly StyledProperty SingleSelectProperty = - AvaloniaProperty.Register(nameof(SingleSelect), true); + public static readonly StyledProperty SelectionModeProperty = + AvaloniaProperty.Register(nameof(SelectionMode)); - public bool SingleSelect + public SelectionMode SelectionMode { - get => GetValue(SingleSelectProperty); - set => SetValue(SingleSelectProperty, value); + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); } public static readonly StyledProperty ViewModeProperty = @@ -134,7 +68,7 @@ namespace SourceGit.Views } public static readonly StyledProperty> ChangesProperty = - AvaloniaProperty.Register>(nameof(Changes), null); + AvaloniaProperty.Register>(nameof(Changes)); public List Changes { @@ -143,7 +77,7 @@ namespace SourceGit.Views } public static readonly StyledProperty> SelectedChangesProperty = - AvaloniaProperty.Register>(nameof(SelectedChanges), null); + AvaloniaProperty.Register>(nameof(SelectedChanges)); public List SelectedChanges { @@ -160,174 +94,196 @@ namespace SourceGit.Views remove { RemoveHandler(ChangeDoubleTappedEvent, value); } } - static ChangeCollectionView() - { - ViewModeProperty.Changed.AddClassHandler((c, e) => c.UpdateSource()); - ChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSource()); - SelectedChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSelected()); - } - public ChangeCollectionView() { InitializeComponent(); } - private void UpdateSource() + public void ToggleNodeIsExpanded(ViewModels.ChangeTreeNode node) { - if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable) - disposable.Dispose(); - - Content = null; - - var changes = Changes; - if (changes == null || changes.Count == 0) - return; - - var viewMode = ViewMode; - if (viewMode == Models.ChangeViewMode.Tree) + if (_displayContext is ViewModels.ChangeCollectionAsTree tree) { - var filetree = ChangeTreeNode.Build(changes, true); - var template = this.FindResource("TreeModeTemplate") as IDataTemplate; - var source = new HierarchicalTreeDataGridSource(filetree) + node.IsExpanded = !node.IsExpanded; + + var depth = node.Depth; + var idx = tree.Rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) { - Columns = + var subrows = new List(); + MakeTreeRows(subrows, node.Children); + tree.Rows.InsertRange(idx + 1, subrows); + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < tree.Rows.Count; i++) { - new HierarchicalExpanderColumn( - new TemplateColumn(null, template, null, GridLength.Auto), - x => x.Children, - x => x.Children.Count > 0, - x => x.IsExpanded) + var row = tree.Rows[i]; + if (row.Depth <= depth) + break; + + removeCount++; } - }; - - var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); - selection.SingleSelect = SingleSelect; - selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); - selection.SelectionChanged += (s, _) => - { - if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) - { - var selected = new List(); - foreach (var c in model.SelectedItems) - CollectChangesInNode(selected, c); - - TrySetSelected(selected); - } - }; - - source.Selection = selection; - CreateTreeDataGrid(source); - } - else if (viewMode == Models.ChangeViewMode.List) - { - var template = this.FindResource("ListModeTemplate") as IDataTemplate; - var source = new FlatTreeDataGridSource(changes) - { - Columns = { new TemplateColumn(null, template, null, GridLength.Auto) } - }; - - var selection = new Models.TreeDataGridSelectionModel(source, null); - selection.SingleSelect = SingleSelect; - selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); - selection.SelectionChanged += (s, _) => - { - if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) - { - var selected = new List(); - foreach (var c in model.SelectedItems) - selected.Add(c); - - TrySetSelected(selected); - } - }; - - source.Selection = selection; - CreateTreeDataGrid(source); - } - else - { - var template = this.FindResource("GridModeTemplate") as IDataTemplate; - var source = new FlatTreeDataGridSource(changes) - { - Columns = { new TemplateColumn(null, template, null, GridLength.Auto) }, - }; - - var selection = new Models.TreeDataGridSelectionModel(source, null); - selection.SingleSelect = SingleSelect; - selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); - selection.SelectionChanged += (s, _) => - { - if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) - { - var selected = new List(); - foreach (var c in model.SelectedItems) - selected.Add(c); - - TrySetSelected(selected); - } - }; - - source.Selection = selection; - CreateTreeDataGrid(source); + tree.Rows.RemoveRange(idx + 1, removeCount); + } } } - private void UpdateSelected() + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (_isSelecting || Content == null) - return; + base.OnPropertyChanged(change); - var tree = Content as TreeDataGrid; - if (tree == null) - return; - - _isSelecting = true; - var selected = SelectedChanges; - if (tree.Source.Selection is Models.TreeDataGridSelectionModel changeSelection) + if (change.Property == ViewModeProperty || change.Property == ChangesProperty) { - if (selected == null || selected.Count == 0) - changeSelection.Clear(); - else - changeSelection.Select(selected); - } - else if (tree.Source.Selection is Models.TreeDataGridSelectionModel treeSelection) - { - if (selected == null || selected.Count == 0) + _disableSelectionChangingEvent = change.Property == ChangesProperty; + var changes = Changes; + if (changes == null || changes.Count == 0) { - treeSelection.Clear(); - _isSelecting = false; + Content = null; + _displayContext = null; + _disableSelectionChangingEvent = false; return; } - var set = new HashSet(); - foreach (var c in selected) - set.Add(c); + if (ViewMode == Models.ChangeViewMode.Tree) + { + HashSet oldFolded = new HashSet(); + if (_displayContext is ViewModels.ChangeCollectionAsTree oldTree) + { + foreach (var row in oldTree.Rows) + { + if (row.IsFolder && !row.IsExpanded) + oldFolded.Add(row.FullPath); + } + } - var nodes = new List(); - foreach (var node in tree.Source.Items) - CollectSelectedNodeByChange(nodes, node as ChangeTreeNode, set); + var tree = new ViewModels.ChangeCollectionAsTree(); + tree.Tree = ViewModels.ChangeTreeNode.Build(changes, oldFolded); - if (nodes.Count == 0) - treeSelection.Clear(); + var rows = new List(); + MakeTreeRows(rows, tree.Tree); + tree.Rows.AddRange(rows); + _displayContext = tree; + } + else if (ViewMode == Models.ChangeViewMode.Grid) + { + var grid = new ViewModels.ChangeCollectionAsGrid(); + grid.Changes.AddRange(changes); + _displayContext = grid; + } else - treeSelection.Select(nodes); + { + var list = new ViewModels.ChangeCollectionAsList(); + list.Changes.AddRange(changes); + _displayContext = list; + } + + Content = _displayContext; + _disableSelectionChangingEvent = false; } - _isSelecting = false; - } - - private void CreateTreeDataGrid(ITreeDataGridSource source) - { - Content = new TreeDataGrid() + else if (change.Property == SelectedChangesProperty) { - AutoDragDropRows = false, - ShowColumnHeaders = false, - CanUserResizeColumns = false, - CanUserSortColumns = false, - Source = source, - }; + if (_disableSelectionChangingEvent) + return; + + var list = this.FindDescendantOfType(); + if (list == null) + return; + + _disableSelectionChangingEvent = true; + + var selected = SelectedChanges; + if (selected == null || selected.Count == 0) + { + list.SelectedItem = null; + } + else if (_displayContext is ViewModels.ChangeCollectionAsTree tree) + { + var sets = new HashSet(); + foreach (var c in selected) + sets.Add(c); + + var nodes = new List(); + foreach (var row in tree.Rows) + { + if (row.Change != null && sets.Contains(row.Change)) + nodes.Add(row); + } + + list.SelectedItems = nodes; + } + else + { + list.SelectedItems = selected; + } + + _disableSelectionChangingEvent = false; + } } - private void CollectChangesInNode(List outs, ChangeTreeNode node) + private void OnRowDoubleTapped(object sender, TappedEventArgs e) + { + var grid = sender as Grid; + if (grid?.DataContext is ViewModels.ChangeTreeNode node) + { + if (node.IsFolder) + { + var posX = e.GetPosition(this).X; + if (posX < node.Depth * 16 + 16) + return; + + ToggleNodeIsExpanded(node); + } + else + { + RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); + } + } + else if (grid?.DataContext is Models.Change) + { + RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); + } + } + + private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _) + { + if (_disableSelectionChangingEvent) + return; + + _disableSelectionChangingEvent = true; + var selected = new List(); + if (sender is ListBox { SelectedItems: not null } list) + { + foreach (var item in list.SelectedItems) + { + if (item is Models.Change c) + selected.Add(c); + else if (item is ViewModels.ChangeTreeNode node) + CollectChangesInNode(selected, node); + } + } + + TrySetSelected(selected); + _disableSelectionChangingEvent = false; + } + + private void MakeTreeRows(List rows, List nodes) + { + foreach (var node in nodes) + { + rows.Add(node); + + if (!node.IsExpanded || !node.IsFolder) + continue; + + MakeTreeRows(rows, node.Children); + } + } + + private void CollectChangesInNode(List outs, ViewModels.ChangeTreeNode node) { if (node.IsFolder) { @@ -340,26 +296,9 @@ namespace SourceGit.Views } } - private void CollectSelectedNodeByChange(List outs, ChangeTreeNode node, HashSet selected) - { - if (node == null) - return; - - if (node.IsFolder) - { - foreach (var child in node.Children) - CollectSelectedNodeByChange(outs, child, selected); - } - else if (node.Change != null && selected.Contains(node.Change)) - { - outs.Add(node); - } - } - private void TrySetSelected(List changes) { var old = SelectedChanges; - if (old == null && changes.Count == 0) return; @@ -379,11 +318,12 @@ namespace SourceGit.Views return; } - _isSelecting = true; + _disableSelectionChangingEvent = true; SetCurrentValue(SelectedChangesProperty, changes); - _isSelecting = false; + _disableSelectionChangingEvent = false; } - private bool _isSelecting = false; + private bool _disableSelectionChangingEvent = false; + private object _displayContext = null; } } diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index fdcb8640..5821b49c 100644 --- a/src/Views/ChangeStatusIcon.cs +++ b/src/Views/ChangeStatusIcon.cs @@ -87,7 +87,7 @@ namespace SourceGit.Views var typeface = new Typeface("fonts:SourceGit#JetBrains Mono"); - IBrush background = null; + IBrush background; string indicator; if (IsWorkingCopyChange) { diff --git a/src/Views/ChangeViewModeSwitcher.axaml b/src/Views/ChangeViewModeSwitcher.axaml index 07617536..3e1cbfed 100644 --- a/src/Views/ChangeViewModeSwitcher.axaml +++ b/src/Views/ChangeViewModeSwitcher.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:v="using:SourceGit.Views" - xmlns:vm="using:SourceGit.ViewModels" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.ChangeViewModeSwitcher" diff --git a/src/Views/CherryPick.axaml b/src/Views/CherryPick.axaml index 9163526c..fcd4c212 100644 --- a/src/Views/CherryPick.axaml +++ b/src/Views/CherryPick.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -13,12 +12,12 @@ Classes="bold" Text="{DynamicResource Text.CherryPick}"/> - - + diff --git a/src/Views/Cleanup.axaml b/src/Views/Cleanup.axaml index dc429420..d385712d 100644 --- a/src/Views/Cleanup.axaml +++ b/src/Views/Cleanup.axaml @@ -2,9 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.Cleanup" x:DataType="vm:Cleanup"> diff --git a/src/Views/Clone.axaml.cs b/src/Views/Clone.axaml.cs index 9148daec..a05a68d6 100644 --- a/src/Views/Clone.axaml.cs +++ b/src/Views/Clone.axaml.cs @@ -11,7 +11,7 @@ namespace SourceGit.Views InitializeComponent(); } - private async void SelectParentFolder(object sender, RoutedEventArgs e) + private async void SelectParentFolder(object _, RoutedEventArgs e) { var options = new FolderPickerOpenOptions() { AllowMultiple = false }; var toplevel = TopLevel.GetTopLevel(this); @@ -25,13 +25,18 @@ namespace SourceGit.Views e.Handled = true; } - private async void SelectSSHKey(object sender, RoutedEventArgs e) + private async void SelectSSHKey(object _, RoutedEventArgs e) { - var options = new FilePickerOpenOptions() { AllowMultiple = false, FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] }; var toplevel = TopLevel.GetTopLevel(this); if (toplevel == null) return; + var options = new FilePickerOpenOptions() + { + AllowMultiple = false, + FileTypeFilter = [new FilePickerFileType("SSHKey") { Patterns = ["*.*"] }] + }; + var selected = await toplevel.StorageProvider.OpenFilePickerAsync(options); if (selected.Count == 1) TxtSshKey.Text = selected[0].Path.LocalPath; diff --git a/src/Views/CommitBaseInfo.axaml.cs b/src/Views/CommitBaseInfo.axaml.cs index 3c8a9ea5..d4488068 100644 --- a/src/Views/CommitBaseInfo.axaml.cs +++ b/src/Views/CommitBaseInfo.axaml.cs @@ -31,8 +31,12 @@ namespace SourceGit.Views private void OnParentSHAPressed(object sender, PointerPressedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail && CanNavigate) - detail.NavigateTo((sender as Control).DataContext as string); + if (sender is Control { DataContext: string sha } && + DataContext is ViewModels.CommitDetail detail && + CanNavigate) + { + detail.NavigateTo(sha); + } e.Handled = true; } diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index fffb3bd3..849ea1bd 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CommitChanges" x:DataType="vm:CommitDetail"> @@ -47,6 +46,7 @@ PreviewKeyDownEvent = - RoutedEvent.Register(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); public event EventHandler PreviewKeyDown { @@ -117,7 +117,7 @@ namespace SourceGit.Views } } - private async void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e) + private async void OnSubjectTextBoxPreviewKeyDown(object _, KeyEventArgs e) { if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length)) { @@ -127,6 +127,8 @@ namespace SourceGit.Views } else if (e.Key == Key.V && ((OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Meta) || (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control))) { + e.Handled = true; + var text = await App.GetClipboardTextAsync(); if (!string.IsNullOrWhiteSpace(text)) { @@ -152,12 +154,10 @@ namespace SourceGit.Views SubjectEditor.Paste(text.ReplaceLineEndings(" ")); } } - - e.Handled = true; } } - private void OnDescriptionTextBoxPreviewKeyDown(object sender, KeyEventArgs e) + private void OnDescriptionTextBoxPreviewKeyDown(object _, KeyEventArgs e) { if ((e.Key == Key.Back || e.Key == Key.Left) && DescriptionEditor.CaretIndex == 0) { diff --git a/src/Views/CreateBranch.axaml b/src/Views/CreateBranch.axaml index f69ea150..5cffbf9a 100644 --- a/src/Views/CreateBranch.axaml +++ b/src/Views/CreateBranch.axaml @@ -15,11 +15,11 @@ Classes="bold" Text="{DynamicResource Text.CreateBranch.Title}"/> - - + diff --git a/src/Views/DeleteMultipleBranches.axaml b/src/Views/DeleteMultipleBranches.axaml index 9fd87b90..e0d2f11f 100644 --- a/src/Views/DeleteMultipleBranches.axaml +++ b/src/Views/DeleteMultipleBranches.axaml @@ -2,10 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteMultipleBranches" x:DataType="vm:DeleteMultipleBranches"> diff --git a/src/Views/DeleteSubmodule.axaml b/src/Views/DeleteSubmodule.axaml index 5f609e12..66f598ae 100644 --- a/src/Views/DeleteSubmodule.axaml +++ b/src/Views/DeleteSubmodule.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:c="using:SourceGit.Converters" xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteSubmodule" @@ -12,8 +11,7 @@ Classes="bold" Text="{DynamicResource Text.DeleteSubmodule}"/> - - - + diff --git a/src/Views/EditRemote.axaml b/src/Views/EditRemote.axaml index 8a51a643..7d64a53a 100644 --- a/src/Views/EditRemote.axaml +++ b/src/Views/EditRemote.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.EditRemote" x:DataType="vm:EditRemote"> @@ -44,7 +42,7 @@ Text="{DynamicResource Text.SSHKey}" IsVisible="{Binding UseSSH}"/> diff --git a/src/Views/Fetch.axaml b/src/Views/Fetch.axaml index 1f91b24a..c15d3e20 100644 --- a/src/Views/Fetch.axaml +++ b/src/Views/Fetch.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.Fetch" x:DataType="vm:Fetch"> @@ -13,11 +12,11 @@ Classes="bold" Text="{DynamicResource Text.Fetch.Title}"/> - - @@ -22,11 +20,11 @@ Text="{DynamicResource Text.GitFlow.FinishHotfix}" IsVisible="{Binding IsHotfix}"/> - - + diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index e81048ff..7d2b78b2 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.GitFlowStart" x:DataType="vm:GitFlowStart"> diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index e07b8d0b..aad86236 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -12,25 +12,59 @@ - + + + + - + + + + + @@ -77,44 +111,53 @@ - + + + + + - + + + + - - - - - - - - - + + + + + - + + + + + - UseHorizontalProperty = - AvaloniaProperty.Register(nameof(UseHorizontal), false); + AvaloniaProperty.Register(nameof(UseHorizontal)); public bool UseHorizontal { @@ -96,10 +96,12 @@ namespace SourceGit.Views { base.Render(context); - var parent = this.FindAncestorOfType(); + var grid = this.FindAncestorOfType()?.CommitDataGrid; + if (grid == null) + return; + var graph = Graph; - var grid = parent.commitDataGrid; - if (graph == null || grid == null) + if (graph == null) return; var rowsPresenter = grid.FindDescendantOfType(); @@ -111,8 +113,7 @@ namespace SourceGit.Views double startY = 0; foreach (var child in rowsPresenter.Children) { - var row = child as DataGridRow; - if (row.IsVisible && row.Bounds.Top <= 0 && row.Bounds.Top > -rowHeight) + if (child is DataGridRow { IsVisible: true, Bounds.Top: <= 0 } row && row.Bounds.Top > -rowHeight) { var test = rowHeight * row.GetIndex() - row.Bounds.Top; if (startY < test) @@ -120,8 +121,11 @@ namespace SourceGit.Views } } + var headerHeight = grid.ColumnHeaderHeight; + startY -= headerHeight; + // Apply scroll offset. - context.PushClip(new Rect(Bounds.Left, Bounds.Top, grid.Columns[0].ActualWidth, Bounds.Height)); + context.PushClip(new Rect(Bounds.Left, Bounds.Top + headerHeight, grid.Columns[0].ActualWidth, Bounds.Height)); context.PushTransform(Matrix.CreateTranslation(0, -startY)); // Calculate bounds. @@ -158,6 +162,7 @@ namespace SourceGit.Views var geo = new StreamGeometry(); var pen = Models.CommitGraph.Pens[line.Color]; + using (var ctx = geo.Open()) { var started = false; @@ -192,7 +197,7 @@ namespace SourceGit.Views if (i < size - 1) { var midY = (last.Y + cur.Y) / 2; - ctx.CubicBezierTo(new Point(last.X, midY + 2), new Point(cur.X, midY - 2), cur); + ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur); } else { @@ -235,7 +240,7 @@ namespace SourceGit.Views public partial class Histories : UserControl { public static readonly StyledProperty NavigationIdProperty = - AvaloniaProperty.Register(nameof(NavigationId), 0); + AvaloniaProperty.Register(nameof(NavigationId)); public long NavigationId { @@ -248,7 +253,7 @@ namespace SourceGit.Views NavigationIdProperty.Changed.AddClassHandler((h, _) => { // Force scroll selected item (current head) into view. see issue #58 - var datagrid = h.commitDataGrid; + var datagrid = h.CommitDataGrid; if (datagrid != null && datagrid.SelectedItems.Count == 1) datagrid.ScrollIntoView(datagrid.SelectedItems[0], null); }); @@ -259,16 +264,16 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnCommitDataGridLayoutUpdated(object sender, EventArgs e) + private void OnCommitDataGridLayoutUpdated(object _1, EventArgs _2) { - commitGraph.InvalidateVisual(); + CommitGraph.InvalidateVisual(); } - private void OnCommitDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnCommitDataGridSelectionChanged(object _, SelectionChangedEventArgs e) { if (DataContext is ViewModels.Histories histories) { - histories.Select(commitDataGrid.SelectedItems); + histories.Select(CommitDataGrid.SelectedItems); } e.Handled = true; } diff --git a/src/Views/Hotkeys.axaml b/src/Views/Hotkeys.axaml index cd89cd50..a808969b 100644 --- a/src/Views/Hotkeys.axaml +++ b/src/Views/Hotkeys.axaml @@ -30,7 +30,7 @@ diff --git a/src/Views/Hotkeys.axaml.cs b/src/Views/Hotkeys.axaml.cs index e318d0d2..3e233009 100644 --- a/src/Views/Hotkeys.axaml.cs +++ b/src/Views/Hotkeys.axaml.cs @@ -10,12 +10,12 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + private void BeginMoveWindow(object _, PointerPressedEventArgs e) { BeginMoveDrag(e); } - private void CloseWindow(object sender, RoutedEventArgs e) + private void CloseWindow(object _1, RoutedEventArgs _2) { Close(); } diff --git a/src/Views/ImageDiffView.axaml b/src/Views/ImageDiffView.axaml index 70a40dbb..e39dc833 100644 --- a/src/Views/ImageDiffView.axaml +++ b/src/Views/ImageDiffView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" - xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -157,16 +156,7 @@ Margin="0" MinHeight="0" Foreground="{DynamicResource Brush.Border1}" - Value="0.5"> - - 0,0,0,4 - 0 - 0 - 8 - 16 - 16 - - + Value="0.5"/> diff --git a/src/Views/ImageDiffView.axaml.cs b/src/Views/ImageDiffView.axaml.cs index bdeda323..bf13e7ba 100644 --- a/src/Views/ImageDiffView.axaml.cs +++ b/src/Views/ImageDiffView.axaml.cs @@ -64,7 +64,7 @@ namespace SourceGit.Views } public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage), null); + AvaloniaProperty.Register(nameof(OldImage)); public Bitmap OldImage { @@ -73,7 +73,7 @@ namespace SourceGit.Views } public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage), null); + AvaloniaProperty.Register(nameof(NewImage)); public Bitmap NewImage { @@ -127,7 +127,7 @@ namespace SourceGit.Views Cursor = new Cursor(StandardCursorType.SizeWestEast); e.Pointer.Capture(this); e.Handled = true; - } + } } protected override void OnPointerReleased(PointerReleasedEventArgs e) @@ -163,7 +163,7 @@ namespace SourceGit.Views _lastInSlider = false; Cursor = null; } - } + } } } @@ -185,13 +185,9 @@ namespace SourceGit.Views private Size GetDesiredSize(Size img, Size available) { - var w = available.Width; - var h = available.Height; - var sw = available.Width / img.Width; var sh = available.Height / img.Height; var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); } @@ -211,7 +207,7 @@ namespace SourceGit.Views } public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage), null); + AvaloniaProperty.Register(nameof(OldImage)); public Bitmap OldImage { @@ -220,7 +216,7 @@ namespace SourceGit.Views } public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage), null); + AvaloniaProperty.Register(nameof(NewImage)); public Bitmap NewImage { @@ -294,13 +290,9 @@ namespace SourceGit.Views private Size GetDesiredSize(Size img, Size available) { - var w = available.Width; - var h = available.Height; - var sw = available.Width / img.Width; var sh = available.Height / img.Height; var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); } diff --git a/src/Views/InitGitFlow.axaml b/src/Views/InitGitFlow.axaml index 632ec9dc..836e39a9 100644 --- a/src/Views/InitGitFlow.axaml +++ b/src/Views/InitGitFlow.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.InitGitFlow" x:DataType="vm:InitGitFlow"> diff --git a/src/Views/InteractiveRebase.axaml b/src/Views/InteractiveRebase.axaml index d4238d65..d0a4cc33 100644 --- a/src/Views/InteractiveRebase.axaml +++ b/src/Views/InteractiveRebase.axaml @@ -31,7 +31,7 @@ diff --git a/src/Views/InteractiveRebase.axaml.cs b/src/Views/InteractiveRebase.axaml.cs index 491d4bab..287a0be1 100644 --- a/src/Views/InteractiveRebase.axaml.cs +++ b/src/Views/InteractiveRebase.axaml.cs @@ -11,17 +11,17 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + private void BeginMoveWindow(object _, PointerPressedEventArgs e) { BeginMoveDrag(e); } - private void CloseWindow(object sender, RoutedEventArgs e) + private void CloseWindow(object _1, RoutedEventArgs _2) { Close(); } - private void OnMoveItemUp(object sender, RoutedEventArgs e) + private void OnMoveItemUp(object sender, RoutedEventArgs e) { if (sender is Control control && DataContext is ViewModels.InteractiveRebase vm) { @@ -39,14 +39,12 @@ namespace SourceGit.Views } } - private void OnDataGridKeyDown(object sender, KeyEventArgs e) + private void OnDataGridKeyDown(object sender, KeyEventArgs e) { - var datagrid = sender as DataGrid; - var item = datagrid.SelectedItem as ViewModels.InteractiveRebaseItem; + var item = (sender as DataGrid)?.SelectedItem as ViewModels.InteractiveRebaseItem; if (item == null) return; - var vm = DataContext as ViewModels.InteractiveRebase; if (e.Key == Key.P) item.SetAction(Models.InteractiveRebaseAction.Pick); else if (e.Key == Key.E) @@ -61,11 +59,14 @@ namespace SourceGit.Views item.SetAction(Models.InteractiveRebaseAction.Drop); } - private async void StartJobs(object sender, RoutedEventArgs e) + private async void StartJobs(object _1, RoutedEventArgs _2) { + var vm = DataContext as ViewModels.InteractiveRebase; + if (vm == null) + return; + Running.IsVisible = true; Running.IsIndeterminate = true; - var vm = DataContext as ViewModels.InteractiveRebase; await vm.Start(); Running.IsIndeterminate = false; Running.IsVisible = false; diff --git a/src/Views/LFSLocks.axaml b/src/Views/LFSLocks.axaml index 54a4fa0a..24abb8a3 100644 --- a/src/Views/LFSLocks.axaml +++ b/src/Views/LFSLocks.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" - xmlns:c="using:SourceGit.Converters" xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LFSLocks" @@ -30,7 +29,7 @@ diff --git a/src/Views/LFSLocks.axaml.cs b/src/Views/LFSLocks.axaml.cs index a46674af..b62bc327 100644 --- a/src/Views/LFSLocks.axaml.cs +++ b/src/Views/LFSLocks.axaml.cs @@ -11,12 +11,12 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + private void BeginMoveWindow(object _, PointerPressedEventArgs e) { BeginMoveDrag(e); } - private void CloseWindow(object sender, RoutedEventArgs e) + private void CloseWindow(object _1, RoutedEventArgs _2) { Close(); } diff --git a/src/Views/LFSTrackCustomPattern.axaml b/src/Views/LFSTrackCustomPattern.axaml index 40403d4a..36eaef65 100644 --- a/src/Views/LFSTrackCustomPattern.axaml +++ b/src/Views/LFSTrackCustomPattern.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LFSTrackCustomPattern" x:DataType="vm:LFSTrackCustomPattern"> diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index b286a5c2..f8bbf93e 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -4,8 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:s="using:SourceGit" xmlns:vm="using:SourceGit.ViewModels" - xmlns:m="using:SourceGit.Models" - xmlns:c="using:SourceGit.Converters" xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Launcher" @@ -75,110 +73,14 @@ - - - + + + - - - - - - + + - - - - - - - - - - - - - - - - @@ -123,16 +122,7 @@ IsSnapToTickEnabled="True" VerticalAlignment="Center" Foreground="{DynamicResource Brush.Border1}" - Value="{Binding MaxHistoryCommits, Mode=TwoWay}"> - - 0,0,0,4 - 0 - 0 - 8 - 16 - 16 - - + Value="{Binding MaxHistoryCommits, Mode=TwoWay}"/> @@ -205,7 +195,7 @@ MinHeight="28" Padding="8,0" HorizontalAlignment="Stretch" - ItemsSource="{Binding #me.InstalledMonospaceFonts}" + ItemsSource="{Binding #ThisControl.InstalledMonospaceFonts}" SelectedItem="{Binding MonospaceFont, Mode=TwoWay}"> @@ -229,15 +219,15 @@ Value="{Binding DefaultFontSize, Mode=TwoWay}"/> + Text="{Binding ThemeOverrides, Mode=TwoWay}"> - @@ -273,12 +263,12 @@ + Text="{Binding #ThisControl.GitVersion}" + IsVisible="{Binding #ThisControl.GitVersion, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + IsVisible="{Binding #ThisControl.GitVersion, Converter={x:Static c:StringConverters.UnderRecommendGitVersion}}"> @@ -344,7 +334,7 @@ + SelectedItem="{Binding #ThisControl.CRLFMode, Mode=TwoWay}"> @@ -409,10 +399,6 @@ - - - - + SelectedItem="{Binding #ThisControl.GPGFormat, Mode=TwoWay}"> @@ -439,13 +425,13 @@ Text="{DynamicResource Text.Preference.GPG.Path}" HorizontalAlignment="Right" Margin="0,0,16,0" - IsVisible="{Binding #me.GPGFormat.NeedFindProgram}"/> + IsVisible="{Binding #ThisControl.GPGFormat.NeedFindProgram}"/> + IsVisible="{Binding #ThisControl.GPGFormat.NeedFindProgram}"> - - + + + + + + - - + + + + + - - + + + - + - - - - - - - - - - - - - - - - - - - - - - + + + - + + + + + + + + + + + + + - - - - - - - + + + + + - - - - - + + + + + + + + - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - + + + + + + + + + + + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - + IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}" + SelectionChanged="OnTagDataGridSelectionChanged" + ContextRequested="OnTagContextRequested" + PropertyChanged="OnLeftSidebarDataGridPropertyChanged"> + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -629,123 +361,328 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + - - - - - - - - + + + + - - - + + - - - - - - - - - - - - - - + + + + - + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 0e3d7a19..eead96ef 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -21,59 +21,17 @@ namespace SourceGit.Views UpdateLeftSidebarLayout(); } - private void OpenWithExternalTools(object sender, RoutedEventArgs e) - { - if (sender is Button button && DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForExternalTools(); - button.OpenContextMenu(menu); - e.Handled = true; - } - } - - private void OpenGitFlowMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitFlow(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OpenGitLFSMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitLFS(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private async void OpenStatistics(object _, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) - { - var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(owner); - e.Handled = true; - } - } - private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { - if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true}) - txtSearchCommitsBox.Focus(); + if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true }) + TxtSearchCommitsBox.Focus(); } private void OnSearchKeyDown(object _, KeyEventArgs e) { if (e.Key == Key.Enter) { - if (DataContext is ViewModels.Repository repo) + if (DataContext is ViewModels.Repository repo && !string.IsNullOrWhiteSpace(repo.SearchCommitFilter)) repo.StartSearchCommits(); e.Handled = true; @@ -86,7 +44,7 @@ namespace SourceGit.Views { repo.NavigateToCommit(commit.SHA); } - + e.Handled = true; } @@ -98,22 +56,22 @@ namespace SourceGit.Views private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; + RemoteBranchTree.UnselectAll(); + TagsList.SelectedItem = null; } - + private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2) { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; + LocalBranchTree.UnselectAll(); + TagsList.SelectedItem = null; } private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _) { if (sender is DataGrid { SelectedItem: Models.Tag tag }) { - localBranchTree.UnselectAll(); - remoteBranchTree.UnselectAll(); + LocalBranchTree.UnselectAll(); + RemoteBranchTree.UnselectAll(); if (DataContext is ViewModels.Repository repo) repo.NavigateToCommit(tag.SHA); @@ -122,17 +80,16 @@ namespace SourceGit.Views private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: Models.Tag tag } grid && DataContext is ViewModels.Repository repo) { - var tag = datagrid.SelectedItem as Models.Tag; var menu = repo.CreateContextMenuForTag(tag); - datagrid.OpenContextMenu(menu); + grid.OpenContextMenu(menu); } e.Handled = true; } - private void OnToggleTagFilter(object sender, RoutedEventArgs e) + private void OnTagFilterIsCheckedChanged(object sender, RoutedEventArgs e) { if (sender is ToggleButton { DataContext: Models.Tag tag } toggle && DataContext is ViewModels.Repository repo) { @@ -144,11 +101,10 @@ namespace SourceGit.Views private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: string submodule } grid && DataContext is ViewModels.Repository repo) { - var submodule = datagrid.SelectedItem as string; var menu = repo.CreateContextMenuForSubmodule(submodule); - datagrid.OpenContextMenu(menu); + grid.OpenContextMenu(menu); } e.Handled = true; @@ -156,9 +112,8 @@ namespace SourceGit.Views private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) { - if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: string submodule } && DataContext is ViewModels.Repository repo) { - var submodule = grid.SelectedItem as string; repo.OpenSubmodule(submodule); } @@ -167,9 +122,8 @@ namespace SourceGit.Views private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: Models.Worktree worktree } grid && DataContext is ViewModels.Repository repo) { - var worktree = grid.SelectedItem as Models.Worktree; var menu = repo.CreateContextMenuForWorktree(worktree); grid.OpenContextMenu(menu); } @@ -179,9 +133,8 @@ namespace SourceGit.Views private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) { - if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + if (sender is DataGrid { SelectedItem: Models.Worktree worktree } && DataContext is ViewModels.Repository repo) { - var worktree = grid.SelectedItem as Models.Worktree; repo.OpenWorktree(worktree); } @@ -205,13 +158,13 @@ namespace SourceGit.Views if (!IsLoaded) return; - var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; - var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0; - var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0; + var leftHeight = LeftSidebarGroups.Bounds.Height - 28.0 * 5; + var localBranchRows = vm.IsLocalBranchGroupExpanded ? LocalBranchTree.Rows.Count : 0; + var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; - var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; - var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; - var desiredWorktree = vm.IsWorktreeGroupExpanded ? worktreeList.RowHeight * vm.Worktrees.Count : 0; + var desiredTag = vm.IsTagGroupExpanded ? TagsList.RowHeight * vm.VisibleTags.Count : 0; + var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? SubmoduleList.RowHeight * vm.Submodules.Count : 0; + var desiredWorktree = vm.IsWorktreeGroupExpanded ? WorktreeList.RowHeight * vm.Worktrees.Count : 0; var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; var hasOverflow = (desiredBranches + desiredOthers > leftHeight); @@ -228,7 +181,7 @@ namespace SourceGit.Views } leftHeight -= height; - tagsList.Height = height; + TagsList.Height = height; hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; } @@ -245,7 +198,7 @@ namespace SourceGit.Views } leftHeight -= height; - submoduleList.Height = height; + SubmoduleList.Height = height; hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; } @@ -262,7 +215,7 @@ namespace SourceGit.Views } leftHeight -= height; - worktreeList.Height = height; + WorktreeList.Height = height; } if (desiredBranches > leftHeight) @@ -276,28 +229,28 @@ namespace SourceGit.Views { if (local < half) { - localBranchTree.Height = local; - remoteBranchTree.Height = leftHeight - local; + LocalBranchTree.Height = local; + RemoteBranchTree.Height = leftHeight - local; } else if (remote < half) { - remoteBranchTree.Height = remote; - localBranchTree.Height = leftHeight - remote; + RemoteBranchTree.Height = remote; + LocalBranchTree.Height = leftHeight - remote; } else { - localBranchTree.Height = half; - remoteBranchTree.Height = half; + LocalBranchTree.Height = half; + RemoteBranchTree.Height = half; } } else { - localBranchTree.Height = leftHeight; + LocalBranchTree.Height = leftHeight; } } else if (vm.IsRemoteGroupExpanded) { - remoteBranchTree.Height = leftHeight; + RemoteBranchTree.Height = leftHeight; } } else @@ -305,17 +258,15 @@ namespace SourceGit.Views if (vm.IsLocalBranchGroupExpanded) { var height = localBranchRows * 24; - localBranchTree.Height = height; + LocalBranchTree.Height = height; } if (vm.IsRemoteGroupExpanded) { var height = remoteBranchRows * 24; - remoteBranchTree.Height = height; + RemoteBranchTree.Height = height; } } - - leftSidebarGroups.InvalidateMeasure(); } } } diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 2d466a08..aa7501a2 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -2,10 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RepositoryConfigure" x:DataType="vm:RepositoryConfigure"> @@ -15,11 +13,11 @@ Text="{DynamicResource Text.Configure}"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs new file mode 100644 index 00000000..40f90291 --- /dev/null +++ b/src/Views/RepositoryToolbar.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class RepositoryToolbar : UserControl + { + public RepositoryToolbar() + { + InitializeComponent(); + } + + private void OpenWithExternalTools(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForExternalTools(); + button.OpenContextMenu(menu); + e.Handled = true; + } + } + + private void OpenGitFlowMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitFlow(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OpenGitLFSMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitLFS(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private async void OpenStatistics(object _, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) + { + var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; + await dialog.ShowDialog(owner); + e.Handled = true; + } + } + } +} + diff --git a/src/Views/Revert.axaml b/src/Views/Revert.axaml index c192dde4..453da420 100644 --- a/src/Views/Revert.axaml +++ b/src/Views/Revert.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -13,12 +12,12 @@ Classes="bold" Text="{DynamicResource Text.Revert}"/> - - + diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 27976d26..745e80f2 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -34,7 +34,7 @@ - + diff --git a/src/Views/RevisionFileTreeView.axaml b/src/Views/RevisionFileTreeView.axaml new file mode 100644 index 00000000..f93b2c4b --- /dev/null +++ b/src/Views/RevisionFileTreeView.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs new file mode 100644 index 00000000..ffed4a92 --- /dev/null +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class RevisionFileTreeNodeToggleButton : ToggleButton + { + protected override Type StyleKeyOverride => typeof(ToggleButton); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.RevisionFileTreeNode { IsFolder: true } node) + { + var tree = this.FindAncestorOfType(); + tree?.ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + } + + public class RevisionTreeNodeIcon : UserControl + { + public static readonly StyledProperty NodeProperty = + AvaloniaProperty.Register(nameof(Node)); + + public ViewModels.RevisionFileTreeNode Node + { + get => GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + static RevisionTreeNodeIcon() + { + NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent()); + } + + private void UpdateContent() + { + var node = Node; + if (node?.Backend == null) + { + Content = null; + return; + } + + var obj = node.Backend; + switch (obj.Type) + { + case Models.ObjectType.Blob: + CreateContent("Icons.File", new Thickness(0, 0, 0, 0)); + break; + case Models.ObjectType.Commit: + CreateContent("Icons.Submodule", new Thickness(0, 0, 0, 0)); + break; + default: + CreateContent(node.IsExpanded ? "Icons.Folder.Open" : "Icons.Folder.Fill", new Thickness(0, 2, 0, 0), Brushes.Goldenrod); + break; + } + } + + private void CreateContent(string iconKey, Thickness margin, IBrush fill = null) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + var icon = new Avalonia.Controls.Shapes.Path() + { + Width = 14, + Height = 14, + Margin = margin, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Data = geo, + }; + + if (fill != null) + icon.Fill = fill; + + Content = icon; + } + } + + public partial class RevisionFileTreeView : UserControl + { + public static readonly StyledProperty RevisionProperty = + AvaloniaProperty.Register(nameof(Revision)); + + public string Revision + { + get => GetValue(RevisionProperty); + set => SetValue(RevisionProperty, value); + } + + public AvaloniaList Rows + { + get => _rows; + } + + public RevisionFileTreeView() + { + InitializeComponent(); + } + + public void ToggleNodeIsExpanded(ViewModels.RevisionFileTreeNode node) + { + _disableSelectionChangingEvent = true; + node.IsExpanded = !node.IsExpanded; + + var depth = node.Depth; + var idx = _rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subtree = GetChildrenOfTreeNode(node); + if (subtree != null && subtree.Count > 0) + { + var subrows = new List(); + MakeRows(subrows, subtree, depth + 1); + _rows.InsertRange(idx + 1, subrows); + } + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < _rows.Count; i++) + { + var row = _rows[i]; + if (row.Depth <= depth) + break; + + removeCount++; + } + _rows.RemoveRange(idx + 1, removeCount); + } + + _disableSelectionChangingEvent = false; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RevisionProperty) + { + _tree.Clear(); + _rows.Clear(); + + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null || vm.Commit == null) + { + GC.Collect(); + return; + } + + var objects = vm.GetRevisionFilesUnderFolder(null); + if (objects == null || objects.Count == 0) + { + GC.Collect(); + return; + } + + foreach (var obj in objects) + _tree.Add(new ViewModels.RevisionFileTreeNode { Backend = obj }); + + _tree.Sort((l, r) => + { + if (l.IsFolder == r.IsFolder) + return string.Compare(l.Name, r.Name, StringComparison.Ordinal); + return l.IsFolder ? -1 : 1; + }); + + var topTree = new List(); + MakeRows(topTree, _tree, 0); + _rows.AddRange(topTree); + GC.Collect(); + } + } + + private void OnTreeNodeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (DataContext is ViewModels.CommitDetail vm && + sender is Grid { DataContext: ViewModels.RevisionFileTreeNode { Backend: { } obj } } grid) + { + if (obj.Type != Models.ObjectType.Tree) + { + var menu = vm.CreateRevisionFileContextMenu(obj); + grid.OpenContextMenu(menu); + } + } + + e.Handled = true; + } + + private void OnTreeNodeDoubleTapped(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.RevisionFileTreeNode { IsFolder: true } node }) + { + var posX = e.GetPosition(this).X; + if (posX < node.Depth * 16 + 16) + return; + + ToggleNodeIsExpanded(node); + } + } + + private void OnRowsSelectionChanged(object sender, SelectionChangedEventArgs _) + { + if (_disableSelectionChangingEvent) + return; + + if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode node } && DataContext is ViewModels.CommitDetail vm) + { + if (!node.IsFolder) + vm.ViewRevisionFile(node.Backend); + else + vm.ViewRevisionFile(null); + } + } + + private List GetChildrenOfTreeNode(ViewModels.RevisionFileTreeNode node) + { + if (!node.IsFolder) + return null; + + if (node.Children.Count > 0) + return node.Children; + + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return null; + + var objects = vm.GetRevisionFilesUnderFolder(node.Backend.Path + "/"); + if (objects == null || objects.Count == 0) + return null; + + foreach (var obj in objects) + node.Children.Add(new ViewModels.RevisionFileTreeNode() { Backend = obj }); + + node.Children.Sort((l, r) => + { + if (l.IsFolder == r.IsFolder) + return string.Compare(l.Name, r.Name, StringComparison.Ordinal); + return l.IsFolder ? -1 : 1; + }); + + return node.Children; + } + + private void MakeRows(List rows, List nodes, int depth) + { + foreach (var node in nodes) + { + node.Depth = depth; + rows.Add(node); + + if (!node.IsExpanded || !node.IsFolder) + continue; + + MakeRows(rows, node.Children, depth + 1); + } + } + + private List _tree = new List(); + private AvaloniaList _rows = new AvaloniaList(); + private bool _disableSelectionChangingEvent = false; + } +} diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 82a06b9f..0004ddb3 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -17,17 +17,7 @@ - - - - - - - - - - - + Children { get; set; } = new List(); - - public bool IsFolder => Backend != null && Backend.Type == Models.ObjectType.Tree; - public string Name => Backend != null ? Path.GetFileName(Backend.Path) : string.Empty; - } - - public class RevisionFileTreeView : UserControl - { - public static readonly StyledProperty RevisionProperty = - AvaloniaProperty.Register(nameof(Revision), null); - - public string Revision - { - get => GetValue(RevisionProperty); - set => SetValue(RevisionProperty, value); - } - - public Models.Object SelectedObject - { - get; - private set; - } = null; - - protected override Type StyleKeyOverride => typeof(UserControl); - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == RevisionProperty) - { - SelectedObject = null; - - if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable) - disposable.Dispose(); - - var vm = DataContext as ViewModels.CommitDetail; - if (vm == null || vm.Commit == null) - { - Content = null; - GC.Collect(); - return; - } - - var objects = vm.GetRevisionFilesUnderFolder(null); - if (objects == null || objects.Count == 0) - { - Content = null; - GC.Collect(); - return; - } - - var toplevelObjects = new List(); - foreach (var obj in objects) - toplevelObjects.Add(new RevisionFileTreeNode() { Backend = obj }); - - toplevelObjects.Sort((l, r) => - { - if (l.IsFolder == r.IsFolder) - return l.Name.CompareTo(r.Name); - return l.IsFolder ? -1 : 1; - }); - - var template = this.FindResource("RevisionFileTreeNodeTemplate") as IDataTemplate; - var source = new HierarchicalTreeDataGridSource(toplevelObjects) - { - Columns = - { - new HierarchicalExpanderColumn( - new TemplateColumn(null, template, null, GridLength.Auto), - GetChildrenOfTreeNode, - x => x.IsFolder, - x => x.IsExpanded) - } - }; - - var selection = new Models.TreeDataGridSelectionModel(source, GetChildrenOfTreeNode); - selection.SingleSelect = true; - selection.SelectionChanged += (s, _) => - { - if (s is Models.TreeDataGridSelectionModel model) - { - var node = model.SelectedItem; - var detail = DataContext as ViewModels.CommitDetail; - - if (node != null && !node.IsFolder) - { - SelectedObject = node.Backend; - detail.ViewRevisionFile(node.Backend); - } - else - { - SelectedObject = null; - detail.ViewRevisionFile(null); - } - } - }; - - source.Selection = selection; - Content = new TreeDataGrid() - { - AutoDragDropRows = false, - ShowColumnHeaders = false, - CanUserResizeColumns = false, - CanUserSortColumns = false, - Source = source, - }; - - GC.Collect(); - } - } - - private List GetChildrenOfTreeNode(RevisionFileTreeNode node) - { - if (!node.IsFolder) - return null; - - if (node.Children.Count > 0) - return node.Children; - - var vm = DataContext as ViewModels.CommitDetail; - if (vm == null) - return null; - - var objects = vm.GetRevisionFilesUnderFolder(node.Backend.Path + "/"); - if (objects == null || objects.Count == 0) - return null; - - foreach (var obj in objects) - node.Children.Add(new RevisionFileTreeNode() { Backend = obj }); - - node.Children.Sort((l, r) => - { - if (l.IsFolder == r.IsFolder) - return l.Name.CompareTo(r.Name); - return l.IsFolder ? -1 : 1; - }); - - return node.Children; - } - } - public class RevisionTextFileView : TextEditor { protected override Type StyleKeyOverride => typeof(TextEditor); @@ -199,8 +49,7 @@ namespace SourceGit.Views { base.OnDataContextChanged(e); - var source = DataContext as Models.RevisionTextFile; - if (source != null) + if (DataContext is Models.RevisionTextFile source) Text = source.Content; else Text = string.Empty; @@ -212,21 +61,24 @@ namespace SourceGit.Views if (string.IsNullOrEmpty(selected)) return; - var icon = new Avalonia.Controls.Shapes.Path(); - icon.Width = 10; - icon.Height = 10; - icon.Stretch = Stretch.Uniform; - icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry; - - var copy = new MenuItem(); - copy.Header = App.Text("Copy"); - copy.Icon = icon; - copy.Click += (o, ev) => + var copy = new MenuItem() { Header = App.Text("Copy") }; + copy.Click += (_, ev) => { App.CopyText(selected); ev.Handled = true; }; + if (this.FindResource("Icons.Copy") is Geometry geo) + { + copy.Icon = new Avalonia.Controls.Shapes.Path() + { + Width = 10, + Height = 10, + Stretch = Stretch.Uniform, + Data = geo, + }; + } + var menu = new ContextMenu(); menu.Items.Add(copy); @@ -241,19 +93,5 @@ namespace SourceGit.Views { InitializeComponent(); } - - private void OnRevisionFileTreeViewContextRequested(object sender, ContextRequestedEventArgs e) - { - if (DataContext is ViewModels.CommitDetail vm && sender is RevisionFileTreeView view) - { - if (view.SelectedObject != null && view.SelectedObject.Type != Models.ObjectType.Tree) - { - var menu = vm.CreateRevisionFileContextMenu(view.SelectedObject); - view.OpenContextMenu(menu); - } - } - - e.Handled = true; - } } } diff --git a/src/Views/SelfUpdate.axaml b/src/Views/SelfUpdate.axaml index 37784fa8..e7933c64 100644 --- a/src/Views/SelfUpdate.axaml +++ b/src/Views/SelfUpdate.axaml @@ -32,7 +32,7 @@ diff --git a/src/Views/SelfUpdate.axaml.cs b/src/Views/SelfUpdate.axaml.cs index 2a0de6e4..800d9295 100644 --- a/src/Views/SelfUpdate.axaml.cs +++ b/src/Views/SelfUpdate.axaml.cs @@ -11,17 +11,17 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + private void BeginMoveWindow(object _, PointerPressedEventArgs e) { BeginMoveDrag(e); } - private void CloseWindow(object sender, RoutedEventArgs e) + private void CloseWindow(object _1, RoutedEventArgs _2) { Close(); } - private void GotoDownload(object sender, RoutedEventArgs e) + private void GotoDownload(object _, RoutedEventArgs e) { Native.OS.OpenBrowser("https://github.com/sourcegit-scm/sourcegit/releases/latest"); e.Handled = true; @@ -29,9 +29,9 @@ namespace SourceGit.Views private void IgnoreThisVersion(object sender, RoutedEventArgs e) { - var button = sender as Button; - var ver = button.DataContext as Models.Version; - ViewModels.Preference.Instance.IgnoreUpdateTag = ver.TagName; + if (sender is Button { DataContext: Models.Version ver }) + ViewModels.Preference.Instance.IgnoreUpdateTag = ver.TagName; + Close(); e.Handled = true; } diff --git a/src/Views/StandaloneCommitMessageEditor.axaml b/src/Views/StandaloneCommitMessageEditor.axaml new file mode 100644 index 00000000..feee7f1f --- /dev/null +++ b/src/Views/StandaloneCommitMessageEditor.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Statistics.axaml.cs b/src/Views/Statistics.axaml.cs index a6e13b4e..2c2dc476 100644 --- a/src/Views/Statistics.axaml.cs +++ b/src/Views/Statistics.axaml.cs @@ -40,7 +40,7 @@ namespace SourceGit.Views } public static readonly StyledProperty> SamplesProperty = - AvaloniaProperty.Register>(nameof(Samples), null); + AvaloniaProperty.Register>(nameof(Samples)); public List Samples { @@ -50,7 +50,7 @@ namespace SourceGit.Views static Chart() { - SamplesProperty.Changed.AddClassHandler((c, e) => + SamplesProperty.Changed.AddClassHandler((c, _) => { c._hitBoxes.Clear(); c._lastHitIdx = -1; @@ -87,7 +87,7 @@ namespace SourceGit.Views maxV = (int)Math.Ceiling(maxV / 500.0) * 500; var typeface = new Typeface("fonts:SourceGit#JetBrains Mono"); - var pen = new Pen(LineBrush, 1); + var pen = new Pen(LineBrush); var width = Bounds.Width; var height = Bounds.Height; @@ -228,12 +228,12 @@ namespace SourceGit.Views InitializeComponent(); } - private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + private void BeginMoveWindow(object _, PointerPressedEventArgs e) { BeginMoveDrag(e); } - private void CloseWindow(object sender, RoutedEventArgs e) + private void CloseWindow(object _1, RoutedEventArgs _2) { Close(); } diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 5e925686..f06bc175 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -22,10 +22,10 @@ using AvaloniaEdit.Utils; namespace SourceGit.Views { - public class IThemedTextDiffPresenter : TextEditor + public class ThemedTextDiffPresenter : TextEditor { public static readonly StyledProperty FileNameProperty = - AvaloniaProperty.Register(nameof(FileName), string.Empty); + AvaloniaProperty.Register(nameof(FileName), string.Empty); public string FileName { @@ -34,7 +34,7 @@ namespace SourceGit.Views } public static readonly StyledProperty LineBrushProperty = - AvaloniaProperty.Register(nameof(LineBrush), new SolidColorBrush(Colors.DarkGray)); + AvaloniaProperty.Register(nameof(LineBrush), new SolidColorBrush(Colors.DarkGray)); public IBrush LineBrush { @@ -43,7 +43,7 @@ namespace SourceGit.Views } public static readonly StyledProperty EmptyContentBackgroundProperty = - AvaloniaProperty.Register(nameof(EmptyContentBackground), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0))); + AvaloniaProperty.Register(nameof(EmptyContentBackground), new SolidColorBrush(Color.FromArgb(60, 0, 0, 0))); public IBrush EmptyContentBackground { @@ -52,7 +52,7 @@ namespace SourceGit.Views } public static readonly StyledProperty AddedContentBackgroundProperty = - AvaloniaProperty.Register(nameof(AddedContentBackground), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0))); + AvaloniaProperty.Register(nameof(AddedContentBackground), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0))); public IBrush AddedContentBackground { @@ -61,7 +61,7 @@ namespace SourceGit.Views } public static readonly StyledProperty DeletedContentBackgroundProperty = - AvaloniaProperty.Register(nameof(DeletedContentBackground), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0))); + AvaloniaProperty.Register(nameof(DeletedContentBackground), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0))); public IBrush DeletedContentBackground { @@ -70,7 +70,7 @@ namespace SourceGit.Views } public static readonly StyledProperty AddedHighlightBrushProperty = - AvaloniaProperty.Register(nameof(AddedHighlightBrush), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0))); + AvaloniaProperty.Register(nameof(AddedHighlightBrush), new SolidColorBrush(Color.FromArgb(90, 0, 255, 0))); public IBrush AddedHighlightBrush { @@ -79,7 +79,7 @@ namespace SourceGit.Views } public static readonly StyledProperty DeletedHighlightBrushProperty = - AvaloniaProperty.Register(nameof(DeletedHighlightBrush), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0))); + AvaloniaProperty.Register(nameof(DeletedHighlightBrush), new SolidColorBrush(Color.FromArgb(80, 255, 0, 0))); public IBrush DeletedHighlightBrush { @@ -88,7 +88,7 @@ namespace SourceGit.Views } public static readonly StyledProperty IndicatorForegroundProperty = - AvaloniaProperty.Register(nameof(IndicatorForeground), Brushes.Gray); + AvaloniaProperty.Register(nameof(IndicatorForeground), Brushes.Gray); public IBrush IndicatorForeground { @@ -97,7 +97,7 @@ namespace SourceGit.Views } public static readonly StyledProperty UseSyntaxHighlightingProperty = - AvaloniaProperty.Register(nameof(UseSyntaxHighlighting), false); + AvaloniaProperty.Register(nameof(UseSyntaxHighlighting)); public bool UseSyntaxHighlighting { @@ -106,7 +106,7 @@ namespace SourceGit.Views } public static readonly StyledProperty ShowHiddenSymbolsProperty = - AvaloniaProperty.Register(nameof(ShowHiddenSymbols), false); + AvaloniaProperty.Register(nameof(ShowHiddenSymbols)); public bool ShowHiddenSymbols { @@ -116,7 +116,7 @@ namespace SourceGit.Views protected override Type StyleKeyOverride => typeof(TextEditor); - public IThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc) + protected ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc) { IsReadOnly = true; ShowLineNumbers = false; @@ -168,7 +168,7 @@ namespace SourceGit.Views } } - protected void UpdateTextMate() + private void UpdateTextMate() { if (UseSyntaxHighlighting) { @@ -197,9 +197,9 @@ namespace SourceGit.Views protected IVisualLineTransformer _lineStyleTransformer = null; } - public class CombinedTextDiffPresenter : IThemedTextDiffPresenter + public class CombinedTextDiffPresenter : ThemedTextDiffPresenter { - public class LineNumberMargin : AbstractMargin + private class LineNumberMargin : AbstractMargin { public LineNumberMargin(CombinedTextDiffPresenter editor, bool isOldLine) { @@ -247,18 +247,16 @@ namespace SourceGit.Views { return new Size(32, 0); } - else - { - var typeface = TextView.CreateTypeface(); - var test = new FormattedText( - $"{_editor.DiffData.MaxLineNumber}", - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - _editor.FontSize, - Brushes.White); - return new Size(test.Width, 0); - } + + var typeface = TextView.CreateTypeface(); + var test = new FormattedText( + $"{_editor.DiffData.MaxLineNumber}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.White); + return new Size(test.Width, 0); } protected override void OnDataContextChanged(EventArgs e) @@ -271,7 +269,7 @@ namespace SourceGit.Views private readonly bool _isOldLine; } - public class VerticalSeperatorMargin : AbstractMargin + private class VerticalSeperatorMargin : AbstractMargin { public VerticalSeperatorMargin(CombinedTextDiffPresenter editor) { @@ -280,7 +278,7 @@ namespace SourceGit.Views public override void Render(DrawingContext context) { - var pen = new Pen(_editor.LineBrush, 1); + var pen = new Pen(_editor.LineBrush); context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); } @@ -292,7 +290,7 @@ namespace SourceGit.Views private readonly CombinedTextDiffPresenter _editor = null; } - public class LineBackgroundRenderer : IBackgroundRenderer + private class LineBackgroundRenderer : IBackgroundRenderer { public KnownLayer Layer => KnownLayer.Background; @@ -344,7 +342,7 @@ namespace SourceGit.Views private readonly CombinedTextDiffPresenter _editor = null; } - public class LineStyleTransformer : DocumentColorizingTransformer + private class LineStyleTransformer : DocumentColorizingTransformer { public LineStyleTransformer(CombinedTextDiffPresenter editor) { @@ -405,7 +403,7 @@ namespace SourceGit.Views base.OnApplyTemplate(e); var scroller = (ScrollViewer)e.NameScope.Find("PART_ScrollViewer"); - scroller.Bind(ScrollViewer.OffsetProperty, new Binding("SyncScrollOffset", BindingMode.TwoWay)); + scroller?.Bind(ScrollViewer.OffsetProperty, new Binding("SyncScrollOffset", BindingMode.TwoWay)); } protected override void OnLoaded(RoutedEventArgs e) @@ -466,7 +464,7 @@ namespace SourceGit.Views var copy = new MenuItem(); copy.Header = App.Text("Copy"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += (o, ev) => + copy.Click += (_, ev) => { App.CopyText(SelectedText); ev.Handled = true; @@ -479,9 +477,9 @@ namespace SourceGit.Views } } - public class SingleSideTextDiffPresenter : IThemedTextDiffPresenter + public class SingleSideTextDiffPresenter : ThemedTextDiffPresenter { - public class LineNumberMargin : AbstractMargin + private class LineNumberMargin : AbstractMargin { public LineNumberMargin(SingleSideTextDiffPresenter editor) { @@ -529,18 +527,16 @@ namespace SourceGit.Views { return new Size(32, 0); } - else - { - var typeface = TextView.CreateTypeface(); - var test = new FormattedText( - $"{_editor.DiffData.MaxLineNumber}", - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - _editor.FontSize, - Brushes.White); - return new Size(test.Width, 0); - } + + var typeface = TextView.CreateTypeface(); + var test = new FormattedText( + $"{_editor.DiffData.MaxLineNumber}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.White); + return new Size(test.Width, 0); } protected override void OnDataContextChanged(EventArgs e) @@ -552,7 +548,7 @@ namespace SourceGit.Views private readonly SingleSideTextDiffPresenter _editor; } - public class VerticalSeperatorMargin : AbstractMargin + private class VerticalSeperatorMargin : AbstractMargin { public VerticalSeperatorMargin(SingleSideTextDiffPresenter editor) { @@ -561,7 +557,7 @@ namespace SourceGit.Views public override void Render(DrawingContext context) { - var pen = new Pen(_editor.LineBrush, 1); + var pen = new Pen(_editor.LineBrush); context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); } @@ -573,7 +569,7 @@ namespace SourceGit.Views private readonly SingleSideTextDiffPresenter _editor = null; } - public class LineBackgroundRenderer : IBackgroundRenderer + private class LineBackgroundRenderer : IBackgroundRenderer { public KnownLayer Layer => KnownLayer.Background; @@ -626,7 +622,7 @@ namespace SourceGit.Views private readonly SingleSideTextDiffPresenter _editor = null; } - public class LineStyleTransformer : DocumentColorizingTransformer + private class LineStyleTransformer : DocumentColorizingTransformer { public LineStyleTransformer(SingleSideTextDiffPresenter editor) { @@ -776,7 +772,7 @@ namespace SourceGit.Views var copy = new MenuItem(); copy.Header = App.Text("Copy"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); - copy.Click += (o, ev) => + copy.Click += (_, ev) => { App.CopyText(SelectedText); ev.Handled = true; @@ -794,7 +790,7 @@ namespace SourceGit.Views public partial class TextDiffView : UserControl { public static readonly StyledProperty UseSideBySideDiffProperty = - AvaloniaProperty.Register(nameof(UseSideBySideDiff), false); + AvaloniaProperty.Register(nameof(UseSideBySideDiff)); public bool UseSideBySideDiff { @@ -804,7 +800,7 @@ namespace SourceGit.Views static TextDiffView() { - UseSideBySideDiffProperty.Changed.AddClassHandler((v, e) => + UseSideBySideDiffProperty.Changed.AddClassHandler((v, _) => { if (v.DataContext is Models.TextDiff diff) { @@ -842,11 +838,7 @@ namespace SourceGit.Views return; if (startLine > endLine) - { - var tmp = startLine; - startLine = endLine; - endLine = tmp; - } + (startLine, endLine) = (endLine, startLine); var selection = GetUnifiedSelection(diff, startLine, endLine, isOldSide); if (!selection.HasChanges) @@ -868,7 +860,7 @@ namespace SourceGit.Views stage.Click += (_, e) => { var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy.StageChanges(new List { change }); + workcopy?.StageChanges(new List { change }); e.Handled = true; }; @@ -878,7 +870,7 @@ namespace SourceGit.Views discard.Click += (_, e) => { var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy.Discard(new List { change }, true); + workcopy?.Discard(new List { change }, true); e.Handled = true; }; @@ -893,7 +885,7 @@ namespace SourceGit.Views unstage.Click += (_, e) => { var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy.UnstageChanges(new List { change }); + workcopy?.UnstageChanges(new List { change }); e.Handled = true; }; @@ -903,7 +895,7 @@ namespace SourceGit.Views discard.Click += (_, e) => { var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; - workcopy.Discard(new List { change }, false); + workcopy?.Discard(new List { change }, false); e.Handled = true; }; @@ -925,6 +917,9 @@ namespace SourceGit.Views stage.Click += (_, e) => { var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + repo.SetWatcherEnabled(false); var tmpFile = Path.GetTempFileName(); @@ -957,6 +952,9 @@ namespace SourceGit.Views discard.Click += (_, e) => { var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + repo.SetWatcherEnabled(false); var tmpFile = Path.GetTempFileName(); @@ -994,6 +992,9 @@ namespace SourceGit.Views unstage.Click += (_, e) => { var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + repo.SetWatcherEnabled(false); var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); @@ -1025,6 +1026,9 @@ namespace SourceGit.Views discard.Click += (_, e) => { var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + repo.SetWatcherEnabled(false); var tmpFile = Path.GetTempFileName(); diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml index 88669bd9..1c4fa737 100644 --- a/src/Views/Welcome.axaml +++ b/src/Views/Welcome.axaml @@ -4,37 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:c="using:SourceGit.Converters" xmlns:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Welcome" x:DataType="vm:Welcome"> - - - - - - - - - - - - - - - - + + - - + + - (); + if (parent?.DataContext is ViewModels.Launcher launcher) + launcher.OpenRepositoryInTab(to, null); - var launcher = TopLevel.GetTopLevel(this).DataContext as ViewModels.Launcher; - launcher.OpenRepositoryInTab(to, launcher.ActivePage); e.Handled = true; } - private async void OpenLocalRepository(object sender, RoutedEventArgs e) + private void OpenOrInitRepository(string path, ViewModels.RepositoryNode parent = null) { - if (!ViewModels.PopupHost.CanCreatePopup()) - return; - - var topLevel = TopLevel.GetTopLevel(this); - var options = new FolderPickerOpenOptions() { AllowMultiple = false }; - var selected = await topLevel.StorageProvider.OpenFolderPickerAsync(options); - if (selected.Count == 1) - { - await OpenOrInitRepository(selected[0].Path.LocalPath); - } - } - - private Task OpenOrInitRepository(string path, ViewModels.RepositoryNode parent = null) - { - var launcher = TopLevel.GetTopLevel(this).DataContext as ViewModels.Launcher; - var page = launcher.ActivePage; - if (!Directory.Exists(path)) { if (File.Exists(path)) path = Path.GetDirectoryName(path); else - return null; + return; } - return Task.Run(() => + var root = new Commands.QueryRepositoryRootPath(path).Result(); + if (string.IsNullOrEmpty(root)) { - var root = new Commands.QueryRepositoryRootPath(path).Result(); - if (string.IsNullOrEmpty(root)) - { - Dispatcher.UIThread.Invoke(() => - { - (DataContext as ViewModels.Welcome).InitRepository(path, parent); - }); - return; - } + (DataContext as ViewModels.Welcome)?.InitRepository(path, parent); + return; + } - Dispatcher.UIThread.Invoke(() => - { - var normalizedPath = root.Replace("\\", "/"); - var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); - launcher.OpenRepositoryInTab(node, page); - }); - }); + var normalizedPath = root.Replace("\\", "/"); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); + var launcher = this.FindAncestorOfType()?.DataContext as ViewModels.Launcher; + launcher?.OpenRepositoryInTab(node, launcher.ActivePage); } private bool _pressedTreeNode = false; diff --git a/src/Views/WelcomeToolbar.axaml b/src/Views/WelcomeToolbar.axaml new file mode 100644 index 00000000..3ef07565 --- /dev/null +++ b/src/Views/WelcomeToolbar.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/src/Views/WelcomeToolbar.axaml.cs b/src/Views/WelcomeToolbar.axaml.cs new file mode 100644 index 00000000..7294ff4f --- /dev/null +++ b/src/Views/WelcomeToolbar.axaml.cs @@ -0,0 +1,58 @@ +using System.IO; + +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public partial class WelcomeToolbar : UserControl + { + public WelcomeToolbar() + { + InitializeComponent(); + } + + private async void OpenLocalRepository(object _1, RoutedEventArgs e) + { + if (!ViewModels.PopupHost.CanCreatePopup()) + return; + + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + var selected = await topLevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + OpenOrInitRepository(selected[0].Path.LocalPath); + + e.Handled = true; + } + + private void OpenOrInitRepository(string path, ViewModels.RepositoryNode parent = null) + { + if (!Directory.Exists(path)) + { + if (File.Exists(path)) + path = Path.GetDirectoryName(path); + else + return; + } + + var root = new Commands.QueryRepositoryRootPath(path).Result(); + if (string.IsNullOrEmpty(root)) + { + (DataContext as ViewModels.Welcome)?.InitRepository(path, parent); + return; + } + + var normalizedPath = root.Replace("\\", "/"); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(normalizedPath, parent, true); + var launcher = this.FindAncestorOfType()?.DataContext as ViewModels.Launcher; + launcher?.OpenRepositoryInTab(node, launcher.ActivePage); + } + } +} + diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 8e77d9d8..a651c15b 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -65,7 +65,7 @@