sources/Google.Solutions.Mvvm/Controls/TaskDialog.cs (247 lines of code) (raw):

// // Copyright 2023 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Interop; using Google.Solutions.Common.Util; using Google.Solutions.Platform.Interop; using System; using System.Linq; using System.Runtime.InteropServices; using System.Windows.Forms; namespace Google.Solutions.Mvvm.Controls { /// <summary> /// A "Vista" style task dialog. /// </summary> public interface ITaskDialog { /// <summary> /// Show a dialog. /// </summary> DialogResult ShowDialog( IWin32Window? parent, TaskDialogParameters parameters); } public class TaskDialog : ITaskDialog { internal const int CommandLinkIdOffset = 1000; /// <summary> /// Surrogate function, for testing only. /// </summary> internal NativeMethods.TaskDialogIndirectDelegate? TaskDialogIndirect { get; set; } public DialogResult ShowDialog( IWin32Window? parent, TaskDialogParameters parameters) { parameters.ExpectNotNull(nameof(parameters)); if (!parameters.Buttons.Any()) { throw new InvalidOperationException ("The dialog must contain at least one button"); } var standardButtons = parameters.Buttons .OfType<TaskDialogStandardButton>() .ToList(); var commandButtons = parameters.Buttons .OfType<TaskDialogCommandLinkButton>() .ToList(); Precondition.Expect( standardButtons.Any(), "At least one standard button is required"); using (var commandButtonsHandle = LocalAllocSafeHandle.LocalAlloc( (uint)(Marshal.SizeOf<TASKDIALOG_BUTTON_RAW>() * commandButtons.Count))) { // // Prepare native struct for command buttons. // var commandButtonTexts = commandButtons .Select(b => { // // Text up to the first new line character is treated as the // command link's main text, the remainder is treated as the // command link's note. // var text = b.Text.Replace('\n', ' '); if (b.Details != null) { text += $"\n{b.Details}"; } return text; }) .Select(text => Marshal.StringToHGlobalUni(text)) .ToArray(); for (var i = 0; i < commandButtons.Count; i++) { Marshal.StructureToPtr( new TASKDIALOG_BUTTON_RAW() { // // Add ID offset to avoid conflict with IDOK/IDCANCEL. // nButtonID = CommandLinkIdOffset + i, pszButtonText = commandButtonTexts[i] }, commandButtonsHandle.DangerousGetHandle() + i * Marshal.SizeOf<TASKDIALOG_BUTTON_RAW>(), false); } try { var flags = TASKDIALOG_FLAGS.TDF_EXPAND_FOOTER_AREA | TASKDIALOG_FLAGS.TDF_ENABLE_HYPERLINKS; if (commandButtons.Count != 0) { flags |= TASKDIALOG_FLAGS.TDF_USE_COMMAND_LINKS; } var config = new TASKDIALOGCONFIG() { cbSize = (uint)Marshal.SizeOf<TASKDIALOGCONFIG>(), hwndParent = parent?.Handle ?? IntPtr.Zero, dwFlags = flags, dwCommonButtons = standardButtons .Select(b => b.Flag) .Aggregate((f1, f2) => f1 | f2), pszWindowTitle = parameters.Caption, MainIcon = parameters.Icon?.Handle ?? IntPtr.Zero, pszMainInstruction = parameters.Heading, pszContent = parameters.Text, pButtons = commandButtons.Count != 0 ? commandButtonsHandle.DangerousGetHandle() : IntPtr.Zero, cButtons = (uint)commandButtons.Count, pszExpandedInformation = parameters.Footnote, pszVerificationText = parameters.VerificationCheckBox?.Text, pfCallback = (hwnd, notification, wParam, lParam, refData) => { if (notification == TASKDIALOG_NOTIFICATIONS.TDN_HYPERLINK_CLICKED) { parameters.PerformLinkClick(); } return HRESULT.S_OK; } }; var function = this.TaskDialogIndirect ?? NativeMethods.TaskDialogIndirect; function( ref config, out var buttonIdPressed, out var _, out var verificationFlagPressed); if (parameters.VerificationCheckBox != null) { parameters.VerificationCheckBox.Checked = verificationFlagPressed; } // // Map the result back to the right button. // if (buttonIdPressed >= CommandLinkIdOffset && buttonIdPressed < CommandLinkIdOffset + commandButtons.Count) { var pressedCommandButton = commandButtons[buttonIdPressed - CommandLinkIdOffset]; pressedCommandButton.PerformClick(); return pressedCommandButton.Result; } else if (standardButtons.FirstOrDefault(b => b.CommandId == buttonIdPressed) is var pressedStandardButton && pressedStandardButton != null) { return pressedStandardButton.Result; } else { throw new InvalidOperationException( $"The TaskDialog returned an unexpected result: {buttonIdPressed}"); } } finally { foreach (var commandButtonText in commandButtonTexts) { Marshal.FreeHGlobal(commandButtonText); } } } } //--------------------------------------------------------------------- // P/Invoke. //--------------------------------------------------------------------- [Flags] internal enum TASKDIALOG_FLAGS : uint { TDF_ENABLE_HYPERLINKS = 0x0001, TDF_USE_HICON_MAIN = 0x0002, TDF_USE_HICON_FOOTER = 0x0004, TDF_ALLOW_DIALOG_CANCELLATION = 0x0008, TDF_USE_COMMAND_LINKS = 0x0010, TDF_USE_COMMAND_LINKS_NO_ICON = 0x0020, TDF_EXPAND_FOOTER_AREA = 0x0040, TDF_EXPANDED_BY_DEFAULT = 0x0080, TDF_VERIFICATION_FLAG_CHECKED = 0x0100, TDF_SHOW_PROGRESS_BAR = 0x0200, TDF_SHOW_MARQUEE_PROGRESS_BAR = 0x0400, TDF_CALLBACK_TIMER = 0x0800, TDF_POSITION_RELATIVE_TO_WINDOW = 0x1000, TDF_RTL_LAYOUT = 0x2000, TDF_NO_DEFAULT_RADIO_BUTTON = 0x4000, TDF_CAN_BE_MINIMIZED = 0x8000 } [Flags] internal enum TASKDIALOG_COMMON_BUTTON_FLAGS : uint { TDCBF_OK_BUTTON = 0x0001, TDCBF_YES_BUTTON = 0x0002, TDCBF_NO_BUTTON = 0x0004, TDCBF_CANCEL_BUTTON = 0x0008, TDCBF_RETRY_BUTTON = 0x0010, TDCBF_CLOSE_BUTTON = 0x0020, } internal enum TASKDIALOG_NOTIFICATIONS : uint { TDN_CREATED = 0, TDN_NAVIGATED = 1, TDN_BUTTON_CLICKED = 2, TDN_HYPERLINK_CLICKED = 3, TDN_TIMER = 4, TDN_DESTROYED = 5, TDN_RADIO_BUTTON_CLICKED = 6, TDN_DIALOG_CONSTRUCTED = 7, TDN_VERIFICATION_CLICKED = 8, TDN_HELP = 9, TDN_EXPANDO_BUTTON_CLICKED = 10 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] internal struct TASKDIALOGCONFIG { public uint cbSize; public IntPtr hwndParent; public IntPtr hInstance; public TASKDIALOG_FLAGS dwFlags; public uint dwCommonButtons; [MarshalAs(UnmanagedType.LPWStr)] public string pszWindowTitle; public IntPtr MainIcon; [MarshalAs(UnmanagedType.LPWStr)] public string pszMainInstruction; [MarshalAs(UnmanagedType.LPWStr)] public string pszContent; public uint cButtons; public IntPtr pButtons; public int nDefaultButton; public uint cRadioButtons; public IntPtr pRadioButtons; public int nDefaultRadioButton; [MarshalAs(UnmanagedType.LPWStr)] public string? pszVerificationText; [MarshalAs(UnmanagedType.LPWStr)] public string? pszExpandedInformation; [MarshalAs(UnmanagedType.LPWStr)] public string pszExpandedControlText; [MarshalAs(UnmanagedType.LPWStr)] public string pszCollapsedControlText; public IntPtr FooterIcon; [MarshalAs(UnmanagedType.LPWStr)] public string pszFooter; public NativeMethods.TaskDialogCallback pfCallback; public IntPtr lpCallbackData; public uint cxWidth; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] internal struct TASKDIALOG_BUTTON_RAW { public int nButtonID; public IntPtr pszButtonText; } internal static class NativeMethods { internal delegate HRESULT TaskDialogCallback( [In] IntPtr hwnd, [In] TASKDIALOG_NOTIFICATIONS msg, [In] UIntPtr wParam, [In] IntPtr lParam, [In] IntPtr refData); internal delegate void TaskDialogIndirectDelegate( [In] ref TASKDIALOGCONFIG pTaskConfig, [Out] out int pnButton, [Out] out int pnRadioButton, [Out] out bool pfVerificationFlagChecked); [DllImport("ComCtl32", CharSet = CharSet.Unicode, PreserveSig = false)] internal static extern void TaskDialogIndirect( [In] ref TASKDIALOGCONFIG pTaskConfig, [Out] out int pnButton, [Out] out int pnRadioButton, [Out] out bool pfVerificationFlagChecked); } } }