Skip to content

Commit

Permalink
Merge pull request #69 from TQLApp/quick-start-window
Browse files Browse the repository at this point in the history
Quick start window improvements
  • Loading branch information
pvginkel authored Nov 24, 2023
2 parents d0d33ee + 2d8e2a3 commit 2838347
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 58 deletions.
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

0 comments on commit 2838347

Please sign in to comment.