355 lines
12 KiB
C#
355 lines
12 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using CursorLang.Interop;
|
|
using CursorLang.Models;
|
|
using CursorLang.Services;
|
|
using CursorLang.Tests.Infrastructure;
|
|
using CursorLang.ViewModels;
|
|
using CursorLang.Views;
|
|
|
|
namespace CursorLang.Tests.Views;
|
|
|
|
/// <summary>
|
|
/// The tooltip window itself: where it ends up and how Windows sees it.
|
|
/// </summary>
|
|
public sealed class LayoutPopupWindowTests
|
|
{
|
|
private const int GwlExstyle = -20;
|
|
private const int WsExNoactivate = 0x08000000;
|
|
private const int WsExToolwindow = 0x00000080;
|
|
|
|
[Fact]
|
|
public void The_window_is_created_before_the_first_show()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var popup = Popup.Create(new AppSettings());
|
|
|
|
// The handle is needed to set the window bounds before the show:
|
|
// otherwise the window flashes at its default size for a moment
|
|
Assert.NotEqual(IntPtr.Zero, popup.Handle);
|
|
Assert.False(popup.Window.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void The_window_shows_what_the_view_model_gave_it()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings();
|
|
using var popup = Popup.Create(settings);
|
|
|
|
Assert.Same(popup.ViewModel, popup.Window.DataContext);
|
|
|
|
popup.ViewModel.ShortName = "RU";
|
|
popup.Window.ShowPopup();
|
|
|
|
Assert.True(popup.Window.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void The_window_does_what_the_popup_service_expects_of_it()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var popup = Popup.Create(new AppSettings());
|
|
|
|
Assert.IsAssignableFrom<ILayoutPopupWindow>(popup.Window);
|
|
});
|
|
}
|
|
|
|
// The tooltip pops up over other applications and must neither take the
|
|
// focus nor turn up in Alt+Tab
|
|
[Fact]
|
|
public void The_window_takes_no_focus_and_stays_out_of_the_switcher()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var popup = Popup.Create(new AppSettings());
|
|
|
|
int style = GetWindowLong(popup.Handle, GwlExstyle);
|
|
|
|
Assert.Equal(WsExNoactivate, style & WsExNoactivate);
|
|
Assert.Equal(WsExToolwindow, style & WsExToolwindow);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void The_window_stays_on_top_and_out_of_the_mouses_way()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var popup = Popup.Create(new AppSettings());
|
|
|
|
Assert.True(popup.Window.Topmost);
|
|
Assert.False(popup.Window.ShowInTaskbar);
|
|
Assert.False(popup.Window.ShowActivated);
|
|
Assert.False(popup.Window.IsHitTestVisible);
|
|
Assert.False(popup.Window.Focusable);
|
|
Assert.Equal(WindowStyle.None, popup.Window.WindowStyle);
|
|
Assert.True(popup.Window.AllowsTransparency);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void The_opacity_comes_from_the_settings()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings { Opacity = 0.42 };
|
|
using var popup = Popup.Create(settings);
|
|
popup.Window.ShowPopup();
|
|
|
|
Assert.Equal(0.42, popup.Window.Opacity, precision: 3);
|
|
|
|
settings.Opacity = 0.75;
|
|
Assert.Equal(0.75, popup.Window.Opacity, precision: 3);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(ScreenPosition.TopLeft)]
|
|
[InlineData(ScreenPosition.Top)]
|
|
[InlineData(ScreenPosition.TopRight)]
|
|
[InlineData(ScreenPosition.Center)]
|
|
[InlineData(ScreenPosition.BottomLeft)]
|
|
[InlineData(ScreenPosition.Bottom)]
|
|
[InlineData(ScreenPosition.BottomRight)]
|
|
public void In_the_fixed_point_mode_the_window_lands_where_it_was_computed(ScreenPosition position)
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings
|
|
{
|
|
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24,
|
|
};
|
|
|
|
using var popup = Popup.Create(settings);
|
|
popup.ViewModel.ShortName = "RU";
|
|
|
|
(PopupWindowNative.Rect before, _) = PopupWindowNative.GetActiveMonitorWorkArea();
|
|
popup.Window.ShowPopup();
|
|
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
|
|
|
if (!before.Equals(work))
|
|
{
|
|
Assert.Skip("The active monitor changed while the check was running");
|
|
}
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
|
|
|
int width = bounds.Right - bounds.Left;
|
|
int height = bounds.Bottom - bounds.Top;
|
|
PopupWindowNative.Point expected = PopupLayout.OnScreen(
|
|
work, position, PopupLayout.ToPixels(settings.ScreenMargin, scale), width, height);
|
|
|
|
Assert.Equal(expected.X, bounds.Left);
|
|
Assert.Equal(expected.Y, bounds.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void At_the_cursor_the_window_lands_next_to_it()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings
|
|
{
|
|
PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16,
|
|
};
|
|
|
|
using var popup = Popup.Create(settings);
|
|
popup.ViewModel.ShortName = "RU";
|
|
|
|
// The place is computed from where the cursor was at the moment of
|
|
// the show. If it was moving right then, show it once more
|
|
PopupWindowNative.Point before = default;
|
|
|
|
for (int attempt = 0; attempt < 10; attempt++)
|
|
{
|
|
before = PopupWindowNative.GetCursorPosition();
|
|
popup.Window.ShowPopup();
|
|
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
|
|
|
|
if (before.X == after.X && before.Y == after.Y)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (attempt == 9)
|
|
{
|
|
Assert.Skip("The cursor kept moving the whole time");
|
|
}
|
|
}
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
|
double scale = PopupWindowNative.GetScaleAt(before);
|
|
|
|
PopupWindowNative.Point expected = PopupLayout.NearAnchor(
|
|
PopupLayout.AsAnchor(before),
|
|
AnchorSide.BottomRight,
|
|
PopupLayout.ToPixels(settings.CursorOffset, scale),
|
|
bounds.Right - bounds.Left,
|
|
bounds.Bottom - bounds.Top);
|
|
|
|
Assert.Equal(expected.X, bounds.Left);
|
|
Assert.Equal(expected.Y, bounds.Top);
|
|
});
|
|
}
|
|
|
|
// There is no caret in the test environment, and the tooltip has to fall
|
|
// back to the cursor
|
|
[Fact]
|
|
public void Without_a_caret_the_window_lands_at_the_cursor()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings
|
|
{
|
|
PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8,
|
|
};
|
|
|
|
using var popup = Popup.Create(settings);
|
|
popup.ViewModel.ShortName = "RU";
|
|
|
|
PopupWindowNative.Point before = default;
|
|
|
|
for (int attempt = 0; attempt < 10; attempt++)
|
|
{
|
|
if (CaretNative.TryGetCaretRect() is not null)
|
|
{
|
|
Assert.Skip("The foreground window reported a caret of its own");
|
|
}
|
|
|
|
before = PopupWindowNative.GetCursorPosition();
|
|
popup.Window.ShowPopup();
|
|
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
|
|
|
|
if (before.X == after.X && before.Y == after.Y)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (attempt == 9)
|
|
{
|
|
Assert.Skip("The cursor kept moving the whole time");
|
|
}
|
|
}
|
|
|
|
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
|
double scale = PopupWindowNative.GetScaleAt(before);
|
|
|
|
PopupWindowNative.Point expected = PopupLayout.NearAnchor(
|
|
PopupLayout.AsAnchor(before),
|
|
settings.CaretSide,
|
|
PopupLayout.ToPixels(settings.CaretOffset, scale),
|
|
bounds.Right - bounds.Left,
|
|
bounds.Bottom - bounds.Top);
|
|
|
|
Assert.Equal(expected.X, bounds.Left);
|
|
Assert.Equal(expected.Y, bounds.Top);
|
|
});
|
|
}
|
|
|
|
// The window size equals the size of the text: the tooltip has no frame
|
|
[Fact]
|
|
public void The_window_size_follows_the_size_of_the_caption()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var small = new AppSettings { FontSize = 12, PlacementMode = PopupPlacementMode.FixedPoint };
|
|
var large = new AppSettings { FontSize = 48, PlacementMode = PopupPlacementMode.FixedPoint };
|
|
|
|
using var smallPopup = Popup.Create(small);
|
|
using var largePopup = Popup.Create(large);
|
|
|
|
smallPopup.ViewModel.ShortName = "RU";
|
|
largePopup.ViewModel.ShortName = "RU";
|
|
|
|
smallPopup.Window.ShowPopup();
|
|
largePopup.Window.ShowPopup();
|
|
|
|
PopupWindowNative.Rect smallBounds = WindowPlacementNative.TryGetBounds(smallPopup.Handle)!.Value;
|
|
PopupWindowNative.Rect largeBounds = WindowPlacementNative.TryGetBounds(largePopup.Handle)!.Value;
|
|
|
|
Assert.True(largeBounds.Right - largeBounds.Left > smallBounds.Right - smallBounds.Left);
|
|
Assert.True(largeBounds.Bottom - largeBounds.Top > smallBounds.Bottom - smallBounds.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Showing_again_moves_the_window_to_its_new_place()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
var settings = new AppSettings
|
|
{
|
|
PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft,
|
|
};
|
|
|
|
using var popup = Popup.Create(settings);
|
|
popup.ViewModel.ShortName = "RU";
|
|
popup.Window.ShowPopup();
|
|
|
|
PopupWindowNative.Rect topLeft = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
|
|
|
settings.ScreenPosition = ScreenPosition.BottomRight;
|
|
popup.Window.ShowPopup();
|
|
|
|
PopupWindowNative.Rect bottomRight = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
|
|
|
Assert.True(bottomRight.Left > topLeft.Left);
|
|
Assert.True(bottomRight.Top > topLeft.Top);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void A_hidden_window_stays_alive()
|
|
{
|
|
Sta.Run(() =>
|
|
{
|
|
using var popup = Popup.Create(new AppSettings());
|
|
|
|
popup.Window.ShowPopup();
|
|
popup.Window.Hide();
|
|
|
|
Assert.False(popup.Window.IsVisible);
|
|
Assert.NotEqual(IntPtr.Zero, popup.Handle);
|
|
|
|
popup.Window.ShowPopup();
|
|
Assert.True(popup.Window.IsVisible);
|
|
});
|
|
}
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
|
|
|
/// <summary>The tooltip window together with everything it needs to work.</summary>
|
|
private sealed class Popup : IDisposable
|
|
{
|
|
private Popup(LayoutPopupWindow window, LayoutPopupViewModel viewModel)
|
|
{
|
|
Window = window;
|
|
ViewModel = viewModel;
|
|
Handle = new WindowInteropHelper(window).Handle;
|
|
}
|
|
|
|
internal LayoutPopupWindow Window { get; }
|
|
|
|
internal LayoutPopupViewModel ViewModel { get; }
|
|
|
|
internal IntPtr Handle { get; }
|
|
|
|
internal static Popup Create(AppSettings settings)
|
|
{
|
|
var viewModel = new LayoutPopupViewModel(settings);
|
|
return new Popup(new LayoutPopupWindow(viewModel, settings), viewModel);
|
|
}
|
|
|
|
public void Dispose() => Window.Close();
|
|
}
|
|
}
|