Merge branch 'release/v2025.18'

This commit is contained in:
leo 2025-05-19 09:43:59 +08:00
commit 7dd1389c25
No known key found for this signature in database
59 changed files with 2347 additions and 749 deletions

2
.gitignore vendored
View file

@ -37,3 +37,5 @@ build/*.deb
build/*.rpm
build/*.AppImage
SourceGit.app/
build.command
src/Properties/launchSettings.json

View file

@ -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)
<details>
<summary>Missing keys in de_DE.axaml</summary>
- 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
</details>
### ![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)
<details>
<summary>Missing keys in es_ES.axaml</summary>
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Launcher.Workspaces
- Text.Launcher.Pages
</details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-94.05%25-yellow)
<details>
<summary>Missing keys in fr_FR.axaml</summary>
@ -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
</details>
### ![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)
<details>
<summary>Missing keys in it_IT.axaml</summary>
- 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
</details>
### ![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)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
@ -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
</details>
### ![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)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
@ -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
</details>
### ![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)
<details>
<summary>Missing keys in ru_RU.axaml</summary>
- Text.Hotkeys.Global.SwitchTab
- Text.Launcher.Pages
</details>
### ![ta__IN](https://img.shields.io/badge/ta__IN-94.05%25-yellow)
<details>
<summary>Missing keys in ta_IN.axaml</summary>
@ -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
</details>
### ![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)
<details>
<summary>Missing keys in uk_UA.axaml</summary>
@ -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

View file

@ -1 +1 @@
2025.17
2025.18

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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<Models.Branch> Result()
public List<Models.Branch> Result(out int localBranchesCount)
{
localBranchesCount = 0;
var branches = new List<Models.Branch>();
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++;
}
}

View file

@ -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<Models.Change>();
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)

View file

@ -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<Models.Submodule>();
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<string, Models.Submodule>();
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<string, ModuleInfo>();
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<string>();
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;
}
}
}

View file

@ -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<Models.Tag> 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,
});
}

View file

@ -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();
}

View file

@ -8,7 +8,7 @@ namespace SourceGit.Converters
public static class ListConverters
{
public static readonly FuncValueConverter<IList, string> ToCount =
new FuncValueConverter<IList, string>(v => v == null ? " (0)" : $" ({v.Count})");
new FuncValueConverter<IList, string>(v => v == null ? "(0)" : $"({v.Count})");
public static readonly FuncValueConverter<IList, bool> IsNullOrEmpty =
new FuncValueConverter<IList, bool>(v => v == null || v.Count == 0);

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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");

View file

@ -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<string>() {

View file

@ -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;
}
}

View file

@ -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<RTL_OSVERSIONINFOEX>();
if (RtlGetVersion(ref v) == 0 && (v.dwMajorVersion < 10 || v.dwBuildNumber < 22000))
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 22000, 0))
{
Window.WindowStateProperty.Changed.AddClassHandler<Window>((w, _) => FixWindowFrameOnWin10(w));
Control.LoadedEvent.AddClassHandler<Window>((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()
{

View file

@ -386,6 +386,8 @@
<x:String x:Key="Text.Hotkeys.Global.GotoPrevTab" xml:space="preserve">Go to previous page</x:String>
<x:String x:Key="Text.Hotkeys.Global.NewTab" xml:space="preserve">Create new page</x:String>
<x:String x:Key="Text.Hotkeys.Global.OpenPreferences" xml:space="preserve">Open Preferences dialog</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchWorkspace" xml:space="preserve">Switch active workspace</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchTab" xml:space="preserve">Switch active page</x:String>
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">REPOSITORY</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Commit" xml:space="preserve">Commit staged changes</x:String>
<x:String x:Key="Text.Hotkeys.Repo.CommitAndPush" xml:space="preserve">Commit and push staged changes</x:String>
@ -406,6 +408,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">Close search panel</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Find next match</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Find previous match</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">Open with external diff/merge tool</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Open search panel</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">Discard</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">Stage</x:String>
@ -427,6 +430,8 @@
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">Open in Browser</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
<x:String x:Key="Text.Launcher.Workspaces" xml:space="preserve">Workspaces</x:String>
<x:String x:Key="Text.Launcher.Pages" xml:space="preserve">Pages</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">Into:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Merge Option:</x:String>
@ -616,6 +621,7 @@
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">Message</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">Current Branch</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">Show Submodules as Tree</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">Show Tags as Tree</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">SKIP</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Statistics</x:String>
@ -704,6 +710,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Relative Path:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Relative folder to store this module.</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">Delete Submodule</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">STATUS</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">modified</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">not initialized</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">revision changed</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">unmerged</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">URL</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">OK</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">Copy Tag Name</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">Copy Tag Message</x:String>

View file

@ -410,6 +410,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">Cerrar panel de búsqueda</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Buscar siguiente coincidencia</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Buscar coincidencia anterior</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">Abrir con herramienta diff/merge externa</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Abrir panel de búsqueda</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">Descartar</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">Stage</x:String>
@ -620,6 +621,7 @@
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">Mensaje</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">Rama Actual</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">Mostrar Submódulos como Árbol</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">Mostrar Etiquetas como Árbol</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">OMITIR</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Estadísticas</x:String>
@ -708,6 +710,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Ruta Relativa:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Carpeta relativa para almacenar este módulo.</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">Eliminar Submódulo</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">ESTADO</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">modificado</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">no inicializado</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">revisión cambiada</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">unmerged</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">URL</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">OK</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">Copiar Nombre de la Etiqueta</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">Copiar Mensaje de la Etiqueta</x:String>

View file

@ -40,6 +40,13 @@
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">NESSUN FILE ASSUNTO COME INVARIATO</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">RIMUOVI</x:String>
<x:String x:Key="Text.BinaryNotSupported" xml:space="preserve">FILE BINARIO NON SUPPORTATO!!!</x:String>
<x:String x:Key="Text.Bisect" xml:space="preserve">Biseca</x:String>
<x:String x:Key="Text.Bisect.Abort" xml:space="preserve">Annulla</x:String>
<x:String x:Key="Text.Bisect.Bad" xml:space="preserve">Cattiva</x:String>
<x:String x:Key="Text.Bisect.Detecting" xml:space="preserve">Bisecando. La HEAD corrente è buona o cattiva?</x:String>
<x:String x:Key="Text.Bisect.Good" xml:space="preserve">Buona</x:String>
<x:String x:Key="Text.Bisect.Skip" xml:space="preserve">Salta</x:String>
<x:String x:Key="Text.Bisect.WaitingForRange" xml:space="preserve">Bisecando. Marca il commit corrente come buono o cattivo e fai checkout di un altro.</x:String>
<x:String x:Key="Text.Blame" xml:space="preserve">Attribuisci</x:String>
<x:String x:Key="Text.BlameTypeNotSupported" xml:space="preserve">L'ATTRIBUZIONE SU QUESTO FILE NON È SUPPORTATA!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>
@ -79,6 +86,7 @@
<x:String x:Key="Text.Checkout.LocalChanges" xml:space="preserve">Modifiche Locali:</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.Discard" xml:space="preserve">Scarta</x:String>
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stasha e Ripristina</x:String>
<x:String x:Key="Text.Checkout.RecurseSubmodules" xml:space="preserve">Aggiorna tutti i sottomoduli</x:String>
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>
<x:String x:Key="Text.CherryPick.AppendSourceToMessage" xml:space="preserve">Aggiungi sorgente al messaggio di commit</x:String>
@ -103,8 +111,11 @@
<x:String x:Key="Text.CommitCM.CherryPickMultiple" xml:space="preserve">Cherry-Pick...</x:String>
<x:String x:Key="Text.CommitCM.CompareWithHead" xml:space="preserve">Confronta con HEAD</x:String>
<x:String x:Key="Text.CommitCM.CompareWithWorktree" xml:space="preserve">Confronta con Worktree</x:String>
<x:String x:Key="Text.CommitCM.CopyAuthor" xml:space="preserve">Autore</x:String>
<x:String x:Key="Text.CommitCM.CopyCommitter" xml:space="preserve">Committer</x:String>
<x:String x:Key="Text.CommitCM.CopyInfo" xml:space="preserve">Informazioni</x:String>
<x:String x:Key="Text.CommitCM.CopySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitCM.CopySubject" xml:space="preserve">Oggetto</x:String>
<x:String x:Key="Text.CommitCM.CustomAction" xml:space="preserve">Azione Personalizzata</x:String>
<x:String x:Key="Text.CommitCM.InteractiveRebase" xml:space="preserve">Riallinea Interattivamente ${0}$ fino a Qui</x:String>
<x:String x:Key="Text.CommitCM.Merge" xml:space="preserve">Unisci a ${0}$</x:String>
@ -136,6 +147,7 @@
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">Apri nel Browser</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">Descrizione</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectCount" xml:space="preserve">OGGETTO</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">Inserisci l'oggetto del commit</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">Configura Repository</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate" xml:space="preserve">TEMPLATE DI COMMIT</x:String>
@ -157,6 +169,7 @@
<x:String x:Key="Text.Configure.Git.AutoFetch" xml:space="preserve">Recupera automaticamente i remoti</x:String>
<x:String x:Key="Text.Configure.Git.AutoFetchIntervalSuffix" xml:space="preserve">Minuto/i</x:String>
<x:String x:Key="Text.Configure.Git.DefaultRemote" xml:space="preserve">Remoto Predefinito</x:String>
<x:String x:Key="Text.Configure.Git.PreferredMergeMode" xml:space="preserve">Modalità di Merge Preferita</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">TRACCIAMENTO ISSUE</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleAzure" xml:space="preserve">Aggiungi una regola di esempio per Azure DevOps</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGiteeIssue" xml:space="preserve">Aggiungi una regola di esempio per un Issue Gitee</x:String>
@ -181,6 +194,10 @@
<x:String x:Key="Text.ConfigureWorkspace.Color" xml:space="preserve">Colore</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Name" xml:space="preserve">Nome</x:String>
<x:String x:Key="Text.ConfigureWorkspace.Restore" xml:space="preserve">Ripristina schede all'avvio</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.Continue" xml:space="preserve">CONTINUA</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.NoLocalChanges" xml:space="preserve">Trovato un commit vuoto! Vuoi procedere (--allow-empty)?</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.StageAllThenCommit" xml:space="preserve">STAGE DI TUTTO E COMMITTA</x:String>
<x:String x:Key="Text.ConfirmEmptyCommit.WithLocalChanges" xml:space="preserve">Trovato un commit vuoto! Vuoi procedere (--allow-empty) o fare lo stage di tutto e committare?</x:String>
<x:String x:Key="Text.ConventionalCommit" xml:space="preserve">Guida Commit Convenzionali</x:String>
<x:String x:Key="Text.ConventionalCommit.BreakingChanges" xml:space="preserve">Modifica Sostanziale:</x:String>
<x:String x:Key="Text.ConventionalCommit.ClosedIssue" xml:space="preserve">Issue Chiusa:</x:String>
@ -190,6 +207,7 @@
<x:String x:Key="Text.ConventionalCommit.Type" xml:space="preserve">Tipo di Modifica:</x:String>
<x:String x:Key="Text.Copy" xml:space="preserve">Copia</x:String>
<x:String x:Key="Text.CopyAllText" xml:space="preserve">Copia Tutto il Testo</x:String>
<x:String x:Key="Text.CopyFullPath" xml:space="preserve">Copia Intero Percorso</x:String>
<x:String x:Key="Text.CopyPath" xml:space="preserve">Copia Percorso</x:String>
<x:String x:Key="Text.CreateBranch" xml:space="preserve">Crea Branch...</x:String>
<x:String x:Key="Text.CreateBranch.BasedOn" xml:space="preserve">Basato Su:</x:String>
@ -309,6 +327,8 @@
<x:String x:Key="Text.GitFlow.FinishHotfix" xml:space="preserve">FLOW - Completa Hotfix</x:String>
<x:String x:Key="Text.GitFlow.FinishRelease" xml:space="preserve">FLOW - Completa Rilascio</x:String>
<x:String x:Key="Text.GitFlow.FinishTarget" xml:space="preserve">Target:</x:String>
<x:String x:Key="Text.GitFlow.FinishWithPush" xml:space="preserve">Invia al remote dopo aver finito</x:String>
<x:String x:Key="Text.GitFlow.FinishWithSquash" xml:space="preserve">Esegui squash durante il merge</x:String>
<x:String x:Key="Text.GitFlow.Hotfix" xml:space="preserve">Hotfix:</x:String>
<x:String x:Key="Text.GitFlow.HotfixPrefix" xml:space="preserve">Prefisso Hotfix:</x:String>
<x:String x:Key="Text.GitFlow.Init" xml:space="preserve">Inizializza Git-Flow</x:String>
@ -390,6 +410,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">Chiudi il pannello di ricerca</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Trova il prossimo risultato</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Trova il risultato precedente</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">Apri con uno strumento di diff/merge esterno</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Apri il pannello di ricerca</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">Scarta</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">Aggiungi in stage</x:String>
@ -476,6 +497,7 @@
<x:String x:Key="Text.Preferences.General.MaxHistoryCommits" xml:space="preserve">Numero massimo di commit nella cronologia</x:String>
<x:String x:Key="Text.Preferences.General.ShowAuthorTime" xml:space="preserve">Mostra nel grafico l'orario dell'autore anziché quello del commit</x:String>
<x:String x:Key="Text.Preferences.General.ShowChildren" xml:space="preserve">Mostra i figli nei dettagli del commit</x:String>
<x:String x:Key="Text.Preferences.General.ShowTagsInGraph" xml:space="preserve">Mostra i tag nel grafico dei commit</x:String>
<x:String x:Key="Text.Preferences.General.SubjectGuideLength" xml:space="preserve">Lunghezza Guida Oggetto</x:String>
<x:String x:Key="Text.Preferences.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preferences.Git.CRLF" xml:space="preserve">Abilita Auto CRLF</x:String>
@ -483,6 +505,7 @@
<x:String x:Key="Text.Preferences.Git.Email" xml:space="preserve">Email Utente</x:String>
<x:String x:Key="Text.Preferences.Git.Email.Placeholder" xml:space="preserve">Email utente Git globale</x:String>
<x:String x:Key="Text.Preferences.Git.EnablePruneOnFetch" xml:space="preserve">Abilita --prune durante il fetch</x:String>
<x:String x:Key="Text.Preferences.Git.IgnoreCRAtEOLInDiff" xml:space="preserve">Abilita --ignore-cr-at-eol nel diff</x:String>
<x:String x:Key="Text.Preferences.Git.Invalid" xml:space="preserve">Questa applicazione richiede Git (&gt;= 2.23.0)</x:String>
<x:String x:Key="Text.Preferences.Git.Path" xml:space="preserve">Percorso Installazione</x:String>
<x:String x:Key="Text.Preferences.Git.SSLVerify" xml:space="preserve">Abilita la verifica HTTP SSL</x:String>
@ -556,6 +579,9 @@
<x:String x:Key="Text.RenameBranch.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Repository.Abort" xml:space="preserve">ANNULLA</x:String>
<x:String x:Key="Text.Repository.AutoFetching" xml:space="preserve">Recupero automatico delle modifiche dai remoti...</x:String>
<x:String x:Key="Text.Repository.BranchSort" xml:space="preserve">Ordina</x:String>
<x:String x:Key="Text.Repository.BranchSort.ByCommitterDate" xml:space="preserve">Per data del committer</x:String>
<x:String x:Key="Text.Repository.BranchSort.ByName" xml:space="preserve">Per nome</x:String>
<x:String x:Key="Text.Repository.Clean" xml:space="preserve">Pulizia (GC e Potatura)</x:String>
<x:String x:Key="Text.Repository.CleanTips" xml:space="preserve">Esegui il comando `git gc` per questo repository.</x:String>
<x:String x:Key="Text.Repository.ClearAllCommitsFilter" xml:space="preserve">Cancella tutto</x:String>
@ -589,11 +615,13 @@
<x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">AGGIUNGI REMOTO</x:String>
<x:String x:Key="Text.Repository.Search" xml:space="preserve">Cerca Commit</x:String>
<x:String x:Key="Text.Repository.Search.ByAuthor" xml:space="preserve">Autore</x:String>
<x:String x:Key="Text.Repository.Search.ByCommitter" xml:space="preserve">Committente</x:String>
<x:String x:Key="Text.Repository.Search.ByCommitter" xml:space="preserve">Committer</x:String>
<x:String x:Key="Text.Repository.Search.ByContent" xml:space="preserve">Contenuto</x:String>
<x:String x:Key="Text.Repository.Search.ByFile" xml:space="preserve">File</x:String>
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">Messaggio</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">Branch Corrente</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">Mostra i Sottomoduli Come Albero</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">Mostra Tag come Albero</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">SALTA</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Statistiche</x:String>
@ -608,6 +636,8 @@
<x:String x:Key="Text.Repository.Tags.Sort" xml:space="preserve">Ordina</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Apri nel Terminale</x:String>
<x:String x:Key="Text.Repository.UseRelativeTimeInHistories" xml:space="preserve">Usa tempo relativo nello storico</x:String>
<x:String x:Key="Text.Repository.ViewLogs" xml:space="preserve">Visualizza i Log</x:String>
<x:String x:Key="Text.Repository.Visit" xml:space="preserve">Visita '{0}' nel Browser</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREE</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">AGGIUNGI WORKTREE</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">POTATURA</x:String>
@ -680,6 +710,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Percorso Relativo:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Cartella relativa per memorizzare questo modulo.</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">Elimina Sottomodulo</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">STATO</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">modificato</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">non inizializzato</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">revisione cambiata</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">non unito</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">URL</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">OK</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">Copia Nome Tag</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">Copia Messaggio Tag</x:String>
@ -693,6 +729,10 @@
<x:String x:Key="Text.UpdateSubmodules.Target" xml:space="preserve">Sottomodulo:</x:String>
<x:String x:Key="Text.UpdateSubmodules.UseRemote" xml:space="preserve">Usa opzione --remote</x:String>
<x:String x:Key="Text.URL" xml:space="preserve">URL:</x:String>
<x:String x:Key="Text.ViewLogs" xml:space="preserve">Log</x:String>
<x:String x:Key="Text.ViewLogs.Clear" xml:space="preserve">CANCELLA TUTTO</x:String>
<x:String x:Key="Text.ViewLogs.CopyLog" xml:space="preserve">Copia</x:String>
<x:String x:Key="Text.ViewLogs.Delete" xml:space="preserve">Elimina</x:String>
<x:String x:Key="Text.Warn" xml:space="preserve">Avviso</x:String>
<x:String x:Key="Text.Welcome" xml:space="preserve">Pagina di Benvenuto</x:String>
<x:String x:Key="Text.Welcome.AddRootFolder" xml:space="preserve">Crea Gruppo</x:String>
@ -722,8 +762,13 @@
<x:String x:Key="Text.WorkingCopy.CommitTip" xml:space="preserve">Attiva evento click</x:String>
<x:String x:Key="Text.WorkingCopy.CommitToEdit" xml:space="preserve">Commit (Modifica)</x:String>
<x:String x:Key="Text.WorkingCopy.CommitWithAutoStage" xml:space="preserve">Stage di tutte le modifiche e fai il commit</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithFilter" xml:space="preserve">Hai stageato {0} file ma solo {1} file mostrati ({2} file sono stati filtrati). Vuoi procedere?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">CONFLITTI RILEVATI</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeTool" xml:space="preserve">APRI STRUMENTO DI MERGE ESTERNO</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts" xml:space="preserve">APRI TUTTI I CONFLITTI NELLO STRUMENTO DI MERGE ESTERNO</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">CONFLITTI NEI FILE RISOLTI</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">USO IL MIO</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">USO IL LORO</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">INCLUDI FILE NON TRACCIATI</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">NESSUN MESSAGGIO RECENTE INSERITO</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">NESSUN TEMPLATE DI COMMIT</x:String>

View file

@ -390,6 +390,7 @@
<x:String x:Key="Text.Hotkeys.Global.GotoPrevTab" xml:space="preserve">Перейти на предыдущую вкладку</x:String>
<x:String x:Key="Text.Hotkeys.Global.NewTab" xml:space="preserve">Создать новую вкладку</x:String>
<x:String x:Key="Text.Hotkeys.Global.OpenPreferences" xml:space="preserve">Открыть диалоговое окно настроек</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchWorkspace" xml:space="preserve">Переключить активное рабочее место</x:String>
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">РЕПОЗИТОРИЙ</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Commit" xml:space="preserve">Зафиксировать сформированные изменения</x:String>
<x:String x:Key="Text.Hotkeys.Repo.CommitAndPush" xml:space="preserve">Зафиксировать и выложить сформированные изменения</x:String>
@ -410,6 +411,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">Закрыть панель поиска</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Найти следующее совпадение</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Найти предыдущее совпадение</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">Открыть с внешним инструментом сравнения/слияние</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Открыть панель поиска</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">Отклонить</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">Сформировать</x:String>
@ -431,6 +433,7 @@
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">Открыть в браузере</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ОШИБКА</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">УВЕДОМЛЕНИЕ</x:String>
<x:String x:Key="Text.Launcher.Workspaces" xml:space="preserve">Рабочие места</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">Влить ветку</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">В:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">Опции слияния:</x:String>
@ -620,6 +623,7 @@
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">Сообщение</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">Текущая ветка</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">Показывать подмодули как дерево</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">Показывать метки как катлог</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">ПРОПУСТИТЬ</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Статистикa </x:String>
@ -708,6 +712,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">Каталог:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">Относительный путь для хранения подмодуля.</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">Удалить подмодуль</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">СОСТОЯНИЕ</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">изменён</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">не создан</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">ревизия изменена</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">не слита</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">URL-адрес</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">ОК</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">Копировать имя метки</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">Копировать сообщение с метки</x:String>

View file

@ -390,6 +390,8 @@
<x:String x:Key="Text.Hotkeys.Global.GotoPrevTab" xml:space="preserve">切换到上一个页面</x:String>
<x:String x:Key="Text.Hotkeys.Global.NewTab" xml:space="preserve">新建页面</x:String>
<x:String x:Key="Text.Hotkeys.Global.OpenPreferences" xml:space="preserve">打开偏好设置面板</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchWorkspace" xml:space="preserve">切换工作区</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchTab" xml:space="preserve">切换显示页面</x:String>
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">仓库页面快捷键</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Commit" xml:space="preserve">提交暂存区更改</x:String>
<x:String x:Key="Text.Hotkeys.Repo.CommitAndPush" xml:space="preserve">提交暂存区更改并推送</x:String>
@ -410,6 +412,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">关闭搜索</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一个匹配搜索的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一个匹配搜索的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">使用外部比对工具查看</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">打开搜索</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">丢弃</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">暂存</x:String>
@ -431,6 +434,8 @@
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">在浏览器中访问</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>
<x:String x:Key="Text.Launcher.Workspaces" xml:space="preserve">工作区列表</x:String>
<x:String x:Key="Text.Launcher.Pages" xml:space="preserve">页面列表</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">合并分支</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合并方式 </x:String>
@ -620,6 +625,7 @@
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">提交信息</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">提交指纹</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">仅在当前分支中查找</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">以树型结构展示</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">以树型结构展示</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">跳过此提交</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交统计</x:String>
@ -708,6 +714,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">相对仓库路径 </x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">本地存放的相对路径。</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">删除子模块</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">状态</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">未提交修改</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">未初始化</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">SHA变更</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">未解决冲突</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">仓库</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">确 定</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">复制标签名</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">复制标签信息</x:String>

View file

@ -390,6 +390,8 @@
<x:String x:Key="Text.Hotkeys.Global.GotoPrevTab" xml:space="preserve">切換到上一個頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.NewTab" xml:space="preserve">新增頁面</x:String>
<x:String x:Key="Text.Hotkeys.Global.OpenPreferences" xml:space="preserve">開啟偏好設定面板</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchWorkspace" xml:space="preserve">切換工作區</x:String>
<x:String x:Key="Text.Hotkeys.Global.SwitchTab" xml:space="preserve">切換目前頁面</x:String>
<x:String x:Key="Text.Hotkeys.Repo" xml:space="preserve">存放庫頁面快速鍵</x:String>
<x:String x:Key="Text.Hotkeys.Repo.Commit" xml:space="preserve">提交暫存區變更</x:String>
<x:String x:Key="Text.Hotkeys.Repo.CommitAndPush" xml:space="preserve">提交暫存區變更並推送</x:String>
@ -410,6 +412,7 @@
<x:String x:Key="Text.Hotkeys.TextEditor.CloseSearch" xml:space="preserve">關閉搜尋面板</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">前往下一個搜尋相符的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">前往上一個搜尋相符的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.OpenExternalMergeTool" xml:space="preserve">使用外部比對工具檢視</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">開啟搜尋面板</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">捨棄</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">暫存</x:String>
@ -431,6 +434,8 @@
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">在瀏覽器中開啟連結</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">發生錯誤</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>
<x:String x:Key="Text.Launcher.Workspaces" xml:space="preserve">工作區列表</x:String>
<x:String x:Key="Text.Launcher.Pages" xml:space="preserve">頁面列表</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">合併分支</x:String>
<x:String x:Key="Text.Merge.Into" xml:space="preserve">目標分支:</x:String>
<x:String x:Key="Text.Merge.Mode" xml:space="preserve">合併方式:</x:String>
@ -620,6 +625,7 @@
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">提交訊息</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">提交編號</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">僅搜尋目前分支</x:String>
<x:String x:Key="Text.Repository.ShowSubmodulesAsTree" xml:space="preserve">以樹型結構展示</x:String>
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">以樹型結構展示</x:String>
<x:String x:Key="Text.Repository.Skip" xml:space="preserve">跳過此提交</x:String>
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交統計</x:String>
@ -708,6 +714,12 @@
<x:String x:Key="Text.Submodule.RelativePath" xml:space="preserve">相對存放庫路徑:</x:String>
<x:String x:Key="Text.Submodule.RelativePath.Placeholder" xml:space="preserve">本機存放的相對路徑。</x:String>
<x:String x:Key="Text.Submodule.Remove" xml:space="preserve">刪除子模組</x:String>
<x:String x:Key="Text.Submodule.Status" xml:space="preserve">狀態</x:String>
<x:String x:Key="Text.Submodule.Status.Modified" xml:space="preserve">未提交變更</x:String>
<x:String x:Key="Text.Submodule.Status.NotInited" xml:space="preserve">未初始化</x:String>
<x:String x:Key="Text.Submodule.Status.RevisionChanged" xml:space="preserve">SHA 變更</x:String>
<x:String x:Key="Text.Submodule.Status.Unmerged" xml:space="preserve">未解決的衝突</x:String>
<x:String x:Key="Text.Submodule.URL" xml:space="preserve">存放庫</x:String>
<x:String x:Key="Text.Sure" xml:space="preserve">確 定</x:String>
<x:String x:Key="Text.TagCM.Copy" xml:space="preserve">複製標籤名稱</x:String>
<x:String x:Key="Text.TagCM.CopyMessage" xml:space="preserve">複製標籤訊息</x:String>

View file

@ -40,7 +40,7 @@
</Style>
<Style Selector="Window[WindowState=Maximized].fix_maximized_padding">
<Setter Property="Padding" Value="6"/>
<Setter Property="Padding" Value="8,6,8,8"/>
</Style>
<Style Selector="Window.custom_window_frame">
@ -1235,7 +1235,7 @@
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleButton.tag_display_mode">
<Style Selector="ToggleButton.show_as_tree">
<Setter Property="Margin" Value="0" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">

View file

@ -14,6 +14,7 @@ namespace SourceGit.ViewModels
public int Depth { get; set; } = 0;
public bool IsSelected { get; set; } = false;
public List<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
public int Counter { get; set; } = 0;
public Models.FilterMode FilterMode
{
@ -48,9 +49,23 @@ namespace SourceGit.ViewModels
get => Backend is Models.Branch { IsUpstreamGone: true };
}
public string BranchesCount
{
get => Counter > 0 ? $"({Counter})" : string.Empty;
}
public string Tooltip
{
get => Backend is Models.Branch b ? b.FriendlyName : null;
get
{
if (Backend is Models.Branch b)
return b.FriendlyName;
if (Backend is Models.Remote r)
return r.URL;
return null;
}
}
private Models.FilterMode _filterMode = Models.FilterMode.None;
@ -102,12 +117,14 @@ namespace SourceGit.ViewModels
if (branch.IsLocal)
{
MakeBranchNode(branch, _locals, folders, "refs/heads", bForceExpanded);
continue;
}
else
var rk = $"refs/remotes/{branch.Remote}";
if (folders.TryGetValue(rk, out var remote))
{
var remote = _remotes.Find(x => x.Name == branch.Remote);
if (remote != null)
MakeBranchNode(branch, remote.Children, folders, $"refs/remotes/{remote.Name}", bForceExpanded);
remote.Counter++;
MakeBranchNode(branch, remote.Children, folders, rk, bForceExpanded);
}
}
@ -158,6 +175,7 @@ namespace SourceGit.ViewModels
if (folders.TryGetValue(folder, out var val))
{
lastFolder = val;
lastFolder.Counter++;
lastFolder.TimeToSort = Math.Max(lastFolder.TimeToSort, time);
if (!lastFolder.IsExpanded)
lastFolder.IsExpanded |= (branch.IsCurrent || _expanded.Contains(folder));
@ -170,6 +188,7 @@ namespace SourceGit.ViewModels
Path = folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
TimeToSort = time,
Counter = 1,
};
roots.Add(lastFolder);
folders.Add(folder, lastFolder);
@ -182,6 +201,7 @@ namespace SourceGit.ViewModels
Path = folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
TimeToSort = time,
Counter = 1,
};
lastFolder.Children.Add(cur);
folders.Add(folder, cur);

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
@ -387,7 +387,7 @@ namespace SourceGit.ViewModels
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine($"{c.SHA.Substring(0, 10)} - {c.Subject}");
builder.AppendLine($"{c.SHA.AsSpan(0, 10)} - {c.Subject}");
App.CopyText(builder.ToString());
e.Handled = true;
@ -780,7 +780,7 @@ namespace SourceGit.ViewModels
copyInfo.Icon = App.CreateMenuIcon("Icons.Info");
copyInfo.Click += (_, e) =>
{
App.CopyText($"{commit.SHA.Substring(0, 10)} - {commit.Subject}");
App.CopyText($"{commit.SHA.AsSpan(0, 10)} - {commit.Subject}");
e.Handled = true;
};

View file

@ -44,6 +44,12 @@ namespace SourceGit.ViewModels
}
}
public IDisposable Switcher
{
get => _switcher;
private set => SetProperty(ref _switcher, value);
}
public Launcher(string startupRepo)
{
_ignoreIndexChange = true;
@ -130,6 +136,85 @@ namespace SourceGit.ViewModels
_ignoreIndexChange = false;
}
public void OpenWorkspaceSwitcher()
{
Switcher = new WorkspaceSwitcher(this);
}
public void OpenTabSwitcher()
{
Switcher = new LauncherPageSwitcher(this);
}
public void CancelSwitcher()
{
Switcher?.Dispose();
Switcher = null;
}
public void SwitchWorkspace(Workspace to)
{
if (to == null || to.IsActive)
return;
foreach (var one in Pages)
{
if (!one.CanCreatePopup() || one.Data is Repository { IsAutoFetching: true })
{
App.RaiseException(null, "You have unfinished task(s) in opened pages. Please wait!!!");
return;
}
}
_ignoreIndexChange = true;
var pref = Preferences.Instance;
foreach (var w in pref.Workspaces)
w.IsActive = false;
ActiveWorkspace = to;
to.IsActive = true;
foreach (var one in Pages)
CloseRepositoryInTab(one, false);
Pages.Clear();
AddNewTab();
var repos = to.Repositories.ToArray();
foreach (var repo in repos)
{
var node = pref.FindNode(repo);
if (node == null)
{
node = new RepositoryNode()
{
Id = repo,
Name = Path.GetFileName(repo),
Bookmark = 0,
IsRepository = true,
};
}
OpenRepositoryInTab(node, null);
}
var activeIdx = to.ActiveIdx;
if (activeIdx >= 0 && activeIdx < Pages.Count)
{
ActivePage = Pages[activeIdx];
}
else
{
ActivePage = Pages[0];
to.ActiveIdx = 0;
}
_ignoreIndexChange = false;
Preferences.Instance.Save();
GC.Collect();
}
public void AddNewTab()
{
var page = new LauncherPage();
@ -494,66 +579,6 @@ namespace SourceGit.ViewModels
return new Commands.QueryGitDir(repo).Result();
}
private void SwitchWorkspace(Workspace to)
{
foreach (var one in Pages)
{
if (!one.CanCreatePopup() || one.Data is Repository { IsAutoFetching: true })
{
App.RaiseException(null, "You have unfinished task(s) in opened pages. Please wait!!!");
return;
}
}
_ignoreIndexChange = true;
var pref = Preferences.Instance;
foreach (var w in pref.Workspaces)
w.IsActive = false;
ActiveWorkspace = to;
to.IsActive = true;
foreach (var one in Pages)
CloseRepositoryInTab(one, false);
Pages.Clear();
AddNewTab();
var repos = to.Repositories.ToArray();
foreach (var repo in repos)
{
var node = pref.FindNode(repo);
if (node == null)
{
node = new RepositoryNode()
{
Id = repo,
Name = Path.GetFileName(repo),
Bookmark = 0,
IsRepository = true,
};
}
OpenRepositoryInTab(node, null);
}
var activeIdx = to.ActiveIdx;
if (activeIdx >= 0 && activeIdx < Pages.Count)
{
ActivePage = Pages[activeIdx];
}
else
{
ActivePage = Pages[0];
to.ActiveIdx = 0;
}
_ignoreIndexChange = false;
Preferences.Instance.Save();
GC.Collect();
}
private void CloseRepositoryInTab(LauncherPage page, bool removeFromWorkspace = true)
{
if (page.Data is Repository repo)
@ -573,7 +598,7 @@ namespace SourceGit.ViewModels
return;
var workspace = _activeWorkspace.Name;
if (_activePage is { Data: Repository repo })
if (_activePage is { Data: Repository })
{
var node = _activePage.Node;
var name = node.Name;
@ -599,5 +624,6 @@ namespace SourceGit.ViewModels
private LauncherPage _activePage = null;
private bool _ignoreIndexChange = false;
private string _title = string.Empty;
private IDisposable _switcher = null;
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class LauncherPageSwitcher : ObservableObject, IDisposable
{
public List<LauncherPage> VisiblePages
{
get => _visiblePages;
private set => SetProperty(ref _visiblePages, value);
}
public string SearchFilter
{
get => _searchFilter;
set
{
if (SetProperty(ref _searchFilter, value))
UpdateVisiblePages();
}
}
public LauncherPage SelectedPage
{
get => _selectedPage;
set => SetProperty(ref _selectedPage, value);
}
public LauncherPageSwitcher(Launcher launcher)
{
_launcher = launcher;
UpdateVisiblePages();
}
public void ClearFilter()
{
SearchFilter = string.Empty;
}
public void Switch()
{
_launcher.ActivePage = _selectedPage ?? _launcher.ActivePage;
_launcher.CancelSwitcher();
}
public void Dispose()
{
_visiblePages.Clear();
_selectedPage = null;
_searchFilter = string.Empty;
}
private void UpdateVisiblePages()
{
var visible = new List<LauncherPage>();
if (string.IsNullOrEmpty(_searchFilter))
{
visible.AddRange(_launcher.Pages);
}
else
{
foreach (var page in _launcher.Pages)
{
if (page.Node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase) ||
(page.Node.IsRepository && page.Node.Id.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)))
{
visible.Add(page);
}
}
}
VisiblePages = visible;
SelectedPage = visible.Count > 0 ? visible[0] : null;
}
private Launcher _launcher = null;
private List<LauncherPage> _visiblePages = [];
private string _searchFilter = string.Empty;
private LauncherPage _selectedPage = null;
}
}

View file

@ -91,8 +91,8 @@ namespace SourceGit.ViewModels
public bool UseSystemWindowFrame
{
get => _useSystemWindowFrame;
set => SetProperty(ref _useSystemWindowFrame, value);
get => Native.OS.UseSystemWindowFrame;
set => Native.OS.UseSystemWindowFrame = value;
}
public double DefaultFontSize
@ -178,9 +178,9 @@ namespace SourceGit.ViewModels
public bool ShowTagsAsTree
{
get => _showTagsAsTree;
set => SetProperty(ref _showTagsAsTree, value);
}
get;
set;
} = false;
public bool ShowTagsInGraph
{
@ -188,6 +188,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _showTagsInGraph, value);
}
public bool ShowSubmodulesAsTree
{
get;
set;
} = false;
public bool UseTwoColumnsLayoutInHistories
{
get => _useTwoColumnsLayoutInHistories;
@ -656,7 +662,6 @@ namespace SourceGit.ViewModels
private string _defaultFontFamily = string.Empty;
private string _monospaceFontFamily = string.Empty;
private bool _onlyUseMonoFontInEditor = false;
private bool _useSystemWindowFrame = false;
private double _defaultFontSize = 13;
private double _editorFontSize = 13;
private int _editorTabWidth = 4;
@ -672,7 +677,6 @@ namespace SourceGit.ViewModels
private double _lastCheckUpdateTime = 0;
private string _ignoreUpdateTag = string.Empty;
private bool _showTagsAsTree = false;
private bool _showTagsInGraph = true;
private bool _useTwoColumnsLayoutInHistories = false;
private bool _displayTimeAsPeriodInHistories = false;

View file

@ -198,7 +198,21 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _tags, value);
}
public List<Models.Tag> VisibleTags
public bool ShowTagsAsTree
{
get => Preferences.Instance.ShowTagsAsTree;
set
{
if (value != Preferences.Instance.ShowTagsAsTree)
{
Preferences.Instance.ShowTagsAsTree = value;
VisibleTags = BuildVisibleTags();
OnPropertyChanged();
}
}
}
public object VisibleTags
{
get => _visibleTags;
private set => SetProperty(ref _visibleTags, value);
@ -210,7 +224,21 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _submodules, value);
}
public List<Models.Submodule> VisibleSubmodules
public bool ShowSubmodulesAsTree
{
get => Preferences.Instance.ShowSubmodulesAsTree;
set
{
if (value != Preferences.Instance.ShowSubmodulesAsTree)
{
Preferences.Instance.ShowSubmodulesAsTree = value;
VisibleSubmodules = BuildVisibleSubmodules();
OnPropertyChanged();
}
}
}
public object VisibleSubmodules
{
get => _visibleSubmodules;
private set => SetProperty(ref _visibleSubmodules, value);
@ -228,6 +256,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _stashesCount, value);
}
public int LocalBranchesCount
{
get => _localBranchesCount;
private set => SetProperty(ref _localBranchesCount, value);
}
public bool IncludeUntracked
{
get => _settings.IncludeUntrackedInLocalChanges;
@ -528,9 +562,9 @@ namespace SourceGit.ViewModels
_localBranchTrees.Clear();
_remoteBranchTrees.Clear();
_tags.Clear();
_visibleTags.Clear();
_visibleTags = null;
_submodules.Clear();
_visibleSubmodules.Clear();
_visibleSubmodules = null;
_searchedCommits.Clear();
_selectedSearchedCommit = null;
@ -926,7 +960,7 @@ namespace SourceGit.ViewModels
if (!changed)
return;
if (isLocal && !string.IsNullOrEmpty(branch.Upstream))
if (isLocal && !string.IsNullOrEmpty(branch.Upstream) && !branch.IsUpstreamGone)
_settings.UpdateHistoriesFilter(branch.Upstream, Models.FilterType.RemoteBranch, mode);
}
else
@ -1020,7 +1054,7 @@ namespace SourceGit.ViewModels
public void RefreshBranches()
{
var branches = new Commands.QueryBranches(_fullpath).Result();
var branches = new Commands.QueryBranches(_fullpath).Result(out var localBranchesCount);
var remotes = new Commands.QueryRemotes(_fullpath).Result();
var builder = BuildBranchTree(branches, remotes);
@ -1033,6 +1067,7 @@ namespace SourceGit.ViewModels
CurrentBranch = branches.Find(x => x.IsCurrent);
LocalBranchTrees = builder.Locals;
RemoteBranchTrees = builder.Remotes;
LocalBranchesCount = localBranchesCount;
if (_workingCopy != null)
_workingCopy.HasRemotes = remotes.Count > 0;
@ -2323,14 +2358,15 @@ namespace SourceGit.ViewModels
return menu;
}
public ContextMenu CreateContextMenuForSubmodule(string submodule)
public ContextMenu CreateContextMenuForSubmodule(Models.Submodule submodule)
{
var open = new MenuItem();
open.Header = App.Text("Submodule.Open");
open.Icon = App.CreateMenuIcon("Icons.Folder.Open");
open.IsEnabled = submodule.Status != Models.SubmoduleStatus.NotInited;
open.Click += (_, ev) =>
{
OpenSubmodule(submodule);
OpenSubmodule(submodule.Path);
ev.Handled = true;
};
@ -2339,7 +2375,7 @@ namespace SourceGit.ViewModels
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (_, ev) =>
{
App.CopyText(submodule);
App.CopyText(submodule.Path);
ev.Handled = true;
};
@ -2349,7 +2385,7 @@ namespace SourceGit.ViewModels
rm.Click += (_, ev) =>
{
if (CanCreatePopup())
ShowPopup(new DeleteSubmodule(this, submodule));
ShowPopup(new DeleteSubmodule(this, submodule.Path));
ev.Handled = true;
};
@ -2470,7 +2506,7 @@ namespace SourceGit.ViewModels
return builder;
}
private List<Models.Tag> BuildVisibleTags()
private object BuildVisibleTags()
{
switch (_settings.TagSortMode)
{
@ -2501,10 +2537,14 @@ namespace SourceGit.ViewModels
var historiesFilters = _settings.CollectHistoriesFilters();
UpdateTagFilterMode(historiesFilters);
return visible;
if (Preferences.Instance.ShowTagsAsTree)
return TagCollectionAsTree.Build(visible, _visibleTags as TagCollectionAsTree);
else
return new TagCollectionAsList() { Tags = visible };
}
private List<Models.Submodule> BuildVisibleSubmodules()
private object BuildVisibleSubmodules()
{
var visible = new List<Models.Submodule>();
if (string.IsNullOrEmpty(_filter))
@ -2519,7 +2559,11 @@ namespace SourceGit.ViewModels
visible.Add(s);
}
}
return visible;
if (Preferences.Instance.ShowSubmodulesAsTree)
return SubmoduleCollectionAsTree.Build(visible, _visibleSubmodules as SubmoduleCollectionAsTree);
else
return new SubmoduleCollectionAsList() { Submodules = visible };
}
private void RefreshHistoriesFilters(bool refresh)
@ -2588,7 +2632,7 @@ namespace SourceGit.ViewModels
if (node.Path.Equals(path, StringComparison.Ordinal))
return node;
if (path!.StartsWith(node.Path, StringComparison.Ordinal))
if (path.StartsWith(node.Path, StringComparison.Ordinal))
{
var founded = FindBranchNode(node.Children, path);
if (founded != null)
@ -2726,6 +2770,7 @@ namespace SourceGit.ViewModels
private int _selectedViewIndex = 0;
private object _selectedView = null;
private int _localBranchesCount = 0;
private int _localChangesCount = 0;
private int _stashesCount = 0;
@ -2748,9 +2793,9 @@ namespace SourceGit.ViewModels
private List<BranchTreeNode> _remoteBranchTrees = new List<BranchTreeNode>();
private List<Models.Worktree> _worktrees = new List<Models.Worktree>();
private List<Models.Tag> _tags = new List<Models.Tag>();
private List<Models.Tag> _visibleTags = new List<Models.Tag>();
private object _visibleTags = null;
private List<Models.Submodule> _submodules = new List<Models.Submodule>();
private List<Models.Submodule> _visibleSubmodules = new List<Models.Submodule>();
private object _visibleSubmodules = null;
private bool _isAutoFetching = false;
private Timer _autoFetchTimer = null;

View file

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class SubmoduleTreeNode : ObservableObject
{
public string FullPath { get; set; } = string.Empty;
public int Depth { get; private set; } = 0;
public Models.Submodule Module { get; private set; } = null;
public List<SubmoduleTreeNode> Children { get; private set; } = [];
public int Counter = 0;
public bool IsFolder
{
get => Module == null;
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public string ChildCounter
{
get => Counter > 0 ? $"({Counter})" : string.Empty;
}
public bool IsDirty
{
get => Module?.IsDirty ?? false;
}
public SubmoduleTreeNode(Models.Submodule module, int depth)
{
FullPath = module.Path;
Depth = depth;
Module = module;
IsExpanded = false;
}
public SubmoduleTreeNode(string path, int depth, bool isExpanded)
{
FullPath = path;
Depth = depth;
IsExpanded = isExpanded;
Counter = 1;
}
public static List<SubmoduleTreeNode> Build(IList<Models.Submodule> submodules, HashSet<string> expaneded)
{
var nodes = new List<SubmoduleTreeNode>();
var folders = new Dictionary<string, SubmoduleTreeNode>();
foreach (var module in submodules)
{
var sepIdx = module.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1)
{
nodes.Add(new SubmoduleTreeNode(module, 0));
}
else
{
SubmoduleTreeNode lastFolder = null;
int depth = 0;
while (sepIdx != -1)
{
var folder = module.Path.Substring(0, sepIdx);
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
lastFolder.Counter++;
}
else if (lastFolder == null)
{
lastFolder = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
folders.Add(folder, lastFolder);
InsertFolder(nodes, lastFolder);
}
else
{
var cur = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
folders.Add(folder, cur);
InsertFolder(lastFolder.Children, cur);
lastFolder = cur;
}
depth++;
sepIdx = module.Path.IndexOf('/', sepIdx + 1);
}
lastFolder?.Children.Add(new SubmoduleTreeNode(module, depth));
}
}
folders.Clear();
return nodes;
}
private static void InsertFolder(List<SubmoduleTreeNode> collection, SubmoduleTreeNode subFolder)
{
for (int i = 0; i < collection.Count; i++)
{
if (!collection[i].IsFolder)
{
collection.Insert(i, subFolder);
return;
}
}
collection.Add(subFolder);
}
private bool _isExpanded = false;
}
public class SubmoduleCollectionAsTree
{
public List<SubmoduleTreeNode> Tree
{
get;
set;
} = [];
public AvaloniaList<SubmoduleTreeNode> Rows
{
get;
set;
} = [];
public static SubmoduleCollectionAsTree Build(List<Models.Submodule> submodules, SubmoduleCollectionAsTree old)
{
var oldExpanded = new HashSet<string>();
if (old != null)
{
foreach (var row in old.Rows)
{
if (row.IsFolder && row.IsExpanded)
oldExpanded.Add(row.FullPath);
}
}
var collection = new SubmoduleCollectionAsTree();
collection.Tree = SubmoduleTreeNode.Build(submodules, oldExpanded);
var rows = new List<SubmoduleTreeNode>();
MakeTreeRows(rows, collection.Tree);
collection.Rows.AddRange(rows);
return collection;
}
public void ToggleExpand(SubmoduleTreeNode node)
{
node.IsExpanded = !node.IsExpanded;
var rows = Rows;
var depth = node.Depth;
var idx = rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{
var subrows = new List<SubmoduleTreeNode>();
MakeTreeRows(subrows, node.Children);
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);
}
}
private static void MakeTreeRows(List<SubmoduleTreeNode> rows, List<SubmoduleTreeNode> nodes)
{
foreach (var node in nodes)
{
rows.Add(node);
if (!node.IsExpanded || !node.IsFolder)
continue;
MakeTreeRows(rows, node.Children);
}
}
}
public class SubmoduleCollectionAsList
{
public List<Models.Submodule> Submodules
{
get;
set;
} = [];
}
}

View file

@ -1,21 +1,34 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class TagTreeNodeToolTip
{
public string Name { get; private set; }
public bool IsAnnotated { get; private set; }
public string Message { get; private set; }
public TagTreeNodeToolTip(Models.Tag t)
{
Name = t.Name;
IsAnnotated = t.IsAnnotated;
Message = t.Message;
}
}
public class TagTreeNode : ObservableObject
{
public string FullPath { get; set; }
public int Depth { get; private set; } = 0;
public Models.Tag Tag { get; private set; } = null;
public TagTreeNodeToolTip ToolTip { get; private set; } = null;
public List<TagTreeNode> Children { get; private set; } = [];
public object ToolTip
{
get => Tag?.Message;
}
public int Counter { get; set; } = 0;
public bool IsFolder
{
@ -28,11 +41,17 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isExpanded, value);
}
public string TagsCount
{
get => Counter > 0 ? $"({Counter})" : string.Empty;
}
public TagTreeNode(Models.Tag t, int depth)
{
FullPath = t.Name;
Depth = depth;
Tag = t;
ToolTip = new TagTreeNodeToolTip(t);
IsExpanded = false;
}
@ -41,9 +60,10 @@ namespace SourceGit.ViewModels
FullPath = path;
Depth = depth;
IsExpanded = isExpanded;
Counter = 1;
}
public static List<TagTreeNode> Build(IList<Models.Tag> tags, HashSet<string> expaneded)
public static List<TagTreeNode> Build(List<Models.Tag> tags, HashSet<string> expaneded)
{
var nodes = new List<TagTreeNode>();
var folders = new Dictionary<string, TagTreeNode>();
@ -66,6 +86,7 @@ namespace SourceGit.ViewModels
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
lastFolder.Counter++;
}
else if (lastFolder == null)
{
@ -112,7 +133,7 @@ namespace SourceGit.ViewModels
public class TagCollectionAsList
{
public AvaloniaList<Models.Tag> Tags
public List<Models.Tag> Tags
{
get;
set;
@ -132,5 +153,71 @@ namespace SourceGit.ViewModels
get;
set;
} = [];
public static TagCollectionAsTree Build(List<Models.Tag> tags, TagCollectionAsTree old)
{
var oldExpanded = new HashSet<string>();
if (old != null)
{
foreach (var row in old.Rows)
{
if (row.IsFolder && row.IsExpanded)
oldExpanded.Add(row.FullPath);
}
}
var collection = new TagCollectionAsTree();
collection.Tree = TagTreeNode.Build(tags, oldExpanded);
var rows = new List<TagTreeNode>();
MakeTreeRows(rows, collection.Tree);
collection.Rows.AddRange(rows);
return collection;
}
public void ToggleExpand(TagTreeNode node)
{
node.IsExpanded = !node.IsExpanded;
var rows = Rows;
var depth = node.Depth;
var idx = rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{
var subrows = new List<TagTreeNode>();
MakeTreeRows(subrows, node.Children);
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);
}
}
private static void MakeTreeRows(List<TagTreeNode> rows, List<TagTreeNode> nodes)
{
foreach (var node in nodes)
{
rows.Add(node);
if (!node.IsExpanded || !node.IsFolder)
continue;
MakeTreeRows(rows, node.Children);
}
}
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class WorkspaceSwitcher : ObservableObject, IDisposable
{
public List<Workspace> VisibleWorkspaces
{
get => _visibleWorkspaces;
private set => SetProperty(ref _visibleWorkspaces, value);
}
public string SearchFilter
{
get => _searchFilter;
set
{
if (SetProperty(ref _searchFilter, value))
UpdateVisibleWorkspaces();
}
}
public Workspace SelectedWorkspace
{
get => _selectedWorkspace;
set => SetProperty(ref _selectedWorkspace, value);
}
public WorkspaceSwitcher(Launcher launcher)
{
_launcher = launcher;
UpdateVisibleWorkspaces();
}
public void ClearFilter()
{
SearchFilter = string.Empty;
}
public void Switch()
{
_launcher.SwitchWorkspace(_selectedWorkspace);
_launcher.CancelSwitcher();
}
public void Dispose()
{
_visibleWorkspaces.Clear();
_selectedWorkspace = null;
_searchFilter = string.Empty;
}
private void UpdateVisibleWorkspaces()
{
var visible = new List<Workspace>();
if (string.IsNullOrEmpty(_searchFilter))
{
visible.AddRange(Preferences.Instance.Workspaces);
}
else
{
foreach (var workspace in Preferences.Instance.Workspaces)
{
if (workspace.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
visible.Add(workspace);
}
}
VisibleWorkspaces = visible;
SelectedWorkspace = visible.Count == 0 ? null : visible[0];
}
private Launcher _launcher = null;
private List<Workspace> _visibleWorkspaces = null;
private string _searchFilter = string.Empty;
private Workspace _selectedWorkspace = null;
}
}

View file

@ -10,7 +10,7 @@
Title="{DynamicResource Text.About}"
Width="520" Height="230"
CanResize="False"
WindowStartupLocation="CenterScreen">
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*">
<!-- TitleBar -->
<Grid Grid.Row="0" Height="28" IsVisible="{Binding !#ThisControl.UseSystemWindowFrame}">
@ -67,4 +67,9 @@
</StackPanel>
</Grid>
</Grid>
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding #ThisControl.Close}"/>
</Window.KeyBindings>
</v:ChromelessWindow>

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Avalonia;
using Avalonia.Controls;
@ -12,14 +13,6 @@ namespace SourceGit.Views
{
public class Avatar : Control, Models.IAvatarHost
{
private static readonly GradientStops[] FALLBACK_GRADIENTS = [
new GradientStops() { new GradientStop(Colors.Orange, 0), new GradientStop(Color.FromRgb(255, 213, 134), 1) },
new GradientStops() { new GradientStop(Colors.DodgerBlue, 0), new GradientStop(Colors.LightSkyBlue, 1) },
new GradientStops() { new GradientStop(Colors.LimeGreen, 0), new GradientStop(Color.FromRgb(124, 241, 124), 1) },
new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
];
public static readonly StyledProperty<Models.User> UserProperty =
AvaloniaProperty.Register<Avatar, Models.User>(nameof(User));
@ -29,11 +22,6 @@ namespace SourceGit.Views
set => SetValue(UserProperty, value);
}
static Avatar()
{
UserProperty.Changed.AddClassHandler<Avatar>(OnUserPropertyChanged);
}
public Avatar()
{
var refetch = new MenuItem() { Header = App.Text("RefetchAvatar") };
@ -55,25 +43,72 @@ namespace SourceGit.Views
return;
var corner = (float)Math.Max(2, Bounds.Width / 16);
var img = Models.AvatarManager.Instance.Request(User.Email, false);
if (img != null)
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
var clip = context.PushClip(new RoundedRect(rect, corner));
if (_img != null)
{
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.PushClip(new RoundedRect(rect, corner));
context.DrawImage(img, rect);
context.DrawImage(_img, rect);
}
else
{
Point textOrigin = new Point((Bounds.Width - _fallbackLabel.Width) * 0.5, (Bounds.Height - _fallbackLabel.Height) * 0.5);
context.DrawRectangle(_fallbackBrush, null, new Rect(0, 0, Bounds.Width, Bounds.Height), corner, corner);
context.DrawText(_fallbackLabel, textOrigin);
context.DrawRectangle(Brushes.White, new Pen(new SolidColorBrush(Colors.Black, 0.3f), 0.65f), rect, corner, corner);
var offsetX = Bounds.Width / 10.0;
var offsetY = Bounds.Height / 10.0;
var stepX = (Bounds.Width - offsetX * 2) / 5.0;
var stepY = (Bounds.Height - offsetY * 2) / 5.0;
var user = User;
var lowered = user.Email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered));
var brush = new SolidColorBrush(new Color(255, hash[0], hash[1], hash[2]));
var switches = new bool[15];
for (int i = 0; i < switches.Length; i++)
switches[i] = hash[i + 1] % 2 == 1;
for (int row = 0; row < 5; row++)
{
var x = offsetX + stepX * 2;
var y = offsetY + stepY * row;
var idx = row * 3;
if (switches[idx])
context.FillRectangle(brush, new Rect(x, y, stepX, stepY));
if (switches[idx + 1])
context.FillRectangle(brush, new Rect(x + stepX, y, stepX, stepY));
if (switches[idx + 2])
context.FillRectangle(brush, new Rect(x + stepX * 2, y, stepX, stepY));
}
for (int row = 0; row < 5; row++)
{
var x = offsetX;
var y = offsetY + stepY * row;
var idx = row * 3 + 2;
if (switches[idx])
context.FillRectangle(brush, new Rect(x, y, stepX, stepY));
if (switches[idx - 1])
context.FillRectangle(brush, new Rect(x + stepX, y, stepX, stepY));
}
}
clip.Dispose();
}
public void OnAvatarResourceChanged(string email)
{
if (User.Email.Equals(email, StringComparison.Ordinal))
{
_img = Models.AvatarManager.Instance.Request(User.Email, false);
InvalidateVisual();
}
}
protected override void OnLoaded(RoutedEventArgs e)
@ -88,53 +123,21 @@ namespace SourceGit.Views
Models.AvatarManager.Instance.Unsubscribe(this);
}
private static void OnUserPropertyChanged(Avatar avatar, AvaloniaPropertyChangedEventArgs e)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (avatar.User == null)
return;
base.OnPropertyChanged(change);
var fallback = GetFallbackString(avatar.User.Name);
var chars = fallback.ToCharArray();
var sum = 0;
foreach (var c in chars)
sum += Math.Abs(c);
avatar._fallbackBrush = new LinearGradientBrush
if (change.Property == UserProperty)
{
GradientStops = FALLBACK_GRADIENTS[sum % FALLBACK_GRADIENTS.Length],
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
};
var user = User;
if (user == null)
return;
var typeface = new Typeface("fonts:SourceGit#JetBrains Mono");
avatar._fallbackLabel = new FormattedText(
fallback,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
avatar.Width * 0.65,
Brushes.White);
avatar.InvalidateVisual();
_img = Models.AvatarManager.Instance.Request(User.Email, false);
InvalidateVisual();
}
}
private static string GetFallbackString(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "?";
var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var chars = new List<char>();
foreach (var part in parts)
chars.Add(part[0]);
if (chars.Count >= 2 && char.IsAsciiLetterOrDigit(chars[0]) && char.IsAsciiLetterOrDigit(chars[^1]))
return string.Format("{0}{1}", chars[0], chars[^1]);
return name.Substring(0, 1);
}
private FormattedText _fallbackLabel = null;
private LinearGradientBrush _fallbackBrush = null;
private Bitmap _img = null;
}
}

View file

@ -32,11 +32,13 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:BranchTreeNode">
<Border Background="Transparent" PointerPressed="OnNodePointerPressed">
<Border Background="Transparent"
PointerPressed="OnNodePointerPressed"
ToolTip.Tip="{Binding Tooltip}"
ToolTip.Placement="Right">
<Grid Height="24"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
ColumnDefinitions="16,*"
ToolTip.Tip="{Binding Tooltip}">
ColumnDefinitions="16,*">
<!-- Tree Expander -->
<v:BranchTreeNodeToggleButton Grid.Column="0"
@ -53,16 +55,16 @@
DoubleTapped="OnDoubleTappedBranchNode">
<!-- Icon -->
<v:BranchTreeNodeIcon Grid.Column="0"
Node="{Binding}"
IsExpanded="{Binding IsExpanded}"/>
<v:BranchTreeNodeIcon Grid.Column="0" IsExpanded="{Binding IsExpanded}"/>
<!-- Name -->
<TextBlock Grid.Column="1"
Classes="primary"
Text="{Binding Name}"
FontWeight="{Binding IsCurrent, Converter={x:Static c:BoolConverters.IsBoldToFontWeight}}"
TextTrimming="CharacterEllipsis"/>
TextTrimming="CharacterEllipsis">
<Run Text="{Binding Name}"/>
<Run Text="{Binding BranchesCount}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
<!-- Upstream invalid tip -->
<Border Grid.Column="2"

View file

@ -17,15 +17,6 @@ namespace SourceGit.Views
{
public class BranchTreeNodeIcon : UserControl
{
public static readonly StyledProperty<ViewModels.BranchTreeNode> NodeProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, ViewModels.BranchTreeNode>(nameof(Node));
public ViewModels.BranchTreeNode Node
{
get => GetValue(NodeProperty);
set => SetValue(NodeProperty, value);
}
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, bool>(nameof(IsExpanded));
@ -35,16 +26,23 @@ namespace SourceGit.Views
set => SetValue(IsExpandedProperty, value);
}
static BranchTreeNodeIcon()
protected override void OnDataContextChanged(EventArgs e)
{
NodeProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, _) => icon.UpdateContent());
IsExpandedProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, _) => icon.UpdateContent());
base.OnDataContextChanged(e);
UpdateContent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
UpdateContent();
}
private void UpdateContent()
{
var node = Node;
if (node == null)
if (DataContext is not ViewModels.BranchTreeNode node)
{
Content = null;
return;

View file

@ -3,7 +3,6 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
namespace SourceGit.Views
{
@ -11,7 +10,7 @@ namespace SourceGit.Views
{
public bool UseSystemWindowFrame
{
get => OperatingSystem.IsLinux() && ViewModels.Preferences.Instance.UseSystemWindowFrame;
get => Native.OS.UseSystemWindowFrame;
}
protected override Type StyleKeyOverride => typeof(Window);
@ -19,32 +18,7 @@ namespace SourceGit.Views
public ChromelessWindow()
{
Focusable = true;
if (OperatingSystem.IsLinux())
{
if (UseSystemWindowFrame)
{
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default;
ExtendClientAreaToDecorationsHint = false;
}
else
{
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
ExtendClientAreaToDecorationsHint = true;
Classes.Add("custom_window_frame");
}
}
else if (OperatingSystem.IsWindows())
{
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
ExtendClientAreaToDecorationsHint = true;
Classes.Add("fix_maximized_padding");
}
else
{
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome;
ExtendClientAreaToDecorationsHint = true;
}
Native.OS.SetupForWindow(this);
}
public void BeginMoveWindow(object _, PointerPressedEventArgs e)

View file

@ -12,6 +12,7 @@
Title="{DynamicResource Text.Warn}"
SizeToContent="WidthAndHeight"
CanResize="False"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,Auto">
<!-- TitleBar -->
@ -64,6 +65,7 @@
Height="30"
Margin="4,0"
Click="CloseWindow"
IsCancel="True"
Content="{DynamicResource Text.Cancel}"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"/>

View file

@ -175,7 +175,17 @@
<Path Width="12" Height="12" Data="{StaticResource Icons.Layout}" Margin="0,2,0,0"/>
</ToggleButton>
<Button Classes="icon_button" Width="28" Command="{Binding OpenExternalMergeTool}" ToolTip.Tip="{DynamicResource Text.Diff.UseMerger}">
<Button Classes="icon_button"
Width="28"
Command="{Binding OpenExternalMergeTool}"
HotKey="{OnPlatform Ctrl+D, macOS=⌘+D}">
<ToolTip.Tip>
<TextBlock>
<Run Text="{DynamicResource Text.Diff.UseMerger}"/>
<Run Text=" "/>
<Run Text="{OnPlatform Ctrl+D, macOS=⌘+D}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
</ToolTip.Tip>
<Path Width="12" Height="12" Stretch="Uniform" Data="{StaticResource Icons.OpenWith}"/>
</Button>
</StackPanel>

View file

@ -45,8 +45,8 @@
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}"
Margin="0,0,0,8"/>
<Grid RowDefinitions="20,20,20,20,20,20,20,20" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+Shift+P, macOS=⌘+\,}"/>
<Grid RowDefinitions="20,20,20,20,20,20,20,20,20,20" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+\,, macOS=⌘+\,}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.Global.OpenPreferences}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+T, macOS=⌘+T}"/>
@ -55,7 +55,7 @@
<TextBlock Grid.Row="2" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+W, macOS=⌘+W}" />
<TextBlock Grid.Row="2" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.Global.CloseTab}" />
<TextBlock Grid.Row="3" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Shift+Ctrl+Tab, macOS=⌘+⌥+←}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+Shift+Tab, macOS=⌘+⌥+←}"/>
<TextBlock Grid.Row="3" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.Global.GotoPrevTab}" />
<TextBlock Grid.Row="4" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+Tab, macOS=⌘+⌥+→}"/>
@ -69,6 +69,12 @@
<TextBlock Grid.Row="7" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+Q, macOS=⌘+Q}"/>
<TextBlock Grid.Row="7" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Quit}" />
<TextBlock Grid.Row="8" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+Shift+P, macOS=⌘+⇧+P}"/>
<TextBlock Grid.Row="8" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.Global.SwitchWorkspace}" />
<TextBlock Grid.Row="9" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+P, macOS=⌘+P}"/>
<TextBlock Grid.Row="9" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.Global.SwitchTab}" />
</Grid>
<TextBlock Text="{DynamicResource Text.Hotkeys.Repo}"
@ -130,7 +136,7 @@
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}"
Margin="0,8"/>
<Grid RowDefinitions="20,20,20,20" ColumnDefinitions="150,*">
<Grid RowDefinitions="20,20,20,20,20" ColumnDefinitions="150,*">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+F, macOS=⌘+F}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.TextEditor.Search}" />
@ -142,6 +148,9 @@
<TextBlock Grid.Row="3" Grid.Column="0" Classes="primary bold" Text="ESC"/>
<TextBlock Grid.Row="3" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.TextEditor.CloseSearch}" />
<TextBlock Grid.Row="4" Grid.Column="0" Classes="primary bold" Text="{OnPlatform Ctrl+D, macOS=⌘+D}"/>
<TextBlock Grid.Row="4" Grid.Column="1" Margin="16,0,0,0" Text="{DynamicResource Text.Hotkeys.TextEditor.OpenExternalMergeTool}" />
</Grid>
</StackPanel>
</Border>

View file

@ -72,7 +72,7 @@
<Path Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
</Button>
<!-- Workspace Switcher -->
<!-- Workspace Dropdown Menu -->
<Button Grid.Column="1" Classes="icon_button" VerticalAlignment="Bottom" Margin="0,0,0,1" Click="OnOpenWorkspaceMenu">
<ToolTip.Tip>
<StackPanel Orientation="Horizontal">
@ -85,11 +85,11 @@
Fill="{Binding ActiveWorkspace.Brush}"/>
</Button>
<!-- Pages Tabs-->
<!-- Pages Tabs -->
<v:LauncherTabBar Grid.Column="2" Height="30" Margin="0,0,16,0" VerticalAlignment="Bottom"/>
<!-- Caption Buttons (Windows/Linux)-->
<Border Grid.Column="3" Margin="16,0,0,0" IsVisible="{Binding #ThisControl.IsRightCaptionButtonsVisible}">
<!-- Caption Buttons (Windows/Linux) -->
<Border Grid.Column="3" Margin="16,0,0,0" IsVisible="{Binding #ThisControl.HasRightCaptionButton}">
<v:CaptionButtons Height="30" VerticalAlignment="Top"/>
</Border>
</Grid>
@ -102,5 +102,27 @@
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<!-- Workspace/Pages Switcher -->
<Border Grid.Row="0" Grid.RowSpan="2"
Background="Transparent"
IsVisible="{Binding Switcher, Converter={x:Static ObjectConverters.IsNotNull}}"
PointerPressed="OnCancelSwitcher">
<Border HorizontalAlignment="Center" VerticalAlignment="Center" Effect="drop-shadow(0 0 12 #A0000000)">
<Border Background="{DynamicResource Brush.Popup}" CornerRadius="8">
<ContentControl Margin="16,10,16,12" Content="{Binding Switcher}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:WorkspaceSwitcher">
<v:WorkspaceSwitcher/>
</DataTemplate>
<DataTemplate DataType="vm:LauncherPageSwitcher">
<v:LauncherPageSwitcher/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Border>
</Border>
</Border>
</Grid>
</v:ChromelessWindow>

View file

@ -29,12 +29,13 @@ namespace SourceGit.Views
set => SetValue(HasLeftCaptionButtonProperty, value);
}
public bool IsRightCaptionButtonsVisible
public bool HasRightCaptionButton
{
get
{
if (OperatingSystem.IsLinux())
return !ViewModels.Preferences.Instance.UseSystemWindowFrame;
return !Native.OS.UseSystemWindowFrame;
return OperatingSystem.IsWindows();
}
}
@ -52,8 +53,7 @@ namespace SourceGit.Views
{
HasLeftCaptionButton = true;
CaptionHeight = new GridLength(34);
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome |
ExtendClientAreaChromeHints.OSXThickTitleBar;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
}
else if (UseSystemWindowFrame)
{
@ -100,7 +100,7 @@ namespace SourceGit.Views
if (change.Property == WindowStateProperty)
{
_lastWindowState = (WindowState)change.OldValue;
_lastWindowState = (WindowState)change.OldValue!;
var state = (WindowState)change.NewValue!;
if (!OperatingSystem.IsMacOS() && !UseSystemWindowFrame)
@ -133,8 +133,8 @@ namespace SourceGit.Views
return;
}
// Ctrl+Shift+P opens preference dialog (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: (KeyModifiers.Control | KeyModifiers.Shift), Key: Key.P })
// Ctrl+, opens preference dialog (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.OemComma })
{
App.ShowWindow(new Preferences(), true);
e.Handled = true;
@ -149,7 +149,7 @@ namespace SourceGit.Views
}
// Ctrl+Q quits the application (macOS use hotkeys in system menu bar)
if (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control && e.Key == Key.Q)
if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.Q })
{
App.Quit(0);
return;
@ -157,6 +157,20 @@ namespace SourceGit.Views
if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control))
{
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.P)
{
vm.OpenWorkspaceSwitcher();
e.Handled = true;
return;
}
if (e.Key == Key.P)
{
vm.OpenTabSwitcher();
e.Handled = true;
return;
}
if (e.Key == Key.W)
{
vm.CloseTab(null);
@ -251,6 +265,7 @@ namespace SourceGit.Views
else if (e.Key == Key.Escape)
{
vm.ActivePage.CancelPopup();
vm.CancelSwitcher();
e.Handled = true;
return;
}
@ -307,6 +322,13 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnCancelSwitcher(object sender, PointerPressedEventArgs e)
{
if (e.Source == sender)
(DataContext as ViewModels.Launcher)?.CancelSwitcher();
e.Handled = true;
}
private KeyModifiers _unhandledModifiers = KeyModifiers.None;
private WindowState _lastWindowState = WindowState.Normal;
}

View file

@ -3,19 +3,27 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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.LauncherTabsSelector"
x:Name="ThisControl">
<Grid RowDefinitions="28,Auto">
<TextBox Grid.Row="0"
x:Class="SourceGit.Views.LauncherPageSwitcher"
x:DataType="vm:LauncherPageSwitcher">
<Grid RowDefinitions="Auto,Auto,Auto">
<TextBlock Grid.Row="0"
Text="{DynamicResource Text.Launcher.Pages}"
FontWeight="Bold"
HorizontalAlignment="Center"/>
<TextBox Grid.Row="1"
Height="24"
Margin="4,0"
Margin="4,8,4,0"
BorderThickness="1"
CornerRadius="12"
Text="{Binding #ThisControl.SearchFilter, Mode=TwoWay}"
Text="{Binding SearchFilter, Mode=TwoWay}"
KeyDown="OnSearchBoxKeyDown"
BorderBrush="{DynamicResource Brush.Border2}"
VerticalContentAlignment="Center">
VerticalContentAlignment="Center"
v:AutoFocusBehaviour.IsEnabled="True">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14"
Margin="6,0,0,0"
@ -27,8 +35,8 @@
<Button Classes="icon_button"
Width="16"
Margin="0,0,6,0"
Click="OnClearSearchFilter"
IsVisible="{Binding #ThisControl.SearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding ClearFilter}"
IsVisible="{Binding SearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Right">
<Path Width="14" Height="14"
Margin="0,1,0,0"
@ -38,19 +46,22 @@
</TextBox.InnerRightContent>
</TextBox>
<ListBox Grid.Row="1"
Width="200"
<ListBox Grid.Row="2"
x:Name="PagesListBox"
Width="300"
MaxHeight="400"
Margin="0,4,0,0"
Background="Transparent"
Margin="4,8,4,0"
BorderThickness="0"
SelectionMode="Single"
Background="Transparent"
Focusable="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding #ThisControl.VisiblePages}"
SelectionChanged="OnPageSelectionChanged">
ItemsSource="{Binding VisiblePages, Mode=OneWay}"
SelectedItem="{Binding SelectedPage, Mode=TwoWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Padding" Value="8,0"/>
<Setter Property="MinHeight" Value="26"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
@ -72,30 +83,28 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:LauncherPage">
<Grid ColumnDefinitions="Auto,*" VerticalAlignment="Center">
<Grid ColumnDefinitions="Auto,6,*" Background="Transparent" DoubleTapped="OnItemDoubleTapped">
<Path Grid.Column="0"
Width="12" Height="12" Margin="12,0"
Width="12" Height="12"
Fill="{Binding Node.Bookmark, Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding Node.IsRepository}"
IsHitTestVisible="False"/>
<Path Grid.Column="0"
Width="12" Height="12" Margin="12,0"
Width="12" Height="12"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Repositories}"
IsVisible="{Binding !Node.IsRepository}"
IsHitTestVisible="False"/>
<TextBlock Grid.Column="1"
<TextBlock Grid.Column="2"
Classes="primary"
VerticalAlignment="Center"
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
Text="{Binding Node.Name}"
IsVisible="{Binding Node.IsRepository}"
IsHitTestVisible="False"/>
<TextBlock Grid.Column="1"
<TextBlock Grid.Column="2"
Classes="primary"
VerticalAlignment="Center"
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
Text="{DynamicResource Text.PageTabBar.Welcome.Title}"
IsVisible="{Binding !Node.IsRepository}"
IsHitTestVisible="False"/>

View file

@ -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;
}
}
}
}

View file

@ -40,7 +40,7 @@
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:LauncherPage}">
<DataTemplate DataType="vm:LauncherPage">
<Border Height="30"
Background="Transparent"
PointerPressed="OnPointerPressedTab"
@ -141,8 +141,106 @@
<Button x:Name="PageSelector" Classes="icon_button" Width="16" Height="16" Margin="8,0">
<Button.Flyout>
<Flyout>
<v:LauncherTabsSelector Pages="{Binding Pages}" PageSelected="OnGotoSelectedPage"/>
<Flyout Opened="OnTabsDropdownOpened" Closed="OnTabsDropdownClosed">
<Grid RowDefinitions="28,Auto" KeyDown="OnTabsDropdownKeyDown" LostFocus="OnTabsDropdownLostFocus">
<TextBox Grid.Row="0"
Height="24"
Margin="4,0"
BorderThickness="1"
CornerRadius="12"
Text="{Binding #ThisControl.SearchFilter, Mode=TwoWay}"
BorderBrush="{DynamicResource Brush.Border2}"
VerticalContentAlignment="Center"
KeyDown="OnTabsDropdownSearchBoxKeyDown"
v:AutoFocusBehaviour.IsEnabled="True">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14"
Margin="6,0,0,0"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
Width="16"
Margin="0,0,6,0"
Click="OnClearSearchFilter"
IsVisible="{Binding #ThisControl.SearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Right">
<Path Width="14" Height="14"
Margin="0,1,0,0"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<ListBox Grid.Row="1"
x:Name="TabsDropdownList"
Focusable="True"
Width="200"
MaxHeight="400"
Margin="0,4,0,0"
Background="Transparent"
SelectionMode="Single"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding #ThisControl.SelectablePages}"
SelectedItem="{Binding ActivePage, Mode=OneWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="MinHeight" Value="26"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="ListBox">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Grid/>
</FocusAdornerTemplate>
</Setter>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:LauncherPage">
<Grid ColumnDefinitions="Auto,*" Background="Transparent" DoubleTapped="OnTabsDropdownItemDoubleTapped">
<Path Grid.Column="0"
Width="12" Height="12" Margin="12,0"
Fill="{Binding Node.Bookmark, Converter={x:Static c:IntConverters.ToBookmarkBrush}}"
Data="{StaticResource Icons.Bookmark}"
IsVisible="{Binding Node.IsRepository}"
IsHitTestVisible="False"/>
<Path Grid.Column="0"
Width="12" Height="12" Margin="12,0"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Repositories}"
IsVisible="{Binding !Node.IsRepository}"
IsHitTestVisible="False"/>
<TextBlock Grid.Column="1"
Classes="primary"
VerticalAlignment="Center"
Text="{Binding Node.Name}"
IsVisible="{Binding Node.IsRepository}"
IsHitTestVisible="False"/>
<TextBlock Grid.Column="1"
Classes="primary"
VerticalAlignment="Center"
Text="{DynamicResource Text.PageTabBar.Welcome.Title}"
IsVisible="{Binding !Node.IsRepository}"
IsHitTestVisible="False"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Flyout>
</Button.Flyout>
<Path Width="14" Height="14" Data="{StaticResource Icons.CircleDown}"/>

View file

@ -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<string> SearchFilterProperty =
AvaloniaProperty.Register<LauncherTabBar, string>(nameof(SearchFilter));
public string SearchFilter
{
get => GetValue(SearchFilterProperty);
set => SetValue(SearchFilterProperty, value);
}
public AvaloniaList<ViewModels.LauncherPage> 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;

View file

@ -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<AvaloniaList<ViewModels.LauncherPage>> PagesProperty =
AvaloniaProperty.Register<LauncherTabsSelector, AvaloniaList<ViewModels.LauncherPage>>(nameof(Pages));
public AvaloniaList<ViewModels.LauncherPage> Pages
{
get => GetValue(PagesProperty);
set => SetValue(PagesProperty, value);
}
public static readonly StyledProperty<string> SearchFilterProperty =
AvaloniaProperty.Register<LauncherTabsSelector, string>(nameof(SearchFilter));
public string SearchFilter
{
get => GetValue(SearchFilterProperty);
set => SetValue(SearchFilterProperty, value);
}
public static readonly RoutedEvent<LauncherTabSelectedEventArgs> PageSelectedEvent =
RoutedEvent.Register<LauncherTabsSelector, LauncherTabSelectedEventArgs>(nameof(PageSelected), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<LauncherTabSelectedEventArgs> PageSelected
{
add { AddHandler(PageSelectedEvent, value); }
remove { RemoveHandler(PageSelectedEvent, value); }
}
public AvaloniaList<ViewModels.LauncherPage> VisiblePages
{
get;
private set;
}
public LauncherTabsSelector()
{
VisiblePages = new AvaloniaList<ViewModels.LauncherPage>();
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;
}
}

View file

@ -129,23 +129,23 @@
<CheckBox Grid.Row="5" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.ShowAuthorTime}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowAuthorTimeInGraph, Mode=TwoWay}"/>
IsChecked="{Binding ShowAuthorTimeInGraph, Mode=TwoWay}"/>
<CheckBox Grid.Row="6" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.ShowTagsInGraph}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowTagsInGraph, Mode=TwoWay}"/>
IsChecked="{Binding ShowTagsInGraph, Mode=TwoWay}"/>
<CheckBox Grid.Row="7" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.ShowChildren}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowChildren, Mode=TwoWay}"/>
IsChecked="{Binding ShowChildren, Mode=TwoWay}"/>
<CheckBox Grid.Row="8" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}"
IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=Check4UpdatesOnStartup, Mode=TwoWay}"/>
IsChecked="{Binding Check4UpdatesOnStartup, Mode=TwoWay}"/>
</Grid>
</TabItem>
@ -257,12 +257,12 @@
<CheckBox Grid.Row="7" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.Appearance.UseFixedTabWidth}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseFixedTabWidth, Mode=TwoWay}"/>
IsChecked="{Binding UseFixedTabWidth, Mode=TwoWay}"/>
<CheckBox Grid.Row="8" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.Appearance.UseNativeWindowFrame}"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSystemWindowFrame, Mode=OneTime}"
IsChecked="{Binding UseSystemWindowFrame, Mode=OneTime}"
IsVisible="{OnPlatform False, Linux=True}"
IsCheckedChanged="OnUseNativeWindowFrameChanged"/>
</Grid>

View file

@ -204,7 +204,10 @@
<ToggleButton Grid.Row="0" Classes="group_expander" IsChecked="{Binding IsLocalBranchGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="16,*,Auto,Auto">
<Path Grid.Column="0" Width="11" Height="11" HorizontalAlignment="Left" Data="{StaticResource Icons.Local}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.LocalBranches}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0">
<Run Text="{DynamicResource Text.Repository.LocalBranches}"/>
<Run Text="{Binding LocalBranchesCount, StringFormat='({0})'}"/>
</TextBlock>
<Button Grid.Column="2"
Classes="icon_button"
Width="14"
@ -231,7 +234,10 @@
<ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="16,*,Auto,Auto">
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Data="{StaticResource Icons.Remotes}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Remotes}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0">
<Run Text="{DynamicResource Text.Repository.Remotes}"/>
<Run Text="{Binding Remotes, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<Button Grid.Column="2"
Classes="icon_button"
Width="14"
@ -256,16 +262,18 @@
<!-- Tags -->
<ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="16,Auto,*,Auto,Auto,Auto">
<Grid ColumnDefinitions="16,*,Auto,Auto,Auto">
<Path Grid.Column="0" Width="11" Height="11" Margin="2,1,0,0" HorizontalAlignment="Left" Data="{StaticResource Icons.Tags}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Tags}"/>
<TextBlock Grid.Column="2" Text="{Binding Tags, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<ToggleButton Grid.Column="3"
Classes="tag_display_mode"
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0">
<Run Text="{DynamicResource Text.Repository.Tags}"/>
<Run Text="{Binding Tags, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<ToggleButton Grid.Column="2"
Classes="show_as_tree"
Width="14"
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowTagsAsTree, Mode=TwoWay}"
IsChecked="{Binding ShowTagsAsTree, Mode=TwoWay}"
ToolTip.Tip="{DynamicResource Text.Repository.ShowTagsAsTree}"/>
<Button Grid.Column="4"
<Button Grid.Column="3"
Classes="icon_button"
Width="14"
Margin="8,0,0,0"
@ -273,7 +281,7 @@
ToolTip.Tip="{DynamicResource Text.Repository.Tags.Sort}">
<Path Width="12" Height="12" Margin="0,2,0,0" Data="{StaticResource Icons.Order}"/>
</Button>
<Button Grid.Column="5"
<Button Grid.Column="4"
Classes="icon_button"
Width="14"
Margin="8,0"
@ -288,8 +296,7 @@
Height="0"
Margin="8,0,4,0"
Background="Transparent"
ShowTagsAsTree="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowTagsAsTree, Mode=OneWay}"
Tags="{Binding VisibleTags}"
Content="{Binding VisibleTags}"
Focusable="False"
IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}"
SelectionChanged="OnTagsSelectionChanged"
@ -297,10 +304,18 @@
<!-- Submodules -->
<ToggleButton Grid.Row="6" Classes="group_expander" IsChecked="{Binding IsSubmoduleGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="16,Auto,*,Auto,Auto">
<Grid ColumnDefinitions="16,*,Auto,Auto,Auto">
<Path Grid.Column="0" Width="10" Height="10" Margin="2,0,0,0" HorizontalAlignment="Left" Data="{StaticResource Icons.Submodules}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Submodules}"/>
<TextBlock Grid.Column="2" Text="{Binding Submodules, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0">
<Run Text="{DynamicResource Text.Repository.Submodules}"/>
<Run Text="{Binding Submodules, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<ToggleButton Grid.Column="2"
Classes="show_as_tree"
Width="14"
IsChecked="{Binding ShowSubmodulesAsTree, Mode=TwoWay}"
IsVisible="{Binding Submodules, Converter={x:Static c:ListConverters.IsNotNullOrEmpty}}"
ToolTip.Tip="{DynamicResource Text.Repository.ShowSubmodulesAsTree}"/>
<Button Grid.Column="3"
Classes="icon_button"
Width="14"
@ -320,44 +335,24 @@
</Button>
</Grid>
</ToggleButton>
<ListBox Grid.Row="7"
x:Name="SubmoduleList"
Height="0"
Margin="12,0,4,0"
Classes="repo_left_content_list"
ItemsSource="{Binding VisibleSubmodules}"
SelectionMode="Single"
ContextRequested="OnSubmoduleContextRequested"
DoubleTapped="OnDoubleTappedSubmodule"
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Submodule">
<Grid ColumnDefinitions="Auto,*,8,8">
<Path Grid.Column="0" Width="10" Height="10" Margin="8,0" Data="{StaticResource Icons.Submodule}"/>
<TextBlock Grid.Column="1" Text="{Binding Path}" ClipToBounds="True" Classes="primary" TextTrimming="CharacterEllipsis"/>
<Path Grid.Column="2"
Width="8" Height="8"
Fill="Goldenrod"
Data="{StaticResource Icons.Modified}"
IsVisible="{Binding IsDirty}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<v:SubmodulesView Grid.Row="7"
x:Name="SubmoduleList"
Height="0"
Margin="8,0,4,0"
Content="{Binding VisibleSubmodules}"
RowsChanged="OnSubmodulesRowsChanged"
Focusable="False"
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}"/>
<!-- Worktrees -->
<ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
<Grid ColumnDefinitions="16,Auto,*,Auto,Auto">
<Grid ColumnDefinitions="16,*,Auto,Auto">
<Path Grid.Column="0" Width="11" Height="11" Margin="1,0,0,0" HorizontalAlignment="Left" Data="{StaticResource Icons.Worktrees}" Fill="{DynamicResource Brush.FG2}"/>
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Worktrees}"/>
<TextBlock Grid.Column="2" Text="{Binding Worktrees, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<Button Grid.Column="3"
<TextBlock Grid.Column="1" Classes="group_header_label" Margin="0">
<Run Text="{DynamicResource Text.Repository.Worktrees}"/>
<Run Text="{Binding Worktrees, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<Button Grid.Column="2"
Classes="icon_button"
Width="14"
Margin="8,0"
@ -366,7 +361,7 @@
ToolTip.Tip="{DynamicResource Text.Repository.Worktrees.Prune}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Loading}"/>
</Button>
<Button Grid.Column="4"
<Button Grid.Column="3"
Classes="icon_button"
Width="14"
Margin="0,0,9,0"
@ -385,7 +380,7 @@
SelectionMode="Single"
ContextRequested="OnWorktreeContextRequested"
DoubleTapped="OnDoubleTappedWorktree"
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
PropertyChanged="OnWorktreeListPropertyChanged"
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
@ -394,7 +389,7 @@
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Worktree">
<Grid ColumnDefinitions="Auto,*,22">
<Grid ColumnDefinitions="Auto,*,22" Background="Transparent">
<Path Grid.Column="0" Width="10" Height="10" Margin="8,0,0,0" Data="{StaticResource Icons.Worktree}"/>
<TextBlock Grid.Column="1" Classes="primary" Margin="8,0,0,0" TextTrimming="CharacterEllipsis">
<Run Text="{Binding Name}"/>

View file

@ -179,24 +179,9 @@ namespace SourceGit.Views
RemoteBranchTree.UnselectAll();
}
private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e)
private void OnSubmodulesRowsChanged(object _, RoutedEventArgs e)
{
if (sender is ListBox { SelectedItem: Models.Submodule submodule } grid && DataContext is ViewModels.Repository repo)
{
var menu = repo.CreateContextMenuForSubmodule(submodule.Path);
menu?.Open(grid);
}
e.Handled = true;
}
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e)
{
if (sender is ListBox { SelectedItem: Models.Submodule submodule } && DataContext is ViewModels.Repository repo)
{
repo.OpenSubmodule(submodule.Path);
}
UpdateLeftSidebarLayout();
e.Handled = true;
}
@ -221,7 +206,7 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnLeftSidebarListBoxPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
private void OnWorktreeListPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ListBox.ItemsSourceProperty || e.Property == ListBox.IsVisibleProperty)
UpdateLeftSidebarLayout();
@ -250,26 +235,26 @@ namespace SourceGit.Views
var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0;
var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0;
var desiredTag = vm.IsTagGroupExpanded ? 24.0 * TagsList.Rows : 0;
var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * vm.VisibleSubmodules.Count : 0;
var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * SubmoduleList.Rows : 0;
var desiredWorktree = vm.IsWorktreeGroupExpanded ? 24.0 * vm.Worktrees.Count : 0;
var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree;
var hasOverflow = (desiredBranches + desiredOthers > leftHeight);
if (vm.IsTagGroupExpanded)
if (vm.IsWorktreeGroupExpanded)
{
var height = desiredTag;
var height = desiredWorktree;
if (hasOverflow)
{
var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree;
var test = leftHeight - desiredBranches - desiredTag - desiredSubmodule;
if (test < 0)
height = Math.Min(200, height);
height = Math.Min(120, height);
else
height = Math.Max(200, test);
height = Math.Max(120, test);
}
leftHeight -= height;
TagsList.Height = height;
hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight;
WorktreeList.Height = height;
hasOverflow = (desiredBranches + desiredTag + desiredSubmodule) > leftHeight;
}
if (vm.IsSubmoduleGroupExpanded)
@ -277,32 +262,32 @@ namespace SourceGit.Views
var height = desiredSubmodule;
if (hasOverflow)
{
var test = leftHeight - desiredBranches - desiredWorktree;
var test = leftHeight - desiredBranches - desiredTag;
if (test < 0)
height = Math.Min(200, height);
height = Math.Min(120, height);
else
height = Math.Max(200, test);
height = Math.Max(120, test);
}
leftHeight -= height;
SubmoduleList.Height = height;
hasOverflow = (desiredBranches + desiredWorktree) > leftHeight;
hasOverflow = (desiredBranches + desiredTag) > leftHeight;
}
if (vm.IsWorktreeGroupExpanded)
if (vm.IsTagGroupExpanded)
{
var height = desiredWorktree;
var height = desiredTag;
if (hasOverflow)
{
var test = leftHeight - desiredBranches;
if (test < 0)
height = Math.Min(200, height);
height = Math.Min(120, height);
else
height = Math.Max(200, test);
height = Math.Max(120, test);
}
leftHeight -= height;
WorktreeList.Height = height;
TagsList.Height = height;
}
if (leftHeight > 0 && desiredBranches > leftHeight)

View file

@ -19,11 +19,13 @@
<!-- Left -->
<Grid Grid.Column="0" RowDefinitions="28,36,*,28,*">
<!-- Stash Bar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto">
<Path Grid.Column="0" Margin="8,0,0,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Stashes}"/>
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Stashes.Stashes}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding Stashes, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<Button Grid.Column="4"
<TextBlock Grid.Column="1" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0">
<Run Text="{DynamicResource Text.Stashes.Stashes}"/>
<Run Text="{Binding Stashes, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<Button Grid.Column="2"
Classes="icon_button"
Width="26" Height="14"
Padding="0"
@ -105,10 +107,12 @@
<!-- Changes Bar -->
<Border Grid.Row="3" BorderThickness="0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid ColumnDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Margin="8,0,0,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Changes}"/>
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Stashes.Changes}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding Changes, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
<TextBlock Grid.Column="1" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0">
<Run Text="{DynamicResource Text.Stashes.Changes}"/>
<Run Text="{Binding Changes, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
</Grid>
</Border>

View file

@ -0,0 +1,223 @@
<UserControl xmlns="https://github.com/avaloniaui"
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: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.SubmodulesView">
<UserControl.DataTemplates>
<DataTemplate DataType="vm:SubmoduleCollectionAsTree">
<ListBox Classes="repo_left_content_list" ItemsSource="{Binding Rows}" SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.DataTemplates>
<DataTemplate DataType="m:Submodule">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Submodule}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Path}"/>
</StackPanel>
<Grid RowDefinitions="24,24" ColumnDefinitions="Auto,Auto" Margin="0,8,0,0">
<TextBlock Grid.Row="0" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.CommitDetail.Info.SHA}"/>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Horizontal"
Margin="8,0,0,0">
<TextBlock Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
VerticalAlignment="Center"/>
<Path Margin="6,0,0,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Width="12" Height="12"
Data="{StaticResource Icons.Check}"
Fill="Green"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}"/>
<Border Height="16"
Margin="6,0,0,0" Padding="4,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Background="DarkOrange"
CornerRadius="4"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}">
<Grid>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.NotInited}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.NotInited}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Modified}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Modified}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.RevisionChanged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.RevisionChanged}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Unmerged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Unmerged}}"/>
</Grid>
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.URL}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Margin="8,0,0,0"
Text="{Binding URL}"
Foreground="{DynamicResource Brush.Link}"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:SubmoduleTreeNode">
<Border Height="24"
Background="Transparent"
DoubleTapped="OnItemDoubleTapped"
ContextRequested="OnItemContextRequested"
ToolTip.Tip="{Binding Module}"
ToolTip.Placement="Right">
<Grid ColumnDefinitions="16,Auto,*,Auto,Auto"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
VerticalAlignment="Center">
<v:SubmoduleTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{Binding IsExpanded, Mode=OneWay}"
IsVisible="{Binding IsFolder}"/>
<v:SubmoduleTreeNodeIcon Grid.Column="1"
IsExpanded="{Binding IsExpanded, Mode=OneWay}"/>
<TextBlock Grid.Column="2"
Classes="primary"
Margin="8,0,0,0"
TextTrimming="CharacterEllipsis">
<Run Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}"/>
<Run Text="{Binding ChildCounter}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
<Path Grid.Column="3"
Width="8" Height="8"
Margin="0,0,12,0"
Fill="Goldenrod"
Data="{StaticResource Icons.Modified}"
IsVisible="{Binding IsDirty}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
<DataTemplate DataType="vm:SubmoduleCollectionAsList">
<ListBox Classes="repo_left_content_list" ItemsSource="{Binding Submodules}" SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Submodule">
<Border Height="24"
Background="Transparent"
DoubleTapped="OnItemDoubleTapped"
ContextRequested="OnItemContextRequested"
ToolTip.Placement="Right">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Submodule}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Path}"/>
</StackPanel>
<Grid RowDefinitions="24,24" ColumnDefinitions="Auto,Auto" Margin="0,8,0,0">
<TextBlock Grid.Row="0" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.CommitDetail.Info.SHA}"/>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Horizontal"
Margin="8,0,0,0">
<TextBlock Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
VerticalAlignment="Center"/>
<Path Margin="6,0,0,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Width="12" Height="12"
Data="{StaticResource Icons.Check}"
Fill="Green"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}"/>
<Border Height="16"
Margin="6,0,0,0" Padding="4,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Background="DarkOrange"
CornerRadius="4"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}">
<Grid>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.NotInited}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.NotInited}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Modified}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Modified}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.RevisionChanged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.RevisionChanged}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Unmerged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Unmerged}}"/>
</Grid>
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.URL}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Margin="8,0,0,0"
Text="{Binding URL}"
Foreground="{DynamicResource Brush.Link}"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ToolTip.Tip>
<Grid ColumnDefinitions="16,*,Auto" Margin="8,0,0,0" VerticalAlignment="Center">
<Path Grid.Column="0" Width="10" Height="10" Margin="8,0" Data="{StaticResource Icons.Submodule}"/>
<TextBlock Grid.Column="1" Text="{Binding Path}" ClipToBounds="True" Classes="primary" TextTrimming="CharacterEllipsis"/>
<Path Grid.Column="2"
Width="8" Height="8"
Margin="8,0,12,0"
Fill="Goldenrod"
Data="{StaticResource Icons.Modified}"
IsVisible="{Binding IsDirty}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</UserControl.DataTemplates>
</UserControl>

View file

@ -0,0 +1,182 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public class SubmoduleTreeNodeToggleButton : ToggleButton
{
protected override Type StyleKeyOverride => typeof(ToggleButton);
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
DataContext is ViewModels.SubmoduleTreeNode { IsFolder: true } node)
{
var view = this.FindAncestorOfType<SubmodulesView>();
view?.ToggleNodeIsExpanded(node);
}
e.Handled = true;
}
}
public class SubmoduleTreeNodeIcon : UserControl
{
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<SubmoduleTreeNodeIcon, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
UpdateContent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
UpdateContent();
}
private void UpdateContent()
{
if (DataContext is not ViewModels.SubmoduleTreeNode node)
{
Content = null;
return;
}
if (node.Module != null)
CreateContent(new Thickness(0, 0, 0, 0), "Icons.Submodule");
else if (node.IsExpanded)
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open");
else
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder");
}
private void CreateContent(Thickness margin, string iconKey)
{
var geo = this.FindResource(iconKey) as StreamGeometry;
if (geo == null)
return;
Content = new Avalonia.Controls.Shapes.Path()
{
Width = 12,
Height = 12,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = margin,
Data = geo,
};
}
}
public partial class SubmodulesView : UserControl
{
public static readonly RoutedEvent<RoutedEventArgs> RowsChangedEvent =
RoutedEvent.Register<TagsView, RoutedEventArgs>(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> RowsChanged
{
add { AddHandler(RowsChangedEvent, value); }
remove { RemoveHandler(RowsChangedEvent, value); }
}
public int Rows
{
get;
private set;
}
public SubmodulesView()
{
InitializeComponent();
}
public void ToggleNodeIsExpanded(ViewModels.SubmoduleTreeNode node)
{
if (Content is ViewModels.SubmoduleCollectionAsTree tree)
{
tree.ToggleExpand(node);
Rows = tree.Rows.Count;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ContentProperty)
{
if (Content is ViewModels.SubmoduleCollectionAsTree tree)
Rows = tree.Rows.Count;
else if (Content is ViewModels.SubmoduleCollectionAsList list)
Rows = list.Submodules.Count;
else
Rows = 0;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
else if (change.Property == IsVisibleProperty)
{
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
private void OnItemDoubleTapped(object sender, TappedEventArgs e)
{
if (sender is Control control && DataContext is ViewModels.Repository repo)
{
if (control.DataContext is ViewModels.SubmoduleTreeNode node)
{
if (node.IsFolder)
ToggleNodeIsExpanded(node);
else if (node.Module.Status != Models.SubmoduleStatus.NotInited)
repo.OpenSubmodule(node.Module.Path);
}
else if (control.DataContext is Models.Submodule m && m.Status != Models.SubmoduleStatus.NotInited)
{
repo.OpenSubmodule(m.Path);
}
}
e.Handled = true;
}
private void OnItemContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is Control control && DataContext is ViewModels.Repository repo)
{
if (control.DataContext is ViewModels.SubmoduleTreeNode node && node.Module != null)
{
var menu = repo.CreateContextMenuForSubmodule(node.Module);
menu?.Open(control);
}
else if (control.DataContext is Models.Submodule m)
{
var menu = repo.CreateContextMenuForSubmodule(m);
menu?.Open(control);
}
}
e.Handled = true;
}
}
}

View file

@ -23,14 +23,36 @@
<ListBox Classes="repo_left_content_list"
ItemsSource="{Binding Rows}"
SelectionMode="Single"
SelectionChanged="OnRowSelectionChanged">
SelectionChanged="OnSelectionChanged">
<ListBox.DataTemplates>
<DataTemplate DataType="vm:TagTreeNodeToolTip">
<StackPanel Orientation="Vertical" Spacing="6">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Tag}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Name}"/>
<Border Background="Green" Margin="4,0,0,0" CornerRadius="4" IsVisible="{Binding IsAnnotated}">
<TextBlock Text="{DynamicResource Text.CreateTag.Type.Annotated}" Classes="primary" Margin="4,0" Foreground="White"/>
</Border>
</StackPanel>
<TextBlock Text="{Binding Message}" IsVisible="{Binding Message, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:TagTreeNode">
<Border Height="24" Background="Transparent" PointerPressed="OnRowPointerPressed" DoubleTapped="OnDoubleTappedNode" ContextRequested="OnRowContextRequested">
<Border Height="24"
Background="Transparent"
PointerPressed="OnItemPointerPressed"
DoubleTapped="OnItemDoubleTapped"
ContextRequested="OnItemContextRequested"
ToolTip.Tip="{Binding ToolTip}"
ToolTip.Placement="Right">
<Grid ColumnDefinitions="16,Auto,*,Auto"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
VerticalAlignment="Center"
ToolTip.Tip="{Binding ToolTip}">
VerticalAlignment="Center">
<v:TagTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
@ -38,14 +60,14 @@
IsChecked="{Binding IsExpanded, Mode=OneWay}"
IsVisible="{Binding IsFolder}"/>
<v:TagTreeNodeIcon Grid.Column="1"
Node="{Binding .}"
IsExpanded="{Binding IsExpanded, Mode=OneWay}"/>
<v:TagTreeNodeIcon Grid.Column="1" IsExpanded="{Binding IsExpanded, Mode=OneWay}"/>
<TextBlock Grid.Column="2"
Classes="primary"
Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}"
Margin="8,0,0,0"/>
Margin="8,0,0,0">
<Run Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}"/>
<Run Text="{Binding TagsCount}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
<ContentControl Grid.Column="3" Content="{Binding Tag}">
<ContentControl.DataTemplates>
@ -66,11 +88,29 @@
Margin="8,0,0,0"
ItemsSource="{Binding Tags}"
SelectionMode="Single"
SelectionChanged="OnRowSelectionChanged">
SelectionChanged="OnSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Tag">
<Border Height="24" Background="Transparent" PointerPressed="OnRowPointerPressed" ContextRequested="OnRowContextRequested">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center" ToolTip.Tip="{Binding Message}">
<Border Height="24"
Background="Transparent"
PointerPressed="OnItemPointerPressed"
ContextRequested="OnItemContextRequested"
ToolTip.Placement="Right">
<ToolTip.Tip>
<StackPanel Orientation="Vertical" Spacing="6">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Tag}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Name}"/>
<Border Background="Green" Margin="4,0,0,0" CornerRadius="4" IsVisible="{Binding IsAnnotated}">
<TextBlock Text="{DynamicResource Text.CreateTag.Type.Annotated}" Classes="primary" Margin="4,0" Foreground="White"/>
</Border>
</StackPanel>
<TextBlock Text="{Binding Message}" IsVisible="{Binding Message, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ToolTip.Tip>
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Path Grid.Column="0"
Margin="8,0,0,0"
Width="12" Height="12"

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
@ -31,15 +30,6 @@ namespace SourceGit.Views
public class TagTreeNodeIcon : UserControl
{
public static readonly StyledProperty<ViewModels.TagTreeNode> NodeProperty =
AvaloniaProperty.Register<TagTreeNodeIcon, ViewModels.TagTreeNode>(nameof(Node));
public ViewModels.TagTreeNode Node
{
get => GetValue(NodeProperty);
set => SetValue(NodeProperty, value);
}
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<TagTreeNodeIcon, bool>(nameof(IsExpanded));
@ -49,16 +39,23 @@ namespace SourceGit.Views
set => SetValue(IsExpandedProperty, value);
}
static TagTreeNodeIcon()
protected override void OnDataContextChanged(EventArgs e)
{
NodeProperty.Changed.AddClassHandler<TagTreeNodeIcon>((icon, _) => icon.UpdateContent());
IsExpandedProperty.Changed.AddClassHandler<TagTreeNodeIcon>((icon, _) => icon.UpdateContent());
base.OnDataContextChanged(e);
UpdateContent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
UpdateContent();
}
private void UpdateContent()
{
var node = Node;
if (node == null)
if (DataContext is not ViewModels.TagTreeNode node)
{
Content = null;
return;
@ -92,24 +89,6 @@ namespace SourceGit.Views
public partial class TagsView : UserControl
{
public static readonly StyledProperty<bool> ShowTagsAsTreeProperty =
AvaloniaProperty.Register<TagsView, bool>(nameof(ShowTagsAsTree));
public bool ShowTagsAsTree
{
get => GetValue(ShowTagsAsTreeProperty);
set => SetValue(ShowTagsAsTreeProperty, value);
}
public static readonly StyledProperty<List<Models.Tag>> TagsProperty =
AvaloniaProperty.Register<TagsView, List<Models.Tag>>(nameof(Tags));
public List<Models.Tag> Tags
{
get => GetValue(TagsProperty);
set => SetValue(TagsProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<TagsView, RoutedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
@ -150,33 +129,7 @@ namespace SourceGit.Views
{
if (Content is ViewModels.TagCollectionAsTree tree)
{
node.IsExpanded = !node.IsExpanded;
var depth = node.Depth;
var idx = tree.Rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{
var subrows = new List<ViewModels.TagTreeNode>();
MakeTreeRows(subrows, node.Children);
tree.Rows.InsertRange(idx + 1, subrows);
}
else
{
var removeCount = 0;
for (int i = idx + 1; i < tree.Rows.Count; i++)
{
var row = tree.Rows[i];
if (row.Depth <= depth)
break;
removeCount++;
}
tree.Rows.RemoveRange(idx + 1, removeCount);
}
tree.ToggleExpand(node);
Rows = tree.Rows.Count;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
@ -186,9 +139,15 @@ namespace SourceGit.Views
{
base.OnPropertyChanged(change);
if (change.Property == ShowTagsAsTreeProperty || change.Property == TagsProperty)
if (change.Property == ContentProperty)
{
UpdateDataSource();
if (Content is ViewModels.TagCollectionAsTree tree)
Rows = tree.Rows.Count;
else if (Content is ViewModels.TagCollectionAsList list)
Rows = list.Tags.Count;
else
Rows = 0;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
else if (change.Property == IsVisibleProperty)
@ -197,7 +156,7 @@ namespace SourceGit.Views
}
}
private void OnDoubleTappedNode(object sender, TappedEventArgs e)
private void OnItemDoubleTapped(object sender, TappedEventArgs e)
{
if (sender is Control { DataContext: ViewModels.TagTreeNode { IsFolder: true } node })
ToggleNodeIsExpanded(node);
@ -205,7 +164,7 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnRowPointerPressed(object sender, PointerPressedEventArgs e)
private void OnItemPointerPressed(object sender, PointerPressedEventArgs e)
{
var p = e.GetCurrentPoint(this);
if (!p.Properties.IsLeftButtonPressed)
@ -220,7 +179,7 @@ namespace SourceGit.Views
repo.NavigateToCommit(nodeTag.SHA);
}
private void OnRowContextRequested(object sender, ContextRequestedEventArgs e)
private void OnItemContextRequested(object sender, ContextRequestedEventArgs e)
{
var control = sender as Control;
if (control == null)
@ -243,7 +202,7 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _)
private void OnSelectionChanged(object sender, SelectionChangedEventArgs _)
{
var selected = (sender as ListBox)?.SelectedItem;
var selectedTag = null as Models.Tag;
@ -255,63 +214,6 @@ namespace SourceGit.Views
if (selectedTag != null)
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void MakeTreeRows(List<ViewModels.TagTreeNode> rows, List<ViewModels.TagTreeNode> nodes)
{
foreach (var node in nodes)
{
rows.Add(node);
if (!node.IsExpanded || !node.IsFolder)
continue;
MakeTreeRows(rows, node.Children);
}
}
private void UpdateDataSource()
{
var tags = Tags;
if (tags == null || tags.Count == 0)
{
Rows = 0;
Content = null;
return;
}
if (ShowTagsAsTree)
{
var oldExpanded = new HashSet<string>();
if (Content is ViewModels.TagCollectionAsTree oldTree)
{
foreach (var row in oldTree.Rows)
{
if (row.IsFolder && row.IsExpanded)
oldExpanded.Add(row.FullPath);
}
}
var tree = new ViewModels.TagCollectionAsTree();
tree.Tree = ViewModels.TagTreeNode.Build(tags, oldExpanded);
var rows = new List<ViewModels.TagTreeNode>();
MakeTreeRows(rows, tree.Tree);
tree.Rows.AddRange(rows);
Content = tree;
Rows = rows.Count;
}
else
{
var list = new ViewModels.TagCollectionAsList();
list.Tags.AddRange(tags);
Content = list;
Rows = tags.Count;
}
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
}

View file

@ -61,13 +61,15 @@
<Grid Grid.Row="1" RowDefinitions="28,*">
<!-- Unstaged Toolbar -->
<Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto,Auto,Auto,Auto,Auto">
<Path Grid.Column="0" Margin="8,0,0,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Changes}"/>
<TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Unstaged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Unstaged, Converter={x:Static c:ListConverters.ToCount}}"/>
<v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsStaging}"/>
<TextBlock Grid.Column="1" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0">
<Run Text="{DynamicResource Text.WorkingCopy.Unstaged}"/>
<Run Text="{Binding Unstaged, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<v:LoadingIcon Grid.Column="2" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsStaging}"/>
<Button Grid.Column="5"
<Button Grid.Column="4"
Classes="icon_button"
Width="26" Height="14"
Padding="0"
@ -75,12 +77,12 @@
Command="{Binding OpenAssumeUnchanged}">
<Path Width="14" Height="14" Data="{StaticResource Icons.File.Ignore}"/>
</Button>
<ToggleButton Grid.Column="6"
<ToggleButton Grid.Column="5"
Classes="toggle_untracked"
Width="26" Height="14"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.IncludeUntracked}"
IsChecked="{Binding IncludeUntracked, Mode=TwoWay}"/>
<Button Grid.Column="7"
<Button Grid.Column="6"
Classes="icon_button"
Width="26" Height="14"
Padding="0"
@ -89,7 +91,7 @@
Command="{Binding OpenExternalMergeToolAllConflicts}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Conflict}"/>
</Button>
<Button Grid.Column="8"
<Button Grid.Column="7"
Classes="icon_button"
Width="26" Height="14"
Padding="0"
@ -102,7 +104,7 @@
</ToolTip.Tip>
<Path Width="14" Height="14" Margin="0,6,0,0" Data="{StaticResource Icons.Down}"/>
</Button>
<Button Grid.Column="9"
<Button Grid.Column="8"
Classes="icon_button"
Width="26" Height="14"
Padding="0"
@ -110,7 +112,7 @@
Command="{Binding StageAll}">
<Path Width="14" Height="14" Data="{StaticResource Icons.DoubleDown}"/>
</Button>
<v:ChangeViewModeSwitcher Grid.Column="10"
<v:ChangeViewModeSwitcher Grid.Column="9"
Width="26" Height="14"
Margin="0,1,0,0"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode, Mode=TwoWay}"/>
@ -142,12 +144,14 @@
<Grid Grid.Row="3" RowDefinitions="28,*">
<!-- Staged Toolbar -->
<Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto,Auto">
<Path Grid.Column="0" Margin="8,0,0,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Changes}"/>
<TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Staged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/>
<v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/>
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Click="OnUnstageSelectedButtonClicked">
<TextBlock Grid.Column="1" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="4,0,0,0">
<Run Text="{DynamicResource Text.WorkingCopy.Staged}"/>
<Run Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/>
</TextBlock>
<v:LoadingIcon Grid.Column="2" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/>
<Button Grid.Column="4" Classes="icon_button" Width="26" Height="14" Padding="0" Click="OnUnstageSelectedButtonClicked">
<ToolTip.Tip>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.WorkingCopy.Staged.Unstage}" VerticalAlignment="Center"/>
@ -156,10 +160,10 @@
</ToolTip.Tip>
<Path Width="14" Height="14" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
</Button>
<Button Grid.Column="6" Classes="icon_button" Width="26" Height="14" Padding="0" ToolTip.Tip="{DynamicResource Text.WorkingCopy.Staged.UnstageAll}" Command="{Binding UnstageAll}">
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" ToolTip.Tip="{DynamicResource Text.WorkingCopy.Staged.UnstageAll}" Command="{Binding UnstageAll}">
<Path Width="14" Height="14" Data="{StaticResource Icons.DoubleUp}"/>
</Button>
<v:ChangeViewModeSwitcher Grid.Column="7"
<v:ChangeViewModeSwitcher Grid.Column="6"
Width="26" Height="14"
Margin="0,1,0,0"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=StagedChangeViewMode, Mode=TwoWay}"/>

View file

@ -0,0 +1,111 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.WorkspaceSwitcher"
x:DataType="vm:WorkspaceSwitcher">
<Grid RowDefinitions="Auto,Auto,Auto">
<TextBlock Grid.Row="0"
Text="{DynamicResource Text.Launcher.Workspaces}"
FontWeight="Bold"
HorizontalAlignment="Center"/>
<TextBox Grid.Row="1"
Height="24"
Margin="4,8,4,0"
BorderThickness="1"
CornerRadius="12"
Text="{Binding SearchFilter, Mode=TwoWay}"
KeyDown="OnSearchBoxKeyDown"
BorderBrush="{DynamicResource Brush.Border2}"
VerticalContentAlignment="Center"
v:AutoFocusBehaviour.IsEnabled="True">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14"
Margin="6,0,0,0"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
Width="16"
Margin="0,0,6,0"
Command="{Binding ClearFilter}"
IsVisible="{Binding SearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Right">
<Path Width="14" Height="14"
Margin="0,1,0,0"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<ListBox Grid.Row="2"
x:Name="WorkspaceListBox"
Width="300"
MaxHeight="400"
Margin="4,8,4,0"
BorderThickness="0"
SelectionMode="Single"
Background="Transparent"
Focusable="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding VisibleWorkspaces, Mode=OneWay}"
SelectedItem="{Binding SelectedWorkspace, Mode=TwoWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="8,0"/>
<Setter Property="MinHeight" Value="26"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="ListBox">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Grid/>
</FocusAdornerTemplate>
</Setter>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:Workspace">
<Grid ColumnDefinitions="Auto,*" Background="Transparent" DoubleTapped="OnItemDoubleTapped">
<Path Grid.Column="0"
Width="12" Height="12"
Fill="{Binding Brush}"
Data="{StaticResource Icons.Workspace}"
IsVisible="{Binding !IsActive}"
IsHitTestVisible="False"/>
<Path Grid.Column="0"
Width="12" Height="12"
Fill="{Binding Brush}"
Data="{StaticResource Icons.Check}"
IsVisible="{Binding IsActive}"
IsHitTestVisible="False"/>
<TextBlock Grid.Column="1"
Margin="8,0,0,0"
Classes="primary"
VerticalAlignment="Center"
Text="{Binding Name}"
IsHitTestVisible="False"
TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View file

@ -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;
}
}
}
}