Files
mmd-pdf/Controls/DialogChrome.cs

277 lines
15 KiB
C#

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace MmdPdf
{
// Chrome for modal dialog windows: Configure (borderless window setup), Frame (the rounded card +
// title bar + grain), and BuildTitleBar (the MmdPdf wordmark + red close button).
internal static class DialogChrome
{
// Keep generated dialog captions on the same close mark as the main window.
// E711 renders noticeably smaller inside the 18x16 Win98 caption face; E8BB is
// the shared chrome glyph used by the main title bar and fills that face correctly.
public const string CloseGlyph = "";
// Brush from the owner (then app) resources, with a safe fallback so the helper never throws.
private static Brush Brush(Window? owner, string key, Brush fallback)
=> (owner?.TryFindResource(key) ?? Application.Current?.TryFindResource(key)) as Brush ?? fallback;
private static T Value<T>(Window? owner, string key, T fallback)
=> (owner?.TryFindResource(key) ?? Application.Current?.TryFindResource(key)) is T value ? value : fallback;
// Builds the title bar.
// win - the window being chromed (used for DragMove on the whole bar)
// owner - supplies the themed brushes + the ChromeCloseButton style (pass the window's owner)
// fullTitle - the complete title, e.g. "MmdPdf - Transform"; the "MmdPdf" part becomes the
// wordmark and the remainder (" - Transform") is rendered in the courier title font
// onClose - invoked when the red close button is clicked (e.g. set a result then Close())
public static Border BuildTitleBar(Window win, Window? owner, string? fullTitle, Action onClose)
{
// Transparent (not null) background so the WHOLE bar is hit-testable and acts as a drag handle.
bool caption = Value(owner, "UseDialogCaption", false);
var bar = new Border
{
Background = caption ? Brush(owner, "TitleBarBrush", Brushes.Navy) : Brushes.Transparent,
SnapsToDevicePixels = true,
UseLayoutRounding = true
};
bar.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) win.DragMove(); };
var grid = new Grid
{
// KillerNotes uses the shared title-bar inset for its dialog caption too. In
// particular, the 2px top inset keeps the 16px caption button centered in the
// 20px classic band instead of riding against its upper edge.
Margin = caption
? Value(owner, "TitleBarPadding", new Thickness(4, 2, 0, 0))
: new Thickness(0)
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var wordmark = UiKit.WordmarkFont;
var wordmarkPdf = UiKit.WordmarkFontPdf;
// Build the wordmark row. A DropShadowEffect applied directly to text rasterizes it and
// disables ClearType, which reads as blurry. So we LAYER it instead: a blurred black duplicate
// sits behind a crisp, effect-free copy - soft shadow, sharp text. `shadow` paints the duplicate.
StackPanel BuildWordmark(bool shadow)
{
var sp = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
Brush primary = shadow ? Brushes.Black : Brush(owner, "TextBrush", Brushes.White);
Brush logo = shadow ? Brushes.Black : Brush(owner, "AccentLogo", Brushes.LimeGreen);
Brush secondary = shadow ? Brushes.Black : Brush(owner, "MutedTextBrush", Brushes.Gray);
int kp = fullTitle?.IndexOf("MmdPdf", StringComparison.Ordinal) ?? -1;
if (kp >= 0)
{
// MMD + PDF in one TextBlock so the two sizes share a baseline (cohesive wordmark).
var logoTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
logoTb.Inlines.Add(new System.Windows.Documents.Run("MMD ") { FontFamily = wordmark, FontWeight = FontWeights.Normal, FontSize = 16, Foreground = primary });
logoTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = wordmarkPdf, FontWeight = FontWeights.Bold, FontSize = 20.8, Foreground = logo });
sp.Children.Add(logoTb);
string after = fullTitle![(kp + "MmdPdf".Length)..];
if (!string.IsNullOrEmpty(after))
sp.Children.Add(new TextBlock { Text = after, FontFamily = UiKit.MonoFont, FontSize = 14, Foreground = secondary, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 1, 0, 0) });
}
else
{
sp.Children.Add(new TextBlock { Text = fullTitle ?? "", FontFamily = UiKit.MonoFont, FontSize = 14, Foreground = primary, VerticalAlignment = VerticalAlignment.Center });
}
return sp;
}
var title = new Grid { Margin = caption ? new Thickness(0) : new Thickness(16, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
if (caption)
{
title.Children.Add(new TextBlock
{
Text = fullTitle ?? "MmdPdf", FontFamily = Value(owner, "ChromeFontFamily", new FontFamily("Tahoma")),
FontSize = 11, FontWeight = FontWeights.Bold,
Foreground = Brush(owner, "ChromeTextBrush", Brushes.White), VerticalAlignment = VerticalAlignment.Center
});
}
else
{
var shadowLayer = BuildWordmark(true);
shadowLayer.Opacity = 0.5;
shadowLayer.Effect = new BlurEffect { Radius = 2 };
shadowLayer.RenderTransform = new TranslateTransform(0.7, 1.2);
title.Children.Add(shadowLayer);
title.Children.Add(BuildWordmark(false));
}
Grid.SetColumn(title, 0);
grid.Children.Add(title);
// The close glyph and its complete raised/pressed face live in ChromeCloseButton.
// Supplying another glyph/font/background here was overriding that canonical style and
// produced the off-centre X and the exposed title-bar pixel seen in classic dialogs.
var close = new Button
{
HorizontalAlignment = HorizontalAlignment.Right,
// The hover face is part of the card's top-right corner. Centering a 26px
// button in the 40px caption left a visible 7px strip above it.
VerticalAlignment = VerticalAlignment.Top,
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
FocusVisualStyle = null,
SnapsToDevicePixels = true,
UseLayoutRounding = true
};
if (owner?.TryFindResource("ChromeCloseButton") is Style chromeClose)
{
close.Style = chromeClose;
}
else
{
close.Content = CloseGlyph;
close.FontFamily = UiKit.IconFont;
close.FontSize = 10;
close.Width = 46; close.Height = 36;
close.Foreground = Brush(owner, "DangerRed", Brushes.Red);
close.Background = Brushes.Transparent;
close.BorderThickness = new Thickness(0);
close.Cursor = Cursors.Hand;
}
close.SetResourceReference(FrameworkElement.WidthProperty, "DialogCloseWidth");
close.SetResourceReference(FrameworkElement.HeightProperty, "DialogCloseHeight");
close.SetResourceReference(FrameworkElement.MarginProperty, "DialogCaptionButtonsMargin");
// Resizable borderless dialogs use WindowChrome. Without this exemption its resize
// band wins the top-right hit test, turning the close button into a resize handle.
System.Windows.Shell.WindowChrome.SetIsHitTestVisibleInChrome(close, true);
// Get the click before the caption's DragMove handler starts its modal mouse loop.
close.PreviewMouseLeftButtonDown += (_, e) => { e.Handled = true; onClose(); };
Grid.SetColumn(close, 1);
grid.Children.Add(close);
bar.Child = grid;
return bar;
}
// Borderless transparent window setup shared by every dialog.
public static void Configure(Window win, Window? owner, bool resizable = false, bool fade = true)
{
win.Owner = owner;
win.WindowStyle = WindowStyle.None;
win.AllowsTransparency = true;
win.Background = Brushes.Transparent;
win.ResizeMode = resizable ? ResizeMode.CanResize : ResizeMode.NoResize;
win.WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen;
win.FontFamily = UiKit.UiFont;
TextOptions.SetTextFormattingMode(win, TextFormattingMode.Display);
TextOptions.SetTextRenderingMode(win, TextRenderingMode.Grayscale);
if (fade) WindowFx.EnableFadeClose(win);
}
private static Border FrameRing(Window? owner, string brushKey, string thicknessKey, string? marginKey = null)
{
var ring = new Border
{
IsHitTestVisible = false,
BorderBrush = Brush(owner, brushKey, Brushes.Transparent),
BorderThickness = Value(owner, thicknessKey, new Thickness(0))
};
if (marginKey != null)
ring.Margin = Value(owner, marginKey, new Thickness(0));
return ring;
}
private static UIElement WindowFrame(Window? owner)
{
var frame = new Grid { IsHitTestVisible = false };
frame.Children.Add(FrameRing(owner, "WindowFrameBrush", "DialogWindowFrameThickness", "WindowFrameMargin"));
frame.Children.Add(FrameRing(owner, "FrameInnerLightBrush", "FrameInnerLightThickness", "FrameInnerMargin"));
frame.Children.Add(FrameRing(owner, "FrameInnerDarkBrush", "FrameInnerDarkThickness", "FrameInnerMargin"));
frame.Children.Add(FrameRing(owner, "FrameOuterLightBrush", "FrameOuterLightThickness"));
frame.Children.Add(FrameRing(owner, "FrameOuterDarkBrush", "FrameOuterDarkThickness"));
return frame;
}
internal static UIElement WrapContent(Window? owner, UIElement content)
{
var host = new Grid { Margin = Value(owner, "DialogHaloMargin", new Thickness(12)) };
var radius = Value(owner, "WindowCornerRadius", new CornerRadius(7));
host.Children.Add(new Border
{
Background = Brush(owner, "WindowFrameBrush", UiKit.Brush("MenuBackgroundBrush")),
CornerRadius = radius,
IsHitTestVisible = false,
Effect = UiKit.ShadowDialog()
});
var card = new Grid();
card.Children.Add(new Border
{
Background = Brush(owner, "BackgroundBrush", UiKit.Brush("BackgroundBrush")),
CornerRadius = radius,
Margin = Value(owner, "DialogWindowFramePadding", new Thickness(0)),
Child = content
});
card.Children.Add(WindowFrame(owner));
// The 1px window outline every dialog was missing: same DialogFrameBrush the file
// picker draws (defaults to AppBorderBrush, the main window's DWM border tone).
card.Children.Add(new Border
{
BorderBrush = Brush(owner, "DialogFrameBrush", UiKit.Brush("MenuBorderBrush")),
BorderThickness = Value(owner, "DialogFrameThickness", new Thickness(1)),
CornerRadius = radius,
IsHitTestVisible = false,
});
host.Children.Add(card);
return host;
}
// Standard dialog: content is inset from the same five-layer frame used by KillerNotes.
public static UIElement Frame(Window win, Window? owner, string title, Action onClose, UIElement body)
{
win.KeyDown += (_, e) => { if (e.Key == Key.Escape) { e.Handled = true; onClose(); } };
var card = new Border
{
// Print Preview already used BackgroundBrush directly. The shared frame used
// MenuBackgroundBrush, making every other generated window a different color.
Background = Brush(owner, "BackgroundBrush", UiKit.Brush("BackgroundBrush")),
CornerRadius = UiKit.RadWindow,
Margin = Value(owner, "WindowFramePadding", new Thickness(0))
};
var root = new DockPanel();
var titleBar = BuildTitleBar(win, owner, title, onClose);
titleBar.Height = Value(owner, "DialogTitleBarHeight", 40.0);
DockPanel.SetDock(titleBar, Dock.Top);
root.Children.Add(titleBar);
root.Children.Add(body);
var grain = (owner as MainWindow)?.GrainTexture;
if (grain != null)
{
var grid = new Grid();
double op = Application.Current?.Resources["GrainOpacity"] is double go ? go : 0.05;
grid.Children.Add(new Border
{
CornerRadius = UiKit.RadWindow, IsHitTestVisible = false, Opacity = op,
Background = new ImageBrush(grain) { TileMode = TileMode.Tile, ViewportUnits = BrushMappingMode.Absolute, Viewport = new Rect(0, 0, 256, 256), Stretch = Stretch.None }
});
grid.Children.Add(root);
card.Child = grid;
}
else
{
var grid = new Grid();
grid.Children.Add(root);
card.Child = grid;
}
var framedContent = card.Child!;
card.Child = null;
return WrapContent(owner, framedContent);
}
internal static void AddBevels(Grid grid, Window? owner)
{
grid.Children.Add(new Border { IsHitTestVisible = false, BorderBrush = Brush(owner, "BevelLightBrush", Brushes.Transparent), BorderThickness = Value(owner, "BevelLightThickness", new Thickness(0)) });
grid.Children.Add(new Border { IsHitTestVisible = false, BorderBrush = Brush(owner, "BevelDarkBrush", Brushes.Transparent), BorderThickness = Value(owner, "BevelDarkThickness", new Thickness(0)) });
}
}
}