Refactor tray icon to use TrayIconHost

This commit is contained in:
ema
2025-07-20 21:54:32 +08:00
parent 55a069046f
commit 8c95a42a64
4 changed files with 62 additions and 348 deletions

View File

@@ -213,9 +213,6 @@ public partial class App : Application
// 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);
@@ -223,6 +220,9 @@ public partial class App : Application
ThemeManager.Apply(OSThemeHelper.AppsUseDarkTheme() ? ApplicationTheme.Dark : ApplicationTheme.Light);
UxTheme.ApplyPreferredAppMode();
// Initialize TrayIcon
_ = TrayIconManager.GetInstance();
base.OnStartup(e);
}
@@ -240,7 +240,7 @@ public partial class App : Application
CheckUpdate();
RunListener(e);
// first instance: run and preview this file
// First instance: run and preview this file
if (e.Args.Any() && (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First())))
PipeServerManager.SendMessage(PipeMessages.Toggle, e.Args.First());
}
@@ -318,12 +318,12 @@ public partial class App : Application
if (isFirst)
return true;
// second instance: preview this file
// Second instance: preview this file
if (args.Any() && (Directory.Exists(args.First()) || File.Exists(args.First())))
{
PipeServerManager.SendMessage(PipeMessages.Toggle, args.First(), [.. args.Skip(1)]);
}
// second instance: duplicate
// Second instance: duplicate
else
MessageBox.Show(TranslationHelper.Get("APP_SECOND_TEXT"), TranslationHelper.Get("APP_SECOND"),
MessageBoxButton.OK, MessageBoxImage.Information);

View File

@@ -1,298 +0,0 @@
// Copyright © 2017-2025 QL-Win Contributors
//
// 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 <http://www.gnu.org/licenses/>.
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, []);
}
/// <summary>
/// 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:
/// </summary>
/// <param name="instructions"></param>
/// <returns></returns>
[HarmonyTranspiler]
public static IEnumerable<CodeInstruction> ShowContextMenuTranspiler(IEnumerable<CodeInstruction> 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;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackpopupmenuex
/// </summary>
[Flags]
private enum TrackPopupMenuFlags : uint
{
/// <summary>
/// The user can select menu items with only the left mouse button.
/// </summary>
TPM_LEFTBUTTON = 0x0000,
/// <summary>
/// The user can select menu items with both the left and right mouse buttons.
/// </summary>
TPM_RIGHTBUTTON = 0x0002,
/// <summary>
/// Positions the shortcut menu so that its left side is aligned with the coordinate specified by the x parameter.
/// </summary>
TPM_LEFTALIGN = 0x0000,
/// <summary>
/// Centers the shortcut menu horizontally relative to the coordinate specified by the x parameter.
/// </summary>
TPM_CENTERALIGN = 0x0004,
/// <summary>
/// Positions the shortcut menu so that its right side is aligned with the coordinate specified by the x parameter.
/// </summary>
TPM_RIGHTALIGN = 0x0008,
/// <summary>
/// Positions the shortcut menu so that its top side is aligned with the coordinate specified by the y parameter.
/// </summary>
TPM_TOPALIGN = 0x0000,
/// <summary>
/// Centers the shortcut menu vertically relative to the coordinate specified by the y parameter.
/// </summary>
TPM_VCENTERALIGN = 0x0010,
/// <summary>
/// Positions the shortcut menu so that its bottom side is aligned with the coordinate specified by the y parameter.
/// </summary>
TPM_BOTTOMALIGN = 0x0020,
/// <summary>
/// TPM_HORIZONTAL
/// </summary>
TPM_HORIZONTAL = 0x0000,
/// <summary>
/// TPM_VERTICAL
/// </summary>
TPM_VERTICAL = 0x0040,
/// <summary>
/// The function does not send notification messages when the user clicks a menu item.
/// </summary>
TPM_NONOTIFY = 0x0080,
/// <summary>
/// The function returns the menu item identifier of the user's selection in the return value.
/// </summary>
TPM_RETURNCMD = 0x0100,
/// <summary>
/// TPM_RECURSE
/// </summary>
TPM_RECURSE = 0x0001,
/// <summary>
/// Animates the menu from left to right.
/// </summary>
TPM_HORPOSANIMATION = 0x0400,
/// <summary>
/// Animates the menu from right to left.
/// </summary>
TPM_HORNEGANIMATION = 0x0800,
/// <summary>
/// Animates the menu from top to bottom.
/// </summary>
TPM_VERPOSANIMATION = 0x1000,
/// <summary>
/// Animates the menu from bottom to top.
/// </summary>
TPM_VERNEGANIMATION = 0x2000,
/// <summary>
/// Displays menu without animation.
/// </summary>
TPM_NOANIMATION = 0x4000,
/// <summary>
/// TPM_LAYOUTRTL
/// </summary>
TPM_LAYOUTRTL = 0x8000,
/// <summary>
/// TPM_WORKAREA
/// </summary>
TPM_WORKAREA = 0x10000,
}
}

View File

@@ -102,7 +102,7 @@
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="WPF-UI.Violeta" Version="4.0.3">
<PackageReference Include="WPF-UI.Violeta" Version="4.0.3.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Lib.Harmony" Version="2.3.6">

View File

@@ -15,79 +15,91 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using QuickLook.Common.Commands;
using QuickLook.Common.Helpers;
using QuickLook.Helpers;
using QuickLook.Properties;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Wpf.Ui.Violeta.Win32;
using ToolTipIcon = Wpf.Ui.Violeta.Win32.ToolTipIcon;
namespace QuickLook;
internal class TrayIconManager : IDisposable
internal partial class TrayIconManager : IDisposable
{
private static TrayIconManager _instance;
private readonly NotifyIcon _icon;
private readonly TrayIconHost _icon;
private readonly MenuItem _itemAutorun =
new(TranslationHelper.Get("Icon_RunAtStartup"),
(sender, e) =>
private readonly TrayMenuItem _itemAutorun =
new()
{
Header = TranslationHelper.Get("Icon_RunAtStartup"),
Command = new RelayCommand(() =>
{
if (AutoStartupHelper.IsAutorun())
AutoStartupHelper.RemoveAutorunShortcut();
else
AutoStartupHelper.CreateAutorunShortcut();
})
{ Enabled = !App.IsUWP };
}),
IsEnabled = !App.IsUWP,
};
private TrayIconManager()
{
_icon = new NotifyIcon
_icon = new TrayIconHost
{
Text = string.Format(TranslationHelper.Get("Icon_ToolTip"),
ToolTipText = string.Format(TranslationHelper.Get("Icon_ToolTip"),
Application.ProductVersion),
Icon = GetTrayIconByDPI(),
ContextMenu = new ContextMenu(
Menu =
[
new MenuItem($"v{Application.ProductVersion}{(App.IsUWP ? " (UWP)" : string.Empty)}") {Enabled = false},
new MenuItem("-"),
new MenuItem(TranslationHelper.Get("Icon_CheckUpdate"), (_, _) => Updater.CheckForUpdates()),
new MenuItem(TranslationHelper.Get("Icon_GetPlugin"),
(_, _) => Process.Start("https://github.com/QL-Win/QuickLook/wiki/Available-Plugins")),
new MenuItem(TranslationHelper.Get("Icon_OpenDataFolder"), (_, _) => Process.Start("explorer.exe", SettingHelper.LocalDataPath)),
new TrayMenuItem()
{
Header = $"v{Application.ProductVersion}{(App.IsUWP ? " (UWP)" : string.Empty)}",
IsEnabled = false,
},
new TraySeparator(),
new TrayMenuItem()
{
Header = TranslationHelper.Get("Icon_CheckUpdate"),
Command = new RelayCommand(() => Updater.CheckForUpdates()),
},
new TrayMenuItem()
{
Header = TranslationHelper.Get("Icon_GetPlugin"),
Command = new RelayCommand(() => Process.Start("https://github.com/QL-Win/QuickLook/wiki/Available-Plugins")),
},
new TrayMenuItem()
{
Header = TranslationHelper.Get("Icon_OpenDataFolder"),
Command = new RelayCommand(() => Process.Start("explorer.exe", SettingHelper.LocalDataPath)),
},
_itemAutorun,
new MenuItem(TranslationHelper.Get("Icon_Restart"), (_, _) => Restart(forced: true)),
new MenuItem(TranslationHelper.Get("Icon_Quit"),
(_, _) => System.Windows.Application.Current.Shutdown())
]),
Visible = SettingHelper.Get("ShowTrayIcon", true)
new TrayMenuItem()
{
Header = TranslationHelper.Get("Icon_Restart"),
Command = new RelayCommand(() => Restart(forced: true)),
},
new TrayMenuItem()
{
Header = TranslationHelper.Get("Icon_Quit"),
Command = new RelayCommand(System.Windows.Application.Current.Shutdown),
}
],
IsVisible = 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();
}
};
}
_icon.RightDown += (sender, e) => { _itemAutorun.IsChecked = AutoStartupHelper.IsAutorun(); };
}
public void Dispose()
{
_icon.Visible = false;
_icon.IsVisible = false;
}
public void Restart(string fileName = null, string dir = null, string args = null, int? exitCode = null, bool forced = false)
@@ -118,16 +130,16 @@ internal class TrayIconManager : IDisposable
Environment.Exit(exitCode ?? 'r' + 'e' + 's' + 't' + 'a' + 'r' + 't');
}
private Icon GetTrayIconByDPI()
private nint GetTrayIconByDPI()
{
var scale = DisplayDeviceHelper.GetCurrentScaleFactor().Vertical;
if (!App.IsWin10)
return scale > 1 ? Resources.app : Resources.app_16;
return scale > 1 ? Resources.app.Handle : Resources.app_16.Handle;
return OSThemeHelper.SystemUsesDarkTheme()
? (scale > 1 ? Resources.app_white : Resources.app_white_16)
: (scale > 1 ? Resources.app_black : Resources.app_black_16);
? (scale > 1 ? Resources.app_white.Handle : Resources.app_white_16.Handle)
: (scale > 1 ? Resources.app_black.Handle : Resources.app_black_16.Handle);
}
public static void ShowNotification(string title, string content, bool isError = false, int timeout = 5000,