1 // Permission is hereby granted, free of charge, to any person obtaining
2 // a copy of this software and associated documentation files (the
3 // "Software"), to deal in the Software without restriction, including
4 // without limitation the rights to use, copy, modify, merge, publish,
5 // distribute, sublicense, and/or sell copies of the Software, and to
6 // permit persons to whom the Software is furnished to do so, subject to
7 // the following conditions:
9 // The above copyright notice and this permission notice shall be
10 // included in all copies or substantial portions of the Software.
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 // Copyright (c) 2004-2005 Novell, Inc.
23 // Jordi Mas i Hernandez, jordi@ximian.com
24 // Mike Kestner <mkestner@novell.com>
25 // Everaldo Canuto <ecanuto@novell.com>
28 using System.Collections;
30 using System.Threading;
32 namespace System.Windows.Forms {
35 When writing this code the Wine project was of great help to
36 understand the logic behind some Win32 issues. Thanks to them. Jordi,
38 internal class MenuTracker {
41 internal bool popup_active;
42 internal bool popdown_menu;
43 internal bool hotkey_active;
44 public Menu CurrentMenu;
48 public MenuTracker (Menu top_menu)
50 TopMenu = CurrentMenu = top_menu;
51 foreach (MenuItem item in TopMenu.MenuItems)
54 if (top_menu is ContextMenu) {
55 grab_control = (top_menu as ContextMenu).SourceControl.FindForm ();
56 grab_control.ActiveTracker = this;
58 grab_control = top_menu.Wnd.FindForm ();
68 KeyNavState keynav_state = KeyNavState.Idle;
70 public bool Navigating {
71 get { return keynav_state != KeyNavState.Idle || active; }
74 internal static Point ScreenToMenu (Menu menu, Point pnt)
78 XplatUI.ScreenToMenu (menu.Wnd.window.Handle, ref x, ref y);
79 return new Point (x, y);
82 private void UpdateCursor ()
84 Control child_control = grab_control.GetRealChildAtPoint (Cursor.Position);
85 if (child_control != null) {
87 XplatUI.SetCursor (child_control.Handle, Cursors.Default.handle);
89 XplatUI.SetCursor (child_control.Handle, child_control.Cursor.handle);
95 bool redrawbar = (keynav_state != KeyNavState.Idle) && (TopMenu is MainMenu);
99 hotkey_active = false;
100 grab_control.ActiveTracker = null;
101 keynav_state = KeyNavState.Idle;
102 if (TopMenu is ContextMenu) {
103 PopUpWindow puw = TopMenu.Wnd as PopUpWindow;
104 DeselectItem (TopMenu.SelectedItem);
107 DeselectItem (TopMenu.SelectedItem);
109 CurrentMenu = TopMenu;
112 (TopMenu as MainMenu).Draw ();
115 MenuItem FindItemByCoords (Menu menu, Point pt)
117 if (menu is MainMenu)
118 pt = ScreenToMenu (menu, pt);
120 pt = menu.Wnd.PointToClient (pt);
121 foreach (MenuItem item in menu.MenuItems) {
122 Rectangle rect = item.bounds;
123 if (rect.Contains (pt))
130 MenuItem GetItemAtXY (int x, int y)
132 Point pnt = new Point (x, y);
133 MenuItem item = null;
134 if (TopMenu.SelectedItem != null)
135 item = FindSubItemByCoord (TopMenu.SelectedItem, Control.MousePosition);
137 item = FindItemByCoords (TopMenu, pnt);
141 public bool OnMouseDown (MouseEventArgs args)
143 MenuItem item = GetItemAtXY (args.X, args.Y);
150 if ((args.Button & MouseButtons.Left) == 0)
156 popdown_menu = active && item.VisibleItems;
158 if (item.IsPopup || (item.Parent is MainMenu)) {
160 item.Parent.InvalidateItem (item);
163 if ((CurrentMenu == TopMenu) && !popdown_menu)
164 SelectItem (item.Parent, item, item.IsPopup);
166 grab_control.ActiveTracker = this;
170 public void OnMotion (MouseEventArgs args)
172 MenuItem item = GetItemAtXY (args.X, args.Y);
176 if (CurrentMenu.SelectedItem == item)
179 grab_control.ActiveTracker = (active || item != null) ? this : null;
182 MenuItem old_item = CurrentMenu.SelectedItem;
184 // Return when is a popup with visible subitems for MainMenu
185 if ((active && old_item.VisibleItems && old_item.IsPopup && (CurrentMenu is MainMenu)))
188 // Also returns when keyboard navigating
189 if (keynav_state == KeyNavState.Navigating)
192 keynav_state = KeyNavState.Navigating;
194 // Select parent menu when move outside of menu item
195 if (old_item.Parent is MenuItem) {
196 MenuItem new_item = (old_item.Parent as MenuItem);
197 if (new_item.IsPopup) {
198 SelectItem (new_item.Parent, new_item, false);
202 if (CurrentMenu != TopMenu)
203 CurrentMenu = CurrentMenu.parent_menu;
205 DeselectItem (old_item);
207 keynav_state = KeyNavState.Idle;
208 SelectItem (item.Parent, item, active && item.IsPopup && popup_active && (CurrentMenu.SelectedItem != item));
212 public void OnMouseUp (MouseEventArgs args)
214 if ((args.Button & MouseButtons.Left) == 0)
217 MenuItem item = GetItemAtXY (args.X, args.Y);
219 /* the user released the mouse button outside the menu */
228 /* Deactivate the menu when is topmenu and popdown and */
229 if (((CurrentMenu == TopMenu) && !(CurrentMenu is ContextMenu) && popdown_menu) || !item.IsPopup) {
234 /* Perform click when is not a popup */
237 item.PerformClick ();
241 static public bool TrackPopupMenu (Menu menu, Point pnt)
245 if (menu.MenuItems.Count <= 0) // No submenus to track
248 MenuTracker tracker = new MenuTracker (menu);
249 tracker.active = true;
250 tracker.popup_active = true;
251 menu.tracker = tracker;
253 menu.Wnd = new PopUpWindow (tracker.grab_control, menu);
254 menu.Wnd.Location = menu.Wnd.PointToClient (pnt);
256 ((PopUpWindow)menu.Wnd).ShowWindow ();
260 queue_id = XplatUI.StartLoop(Thread.CurrentThread);
262 while ((menu.Wnd != null) && menu.Wnd.Visible && no_quit) {
263 MSG msg = new MSG ();
264 no_quit = XplatUI.GetMessage(queue_id, ref msg, IntPtr.Zero, 0, 0);
265 XplatUI.TranslateMessage(ref msg);
266 XplatUI.DispatchMessage(ref msg);
270 XplatUI.PostQuitMessage(0);
272 if (menu.Wnd != null) {
280 void DeselectItem (MenuItem item)
285 item.Selected = false;
287 /* When popup item then close all sub popups and unselect all sub items */
289 HideSubPopups (item, TopMenu);
291 /* Unselect all selected sub itens */
292 foreach (MenuItem subitem in item.MenuItems)
293 if (subitem.Selected)
294 DeselectItem (subitem);
297 Menu menu = item.Parent;
298 menu.InvalidateItem (item);
301 void SelectItem (Menu menu, MenuItem item, bool execute)
303 MenuItem prev_item = CurrentMenu.SelectedItem;
305 if (prev_item != item.Parent) {
306 DeselectItem (prev_item);
307 if ((CurrentMenu != menu) && (prev_item.Parent != item) && (prev_item.Parent is MenuItem)) {
308 DeselectItem (prev_item.Parent as MenuItem);
312 if (CurrentMenu != menu)
315 item.Selected = true;
316 menu.InvalidateItem (item);
318 if (((CurrentMenu == TopMenu) && execute) || ((CurrentMenu != TopMenu) && popup_active))
319 item.PerformSelect ();
321 if ((execute) && ((prev_item == null) || (item != prev_item.Parent)))
322 ExecFocusedItem (menu, item);
325 // Used when the user executes the action of an item (press enter, shortcut)
326 // or a sub-popup menu has to be shown
327 void ExecFocusedItem (Menu menu, MenuItem item)
336 ShowSubPopup (menu, item);
339 item.PerformClick ();
343 // Create a popup window and show it or only show it if it is already created
344 void ShowSubPopup (Menu menu, MenuItem item)
346 if (item.Enabled == false)
349 if (!popdown_menu || !item.VisibleItems)
350 item.PerformPopup ();
352 if (item.VisibleItems == false)
355 if (item.Wnd != null) {
360 PopUpWindow puw = new PopUpWindow (grab_control, item);
363 if (menu is MainMenu)
364 pnt = new Point (item.X, item.Y + item.Height - 2 - menu.Height);
366 pnt = new Point (item.X + item.Width - 3, item.Y - 3);
367 pnt = menu.Wnd.PointToScreen (pnt);
374 static public void HideSubPopups (Menu menu, Menu topmenu)
376 foreach (MenuItem item in menu.MenuItems)
378 HideSubPopups (item, null);
380 if (menu.Wnd == null)
383 PopUpWindow puw = menu.Wnd as PopUpWindow;
388 if ((topmenu != null) && (topmenu is MainMenu))
389 ((MainMenu) topmenu).OnCollapse (EventArgs.Empty);
393 MenuItem FindSubItemByCoord (Menu menu, Point pnt)
395 foreach (MenuItem item in menu.MenuItems) {
397 if (item.IsPopup && item.Wnd != null && item.Wnd.Visible && item == menu.SelectedItem) {
398 MenuItem result = FindSubItemByCoord (item, pnt);
403 if (menu.Wnd == null || !menu.Wnd.Visible)
406 Rectangle rect = item.bounds;
407 Point pnt_client = menu.Wnd.PointToScreen (new Point (item.X, item.Y));
408 rect.X = pnt_client.X;
409 rect.Y = pnt_client.Y;
411 if (rect.Contains (pnt) == true)
418 static MenuItem FindItemByKey (Menu menu, IntPtr key)
420 char key_char = (char ) (key.ToInt32() & 0xff);
421 key_char = Char.ToUpper (key_char);
423 foreach (MenuItem item in menu.MenuItems) {
424 if (item.Mnemonic == '\0')
427 if (item.Mnemonic == key_char)
434 enum ItemNavigation {
441 static MenuItem GetNextItem (Menu menu, ItemNavigation navigation)
444 bool selectable_items = false;
447 // Check if there is at least a selectable item
448 for (int i = 0; i < menu.MenuItems.Count; i++) {
449 item = menu.MenuItems [i];
450 if (item.Separator == false && item.Visible == true) {
451 selectable_items = true;
456 if (selectable_items == false)
459 switch (navigation) {
460 case ItemNavigation.First:
462 /* First item that is not separator and it is visible*/
463 for (pos = 0; pos < menu.MenuItems.Count; pos++) {
464 item = menu.MenuItems [pos];
465 if (item.Separator == false && item.Visible == true)
471 case ItemNavigation.Last: // Not used
474 case ItemNavigation.Next:
476 if (menu.SelectedItem != null)
477 pos = menu.SelectedItem.Index;
479 /* Next item that is not separator and it is visible*/
480 for (pos++; pos < menu.MenuItems.Count; pos++) {
481 item = menu.MenuItems [pos];
482 if (item.Separator == false && item.Visible == true)
486 if (pos >= menu.MenuItems.Count) { /* Jump at the start of the menu */
488 /* Next item that is not separator and it is visible*/
489 for (; pos < menu.MenuItems.Count; pos++) {
490 item = menu.MenuItems [pos];
491 if (item.Separator == false && item.Visible == true)
497 case ItemNavigation.Previous:
499 if (menu.SelectedItem != null)
500 pos = menu.SelectedItem.Index;
502 /* Previous item that is not separator and it is visible*/
503 for (pos--; pos >= 0; pos--) {
504 item = menu.MenuItems [pos];
505 if (item.Separator == false && item.Visible == true)
509 if (pos < 0 ) { /* Jump at the end of the menu*/
510 pos = menu.MenuItems.Count - 1;
511 /* Previous item that is not separator and it is visible*/
512 for (; pos >= 0; pos--) {
513 item = menu.MenuItems [pos];
514 if (item.Separator == false && item.Visible == true)
525 return menu.MenuItems [pos];
528 void ProcessMenuKey (Msg msg_type)
530 if (TopMenu.MenuItems.Count == 0)
533 MainMenu main_menu = TopMenu as MainMenu;
536 case Msg.WM_SYSKEYDOWN:
537 switch (keynav_state) {
538 case KeyNavState.Idle:
539 keynav_state = KeyNavState.Startup;
540 hotkey_active = true;
541 grab_control.ActiveTracker = this;
542 CurrentMenu = TopMenu;
545 case KeyNavState.Startup:
554 case Msg.WM_SYSKEYUP:
555 switch (keynav_state) {
556 case KeyNavState.Idle:
557 case KeyNavState.Navigating:
559 case KeyNavState.Startup:
560 keynav_state = KeyNavState.NoPopups;
561 SelectItem (TopMenu, TopMenu.MenuItems [0], false);
572 bool ProcessMnemonic (Message msg, Keys key_data)
574 keynav_state = KeyNavState.Navigating;
575 MenuItem item = FindItemByKey (CurrentMenu, msg.WParam);
580 grab_control.ActiveTracker = this;
582 SelectItem (CurrentMenu, item, true);
585 SelectItem (item, item.MenuItems [0], false);
590 Hashtable shortcuts = new Hashtable ();
592 public void AddShortcuts (MenuItem item)
594 foreach (MenuItem child in item.MenuItems) {
595 AddShortcuts (child);
596 if (child.Shortcut != Shortcut.None)
597 shortcuts [(int)child.Shortcut] = child;
600 if (item.Shortcut != Shortcut.None)
601 shortcuts [(int)item.Shortcut] = item;
604 public void RemoveShortcuts (MenuItem item)
606 foreach (MenuItem child in item.MenuItems) {
607 RemoveShortcuts (child);
608 if (child.Shortcut != Shortcut.None)
609 shortcuts.Remove ((int)child.Shortcut);
612 if (item.Shortcut != Shortcut.None)
613 shortcuts.Remove ((int)item.Shortcut);
616 bool ProcessShortcut (Keys keyData)
618 MenuItem item = shortcuts [(int)keyData] as MenuItem;
623 item.PerformClick ();
627 public bool ProcessKeys (ref Message msg, Keys keyData)
629 if ((Msg)msg.Msg != Msg.WM_SYSKEYUP && ProcessShortcut (keyData))
631 else if ((keyData & Keys.KeyCode) == Keys.Menu && TopMenu is MainMenu) {
632 ProcessMenuKey ((Msg) msg.Msg);
634 } else if ((keyData & Keys.Alt) == Keys.Alt)
635 return ProcessMnemonic (msg, keyData);
636 else if ((Msg)msg.Msg == Msg.WM_SYSKEYUP)
638 else if (!Navigating)
645 if (CurrentMenu is MainMenu)
647 else if (CurrentMenu.MenuItems.Count == 1 && CurrentMenu.parent_menu == TopMenu) {
648 DeselectItem (CurrentMenu.SelectedItem);
649 CurrentMenu = TopMenu;
652 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
654 SelectItem (CurrentMenu, item, false);
658 if (CurrentMenu is MainMenu) {
659 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
660 keynav_state = KeyNavState.Navigating;
661 item = CurrentMenu.SelectedItem;
662 ShowSubPopup (CurrentMenu, item);
663 SelectItem (item, item.MenuItems [0], false);
666 grab_control.ActiveTracker = this;
670 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
672 SelectItem (CurrentMenu, item, false);
676 if (CurrentMenu is MainMenu) {
677 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
678 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
679 SelectItem (CurrentMenu, item, popup);
681 SelectItem (item, item.MenuItems [0], false);
684 } else if (CurrentMenu.SelectedItem.IsPopup) {
685 item = CurrentMenu.SelectedItem;
686 ShowSubPopup (CurrentMenu, item);
687 SelectItem (item, item.MenuItems [0], false);
689 } else if (CurrentMenu.parent_menu is MainMenu) {
690 item = GetNextItem (CurrentMenu.parent_menu, ItemNavigation.Next);
691 SelectItem (CurrentMenu.parent_menu, item, item.IsPopup);
693 SelectItem (item, item.MenuItems [0], false);
700 if (CurrentMenu is MainMenu) {
701 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
702 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
703 SelectItem (CurrentMenu, item, popup);
705 SelectItem (item, item.MenuItems [0], false);
708 } else if (CurrentMenu.parent_menu is MainMenu) {
709 item = GetNextItem (CurrentMenu.parent_menu, ItemNavigation.Previous);
710 SelectItem (CurrentMenu.parent_menu, item, item.IsPopup);
712 SelectItem (item, item.MenuItems [0], false);
716 HideSubPopups (CurrentMenu, TopMenu);
717 CurrentMenu = CurrentMenu.parent_menu;
722 if (CurrentMenu is MainMenu) {
723 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
724 keynav_state = KeyNavState.Navigating;
725 item = CurrentMenu.SelectedItem;
726 ShowSubPopup (CurrentMenu, item);
727 SelectItem (item, item.MenuItems [0], false);
730 grab_control.ActiveTracker = this;
735 ExecFocusedItem (CurrentMenu, CurrentMenu.SelectedItem);
743 ProcessMnemonic (msg, keyData);
751 internal class PopUpWindow : Control
756 public PopUpWindow (Form form, Menu menu): base ()
760 SetStyle (ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
761 SetStyle (ControlStyles.ResizeRedraw | ControlStyles.Opaque, true);
765 protected override CreateParams CreateParams
768 CreateParams cp = base.CreateParams;
769 cp.Caption = "Menu PopUp";
770 cp.Style = unchecked ((int)(WindowStyles.WS_POPUP));
771 cp.ExStyle |= (int)(WindowExStyles.WS_EX_TOOLWINDOW | WindowExStyles.WS_EX_TOPMOST);
776 public void ShowWindow ()
778 XplatUI.SetCursor(form.Handle, Cursors.Default.handle);
783 internal override void OnPaintInternal (PaintEventArgs args)
785 ThemeEngine.Current.DrawPopupMenu (args.Graphics, menu, args.ClipRectangle, ClientRectangle);
788 public void HideWindow ()
790 XplatUI.SetCursor (form.Handle, form.Cursor.handle);
791 MenuTracker.HideSubPopups (menu, null);
796 protected override bool ProcessCmdKey (ref Message msg, Keys keyData)
798 return MenuTracker.ProcessKeys (menu, ref msg, keyData, tracker);
802 protected override void CreateHandle ()
804 base.CreateHandle ();
808 // Called when the number of items has changed
809 internal void RefreshItems ()
811 ThemeEngine.Current.CalcPopupMenuSize (DeviceContext, menu);
813 if ((Location.X + menu.Rect.Width) > SystemInformation.WorkingArea.Width) {
814 Location = new Point (Location.X - menu.Rect.Width, Location.Y);
816 if ((Location.Y + menu.Rect.Height) > SystemInformation.WorkingArea.Height) {
817 Location = new Point (Location.X, Location.Y - menu.Rect.Height);
820 Width = menu.Rect.Width;
821 Height = menu.Rect.Height;
824 internal override bool ActivateOnShow { get { return false; } }