From 689bb33427f03b4380b77aff69216a7999b29251 Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:13:21 +0300 Subject: [PATCH 1/6] Use NamedPipe, instead of second instance, to invoke preview from command line --- QuickLook/App.xaml.cs | 81 +++++++++++++++---------------- QuickLook/Helpers/PidHelper.cs | 59 ---------------------- QuickLook/PipeServerManager.cs | 89 ++++++++++++++++++++++++++++++++++ QuickLook/QuickLook.csproj | 2 +- 4 files changed, 128 insertions(+), 103 deletions(-) delete mode 100644 QuickLook/Helpers/PidHelper.cs create mode 100644 QuickLook/PipeServerManager.cs diff --git a/QuickLook/App.xaml.cs b/QuickLook/App.xaml.cs index b0ecd3b..85efc4c 100644 --- a/QuickLook/App.xaml.cs +++ b/QuickLook/App.xaml.cs @@ -1,10 +1,10 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Windows; -using QuickLook.Helpers; +using System.Windows.Threading; namespace QuickLook { @@ -16,9 +16,9 @@ namespace QuickLook public static readonly bool Is64Bit = Environment.Is64BitProcess; public static readonly string AppFullPath = Assembly.GetExecutingAssembly().Location; public static readonly string AppPath = Path.GetDirectoryName(AppFullPath); - public static bool RunningAsViewer; - private static bool _duplicated; + private bool _isFirstInstance; + private Mutex _isRunning; protected override void OnStartup(StartupEventArgs e) { @@ -27,7 +27,7 @@ namespace QuickLook MessageBox.Show(((Exception) args.ExceptionObject).Message + Environment.NewLine + ((Exception) args.ExceptionObject).StackTrace); - Current.Shutdown(); + Shutdown(); }; base.OnStartup(e); @@ -35,65 +35,60 @@ namespace QuickLook private void Application_Startup(object sender, StartupEventArgs e) { - if (e.Args.Any()) - if (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First())) - RunAsViewer(e); + EnsureFirstInstance(); + + if (!_isFirstInstance) + { + // second instance: preview this file + if (e.Args.Any() && (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First()))) + RemoteCallShowPreview(e); + // second instance: duplicate else - RunAsListener(e); - else - RunAsListener(e); - } + MessageBox.Show("QuickLook is already running in the background."); - private void RunAsViewer(StartupEventArgs e) - { - RunningAsViewer = true; - - var runningPid = PidHelper.GetRunningInstance(); - if (runningPid != -1) - { - Process.GetProcessById(runningPid).Kill(); - - Current.Shutdown(); + Shutdown(); return; } - PidHelper.WritePid(); + RunListener(e); - ViewWindowManager.GetInstance().InvokeViewer(e.Args.First()); + // second instance: run and preview this file + if (e.Args.Any() && (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First()))) + RemoteCallShowPreview(e); } - private void RunAsListener(StartupEventArgs e) + private void RemoteCallShowPreview(StartupEventArgs e) { - RunningAsViewer = false; - - if (PidHelper.GetRunningInstance() != -1) - { - _duplicated = true; - - MessageBox.Show("QuickLook is already running in the background."); - - Current.Shutdown(); - return; - } - - PidHelper.WritePid(); + PipeServerManager.SendMessage(e.Args.First()); + } + private void RunListener(StartupEventArgs e) + { TrayIconManager.GetInstance(); if (!e.Args.Contains("/autorun")) TrayIconManager.GetInstance().ShowNotification("", "QuickLook is running in the background."); PluginManager.GetInstance(); - BackgroundListener.GetInstance(); + PipeServerManager.GetInstance().MessageReceived += + (msg, ea) => Dispatcher.BeginInvoke( + new Action(() => ViewWindowManager.GetInstance().InvokeViewer(msg as string)), + DispatcherPriority.ApplicationIdle); } private void App_OnExit(object sender, ExitEventArgs e) { - TrayIconManager.GetInstance().Dispose(); - BackgroundListener.GetInstance().Dispose(); + if (_isFirstInstance) + { + PipeServerManager.GetInstance().Dispose(); + TrayIconManager.GetInstance().Dispose(); + BackgroundListener.GetInstance().Dispose(); + } + } - if (!_duplicated) - PidHelper.DeletePid(); + private void EnsureFirstInstance() + { + _isRunning = new Mutex(true, "QuickLook.App.Mutex", out _isFirstInstance); } } } \ No newline at end of file diff --git a/QuickLook/Helpers/PidHelper.cs b/QuickLook/Helpers/PidHelper.cs deleted file mode 100644 index b646dba..0000000 --- a/QuickLook/Helpers/PidHelper.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics; -using System.IO; - -namespace QuickLook.Helpers -{ - internal static class PidHelper - { - private static FileStream _pidLocker; - - private static readonly string PidListener = - Path.Combine(Path.GetTempPath(), "QuickLook.App.Listener.D6EC3F8DDF6B.pid"); - - private static readonly string PidViewer = - Path.Combine(Path.GetTempPath(), "QuickLook.App.Viewer.A6FA53E93515.pid"); - - internal static int GetRunningInstance() - { - var pid = App.RunningAsViewer ? PidViewer : PidListener; - - if (!File.Exists(pid)) - return -1; - - var ppid = -1; - using (var file = File.Open(pid, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - using (var sr = new StreamReader(file)) - { - int.TryParse(sr.ReadToEnd(), out ppid); - } - - try - { - Process.GetProcessById(ppid); - } - catch - { - return -1; - } - - return ppid; - } - - internal static void WritePid() - { - var pidFile = App.RunningAsViewer ? PidViewer : PidListener; - - File.WriteAllText(pidFile, Process.GetCurrentProcess().Id.ToString()); - - _pidLocker = File.Open(pidFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - } - - internal static void DeletePid() - { - _pidLocker?.Close(); - _pidLocker = null; - - File.Delete(App.RunningAsViewer ? PidViewer : PidListener); - } - } -} \ No newline at end of file diff --git a/QuickLook/PipeServerManager.cs b/QuickLook/PipeServerManager.cs new file mode 100644 index 0000000..1c255d6 --- /dev/null +++ b/QuickLook/PipeServerManager.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace QuickLook +{ + internal class PipeServerManager : IDisposable + { + private const string PipeName = "QuickLook.App.Pipe"; + private const string PipeCloseMessage = "QuickLook.App.Pipe.QuitSingal"; + private static PipeServerManager _instance; + + private NamedPipeServerStream _server; + + public PipeServerManager() + { + _server = new NamedPipeServerStream(PipeName, PipeDirection.In); + + new Task(() => + { + using (var reader = new StreamReader(_server)) + { + while (true) + { + Debug.WriteLine("PipeManager: WaitForConnection"); + + _server.WaitForConnection(); + var msg = reader.ReadLine(); + + Debug.WriteLine($"PipeManager: {msg}"); + + if (msg == PipeCloseMessage) + return; + + // dispatch message + MessageReceived?.Invoke(msg, new EventArgs()); + + _server.Disconnect(); + } + } + }).Start(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + if (_server != null) + SendMessage(PipeCloseMessage); + _server?.Dispose(); + _server = null; + } + + public event EventHandler MessageReceived; + + public static void SendMessage(string msg) + { + try + { + using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out)) + { + client.Connect(); + + using (var writer = new StreamWriter(client)) + { + writer.WriteLine(msg); + writer.Flush(); + } + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + ~PipeServerManager() + { + Dispose(); + } + + public static PipeServerManager GetInstance() + { + return _instance ?? (_instance = new PipeServerManager()); + } + } +} \ No newline at end of file diff --git a/QuickLook/QuickLook.csproj b/QuickLook/QuickLook.csproj index 51169ca..1d4a087 100644 --- a/QuickLook/QuickLook.csproj +++ b/QuickLook/QuickLook.csproj @@ -131,7 +131,7 @@ - + From 1c4d16805fa6e38eb0d64ef5eee5f18ef86cb121 Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:26:18 +0300 Subject: [PATCH 2/6] Expose TopMost parameter to outside world --- QuickLook/MainWindowTransparent.xaml.cs | 4 ---- QuickLook/ViewWindowManager.cs | 7 ++++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/QuickLook/MainWindowTransparent.xaml.cs b/QuickLook/MainWindowTransparent.xaml.cs index f5aa3ac..c0bafdf 100644 --- a/QuickLook/MainWindowTransparent.xaml.cs +++ b/QuickLook/MainWindowTransparent.xaml.cs @@ -23,10 +23,6 @@ namespace QuickLook InitializeComponent(); - // do not set TopMost property if we are now debugging. it makes debugging painful... - if (!Debugger.IsAttached) - Topmost = true; - SourceInitialized += (sender, e) => { if (AllowsTransparency) diff --git a/QuickLook/ViewWindowManager.cs b/QuickLook/ViewWindowManager.cs index 9cd5c10..585d358 100644 --- a/QuickLook/ViewWindowManager.cs +++ b/QuickLook/ViewWindowManager.cs @@ -158,7 +158,7 @@ namespace QuickLook } } - internal bool InvokeViewer(string path = null) + internal bool InvokeViewer(string path = null, bool topMost = true) { if (path != null) _path = path; @@ -172,12 +172,12 @@ namespace QuickLook var matchedPlugin = PluginManager.GetInstance().FindMatch(_path); - BeginShowNewWindow(matchedPlugin); + BeginShowNewWindow(matchedPlugin, topMost); return true; } - private void BeginShowNewWindow(IViewer matchedPlugin) + private void BeginShowNewWindow(IViewer matchedPlugin, bool topMost = true) { _currentMainWindow.UnloadPlugin(); @@ -189,6 +189,7 @@ namespace QuickLook if (!ReferenceEquals(oldWindow, _currentMainWindow)) oldWindow.BeginHide(); + _currentMainWindow.Topmost = !Debugger.IsAttached && topMost; _currentMainWindow.BeginShow(matchedPlugin, _path, CurrentPluginFailed); } From 78eba880aa710b102d90b0f4304e52acf6316285 Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:40:10 +0300 Subject: [PATCH 3/6] Expose BalloonTip events to the outside world --- QuickLook/TrayIconManager.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/QuickLook/TrayIconManager.cs b/QuickLook/TrayIconManager.cs index fc0c2f9..b8f181f 100644 --- a/QuickLook/TrayIconManager.cs +++ b/QuickLook/TrayIconManager.cs @@ -47,9 +47,25 @@ namespace QuickLook _icon.Visible = false; } - public void ShowNotification(string title, string content, bool isError = false) + public void ShowNotification(string title, string content, bool isError = false, Action clickEvent = null, + Action closeEvent = null) { _icon.ShowBalloonTip(5000, title, content, isError ? ToolTipIcon.Error : ToolTipIcon.Info); + _icon.BalloonTipClicked += OnIconOnBalloonTipClicked; + _icon.BalloonTipClosed += OnIconOnBalloonTipClosed; + + void OnIconOnBalloonTipClicked(object sender, EventArgs e) + { + clickEvent?.Invoke(); + _icon.BalloonTipClicked -= OnIconOnBalloonTipClicked; + } + + + void OnIconOnBalloonTipClosed(object sender, EventArgs e) + { + closeEvent?.Invoke(); + _icon.BalloonTipClosed -= OnIconOnBalloonTipClosed; + } } internal static TrayIconManager GetInstance() From 9a0b4f23fe485b4f6b37000565304939bc22b46e Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:13:21 +0300 Subject: [PATCH 4/6] Use NamedPipe, instead of second instance, to invoke preview from command line --- QuickLook/App.xaml.cs | 81 +++++++++++++++---------------- QuickLook/Helpers/PidHelper.cs | 59 ---------------------- QuickLook/PipeServerManager.cs | 89 ++++++++++++++++++++++++++++++++++ QuickLook/QuickLook.csproj | 2 +- 4 files changed, 128 insertions(+), 103 deletions(-) delete mode 100644 QuickLook/Helpers/PidHelper.cs create mode 100644 QuickLook/PipeServerManager.cs diff --git a/QuickLook/App.xaml.cs b/QuickLook/App.xaml.cs index b0ecd3b..85efc4c 100644 --- a/QuickLook/App.xaml.cs +++ b/QuickLook/App.xaml.cs @@ -1,10 +1,10 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Windows; -using QuickLook.Helpers; +using System.Windows.Threading; namespace QuickLook { @@ -16,9 +16,9 @@ namespace QuickLook public static readonly bool Is64Bit = Environment.Is64BitProcess; public static readonly string AppFullPath = Assembly.GetExecutingAssembly().Location; public static readonly string AppPath = Path.GetDirectoryName(AppFullPath); - public static bool RunningAsViewer; - private static bool _duplicated; + private bool _isFirstInstance; + private Mutex _isRunning; protected override void OnStartup(StartupEventArgs e) { @@ -27,7 +27,7 @@ namespace QuickLook MessageBox.Show(((Exception) args.ExceptionObject).Message + Environment.NewLine + ((Exception) args.ExceptionObject).StackTrace); - Current.Shutdown(); + Shutdown(); }; base.OnStartup(e); @@ -35,65 +35,60 @@ namespace QuickLook private void Application_Startup(object sender, StartupEventArgs e) { - if (e.Args.Any()) - if (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First())) - RunAsViewer(e); + EnsureFirstInstance(); + + if (!_isFirstInstance) + { + // second instance: preview this file + if (e.Args.Any() && (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First()))) + RemoteCallShowPreview(e); + // second instance: duplicate else - RunAsListener(e); - else - RunAsListener(e); - } + MessageBox.Show("QuickLook is already running in the background."); - private void RunAsViewer(StartupEventArgs e) - { - RunningAsViewer = true; - - var runningPid = PidHelper.GetRunningInstance(); - if (runningPid != -1) - { - Process.GetProcessById(runningPid).Kill(); - - Current.Shutdown(); + Shutdown(); return; } - PidHelper.WritePid(); + RunListener(e); - ViewWindowManager.GetInstance().InvokeViewer(e.Args.First()); + // second instance: run and preview this file + if (e.Args.Any() && (Directory.Exists(e.Args.First()) || File.Exists(e.Args.First()))) + RemoteCallShowPreview(e); } - private void RunAsListener(StartupEventArgs e) + private void RemoteCallShowPreview(StartupEventArgs e) { - RunningAsViewer = false; - - if (PidHelper.GetRunningInstance() != -1) - { - _duplicated = true; - - MessageBox.Show("QuickLook is already running in the background."); - - Current.Shutdown(); - return; - } - - PidHelper.WritePid(); + PipeServerManager.SendMessage(e.Args.First()); + } + private void RunListener(StartupEventArgs e) + { TrayIconManager.GetInstance(); if (!e.Args.Contains("/autorun")) TrayIconManager.GetInstance().ShowNotification("", "QuickLook is running in the background."); PluginManager.GetInstance(); - BackgroundListener.GetInstance(); + PipeServerManager.GetInstance().MessageReceived += + (msg, ea) => Dispatcher.BeginInvoke( + new Action(() => ViewWindowManager.GetInstance().InvokeViewer(msg as string)), + DispatcherPriority.ApplicationIdle); } private void App_OnExit(object sender, ExitEventArgs e) { - TrayIconManager.GetInstance().Dispose(); - BackgroundListener.GetInstance().Dispose(); + if (_isFirstInstance) + { + PipeServerManager.GetInstance().Dispose(); + TrayIconManager.GetInstance().Dispose(); + BackgroundListener.GetInstance().Dispose(); + } + } - if (!_duplicated) - PidHelper.DeletePid(); + private void EnsureFirstInstance() + { + _isRunning = new Mutex(true, "QuickLook.App.Mutex", out _isFirstInstance); } } } \ No newline at end of file diff --git a/QuickLook/Helpers/PidHelper.cs b/QuickLook/Helpers/PidHelper.cs deleted file mode 100644 index b646dba..0000000 --- a/QuickLook/Helpers/PidHelper.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics; -using System.IO; - -namespace QuickLook.Helpers -{ - internal static class PidHelper - { - private static FileStream _pidLocker; - - private static readonly string PidListener = - Path.Combine(Path.GetTempPath(), "QuickLook.App.Listener.D6EC3F8DDF6B.pid"); - - private static readonly string PidViewer = - Path.Combine(Path.GetTempPath(), "QuickLook.App.Viewer.A6FA53E93515.pid"); - - internal static int GetRunningInstance() - { - var pid = App.RunningAsViewer ? PidViewer : PidListener; - - if (!File.Exists(pid)) - return -1; - - var ppid = -1; - using (var file = File.Open(pid, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - using (var sr = new StreamReader(file)) - { - int.TryParse(sr.ReadToEnd(), out ppid); - } - - try - { - Process.GetProcessById(ppid); - } - catch - { - return -1; - } - - return ppid; - } - - internal static void WritePid() - { - var pidFile = App.RunningAsViewer ? PidViewer : PidListener; - - File.WriteAllText(pidFile, Process.GetCurrentProcess().Id.ToString()); - - _pidLocker = File.Open(pidFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - } - - internal static void DeletePid() - { - _pidLocker?.Close(); - _pidLocker = null; - - File.Delete(App.RunningAsViewer ? PidViewer : PidListener); - } - } -} \ No newline at end of file diff --git a/QuickLook/PipeServerManager.cs b/QuickLook/PipeServerManager.cs new file mode 100644 index 0000000..1c255d6 --- /dev/null +++ b/QuickLook/PipeServerManager.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace QuickLook +{ + internal class PipeServerManager : IDisposable + { + private const string PipeName = "QuickLook.App.Pipe"; + private const string PipeCloseMessage = "QuickLook.App.Pipe.QuitSingal"; + private static PipeServerManager _instance; + + private NamedPipeServerStream _server; + + public PipeServerManager() + { + _server = new NamedPipeServerStream(PipeName, PipeDirection.In); + + new Task(() => + { + using (var reader = new StreamReader(_server)) + { + while (true) + { + Debug.WriteLine("PipeManager: WaitForConnection"); + + _server.WaitForConnection(); + var msg = reader.ReadLine(); + + Debug.WriteLine($"PipeManager: {msg}"); + + if (msg == PipeCloseMessage) + return; + + // dispatch message + MessageReceived?.Invoke(msg, new EventArgs()); + + _server.Disconnect(); + } + } + }).Start(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + + if (_server != null) + SendMessage(PipeCloseMessage); + _server?.Dispose(); + _server = null; + } + + public event EventHandler MessageReceived; + + public static void SendMessage(string msg) + { + try + { + using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out)) + { + client.Connect(); + + using (var writer = new StreamWriter(client)) + { + writer.WriteLine(msg); + writer.Flush(); + } + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + ~PipeServerManager() + { + Dispose(); + } + + public static PipeServerManager GetInstance() + { + return _instance ?? (_instance = new PipeServerManager()); + } + } +} \ No newline at end of file diff --git a/QuickLook/QuickLook.csproj b/QuickLook/QuickLook.csproj index 3662ed6..b2fa8c8 100644 --- a/QuickLook/QuickLook.csproj +++ b/QuickLook/QuickLook.csproj @@ -134,7 +134,7 @@ - + From eb6524f15c4e01254c1293cfd340bc34a386ffe7 Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:26:18 +0300 Subject: [PATCH 5/6] Expose TopMost parameter to outside world --- QuickLook/MainWindowTransparent.xaml.cs | 4 ---- QuickLook/ViewWindowManager.cs | 7 ++++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/QuickLook/MainWindowTransparent.xaml.cs b/QuickLook/MainWindowTransparent.xaml.cs index f5aa3ac..c0bafdf 100644 --- a/QuickLook/MainWindowTransparent.xaml.cs +++ b/QuickLook/MainWindowTransparent.xaml.cs @@ -23,10 +23,6 @@ namespace QuickLook InitializeComponent(); - // do not set TopMost property if we are now debugging. it makes debugging painful... - if (!Debugger.IsAttached) - Topmost = true; - SourceInitialized += (sender, e) => { if (AllowsTransparency) diff --git a/QuickLook/ViewWindowManager.cs b/QuickLook/ViewWindowManager.cs index 9cd5c10..585d358 100644 --- a/QuickLook/ViewWindowManager.cs +++ b/QuickLook/ViewWindowManager.cs @@ -158,7 +158,7 @@ namespace QuickLook } } - internal bool InvokeViewer(string path = null) + internal bool InvokeViewer(string path = null, bool topMost = true) { if (path != null) _path = path; @@ -172,12 +172,12 @@ namespace QuickLook var matchedPlugin = PluginManager.GetInstance().FindMatch(_path); - BeginShowNewWindow(matchedPlugin); + BeginShowNewWindow(matchedPlugin, topMost); return true; } - private void BeginShowNewWindow(IViewer matchedPlugin) + private void BeginShowNewWindow(IViewer matchedPlugin, bool topMost = true) { _currentMainWindow.UnloadPlugin(); @@ -189,6 +189,7 @@ namespace QuickLook if (!ReferenceEquals(oldWindow, _currentMainWindow)) oldWindow.BeginHide(); + _currentMainWindow.Topmost = !Debugger.IsAttached && topMost; _currentMainWindow.BeginShow(matchedPlugin, _path, CurrentPluginFailed); } From ad083d8a5719ae51d3610c3072b5986e4c179e03 Mon Sep 17 00:00:00 2001 From: Paddy Xu Date: Fri, 9 Jun 2017 21:40:10 +0300 Subject: [PATCH 6/6] Expose BalloonTip events to the outside world --- QuickLook/TrayIconManager.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/QuickLook/TrayIconManager.cs b/QuickLook/TrayIconManager.cs index 679bc70..6705778 100644 --- a/QuickLook/TrayIconManager.cs +++ b/QuickLook/TrayIconManager.cs @@ -47,9 +47,25 @@ namespace QuickLook _icon.Visible = false; } - public void ShowNotification(string title, string content, bool isError = false) + public void ShowNotification(string title, string content, bool isError = false, Action clickEvent = null, + Action closeEvent = null) { _icon.ShowBalloonTip(5000, title, content, isError ? ToolTipIcon.Error : ToolTipIcon.Info); + _icon.BalloonTipClicked += OnIconOnBalloonTipClicked; + _icon.BalloonTipClosed += OnIconOnBalloonTipClosed; + + void OnIconOnBalloonTipClicked(object sender, EventArgs e) + { + clickEvent?.Invoke(); + _icon.BalloonTipClicked -= OnIconOnBalloonTipClicked; + } + + + void OnIconOnBalloonTipClosed(object sender, EventArgs e) + { + closeEvent?.Invoke(); + _icon.BalloonTipClosed -= OnIconOnBalloonTipClosed; + } } internal static TrayIconManager GetInstance()