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;
47 Point last_motion = Point.Empty;
49 public MenuTracker (Menu top_menu)
51 TopMenu = CurrentMenu = top_menu;
52 foreach (MenuItem item in TopMenu.MenuItems)
55 if (top_menu is ContextMenu) {
57 Control source_control = (top_menu as ContextMenu).SourceControl;
58 grab_control = source_control.FindForm ();
59 if (grab_control == null)
60 grab_control = source_control.FindRootParent ();
62 grab_control.ActiveTracker = this;
65 grab_control = top_menu.Wnd.FindForm ();
75 KeyNavState keynav_state = KeyNavState.Idle;
77 public bool Navigating {
78 get { return keynav_state != KeyNavState.Idle || active; }
81 internal static Point ScreenToMenu (Menu menu, Point pnt)
85 XplatUI.ScreenToMenu (menu.Wnd.window.Handle, ref x, ref y);
86 return new Point (x, y);
89 private void UpdateCursor ()
91 Control child_control = grab_control.GetRealChildAtPoint (Cursor.Position);
92 if (child_control != null) {
94 XplatUI.SetCursor (child_control.Handle, Cursors.Default.handle);
96 XplatUI.SetCursor (child_control.Handle, child_control.Cursor.handle);
102 bool redrawbar = (keynav_state != KeyNavState.Idle) && (TopMenu is MainMenu);
105 popup_active = false;
106 hotkey_active = false;
107 grab_control.ActiveTracker = null;
108 keynav_state = KeyNavState.Idle;
109 if (TopMenu is ContextMenu) {
110 PopUpWindow puw = TopMenu.Wnd as PopUpWindow;
111 DeselectItem (TopMenu.SelectedItem);
114 DeselectItem (TopMenu.SelectedItem);
116 CurrentMenu = TopMenu;
119 (TopMenu as MainMenu).Draw ();
122 MenuItem FindItemByCoords (Menu menu, Point pt)
124 if (menu is MainMenu)
125 pt = ScreenToMenu (menu, pt);
127 pt = menu.Wnd.PointToClient (pt);
128 foreach (MenuItem item in menu.MenuItems) {
129 Rectangle rect = item.bounds;
130 if (rect.Contains (pt))
137 MenuItem GetItemAtXY (int x, int y)
139 Point pnt = new Point (x, y);
140 MenuItem item = null;
141 if (TopMenu.SelectedItem != null)
142 item = FindSubItemByCoord (TopMenu.SelectedItem, Control.MousePosition);
144 item = FindItemByCoords (TopMenu, pnt);
148 public bool OnMouseDown (MouseEventArgs args)
150 MenuItem item = GetItemAtXY (args.X, args.Y);
157 if ((args.Button & MouseButtons.Left) == 0)
163 popdown_menu = active && item.VisibleItems;
165 if (item.IsPopup || (item.Parent is MainMenu)) {
167 item.Parent.InvalidateItem (item);
170 if ((CurrentMenu == TopMenu) && !popdown_menu)
171 SelectItem (item.Parent, item, item.IsPopup);
173 grab_control.ActiveTracker = this;
177 public void OnMotion (MouseEventArgs args)
179 // Windows helpfully sends us MOUSEMOVE messages when any key is pressed.
180 // So if the mouse hasn't actually moved since the last MOUSEMOVE, ignore it.
181 if (args.Location == last_motion)
184 last_motion = args.Location;
186 MenuItem item = GetItemAtXY (args.X, args.Y);
190 if (CurrentMenu.SelectedItem == item)
193 grab_control.ActiveTracker = (active || item != null) ? this : null;
196 MenuItem old_item = CurrentMenu.SelectedItem;
198 // Return when is a popup with visible subitems for MainMenu
199 if ((active && old_item.VisibleItems && old_item.IsPopup && (CurrentMenu is MainMenu)))
202 // Also returns when keyboard navigating
203 if (keynav_state == KeyNavState.Navigating)
206 keynav_state = KeyNavState.Navigating;
208 // Select parent menu when move outside of menu item
209 if (old_item.Parent is MenuItem) {
210 MenuItem new_item = (old_item.Parent as MenuItem);
211 if (new_item.IsPopup) {
212 SelectItem (new_item.Parent, new_item, false);
216 if (CurrentMenu != TopMenu)
217 CurrentMenu = CurrentMenu.parent_menu;
219 DeselectItem (old_item);
221 keynav_state = KeyNavState.Idle;
222 SelectItem (item.Parent, item, active && item.IsPopup && popup_active && (CurrentMenu.SelectedItem != item));
226 public void OnMouseUp (MouseEventArgs args)
228 if ((args.Button & MouseButtons.Left) == 0)
231 MenuItem item = GetItemAtXY (args.X, args.Y);
233 /* the user released the mouse button outside the menu */
242 /* Deactivate the menu when is topmenu and popdown and */
243 if (((CurrentMenu == TopMenu) && !(CurrentMenu is ContextMenu) && popdown_menu) || !item.IsPopup) {
248 /* Perform click when is not a popup */
251 item.PerformClick ();
255 static public bool TrackPopupMenu (Menu menu, Point pnt)
259 if (menu.MenuItems.Count <= 0) // No submenus to track
262 MenuTracker tracker = new MenuTracker (menu);
263 tracker.active = true;
264 tracker.popup_active = true;
265 menu.tracker = tracker;
267 menu.Wnd = new PopUpWindow (tracker.grab_control, menu);
268 menu.Wnd.Location = menu.Wnd.PointToClient (pnt);
270 ((PopUpWindow)menu.Wnd).ShowWindow ();
274 queue_id = XplatUI.StartLoop(Thread.CurrentThread);
276 // Save result of capture status.
277 bool captured = tracker.grab_control.Capture;
279 while ((menu.Wnd != null) && menu.Wnd.Visible && no_quit) {
280 MSG msg = new MSG ();
281 no_quit = XplatUI.GetMessage(queue_id, ref msg, IntPtr.Zero, 0, 0);
282 XplatUI.TranslateMessage(ref msg);
283 XplatUI.DispatchMessage(ref msg);
286 if (tracker.grab_control.IsDisposed)
290 tracker.grab_control.Capture = true;
293 XplatUI.PostQuitMessage(0);
295 if (menu.Wnd != null) {
303 void DeselectItem (MenuItem item)
308 item.Selected = false;
310 /* When popup item then close all sub popups and unselect all sub items */
312 HideSubPopups (item, TopMenu);
314 /* Unselect all selected sub itens */
315 foreach (MenuItem subitem in item.MenuItems)
316 if (subitem.Selected)
317 DeselectItem (subitem);
320 Menu menu = item.Parent;
321 menu.InvalidateItem (item);
324 void SelectItem (Menu menu, MenuItem item, bool execute)
326 MenuItem prev_item = CurrentMenu.SelectedItem;
328 if (prev_item != item.Parent) {
329 DeselectItem (prev_item);
330 if ((CurrentMenu != menu) && (prev_item.Parent != item) && (prev_item.Parent is MenuItem)) {
331 DeselectItem (prev_item.Parent as MenuItem);
335 if (CurrentMenu != menu)
338 item.Selected = true;
339 menu.InvalidateItem (item);
341 if (((CurrentMenu == TopMenu) && execute) || ((CurrentMenu != TopMenu) && popup_active))
342 item.PerformSelect ();
344 if ((execute) && ((prev_item == null) || (item != prev_item.Parent)))
345 ExecFocusedItem (menu, item);
348 // Used when the user executes the action of an item (press enter, shortcut)
349 // or a sub-popup menu has to be shown
350 void ExecFocusedItem (Menu menu, MenuItem item)
359 ShowSubPopup (menu, item);
362 item.PerformClick ();
366 // Create a popup window and show it or only show it if it is already created
367 void ShowSubPopup (Menu menu, MenuItem item)
369 if (item.Enabled == false)
372 if (!popdown_menu || !item.VisibleItems)
373 item.PerformPopup ();
375 if (item.VisibleItems == false)
378 if (item.Wnd != null) {
383 PopUpWindow puw = new PopUpWindow (grab_control, item);
386 if (menu is MainMenu)
387 pnt = new Point (item.X, item.Y + item.Height - 2 - menu.Height);
389 pnt = new Point (item.X + item.Width - 3, item.Y - 3);
390 pnt = menu.Wnd.PointToScreen (pnt);
397 static public void HideSubPopups (Menu menu, Menu topmenu)
399 foreach (MenuItem item in menu.MenuItems)
401 HideSubPopups (item, null);
403 if (menu.Wnd == null)
406 PopUpWindow puw = menu.Wnd as PopUpWindow;
411 if ((topmenu != null) && (topmenu is MainMenu))
412 ((MainMenu) topmenu).OnCollapse (EventArgs.Empty);
416 MenuItem FindSubItemByCoord (Menu menu, Point pnt)
418 foreach (MenuItem item in menu.MenuItems) {
420 if (item.IsPopup && item.Wnd != null && item.Wnd.Visible && item == menu.SelectedItem) {
421 MenuItem result = FindSubItemByCoord (item, pnt);
426 if (menu.Wnd == null || !menu.Wnd.Visible)
429 Rectangle rect = item.bounds;
430 Point pnt_client = menu.Wnd.PointToScreen (new Point (item.X, item.Y));
431 rect.X = pnt_client.X;
432 rect.Y = pnt_client.Y;
434 if (rect.Contains (pnt) == true)
441 static MenuItem FindItemByKey (Menu menu, IntPtr key)
443 char key_char = (char ) (key.ToInt32() & 0xff);
444 key_char = Char.ToUpper (key_char);
446 foreach (MenuItem item in menu.MenuItems) {
447 if (item.Mnemonic == '\0')
450 if (item.Mnemonic == key_char)
457 enum ItemNavigation {
464 static MenuItem GetNextItem (Menu menu, ItemNavigation navigation)
467 bool selectable_items = false;
470 // Check if there is at least a selectable item
471 for (int i = 0; i < menu.MenuItems.Count; i++) {
472 item = menu.MenuItems [i];
473 if (item.Separator == false && item.Visible == true) {
474 selectable_items = true;
479 if (selectable_items == false)
482 switch (navigation) {
483 case ItemNavigation.First:
485 /* First item that is not separator and it is visible*/
486 for (pos = 0; pos < menu.MenuItems.Count; pos++) {
487 item = menu.MenuItems [pos];
488 if (item.Separator == false && item.Visible == true)
494 case ItemNavigation.Last: // Not used
497 case ItemNavigation.Next:
499 if (menu.SelectedItem != null)
500 pos = menu.SelectedItem.Index;
502 /* Next item that is not separator and it is visible*/
503 for (pos++; pos < menu.MenuItems.Count; pos++) {
504 item = menu.MenuItems [pos];
505 if (item.Separator == false && item.Visible == true)
509 if (pos >= menu.MenuItems.Count) { /* Jump at the start of the menu */
511 /* Next item that is not separator and it is visible*/
512 for (; pos < menu.MenuItems.Count; pos++) {
513 item = menu.MenuItems [pos];
514 if (item.Separator == false && item.Visible == true)
520 case ItemNavigation.Previous:
522 if (menu.SelectedItem != null)
523 pos = menu.SelectedItem.Index;
525 /* Previous item that is not separator and it is visible*/
526 for (pos--; pos >= 0; pos--) {
527 item = menu.MenuItems [pos];
528 if (item.Separator == false && item.Visible == true)
532 if (pos < 0 ) { /* Jump at the end of the menu*/
533 pos = menu.MenuItems.Count - 1;
534 /* Previous item that is not separator and it is visible*/
535 for (; pos >= 0; pos--) {
536 item = menu.MenuItems [pos];
537 if (item.Separator == false && item.Visible == true)
548 return menu.MenuItems [pos];
551 void ProcessMenuKey (Msg msg_type)
553 if (TopMenu.MenuItems.Count == 0)
556 MainMenu main_menu = TopMenu as MainMenu;
559 case Msg.WM_SYSKEYDOWN:
560 switch (keynav_state) {
561 case KeyNavState.Idle:
562 keynav_state = KeyNavState.Startup;
563 hotkey_active = true;
564 grab_control.ActiveTracker = this;
565 CurrentMenu = TopMenu;
568 case KeyNavState.Startup:
577 case Msg.WM_SYSKEYUP:
578 switch (keynav_state) {
579 case KeyNavState.Idle:
580 case KeyNavState.Navigating:
582 case KeyNavState.Startup:
583 keynav_state = KeyNavState.NoPopups;
584 SelectItem (TopMenu, TopMenu.MenuItems [0], false);
595 bool ProcessMnemonic (Message msg, Keys key_data)
597 keynav_state = KeyNavState.Navigating;
598 MenuItem item = FindItemByKey (CurrentMenu, msg.WParam);
603 grab_control.ActiveTracker = this;
605 SelectItem (CurrentMenu, item, true);
608 SelectItem (item, item.MenuItems [0], false);
613 Hashtable shortcuts = new Hashtable ();
615 public void AddShortcuts (MenuItem item)
617 foreach (MenuItem child in item.MenuItems) {
618 AddShortcuts (child);
619 if (child.Shortcut != Shortcut.None)
620 shortcuts [(int)child.Shortcut] = child;
623 if (item.Shortcut != Shortcut.None)
624 shortcuts [(int)item.Shortcut] = item;
627 public void RemoveShortcuts (MenuItem item)
629 foreach (MenuItem child in item.MenuItems) {
630 RemoveShortcuts (child);
631 if (child.Shortcut != Shortcut.None)
632 shortcuts.Remove ((int)child.Shortcut);
635 if (item.Shortcut != Shortcut.None)
636 shortcuts.Remove ((int)item.Shortcut);
639 bool ProcessShortcut (Keys keyData)
641 MenuItem item = shortcuts [(int)keyData] as MenuItem;
646 item.PerformClick ();
650 public bool ProcessKeys (ref Message msg, Keys keyData)
652 // If we get Alt-F4, Windows will ignore it because we have a capture,
653 // release the capture and the program will exit. (X11 doesn't care.)
654 if ((keyData & Keys.Alt) == Keys.Alt && (keyData & Keys.F4) == Keys.F4) {
655 grab_control.ActiveTracker = null;
659 if ((Msg)msg.Msg != Msg.WM_SYSKEYUP && ProcessShortcut (keyData))
661 else if ((keyData & Keys.KeyCode) == Keys.Menu && TopMenu is MainMenu) {
662 ProcessMenuKey ((Msg) msg.Msg);
664 } else if ((keyData & Keys.Alt) == Keys.Alt)
665 return ProcessMnemonic (msg, keyData);
666 else if ((Msg)msg.Msg == Msg.WM_SYSKEYUP)
668 else if (!Navigating)
675 if (CurrentMenu is MainMenu)
677 else if (CurrentMenu.MenuItems.Count == 1 && CurrentMenu.parent_menu == TopMenu) {
678 DeselectItem (CurrentMenu.SelectedItem);
679 CurrentMenu = TopMenu;
682 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
684 SelectItem (CurrentMenu, item, false);
688 if (CurrentMenu is MainMenu) {
689 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
690 keynav_state = KeyNavState.Navigating;
691 item = CurrentMenu.SelectedItem;
692 ShowSubPopup (CurrentMenu, item);
693 SelectItem (item, item.MenuItems [0], false);
696 grab_control.ActiveTracker = this;
700 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
702 SelectItem (CurrentMenu, item, false);
706 if (CurrentMenu is MainMenu) {
707 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
708 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
709 SelectItem (CurrentMenu, item, popup);
711 SelectItem (item, item.MenuItems [0], false);
714 } else if (CurrentMenu.SelectedItem.IsPopup) {
715 item = CurrentMenu.SelectedItem;
716 ShowSubPopup (CurrentMenu, item);
717 SelectItem (item, item.MenuItems [0], false);
719 } else if (CurrentMenu.parent_menu is MainMenu) {
720 item = GetNextItem (CurrentMenu.parent_menu, ItemNavigation.Next);
721 SelectItem (CurrentMenu.parent_menu, item, item.IsPopup);
723 SelectItem (item, item.MenuItems [0], false);
730 if (CurrentMenu is MainMenu) {
731 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
732 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
733 SelectItem (CurrentMenu, item, popup);
735 SelectItem (item, item.MenuItems [0], false);
738 } else if (CurrentMenu.parent_menu is MainMenu) {
739 item = GetNextItem (CurrentMenu.parent_menu, ItemNavigation.Previous);
740 SelectItem (CurrentMenu.parent_menu, item, item.IsPopup);
742 SelectItem (item, item.MenuItems [0], false);
746 HideSubPopups (CurrentMenu, TopMenu);
747 CurrentMenu = CurrentMenu.parent_menu;
752 if (CurrentMenu is MainMenu) {
753 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
754 keynav_state = KeyNavState.Navigating;
755 item = CurrentMenu.SelectedItem;
756 ShowSubPopup (CurrentMenu, item);
757 SelectItem (item, item.MenuItems [0], false);
760 grab_control.ActiveTracker = this;
765 ExecFocusedItem (CurrentMenu, CurrentMenu.SelectedItem);
773 ProcessMnemonic (msg, keyData);
781 internal class PopUpWindow : Control
784 private Control form;
786 public PopUpWindow (Control form, Menu menu): base ()
790 SetStyle (ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
791 SetStyle (ControlStyles.ResizeRedraw | ControlStyles.Opaque, true);
795 protected override CreateParams CreateParams
798 CreateParams cp = base.CreateParams;
799 cp.Caption = "Menu PopUp";
800 cp.Style = unchecked ((int)(WindowStyles.WS_POPUP));
801 cp.ExStyle |= (int)(WindowExStyles.WS_EX_TOOLWINDOW | WindowExStyles.WS_EX_TOPMOST);
806 public void ShowWindow ()
808 XplatUI.SetCursor(form.Handle, Cursors.Default.handle);
813 internal override void OnPaintInternal (PaintEventArgs args)
815 ThemeEngine.Current.DrawPopupMenu (args.Graphics, menu, args.ClipRectangle, ClientRectangle);
818 public void HideWindow ()
820 XplatUI.SetCursor (form.Handle, form.Cursor.handle);
821 MenuTracker.HideSubPopups (menu, null);
826 protected override bool ProcessCmdKey (ref Message msg, Keys keyData)
828 return MenuTracker.ProcessKeys (menu, ref msg, keyData, tracker);
832 protected override void CreateHandle ()
834 base.CreateHandle ();
838 // Called when the number of items has changed
839 internal void RefreshItems ()
841 ThemeEngine.Current.CalcPopupMenuSize (DeviceContext, menu);
843 if ((Location.X + menu.Rect.Width) > SystemInformation.WorkingArea.Width) {
844 Location = new Point (Location.X - menu.Rect.Width, Location.Y);
846 if ((Location.Y + menu.Rect.Height) > SystemInformation.WorkingArea.Height) {
847 if ((Location.Y - menu.Rect.Height) > 0)
848 Location = new Point (Location.X, Location.Y - menu.Rect.Height);
850 Location = new Point (Location.X, SystemInformation.WorkingArea.Height - menu.Rect.Height);
853 Width = menu.Rect.Width;
854 Height = menu.Rect.Height;
857 internal override bool ActivateOnShow { get { return false; } }