From 370c76b6fa0f208bc2682c6db0ad43dbe26e6e0d Mon Sep 17 00:00:00 2001 From: ema Date: Sat, 14 Dec 2024 05:45:01 +0800 Subject: [PATCH] Readjust the position of TrayIcon ContextMenu Added support to disable the `ModernTrayIcon` by setting it to false --- QuickLook/App.xaml.cs | 5 +- QuickLook/Helpers/TrayIconPatcher.cs | 298 +++++++++++++++++++++++++++ QuickLook/TrayIconManager.cs | 19 +- 3 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 QuickLook/Helpers/TrayIconPatcher.cs diff --git a/QuickLook/App.xaml.cs b/QuickLook/App.xaml.cs index d94f5c4..9149df0 100644 --- a/QuickLook/App.xaml.cs +++ b/QuickLook/App.xaml.cs @@ -57,9 +57,12 @@ public partial class App : Application ProcessHelper.WriteLog(((Exception)args.ExceptionObject).ToString()); }; - bool modernMessageBox = SettingHelper.Get("ModernMessageBox", true, "QuickLook"); // Initialize MessageBox patching + bool modernMessageBox = SettingHelper.Get("ModernMessageBox", true, "QuickLook"); if (modernMessageBox) MessageBoxPatcher.Initialize(); + // Initialize TrayIcon patching + bool modernTrayIcon = SettingHelper.Get("ModernTrayIcon", true, "QuickLook"); + if (modernTrayIcon) TrayIconPatcher.Initialize(); // Set initial theme based on system settings ThemeManager.Apply(OSThemeHelper.AppsUseDarkTheme() ? ApplicationTheme.Dark : ApplicationTheme.Light); diff --git a/QuickLook/Helpers/TrayIconPatcher.cs b/QuickLook/Helpers/TrayIconPatcher.cs new file mode 100644 index 0000000..62e8ea5 --- /dev/null +++ b/QuickLook/Helpers/TrayIconPatcher.cs @@ -0,0 +1,298 @@ +// Copyright © 2024 ema +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Windows.Forms; + +namespace QuickLook.Helpers; + +public static class TrayIconPatcher +{ + private static readonly Harmony Harmony = new("com.quicklook.trayicon.patch"); + + public static void Initialize() + { + var targetMethod = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.NonPublic | BindingFlags.Instance); + + if (targetMethod != null) + { + _ = Harmony.Patch(targetMethod, transpiler: new HarmonyMethod(typeof(TrayIconPatcher).GetMethod(nameof(ShowContextMenuTranspiler)))); + } + } + + public static void ShowContextMenu(this NotifyIcon icon) + { + var targetMethod = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.NonPublic | BindingFlags.Instance); + + targetMethod?.Invoke(icon, []); + } + + /// + /// We need to change the NotifyIcon.ShowContextMenu to use TPM_RIGHTBUTTON instead of TPM_VERTICAL. + /// private void ShowContextMenu() + /// { + /// if (contextMenu != null || contextMenuStrip != null) + /// { + /// NativeMethods.POINT pOINT = new NativeMethods.POINT(); + /// UnsafeNativeMethods.GetCursorPos(pOINT); + /// UnsafeNativeMethods.SetForegroundWindow(new HandleRef(window, window.Handle)); + /// if (contextMenu != null) + /// { + /// contextMenu.OnPopup(EventArgs.Empty); + /// SafeNativeMethods.TrackPopupMenuEx(new HandleRef(contextMenu, contextMenu.Handle), 72, pOINT.x, pOINT.y, new HandleRef(window, window.Handle), null); + /// UnsafeNativeMethods.PostMessage(new HandleRef(window, window.Handle), 0, IntPtr.Zero, IntPtr.Zero); + /// } + /// else if (contextMenuStrip != null) + /// { + /// contextMenuStrip.ShowInTaskbar(pOINT.x, pOINT.y); + /// } + /// } + /// } + /// --- + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenu contextMenu + /// Opcode: brtrue.s, Operand: System.Reflection.Emit.Label + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenuStrip contextMenuStrip + /// Opcode: brfalse, Operand: System.Reflection.Emit.Label + /// Opcode: newobj, Operand: Void.ctor() + /// Opcode: stloc.0, Operand: + /// Opcode: ldloc.0, Operand: + /// Opcode: call, Operand: Boolean GetCursorPos(POINT) + /// Opcode: pop, Operand: + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: callvirt, Operand: IntPtr get_Handle() + /// Opcode: newobj, Operand: Void.ctor(System.Object, IntPtr) + /// Opcode: call, Operand: Boolean SetForegroundWindow(System.Runtime.InteropServices.HandleRef) + /// Opcode: pop, Operand: + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenu contextMenu + /// Opcode: brfalse.s, Operand: System.Reflection.Emit.Label + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenu contextMenu + /// Opcode: ldsfld, Operand: System.EventArgs Empty + /// Opcode: callvirt, Operand: Void OnPopup(System.EventArgs) + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenu contextMenu + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenu contextMenu + /// Opcode: callvirt, Operand: IntPtr get_Handle() + /// Opcode: newobj, Operand: Void.ctor(System.Object, IntPtr) + /// Opcode: ldc.i4.s, Operand: 72 + /// Opcode: ldloc.0, Operand: + /// Opcode: ldfld, Operand: Int32 x + /// Opcode: ldloc.0, Operand: + /// Opcode: ldfld, Operand: Int32 y + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: callvirt, Operand: IntPtr get_Handle() + /// Opcode: newobj, Operand: Void.ctor(System.Object, IntPtr) + /// Opcode: ldnull, Operand: + /// Opcode: call, Operand: Boolean TrackPopupMenuEx(System.Runtime.InteropServices.HandleRef, Int32, Int32, Int32, System.Runtime.InteropServices.HandleRef, TPMPARAMS) + /// Opcode: pop, Operand: + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: NotifyIconNativeWindow window + /// Opcode: callvirt, Operand: IntPtr get_Handle() + /// Opcode: newobj, Operand: Void.ctor(System.Object, IntPtr) + /// Opcode: ldc.i4.0, Operand: + /// Opcode: ldsfld, Operand: IntPtr Zero + /// Opcode: ldsfld, Operand: IntPtr Zero + /// Opcode: call, Operand: Boolean PostMessage(System.Runtime.InteropServices.HandleRef, Int32, IntPtr, IntPtr) + /// Opcode: pop, Operand: + /// Opcode: ret, Operand: + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenuStrip contextMenuStrip + /// Opcode: brfalse.s, Operand: System.Reflection.Emit.Label + /// Opcode: ldarg.0, Operand: + /// Opcode: ldfld, Operand: System.Windows.Forms.ContextMenuStrip contextMenuStrip + /// Opcode: ldloc.0, Operand: + /// Opcode: ldfld, Operand: Int32 x + /// Opcode: ldloc.0, Operand: + /// Opcode: ldfld, Operand: Int32 y + /// Opcode: callvirt, Operand: Void ShowInTaskbar(Int32, Int32) + /// Opcode: ret, Operand: + /// + /// + /// + [HarmonyTranspiler] + public static IEnumerable ShowContextMenuTranspiler(IEnumerable instructions) + { + try + { + var codes = instructions.ToArray(); + + if (Debugger.IsAttached) + { + // Test code to print the IL codes + foreach (var code in codes) + { + Debug.WriteLine($"Opcode: {code.opcode}, Operand: {code.operand}"); + } + } + + // How to change source code for proxy: + // from SafeNativeMethods.TrackPopupMenuEx(new HandleRef(contextMenu, contextMenu.Handle), 72, pOINT.x, pOINT.y, new HandleRef(window, window.Handle), null); + // to SafeNativeMethods.TrackPopupMenuEx(new HandleRef(contextMenu, contextMenu.Handle), 66, pOINT.x, pOINT.y, new HandleRef(window, window.Handle), null); + // [TrackPopupMenuEx] uFlags: A set of flags that determine how the menu behaves. + // The number 72 in binary is 0100 1000. Breaking it down: + // * TPM_VERTICAL(0x0040) + // * TPM_RIGHTALIGN(0x0008) + // The number 64 in binary is 0100 0000. Breaking it down: + // * TPM_VERTICAL(0x0040) + // * TPM_LEFTALIGN(0x0000) + const sbyte sourceFlag = (sbyte)(TrackPopupMenuFlags.TPM_VERTICAL | TrackPopupMenuFlags.TPM_RIGHTALIGN); + const sbyte targetFlag = (sbyte)(TrackPopupMenuFlags.TPM_VERTICAL | TrackPopupMenuFlags.TPM_LEFTALIGN); + + for (int i = 0; i < codes.Length; i++) + { + if (codes[i].opcode == OpCodes.Ldc_I4_S && (sbyte)codes[i].operand == sourceFlag) + { + codes[i].operand = targetFlag; + break; + } + } + + return codes; + } + catch + { + // No fallback needed in this case + } + + return instructions; + } + + /// + /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackpopupmenuex + /// + [Flags] + private enum TrackPopupMenuFlags : uint + { + /// + /// The user can select menu items with only the left mouse button. + /// + TPM_LEFTBUTTON = 0x0000, + + /// + /// The user can select menu items with both the left and right mouse buttons. + /// + TPM_RIGHTBUTTON = 0x0002, + + /// + /// Positions the shortcut menu so that its left side is aligned with the coordinate specified by the x parameter. + /// + TPM_LEFTALIGN = 0x0000, + + /// + /// Centers the shortcut menu horizontally relative to the coordinate specified by the x parameter. + /// + TPM_CENTERALIGN = 0x0004, + + /// + /// Positions the shortcut menu so that its right side is aligned with the coordinate specified by the x parameter. + /// + TPM_RIGHTALIGN = 0x0008, + + /// + /// Positions the shortcut menu so that its top side is aligned with the coordinate specified by the y parameter. + /// + TPM_TOPALIGN = 0x0000, + + /// + /// Centers the shortcut menu vertically relative to the coordinate specified by the y parameter. + /// + TPM_VCENTERALIGN = 0x0010, + + /// + /// Positions the shortcut menu so that its bottom side is aligned with the coordinate specified by the y parameter. + /// + TPM_BOTTOMALIGN = 0x0020, + + /// + /// TPM_HORIZONTAL + /// + TPM_HORIZONTAL = 0x0000, + + /// + /// TPM_VERTICAL + /// + TPM_VERTICAL = 0x0040, + + /// + /// The function does not send notification messages when the user clicks a menu item. + /// + TPM_NONOTIFY = 0x0080, + + /// + /// The function returns the menu item identifier of the user's selection in the return value. + /// + TPM_RETURNCMD = 0x0100, + + /// + /// TPM_RECURSE + /// + TPM_RECURSE = 0x0001, + + /// + /// Animates the menu from left to right. + /// + TPM_HORPOSANIMATION = 0x0400, + + /// + /// Animates the menu from right to left. + /// + TPM_HORNEGANIMATION = 0x0800, + + /// + /// Animates the menu from top to bottom. + /// + TPM_VERPOSANIMATION = 0x1000, + + /// + /// Animates the menu from bottom to top. + /// + TPM_VERNEGANIMATION = 0x2000, + + /// + /// Displays menu without animation. + /// + TPM_NOANIMATION = 0x4000, + + /// + /// TPM_LAYOUTRTL + /// + TPM_LAYOUTRTL = 0x8000, + + /// + /// TPM_WORKAREA + /// + TPM_WORKAREA = 0x10000, + } +} diff --git a/QuickLook/TrayIconManager.cs b/QuickLook/TrayIconManager.cs index 5b6cb42..94de5d7 100644 --- a/QuickLook/TrayIconManager.cs +++ b/QuickLook/TrayIconManager.cs @@ -57,17 +57,32 @@ internal class TrayIconManager : IDisposable new MenuItem("-"), new MenuItem(TranslationHelper.Get("Icon_CheckUpdate"), (_, _) => Updater.CheckForUpdates()), new MenuItem(TranslationHelper.Get("Icon_GetPlugin"), - (sender, e) => Process.Start("https://github.com/QL-Win/QuickLook/wiki/Available-Plugins")), + (_, _) => Process.Start("https://github.com/QL-Win/QuickLook/wiki/Available-Plugins")), new MenuItem(TranslationHelper.Get("Icon_OpenDataFolder"), (_, _) => Process.Start("explorer.exe", SettingHelper.LocalDataPath)), _itemAutorun, new MenuItem(TranslationHelper.Get("Icon_Restart"), (_, _) => Restart(forced: true)), new MenuItem(TranslationHelper.Get("Icon_Quit"), - (sender, e) => System.Windows.Application.Current.Shutdown()) + (_, _) => System.Windows.Application.Current.Shutdown()) ]), Visible = SettingHelper.Get("ShowTrayIcon", true) }; _icon.ContextMenu.Popup += (sender, e) => { _itemAutorun.Checked = AutoStartupHelper.IsAutorun(); }; + + // Readjust the display position of ContextMenu + if (SettingHelper.Get("ModernTrayIcon", true, "QuickLook")) + { + _icon.MouseDown += (_, e) => + { + if (e.Button == MouseButtons.Right) + { + // Call ShowContextMenu here will be later than the native call, + // so here can readjust the ContextMenu position. + // You can check the source code to determine the behavior. + _icon.ShowContextMenu(); + } + }; + } } public void Dispose()