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
4 changes: 2 additions & 2 deletions src/Devolutions.Terminal.App/Models/NewTabMenuResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public sealed record ResolvedNewTabMenuItem(

public static class NewTabMenuResolver
{
public static ProfileSettings ForMenuLaunch(ProfileSettings profile, bool shiftHeld)
public static ProfileSettings ForMenuLaunch(ProfileSettings profile, bool elevate)
{
ArgumentNullException.ThrowIfNull(profile);
return shiftHeld && OperatingSystem.IsWindows()
return elevate && OperatingSystem.IsWindows()
? profile.WithOverrides(new NewTerminalArgs(Elevate: true))
: profile;
}
Expand Down
135 changes: 92 additions & 43 deletions src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public partial class MainWindow :
private readonly DispatcherTimer _notificationTimer;
private PixelPoint? _normalPosition;
private WindowSizeState _normalSize = new();
private KeyModifiers _newTabButtonModifiers;

public MainWindow() : this(0, string.Empty, null)
{
Expand Down Expand Up @@ -145,11 +146,12 @@ public MainWindow(
var newTabShortcut = _settings.ActionMap
.GetKeyBindingForAction("Terminal.OpenNewTab")?
.ToDisplayString();
ToolTip.SetTip(
NewTabButton,
string.IsNullOrWhiteSpace(newTabShortcut)
? "New tab"
: $"New tab ({newTabShortcut})");
ToolTip.SetTip(NewTabButton, NewTabButtonTooltip(newTabShortcut));
NewTabButton.AddHandler(
PointerPressedEvent,
(_, e) => _newTabButtonModifiers = e.KeyModifiers,
RoutingStrategies.Tunnel,
handledEventsToo: true);
ApplyWindowChrome();
RefreshJumpList();
_stateStore = stateStore ?? SettingsService.LoadApplicationState();
Expand Down Expand Up @@ -373,8 +375,28 @@ activation.Actions[0] is
} &&
terminal == new NewTerminalArgs()));

private async void NewTab_OnClick(object? sender, RoutedEventArgs e) =>
await CreateTabAsync(_settings.GetDefaultProfile()).ConfigureAwait(true);
private async void NewTab_OnClick(object? sender, RoutedEventArgs e)
{
var modifiers = _newTabButtonModifiers;
_newTabButtonModifiers = KeyModifiers.None;
await LaunchProfileAsync(_settings.GetDefaultProfile(), modifiers).ConfigureAwait(true);
}

private static string NewTabButtonTooltip(string? newTabShortcut)
{
var lines = new List<string>
{
string.IsNullOrWhiteSpace(newTabShortcut) ? "New tab" : $"New tab ({newTabShortcut})",
"Alt+Click to split the current window",
"Shift+Click to open a new window",
};
if (OperatingSystem.IsWindows())
{
lines.Add("Ctrl+Click to open as administrator");
}

return string.Join(Environment.NewLine, lines);
}

private void Menu_OnClick(object? sender, RoutedEventArgs e)
{
Expand Down Expand Up @@ -507,29 +529,26 @@ private MenuItem CreateMenuItem(ResolvedNewTabMenuItem item)
menu.Icon = CreateTabIcon(ProfileVisualDefaults.Icon(item.Profile));
menu.InputGesture = ProfileMenuGesture(item.Profile);
var profile = item.Profile;
var shiftHeld = false;
if (OperatingSystem.IsWindows())
{
ToolTip.SetTip(menu, profile.Elevate
? "Opens as administrator"
: "Shift-click to open as administrator");
menu.AddHandler(
PointerPressedEvent,
(_, e) => shiftHeld = e.KeyModifiers.HasFlag(KeyModifiers.Shift),
RoutingStrategies.Tunnel,
handledEventsToo: true);
menu.AddHandler(
KeyDownEvent,
(_, e) => shiftHeld = e.KeyModifiers.HasFlag(KeyModifiers.Shift),
RoutingStrategies.Tunnel,
handledEventsToo: true);
}
var pressedModifiers = KeyModifiers.None;
ToolTip.SetTip(
menu,
profile.Elevate ? "Opens as administrator" : NewTabButtonTooltip(newTabShortcut: null));
menu.AddHandler(
PointerPressedEvent,
(_, e) => pressedModifiers = e.KeyModifiers,
RoutingStrategies.Tunnel,
handledEventsToo: true);
menu.AddHandler(
KeyDownEvent,
(_, e) => pressedModifiers = e.KeyModifiers,
RoutingStrategies.Tunnel,
handledEventsToo: true);

menu.Command = new RelayCommand(() =>
{
var elevate = shiftHeld;
shiftHeld = false;
_ = CreateTabAsync(NewTabMenuResolver.ForMenuLaunch(profile, elevate));
var modifiers = pressedModifiers;
pressedModifiers = KeyModifiers.None;
_ = LaunchProfileAsync(profile, modifiers);
});
}
else if (item.ActionId is { } actionId &&
Expand All @@ -545,6 +564,51 @@ private MenuItem CreateMenuItem(ResolvedNewTabMenuItem item)
return menu;
}

private void OpenNewWindow(INewContentArgs? content)
{
content ??= new NewTerminalArgs();
if (_newWindowRequested is not null)
{
_newWindowRequested(new(
null,
null,
null,
null,
TerminalWindowLaunchMode.Default,
[new(ShortcutAction.NewTab, new NewTabArgs(content))]));
}
else
{
new MainWindow(ResolveProfile(content)).Show();
}
}

/// <summary>
/// Launches <paramref name="profile"/> honoring the same click modifiers as Windows Terminal:
/// Alt splits the current pane, Shift opens a new window, and Ctrl opens elevated (Windows only).
/// </summary>
private async Task LaunchProfileAsync(ProfileSettings profile, KeyModifiers modifiers)
{
var elevate = OperatingSystem.IsWindows() && modifiers.HasFlag(KeyModifiers.Control);
var effectiveProfile = NewTabMenuResolver.ForMenuLaunch(profile, elevate);

if (modifiers.HasFlag(KeyModifiers.Alt))
{
await SplitActivePaneAsync(PaneSplitOrientation.Vertical, effectiveProfile).ConfigureAwait(true);
return;
}

if (modifiers.HasFlag(KeyModifiers.Shift))
{
OpenNewWindow(new NewTerminalArgs(
Profile: profile.Guid ?? profile.Name,
Elevate: elevate ? true : null));
return;
}

await CreateTabAsync(effectiveProfile).ConfigureAwait(true);
}

private async Task CreateTabAsync(ProfileSettings profile)
{
TerminalPane? pane = null;
Expand Down Expand Up @@ -2023,22 +2087,7 @@ args with
});
Register(ShortcutAction.NewWindow, ActionScope.Application, _ => true, action =>
{
var content = (action.Args as NewWindowArgs)?.ContentArgs ?? new NewTerminalArgs();
if (_newWindowRequested is not null)
{
_newWindowRequested(new(
null,
null,
null,
null,
TerminalWindowLaunchMode.Default,
[new(ShortcutAction.NewTab, new NewTabArgs(content))]));
}
else
{
new MainWindow(ResolveProfile(content)).Show();
}

OpenNewWindow((action.Args as NewWindowArgs)?.ContentArgs);
return Task.CompletedTask;
});
Register(ShortcutAction.CloseWindow, ActionScope.Window, _ => true, _ =>
Expand Down
10 changes: 5 additions & 5 deletions tests/Devolutions.Terminal.App.Tests/NewTabMenuResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ public void EmptyFoldersAreOnlyIncludedWhenAllowed()
}

[Fact]
public void ShiftOpeningAProfileElevatesOnlyThatLaunchOnWindows()
public void CtrlClickOpeningAProfileElevatesOnlyThatLaunchOnWindows()
{
var local = ProfileSettings.CreateCmd();
var normal = NewTabMenuResolver.ForMenuLaunch(local, shiftHeld: false);
var shifted = NewTabMenuResolver.ForMenuLaunch(local, shiftHeld: true);
var normal = NewTabMenuResolver.ForMenuLaunch(local, elevate: false);
var elevated = NewTabMenuResolver.ForMenuLaunch(local, elevate: true);

Assert.Same(local, normal);
Assert.Equal(OperatingSystem.IsWindows(), shifted.Elevate);
Assert.Equal(OperatingSystem.IsWindows(), elevated.Elevate);
Assert.False(local.Elevate);
if (OperatingSystem.IsWindows())
{
Assert.NotSame(local, shifted);
Assert.NotSame(local, elevated);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Devolutions.Terminal.UI.Tests;
public sealed class WindowActionRegistrationTests
{
[AvaloniaFact]
public void NewTabDropdownUsesExistingProfileEntriesForShiftLaunch()
public void NewTabDropdownUsesExistingProfileEntriesForCtrlClickLaunch()
{
var window = new MainWindow();

Expand All @@ -25,9 +25,12 @@ public void NewTabDropdownUsesExistingProfileEntriesForShiftLaunch()
Assert.NotEmpty(profiles);
foreach (var profile in profiles)
{
var tip = Assert.IsType<string>(ToolTip.GetTip(profile));
Assert.Contains("Alt+Click to split the current window", tip);
Assert.Contains("Shift+Click to open a new window", tip);
Assert.Equal(
OperatingSystem.IsWindows() ? "Shift-click to open as administrator" : null,
ToolTip.GetTip(profile));
OperatingSystem.IsWindows(),
tip.Contains("Ctrl+Click to open as administrator", StringComparison.Ordinal));
}
}

Expand Down
Loading