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
11 changes: 8 additions & 3 deletions src/SQLBI.Whiteboard/Export/ExportWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Export"
Width="1000"
Height="720"
SizeToContent="Height"
MinWidth="760"
MinHeight="560"
MinHeight="420"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize"
ShowInTaskbar="False"
Background="{DynamicResource ToolbarBackgroundBrush}"
Stylus.IsPressAndHoldEnabled="False"
PreviewKeyDown="Window_PreviewKeyDown"
SourceInitialized="Window_SourceInitialized"
SizeChanged="Window_SizeChanged"
Loaded="Window_Loaded"
Closing="Window_Closing">
<Window.Resources>
Expand Down Expand Up @@ -70,9 +72,12 @@
<ColumnDefinition Width="300" />
</Grid.ColumnDefinitions>

<!-- The options decide the height; the preview takes what they leave,
so a tall board does not make a tall dialog. -->
<Border Background="White"
BorderBrush="{DynamicResource ToolbarBorderBrush}"
BorderThickness="1">
BorderThickness="1"
MaxHeight="{Binding ActualHeight, ElementName=Options}">
<Grid>
<Image x:Name="PreviewImage"
Margin="8"
Expand Down
18 changes: 14 additions & 4 deletions src/SQLBI.Whiteboard/Export/ExportWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,25 @@ public ExportWindow(
_persistSettings = persistSettings;
_content = document.ContentBounds ?? new RectD(0, 0, 1, 1);
InitializeComponent();

// Tall enough for every option to show without scrolling, on any screen
// that has the room; a small screen keeps the scrollbar.
Height = Math.Max(MinHeight, Math.Min(820, SystemParameters.WorkArea.Height - 60));
PopulateOptions();
_suppressChange = false;
Rebuild();
}

// The height follows the options, up to the working area of the monitor the
// board is on; past that the options scroll. A change of format regrows the
// window in place, downward, so it is moved back inside the screen after.
private void Window_SourceInitialized(object sender, EventArgs e) =>
MaxHeight = Math.Max(MinHeight, MonitorStartupPlacement.WorkAreaHeight(this) - 24);

private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.HeightChanged && IsLoaded)
{
MonitorStartupPlacement.KeepWithinWorkArea(this);
}
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Rendered once the window is on screen, so the dialog appears before a
Expand Down
79 changes: 79 additions & 0 deletions src/SQLBI.Whiteboard/MonitorStartupPlacement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using Microsoft.Win32;
using SQLBI.Whiteboard.Core.Settings;

Expand All @@ -22,6 +23,7 @@ internal static class MonitorStartupPlacement
private const uint SwpNoZOrder = 0x0004;
private const uint SwpNoActivate = 0x0010;
private const uint SwpFrameChanged = 0x0020;
private const uint SwpNoSize = 0x0001;

public static IReadOnlyList<DisplayMonitor> Enumerate() =>
EnumerateMonitors()
Expand Down Expand Up @@ -117,6 +119,79 @@ public static void FillCurrentMonitor(Window window)
SwpNoZOrder | SwpNoActivate | SwpFrameChanged);
}

/// <summary>
/// The height of the working area, in this window's device-independent
/// units, of the monitor the window (or, before it has a handle, its owner)
/// is on. A dialog sizes itself against this rather than the primary
/// monitor's, which is the wrong screen whenever the board is on another.
/// </summary>
public static double WorkAreaHeight(Window window)
{
ArgumentNullException.ThrowIfNull(window);

var anchor = new WindowInteropHelper(window).Handle == 0 && window.Owner is { } owner ? owner : window;
if (!TryGetWorkArea(anchor, out var area))
{
return SystemParameters.WorkArea.Height;
}

var scale = VisualTreeHelper.GetDpi(anchor).DpiScaleY;
return (area.Bottom - area.Top) / Math.Max(scale, 0.01);
}

/// <summary>
/// Moves the window up or left as far as needed for the whole of it to sit
/// inside its monitor's working area. Called after a window grows in place,
/// which WPF does downward from wherever it was centred.
/// </summary>
public static void KeepWithinWorkArea(Window window)
{
ArgumentNullException.ThrowIfNull(window);

var windowHandle = new WindowInteropHelper(window).Handle;
if (windowHandle == 0 ||
!TryGetWorkArea(window, out var area) ||
!GetWindowRect(windowHandle, out var bounds))
{
return;
}

var left = Math.Max(area.Left, Math.Min(bounds.Left, area.Right - (bounds.Right - bounds.Left)));
var top = Math.Max(area.Top, Math.Min(bounds.Top, area.Bottom - (bounds.Bottom - bounds.Top)));
if (left != bounds.Left || top != bounds.Top)
{
SetWindowPos(windowHandle, 0, left, top, 0, 0, SwpNoZOrder | SwpNoActivate | SwpNoSize);
}
}

private static bool TryGetWorkArea(Window window, out NativeRectangle area)
{
area = default;
var windowHandle = new WindowInteropHelper(window).Handle;
if (windowHandle == 0)
{
return false;
}

var monitorHandle = MonitorFromWindow(windowHandle, MonitorDefaultToNearest);
if (monitorHandle == 0)
{
return false;
}

var monitorInfo = new NativeMonitorInfo
{
Size = (uint)Marshal.SizeOf<NativeMonitorInfo>(),
};
if (!GetMonitorInfo(monitorHandle, ref monitorInfo))
{
return false;
}

area = monitorInfo.WorkingArea;
return true;
}

private static IReadOnlyList<MonitorDescriptor> EnumerateMonitors()
{
var monitors = new List<MonitorDescriptor>();
Expand Down Expand Up @@ -327,6 +402,10 @@ private static extern bool EnumDisplayDevices(
ref NativeDisplayDevice displayDevice,
uint flags);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(nint windowHandle, out NativeRectangle rectangle);

[DllImport("user32.dll")]
private static extern nint MonitorFromWindow(
nint windowHandle,
Expand Down