From ea95e5cbac47675b3e32bbcc2c74ac9a9f7fe2a1 Mon Sep 17 00:00:00 2001 From: chylex <contact@chylex.com> Date: Sun, 26 Jun 2022 14:06:43 +0200 Subject: [PATCH] Re-add ContextMenu that was removed in .NET Core 3.1 --- README.md | 4 + TweetDuck.sln | 6 + .../Browser/Base/ContextMenuBrowser.cs | 20 +- windows/TweetDuck/Browser/FormBrowser.cs | 2 +- windows/TweetDuck/Browser/TrayIcon.cs | 28 +- windows/TweetDuck/TweetDuck.csproj | 1 + .../TweetLib.WinForms.Legacy.csproj | 30 ++ .../Windows/Forms/Command2.cs | 29 ++ .../Windows/Forms/ContextMenu.cs | 35 ++ .../Windows/Forms/Menu.cs | 261 ++++++++++ .../Windows/Forms/MenuItem.cs | 454 ++++++++++++++++++ .../Windows/Forms/NativeMethods.cs | 78 +++ 12 files changed, 928 insertions(+), 20 deletions(-) create mode 100644 windows/TweetLib.WinForms.Legacy/TweetLib.WinForms.Legacy.csproj create mode 100644 windows/TweetLib.WinForms.Legacy/Windows/Forms/Command2.cs create mode 100644 windows/TweetLib.WinForms.Legacy/Windows/Forms/ContextMenu.cs create mode 100644 windows/TweetLib.WinForms.Legacy/Windows/Forms/Menu.cs create mode 100644 windows/TweetLib.WinForms.Legacy/Windows/Forms/MenuItem.cs create mode 100644 windows/TweetLib.WinForms.Legacy/Windows/Forms/NativeMethods.cs diff --git a/README.md b/README.md index 93026a4e..20c04784 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,10 @@ By default, [CefSharp](https://github.com/cefsharp/CefSharp/) is not built with Windows library that implements `TweetLib.Browser.CEF` using the [CefSharp](https://github.com/cefsharp/CefSharp/) library and Windows Forms. +#### TweetLib.WinForms.Legacy + +Windows library that re-adds some legacy Windows Forms components that were removed in .NET Core 3.1. The sources were taken from the [.NET Core 3.0 sources of Windows Forms](https://github.com/dotnet/winforms/tree/v3.0.2), and edited to remove unnecessary features. + ### Linux Projects #### TweetDuck diff --git a/TweetDuck.sln b/TweetDuck.sln index 94408e6b..a3aef2f2 100644 --- a/TweetDuck.sln +++ b/TweetDuck.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "windows\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windows\TweetImpl.CefSharp\TweetImpl.CefSharp.csproj", "{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.WinForms.Legacy", "windows\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj", "{B54E732A-4090-4DAA-9ABD-311368C17B68}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}" @@ -48,6 +50,10 @@ Global {44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Debug|x86.Build.0 = Debug|x86 {44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.ActiveCfg = Release|x86 {44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.Build.0 = Release|x86 + {B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.ActiveCfg = Debug|x86 + {B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.Build.0 = Debug|x86 + {B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.ActiveCfg = Release|x86 + {B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.Build.0 = Release|x86 {72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86 {72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86 {72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86 diff --git a/windows/TweetDuck/Browser/Base/ContextMenuBrowser.cs b/windows/TweetDuck/Browser/Base/ContextMenuBrowser.cs index 7407ef15..166c9a3c 100644 --- a/windows/TweetDuck/Browser/Base/ContextMenuBrowser.cs +++ b/windows/TweetDuck/Browser/Base/ContextMenuBrowser.cs @@ -99,18 +99,18 @@ public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser extraContext.Reset(); } - public static ContextMenuStrip CreateMenu(FormBrowser form) { - ContextMenuStrip menu = new ContextMenuStrip(); + public static ContextMenu CreateMenu(FormBrowser form) { + ContextMenu menu = new ContextMenu(); - menu.Items.Add(TitleReloadBrowser, null, (_, _) => form.ReloadToTweetDeck()); - menu.Items.Add(TitleMuteNotifications, null, static (_, _) => ToggleMuteNotifications()); - menu.Items.Add("-"); - menu.Items.Add(TitleSettings, null, (_, _) => form.OpenSettings()); - menu.Items.Add(TitlePlugins, null, (_, _) => form.OpenPlugins()); - menu.Items.Add(TitleAboutProgram, null, (_, _) => form.OpenAbout()); + menu.MenuItems.Add(TitleReloadBrowser, (_, _) => form.ReloadToTweetDeck()); + menu.MenuItems.Add(TitleMuteNotifications, (_, _) => ToggleMuteNotifications()); + menu.MenuItems.Add("-"); + menu.MenuItems.Add(TitleSettings, (_, _) => form.OpenSettings()); + menu.MenuItems.Add(TitlePlugins, (_, _) => form.OpenPlugins()); + menu.MenuItems.Add(TitleAboutProgram, (_, _) => form.OpenAbout()); - menu.Opening += (_, _) => { - ((ToolStripMenuItem) menu.Items[1]).Checked = Config.MuteNotifications; + menu.Popup += (_, _) => { + menu.MenuItems[1].Checked = Config.MuteNotifications; }; return menu; diff --git a/windows/TweetDuck/Browser/FormBrowser.cs b/windows/TweetDuck/Browser/FormBrowser.cs index d6a9c734..cc4ac0df 100644 --- a/windows/TweetDuck/Browser/FormBrowser.cs +++ b/windows/TweetDuck/Browser/FormBrowser.cs @@ -57,7 +57,7 @@ public bool IsWaiting { private readonly FormNotificationTweet notification; private readonly PluginManager plugins; private readonly UpdateChecker updates; - private readonly ContextMenuStrip contextMenu; + private readonly ContextMenu contextMenu; private readonly uint windowRestoreMessage; private bool isLoaded; diff --git a/windows/TweetDuck/Browser/TrayIcon.cs b/windows/TweetDuck/Browser/TrayIcon.cs index 402bf6be..4066e4fc 100644 --- a/windows/TweetDuck/Browser/TrayIcon.cs +++ b/windows/TweetDuck/Browser/TrayIcon.cs @@ -45,19 +45,23 @@ public bool HasNotifications { } } - private readonly ContextMenuStrip contextMenu; + private readonly ContextMenu contextMenu; + private readonly ContextMenuStrip fakeContextMenu; private bool hasNotifications; private TrayIcon() { InitializeComponent(); - this.contextMenu = new ContextMenuStrip(); - this.contextMenu.Items.Add("Restore", null, menuItemRestore_Click); - this.contextMenu.Items.Add("Mute notifications", null, menuItemMuteNotifications_Click); - this.contextMenu.Items.Add("Close", null, menuItemClose_Click); - this.contextMenu.Opening += contextMenu_Popup; + this.contextMenu = new ContextMenu(); + this.contextMenu.MenuItems.Add("Restore", menuItemRestore_Click); + this.contextMenu.MenuItems.Add("Mute notifications", menuItemMuteNotifications_Click); + this.contextMenu.MenuItems.Add("Close", menuItemClose_Click); + this.contextMenu.Popup += contextMenu_Popup; - this.notifyIcon.ContextMenuStrip = contextMenu; + this.fakeContextMenu = new ContextMenuStrip(); + this.fakeContextMenu.Opening += fakeContextMenu_Opening; + + this.notifyIcon.ContextMenuStrip = this.fakeContextMenu; this.notifyIcon.Text = Program.BrandName; Config.MuteToggled += Config_MuteToggled; @@ -72,6 +76,7 @@ protected override void Dispose(bool disposing) { if (disposing) { components?.Dispose(); contextMenu.Dispose(); + fakeContextMenu.Dispose(); } base.Dispose(disposing); @@ -95,8 +100,13 @@ private void trayIcon_MouseClick(object? sender, MouseEventArgs e) { } } + private void fakeContextMenu_Opening(object? sender, CancelEventArgs args) { + args.Cancel = true; + contextMenu.Show(notifyIcon, Cursor.Position); + } + private void contextMenu_Popup(object? sender, EventArgs e) { - ((ToolStripMenuItem) contextMenu.Items[1]).Checked = Config.MuteNotifications; + contextMenu.MenuItems[1].Checked = Config.MuteNotifications; } private void menuItemRestore_Click(object? sender, EventArgs e) { @@ -104,7 +114,7 @@ private void menuItemRestore_Click(object? sender, EventArgs e) { } private void menuItemMuteNotifications_Click(object? sender, EventArgs e) { - Config.MuteNotifications = !((ToolStripMenuItem) contextMenu.Items[1]).Checked; + Config.MuteNotifications = !contextMenu.MenuItems[1].Checked; Config.Save(); } diff --git a/windows/TweetDuck/TweetDuck.csproj b/windows/TweetDuck/TweetDuck.csproj index 7f86448d..65a3cfc4 100644 --- a/windows/TweetDuck/TweetDuck.csproj +++ b/windows/TweetDuck/TweetDuck.csproj @@ -48,6 +48,7 @@ <ProjectReference Include="..\TweetDuck.Browser\TweetDuck.Browser.csproj" /> <ProjectReference Include="..\TweetDuck.Video\TweetDuck.Video.csproj" /> <ProjectReference Include="..\TweetImpl.CefSharp\TweetImpl.CefSharp.csproj" /> + <ProjectReference Include="..\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj" /> </ItemGroup> <ItemGroup> diff --git a/windows/TweetLib.WinForms.Legacy/TweetLib.WinForms.Legacy.csproj b/windows/TweetLib.WinForms.Legacy/TweetLib.WinForms.Legacy.csproj new file mode 100644 index 00000000..9e46988d --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/TweetLib.WinForms.Legacy.csproj @@ -0,0 +1,30 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0-windows</TargetFramework> + <Configurations>Debug;Release</Configurations> + <Platforms>x86</Platforms> + <RuntimeIdentifier>win7-x86</RuntimeIdentifier> + <LangVersion>10</LangVersion> + <Nullable>disable</Nullable> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + </PropertyGroup> + + <PropertyGroup> + <OutputType>Library</OutputType> + <UseWindowsForms>true</UseWindowsForms> + <RootNamespace>System</RootNamespace> + <AssemblyName>TweetLib.WinForms.Legacy</AssemblyName> + </PropertyGroup> + + <PropertyGroup> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> + <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> + </PropertyGroup> + + <ItemGroup> + <Compile Include="..\..\Version.cs" Link="Version.cs" /> + </ItemGroup> + +</Project> diff --git a/windows/TweetLib.WinForms.Legacy/Windows/Forms/Command2.cs b/windows/TweetLib.WinForms.Legacy/Windows/Forms/Command2.cs new file mode 100644 index 00000000..a60f6720 --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/Windows/Forms/Command2.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Reflection; + +namespace System.Windows.Forms { + internal sealed class Command2 { + private static readonly Type Type = typeof(Form).Assembly.GetType("System.Windows.Forms.Command"); + private static readonly ConstructorInfo Constructor = Type.GetConstructor(new Type[] { typeof(ICommandExecutor) }) ?? throw new NullReferenceException(); + private static readonly MethodInfo DisposeMethod = Type.GetMethod("Dispose", BindingFlags.Instance | BindingFlags.Public) ?? throw new NullReferenceException(); + private static readonly PropertyInfo IDProperty = Type.GetProperty("ID") ?? throw new NullReferenceException(); + + public int ID { get; } + + private readonly object cmd; + + public Command2(ICommandExecutor executor) { + this.cmd = Constructor.Invoke(new object[] { executor }); + this.ID = (int) IDProperty.GetValue(cmd)!; + } + + public void Dispose() { + try { + DisposeMethod.Invoke(cmd, null); + } catch (Exception e) { + Debug.WriteLine(e); + Debugger.Break(); + } + } + } +} diff --git a/windows/TweetLib.WinForms.Legacy/Windows/Forms/ContextMenu.cs b/windows/TweetLib.WinForms.Legacy/Windows/Forms/ContextMenu.cs new file mode 100644 index 00000000..f20c053b --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/Windows/Forms/ContextMenu.cs @@ -0,0 +1,35 @@ +// 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.Reflection; +using System.Runtime.InteropServices; + +namespace System.Windows.Forms { + public sealed class ContextMenu : Menu { + private static readonly FieldInfo NotifyIconWindowField = typeof(NotifyIcon).GetField("window", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException(); + + public event EventHandler Popup; + + public void Show(Control control, Point pos) { + if (control == null) { + throw new ArgumentNullException(nameof(control)); + } + + if (!control.IsHandleCreated || !control.Visible) { + throw new ArgumentException(null, nameof(control)); + } + + Popup?.Invoke(this, EventArgs.Empty); + pos = control.PointToScreen(pos); + NativeMethods.TrackPopupMenuEx(new HandleRef(this, Handle), NativeMethods.TPM_VERTICAL | NativeMethods.TPM_RIGHTBUTTON, pos.X, pos.Y, new HandleRef(control, control.Handle), IntPtr.Zero); + } + + public void Show(NotifyIcon icon, Point pos) { + Popup?.Invoke(this, EventArgs.Empty); + NativeWindow window = (NativeWindow) NotifyIconWindowField.GetValue(icon); + NativeMethods.TrackPopupMenuEx(new HandleRef(this, Handle), NativeMethods.TPM_VERTICAL | NativeMethods.TPM_RIGHTALIGN, pos.X, pos.Y, new HandleRef(window, window.Handle), IntPtr.Zero); + NativeMethods.PostMessage(new HandleRef(window, window.Handle), 0, IntPtr.Zero, IntPtr.Zero); + } + } +} diff --git a/windows/TweetLib.WinForms.Legacy/Windows/Forms/Menu.cs b/windows/TweetLib.WinForms.Legacy/Windows/Forms/Menu.cs new file mode 100644 index 00000000..fe827441 --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/Windows/Forms/Menu.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace System.Windows.Forms { + public abstract class Menu : Component { + internal const int CHANGE_ITEMS = 0; + internal const int CHANGE_VISIBLE = 1; + internal const int CHANGE_ITEMADDED = 4; + + private MenuItemCollection itemsCollection; + internal MenuItem[] items; + internal IntPtr handle; + internal bool created; + + internal IntPtr Handle { + get { + if (handle == IntPtr.Zero) { + handle = CreateMenuHandle(); + } + + CreateMenuItems(); + return handle; + } + } + + protected bool IsParent => items != null && ItemCount > 0; + + internal int ItemCount { get; private set; } + + public MenuItemCollection MenuItems => itemsCollection ??= new MenuItemCollection(this); + + internal void ClearHandles() { + if (handle != IntPtr.Zero) { + NativeMethods.DestroyMenu(new HandleRef(this, handle)); + } + + handle = IntPtr.Zero; + + if (created) { + for (int i = 0; i < ItemCount; i++) { + items[i].ClearHandles(); + } + + created = false; + } + } + + protected void CloneMenu(Menu menuSrc) { + if (menuSrc == null) { + throw new ArgumentNullException(nameof(menuSrc)); + } + + MenuItem[] newItems = null; + if (menuSrc.items != null) { + int count = menuSrc.MenuItems.Count; + newItems = new MenuItem[count]; + for (int i = 0; i < count; i++) { + newItems[i] = menuSrc.MenuItems[i].CloneMenu(); + } + } + + MenuItems.Clear(); + if (newItems != null) { + MenuItems.AddRange(newItems); + } + } + + private IntPtr CreateMenuHandle() { + return NativeMethods.CreatePopupMenu(); + } + + internal void CreateMenuItems() { + if (!created) { + for (int i = 0; i < ItemCount; i++) { + items[i].CreateMenuItem(); + } + + created = true; + } + } + + private void DestroyMenuItems() { + if (created) { + for (int i = 0; i < ItemCount; i++) { + items[i].ClearHandles(); + } + + while (NativeMethods.GetMenuItemCount(new HandleRef(this, handle)) > 0) { + NativeMethods.RemoveMenu(new HandleRef(this, handle), 0, NativeMethods.MF_BYPOSITION); + } + + created = false; + } + } + + protected override void Dispose(bool disposing) { + if (disposing) { + while (ItemCount > 0) { + MenuItem item = items[--ItemCount]; + + if (item.Site is { Container: {} }) { + item.Site.Container.Remove(item); + } + + item.Parent = null; + item.Dispose(); + } + + items = null; + } + + if (handle != IntPtr.Zero) { + NativeMethods.DestroyMenu(new HandleRef(this, handle)); + handle = IntPtr.Zero; + if (disposing) { + ClearHandles(); + } + } + + base.Dispose(disposing); + } + + internal virtual void ItemsChanged(int change) { + switch (change) { + case CHANGE_ITEMS: + case CHANGE_VISIBLE: + DestroyMenuItems(); + break; + } + } + + public sealed class MenuItemCollection { + private readonly Menu owner; + + internal MenuItemCollection(Menu owner) { + this.owner = owner; + } + + public MenuItem this[int index] { + get { + if (index < 0 || index >= owner.ItemCount) { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return owner.items[index]; + } + } + + internal int Count => owner.ItemCount; + + public void Add(string caption) { + Add(new MenuItem(caption)); + } + + public void Add(string caption, EventHandler onClick) { + Add(new MenuItem(caption, onClick)); + } + + internal void Add(MenuItem item) { + Add(owner.ItemCount, item); + } + + private void Add(int index, MenuItem item) { + if (item == null) { + throw new ArgumentNullException(nameof(item)); + } + + // MenuItems can only belong to one menu at a time + if (item.Parent != null) { + throw new InvalidOperationException(); + } + + // Validate our index + if (index < 0 || index > owner.ItemCount) { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (owner.items == null || owner.items.Length == owner.ItemCount) { + MenuItem[] newItems = new MenuItem[owner.ItemCount < 2 ? 4 : owner.ItemCount * 2]; + if (owner.ItemCount > 0) { + Array.Copy(owner.items!, 0, newItems, 0, owner.ItemCount); + } + + owner.items = newItems; + } + + Array.Copy(owner.items, index, owner.items, index + 1, owner.ItemCount - index); + owner.items[index] = item; + owner.ItemCount++; + item.Parent = owner; + owner.ItemsChanged(CHANGE_ITEMS); + if (owner is MenuItem menuItem) { + menuItem.ItemsChanged(CHANGE_ITEMADDED, item); + } + } + + internal void AddRange(MenuItem[] items) { + if (items == null) { + throw new ArgumentNullException(nameof(items)); + } + + foreach (MenuItem item in items) { + Add(item); + } + } + + internal bool Contains(MenuItem value) { + for (int index = 0; index < Count; ++index) { + if (this[index] == value) { + return true; + } + } + + return false; + } + + internal void Clear() { + if (owner.ItemCount > 0) { + for (int i = 0; i < owner.ItemCount; i++) { + owner.items[i].Parent = null; + } + + owner.ItemCount = 0; + owner.items = null; + + owner.ItemsChanged(CHANGE_ITEMS); + + if (owner is MenuItem item) { + item.UpdateMenuItem(true); + } + } + } + + internal void Remove(MenuItem item) { + if (item.Parent == owner) { + RemoveAt(item.Index); + } + } + + private void RemoveAt(int index) { + if (index < 0 || index >= owner.ItemCount) { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + MenuItem item = owner.items[index]; + item.Parent = null; + owner.ItemCount--; + Array.Copy(owner.items, index + 1, owner.items, index, owner.ItemCount - index); + owner.items[owner.ItemCount] = null; + owner.ItemsChanged(CHANGE_ITEMS); + + if (owner.ItemCount == 0) { + Clear(); + } + } + } + } +} diff --git a/windows/TweetLib.WinForms.Legacy/Windows/Forms/MenuItem.cs b/windows/TweetLib.WinForms.Legacy/Windows/Forms/MenuItem.cs new file mode 100644 index 00000000..2d5c8cbb --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/Windows/Forms/MenuItem.cs @@ -0,0 +1,454 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace System.Windows.Forms { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public sealed class MenuItem : Menu { + private const int StateBarBreak = 0x00000020; + private const int StateBreak = 0x00000040; + private const int StateChecked = 0x00000008; + private const int StateDefault = 0x00001000; + private const int StateDisabled = 0x00000003; + private const int StateRadioCheck = 0x00000200; + private const int StateHidden = 0x00010000; + private const int StateCloneMask = 0x0003136B; + private const int StateOwnerDraw = 0x00000100; + + private bool _hasHandle; + private MenuItemData _data; + private MenuItem _nextLinkedItem; // Next item linked to the same MenuItemData. + + private const uint FirstUniqueID = 0xC0000000; + private uint _uniqueID = 0; + private IntPtr _msaaMenuInfoPtr = IntPtr.Zero; + + private MenuItem() : this(0, null, null, null, null) {} + + internal MenuItem(string text) : this(0, text, null, null, null) {} + + internal MenuItem(string text, EventHandler onClick) : this(0, text, onClick, null, null) {} + + private MenuItem(Shortcut shortcut, string text, EventHandler onClick, EventHandler onPopup, EventHandler onSelect) { + var _ = new MenuItemData(this, shortcut, true, text, onClick, onPopup, onSelect, null, null); + } + + internal Menu Parent { get; set; } + + internal int Index { + get { + if (Parent != null) { + for (int i = 0; i < Parent.ItemCount; i++) { + if (Parent.items[i] == this) { + return i; + } + } + } + + return -1; + } + } + + private int MenuID { + get { + CheckIfDisposed(); + return _data.GetMenuID(); + } + } + + public bool Enabled { + get { + CheckIfDisposed(); + return (_data.State & StateDisabled) == 0; + } + set { + CheckIfDisposed(); + _data.SetState(StateDisabled, !value); + } + } + + public bool Checked { + get { + CheckIfDisposed(); + return (_data.State & StateChecked) != 0; + } + set { + CheckIfDisposed(); + + if (value && ItemCount != 0) { + throw new ArgumentException(null, nameof(value)); + } + + _data.SetState(StateChecked, value); + } + } + + public bool RadioCheck { + get { + CheckIfDisposed(); + return (_data.State & StateRadioCheck) != 0; + } + set { + CheckIfDisposed(); + _data.SetState(StateRadioCheck, value); + } + } + + private string Text { + get { + CheckIfDisposed(); + return _data.caption; + } + } + + private Shortcut Shortcut { + get { + CheckIfDisposed(); + return _data.shortcut; + } + } + + private bool ShowShortcut { + get { + CheckIfDisposed(); + return _data.showShortcut; + } + } + + public bool Visible { + get { + CheckIfDisposed(); + return _data.Visible; + } + set { + CheckIfDisposed(); + _data.Visible = value; + } + } + + internal MenuItem CloneMenu() { + var newItem = new MenuItem(); + newItem.CloneMenu(this); + return newItem; + } + + private void CloneMenu(MenuItem itemSrc) { + base.CloneMenu(itemSrc); + int state = itemSrc._data.State; + var _ = new MenuItemData(this, itemSrc.Shortcut, itemSrc.ShowShortcut, itemSrc.Text, itemSrc._data.onClick, itemSrc._data.onPopup, itemSrc._data.onSelect, itemSrc._data.onDrawItem, itemSrc._data.onMeasureItem); + _data.SetState(state & StateCloneMask, true); + } + + internal void CreateMenuItem() { + if ((_data.State & StateHidden) == 0) { + NativeMethods.MENUITEMINFO_T info = CreateMenuItemInfo(); + NativeMethods.InsertMenuItem(new HandleRef(Parent, Parent.handle), -1, true, info); + _hasHandle = info.hSubMenu != IntPtr.Zero; + } + } + + private NativeMethods.MENUITEMINFO_T CreateMenuItemInfo() { + var info = new NativeMethods.MENUITEMINFO_T { + fMask = NativeMethods.MIIM_ID | NativeMethods.MIIM_STATE | NativeMethods.MIIM_SUBMENU | NativeMethods.MIIM_TYPE | NativeMethods.MIIM_DATA, + fType = _data.State & (StateBarBreak | StateBreak | StateRadioCheck | StateOwnerDraw) + }; + + if (_data.caption.Equals("-")) { + info.fType |= NativeMethods.MFT_SEPARATOR; + } + + info.fState = _data.State & (StateChecked | StateDefault | StateDisabled); + + info.wID = MenuID; + if (IsParent) { + info.hSubMenu = Handle; + } + + info.hbmpChecked = IntPtr.Zero; + info.hbmpUnchecked = IntPtr.Zero; + + if (IntPtr.Size == 4) { + // Store the unique ID in the dwItemData.. + // For simple menu items, we can just put the unique ID in the dwItemData. + // But for owner-draw items, we need to point the dwItemData at an MSAAMENUINFO + // structure so that MSAA can get the item text. + // To allow us to reliably distinguish between IDs and structure pointers later + // on, we keep IDs in the 0xC0000000-0xFFFFFFFF range. This is the top 1Gb of + // unmananged process memory, where an app's heap allocations should never come + // from. So that we can still get the ID from the dwItemData for an owner-draw + // item later on, a copy of the ID is tacked onto the end of the MSAAMENUINFO + // structure. + if (_data.OwnerDraw) { + info.dwItemData = AllocMsaaMenuInfo(); + } + else { + info.dwItemData = (IntPtr) unchecked((int) _uniqueID); + } + } + else { + // On Win64, there are no reserved address ranges we can use for menu item IDs. So instead we will + // have to allocate an MSAMENUINFO heap structure for all menu items, not just owner-drawn ones. + info.dwItemData = AllocMsaaMenuInfo(); + } + + // We won't render the shortcut if: 1) it's not set, 2) we're a parent, 3) we're toplevel + if (_data.showShortcut && _data.shortcut != 0 && !IsParent) { + info.dwTypeData = _data.caption + "\t" + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString((Keys) (int) _data.shortcut); + } + else { + // Windows issue: Items with empty captions sometimes block keyboard + // access to other items in same menu. + info.dwTypeData = (_data.caption.Length == 0 ? " " : _data.caption); + } + + info.cch = 0; + + return info; + } + + protected override void Dispose(bool disposing) { + if (disposing) { + Parent?.MenuItems.Remove(this); + _data?.RemoveItem(this); + _uniqueID = 0; + } + + FreeMsaaMenuInfo(); + base.Dispose(disposing); + } + + [StructLayout(LayoutKind.Sequential)] + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] + private struct MsaaMenuInfoWithId { + public readonly NativeMethods.MSAAMENUINFO _msaaMenuInfo; + public readonly uint _uniqueID; + + public MsaaMenuInfoWithId(string text, uint uniqueID) { + _msaaMenuInfo = new NativeMethods.MSAAMENUINFO(text); + _uniqueID = uniqueID; + } + } + + private IntPtr AllocMsaaMenuInfo() { + FreeMsaaMenuInfo(); + _msaaMenuInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf<MsaaMenuInfoWithId>()); + + if (IntPtr.Size == 4) { + // We only check this on Win32, irrelevant on Win64 (see CreateMenuItemInfo) + // Check for incursion into menu item ID range (unlikely!) + Debug.Assert(((uint) (ulong) _msaaMenuInfoPtr) < FirstUniqueID); + } + + MsaaMenuInfoWithId msaaMenuInfoStruct = new MsaaMenuInfoWithId(_data.caption, _uniqueID); + Marshal.StructureToPtr(msaaMenuInfoStruct, _msaaMenuInfoPtr, false); + Debug.Assert(_msaaMenuInfoPtr != IntPtr.Zero); + return _msaaMenuInfoPtr; + } + + private void FreeMsaaMenuInfo() { + if (_msaaMenuInfoPtr != IntPtr.Zero) { + Marshal.DestroyStructure(_msaaMenuInfoPtr, typeof(MsaaMenuInfoWithId)); + Marshal.FreeHGlobal(_msaaMenuInfoPtr); + _msaaMenuInfoPtr = IntPtr.Zero; + } + } + + internal override void ItemsChanged(int change) { + base.ItemsChanged(change); + + if (change == CHANGE_ITEMS) { + // when the menu collection changes deal with it locally + Debug.Assert(!created, "base.ItemsChanged should have wiped out our handles"); + if (Parent is { created: true }) { + UpdateMenuItem(force: true); + CreateMenuItems(); + } + } + else { + if (!_hasHandle && IsParent) { + UpdateMenuItem(force: true); + } + } + } + + internal void ItemsChanged(int change, MenuItem item) { + if (change == CHANGE_ITEMADDED && _data is { baseItem: {} } && _data.baseItem.MenuItems.Contains(item)) { + if (Parent is { created: true }) { + UpdateMenuItem(force: true); + CreateMenuItems(); + } + else if (_data != null) { + MenuItem currentMenuItem = _data.firstItem; + while (currentMenuItem != null) { + if (currentMenuItem.created) { + MenuItem newItem = item.CloneMenu(); + item._data.AddItem(newItem); + currentMenuItem.MenuItems.Add(newItem); + break; + } + + currentMenuItem = currentMenuItem._nextLinkedItem; + } + } + } + } + + private void OnClick(EventArgs e) { + CheckIfDisposed(); + + if (_data.baseItem != this) { + _data.baseItem.OnClick(e); + } + else { + _data.onClick?.Invoke(this, e); + } + } + + internal void UpdateMenuItem(bool force) { + if (Parent is not { created: true }) { + return; + } + + if (force || Parent is ContextMenu) { + NativeMethods.MENUITEMINFO_T info = CreateMenuItemInfo(); + NativeMethods.SetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, info); + + if (_hasHandle && info.hSubMenu == IntPtr.Zero) { + ClearHandles(); + } + + _hasHandle = info.hSubMenu != IntPtr.Zero; + } + } + + private void CheckIfDisposed() { + if (_data == null) { + throw new ObjectDisposedException(GetType().FullName); + } + } + + private sealed class MenuItemData : ICommandExecutor { + internal MenuItem baseItem; + internal MenuItem firstItem; + + internal readonly string caption; + internal readonly Shortcut shortcut; + internal readonly bool showShortcut; + internal EventHandler onClick; + internal EventHandler onPopup; + internal EventHandler onSelect; + internal DrawItemEventHandler onDrawItem; + internal MeasureItemEventHandler onMeasureItem; + + private Command2 cmd; + + internal MenuItemData(MenuItem baseItem, Shortcut shortcut, bool showShortcut, string caption, EventHandler onClick, EventHandler onPopup, EventHandler onSelect, DrawItemEventHandler onDrawItem, MeasureItemEventHandler onMeasureItem) { + AddItem(baseItem); + this.shortcut = shortcut; + this.showShortcut = showShortcut; + this.caption = caption ?? string.Empty; + this.onClick = onClick; + this.onPopup = onPopup; + this.onSelect = onSelect; + this.onDrawItem = onDrawItem; + this.onMeasureItem = onMeasureItem; + } + + internal int State { get; private set; } + + internal bool OwnerDraw => (State & StateOwnerDraw) != 0; + + internal bool Visible { + get => (State & StateHidden) == 0; + set { + if (((State & StateHidden) == 0) != value) { + State = value ? State & ~StateHidden : State | StateHidden; + ItemsChanged(CHANGE_VISIBLE); + } + } + } + + internal void AddItem(MenuItem item) { + if (item._data != this) { + item._data?.RemoveItem(item); + + item._nextLinkedItem = firstItem; + firstItem = item; + baseItem ??= item; + + item._data = this; + item.UpdateMenuItem(false); + } + } + + public void Execute() { + baseItem?.OnClick(EventArgs.Empty); + } + + internal int GetMenuID() { + cmd ??= new Command2(this); + return cmd.ID; + } + + private void ItemsChanged(int change) { + for (MenuItem item = firstItem; item != null; item = item._nextLinkedItem) { + item.Parent?.ItemsChanged(change); + } + } + + internal void RemoveItem(MenuItem item) { + Debug.Assert(item._data == this, "bad item passed to MenuItemData.removeItem"); + + if (item == firstItem) { + firstItem = item._nextLinkedItem; + } + else { + MenuItem itemT; + for (itemT = firstItem; item != itemT._nextLinkedItem;) { + itemT = itemT._nextLinkedItem; + } + + itemT._nextLinkedItem = item._nextLinkedItem; + } + + item._nextLinkedItem = null; + item._data = null; + + if (item == baseItem) { + baseItem = firstItem; + } + + if (firstItem == null) { + // No longer needed. Toss all references and the Command object. + Debug.Assert(baseItem == null, "why isn't baseItem null?"); + onClick = null; + onPopup = null; + onSelect = null; + onDrawItem = null; + onMeasureItem = null; + if (cmd != null) { + cmd.Dispose(); + cmd = null; + } + } + } + + internal void SetState(int flag, bool value) { + if (((State & flag) != 0) != value) { + State = value ? State | flag : State & ~flag; + UpdateMenuItems(); + } + } + + private void UpdateMenuItems() { + for (MenuItem item = firstItem; item != null; item = item._nextLinkedItem) { + item.UpdateMenuItem(force: true); + } + } + } + } +} diff --git a/windows/TweetLib.WinForms.Legacy/Windows/Forms/NativeMethods.cs b/windows/TweetLib.WinForms.Legacy/Windows/Forms/NativeMethods.cs new file mode 100644 index 00000000..92f3d6d1 --- /dev/null +++ b/windows/TweetLib.WinForms.Legacy/Windows/Forms/NativeMethods.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace System.Windows.Forms { + [SuppressMessage("ReSharper", "InconsistentNaming")] + [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] + [SuppressMessage("ReSharper", "NotAccessedField.Global")] + [SuppressMessage("ReSharper", "UnusedMember.Global")] + internal static class NativeMethods { + public const int MIIM_STATE = 0x00000001; + public const int MIIM_ID = 0x00000002; + public const int MIIM_SUBMENU = 0x00000004; + public const int MIIM_TYPE = 0x00000010; + public const int MIIM_DATA = 0x00000020; + public const int MF_BYPOSITION = 0x00000400; + public const int MFT_SEPARATOR = 0x00000800; + public const int TPM_RIGHTBUTTON = 0x0002; + public const int TPM_RIGHTALIGN = 0x0008; + public const int TPM_VERTICAL = 0x0040; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public sealed class MENUITEMINFO_T { + public int cbSize = Marshal.SizeOf<MENUITEMINFO_T>(); + public int fMask; + public int fType; + public int fState; + public int wID; + public IntPtr hSubMenu; + public IntPtr hbmpChecked; + public IntPtr hbmpUnchecked; + public IntPtr dwItemData; + public string dwTypeData; + public int cch; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct MSAAMENUINFO { + private const int MSAA_MENU_SIG = (unchecked((int) 0xAA0DF00D)); + + public readonly int dwMSAASignature; + public readonly int cchWText; + public readonly string pszWText; + + public MSAAMENUINFO(string text) { + dwMSAASignature = MSAA_MENU_SIG; + cchWText = text.Length; + pszWText = text; + } + } + + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] + public static extern bool TrackPopupMenuEx(HandleRef hmenu, int fuFlags, int x, int y, HandleRef hwnd, IntPtr tpm); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool InsertMenuItem(HandleRef hMenu, int uItem, bool fByPosition, MENUITEMINFO_T lpmii); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool SetMenuItemInfo(HandleRef hMenu, int uItem, bool fByPosition, MENUITEMINFO_T lpmii); + + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] + public static extern int GetMenuItemCount(HandleRef hMenu); + + [DllImport("user32.dll", ExactSpelling = true)] + public static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] + public static extern bool RemoveMenu(HandleRef hMenu, int uPosition, int uFlags); + + [DllImport("user32.dll", ExactSpelling = true)] + public static extern bool DestroyMenu(HandleRef hMenu); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool PostMessage(HandleRef hwnd, int msg, IntPtr wparam, IntPtr lparam); + } +}