From ba985efe82e8aa3bc4377c42e10ac1b65026be80 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Fri, 9 Mar 2012 20:35:43 +0000 Subject: [PATCH] windows: close status icon menu when clicking elsewhere on the screen --- SparkleShare/Windows/SparkleNotifyIcon.cs | 319 ++++++++++++++++++++++ SparkleShare/Windows/SparkleShare.csproj | 1 + SparkleShare/Windows/SparkleStatusIcon.cs | 74 ++--- 3 files changed, 347 insertions(+), 47 deletions(-) create mode 100644 SparkleShare/Windows/SparkleNotifyIcon.cs diff --git a/SparkleShare/Windows/SparkleNotifyIcon.cs b/SparkleShare/Windows/SparkleNotifyIcon.cs new file mode 100644 index 00000000..571e7aea --- /dev/null +++ b/SparkleShare/Windows/SparkleNotifyIcon.cs @@ -0,0 +1,319 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons +// +// 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 System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; + +using Drawing = System.Drawing; +using Forms = System.Windows.Forms; + +namespace SparkleShare { + + [ContentProperty("Text")] + [DefaultEvent("MouseDoubleClick")] + public class SparkleNotifyIcon : FrameworkElement, IAddChild { + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetModuleHandle(string module_name); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + private static extern int CallNextHookEx (int hook_id, int code, int param, IntPtr data_pointer); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + private static extern int SetWindowsHookEx (int hook_id, HookProc function, IntPtr instance, int thread_id); + + [DllImport("user32.dll", EntryPoint = "DestroyIcon")] + static extern bool DestroyIcon (IntPtr hIcon); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + private static extern int UnhookWindowsHookEx (int hook_id); + + [StructLayout (LayoutKind.Sequential)] + private struct MouseLLHook + { + internal int X; + internal int Y; + internal int MouseData; + internal int Flags; + internal int Time; + internal int Info; + } + + + public Drawing.Bitmap Icon { + set { + this.notify_icon.Icon = GetIconFromBitmap (value); + } + } + + public string Text { + get { + return (string) GetValue (TextProperty); + } + + set { + SetValue (TextProperty, value); + } + } + + public event MouseButtonEventHandler MouseClick { + add { + AddHandler (MouseClickEvent, value); + } + + remove { + RemoveHandler (MouseClickEvent, value); + } + } + + public event MouseButtonEventHandler MouseDoubleClick { + add { + AddHandler (MouseDoubleClickEvent, value); + } + + remove { + RemoveHandler (MouseDoubleClickEvent, value); + } + } + + + public readonly RoutedEvent MouseClickEvent = EventManager.RegisterRoutedEvent ( + "MouseClick", RoutingStrategy.Bubble, typeof (MouseButtonEventHandler), typeof (SparkleNotifyIcon)); + + public readonly RoutedEvent MouseDoubleClickEvent = EventManager.RegisterRoutedEvent( + "MouseDoubleClick", RoutingStrategy.Bubble, typeof (MouseButtonEventHandler), typeof (SparkleNotifyIcon)); + + public readonly DependencyProperty TextProperty = DependencyProperty.Register( + "Text", typeof(string), typeof (SparkleNotifyIcon), new PropertyMetadata (OnTextChanged)); + + + private Forms.NotifyIcon notify_icon; + private HookProc hook_proc_ref; + private int mouse_hook_handle; + private delegate int HookProc (int code, int param, IntPtr struct_pointer); + + + public SparkleNotifyIcon () + { + VisibilityProperty.OverrideMetadata (typeof (SparkleNotifyIcon), + new PropertyMetadata (OnVisibilityChanged)); + + this.notify_icon = new Forms.NotifyIcon () { + Text = Text, + Visible = true + }; + + this.notify_icon.MouseDown += OnMouseDown; + this.notify_icon.MouseUp += OnMouseUp; + this.notify_icon.MouseClick += OnMouseClick; + this.notify_icon.MouseDoubleClick += OnMouseDoubleClick; + + this.hook_proc_ref = OnMouseEventProc; + } + + + public void Dispose() + { + this.notify_icon.Dispose(); + } + + + void IAddChild.AddChild (object value) + { + throw new InvalidOperationException (); + } + + + void IAddChild.AddText (string text) + { + if (text == null) + throw new ArgumentNullException (); + + Text = text; + } + + + private static MouseButtonEventArgs CreateMouseButtonEventArgs( + RoutedEvent handler, Forms.MouseButtons button) + { + MouseButton mouse_button; + + if (button == Forms.MouseButtons.Left) { + mouse_button = MouseButton.Left; + + } else if (button == Forms.MouseButtons.Right) { + mouse_button = MouseButton.Right; + + } else if (button == Forms.MouseButtons.Middle) { + mouse_button = MouseButton.Middle; + + } else if (button == Forms.MouseButtons.XButton1) { + mouse_button = MouseButton.XButton1; + + } else if (button == Forms.MouseButtons.XButton2) { + mouse_button = MouseButton.XButton2; + + } else { + throw new InvalidOperationException (); + } + + return new MouseButtonEventArgs (InputManager.Current.PrimaryMouseDevice, 0, mouse_button) { + RoutedEvent = handler + }; + } + + + private void ShowContextMenu () + { + if (ContextMenu != null) { + ContextMenu.Opened += OnContextMenuOpened; + ContextMenu.Closed += OnContextMenuClosed; + + ContextMenu.Placement = PlacementMode.Mouse; + ContextMenu.IsOpen = true; + } + } + + + private Rect GetContextMenuRect (ContextMenu menu) + { + Point start_point = menu.PointToScreen (new Point (0, 0)); + Point end_point = menu.PointToScreen (new Point (menu.ActualWidth, menu.ActualHeight)); + + return new Rect (start_point, end_point); + } + + + private Point GetHitPoint (IntPtr struct_pointer) + { + MouseLLHook mouse_hook = (MouseLLHook) Marshal.PtrToStructure ( + struct_pointer, typeof (MouseLLHook)); + + return new Point (mouse_hook.X, mouse_hook.Y); + } + + + private int OnMouseEventProc (int code, int button, IntPtr data_pointer) + { + int left_button_down = 0x201; + int right_button_down = 0x204; + + if (button == left_button_down || button == right_button_down) { + Rect context_menu_rect = GetContextMenuRect (ContextMenu); + Point hit_point = GetHitPoint (data_pointer); + + if (!context_menu_rect.Contains (hit_point)) + ContextMenu.IsOpen = false; + } + + return CallNextHookEx (this.mouse_hook_handle, code, button, data_pointer); + } + + + private void OnContextMenuOpened (object sender, RoutedEventArgs args) + { + using (Process process = Process.GetCurrentProcess ()) + using (ProcessModule module = process.MainModule) + { + this.mouse_hook_handle = SetWindowsHookEx (14, this.hook_proc_ref, + GetModuleHandle (module.ModuleName), 0); + } + + if (this.mouse_hook_handle == 0) + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + + + private void OnContextMenuClosed (object sender, RoutedEventArgs args) + { + UnhookWindowsHookEx (this.mouse_hook_handle); + + ContextMenu.Opened -= OnContextMenuOpened; + ContextMenu.Closed -= OnContextMenuClosed; + } + + + private void OnVisibilityChanged (DependencyObject target, + DependencyPropertyChangedEventArgs args) + { + SparkleNotifyIcon control = (SparkleNotifyIcon) target; + control.notify_icon.Visible = (control.Visibility == Visibility.Visible); + } + + + private void OnMouseDown(object sender, Forms.MouseEventArgs args) + { + RaiseEvent (CreateMouseButtonEventArgs (MouseDownEvent, args.Button)); + } + + + private void OnMouseClick (object sender, Forms.MouseEventArgs args) + { + RaiseEvent (CreateMouseButtonEventArgs (MouseClickEvent, args.Button)); + } + + + private void OnMouseDoubleClick (object sender, Forms.MouseEventArgs args) + { + RaiseEvent (CreateMouseButtonEventArgs (MouseDoubleClickEvent, args.Button)); + } + + + private void OnMouseUp (object sender, Forms.MouseEventArgs args) + { + if (args.Button == Forms.MouseButtons.Left || + args.Button == Forms.MouseButtons.Right) { + + ShowContextMenu (); + } + + RaiseEvent (CreateMouseButtonEventArgs (MouseUpEvent, args.Button)); + } + + + protected override void OnVisualParentChanged (DependencyObject parent) + { + base.OnVisualParentChanged (parent); + } + + + private static void OnTextChanged (DependencyObject target, + DependencyPropertyChangedEventArgs args) + { + SparkleNotifyIcon control = (SparkleNotifyIcon) target; + control.notify_icon.Text = control.Text; + } + + + private Drawing.Icon GetIconFromBitmap (Drawing.Bitmap bitmap) + { + IntPtr unmanaged_icon = bitmap.GetHicon (); + Drawing.Icon icon = (Drawing.Icon) Drawing.Icon.FromHandle (unmanaged_icon).Clone (); + DestroyIcon (unmanaged_icon); + + return icon; + } + } +} diff --git a/SparkleShare/Windows/SparkleShare.csproj b/SparkleShare/Windows/SparkleShare.csproj index 0f84b535..04127273 100644 --- a/SparkleShare/Windows/SparkleShare.csproj +++ b/SparkleShare/Windows/SparkleShare.csproj @@ -137,6 +137,7 @@ Program.cs + diff --git a/SparkleShare/Windows/SparkleStatusIcon.cs b/SparkleShare/Windows/SparkleStatusIcon.cs index 46012bf6..aad025b4 100644 --- a/SparkleShare/Windows/SparkleStatusIcon.cs +++ b/SparkleShare/Windows/SparkleStatusIcon.cs @@ -19,6 +19,7 @@ using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows; +using System.Windows.Media; using System.Windows.Controls; using System.Windows.Controls.Primitives; using Forms = System.Windows.Forms; @@ -30,19 +31,18 @@ namespace SparkleShare { public SparkleStatusIconController Controller = new SparkleStatusIconController(); private Forms.Timer Animation; - private Icon [] AnimationFrames; - private Icon ErrorIcon; + private Bitmap [] AnimationFrames; + private Bitmap ErrorIcon; private int FrameNumber; private string StateText; private ContextMenu context_menu; private SparkleMenuItem status_item; private SparkleMenuItem exit_item; - private Forms.NotifyIcon notify_icon = new Forms.NotifyIcon () { - Text = "SparkleShare", - Visible = true - }; - + private SparkleNotifyIcon notify_icon = new SparkleNotifyIcon () { + Text = "SparkleShare" + }; + // Short alias for the translations public static string _ (string s) @@ -54,21 +54,17 @@ namespace SparkleShare { public SparkleStatusIcon () { AnimationFrames = CreateAnimationFrames (); - Animation = CreateAnimation (); - this.notify_icon.Icon = AnimationFrames [0]; - ErrorIcon = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("sparkleshare-syncing-error-windows")); + Animation = CreateAnimation (); + ErrorIcon = SparkleUIHelpers.GetBitmap ("sparkleshare-syncing-error-windows"); - this.notify_icon.MouseClick += delegate { - this.context_menu.Placement = PlacementMode.Mouse; - this.context_menu.IsOpen = true; - }; + this.notify_icon.Icon = AnimationFrames [0]; if (Controller.Folders.Length == 0) - StateText = _("Welcome to SparkleShare!"); - else - StateText = _("Files up to date") + Controller.FolderSize; + StateText = _("Welcome to SparkleShare!"); + else + StateText = _("Files up to date") + Controller.FolderSize; - CreateMenu (); + CreateMenu (); Controller.UpdateQuitItemEvent += delegate (bool enable) { @@ -77,7 +73,7 @@ namespace SparkleShare { this.exit_item.UpdateLayout (); }); }; - + Controller.UpdateMenuEvent += delegate (IconState state) { Dispatcher.Invoke ((Action) delegate { @@ -134,19 +130,15 @@ namespace SparkleShare { } - - // Slices up the graphic that contains the - // animation frames. - private Icon [] CreateAnimationFrames () + private Bitmap [] CreateAnimationFrames () { - Icon [] animation_frames = new Icon [5]; - animation_frames [0] = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-i")); - animation_frames [1] = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-ii")); - animation_frames [2] = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iii")); - animation_frames [3] = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iiii")); - animation_frames [4] = GetIconFromBitmap (SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iiiii")); - - return animation_frames; + return new Bitmap [] { + SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-i"), + SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-ii"), + SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iii"), + SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iiii"), + SparkleUIHelpers.GetBitmap ("process-syncing-sparkleshare-windows-iiiii") + }; } @@ -262,7 +254,7 @@ namespace SparkleShare { i2.Width = 16; i2.Height = 16; subfolder_item.Icon = i2; - /* + /* TODO if (Program.Controller.UnsyncedFolders.Contains (folder_name)) subfolder_item.Icon = Icons.dialog_error_16; else @@ -290,6 +282,8 @@ namespace SparkleShare { this.context_menu.Items.Add (about_item); this.context_menu.Items.Add (new Separator ()); this.context_menu.Items.Add (this.exit_item); + + this.notify_icon.ContextMenu = this.context_menu; } @@ -298,7 +292,7 @@ namespace SparkleShare { // TODO: // - Use the image pointed to by image_path // - Find a way to use the prettier (Win7?) balloons - this.notify_icon.ShowBalloonTip (5 * 1000, title, subtext, Forms.ToolTipIcon.Info); + //this.notify_icon.ShowBalloonTip (5 * 1000, title, subtext, Forms.ToolTipIcon.Info); } @@ -316,20 +310,6 @@ namespace SparkleShare { Controller.SubfolderClicked (folder_name); }; } - - - private Icon GetIconFromBitmap (Bitmap bitmap) - { - IntPtr unmanaged_icon = bitmap.GetHicon (); - Icon icon = (Icon) Icon.FromHandle (unmanaged_icon).Clone (); - DestroyIcon (unmanaged_icon); - - return icon; - } - - - [DllImport("user32.dll", EntryPoint = "DestroyIcon")] - static extern bool DestroyIcon (IntPtr hIcon); }