Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 100 additions & 19 deletions src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -176,6 +177,7 @@ private MainWindow(ProfileSettings initialProfile) : this()
public IReadOnlyCollection<ShortcutAction> RegisteredActions =>
_actionDispatcher.RegisteredActions;
public TerminalTab? ActiveTab => _activeTab;
public bool CanRestoreLastClosedTab => _tabCollection.ClosedCount > 0;
public IReadOnlyList<string> WorkspaceNames => _stateStore.GetWorkspaceNames();
public bool AlwaysShowNotificationIcon => _settings.AlwaysShowNotificationIcon;
public bool MinimizeToNotificationArea => _settings.MinimizeToNotificationArea;
Expand Down Expand Up @@ -204,7 +206,7 @@ public async ValueTask<TerminalWindowActivationResult> ActivateAsync(
results.Add(await DispatchActionAsync(action).ConfigureAwait(true));
}

if (!activation.Actions.Any(ManagesWindowVisibility))
if (!activation.Actions.Any(ManagesWindowVisibility) && !_isClosed)
{
Show();
Activate();
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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,
Expand All @@ -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) =>
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<TerminalPane> panes)
{
Id = id == Guid.Empty ? Guid.NewGuid() : id;
Expand Down
11 changes: 5 additions & 6 deletions src/Devolutions.Terminal.Settings.Editor/Controls/SettingsRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -47,16 +46,16 @@ 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);
layout.Children.Add(_value);
var border = new Border
{
CornerRadius = new CornerRadius(4),
BorderThickness = new Thickness(1),
BorderThickness = new Thickness(0),
Child = layout,
};
border.Classes.Add("settings-row");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Layout;

namespace Devolutions.Terminal.Settings.Editor.Controls;

/// <summary>
/// A Windows Terminal style toggle: the current state is written to the left of the switch.
/// </summary>
public sealed class SettingsToggle : UserControl
{
private readonly TextBlock _state;
private readonly ToggleSwitch _toggle;

public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<SettingsToggle, bool>(
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;
}
}
}
Loading
Loading