diff --git a/.gitignore b/.gitignore index 0c66b11e..e686a534 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ build/*.deb build/*.rpm build/*.AppImage SourceGit.app/ +build.command +src/Properties/launchSettings.json diff --git a/TRANSLATION.md b/TRANSLATION.md index 479f5188..1a9d00d1 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,19 +6,41 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-99.74%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-98.23%25-yellow)
Missing keys in de_DE.axaml - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages +- Text.Repository.ShowSubmodulesAsTree +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL
-### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) +### ![es__ES](https://img.shields.io/badge/es__ES-99.49%25-yellow) -### ![fr__FR](https://img.shields.io/badge/fr__FR-95.50%25-yellow) +
+Missing keys in es_ES.axaml + +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Launcher.Workspaces +- Text.Launcher.Pages + +
+ +### ![fr__FR](https://img.shields.io/badge/fr__FR-94.05%25-yellow)
Missing keys in fr_FR.axaml @@ -42,13 +64,25 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.ViewLogs - Text.Repository.Visit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -61,52 +95,19 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-95.24%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-99.49%25-yellow)
Missing keys in it_IT.axaml -- Text.Bisect -- Text.Bisect.Abort -- Text.Bisect.Bad -- Text.Bisect.Detecting -- Text.Bisect.Good -- Text.Bisect.Skip -- Text.Bisect.WaitingForRange -- Text.Checkout.RecurseSubmodules -- Text.CommitCM.CopyAuthor -- Text.CommitCM.CopyCommitter -- Text.CommitCM.CopySubject -- Text.CommitMessageTextBox.SubjectCount -- Text.Configure.Git.PreferredMergeMode -- Text.ConfirmEmptyCommit.Continue -- Text.ConfirmEmptyCommit.NoLocalChanges -- Text.ConfirmEmptyCommit.StageAllThenCommit -- Text.ConfirmEmptyCommit.WithLocalChanges -- Text.CopyFullPath -- Text.GitFlow.FinishWithPush -- Text.GitFlow.FinishWithSquash -- Text.Preferences.General.ShowTagsInGraph -- Text.Preferences.Git.IgnoreCRAtEOLInDiff -- Text.Repository.BranchSort -- Text.Repository.BranchSort.ByCommitterDate -- Text.Repository.BranchSort.ByName -- Text.Repository.Search.ByContent -- Text.Repository.ViewLogs -- Text.Repository.Visit -- Text.ViewLogs -- Text.ViewLogs.Clear -- Text.ViewLogs.CopyLog -- Text.ViewLogs.Delete -- Text.WorkingCopy.ConfirmCommitWithFilter -- Text.WorkingCopy.Conflicts.OpenExternalMergeTool -- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts -- Text.WorkingCopy.Conflicts.UseMine -- Text.WorkingCopy.Conflicts.UseTheirs +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Launcher.Workspaces +- Text.Launcher.Pages
-### ![ja__JP](https://img.shields.io/badge/ja__JP-95.24%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-93.80%25-yellow)
Missing keys in ja_JP.axaml @@ -130,15 +131,27 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.FilterCommits - Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.Tags.OrderByNameDes - Text.Repository.ViewLogs - Text.Repository.Visit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -151,7 +164,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-86.89%25-yellow) +### ![pt__BR](https://img.shields.io/badge/pt__BR-85.57%25-yellow)
Missing keys in pt_BR.axaml @@ -204,10 +217,15 @@ This document shows the translation status of each locale file in the repository - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash - Text.Hotkeys.Global.Clone +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool - Text.InProgress.CherryPick.Head - Text.InProgress.Merge.Operating - Text.InProgress.Rebase.StoppedAt - Text.InProgress.Revert.Head +- Text.Launcher.Workspaces +- Text.Launcher.Pages - Text.Merge.Source - Text.MergeMultiple - Text.MergeMultiple.CommitChanges @@ -231,6 +249,7 @@ This document shows the translation status of each locale file in the repository - Text.Repository.Notifications.Clear - Text.Repository.OnlyHighlightCurrentBranchInHistories - Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.Skip - Text.Repository.Tags.OrderByCreatorDate - Text.Repository.Tags.OrderByNameAsc @@ -247,6 +266,12 @@ This document shows the translation status of each locale file in the repository - Text.Stash.AutoRestore - Text.Stash.AutoRestore.Tip - Text.StashCM.SaveAsPatch +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog @@ -261,9 +286,17 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.75%25-yellow) -### ![ta__IN](https://img.shields.io/badge/ta__IN-95.50%25-yellow) +
+Missing keys in ru_RU.axaml + +- Text.Hotkeys.Global.SwitchTab +- Text.Launcher.Pages + +
+ +### ![ta__IN](https://img.shields.io/badge/ta__IN-94.05%25-yellow)
Missing keys in ta_IN.axaml @@ -287,13 +320,25 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmEmptyCommit.WithLocalChanges - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.ViewLogs - Text.Repository.Visit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.UpdateSubmodules.Target - Text.ViewLogs - Text.ViewLogs.Clear @@ -306,7 +351,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-96.66%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-95.19%25-yellow)
Missing keys in uk_UA.axaml @@ -326,13 +371,25 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureWorkspace.Name - Text.GitFlow.FinishWithPush - Text.GitFlow.FinishWithSquash +- Text.Hotkeys.Global.SwitchWorkspace +- Text.Hotkeys.Global.SwitchTab +- Text.Hotkeys.TextEditor.OpenExternalMergeTool +- Text.Launcher.Workspaces +- Text.Launcher.Pages - Text.Preferences.Git.IgnoreCRAtEOLInDiff - Text.Repository.BranchSort - Text.Repository.BranchSort.ByCommitterDate - Text.Repository.BranchSort.ByName - Text.Repository.Search.ByContent +- Text.Repository.ShowSubmodulesAsTree - Text.Repository.ViewLogs - Text.Repository.Visit +- Text.Submodule.Status +- Text.Submodule.Status.Modified +- Text.Submodule.Status.NotInited +- Text.Submodule.Status.RevisionChanged +- Text.Submodule.Status.Unmerged +- Text.Submodule.URL - Text.ViewLogs - Text.ViewLogs.Clear - Text.ViewLogs.CopyLog diff --git a/VERSION b/VERSION index 52dc0d52..60bea9b2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.17 \ No newline at end of file +2025.18 \ No newline at end of file diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 6e45164d..45ab0b8c 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -107,13 +107,24 @@ namespace SourceGit #region Utility Functions public static void ShowWindow(object data, bool showAsDialog) { + var impl = (Views.ChromelessWindow target, bool isDialog) => + { + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + { + if (isDialog) + target.ShowDialog(owner); + else + target.Show(owner); + } + else + { + target.Show(); + } + }; + if (data is Views.ChromelessWindow window) { - if (showAsDialog && Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - window.ShowDialog(owner); - else - window.Show(); - + impl(window, showAsDialog); return; } @@ -130,10 +141,7 @@ namespace SourceGit if (window != null) { window.DataContext = data; - if (showAsDialog && Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - window.ShowDialog(owner); - else - window.Show(); + impl(window, showAsDialog); } } diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index fcbe8591..e61fe1e6 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -35,7 +35,7 @@ namespace SourceGit.Commands App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}"); }); } - + new Restore(repo) { Log = log }.Exec(); if (includeIgnored) new Clean(repo) { Log = log }.Exec(); diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 19514954..39794090 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -17,8 +17,10 @@ namespace SourceGit.Commands Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; } - public List Result() + public List Result(out int localBranchesCount) { + localBranchesCount = 0; + var branches = new List(); var rs = ReadToEnd(); if (!rs.IsSuccess) @@ -34,6 +36,8 @@ namespace SourceGit.Commands branches.Add(b); if (!b.IsLocal) remoteBranches.Add(b.FullName); + else + localBranchesCount++; } } diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 4e626a79..404f5be6 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using Avalonia.Threading; namespace SourceGit.Commands { @@ -22,7 +23,10 @@ namespace SourceGit.Commands var outs = new List(); var rs = ReadToEnd(); if (!rs.IsSuccess) + { + Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr)); return outs; + } var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) diff --git a/src/Commands/QuerySubmodules.cs b/src/Commands/QuerySubmodules.cs index 6016b0be..86147f97 100644 --- a/src/Commands/QuerySubmodules.cs +++ b/src/Commands/QuerySubmodules.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -6,12 +7,12 @@ namespace SourceGit.Commands { public partial class QuerySubmodules : Command { - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")] - private static partial Regex REG_FORMAT1(); - [GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")] - private static partial Regex REG_FORMAT2(); - [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + [GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")] private static partial Regex REG_FORMAT_STATUS(); + [GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")] + private static partial Regex REG_FORMAT_DIRTY(); + [GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")] + private static partial Regex REG_FORMAT_MODULE_INFO(); public QuerySubmodules(string repo) { @@ -25,52 +26,117 @@ namespace SourceGit.Commands var submodules = new List(); var rs = ReadToEnd(); - var builder = new StringBuilder(); - var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var map = new Dictionary(); + var needCheckLocalChanges = false; foreach (var line in lines) { - var match = REG_FORMAT1().Match(line); + var match = REG_FORMAT_STATUS().Match(line); if (match.Success) { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); - continue; - } + var stat = match.Groups[1].Value; + var sha = match.Groups[2].Value; + var path = match.Groups[3].Value; - match = REG_FORMAT2().Match(line); - if (match.Success) - { - var path = match.Groups[1].Value; - builder.Append($"\"{path}\" "); - submodules.Add(new Models.Submodule() { Path = path }); + var module = new Models.Submodule() { Path = path, SHA = sha }; + switch (stat[0]) + { + case '-': + module.Status = Models.SubmoduleStatus.NotInited; + break; + case '+': + module.Status = Models.SubmoduleStatus.RevisionChanged; + break; + case 'U': + module.Status = Models.SubmoduleStatus.Unmerged; + break; + default: + module.Status = Models.SubmoduleStatus.Normal; + needCheckLocalChanges = true; + break; + } + + map.Add(path, module); + submodules.Add(module); } } if (submodules.Count > 0) { + Args = "config --file .gitmodules --list"; + rs = ReadToEnd(); + if (rs.IsSuccess) + { + var modules = new Dictionary(); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT_MODULE_INFO().Match(line); + if (match.Success) + { + var name = match.Groups[1].Value; + var key = match.Groups[2].Value; + var val = match.Groups[3].Value; + + if (!modules.TryGetValue(name, out var m)) + { + m = new ModuleInfo(); + modules.Add(name, m); + } + + if (key.Equals("path", StringComparison.Ordinal)) + m.Path = val; + else if (key.Equals("url", StringComparison.Ordinal)) + m.URL = val; + } + } + + foreach (var kv in modules) + { + if (map.TryGetValue(kv.Value.Path, out var m)) + m.URL = kv.Value.URL; + } + } + } + + if (needCheckLocalChanges) + { + var builder = new StringBuilder(); + foreach (var kv in map) + { + if (kv.Value.Status == Models.SubmoduleStatus.Normal) + { + builder.Append('"'); + builder.Append(kv.Key); + builder.Append("\" "); + } + } + Args = $"--no-optional-locks status -uno --porcelain -- {builder}"; rs = ReadToEnd(); if (!rs.IsSuccess) return submodules; - var dirty = new HashSet(); - lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries); + lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { - var match = REG_FORMAT_STATUS().Match(line); + var match = REG_FORMAT_DIRTY().Match(line); if (match.Success) { var path = match.Groups[1].Value; - dirty.Add(path); + if (map.TryGetValue(path, out var m)) + m.Status = Models.SubmoduleStatus.Modified; } } - - foreach (var submodule in submodules) - submodule.IsDirty = dirty.Contains(submodule.Path); } return submodules; } + + private class ModuleInfo + { + public string Path { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + } } } diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 73f63d8b..4b706439 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands Context = repo; WorkingDirectory = repo; - Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; + Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\""; } public List Result() @@ -25,16 +25,21 @@ namespace SourceGit.Commands foreach (var record in records) { var subs = record.Split('\0', StringSplitOptions.None); - if (subs.Length != 5) + if (subs.Length != 6) continue; - var message = subs[4].Trim(); + var name = subs[0].Substring(10); + var message = subs[5].Trim(); + if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal)) + message = null; + tags.Add(new Models.Tag() { - Name = subs[0].Substring(10), - SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2], - CreatorDate = ulong.Parse(subs[3]), - Message = string.IsNullOrEmpty(message) ? null : message, + Name = name, + IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal), + SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3], + CreatorDate = ulong.Parse(subs[4]), + Message = message, }); } diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs index 6fc8dc34..10a7ba87 100644 --- a/src/Commands/Tag.cs +++ b/src/Commands/Tag.cs @@ -9,7 +9,7 @@ namespace SourceGit.Commands var cmd = new Command(); cmd.WorkingDirectory = repo; cmd.Context = repo; - cmd.Args = $"tag {name} {basedOn}"; + cmd.Args = $"tag --no-sign {name} {basedOn}"; cmd.Log = log; return cmd.Exec(); } diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index 81cac8b7..e0c5967e 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -8,7 +8,7 @@ namespace SourceGit.Converters public static class ListConverters { public static readonly FuncValueConverter ToCount = - new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})"); + new FuncValueConverter(v => v == null ? "(0)" : $"({v.Count})"); public static readonly FuncValueConverter IsNullOrEmpty = new FuncValueConverter(v => v == null || v.Count == 0); diff --git a/src/Models/Submodule.cs b/src/Models/Submodule.cs index ce00ac02..ca73a8de 100644 --- a/src/Models/Submodule.cs +++ b/src/Models/Submodule.cs @@ -1,8 +1,20 @@ namespace SourceGit.Models { + public enum SubmoduleStatus + { + Normal = 0, + NotInited, + RevisionChanged, + Unmerged, + Modified, + } + public class Submodule { - public string Path { get; set; } = ""; - public bool IsDirty { get; set; } = false; + public string Path { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + public string URL { get; set; } = string.Empty; + public SubmoduleStatus Status { get; set; } = SubmoduleStatus.Normal; + public bool IsDirty => Status > SubmoduleStatus.NotInited; } } diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 51681d93..20678530 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -12,6 +12,7 @@ namespace SourceGit.Models public class Tag : ObservableObject { public string Name { get; set; } = string.Empty; + public bool IsAnnotated { get; set; } = false; public string SHA { get; set; } = string.Empty; public ulong CreatorDate { get; set; } = 0; public string Message { get; set; } = string.Empty; diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index bfb98500..2bdcf561 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -5,6 +5,8 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -16,6 +18,21 @@ namespace SourceGit.Native builder.With(new X11PlatformOptions() { EnableIme = true }); } + public void SetupWindow(Window window) + { + if (OS.UseSystemWindowFrame) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; + window.ExtendClientAreaToDecorationsHint = false; + } + else + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("custom_window_frame"); + } + } + public string FindGitExecutable() { return FindExecutable("git"); diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index 0966233f..b76d239a 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -5,6 +5,8 @@ using System.IO; using System.Runtime.Versioning; using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; namespace SourceGit.Native { @@ -36,6 +38,12 @@ namespace SourceGit.Native Environment.SetEnvironmentVariable("PATH", path); } + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome; + window.ExtendClientAreaToDecorationsHint = true; + } + public string FindGitExecutable() { var gitPathVariants = new List() { diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 320b5208..4c891283 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using Avalonia; +using Avalonia.Controls; namespace SourceGit.Native { @@ -14,6 +15,7 @@ namespace SourceGit.Native public interface IBackend { void SetupApp(AppBuilder builder); + void SetupWindow(Window window); string FindGitExecutable(); string FindTerminal(Models.ShellOrTerminal shell); @@ -68,6 +70,12 @@ namespace SourceGit.Native set; } = []; + public static bool UseSystemWindowFrame + { + get => OperatingSystem.IsLinux() && _enableSystemWindowFrame; + set => _enableSystemWindowFrame = value; + } + static OS() { if (OperatingSystem.IsWindows()) @@ -121,6 +129,11 @@ namespace SourceGit.Native ExternalTools = _backend.FindExternalTools(); } + public static void SetupForWindow(Window window) + { + _backend.SetupWindow(window); + } + public static string FindGitExecutable() { return _backend.FindGitExecutable(); @@ -225,5 +238,6 @@ namespace SourceGit.Native private static IBackend _backend = null; private static string _gitExecutable = string.Empty; + private static bool _enableSystemWindowFrame = false; } } diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index eb354f10..83b11c47 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -8,6 +8,7 @@ using System.Text; using Avalonia; using Avalonia.Controls; +using Avalonia.Platform; using Avalonia.Threading; namespace SourceGit.Native @@ -15,16 +16,12 @@ namespace SourceGit.Native [SupportedOSPlatform("windows")] internal class Windows : OS.IBackend { - [StructLayout(LayoutKind.Sequential)] - internal struct RTL_OSVERSIONINFOEX + internal struct RECT { - internal uint dwOSVersionInfoSize; - internal uint dwMajorVersion; - internal uint dwMinorVersion; - internal uint dwBuildNumber; - internal uint dwPlatformId; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - internal string szCSDVersion; + public int left; + public int top; + public int right; + public int bottom; } [StructLayout(LayoutKind.Sequential)] @@ -36,9 +33,6 @@ namespace SourceGit.Native public int cyBottomHeight; } - [DllImport("ntdll.dll")] - private static extern int RtlGetVersion(ref RTL_OSVERSIONINFOEX lpVersionInformation); - [DllImport("dwmapi.dll")] private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); @@ -54,18 +48,79 @@ namespace SourceGit.Native [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + public void SetupApp(AppBuilder builder) { // Fix drop shadow issue on Windows 10 - RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); - if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000)) + if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000, 0)) { Window.WindowStateProperty.Changed.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); Control.LoadedEvent.AddClassHandler((w, _) => FixWindowFrameOnWin10(w)); } } + public void SetupWindow(Window window) + { + window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + window.ExtendClientAreaToDecorationsHint = true; + window.Classes.Add("fix_maximized_padding"); + + Win32Properties.AddWndProcHookCallback(window, (IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled) => + { + // Custom WM_NCHITTEST + if (msg == 0x0084) + { + handled = true; + + if (window.WindowState == WindowState.FullScreen || window.WindowState == WindowState.Maximized) + return 1; // HTCLIENT + + var p = IntPtrToPixelPoint(lParam); + GetWindowRect(hWnd, out var rcWindow); + + var borderThinkness = (int)(4 * window.RenderScaling); + int y = 1; + int x = 1; + if (p.X >= rcWindow.left && p.X < rcWindow.left + borderThinkness) + x = 0; + else if (p.X < rcWindow.right && p.X >= rcWindow.right - borderThinkness) + x = 2; + + if (p.Y >= rcWindow.top && p.Y < rcWindow.top + borderThinkness) + y = 0; + else if (p.Y < rcWindow.bottom && p.Y >= rcWindow.bottom - borderThinkness) + y = 2; + + var zone = y * 3 + x; + switch (zone) + { + case 0: + return 13; // HTTOPLEFT + case 1: + return 12; // HTTOP + case 2: + return 14; // HTTOPRIGHT + case 3: + return 10; // HTLEFT + case 4: + return 1; // HTCLIENT + case 5: + return 11; // HTRIGHT + case 6: + return 16; // HTBOTTOMLEFT + case 7: + return 15; // HTBOTTOM + default: + return 17; // HTBOTTOMRIGHT + } + } + + return IntPtr.Zero; + }); + } + public string FindGitExecutable() { var reg = Microsoft.Win32.RegistryKey.OpenBaseKey( @@ -228,6 +283,12 @@ namespace SourceGit.Native }, DispatcherPriority.Render); } + private PixelPoint IntPtrToPixelPoint(IntPtr param) + { + var v = IntPtr.Size == 4 ? param.ToInt32() : (int)(param.ToInt64() & 0xFFFFFFFF); + return new PixelPoint((short)(v & 0xffff), (short)(v >> 16)); + } + #region EXTERNAL_EDITOR_FINDER private string FindVSCode() { diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index e2281a86..bb259272 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -386,6 +386,8 @@ Go to previous page Create new page Open Preferences dialog + Switch active workspace + Switch active page REPOSITORY Commit staged changes Commit and push staged changes @@ -406,6 +408,7 @@ Close search panel Find next match Find previous match + Open with external diff/merge tool Open search panel Discard Stage @@ -427,6 +430,8 @@ Open in Browser ERROR NOTICE + Workspaces + Pages Merge Branch Into: Merge Option: @@ -616,6 +621,7 @@ Message SHA Current Branch + Show Submodules as Tree Show Tags as Tree SKIP Statistics @@ -704,6 +710,12 @@ Relative Path: Relative folder to store this module. Delete Submodule + STATUS + modified + not initialized + revision changed + unmerged + URL OK Copy Tag Name Copy Tag Message diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index b11d0e98..e2b16329 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -410,6 +410,7 @@ Cerrar panel de búsqueda Buscar siguiente coincidencia Buscar coincidencia anterior + Abrir con herramienta diff/merge externa Abrir panel de búsqueda Descartar Stage @@ -620,6 +621,7 @@ Mensaje SHA Rama Actual + Mostrar Submódulos como Árbol Mostrar Etiquetas como Árbol OMITIR Estadísticas @@ -708,6 +710,12 @@ Ruta Relativa: Carpeta relativa para almacenar este módulo. Eliminar Submódulo + ESTADO + modificado + no inicializado + revisión cambiada + unmerged + URL OK Copiar Nombre de la Etiqueta Copiar Mensaje de la Etiqueta diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 545261e9..05fee42b 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -40,6 +40,13 @@ NESSUN FILE ASSUNTO COME INVARIATO RIMUOVI FILE BINARIO NON SUPPORTATO!!! + Biseca + Annulla + Cattiva + Bisecando. La HEAD corrente è buona o cattiva? + Buona + Salta + Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro. Attribuisci L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!! Checkout ${0}$... @@ -79,6 +86,7 @@ Modifiche Locali: Scarta Stasha e Ripristina + Aggiorna tutti i sottomoduli Branch: Cherry Pick Aggiungi sorgente al messaggio di commit @@ -103,8 +111,11 @@ Cherry-Pick... Confronta con HEAD Confronta con Worktree + Autore + Committer Informazioni SHA + Oggetto Azione Personalizzata Riallinea Interattivamente ${0}$ fino a Qui Unisci a ${0}$ @@ -136,6 +147,7 @@ SHA Apri nel Browser Descrizione + OGGETTO Inserisci l'oggetto del commit Configura Repository TEMPLATE DI COMMIT @@ -157,6 +169,7 @@ Recupera automaticamente i remoti Minuto/i Remoto Predefinito + Modalità di Merge Preferita TRACCIAMENTO ISSUE Aggiungi una regola di esempio per Azure DevOps Aggiungi una regola di esempio per un Issue Gitee @@ -181,6 +194,10 @@ Colore Nome Ripristina schede all'avvio + CONTINUA + Trovato un commit vuoto! Vuoi procedere (--allow-empty)? + STAGE DI TUTTO E COMMITTA + Trovato un commit vuoto! Vuoi procedere (--allow-empty) o fare lo stage di tutto e committare? Guida Commit Convenzionali Modifica Sostanziale: Issue Chiusa: @@ -190,6 +207,7 @@ Tipo di Modifica: Copia Copia Tutto il Testo + Copia Intero Percorso Copia Percorso Crea Branch... Basato Su: @@ -309,6 +327,8 @@ FLOW - Completa Hotfix FLOW - Completa Rilascio Target: + Invia al remote dopo aver finito + Esegui squash durante il merge Hotfix: Prefisso Hotfix: Inizializza Git-Flow @@ -390,6 +410,7 @@ Chiudi il pannello di ricerca Trova il prossimo risultato Trova il risultato precedente + Apri con uno strumento di diff/merge esterno Apri il pannello di ricerca Scarta Aggiungi in stage @@ -476,6 +497,7 @@ Numero massimo di commit nella cronologia Mostra nel grafico l'orario dell'autore anziché quello del commit Mostra i figli nei dettagli del commit + Mostra i tag nel grafico dei commit Lunghezza Guida Oggetto GIT Abilita Auto CRLF @@ -483,6 +505,7 @@ Email Utente Email utente Git globale Abilita --prune durante il fetch + Abilita --ignore-cr-at-eol nel diff Questa applicazione richiede Git (>= 2.23.0) Percorso Installazione Abilita la verifica HTTP SSL @@ -556,6 +579,9 @@ Branch: ANNULLA Recupero automatico delle modifiche dai remoti... + Ordina + Per data del committer + Per nome Pulizia (GC e Potatura) Esegui il comando `git gc` per questo repository. Cancella tutto @@ -589,11 +615,13 @@ AGGIUNGI REMOTO Cerca Commit Autore - Committente + Committer + Contenuto File Messaggio SHA Branch Corrente + Mostra i Sottomoduli Come Albero Mostra Tag come Albero SALTA Statistiche @@ -608,6 +636,8 @@ Ordina Apri nel Terminale Usa tempo relativo nello storico + Visualizza i Log + Visita '{0}' nel Browser WORKTREE AGGIUNGI WORKTREE POTATURA @@ -680,6 +710,12 @@ Percorso Relativo: Cartella relativa per memorizzare questo modulo. Elimina Sottomodulo + STATO + modificato + non inizializzato + revisione cambiata + non unito + URL OK Copia Nome Tag Copia Messaggio Tag @@ -693,6 +729,10 @@ Sottomodulo: Usa opzione --remote URL: + Log + CANCELLA TUTTO + Copia + Elimina Avviso Pagina di Benvenuto Crea Gruppo @@ -722,8 +762,13 @@ Attiva evento click Commit (Modifica) Stage di tutte le modifiche e fai il commit + Hai stageato {0} file ma solo {1} file mostrati ({2} file sono stati filtrati). Vuoi procedere? CONFLITTI RILEVATI + APRI STRUMENTO DI MERGE ESTERNO + APRI TUTTI I CONFLITTI NELLO STRUMENTO DI MERGE ESTERNO CONFLITTI NEI FILE RISOLTI + USO IL MIO + USO IL LORO INCLUDI FILE NON TRACCIATI NESSUN MESSAGGIO RECENTE INSERITO NESSUN TEMPLATE DI COMMIT diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 1c45f22f..77a7ba4f 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -390,6 +390,7 @@ Перейти на предыдущую вкладку Создать новую вкладку Открыть диалоговое окно настроек + Переключить активное рабочее место РЕПОЗИТОРИЙ Зафиксировать сформированные изменения Зафиксировать и выложить сформированные изменения @@ -410,6 +411,7 @@ Закрыть панель поиска Найти следующее совпадение Найти предыдущее совпадение + Открыть с внешним инструментом сравнения/слияние Открыть панель поиска Отклонить Сформировать @@ -431,6 +433,7 @@ Открыть в браузере ОШИБКА УВЕДОМЛЕНИЕ + Рабочие места Влить ветку В: Опции слияния: @@ -620,6 +623,7 @@ Сообщение SHA Текущая ветка + Показывать подмодули как дерево Показывать метки как катлог ПРОПУСТИТЬ Статистикa @@ -708,6 +712,12 @@ Каталог: Относительный путь для хранения подмодуля. Удалить подмодуль + СОСТОЯНИЕ + изменён + не создан + ревизия изменена + не слита + URL-адрес ОК Копировать имя метки Копировать сообщение с метки diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index d08c6f70..18569a14 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -390,6 +390,8 @@ 切换到上一个页面 新建页面 打开偏好设置面板 + 切换工作区 + 切换显示页面 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 @@ -410,6 +412,7 @@ 关闭搜索 定位到下一个匹配搜索的位置 定位到上一个匹配搜索的位置 + 使用外部比对工具查看 打开搜索 丢弃 暂存 @@ -431,6 +434,8 @@ 在浏览器中访问 出错了 系统提示 + 工作区列表 + 页面列表 合并分支 目标分支 : 合并方式 : @@ -620,6 +625,7 @@ 提交信息 提交指纹 仅在当前分支中查找 + 以树型结构展示 以树型结构展示 跳过此提交 提交统计 @@ -708,6 +714,12 @@ 相对仓库路径 : 本地存放的相对路径。 删除子模块 + 状态 + 未提交修改 + 未初始化 + SHA变更 + 未解决冲突 + 仓库 确 定 复制标签名 复制标签信息 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 4cd68cad..ded99a14 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -390,6 +390,8 @@ 切換到上一個頁面 新增頁面 開啟偏好設定面板 + 切換工作區 + 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 @@ -410,6 +412,7 @@ 關閉搜尋面板 前往下一個搜尋相符的位置 前往上一個搜尋相符的位置 + 使用外部比對工具檢視 開啟搜尋面板 捨棄 暫存 @@ -431,6 +434,8 @@ 在瀏覽器中開啟連結 發生錯誤 系統提示 + 工作區列表 + 頁面列表 合併分支 目標分支: 合併方式: @@ -620,6 +625,7 @@ 提交訊息 提交編號 僅搜尋目前分支 + 以樹型結構展示 以樹型結構展示 跳過此提交 提交統計 @@ -708,6 +714,12 @@ 相對存放庫路徑: 本機存放的相對路徑。 刪除子模組 + 狀態 + 未提交變更 + 未初始化 + SHA 變更 + 未解決的衝突 + 存放庫 確 定 複製標籤名稱 複製標籤訊息 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 093ae4e0..923ef22b 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -40,7 +40,7 @@ - @@ -72,30 +83,28 @@ - + - - diff --git a/src/Views/LauncherPageSwitcher.axaml.cs b/src/Views/LauncherPageSwitcher.axaml.cs new file mode 100644 index 00000000..1effb93c --- /dev/null +++ b/src/Views/LauncherPageSwitcher.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class LauncherPageSwitcher : UserControl + { + public LauncherPageSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && PagesListBox.ItemCount > 0) + { + PagesListBox.Focus(NavigationMethod.Directional); + + if (PagesListBox.SelectedIndex < 0) + PagesListBox.SelectedIndex = 0; + else if (PagesListBox.SelectedIndex < PagesListBox.ItemCount) + PagesListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +} + diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 0376e259..a56da2b0 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -40,7 +40,7 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index 12bca91f..270902bc 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -1,6 +1,7 @@ using System; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -19,6 +20,20 @@ namespace SourceGit.Views set => SetValue(IsScrollerVisibleProperty, value); } + public static readonly StyledProperty SearchFilterProperty = + AvaloniaProperty.Register(nameof(SearchFilter)); + + public string SearchFilter + { + get => GetValue(SearchFilterProperty); + set => SetValue(SearchFilterProperty, value); + } + + public AvaloniaList SelectablePages + { + get; + } = []; + public LauncherTabBar() { InitializeComponent(); @@ -126,6 +141,14 @@ namespace SourceGit.Views context.DrawGeometry(fill, stroke, geo); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SearchFilterProperty) + UpdateSelectablePages(); + } + private void ScrollTabs(object _, PointerWheelEventArgs e) { if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) @@ -248,13 +271,95 @@ namespace SourceGit.Views e.Handled = true; } - private void OnGotoSelectedPage(object sender, LauncherTabSelectedEventArgs e) + private void OnTabsDropdownOpened(object sender, EventArgs e) { - if (DataContext is ViewModels.Launcher vm) - vm.ActivePage = e.Page; + UpdateSelectablePages(); + } - PageSelector.Flyout?.Hide(); - e.Handled = true; + private void OnTabsDropdownClosed(object sender, EventArgs e) + { + SelectablePages.Clear(); + SearchFilter = string.Empty; + } + + private void OnTabsDropdownKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + if (TabsDropdownList.SelectedItem is ViewModels.LauncherPage page && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + } + + private void OnTabsDropdownSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && TabsDropdownList.ItemCount > 0) + { + TabsDropdownList.Focus(NavigationMethod.Directional); + + if (TabsDropdownList.SelectedIndex < 0) + TabsDropdownList.SelectedIndex = 0; + else if (TabsDropdownList.SelectedIndex < TabsDropdownList.ItemCount) + TabsDropdownList.SelectedIndex++; + + e.Handled = true; + } + } + + private void OnTabsDropdownLostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control { IsFocused: false, IsKeyboardFocusWithin: false }) + PageSelector.Flyout?.Hide(); + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + SearchFilter = string.Empty; + } + + private void OnTabsDropdownItemDoubleTapped(object sender, TappedEventArgs e) + { + if (sender is Control { DataContext: ViewModels.LauncherPage page } && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + + private void UpdateSelectablePages() + { + if (DataContext is not ViewModels.Launcher vm) + return; + + SelectablePages.Clear(); + + var pages = vm.Pages; + var filter = SearchFilter?.Trim() ?? ""; + if (string.IsNullOrEmpty(filter)) + { + SelectablePages.AddRange(pages); + return; + } + + foreach (var page in pages) + { + var node = page.Node; + if (node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + (node.IsRepository && node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase))) + SelectablePages.Add(page); + } } private bool _pressedTab = false; diff --git a/src/Views/LauncherTabsSelector.axaml.cs b/src/Views/LauncherTabsSelector.axaml.cs deleted file mode 100644 index 61d7a966..00000000 --- a/src/Views/LauncherTabsSelector.axaml.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Interactivity; - -namespace SourceGit.Views -{ - public class LauncherTabSelectedEventArgs : RoutedEventArgs - { - public ViewModels.LauncherPage Page { get; } - - public LauncherTabSelectedEventArgs(ViewModels.LauncherPage page) - { - RoutedEvent = LauncherTabsSelector.PageSelectedEvent; - Page = page; - } - } - - public partial class LauncherTabsSelector : UserControl - { - public static readonly StyledProperty> PagesProperty = - AvaloniaProperty.Register>(nameof(Pages)); - - public AvaloniaList Pages - { - get => GetValue(PagesProperty); - set => SetValue(PagesProperty, value); - } - - public static readonly StyledProperty SearchFilterProperty = - AvaloniaProperty.Register(nameof(SearchFilter)); - - public string SearchFilter - { - get => GetValue(SearchFilterProperty); - set => SetValue(SearchFilterProperty, value); - } - - public static readonly RoutedEvent PageSelectedEvent = - RoutedEvent.Register(nameof(PageSelected), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - public event EventHandler PageSelected - { - add { AddHandler(PageSelectedEvent, value); } - remove { RemoveHandler(PageSelectedEvent, value); } - } - - public AvaloniaList VisiblePages - { - get; - private set; - } - - public LauncherTabsSelector() - { - VisiblePages = new AvaloniaList(); - InitializeComponent(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == PagesProperty || change.Property == SearchFilterProperty) - UpdateVisiblePages(); - } - - private void OnClearSearchFilter(object sender, RoutedEventArgs e) - { - SearchFilter = string.Empty; - } - - private void OnPageSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is ListBox { SelectedItem: ViewModels.LauncherPage page }) - { - _isProcessingSelection = true; - RaiseEvent(new LauncherTabSelectedEventArgs(page)); - _isProcessingSelection = false; - } - - e.Handled = true; - } - - private void UpdateVisiblePages() - { - if (_isProcessingSelection) - return; - - VisiblePages.Clear(); - - if (Pages == null) - return; - - var filter = SearchFilter?.Trim() ?? ""; - if (string.IsNullOrEmpty(filter)) - { - foreach (var p in Pages) - VisiblePages.Add(p); - - return; - } - - foreach (var page in Pages) - { - if (!page.Node.IsRepository) - continue; - - if (page.Node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || - page.Node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) - VisiblePages.Add(page); - } - } - - private bool _isProcessingSelection = false; - } -} - diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 6742bcfc..b5826d11 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -129,23 +129,23 @@ + IsChecked="{Binding ShowAuthorTimeInGraph, Mode=TwoWay}"/> + IsChecked="{Binding ShowTagsInGraph, Mode=TwoWay}"/> + IsChecked="{Binding ShowChildren, Mode=TwoWay}"/> + IsChecked="{Binding Check4UpdatesOnStartup, Mode=TwoWay}"/> @@ -257,12 +257,12 @@ + IsChecked="{Binding UseFixedTabWidth, Mode=TwoWay}"/> diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2896a5ff..4eb303fe 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -204,7 +204,10 @@ - + + + + - - - - - - - @@ -142,12 +144,14 @@ - + - - - - - - diff --git a/src/Views/WorkspaceSwitcher.axaml b/src/Views/WorkspaceSwitcher.axaml new file mode 100644 index 00000000..4a324f3e --- /dev/null +++ b/src/Views/WorkspaceSwitcher.axaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/WorkspaceSwitcher.axaml.cs b/src/Views/WorkspaceSwitcher.axaml.cs new file mode 100644 index 00000000..04bdae1a --- /dev/null +++ b/src/Views/WorkspaceSwitcher.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class WorkspaceSwitcher : UserControl + { + public WorkspaceSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.WorkspaceSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.WorkspaceSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && WorkspaceListBox.ItemCount > 0) + { + WorkspaceListBox.Focus(NavigationMethod.Directional); + + if (WorkspaceListBox.SelectedIndex < 0) + WorkspaceListBox.SelectedIndex = 0; + else if (WorkspaceListBox.SelectedIndex < WorkspaceListBox.ItemCount) + WorkspaceListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +} +