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()