windows: close status icon menu when clicking elsewhere on the screen

This commit is contained in:
Hylke Bons 2012-03-09 20:35:43 +00:00
parent 5b0ccc607c
commit ba985efe82
3 changed files with 347 additions and 47 deletions

View file

@ -0,0 +1,319 @@
// SparkleShare, a collaboration and sharing tool.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// 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 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;
}
}
}

View file

@ -137,6 +137,7 @@
<Compile Include="..\Program.cs">
<Link>Program.cs</Link>
</Compile>
<Compile Include="SparkleNotifyIcon.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SparkleLib\windows\SparkleLib.csproj">

View file

@ -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);
}