Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quick start window improvements #69

Merged
merged 6 commits into from
Nov 24, 2023
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ available!
- The theme came from https://github.com/AngryCarrot789/WPFDarkTheme.
- Some icons came from https://github.com/microsoft/fluentui-system-icons
(search engine is at https://fluenticons.co/).
- The original grab cursor came from
https://www.svgrepo.com/svg/372329/cursor-hand-grab. The one I'm using is
modified to better support high DPI.
1 change: 1 addition & 0 deletions Tql.App/Images.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ private static DrawingImage LoadImage(string resourceName, Color? color)
public static readonly DrawingImage PinOff = GetImage("Pin Off.svg");
public static readonly DrawingImage NuGet = GetImage("NuGet.svg");
public static readonly DrawingImage CheckmarkCircle = GetImage("Checkmark Circle.svg");
public static readonly DrawingImage Grab = GetImage("Grab.svg");
}
21 changes: 18 additions & 3 deletions Tql.App/MainWindow.WinForms.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,24 @@ private void RepositionScreen()

Left =
(screen.WorkingArea.Left / scaleX) + ((screen.WorkingArea.Width / scaleX) - Width) / 2;
// The window is roughly 100 units in height. Most of the time though, it'll have
// content. Position it slightly above center so that the quick start window

// The window without content is roughly 100 units in height. Most of the time though,
// it'll have content. Position it slightly above center so that the quick start window
// has enough space to point at the configuration icon.
Top = (screen.WorkingArea.Top / scaleY) + ((screen.WorkingArea.Height / scaleY) - 140) / 2;
var top =
(screen.WorkingArea.Top / scaleY) + ((screen.WorkingArea.Height / scaleY) - 140) / 2;

// The window height with content is 416 units. Move the window up somewhat if it
// would not fit in the work area.
var maxTop = (screen.WorkingArea.Bottom / scaleY) - (416 + 10);
if (top > maxTop)
top = maxTop;

// And ensure it doesn't go out of the screen at the top.
var minTop = (screen.WorkingArea.Top / scaleY) + 10;
if (top < minTop)
top = minTop;

Top = top;
}
}
43 changes: 43 additions & 0 deletions Tql.App/QuickStart/QuickStartAdorner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace Tql.App.QuickStart;

internal class QuickStartAdorner : Adorner
{
public const int Distance = 6;

private readonly Border _border;

protected override int VisualChildrenCount => 1;

public QuickStartAdorner(UIElement adornedElement)
: base(adornedElement)
{
_border = new Border
{
BorderBrush = new SolidColorBrush(Color.FromRgb(87, 157, 255)),
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(4),
Margin = new Thickness(-Distance)
};

AddVisualChild(_border);
}

protected override Size MeasureOverride(Size constraint)
{
_border.Measure(constraint);

return base.MeasureOverride(constraint);
}

protected override Size ArrangeOverride(Size finalSize)
{
_border.Arrange(new Rect(finalSize));

return base.ArrangeOverride(finalSize);
}

protected override Visual GetVisualChild(int index)
{
return _border;
}
}
200 changes: 159 additions & 41 deletions Tql.App/QuickStart/QuickStartManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class QuickStartManager
private readonly UI _ui;
private QuickStartDto _state;
private QuickStartWindow? _window;
private bool _repositioning;

public QuickStartDto State
{
Expand Down Expand Up @@ -51,6 +52,15 @@ public IDisposable Show(
Window.GetWindow(owner) ?? throw new InvalidOperationException("Cannot resolve window");
var ownerIsControl = ownerWindow != owner;

var adorner = default(Adorner);

if (ownerIsControl)
{
adorner = new QuickStartAdorner(owner);

AdornerLayer.GetAdornerLayer(owner)!.Add(adorner);
}

_window = new QuickStartWindow(_ui)
{
DataContext = popup,
Expand Down Expand Up @@ -92,12 +102,15 @@ public IDisposable Show(

_window.Owner = ownerWindow;

_window.SourceInitialized += (_, _) => UpdateWindowLocation(owner, popup, mode);
ownerWindow.LocationChanged += (_, _) => UpdateWindowLocation(owner, popup, mode);
ownerWindow.SizeChanged += (_, _) => UpdateWindowLocation(owner, popup, mode);
_window.SourceInitialized += (_, _) => UpdateWindowLocation(owner, popup, mode, true);
ownerWindow.LocationChanged += (_, _) => UpdateWindowLocation(owner, popup, mode, false);
ownerWindow.SizeChanged += (_, _) => UpdateWindowLocation(owner, popup, mode, false);

_window.Closed += (s, _) =>
{
if (adorner != null)
AdornerLayer.GetAdornerLayer(owner)!.Remove(adorner);

if (_window == (Window?)s)
_window = null;
};
Expand Down Expand Up @@ -125,12 +138,32 @@ public IDisposable Show(
private void UpdateWindowLocation(
FrameworkElement owner,
QuickStartPopup popup,
QuickStartPopupMode mode = QuickStartPopupMode.None
QuickStartPopupMode mode,
bool initialUpdate
)
{
if (_window == null || !ReferenceEquals(_window.DataContext, popup))
if (_repositioning || _window == null || !ReferenceEquals(_window.DataContext, popup))
return;

_repositioning = true;

try
{
DoUpdateWindowLocation(owner, popup, mode, initialUpdate);
}
finally
{
_repositioning = false;
}
}

private void DoUpdateWindowLocation(
FrameworkElement owner,
QuickStartPopup popup,
QuickStartPopupMode mode,
bool initialUpdate
)
{
var ownerWindow =
Window.GetWindow(owner) ?? throw new InvalidOperationException("Cannot resolve window");
var ownerIsControl = ownerWindow != owner;
Expand All @@ -143,7 +176,8 @@ private void UpdateWindowLocation(
var scaleY = source.CompositionTarget!.TransformToDevice.M22;

var ownerBounds = ScaleBounds(GetOwnerBounds());
var windowSize = ScaleSize(new Size(_window.ActualWidth, _window.ActualHeight));
var ownerWindowBounds = GetOwnerWindowBounds();
var windowSize = ScaleSize(new Size(_window!.ActualWidth, _window.ActualHeight));
var showOnScreen = ShowOnScreenManager.Create(_settings.ShowOnScreen);
var screen = showOnScreen.GetScreen();

Expand All @@ -169,11 +203,26 @@ private void UpdateWindowLocation(
// Ensure the quick start window is always this amount of pixels from
// the owner window and the screen border.
var edgeDistance = 10 * scaleX;
var repositionEdge = Edge.None;

if (ownerIsControl)
{
x -= windowSize.Width / 2;
y = ownerBounds.Bottom + edgeDistance;
y = ownerBounds.Bottom + (QuickStartAdorner.Distance - 1) * scaleY;

// Ensure that the quick start window is visible.
var bottom = y + windowSize.Height + edgeDistance;
var overhang = bottom - screen.WorkingArea.Bottom;
if (overhang > 0)
{
var availableSpace =
ownerWindowBounds.Top - (screen.WorkingArea.Top + edgeDistance);
if (overhang > availableSpace)
overhang = availableSpace;

y -= overhang;
ownerWindow.Top -= overhang / scaleY;
}
}
else if (mode.HasFlag(QuickStartPopupMode.Modal))
{
Expand All @@ -183,27 +232,37 @@ private void UpdateWindowLocation(
}
else
{
var maxEdge = 4;

// Prevent the quick start window from showing at the top of
// bottom if it wouldn't fit.
var requiredSpace = (windowSize.Height + ownerWindowBounds.Height + edgeDistance * 2);
if (requiredSpace > screen.WorkingArea.Height)
maxEdge = 2;

// Put the quick start window to the side, at a random side.
switch (random.Next(0, 4))
repositionEdge = (Edge)random.Next(0, maxEdge);

switch (repositionEdge)
{
case 0: // North
x += xOffset;
y = ownerBounds.Top - (windowSize.Height + edgeDistance);
case Edge.Left:
x = ownerBounds.Left - (windowSize.Width + edgeDistance);
y += yOffset;
break;

case 1: // East
case Edge.Right:
x = ownerBounds.Right + edgeDistance;
y += yOffset;
break;

case 2: // South
case Edge.Top:
x += xOffset;
y = ownerBounds.Bottom + edgeDistance;
y = ownerBounds.Top - (windowSize.Height + edgeDistance);
break;

case 3: // West
x = ownerBounds.Left - (windowSize.Width + edgeDistance);
y += yOffset;
case Edge.Bottom:
x += xOffset;
y = ownerBounds.Bottom + edgeDistance;
break;

default:
Expand All @@ -214,44 +273,94 @@ private void UpdateWindowLocation(
if (!ownerIsControl)
{
// Make sure the quick start window is visible on the screen.
if (x < screen.Bounds.Left + edgeDistance)
x = screen.Bounds.Left + edgeDistance;
else if (x + windowSize.Width > screen.Bounds.Right - edgeDistance)
x = screen.Bounds.Right - edgeDistance - windowSize.Width;
if (y < screen.Bounds.Top + edgeDistance)
y = screen.Bounds.Top + edgeDistance;
else if (y + windowSize.Height > screen.Bounds.Bottom - edgeDistance)
y = screen.Bounds.Bottom - edgeDistance - windowSize.Height;
if (x < screen.WorkingArea.Left + edgeDistance)
x = screen.WorkingArea.Left + edgeDistance;
else if (x + windowSize.Width > screen.WorkingArea.Right - edgeDistance)
x = screen.WorkingArea.Right - edgeDistance - windowSize.Width;
if (y < screen.WorkingArea.Top + edgeDistance)
y = screen.WorkingArea.Top + edgeDistance;
else if (y + windowSize.Height > screen.WorkingArea.Bottom - edgeDistance)
y = screen.WorkingArea.Bottom - edgeDistance - windowSize.Height;
}

// Minimize overlap with the owner window.
if (initialUpdate)
{
switch (repositionEdge)
{
case Edge.Left:
var right = x + windowSize.Width;
var overhang = right - ownerWindowBounds.Left;
if (overhang > 0)
{
var availableSpace =
screen.WorkingArea.Right - ownerWindowBounds.Right - edgeDistance;
ownerWindow.Left += Math.Min(overhang, availableSpace);
}
break;

case Edge.Right:
overhang = ownerWindowBounds.Right - x;
if (overhang > 0)
{
var availableSpace =
ownerWindowBounds.Left - screen.WorkingArea.Left - edgeDistance;
ownerWindow.Left -= Math.Min(overhang, availableSpace);
}
break;

case Edge.Top:
var bottom = y + windowSize.Height;
overhang = bottom - ownerWindowBounds.Top;
if (overhang > 0)
{
var availableSpace =
screen.WorkingArea.Bottom - ownerWindowBounds.Bottom - edgeDistance;
ownerWindow.Top += Math.Min(overhang, availableSpace);
}
break;

case Edge.Bottom:
overhang = ownerWindowBounds.Bottom - y;
if (overhang > 0)
{
var availableSpace =
ownerWindowBounds.Top - screen.WorkingArea.Top - edgeDistance;
ownerWindow.Top -= Math.Min(overhang, availableSpace);
}
break;
}
}

_window.Left = x / scaleX;
_window.Top = y / scaleY;

Rect GetOwnerBounds()
{
if (ownerIsControl)
{
var location = owner.TransformToAncestor(ownerWindow).Transform(new Point());

var clientRect = WindowInterop.GetClientRect(
new WindowInteropHelper(ownerWindow).Handle
);

return new Rect(
ownerWindow.Left + (clientRect.Left / scaleX) + location.X,
ownerWindow.Top + (clientRect.Top / scaleY) + location.Y,
owner.ActualWidth,
owner.ActualHeight
);
}
if (!ownerIsControl)
return GetOwnerWindowBounds();

var location = owner.TransformToAncestor(ownerWindow).Transform(new Point());

var clientRect = WindowInterop.GetClientRect(
new WindowInteropHelper(ownerWindow).Handle
);

return new Rect(
ownerWindow.Left + (clientRect.Left / scaleX) + location.X,
ownerWindow.Top + (clientRect.Top / scaleY) + location.Y,
owner.ActualWidth,
owner.ActualHeight
);
}

Rect GetOwnerWindowBounds() =>
new(
ownerWindow.Left,
ownerWindow.Top,
ownerWindow.ActualWidth,
ownerWindow.ActualHeight
);
}

Rect ScaleBounds(Rect bounds) =>
new(
Expand All @@ -276,4 +385,13 @@ public void Dispose()
window.Close();
}
}

private enum Edge
{
None = -1,
Left = 0,
Right,
Top,
Bottom
}
}
Loading