|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Drawing;
using System.Globalization;
using Windows.Win32.Data.HtmlHelp;
using static Windows.Win32.Data.HtmlHelp.HTML_HELP_COMMAND;
namespace System.Windows.Forms;
/// <summary>
/// Represents the HTML 1.0 Help engine.
/// </summary>
public static class Help
{
private const int HTML10HELP = 2;
private const int HTMLFILE = 3;
/// <summary>
/// Displays the contents of the Help file at located at a specified Url.
/// </summary>
public static void ShowHelp(Control? parent, string? url)
{
ShowHelp(parent, url, HelpNavigator.TableOfContents, null);
}
/// <summary>
/// Displays the contents of the Help file for a specific topic found at the specified Url.
/// </summary>
public static void ShowHelp(Control? parent, string? url, HelpNavigator navigator)
{
ShowHelp(parent, url, navigator, null);
}
/// <summary>
/// Displays the contents of the Help file for a specific topic found at the specified Url.
/// </summary>
public static void ShowHelp(Control? parent, string? url, string? keyword)
{
if (keyword is not null && keyword.Length != 0)
{
ShowHelp(parent, url, HelpNavigator.Topic, keyword);
}
else
{
ShowHelp(parent, url, HelpNavigator.TableOfContents, null);
}
}
/// <summary>
/// Displays the contents of the Help file located at the Url supplied by the user.
/// </summary>
public static void ShowHelp(Control? parent, string? url, HelpNavigator command, object? parameter)
{
switch (GetHelpFileType(url))
{
case HTML10HELP:
ShowHTML10Help(parent, url, command, parameter);
break;
case HTMLFILE:
ShowHTMLFile(parent, url, command, parameter);
break;
}
}
/// <summary>
/// Displays the index of the specified file.
/// </summary>
public static void ShowHelpIndex(Control? parent, string? url)
{
ShowHelp(parent, url, HelpNavigator.Index, null);
}
/// <summary>
/// Displays a Help pop-up window.
/// </summary>
public static unsafe void ShowPopup(Control? parent, string caption, Point location)
{
HH_POPUP pop = new()
{
cbStruct = sizeof(HH_POPUP),
pt = location,
rcMargins = new RECT(-1, -1, -1, -1), // Ignore
clrForeground = new COLORREF(unchecked((uint)-1)), // Ignore
clrBackground = SystemColors.Window
};
Font font = SystemFonts.StatusFont ?? SystemFonts.DefaultFont;
string captionFont = $"{font.Name}, {font.SizeInPoints}, , {(font.Bold ? "BOLD" : "")}{(font.Italic ? "ITALIC" : "")}{(font.Underline ? "UNDERLINE" : "")}";
fixed (char* pszText = caption, pszFont = captionFont)
{
pop.pszText = (sbyte*)pszText;
pop.pszFont = (sbyte*)pszFont;
ShowHTML10Help(parent, null, HelpNavigator.Topic, pop);
}
}
/// <summary>
/// Displays HTML 1.0 Help with the specified parameters.
/// </summary>
private static unsafe void ShowHTML10Help(Control? parent, string? url, HelpNavigator command, object? param)
{
// See if we can get a full path and file name and if that will
// resolve the out of memory condition with file names that include spaces.
// If we can't, though, we can't assume that the path's no good: it might be in
// the Windows help directory.
Uri? file = null;
string? pathAndFileName = url; // This is our best guess at the path yet.
file = Resolve(url);
if (file is not null)
{
// Can't assume we have a good url
pathAndFileName = file.AbsoluteUri;
}
if (file is null || file.IsFile)
{
string? localPath = (file is not null && file.IsFile) ? file.LocalPath : url;
// If this is a local path, convert it to a short path name. Pass 0 as the length the first time
uint requiredStringSize = PInvoke.GetShortPathName(localPath, null, 0);
if (requiredStringSize > 0)
{
// It's able to make it a short path.
using BufferScope<char> shortName = new((int)requiredStringSize);
fixed (char* pShortName = shortName)
{
requiredStringSize = PInvoke.GetShortPathName(localPath, pShortName, requiredStringSize);
// If it can't make it a short path, just leave the path we had.
pathAndFileName = shortName[..(int)requiredStringSize].ToString();
}
}
}
HandleRef<HWND> handle = parent is not null ? (new(parent)) : Control.GetHandleRef(PInvoke.GetActiveWindow());
object? htmlParam;
if (param is string stringParam)
{
HTML_HELP_COMMAND htmlCommand = MapCommandToHTMLCommand(command, stringParam, out htmlParam);
if (htmlParam is string stringHtmlParam)
{
PInvoke.HtmlHelp(handle, pathAndFileName, htmlCommand, stringHtmlParam);
}
else if (htmlParam is int intParam)
{
PInvoke.HtmlHelp(handle, pathAndFileName, htmlCommand, (nuint)intParam);
}
else if (htmlParam is HH_FTS_QUERY query)
{
fixed (char* pszSearchQuery = stringParam)
{
query.pszSearchQuery = (sbyte*)pszSearchQuery;
PInvoke.HtmlHelp(handle, pathAndFileName, htmlCommand, in query);
}
}
else if (htmlParam is HH_AKLINK aLink)
{
// According to MSDN documentation, we have to ensure that the help window is up
// before we call ALINK lookup.
PInvoke.HtmlHelp(HWND.Null, pathAndFileName, HH_DISPLAY_TOPIC, null);
fixed (char* pszKeywords = stringParam)
{
aLink.pszKeywords = (sbyte*)pszKeywords;
PInvoke.HtmlHelp(handle, pathAndFileName, htmlCommand, in aLink);
}
}
else
{
Debug.Fail($"Cannot handle HTML parameter of type: {htmlParam!.GetType()}");
PInvoke.HtmlHelp(handle, pathAndFileName, htmlCommand, (string)param);
}
}
else if (param is null)
{
PInvoke.HtmlHelp(handle, pathAndFileName, MapCommandToHTMLCommand(command, null, out htmlParam), null);
}
else if (param is HH_POPUP popup)
{
PInvoke.HtmlHelp(handle, pathAndFileName, HH_DISPLAY_TEXT_POPUP, ref popup);
}
else if (param.GetType() == typeof(int))
{
throw new ArgumentException(string.Format(SR.InvalidArgument, nameof(param), "Integer"), nameof(param));
}
}
/// <summary>
/// Displays HTMLFile with the specified parameters
/// </summary>
private static void ShowHTMLFile(Control? parent, string? url, HelpNavigator command, object? param)
{
Uri? file = Resolve(url) ?? throw new ArgumentException(string.Format(SR.HelpInvalidURL, url), nameof(url));
switch (command)
{
case HelpNavigator.TableOfContents:
case HelpNavigator.Find:
case HelpNavigator.Index:
// Nothing needed.
break;
case HelpNavigator.Topic:
if (param is string stringParam)
{
file = new Uri($"{file}#{stringParam}");
}
break;
}
HandleRef<HWND> handle = parent is not null ? new(parent) : Control.GetHandleRef(PInvoke.GetActiveWindow());
string fileName = file.ToString();
string? executable = file.IsFile ? FindExecutableInternal(file.LocalPath.ToString()) : null;
PInvoke.ShellExecute(handle.Handle, lpOperation: null, executable ?? fileName, executable is not null ? fileName : null, lpDirectory: null, SHOW_WINDOW_CMD.SW_NORMAL);
GC.KeepAlive(handle.Wrapper);
}
private static unsafe string? FindExecutableInternal(string uri)
{
HINSTANCE result;
Span<char> buffer = stackalloc char[(int)PInvokeCore.MAX_PATH + 1];
fixed (char* lpFileLocal = uri)
{
fixed (char* b = buffer)
{
result = PInvoke.FindExecutable(lpFileLocal, lpDirectory: null, lpResult: b);
}
}
return result <= 32 ? null : buffer.SliceAtFirstNull().ToString();
}
private static Uri? Resolve(string? partialUri)
{
Uri? file = null;
if (!string.IsNullOrEmpty(partialUri))
{
try
{
file = new Uri(partialUri);
}
catch (UriFormatException)
{
// Ignore invalid uris.
}
}
if (file is not null && file.Scheme == "file")
{
string localPath = file.LocalPath + file.Fragment;
if (!File.Exists(localPath))
{
// Clear, and try relative to AppBase.
file = null;
}
}
if (file is null)
{
try
{
// Try relative to AppBase.
file = new Uri(new Uri(AppContext.BaseDirectory),
partialUri);
}
catch (UriFormatException)
{
// Ignore invalid uris.
}
if (file is not null && file.Scheme == "file")
{
string localPath = file.LocalPath + file.Fragment;
if (!File.Exists(localPath))
{
// Clear - file isn't there.
file = null;
}
}
}
return file;
}
private static int GetHelpFileType(string? url)
{
if (url is null)
{
return HTMLFILE;
}
Uri? file = Resolve(url);
if (file is null || file.Scheme == "file")
{
string ext = Path.GetExtension(file is null ? url : file.LocalPath + file.Fragment).ToLower(CultureInfo.InvariantCulture);
if (ext is ".chm" or ".col")
{
return HTML10HELP;
}
}
return HTMLFILE;
}
/// <summary>
/// Maps one of the COMMAND_* constants to the HTML 1.0 Help equivalent.
/// </summary>
private static unsafe HTML_HELP_COMMAND MapCommandToHTMLCommand(HelpNavigator command, string? param, out object? htmlParam)
{
htmlParam = param;
if (string.IsNullOrEmpty(param) && (command == HelpNavigator.AssociateIndex || command == HelpNavigator.KeywordIndex))
{
return HH_DISPLAY_INDEX;
}
switch (command)
{
case HelpNavigator.Topic:
return HH_DISPLAY_TOPIC;
case HelpNavigator.TableOfContents:
return HH_DISPLAY_TOC;
case HelpNavigator.Index:
return HH_DISPLAY_INDEX;
case HelpNavigator.Find:
{
HH_FTS_QUERY ftsQuery = new()
{
cbStruct = sizeof(HH_FTS_QUERY),
iProximity = (int)HH_FTS_DEFAULT_PROXIMITY,
fExecute = true,
fUniCodeStrings = true
};
htmlParam = ftsQuery;
return HH_DISPLAY_SEARCH;
}
case HelpNavigator.TopicId:
{
if (int.TryParse(param, out int htmlParamAsInt))
{
htmlParam = htmlParamAsInt;
return HH_HELP_CONTEXT;
}
// default to just showing the index
return HH_DISPLAY_INDEX;
}
case HelpNavigator.KeywordIndex:
case HelpNavigator.AssociateIndex:
{
HH_AKLINK alink = new()
{
cbStruct = sizeof(HH_AKLINK),
fIndexOnFail = true,
fReserved = false
};
htmlParam = alink;
return command == HelpNavigator.KeywordIndex ? HH_KEYWORD_LOOKUP : HH_ALINK_LOOKUP;
}
default:
return (HTML_HELP_COMMAND)command;
}
}
}
|