sourcegit/src/ViewModels/Preference.cs

615 lines
18 KiB
C#
Raw Normal View History

2024-03-20 00:36:10 -07:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Avalonia.Collections;
2024-03-20 00:36:10 -07:00
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Preference : ObservableObject
{
[JsonIgnore]
public static Preference Instance
{
get
{
if (_instance != null)
return _instance;
_isLoading = true;
_instance = Load();
_isLoading = false;
2024-03-20 00:36:10 -07:00
_instance.PrepareGit();
_instance.PrepareShellOrTerminal();
_instance.PrepareWorkspaces();
2024-03-20 00:36:10 -07:00
return _instance;
}
}
public string Locale
{
get => _locale;
set
{
if (SetProperty(ref _locale, value) && !_isLoading)
2024-03-20 00:36:10 -07:00
App.SetLocale(value);
}
}
public string Theme
{
get => _theme;
set
{
if (SetProperty(ref _theme, value) && !_isLoading)
App.SetTheme(_theme, _themeOverrides);
}
}
public string ThemeOverrides
{
get => _themeOverrides;
set
{
if (SetProperty(ref _themeOverrides, value) && !_isLoading)
App.SetTheme(_theme, value);
2024-03-20 00:36:10 -07:00
}
}
public string DefaultFontFamily
2024-03-21 03:02:06 -07:00
{
get => _defaultFontFamily;
2024-08-19 20:53:37 -07:00
set
{
if (SetProperty(ref _defaultFontFamily, value) && !_isLoading)
App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor);
2024-07-29 19:29:07 -07:00
}
2024-03-21 03:02:06 -07:00
}
public string MonospaceFontFamily
2024-03-21 03:02:06 -07:00
{
get => _monospaceFontFamily;
2024-07-29 19:29:07 -07:00
set
{
if (SetProperty(ref _monospaceFontFamily, value) && !_isLoading)
App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor);
2024-07-29 19:29:07 -07:00
}
2024-03-21 03:02:06 -07:00
}
public bool OnlyUseMonoFontInEditor
{
get => _onlyUseMonoFontInEditor;
set
{
if (SetProperty(ref _onlyUseMonoFontInEditor, value) && !_isLoading)
App.SetFonts(_defaultFontFamily, _monospaceFontFamily, _onlyUseMonoFontInEditor);
}
}
public bool UseSystemWindowFrame
{
get => _useSystemWindowFrame;
set => SetProperty(ref _useSystemWindowFrame, value);
}
2024-03-21 21:03:04 -07:00
public double DefaultFontSize
{
get => _defaultFontSize;
set => SetProperty(ref _defaultFontSize, value);
}
public LayoutInfo Layout
{
get => _layout;
set => SetProperty(ref _layout, value);
}
2024-03-20 00:36:10 -07:00
public int MaxHistoryCommits
{
get => _maxHistoryCommits;
set => SetProperty(ref _maxHistoryCommits, value);
}
public int SubjectGuideLength
{
get => _subjectGuideLength;
set => SetProperty(ref _subjectGuideLength, value);
}
2024-03-20 00:36:10 -07:00
public bool UseFixedTabWidth
{
get => _useFixedTabWidth;
set => SetProperty(ref _useFixedTabWidth, value);
}
public bool Check4UpdatesOnStartup
{
get => _check4UpdatesOnStartup;
set => SetProperty(ref _check4UpdatesOnStartup, value);
}
public bool ShowAuthorTimeInGraph
{
get => _showAuthorTimeInGraph;
set => SetProperty(ref _showAuthorTimeInGraph, value);
}
public string IgnoreUpdateTag
{
get => _ignoreUpdateTag;
set => SetProperty(ref _ignoreUpdateTag, value);
}
public bool ShowTagsAsTree
{
get => _showTagsAsTree;
set => SetProperty(ref _showTagsAsTree, value);
}
2024-03-20 00:36:10 -07:00
public bool UseTwoColumnsLayoutInHistories
{
get => _useTwoColumnsLayoutInHistories;
set => SetProperty(ref _useTwoColumnsLayoutInHistories, value);
}
public bool DisplayTimeAsPeriodInHistories
{
get => _displayTimeAsPeriodInHistories;
set => SetProperty(ref _displayTimeAsPeriodInHistories, value);
}
2024-03-20 00:36:10 -07:00
public bool UseSideBySideDiff
{
get => _useSideBySideDiff;
set => SetProperty(ref _useSideBySideDiff, value);
}
public bool UseSyntaxHighlighting
{
get => _useSyntaxHighlighting;
set => SetProperty(ref _useSyntaxHighlighting, value);
}
public bool EnableDiffViewWordWrap
{
get => _enableDiffViewWordWrap;
set => SetProperty(ref _enableDiffViewWordWrap, value);
}
public bool ShowHiddenSymbolsInDiffView
{
get => _showHiddenSymbolsInDiffView;
set => SetProperty(ref _showHiddenSymbolsInDiffView, value);
}
feature: diff - toggle show all lines (#615) (#652) * Renamed 1 of 2 SyncScrollOffset props, for clarity The property "SyncScrollOffset" in TextDiff is distinct from the one with the same name in TwoSideTextDiff. These two properties are used in separate (though slightly related) ways and are not really connected. The one in TwoSideTextDiff is mainly used to keep the scroll-pos of the two SingleSideTextDiffPresenter views in sync (aligned), while the one in TextDiff is used only to preserve/reset the scroll-pos in the single CombinedTextDiffPresenter view when (re)loading Diff Content (so not really syncing anything). To clarify this and to make the two properties more distinguishable, I renamed the one in TextDiff to simply "ScrollOffset". * Added icon and string for "Show All Lines" New StreamGeometry "Icons.Lines.All" using SVG path from "text_line_spacing_regular" at https://avaloniaui.github.io/icons.html. New String "Text.Diff.VisualLines.All" for en_US locale (no translations yet). * Implemented new TextDiff feature "Show All Lines" (toggle) * Added new ToggleButton in DiffView toolbar, visible when IsTextDiff, disabling the buttons "Increase/Decrease Number of Visible Lines" when on. * Added new Preference property "UseFullTextDiff". * Added StyledProperty "UseFullTextDiffProperty" in TextDiffView, with a DataTemplate binding to the corresponding preference property. * When changed, UseFullTextDiffProperty is handled identically as UseSideBySideDiffProperty (via new helper method RefreshContent(), for unification with OnDataContextChanged()). * Added new method DiffContext.ToggleFullTextDiff() for changing the preference property and reloading the diff content. * Implemented the new feature by overriding the "unified" (number of context lines) for Commands.Diff() with a very high number. NOTE: The number used (~1 billion) is supposed to be the highest one working on Mac, according to this forum comment: https://stackoverflow.com/questions/28727424/for-git-diff-is-there-a-uinfinity-option-to-show-the-whole-file#comment135202820_28846576
2024-11-04 00:32:51 -08:00
public bool UseFullTextDiff
{
get => _useFullTextDiff;
set => SetProperty(ref _useFullTextDiff, value);
}
2024-03-20 00:36:10 -07:00
public Models.ChangeViewMode UnstagedChangeViewMode
{
get => _unstagedChangeViewMode;
set => SetProperty(ref _unstagedChangeViewMode, value);
}
public Models.ChangeViewMode StagedChangeViewMode
{
get => _stagedChangeViewMode;
set => SetProperty(ref _stagedChangeViewMode, value);
}
public Models.ChangeViewMode CommitChangeViewMode
{
get => _commitChangeViewMode;
set => SetProperty(ref _commitChangeViewMode, value);
}
public string GitInstallPath
{
get => Native.OS.GitExecutable;
2024-03-20 00:36:10 -07:00
set
{
if (Native.OS.GitExecutable != value)
2024-03-20 00:36:10 -07:00
{
Native.OS.GitExecutable = value;
2024-07-14 09:30:31 -07:00
OnPropertyChanged();
2024-03-20 00:36:10 -07:00
}
}
}
public string GitDefaultCloneDir
{
get => _gitDefaultCloneDir;
set => SetProperty(ref _gitDefaultCloneDir, value);
}
public int ShellOrTerminal
{
get => _shellOrTerminal;
set
{
if (SetProperty(ref _shellOrTerminal, value))
{
if (value >= 0 && value < Models.ShellOrTerminal.Supported.Count)
Native.OS.SetShellOrTerminal(Models.ShellOrTerminal.Supported[value]);
else
Native.OS.SetShellOrTerminal(null);
OnPropertyChanged(nameof(ShellOrTerminalPath));
}
}
}
public string ShellOrTerminalPath
{
get => Native.OS.ShellOrTerminal;
set
{
if (value != Native.OS.ShellOrTerminal)
{
Native.OS.ShellOrTerminal = value;
OnPropertyChanged();
}
}
}
2024-03-20 00:36:10 -07:00
public int ExternalMergeToolType
{
get => _externalMergeToolType;
set
{
var changed = SetProperty(ref _externalMergeToolType, value);
if (changed && !OperatingSystem.IsWindows() && value > 0 && value < Models.ExternalMerger.Supported.Count)
2024-03-20 00:36:10 -07:00
{
var tool = Models.ExternalMerger.Supported[value];
if (File.Exists(tool.Exec))
ExternalMergeToolPath = tool.Exec;
else
ExternalMergeToolPath = string.Empty;
2024-03-20 00:36:10 -07:00
}
}
}
public string ExternalMergeToolPath
{
get => _externalMergeToolPath;
set => SetProperty(ref _externalMergeToolPath, value);
}
public uint StatisticsSampleColor
{
get => _statisticsSampleColor;
set => SetProperty(ref _statisticsSampleColor, value);
}
public List<RepositoryNode> RepositoryNodes
2024-03-20 00:36:10 -07:00
{
get;
set;
} = [];
2024-03-20 00:36:10 -07:00
2024-09-09 03:26:43 -07:00
public List<Workspace> Workspaces
2024-03-20 00:36:10 -07:00
{
get;
set;
} = [];
2024-03-20 00:36:10 -07:00
public AvaloniaList<Models.OpenAIService> OpenAIServices
{
get;
set;
} = [];
public double LastCheckUpdateTime
{
get => _lastCheckUpdateTime;
set => SetProperty(ref _lastCheckUpdateTime, value);
}
public bool IsGitConfigured()
{
var path = GitInstallPath;
return !string.IsNullOrEmpty(path) && File.Exists(path);
}
public bool ShouldCheck4UpdateOnStartup()
{
if (!_check4UpdatesOnStartup)
return false;
var lastCheck = DateTime.UnixEpoch.AddSeconds(LastCheckUpdateTime).ToLocalTime();
var now = DateTime.Now;
if (lastCheck.Year == now.Year && lastCheck.Month == now.Month && lastCheck.Day == now.Day)
return false;
LastCheckUpdateTime = now.Subtract(DateTime.UnixEpoch.ToLocalTime()).TotalSeconds;
return true;
}
2024-09-09 03:26:43 -07:00
public Workspace GetActiveWorkspace()
{
foreach (var w in Workspaces)
{
if (w.IsActive)
return w;
}
var first = Workspaces[0];
first.IsActive = true;
return first;
}
public void AddNode(RepositoryNode node, RepositoryNode to, bool save)
2024-03-20 00:36:10 -07:00
{
var collection = to == null ? RepositoryNodes : to.SubNodes;
collection.Add(node);
collection.Sort((l, r) =>
2024-03-20 00:36:10 -07:00
{
if (l.IsRepository != r.IsRepository)
return l.IsRepository ? 1 : -1;
2024-07-14 00:55:15 -07:00
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
2024-03-20 00:36:10 -07:00
});
if (save)
Save();
2024-03-20 00:36:10 -07:00
}
public RepositoryNode FindNode(string id)
2024-03-20 00:36:10 -07:00
{
return FindNodeRecursive(id, RepositoryNodes);
2024-03-20 00:36:10 -07:00
}
public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode)
{
var node = FindNodeRecursive(repo, RepositoryNodes);
if (node == null)
{
node = new RepositoryNode()
{
Id = repo,
Name = Path.GetFileName(repo),
Bookmark = 0,
IsRepository = true,
};
AddNode(node, parent, true);
}
else if (shouldMoveNode)
{
MoveNode(node, parent, true);
}
return node;
}
public void MoveNode(RepositoryNode node, RepositoryNode to, bool save)
2024-03-20 00:36:10 -07:00
{
if (to == null && RepositoryNodes.Contains(node))
return;
if (to != null && to.SubNodes.Contains(node))
return;
2024-03-20 00:36:10 -07:00
RemoveNode(node, false);
AddNode(node, to, false);
if (save)
Save();
2024-03-20 00:36:10 -07:00
}
public void RemoveNode(RepositoryNode node, bool save)
2024-03-20 00:36:10 -07:00
{
RemoveNodeRecursive(node, RepositoryNodes);
if (save)
Save();
2024-03-20 00:36:10 -07:00
}
public void SortByRenamedNode(RepositoryNode node)
{
var container = FindNodeContainer(node, RepositoryNodes);
container?.Sort((l, r) =>
{
if (l.IsRepository != r.IsRepository)
return l.IsRepository ? 1 : -1;
2024-07-14 00:55:15 -07:00
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
});
Save();
}
public void AutoRemoveInvalidNode()
{
var changed = RemoveInvalidRepositoriesRecursive(RepositoryNodes);
if (changed)
Save();
}
public void Save()
2024-03-20 00:36:10 -07:00
{
if (_isLoading)
return;
2024-09-09 03:47:53 -07:00
var file = Path.Combine(Native.OS.DataDir, "preference.json");
var data = JsonSerializer.Serialize(this, JsonCodeGen.Default.Preference);
2024-09-09 03:47:53 -07:00
File.WriteAllText(file, data);
2024-03-20 00:36:10 -07:00
}
private static Preference Load()
{
var path = Path.Combine(Native.OS.DataDir, "preference.json");
if (!File.Exists(path))
return new Preference();
try
{
return JsonSerializer.Deserialize(File.ReadAllText(path), JsonCodeGen.Default.Preference);
}
catch
{
return new Preference();
}
}
private void PrepareGit()
{
var path = Native.OS.GitExecutable;
if (string.IsNullOrEmpty(path) || !File.Exists(path))
GitInstallPath = Native.OS.FindGitExecutable();
}
private void PrepareShellOrTerminal()
{
if (_shellOrTerminal >= 0)
return;
for (int i = 0; i < Models.ShellOrTerminal.Supported.Count; i++)
{
var shell = Models.ShellOrTerminal.Supported[i];
if (Native.OS.TestShellOrTerminal(shell))
{
ShellOrTerminal = i;
break;
}
}
}
private void PrepareWorkspaces()
{
if (Workspaces.Count == 0)
{
Workspaces.Add(new Workspace() { Name = "Default" });
return;
}
foreach (var workspace in Workspaces)
{
if (!workspace.RestoreOnStartup)
{
workspace.Repositories.Clear();
workspace.ActiveIdx = 0;
}
}
}
private RepositoryNode FindNodeRecursive(string id, List<RepositoryNode> collection)
2024-03-20 00:36:10 -07:00
{
foreach (var node in collection)
{
if (node.Id == id)
return node;
2024-03-20 00:36:10 -07:00
var sub = FindNodeRecursive(id, node.SubNodes);
if (sub != null)
return sub;
2024-03-20 00:36:10 -07:00
}
return null;
}
private List<RepositoryNode> FindNodeContainer(RepositoryNode node, List<RepositoryNode> collection)
{
foreach (var sub in collection)
{
if (node == sub)
return collection;
var subCollection = FindNodeContainer(node, sub.SubNodes);
if (subCollection != null)
return subCollection;
}
return null;
}
private bool RemoveNodeRecursive(RepositoryNode node, List<RepositoryNode> collection)
2024-03-20 00:36:10 -07:00
{
if (collection.Contains(node))
{
collection.Remove(node);
return true;
}
foreach (var one in collection)
2024-03-20 00:36:10 -07:00
{
if (RemoveNodeRecursive(node, one.SubNodes))
return true;
2024-03-20 00:36:10 -07:00
}
return false;
}
private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
{
bool changed = false;
for (int i = collection.Count - 1; i >= 0; i--)
{
var node = collection[i];
if (node.IsInvalid)
{
collection.RemoveAt(i);
changed = true;
}
else if (!node.IsRepository)
{
changed |= RemoveInvalidRepositoriesRecursive(node.SubNodes);
}
}
return changed;
}
2024-03-20 00:36:10 -07:00
private static Preference _instance = null;
private static bool _isLoading = false;
2024-03-20 00:36:10 -07:00
private string _locale = "en_US";
private string _theme = "Default";
private string _themeOverrides = string.Empty;
private string _defaultFontFamily = string.Empty;
private string _monospaceFontFamily = string.Empty;
private bool _onlyUseMonoFontInEditor = false;
private bool _useSystemWindowFrame = false;
2024-03-21 21:03:04 -07:00
private double _defaultFontSize = 13;
private LayoutInfo _layout = new LayoutInfo();
2024-03-21 03:02:06 -07:00
2024-03-20 00:36:10 -07:00
private int _maxHistoryCommits = 20000;
private int _subjectGuideLength = 50;
2024-03-20 00:36:10 -07:00
private bool _useFixedTabWidth = true;
private bool _showAuthorTimeInGraph = false;
private bool _check4UpdatesOnStartup = true;
private double _lastCheckUpdateTime = 0;
private string _ignoreUpdateTag = string.Empty;
private bool _showTagsAsTree = false;
2024-03-20 00:36:10 -07:00
private bool _useTwoColumnsLayoutInHistories = false;
private bool _displayTimeAsPeriodInHistories = false;
2024-03-20 00:36:10 -07:00
private bool _useSideBySideDiff = false;
private bool _useSyntaxHighlighting = false;
private bool _enableDiffViewWordWrap = false;
private bool _showHiddenSymbolsInDiffView = false;
feature: diff - toggle show all lines (#615) (#652) * Renamed 1 of 2 SyncScrollOffset props, for clarity The property "SyncScrollOffset" in TextDiff is distinct from the one with the same name in TwoSideTextDiff. These two properties are used in separate (though slightly related) ways and are not really connected. The one in TwoSideTextDiff is mainly used to keep the scroll-pos of the two SingleSideTextDiffPresenter views in sync (aligned), while the one in TextDiff is used only to preserve/reset the scroll-pos in the single CombinedTextDiffPresenter view when (re)loading Diff Content (so not really syncing anything). To clarify this and to make the two properties more distinguishable, I renamed the one in TextDiff to simply "ScrollOffset". * Added icon and string for "Show All Lines" New StreamGeometry "Icons.Lines.All" using SVG path from "text_line_spacing_regular" at https://avaloniaui.github.io/icons.html. New String "Text.Diff.VisualLines.All" for en_US locale (no translations yet). * Implemented new TextDiff feature "Show All Lines" (toggle) * Added new ToggleButton in DiffView toolbar, visible when IsTextDiff, disabling the buttons "Increase/Decrease Number of Visible Lines" when on. * Added new Preference property "UseFullTextDiff". * Added StyledProperty "UseFullTextDiffProperty" in TextDiffView, with a DataTemplate binding to the corresponding preference property. * When changed, UseFullTextDiffProperty is handled identically as UseSideBySideDiffProperty (via new helper method RefreshContent(), for unification with OnDataContextChanged()). * Added new method DiffContext.ToggleFullTextDiff() for changing the preference property and reloading the diff content. * Implemented the new feature by overriding the "unified" (number of context lines) for Commands.Diff() with a very high number. NOTE: The number used (~1 billion) is supposed to be the highest one working on Mac, according to this forum comment: https://stackoverflow.com/questions/28727424/for-git-diff-is-there-a-uinfinity-option-to-show-the-whole-file#comment135202820_28846576
2024-11-04 00:32:51 -08:00
private bool _useFullTextDiff = false;
2024-03-20 00:36:10 -07:00
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;
private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List;
private Models.ChangeViewMode _commitChangeViewMode = Models.ChangeViewMode.List;
private string _gitDefaultCloneDir = string.Empty;
private int _shellOrTerminal = -1;
2024-03-20 00:36:10 -07:00
private int _externalMergeToolType = 0;
private string _externalMergeToolPath = string.Empty;
private uint _statisticsSampleColor = 0xFF00FF00;
2024-03-20 00:36:10 -07:00
}
}