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);
+	}
+}