diff --git a/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs b/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs index 006820d..42fcd57 100644 --- a/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs +++ b/src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs @@ -55,6 +55,7 @@ public partial class MainWindow : private Point _dragStart; private PaletteMode _paletteMode; private bool _layoutPersisted; + private bool _isClosed; private bool _focusMode; private bool _persistenceBlockedByInvalidLayout; private ActionDispatchResult? _lastDispatchResult; @@ -176,6 +177,7 @@ private MainWindow(ProfileSettings initialProfile) : this() public IReadOnlyCollection RegisteredActions => _actionDispatcher.RegisteredActions; public TerminalTab? ActiveTab => _activeTab; + public bool CanRestoreLastClosedTab => _tabCollection.ClosedCount > 0; public IReadOnlyList WorkspaceNames => _stateStore.GetWorkspaceNames(); public bool AlwaysShowNotificationIcon => _settings.AlwaysShowNotificationIcon; public bool MinimizeToNotificationArea => _settings.MinimizeToNotificationArea; @@ -204,7 +206,7 @@ public async ValueTask ActivateAsync( results.Add(await DispatchActionAsync(action).ConfigureAwait(true)); } - if (!activation.Actions.Any(ManagesWindowVisibility)) + if (!activation.Actions.Any(ManagesWindowVisibility) && !_isClosed) { Show(); Activate(); @@ -608,7 +610,7 @@ private async Task SplitActivePaneAsync( { var tab = _activeTab; var activePane = tab?.Panes.ActiveContent; - if (tab is null || activePane is null || tab.IsClosing) + if (tab is null || tab.IsSettingsTab || activePane is null || tab.IsClosing) { return; } @@ -656,6 +658,12 @@ private void ActivateTab(TerminalTab tab) SynchronizeTitle(tab); RebuildTabs(); RebuildTerminalHost(); + if (tab.IsSettingsTab) + { + tab.CustomContent?.Focus(); + return; + } + tab.Panes.ActiveContent?.Control.Focus(); } @@ -711,7 +719,7 @@ private async Task CloseTabAsync(TerminalTab tab, bool remember = true) tab.IsClosing = true; var wasActive = ReferenceEquals(_activeTab, tab); DetachPaneControls(tab); - _tabCollection.Close(tab, CaptureTab, remember); + _tabCollection.Close(tab, CaptureTab, remember && !tab.IsSettingsTab); if (wasActive) { _activeTab = null; @@ -755,6 +763,19 @@ private void RebuildTerminalHost() TerminalHost.Children.Clear(); var tab = _activeTab; + if (tab?.CustomContent is { } custom) + { + if (custom.Parent is Panel parent) + { + parent.Children.Remove(custom); + } + + custom.HorizontalAlignment = HorizontalAlignment.Stretch; + custom.VerticalAlignment = VerticalAlignment.Stretch; + TerminalHost.Children.Add(custom); + return; + } + if (tab?.Panes.Root is null) { return; @@ -876,7 +897,17 @@ private void RebuildTabs() ClipToBounds = true, }; var prefix = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 }; - if (!string.IsNullOrWhiteSpace(presentation.Icon)) + if (tab.IsSettingsTab) + { + prefix.Children.Add(new TextBlock + { + Text = "\uE713", + FontFamily = new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets"), + FontSize = 14, + VerticalAlignment = VerticalAlignment.Center, + }); + } + else if (!string.IsNullOrWhiteSpace(presentation.Icon)) { prefix.Children.Add(CreateTabIcon(presentation.Icon)); } @@ -1103,7 +1134,14 @@ private ContextMenu CreateTabContextMenu(TerminalTab tab) => new MenuItem { Header = "Duplicate", - Command = new RelayCommand(() => _ = RestoreTabAsync(CaptureTab(tab), regenerateIdentities: true)), + IsEnabled = !tab.IsSettingsTab, + Command = new RelayCommand(() => + { + if (!tab.IsSettingsTab) + { + _ = RestoreTabAsync(CaptureTab(tab), regenerateIdentities: true); + } + }), }, new MenuItem { @@ -1174,7 +1212,8 @@ private void EndTabDrag(TerminalTab tab, Control control, PointerReleasedEventAr return; } - if (position.Y < -24 || position.Y > TabStrip.Bounds.Height + 24) + if (!tab.IsSettingsTab && + (position.Y < -24 || position.Y > TabStrip.Bounds.Height + 24)) { var local = e.GetPosition(this); var screen = new PixelPoint(Position.X + (int)local.X, Position.Y + (int)local.Y); @@ -1242,7 +1281,9 @@ private Button CreateCloseButton(TerminalTab tab) return close; } - private TermControl? ActiveControl => _activeTab?.Panes.ActiveContent?.Control; + private TermControl? ActiveControl => _activeTab is { IsSettingsTab: false } terminalTab + ? terminalTab.Panes.ActiveContent?.Control + : null; private void ConfigureActionDispatcher() { @@ -1357,7 +1398,8 @@ private void ConfigureActionDispatcher() Register(ShortcutAction.NewTab, ActionScope.Tab, _ => true, async action => await CreateTabAsync(ResolveProfile((action.Args as NewTabArgs)?.ContentArgs)).ConfigureAwait(true)); - Register(ShortcutAction.DuplicateTab, ActionScope.Tab, _ => _activeTab?.Panes.ActiveContent is not null, + Register(ShortcutAction.DuplicateTab, ActionScope.Tab, + _ => _activeTab is { IsSettingsTab: false } && _activeTab.Panes.ActiveContent is not null, async _ => await RestoreTabAsync(CaptureTab(_activeTab!), regenerateIdentities: true).ConfigureAwait(true)); Register(ShortcutAction.CloseTab, ActionScope.Tab, action => ResolveTab((action.Args as CloseTabArgs)?.Index) is not null, async action => await CloseTabAsync(ResolveTab((action.Args as CloseTabArgs)?.Index)!).ConfigureAwait(true)); @@ -1575,7 +1617,7 @@ action.Args is SwapPaneArgs args && }); Register(ShortcutAction.RestartConnection, ActionScope.Pane, _ => ActiveControl is not null, async _ => await ActiveControl!.RestartAsync().ConfigureAwait(true)); - Register(ShortcutAction.TogglePaneReadOnly, ActionScope.Pane, _ => _activeTab?.Panes.ActiveContent is not null, _ => + Register(ShortcutAction.TogglePaneReadOnly, ActionScope.Pane, _ => ActiveControl is not null, _ => { var state = _activeTab!.Panes.ActiveContent!.Presentation; state.IsReadOnly = !state.IsReadOnly; @@ -2112,8 +2154,9 @@ private void MoveFocus(MoveFocusArgs args) private bool CanMovePane(ActionAndArgs action) => action.Args is MovePaneArgs args && string.IsNullOrEmpty(args.Window) && - _activeTab?.Panes.ActiveContent is not null && - ResolveTab(args.TabIndex) is { } target && + _activeTab is { IsSettingsTab: false } && + _activeTab.Panes.ActiveContent is not null && + ResolveTab(args.TabIndex) is { IsSettingsTab: false } target && !ReferenceEquals(target, _activeTab); private void MovePane(MovePaneArgs args) @@ -2544,6 +2587,7 @@ private async Task PasteCoordinatedAsync() var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; var text = clipboard is null ? null : await clipboard.TryGetTextAsync().ConfigureAwait(true); if (tab is null || + tab.IsSettingsTab || activePane is null || !_tabs.Contains(tab) || string.IsNullOrEmpty(text)) @@ -2646,7 +2690,8 @@ private bool TryRenameWindow(string name) } private bool CanPaste() => - _activeTab?.Panes.ActiveContent is { } activePane && + _activeTab is { IsSettingsTab: false } && + _activeTab.Panes.ActiveContent is { } activePane && _activeTab.BroadcastInput.ResolveTargets(activePane, _activeTab.Panes.Leaves()).Count > 0; private void TitleBar_OnPointerPressed(object? sender, PointerPressedEventArgs e) @@ -2908,16 +2953,45 @@ private void CaptureNormalWindowBounds() }; } + private void OpenSettingsTab() + { + var existing = _tabs.FirstOrDefault(static tab => tab.IsSettingsTab); + if (existing is not null) + { + ActivateTab(existing); + return; + } + + var view = SettingsViewFactory.CreateView( + () => SettingsService.LoadWithDynamicProfiles(_dynamicProfileManager), + SaveSettingsAndRefresh, + SettingsService.CreateDefault); + var profile = new ProfileSettings + { + Name = "Settings", + TabTitle = "Settings", + }; + var pane = new TerminalPane( + _nextPaneId++, + CreateSessionDescriptor(profile), + profile, + new TermControl()); + var tab = new TerminalTab(pane) + { + CustomContent = view, + Title = "Settings", + }; + _tabCollection.Add(tab); + ActivateTab(tab); + } + private void OpenSettings(SettingsTarget target = SettingsTarget.SettingsUI) { switch (target) { case SettingsTarget.SettingsUI: case SettingsTarget.AllFiles: - SettingsViewFactory.CreateWindow( - () => SettingsService.LoadWithDynamicProfiles(_dynamicProfileManager), - SaveSettingsAndRefresh, - SettingsService.CreateDefault).Show(this); + OpenSettingsTab(); break; case SettingsTarget.SettingsFile: SaveSettingsAndRefresh(SettingsService.LoadWithDynamicProfiles(_dynamicProfileManager)); @@ -3046,7 +3120,9 @@ private async Task OpenWorkspaceAsync(string name) private (int Columns, int Rows) InitialTerminalSize() { - var profile = _activeTab?.Panes.ActiveContent?.Profile ?? _settings.GetDefaultProfile(); + var profile = _activeTab is { IsSettingsTab: false } terminalTab + ? terminalTab.Panes.ActiveContent?.Profile ?? _settings.GetDefaultProfile() + : _settings.GetDefaultProfile(); var cell = ActiveControl?.CellSize ?? TermControl.MeasureCell(profile, DisplayScale()); var columns = Math.Max( 20, @@ -3063,8 +3139,8 @@ private static int ScrollbarWidth(ProfileSettings profile) => public TerminalWindowLayoutDescriptor CaptureLayout() => new() { - ActiveTabId = _activeTab?.Id, - Tabs = _tabs.Select(CaptureTab).ToList(), + ActiveTabId = _activeTab is { IsSettingsTab: false } ? _activeTab.Id : null, + Tabs = _tabs.Where(static tab => !tab.IsSettingsTab).Select(CaptureTab).ToList(), }; private TabLayoutDescriptor CaptureTab(TerminalTab tab) => @@ -3998,6 +4074,7 @@ protected override void OnClosing(WindowClosingEventArgs e) protected override async void OnClosed(EventArgs e) { + _isClosed = true; if (!_layoutPersisted && _tabs.Count > 0) { TryPersistCurrentLayout(CaptureLayout()); @@ -4233,6 +4310,10 @@ public TerminalTab(TerminalPane initialPane) Color = initialPane.Presentation.Color; } + public Control? CustomContent { get; set; } + + public bool IsSettingsTab => CustomContent is not null; + public TerminalTab(Guid id, PaneTree panes) { Id = id == Guid.Empty ? Guid.NewGuid() : id; diff --git a/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsRow.cs b/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsRow.cs index dc76cd2..21665bb 100644 --- a/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsRow.cs +++ b/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsRow.cs @@ -31,9 +31,8 @@ public SettingsRow() }; _value = new ContentControl { - Width = 248, - MinWidth = 248, - MaxWidth = 248, + MinWidth = 120, + MaxWidth = 240, Margin = new Thickness(24, 0, 0, 0), HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, @@ -47,8 +46,8 @@ public SettingsRow() var layout = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto"), - MinHeight = 44, - Margin = new Thickness(16, 6), + MinHeight = 40, + Margin = new Thickness(16, 11), }; layout.Children.Add(labels); Grid.SetColumn(_value, 1); @@ -56,7 +55,7 @@ public SettingsRow() var border = new Border { CornerRadius = new CornerRadius(4), - BorderThickness = new Thickness(1), + BorderThickness = new Thickness(0), Child = layout, }; border.Classes.Add("settings-row"); diff --git a/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsToggle.cs b/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsToggle.cs new file mode 100644 index 0000000..a896976 --- /dev/null +++ b/src/Devolutions.Terminal.Settings.Editor/Controls/SettingsToggle.cs @@ -0,0 +1,73 @@ +using Avalonia; +using Avalonia.Automation; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; + +namespace Devolutions.Terminal.Settings.Editor.Controls; + +/// +/// A Windows Terminal style toggle: the current state is written to the left of the switch. +/// +public sealed class SettingsToggle : UserControl +{ + private readonly TextBlock _state; + private readonly ToggleSwitch _toggle; + + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register( + nameof(IsChecked), + defaultBindingMode: BindingMode.TwoWay); + + public SettingsToggle() + { + _state = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + Opacity = 0.72, + }; + _toggle = new ToggleSwitch + { + OnContent = string.Empty, + OffContent = string.Empty, + }; + _toggle.IsCheckedChanged += (_, _) => IsChecked = _toggle.IsChecked ?? false; + var layout = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 10, + HorizontalAlignment = HorizontalAlignment.Right, + Children = { _state, _toggle }, + }; + base.Content = layout; + UpdateState(); + } + + public bool IsChecked + { + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == IsCheckedProperty) + { + UpdateState(); + } + else if (change.Property == AutomationProperties.NameProperty) + { + AutomationProperties.SetName(_toggle, AutomationProperties.GetName(this) ?? string.Empty); + } + } + + private void UpdateState() + { + _state.Text = IsChecked ? "On" : "Off"; + if (_toggle.IsChecked != IsChecked) + { + _toggle.IsChecked = IsChecked; + } + } +} diff --git a/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs b/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs index 5c4f886..e957b60 100644 --- a/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs +++ b/src/Devolutions.Terminal.Settings.Editor/PageViewModels.cs @@ -54,7 +54,10 @@ public bool CenterOnLaunch public string CenterOnLaunchState => CenterOnLaunch ? "On" : "Off"; public LaunchMode LaunchMode { get => settings.LaunchMode; set => Change(settings.LaunchMode, value, v => settings.LaunchMode = v); } public string FirstWindowPreference { get => settings.FirstWindowPreference; set => Change(settings.FirstWindowPreference, value, v => settings.FirstWindowPreference = v); } + public IReadOnlyList FirstWindowPreferenceChoices { get; } = + ["defaultProfile", "persistedLayoutAndContent", "persistedWindowLayout"]; public string WindowingBehavior { get => settings.WindowingBehavior; set => Change(settings.WindowingBehavior, value, v => settings.WindowingBehavior = v); } + public IReadOnlyList WindowingBehaviorChoices { get; } = ["useNew", "useExisting"]; public string StartupActions { get => settings.StartupActions; set => Change(settings.StartupActions, value, v => settings.StartupActions = v); } public bool AlwaysOnTop { @@ -102,6 +105,7 @@ public sealed class InteractionSettingsViewModel(AppSettings settings, Action ch public bool DetectUrls { get => settings.DetectUrls; set => Change(settings.DetectUrls, value, v => settings.DetectUrls = v); } public bool WarnAboutLargePaste { get => settings.WarnAboutLargePaste; set => Change(settings.WarnAboutLargePaste, value, v => settings.WarnAboutLargePaste = v); } public string WarnAboutMultiLinePaste { get => settings.WarnAboutMultiLinePaste; set => Change(settings.WarnAboutMultiLinePaste, value, v => settings.WarnAboutMultiLinePaste = v); } + public IReadOnlyList WarnAboutMultiLinePasteChoices { get; } = ["automatic", "always", "never"]; public string WordDelimiters { get => settings.WordDelimiters; set => Change(settings.WordDelimiters, value, v => settings.WordDelimiters = v); } private void Change(T oldValue, T newValue, Action update) @@ -139,6 +143,7 @@ public ThemeItemViewModel? SelectedTheme public bool UseAcrylicInTabRow { get => settings.UseAcrylicInTabRow; set => Change(settings.UseAcrylicInTabRow, value, v => settings.UseAcrylicInTabRow = v); } public bool DisableAnimations { get => settings.DisableAnimations; set => Change(settings.DisableAnimations, value, v => settings.DisableAnimations = v); } public string NewTabPosition { get => settings.NewTabPosition; set => Change(settings.NewTabPosition, value, v => settings.NewTabPosition = v); } + public IReadOnlyList NewTabPositionChoices { get; } = ["afterLastTab", "atEnd"]; private void Change(T oldValue, T newValue, Action update) { @@ -397,6 +402,9 @@ public ProfileTerminalSettingsViewModel(IReadOnlyList prof } public IReadOnlyList CloseOnExitModes { get; } = Enum.GetValues(); + public IReadOnlyList CursorShapeChoices { get; } = + ["bar", "doubleUnderscore", "emptyBox", "filledBox", "underscore", "vintage"]; + public IReadOnlyList ScrollbarStateChoices { get; } = ["visible", "hidden"]; public IReadOnlyList Profiles { get; } public ProfileItemViewModel? SelectedProfile { get => _selectedProfile; set => SetProperty(ref _selectedProfile, value); } } @@ -789,6 +797,7 @@ public string TerminalEngine v => settings.TerminalEngine = v); } public string GraphicsApi { get => settings.GraphicsApi; set => Change(settings.GraphicsApi, value, v => settings.GraphicsApi = v); } + public IReadOnlyList GraphicsApiChoices { get; } = ["automatic", "direct3d11", "opengl", "software"]; public bool DisablePartialInvalidation { get => settings.DisablePartialInvalidation; set => Change(settings.DisablePartialInvalidation, value, v => settings.DisablePartialInvalidation = v); } public bool SoftwareRendering { get => settings.SoftwareRendering; set => Change(settings.SoftwareRendering, value, v => settings.SoftwareRendering = v); } public bool UseBackgroundImageForWindow { get => settings.UseBackgroundImageForWindow; set => Change(settings.UseBackgroundImageForWindow, value, v => settings.UseBackgroundImageForWindow = v); } @@ -807,8 +816,11 @@ public sealed class CompatibilitySettingsViewModel(AppSettings settings, Action : SettingsPageViewModel("Compatibility", "Configure text measurement, character width, and compatibility switches.") { public string TextMeasurement { get => settings.TextMeasurement; set => Change(settings.TextMeasurement, value, v => settings.TextMeasurement = v); } + public IReadOnlyList TextMeasurementChoices { get; } = ["graphemes", "cells"]; public string AmbiguousWidth { get => settings.AmbiguousWidth; set => Change(settings.AmbiguousWidth, value, v => settings.AmbiguousWidth = v); } + public IReadOnlyList AmbiguousWidthChoices { get; } = ["narrow", "wide"]; public string DefaultInputScope { get => settings.DefaultInputScope; set => Change(settings.DefaultInputScope, value, v => settings.DefaultInputScope = v); } + public IReadOnlyList DefaultInputScopeChoices { get; } = ["default", "keyboard", "touch"]; public bool AllowHeadless { get => settings.AllowHeadless; set => Change(settings.AllowHeadless, value, v => settings.AllowHeadless = v); } public bool EnableUnfocusedAcrylic { get => settings.EnableUnfocusedAcrylic; set => Change(settings.EnableUnfocusedAcrylic, value, v => settings.EnableUnfocusedAcrylic = v); } public bool InputServiceWarning { get => settings.InputServiceWarning; set => Change(settings.InputServiceWarning, value, v => settings.InputServiceWarning = v); } diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsEditorViewModel.cs b/src/Devolutions.Terminal.Settings.Editor/SettingsEditorViewModel.cs index 8eea287..ea1c026 100644 --- a/src/Devolutions.Terminal.Settings.Editor/SettingsEditorViewModel.cs +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsEditorViewModel.cs @@ -17,6 +17,7 @@ public sealed class SettingsNavigationItem public bool HasGroupHeader => !string.IsNullOrEmpty(GroupHeader); public required string Keywords { get; init; } public required object ViewModel { get; init; } + public ProfileItemViewModel? Profile { get; init; } public override string ToString() => Title; } @@ -62,10 +63,11 @@ public SettingsEditorViewModel( _getRevision = getRevision ?? (() => null); _settings = _load(); _loadedRevision = _getRevision(); - ApplyCommand = new(Apply, () => IsDirty); - RevertCommand = new(Revert, () => IsDirty); + ApplyCommand = new(Apply); + RevertCommand = new(Revert); ResetCommand = new(ResetToDefaults); OpenJsonCommand = new(OpenJsonFile); + AddProfileCommand = new(AddProfile); BuildPages(SettingsPage.Startup); } @@ -106,6 +108,12 @@ public SettingsNavigationItem? SelectedNavigationItem { if (SetProperty(ref _selectedNavigationItem, value)) { + if (value?.Profile is { } profile && + value.ViewModel is ProfilesSettingsViewModel profilesPage) + { + profilesPage.SelectedProfile = profile; + } + OnPropertyChanged(nameof(CurrentPage)); } } @@ -131,6 +139,7 @@ private set public RelayCommand RevertCommand { get; } public RelayCommand ResetCommand { get; } public RelayCommand OpenJsonCommand { get; } + public RelayCommand AddProfileCommand { get; } public void SelectPage(SettingsPage page) { @@ -190,6 +199,37 @@ public void ResetToDefaults() StatusMessage = "Factory defaults loaded. Apply to save them."; } + public void AddProfile() + { + if (!TryCommitEditors(out var error)) + { + StatusMessage = error!; + return; + } + + var profile = new ProfileSettings + { + Guid = $"{{{Guid.NewGuid()}}}", + Name = "New profile", + Commandline = OperatingSystem.IsWindows() + ? @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" + : "/bin/bash", + Origin = SettingsOrigin.User, + }; + _settings.Profiles.Add(profile); + MarkDirty(); + BuildPages(SettingsPage.Profiles); + var created = _navigationItems.FirstOrDefault(item => + item.Profile is not null && + string.Equals(item.Profile.Guid, profile.Guid, StringComparison.OrdinalIgnoreCase)); + if (created is not null) + { + SelectedNavigationItem = created; + } + + StatusMessage = "New profile added. Apply to save it."; + } + private void OpenJsonFile() { try @@ -239,8 +279,9 @@ private void BuildPages(SettingsPage selectedPage) .ToArray(); _actions = new(_settings, MarkDirty); _newTabMenu = new(_settings, MarkDirty); - _navigationItems = - [ + var profilesPage = new ProfilesSettingsViewModel(_profiles); + var items = new List + { Item(SettingsPage.Startup, "Startup", "launch default profile window position startup actions", new StartupSettingsViewModel(_settings, MarkDirty)), Item(SettingsPage.Interaction, "Interaction", "copy paste selection mouse urls focus", new InteractionSettingsViewModel(_settings, MarkDirty)), Item(SettingsPage.Appearance, "Appearance", "global themes tabs acrylic mica visual", new AppearanceSettingsViewModel(_settings, MarkDirty)), @@ -248,13 +289,29 @@ private void BuildPages(SettingsPage selectedPage) Item(SettingsPage.Rendering, "Rendering", "graphics api software invalidation", new RenderingSettingsViewModel(_settings, MarkDirty)), Item(SettingsPage.Compatibility, "Compatibility", "text measurement width input headless acrylic", new CompatibilitySettingsViewModel(_settings, MarkDirty)), Item(SettingsPage.Actions, "Actions", "keybindings key chord command json", _actions), - Item(SettingsPage.NewTabMenu, "New tab menu", "menu folder separator profile action json", _newTabMenu), + Item(SettingsPage.NewTabMenu, "New Tab Menu", "menu folder separator profile action json", _newTabMenu), Item(SettingsPage.Extensions, "Extensions", "sources fragments experimental language notification", new ExtensionsSettingsViewModel(_settings, MarkDirty)), - Item(SettingsPage.Profiles, "Defaults", "profile commandline directory icon tab title hidden", new ProfilesSettingsViewModel(_profiles), "Profiles"), - Item(SettingsPage.ProfileAppearance, "Profile appearance", "profile font colors opacity background image", new ProfileAppearanceSettingsViewModel(_profiles)), - Item(SettingsPage.ProfileTerminal, "Profile terminal", "profile scrollback cursor close antialiasing", new ProfileTerminalSettingsViewModel(_profiles)), - Item(SettingsPage.ProfileAdvanced, "Profile advanced", "profile vt environment kitty osc compatibility", new ProfileAdvancedSettingsViewModel(_profiles)), - ]; + Item(SettingsPage.Profiles, "Defaults", "profile commandline directory icon tab title hidden", profilesPage, "Profiles"), + }; + var windows = OperatingSystem.IsWindows(); + foreach (var profile in _profiles) + { + items.Add(new SettingsNavigationItem + { + Page = SettingsPage.Profiles, + IconFontFamily = windows ? "Segoe Fluent Icons" : "Cascadia Mono", + Icon = NavigationIcon(profile.Icon, windows), + Title = profile.Name, + Keywords = $"profile {profile.Name} {profile.Commandline}", + ViewModel = profilesPage, + Profile = profile, + }); + } + + items.Add(Item(SettingsPage.ProfileAppearance, "Profile appearance", "profile font colors opacity background image", new ProfileAppearanceSettingsViewModel(_profiles))); + items.Add(Item(SettingsPage.ProfileTerminal, "Profile terminal", "profile scrollback cursor close antialiasing", new ProfileTerminalSettingsViewModel(_profiles))); + items.Add(Item(SettingsPage.ProfileAdvanced, "Profile advanced", "profile vt environment kitty osc compatibility", new ProfileAdvancedSettingsViewModel(_profiles))); + _navigationItems = items; Diagnostics = _settings.Diagnostics .Select(diagnostic => new SettingsDiagnosticViewModel( diagnostic.Severity.ToString(), @@ -269,6 +326,20 @@ private void BuildPages(SettingsPage selectedPage) OnPropertyChanged(nameof(SettingsPath)); } + private static string NavigationIcon(string? icon, bool windows) + { + if (string.IsNullOrWhiteSpace(icon) || + icon.Contains("://", StringComparison.Ordinal) || + icon.Contains('\\') || + icon.Contains('/') || + icon.Length > 4) + { + return windows ? "\uE756" : ">"; + } + + return icon; + } + private static SettingsNavigationItem Item( SettingsPage page, string title, diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsTheme.axaml b/src/Devolutions.Terminal.Settings.Editor/SettingsTheme.axaml index d3e7f52..c8cbbdd 100644 --- a/src/Devolutions.Terminal.Settings.Editor/SettingsTheme.axaml +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsTheme.axaml @@ -6,33 +6,41 @@ - + - + - + + - + - + - - + + + - - + + - + + + + + + @@ -149,20 +174,48 @@ + + + + + + + + diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml new file mode 100644 index 0000000..b1e80fb --- /dev/null +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml @@ -0,0 +1,895 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml.cs b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml.cs new file mode 100644 index 0000000..4a4d878 --- /dev/null +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Devolutions.Terminal.Settings.Editor; + +public partial class SettingsView : UserControl +{ + public SettingsView() + : this(new SettingsEditorViewModel()) + { + } + + public SettingsView(SettingsEditorViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(viewModel); + AvaloniaXamlLoader.Load(this); + DataContext = viewModel; + } +} diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsViewFactory.cs b/src/Devolutions.Terminal.Settings.Editor/SettingsViewFactory.cs index 1965afd..c303f3b 100644 --- a/src/Devolutions.Terminal.Settings.Editor/SettingsViewFactory.cs +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsViewFactory.cs @@ -11,5 +11,19 @@ public static SettingsWindow CreateWindow( Action save, Func createDefault, Func? getRevision = null) => - new(new SettingsEditorViewModel(load, save, createDefault, getRevision)); + new(CreateViewModel(load, save, createDefault, getRevision)); + + public static SettingsView CreateView( + Func load, + Action save, + Func createDefault, + Func? getRevision = null) => + new(CreateViewModel(load, save, createDefault, getRevision)); + + private static SettingsEditorViewModel CreateViewModel( + Func load, + Action save, + Func createDefault, + Func? getRevision) => + new(load, save, createDefault, getRevision); } diff --git a/src/Devolutions.Terminal.Settings.Editor/SettingsWindow.axaml b/src/Devolutions.Terminal.Settings.Editor/SettingsWindow.axaml index b942348..9e429d0 100644 --- a/src/Devolutions.Terminal.Settings.Editor/SettingsWindow.axaml +++ b/src/Devolutions.Terminal.Settings.Editor/SettingsWindow.axaml @@ -1,670 +1,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -