using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Shapes; using KillerPDF.Services; namespace KillerPDF { // ============================================================ // Themed dialog - replaces MessageBox for dark-UI consistency // ============================================================ internal static class KillerDialog { // Pulls the current theme brush at call time so dialogs respect light/dark/HC themes. private static SolidColorBrush R(string key) => (SolidColorBrush)Application.Current.Resources[key]; private static string L(string key, string fallback) => Application.Current.TryFindResource(key) as string ?? fallback; // Carries the checkbox state of the last Show() call back to ShowWithCheckbox. Dialogs are // modal and UI-thread only, so a shared field is safe and avoids a duplicate dialog body. private static bool _lastCheckboxChecked; #pragma warning disable IDE0060 // image intentionally kept for API parity with MessageBox; not yet rendered public static MessageBoxResult Show( Window? owner, string message, string title = "KillerPDF", MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None, bool fadeClose = true, string? checkboxText = null, MessageBoxResult? defaultResult = null) #pragma warning restore IDE0060 { var result = MessageBoxResult.OK; bool boxChecked = false; var win = new Window { Title = title, Width = 380, SizeToContent = SizeToContent.Height }; DialogChrome.Configure(win, owner, fade: fadeClose); var outerBorder = new Border { Background = R("MenuBackgroundBrush"), BorderBrush = UiKit.Brush("DialogFrameBrush"), BorderThickness = Application.Current.TryFindResource("DialogFrameThickness") is Thickness dft ? dft : new Thickness(1), Padding = Application.Current.TryFindResource("DialogFramePadding") is Thickness dfp ? dfp : new Thickness(0), CornerRadius = UiKit.RadWindow, Margin = Application.Current.TryFindResource("DialogHaloMargin") is Thickness hm ? hm : new Thickness(10), Effect = UiKit.ShadowDialog() }; var root = new StackPanel(); // Title bar var titleBar = new Border { // Transparent so the dialog-wide film grain shows through the title bar too (it sits // over the same BgModal surface, so it still reads as one continuous surface). Background = Application.Current.TryFindResource("UseDialogCaption") is true ? UiKit.Brush("TitleBarBrush") : Brushes.Transparent, Padding = new Thickness(16, 10, 16, 10), CornerRadius = new CornerRadius(5, 5, 0, 0) }; titleBar.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) win.DragMove(); }; // When the title is just "KillerPDF", render it as the main window's wordmark - "Killer" // in the primary text color and "PDF" in the green logo accent, bold, with a soft shadow. if (title == "KillerPDF") { var wm = new StackPanel { Orientation = Orientation.Horizontal }; var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") }); wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 19.5, Foreground = R("AccentLogo") }); wm.Children.Add(wmTb); // No DropShadowEffect on the text - it rasterizes and blurs the wordmark. Kept crisp. titleBar.Child = wm; } else { titleBar.Child = new TextBlock { Text = title, Foreground = R("PrimaryBrush"), FontWeight = FontWeights.Bold, // blue title -> bold FontSize = 14, FontFamily = UiKit.MonoFont }; } if (Application.Current.TryFindResource("UseDialogCaption") is true) titleBar = DialogChrome.BuildTitleBar(win, owner, title, () => { result = MessageBoxResult.Cancel; win.Close(); }); titleBar.Height = Application.Current.TryFindResource("DialogTitleBarHeight") is double titleHeight ? titleHeight : double.NaN; root.Children.Add(titleBar); // Message var msgBorder = new Border { Padding = new Thickness(20, 16, 20, 8), Child = new TextBlock { Text = message, Foreground = R("TextBrush"), FontSize = 13, TextWrapping = TextWrapping.Wrap } }; root.Children.Add(msgBorder); // Optional checkbox (e.g. "Remember my choice"). Extra top padding sets it apart from the message. if (checkboxText is not null) { var chk = UiKit.CheckBox(checkboxText); chk.Margin = new Thickness(20, 10, 20, 4); chk.Checked += (_, _2) => boxChecked = true; chk.Unchecked += (_, _2) => boxChecked = false; root.Children.Add(chk); } // Buttons var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; // Build a minimal ControlTemplate so Background binds correctly and // WPF's default blue hover chrome can't override our colors. Button MakeBtn(string label, MessageBoxResult res, bool accent = false) { // Enter triggers the primary action. Normally that's the accent button; a caller can // override which button is the default (e.g. the quit prompt makes the safe "No" the // default), and the accent highlight follows so the Enter target is obvious. bool isDefault = defaultResult is MessageBoxResult dr ? res == dr : accent; // Shared themed button (UiKit.Make) so this dialog matches the print dialog et al. var btn = UiKit.Make(label, isDefault); btn.Margin = new Thickness(8, 0, 0, 0); btn.IsDefault = isDefault; btn.IsCancel = res == MessageBoxResult.Cancel; // Esc triggers Cancel where there is one btn.Click += (_, _2) => { result = res; win.Close(); }; return btn; } switch (buttons) { case MessageBoxButton.OK: btnPanel.Children.Add(MakeBtn(L("Str_Btn_OK", "OK"), MessageBoxResult.OK, accent: true)); break; case MessageBoxButton.OKCancel: btnPanel.Children.Add(MakeBtn(L("Str_Btn_OK", "OK"), MessageBoxResult.OK, accent: true)); btnPanel.Children.Add(MakeBtn(L("Str_Btn_Cancel", "Cancel"), MessageBoxResult.Cancel)); break; case MessageBoxButton.YesNo: btnPanel.Children.Add(MakeBtn(L("Str_Btn_Yes", "Yes"), MessageBoxResult.Yes, accent: true)); btnPanel.Children.Add(MakeBtn(L("Str_Btn_No", "No"), MessageBoxResult.No)); break; case MessageBoxButton.YesNoCancel: btnPanel.Children.Add(MakeBtn(L("Str_Btn_Yes", "Yes"), MessageBoxResult.Yes, accent: true)); btnPanel.Children.Add(MakeBtn(L("Str_Btn_No", "No"), MessageBoxResult.No)); btnPanel.Children.Add(MakeBtn(L("Str_Btn_Cancel", "Cancel"), MessageBoxResult.Cancel)); break; } root.Children.Add(new Border { Padding = new Thickness(16, 8, 16, 16), Child = btnPanel }); // Paint the same film-grain texture the app's panels use, behind the content, so the // dialog reads as part of the same surface family instead of a flat box. var contentGrid = new Grid(); var grain = (owner as MainWindow)?.GrainTexture; if (grain is not null) { double grainOpacity = Application.Current.Resources["GrainOpacity"] is double go ? go : 0.05; contentGrid.Children.Add(new Border { CornerRadius = new CornerRadius(6), IsHitTestVisible = false, Opacity = grainOpacity, Background = new System.Windows.Media.ImageBrush(grain) { TileMode = System.Windows.Media.TileMode.Tile, ViewportUnits = System.Windows.Media.BrushMappingMode.Absolute, Viewport = new Rect(0, 0, 256, 256), Stretch = System.Windows.Media.Stretch.None } }); } contentGrid.Children.Add(root); win.Content = DialogChrome.WrapContent(owner, contentGrid); win.ShowDialog(); _lastCheckboxChecked = boxChecked; return result; } /// /// Like but with a custom set of buttons. Returns the index of the clicked /// button, or -1 if the dialog was closed without a choice. The button at /// is rendered as the primary (accent) action. /// public static int ShowChoices( Window? owner, string message, string[] labels, int accentIndex = 0, string title = "KillerPDF") { int result = -1; var win = new Window { Title = title, MinWidth = 380, MaxWidth = 760, SizeToContent = SizeToContent.WidthAndHeight }; DialogChrome.Configure(win, owner, fade: true); var outerBorder = new Border { Background = R("MenuBackgroundBrush"), BorderBrush = UiKit.Brush("DialogFrameBrush"), BorderThickness = Application.Current.TryFindResource("DialogFrameThickness") is Thickness dft ? dft : new Thickness(1), Padding = Application.Current.TryFindResource("DialogFramePadding") is Thickness dfp ? dfp : new Thickness(0), CornerRadius = UiKit.RadWindow, Margin = Application.Current.TryFindResource("DialogHaloMargin") is Thickness hm ? hm : new Thickness(10), Effect = UiKit.ShadowDialog() }; var root = new StackPanel(); var titleBar = new Border { Background = Application.Current.TryFindResource("UseDialogCaption") is true ? UiKit.Brush("TitleBarBrush") : Brushes.Transparent, Padding = new Thickness(16, 10, 16, 10) }; titleBar.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) win.DragMove(); }; if (title == "KillerPDF") { var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") }); wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 19.5, Foreground = R("AccentLogo") }); titleBar.Child = wmTb; } else { titleBar.Child = new TextBlock { Text = title, Foreground = R("PrimaryBrush"), FontWeight = FontWeights.Bold, FontSize = 14, FontFamily = UiKit.MonoFont }; } if (Application.Current.TryFindResource("UseDialogCaption") is true) titleBar = DialogChrome.BuildTitleBar(win, owner, title, () => win.Close()); titleBar.Height = Application.Current.TryFindResource("DialogTitleBarHeight") is double titleHeight ? titleHeight : double.NaN; root.Children.Add(titleBar); root.Children.Add(new Border { Padding = new Thickness(20, 16, 20, 8), Child = new TextBlock { Text = message, Foreground = R("TextBrush"), FontSize = 13, TextWrapping = TextWrapping.Wrap, MaxWidth = 560 } }); var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; for (int i = 0; i < labels.Length; i++) { int idx = i; var btn = UiKit.Make(labels[i], accent: i == accentIndex); btn.Padding = new Thickness(22, 8, 22, 8); btn.MinWidth = 96; btn.Margin = new Thickness(8, 0, 0, 0); btn.Click += (_, _2) => { result = idx; win.Close(); }; btnPanel.Children.Add(btn); } root.Children.Add(new Border { Padding = new Thickness(16, 8, 16, 16), Child = btnPanel }); var contentGrid = new Grid(); var grain = (owner as MainWindow)?.GrainTexture; if (grain is not null) { double grainOpacity = Application.Current.Resources["GrainOpacity"] is double go ? go : 0.05; contentGrid.Children.Add(new Border { CornerRadius = new CornerRadius(6), IsHitTestVisible = false, Opacity = grainOpacity, Background = new ImageBrush(grain) { TileMode = TileMode.Tile, ViewportUnits = BrushMappingMode.Absolute, Viewport = new Rect(0, 0, 256, 256), Stretch = Stretch.None } }); } contentGrid.Children.Add(root); win.Content = DialogChrome.WrapContent(owner, contentGrid); win.ShowDialog(); return result; } /// /// Like but with a "don't warn again" style checkbox between the message and the /// buttons. Returns the button result and the checkbox state. /// // Same dialog as Show(), plus a checkbox (e.g. "Remember my choice"). Delegates to the single // Show() implementation so there is one KillerDialog box, not a duplicate. public static (MessageBoxResult result, bool isChecked) ShowWithCheckbox( Window? owner, string message, string checkboxText, string title = "KillerPDF", MessageBoxButton buttons = MessageBoxButton.OKCancel, MessageBoxResult? defaultResult = null) { var result = Show(owner, message, title, buttons, checkboxText: checkboxText, defaultResult: defaultResult); return (result, _lastCheckboxChecked); } /// /// KillerFind-style quit prompt (family standard): a short question with TWO opt-out /// checkboxes stacked between the message and the buttons - "Close my open tabs" /// (unchecked = session reopens next launch) and "Remember my choice" - plus /// Cancel / Quit buttons where Quit is the accent + Enter default and Esc cancels. /// Returns (confirmed, closeTabsChecked, rememberChecked). /// /// A thin wrapper over since the install prompt needed the /// same two-checkbox shape. Passing check2Initial:false keeps this behavior identical to /// what it was when the body lived here. /// public static (bool confirmed, bool closeTabs, bool remember) ShowQuitPrompt( Window? owner, string message, string closeTabsText, bool closeTabsInitial, string rememberText, string quitLabel, string cancelLabel) => ShowTwoCheckPrompt(owner, message, closeTabsText, closeTabsInitial, rememberText, false, quitLabel, cancelLabel); /// /// The family two-checkbox confirm: a short question, two checkboxes stacked between the /// message and the buttons, and Cancel / confirm buttons where confirm is the accent + /// Enter default and Esc cancels. Used by the quit prompt and by the install prompt /// (desktop shortcut + install for all users). /// public static (bool confirmed, bool check1, bool check2) ShowTwoCheckPrompt( Window? owner, string message, string check1Text, bool check1Initial, string check2Text, bool check2Initial, string confirmLabel, string cancelLabel) { bool confirmed = false; bool closeTabs = check1Initial; bool remember = check2Initial; var win = new Window { Title = "KillerPDF", Width = 380, SizeToContent = SizeToContent.Height }; // fade:false - the app's own fade-out follows immediately on confirm; two fades // back-to-back read as lag (same reasoning as the unsaved-changes prompt). DialogChrome.Configure(win, owner, fade: false); var outerBorder = new Border { Background = R("MenuBackgroundBrush"), BorderBrush = UiKit.Brush("DialogFrameBrush"), BorderThickness = Application.Current.TryFindResource("DialogFrameThickness") is Thickness dft ? dft : new Thickness(1), Padding = Application.Current.TryFindResource("DialogFramePadding") is Thickness dfp ? dfp : new Thickness(0), CornerRadius = UiKit.RadWindow, Margin = Application.Current.TryFindResource("DialogHaloMargin") is Thickness hm ? hm : new Thickness(10), Effect = UiKit.ShadowDialog() }; var root = new StackPanel(); // Title bar: the wordmark, exactly like Show()'s "KillerPDF" branch. var titleBar = new Border { Background = Application.Current.TryFindResource("UseDialogCaption") is true ? UiKit.Brush("TitleBarBrush") : Brushes.Transparent, Padding = new Thickness(16, 10, 16, 10), CornerRadius = new CornerRadius(5, 5, 0, 0) }; titleBar.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) win.DragMove(); }; var wm = new StackPanel { Orientation = Orientation.Horizontal }; var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") }); wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 18, Foreground = R("AccentLogo") }); wm.Children.Add(wmTb); titleBar.Child = wm; if (Application.Current.TryFindResource("UseDialogCaption") is true) titleBar = DialogChrome.BuildTitleBar(win, owner, "KillerPDF", () => win.Close()); titleBar.Height = Application.Current.TryFindResource("DialogTitleBarHeight") is double titleHeight ? titleHeight : double.NaN; root.Children.Add(titleBar); root.Children.Add(new Border { Padding = new Thickness(20, 16, 20, 8), Child = new TextBlock { Text = message, Foreground = R("TextBrush"), FontSize = 13, TextWrapping = TextWrapping.Wrap } }); var chk1 = UiKit.CheckBox(check1Text); chk1.Margin = new Thickness(20, 10, 20, 0); chk1.IsChecked = check1Initial; chk1.Checked += (_, _2) => closeTabs = true; chk1.Unchecked += (_, _2) => closeTabs = false; root.Children.Add(chk1); var chk2 = UiKit.CheckBox(check2Text); chk2.Margin = new Thickness(20, 8, 20, 4); chk2.IsChecked = check2Initial; chk2.Checked += (_, _2) => remember = true; chk2.Unchecked += (_, _2) => remember = false; root.Children.Add(chk2); var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; var cancelBtn = UiKit.Make(cancelLabel, false); cancelBtn.Margin = new Thickness(8, 0, 0, 0); cancelBtn.IsCancel = true; cancelBtn.Click += (_, _2) => win.Close(); var quitBtn = UiKit.Make(confirmLabel, true); quitBtn.Margin = new Thickness(8, 0, 0, 0); quitBtn.IsDefault = true; quitBtn.Click += (_, _2) => { confirmed = true; win.Close(); }; btnPanel.Children.Add(cancelBtn); btnPanel.Children.Add(quitBtn); root.Children.Add(new Border { Padding = new Thickness(16, 12, 16, 16), Child = btnPanel }); // Film grain across the whole card, same as Show(). var contentGrid = new Grid(); var grain = (owner as MainWindow)?.GrainTexture; if (grain is not null) { double grainOpacity = Application.Current.Resources["GrainOpacity"] is double go ? go : 0.05; contentGrid.Children.Add(new Border { CornerRadius = new CornerRadius(6), IsHitTestVisible = false, Opacity = grainOpacity, Background = new System.Windows.Media.ImageBrush(grain) { TileMode = System.Windows.Media.TileMode.Tile, ViewportUnits = System.Windows.Media.BrushMappingMode.Absolute, Viewport = new Rect(0, 0, 256, 256), Stretch = System.Windows.Media.Stretch.None } }); } contentGrid.Children.Add(root); win.Content = DialogChrome.WrapContent(owner, contentGrid); win.ShowDialog(); return (confirmed, closeTabs, remember); } /// /// Themed "Password Required" prompt: the family dialog chrome (wordmark title bar, grain, /// red close, Esc to cancel) around a themed PasswordBox. Returns the entered password, or /// null if the user canceled / closed the dialog. /// public static string? PromptPassword(Window? owner, string filename) { string? result = null; var win = new Window { Width = 380, SizeToContent = SizeToContent.Height }; DialogChrome.Configure(win, owner, fade: true); void CloseCancel() { result = null; win.Close(); } var body = new StackPanel(); // Message: "" is password protected. var msg = new TextBlock { Foreground = R("TextBrush"), FontSize = 13, TextWrapping = TextWrapping.Wrap }; msg.Inlines.Add(new System.Windows.Documents.Run($"“{System.IO.Path.GetFileName(filename)}” ") { FontWeight = FontWeights.SemiBold }); msg.Inlines.Add(new System.Windows.Documents.Run("is password protected.")); body.Children.Add(new Border { Padding = new Thickness(20, 4, 20, 10), Child = msg }); var pw = UiKit.PasswordField(); body.Children.Add(new Border { Padding = new Thickness(20, 0, 20, 4), Child = pw }); var openBtn = UiKit.Make("Open", accent: true); openBtn.IsDefault = true; openBtn.Click += (_, _2) => { result = pw.Password; win.Close(); }; var cancelBtn = UiKit.Make("Cancel", accent: false); cancelBtn.IsCancel = true; cancelBtn.Click += (_, _2) => CloseCancel(); body.Children.Add(new Border { Padding = new Thickness(16, 12, 16, 16), Child = UiKit.ButtonRow(openBtn, cancelBtn) }); // Enter anywhere in the field submits. pw.KeyDown += (_, e) => { if (e.Key == Key.Enter) { result = pw.Password; win.Close(); } }; win.Content = DialogChrome.Frame(win, owner, "KillerPDF", CloseCancel, body); win.Loaded += (_, _2) => pw.Focus(); win.ShowDialog(); return result; } } }