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 // UIA Framework Note: This class used by UIA for its mouse action methods.
39 internal class MenuTracker {
42 internal bool popup_active;
43 internal bool popdown_menu;
44 internal bool hotkey_active;
45 private bool mouse_down = false;
46 public Menu CurrentMenu;
48 public Control GrabControl;
49 Point last_motion = Point.Empty;
51 public MenuTracker (Menu top_menu)
53 TopMenu = CurrentMenu = top_menu;
54 foreach (MenuItem item in TopMenu.MenuItems)
65 KeyNavState keynav_state = KeyNavState.Idle;
67 public bool Navigating {
68 get { return keynav_state != KeyNavState.Idle || active; }
71 internal static Point ScreenToMenu (Menu menu, Point pnt)
75 XplatUI.ScreenToMenu (menu.Wnd.window.Handle, ref x, ref y);
76 return new Point (x, y);
79 private void UpdateCursor ()
81 Control child_control = GrabControl.GetRealChildAtPoint (Cursor.Position);
82 if (child_control != null) {
84 XplatUI.SetCursor (child_control.Handle, Cursors.Default.handle);
86 XplatUI.SetCursor (child_control.Handle, child_control.Cursor.handle);
90 internal void Deactivate ()
92 bool redrawbar = (keynav_state != KeyNavState.Idle) && (TopMenu is MainMenu);
96 hotkey_active = false;
97 if (GrabControl != null)
98 GrabControl.ActiveTracker = null;
99 keynav_state = KeyNavState.Idle;
100 if (TopMenu is ContextMenu) {
101 PopUpWindow puw = TopMenu.Wnd as PopUpWindow;
102 DeselectItem (TopMenu.SelectedItem);
106 DeselectItem (TopMenu.SelectedItem);
108 CurrentMenu = TopMenu;
111 (TopMenu as MainMenu).Draw ();
114 MenuItem FindItemByCoords (Menu menu, Point pt)
116 if (menu is MainMenu)
117 pt = ScreenToMenu (menu, pt);
119 if (menu.Wnd == null) {
122 pt = menu.Wnd.PointToClient (pt);
124 foreach (MenuItem item in menu.MenuItems) {
125 Rectangle rect = item.bounds;
126 if (rect.Contains (pt))
133 MenuItem GetItemAtXY (int x, int y)
135 Point pnt = new Point (x, y);
136 MenuItem item = null;
137 if (TopMenu.SelectedItem != null)
138 item = FindSubItemByCoord (TopMenu.SelectedItem, Control.MousePosition);
140 item = FindItemByCoords (TopMenu, pnt);
144 // UIA Framework Note: Used to expand/collapse MenuItems
145 public bool OnMouseDown (MouseEventArgs args)
147 MenuItem item = GetItemAtXY (args.X, args.Y);
156 if ((args.Button & MouseButtons.Left) == 0)
162 popdown_menu = active && item.VisibleItems;
164 if (item.IsPopup || (item.Parent is MainMenu)) {
166 item.Parent.InvalidateItem (item);
169 if ((CurrentMenu == TopMenu) && !popdown_menu)
170 SelectItem (item.Parent, item, item.IsPopup);
172 GrabControl.ActiveTracker = this;
176 // UIA Framework Note: Used to select MenuItems
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 GrabControl.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 // Select parent menu when move outside of menu item
207 if (old_item.Parent is MenuItem) {
208 MenuItem new_item = (old_item.Parent as MenuItem);
209 if (new_item.IsPopup) {
210 SelectItem (new_item.Parent, new_item, false);
214 if (CurrentMenu != TopMenu)
215 CurrentMenu = CurrentMenu.parent_menu;
217 DeselectItem (old_item);
219 keynav_state = KeyNavState.Idle;
220 SelectItem (item.Parent, item, active && item.IsPopup && popup_active && (CurrentMenu.SelectedItem != item));
224 // UIA Framework Note: Used to expand/collapse MenuItems
225 public void OnMouseUp (MouseEventArgs args)
227 /* mouse down dont comes from menu */
233 /* is not left button */
234 if ((args.Button & MouseButtons.Left) == 0)
237 MenuItem item = GetItemAtXY (args.X, args.Y);
239 /* the user released the mouse button outside the menu */
248 /* Deactivate the menu when is topmenu and popdown and */
249 if (((CurrentMenu == TopMenu) && !(CurrentMenu is ContextMenu) && popdown_menu) || !item.IsPopup) {
254 /* Perform click when is not a popup */
258 // Raise the form's MenuComplete event
259 if (TopMenu != null && TopMenu.Wnd != null) {
260 Form f = TopMenu.Wnd.FindForm ();
263 f.OnMenuComplete (EventArgs.Empty);
266 item.PerformClick ();
270 static public bool TrackPopupMenu (Menu menu, Point pnt)
272 if (menu.MenuItems.Count <= 0) // No submenus to track
275 MenuTracker tracker = menu.tracker;
276 tracker.active = true;
277 tracker.popup_active = true;
280 Control src_ctrl = (tracker.TopMenu as ContextMenu).SourceControl;
281 tracker.GrabControl = src_ctrl.FindForm ();
282 if (tracker.GrabControl == null)
283 tracker.GrabControl = src_ctrl.FindRootParent ();
284 tracker.GrabControl.ActiveTracker = tracker;
286 menu.Wnd = new PopUpWindow (tracker.GrabControl, menu);
287 menu.Wnd.Location = menu.Wnd.PointToClient (pnt);
288 ((PopUpWindow)menu.Wnd).ShowWindow ();
292 Object queue_id = XplatUI.StartLoop(Thread.CurrentThread);
294 while ((menu.Wnd != null) && menu.Wnd.Visible && no_quit) {
295 MSG msg = new MSG ();
296 no_quit = XplatUI.GetMessage(queue_id, ref msg, IntPtr.Zero, 0, 0);
298 switch((Msg)msg.message) {
300 case Msg.WM_SYSKEYDOWN:
304 case Msg.WM_SYSKEYUP:
305 Control c = Control.FromHandle(msg.hwnd);
307 Message m = Message.Create(msg.hwnd, (int)msg.message, msg.wParam, msg.lParam);
308 c.PreProcessControlMessageInternal (ref m);
312 XplatUI.TranslateMessage (ref msg);
313 XplatUI.DispatchMessage (ref msg);
318 if (tracker.GrabControl.IsDisposed)
322 XplatUI.PostQuitMessage(0);
324 if (menu.Wnd != null) {
332 void DeselectItem (MenuItem item)
337 item.Selected = false;
339 /* When popup item then close all sub popups and unselect all sub items */
341 HideSubPopups (item, TopMenu);
343 /* Unselect all selected sub itens */
344 foreach (MenuItem subitem in item.MenuItems)
345 if (subitem.Selected)
346 DeselectItem (subitem);
349 Menu menu = item.Parent;
350 menu.InvalidateItem (item);
353 void SelectItem (Menu menu, MenuItem item, bool execute)
355 MenuItem prev_item = CurrentMenu.SelectedItem;
357 if (prev_item != item.Parent) {
358 DeselectItem (prev_item);
359 if ((CurrentMenu != menu) && (prev_item.Parent != item) && (prev_item.Parent is MenuItem)) {
360 DeselectItem (prev_item.Parent as MenuItem);
364 if (CurrentMenu != menu)
367 item.Selected = true;
368 menu.InvalidateItem (item);
370 if (((CurrentMenu == TopMenu) && execute) || ((CurrentMenu != TopMenu) && popup_active))
371 item.PerformSelect ();
373 if ((execute) && ((prev_item == null) || (item != prev_item.Parent)))
374 ExecFocusedItem (menu, item);
377 // Used when the user executes the action of an item (press enter, shortcut)
378 // or a sub-popup menu has to be shown
379 void ExecFocusedItem (Menu menu, MenuItem item)
388 ShowSubPopup (menu, item);
391 item.PerformClick ();
395 // Create a popup window and show it or only show it if it is already created
396 void ShowSubPopup (Menu menu, MenuItem item)
398 if (item.Enabled == false)
401 if (!popdown_menu || !item.VisibleItems)
402 item.PerformPopup ();
404 if (item.VisibleItems == false)
407 if (item.Wnd != null) {
412 PopUpWindow puw = new PopUpWindow (GrabControl, item);
415 if (menu is MainMenu)
416 pnt = new Point (item.X, item.Y + item.Height - 2 - menu.Height);
418 pnt = new Point (item.X + item.Width - 3, item.Y - 3);
419 pnt = menu.Wnd.PointToScreen (pnt);
426 static public void HideSubPopups (Menu menu, Menu topmenu)
428 foreach (MenuItem item in menu.MenuItems)
430 HideSubPopups (item, null);
432 if (menu.Wnd == null)
435 PopUpWindow puw = menu.Wnd as PopUpWindow;
442 if ((topmenu != null) && (topmenu is MainMenu))
443 ((MainMenu) topmenu).OnCollapse (EventArgs.Empty);
446 MenuItem FindSubItemByCoord (Menu menu, Point pnt)
448 foreach (MenuItem item in menu.MenuItems) {
450 if (item.IsPopup && item.Wnd != null && item.Wnd.Visible && item == menu.SelectedItem) {
451 MenuItem result = FindSubItemByCoord (item, pnt);
456 if (menu.Wnd == null || !menu.Wnd.Visible)
459 Rectangle rect = item.bounds;
460 Point pnt_client = menu.Wnd.PointToScreen (new Point (item.X, item.Y));
461 rect.X = pnt_client.X;
462 rect.Y = pnt_client.Y;
464 if (rect.Contains (pnt) == true)
471 static MenuItem FindItemByKey (Menu menu, IntPtr key)
473 char key_char = Char.ToUpper ((char) (key.ToInt32() & 0xff));
474 foreach (MenuItem item in menu.MenuItems) {
475 if (item.Mnemonic == key_char)
479 string key_str = key_char.ToString ();
480 foreach (MenuItem item in menu.MenuItems) {
481 //if (item.Mnemonic == key_char)
482 if (item.Text.StartsWith (key_str))
489 enum ItemNavigation {
496 static MenuItem GetNextItem (Menu menu, ItemNavigation navigation)
499 bool selectable_items = false;
502 // Check if there is at least a selectable item
503 for (int i = 0; i < menu.MenuItems.Count; i++) {
504 item = menu.MenuItems [i];
505 if (item.Separator == false && item.Visible == true) {
506 selectable_items = true;
511 if (selectable_items == false)
514 switch (navigation) {
515 case ItemNavigation.First:
517 /* First item that is not separator and it is visible*/
518 for (pos = 0; pos < menu.MenuItems.Count; pos++) {
519 item = menu.MenuItems [pos];
520 if (item.Separator == false && item.Visible == true)
526 case ItemNavigation.Last: // Not used
529 case ItemNavigation.Next:
531 pos = menu.SelectedItem == null ? - 1 : menu.SelectedItem.Index;
533 /* Next item that is not separator and it is visible*/
534 for (pos++; pos < menu.MenuItems.Count; pos++) {
535 item = menu.MenuItems [pos];
536 if (item.Separator == false && item.Visible == true)
540 if (pos >= menu.MenuItems.Count) { /* Jump at the start of the menu */
542 /* Next item that is not separator and it is visible*/
543 for (; pos < menu.MenuItems.Count; pos++) {
544 item = menu.MenuItems [pos];
545 if (item.Separator == false && item.Visible == true)
551 case ItemNavigation.Previous:
553 if (menu.SelectedItem != null)
554 pos = menu.SelectedItem.Index;
556 /* Previous item that is not separator and it is visible*/
557 for (pos--; pos >= 0; pos--) {
558 item = menu.MenuItems [pos];
559 if (item.Separator == false && item.Visible == true)
563 if (pos < 0 ) { /* Jump at the end of the menu*/
564 pos = menu.MenuItems.Count - 1;
565 /* Previous item that is not separator and it is visible*/
566 for (; pos >= 0; pos--) {
567 item = menu.MenuItems [pos];
568 if (item.Separator == false && item.Visible == true)
579 return menu.MenuItems [pos];
582 void ProcessMenuKey (Msg msg_type)
584 if (TopMenu.MenuItems.Count == 0)
587 MainMenu main_menu = TopMenu as MainMenu;
590 case Msg.WM_SYSKEYDOWN:
591 switch (keynav_state) {
592 case KeyNavState.Idle:
593 keynav_state = KeyNavState.Startup;
594 hotkey_active = true;
595 GrabControl.ActiveTracker = this;
596 CurrentMenu = TopMenu;
599 case KeyNavState.Startup:
608 case Msg.WM_SYSKEYUP:
609 switch (keynav_state) {
610 case KeyNavState.Idle:
611 case KeyNavState.Navigating:
613 case KeyNavState.Startup:
614 keynav_state = KeyNavState.NoPopups;
615 SelectItem (TopMenu, TopMenu.MenuItems [0], false);
626 bool ProcessMnemonic (Message msg, Keys key_data)
628 keynav_state = KeyNavState.Navigating;
629 MenuItem item = FindItemByKey (CurrentMenu, msg.WParam);
630 if ((item == null) || (GrabControl == null) || (GrabControl.ActiveTracker == null))
634 GrabControl.ActiveTracker = this;
636 SelectItem (CurrentMenu, item, true);
639 SelectItem (item, item.MenuItems [0], false);
644 Hashtable shortcuts = new Hashtable ();
646 public void AddShortcuts (MenuItem item)
648 foreach (MenuItem child in item.MenuItems) {
649 AddShortcuts (child);
650 if (child.Shortcut != Shortcut.None)
651 shortcuts [(int)child.Shortcut] = child;
654 if (item.Shortcut != Shortcut.None)
655 shortcuts [(int)item.Shortcut] = item;
658 public void RemoveShortcuts (MenuItem item)
660 foreach (MenuItem child in item.MenuItems) {
661 RemoveShortcuts (child);
662 if (child.Shortcut != Shortcut.None)
663 shortcuts.Remove ((int)child.Shortcut);
666 if (item.Shortcut != Shortcut.None)
667 shortcuts.Remove ((int)item.Shortcut);
670 bool ProcessShortcut (Keys keyData)
672 MenuItem item = shortcuts [(int)keyData] as MenuItem;
673 if (item == null || !item.Enabled)
678 item.PerformClick ();
682 public bool ProcessKeys (ref Message msg, Keys keyData)
684 // We should process Alt+key only if we don't have an active menu,
685 // and hide it otherwise.
686 if ((keyData & Keys.Alt) == Keys.Alt && active) {
691 // If we get Alt-F4, Windows will ignore it because we have a capture,
692 // release the capture and the program will exit. (X11 doesn't care.)
693 if ((keyData & Keys.Alt) == Keys.Alt && (keyData & Keys.F4) == Keys.F4) {
694 if (GrabControl != null)
695 GrabControl.ActiveTracker = null;
700 if ((Msg)msg.Msg != Msg.WM_SYSKEYUP && ProcessShortcut (keyData))
702 else if ((keyData & Keys.KeyCode) == Keys.Menu && TopMenu is MainMenu) {
703 ProcessMenuKey ((Msg) msg.Msg);
705 } else if ((keyData & Keys.Alt) == Keys.Alt)
706 return ProcessMnemonic (msg, keyData);
707 else if ((Msg)msg.Msg == Msg.WM_SYSKEYUP)
709 else if (!Navigating)
716 if (CurrentMenu is MainMenu)
718 else if (CurrentMenu.MenuItems.Count == 1 && CurrentMenu.parent_menu == TopMenu) {
719 DeselectItem (CurrentMenu.SelectedItem);
720 CurrentMenu = TopMenu;
723 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
725 SelectItem (CurrentMenu, item, false);
729 if (CurrentMenu is MainMenu) {
730 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
731 keynav_state = KeyNavState.Navigating;
732 item = CurrentMenu.SelectedItem;
733 ShowSubPopup (CurrentMenu, item);
734 SelectItem (item, item.MenuItems [0], false);
737 GrabControl.ActiveTracker = this;
741 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
743 SelectItem (CurrentMenu, item, false);
747 if (CurrentMenu is MainMenu) {
748 item = GetNextItem (CurrentMenu, ItemNavigation.Next);
749 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
750 SelectItem (CurrentMenu, item, popup);
752 SelectItem (item, item.MenuItems [0], false);
755 } else if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
756 item = CurrentMenu.SelectedItem;
757 ShowSubPopup (CurrentMenu, item);
758 SelectItem (item, item.MenuItems [0], false);
761 //Search up for a main menu
762 Menu Prnt = CurrentMenu.parent_menu;
763 while (Prnt != null && !(Prnt is MainMenu)) {
764 Prnt = Prnt.parent_menu;
766 if (Prnt is MainMenu)
768 item = GetNextItem(Prnt, ItemNavigation.Next);
769 SelectItem(Prnt, item, item.IsPopup);
772 SelectItem(item, item.MenuItems[0], false);
780 if (CurrentMenu is MainMenu) {
781 item = GetNextItem (CurrentMenu, ItemNavigation.Previous);
782 bool popup = item.IsPopup && keynav_state != KeyNavState.NoPopups;
783 SelectItem (CurrentMenu, item, popup);
785 SelectItem (item, item.MenuItems [0], false);
788 } else if (CurrentMenu.parent_menu is MainMenu) {
789 item = GetNextItem (CurrentMenu.parent_menu, ItemNavigation.Previous);
790 SelectItem (CurrentMenu.parent_menu, item, item.IsPopup);
792 SelectItem (item, item.MenuItems [0], false);
795 } else if (!(CurrentMenu is ContextMenu)) { // ContextMenu root remains active.
796 HideSubPopups (CurrentMenu, TopMenu);
797 if (CurrentMenu.parent_menu != null)
798 CurrentMenu = CurrentMenu.parent_menu;
803 if (CurrentMenu.SelectedItem != null && CurrentMenu.SelectedItem.IsPopup) {
804 keynav_state = KeyNavState.Navigating;
805 item = CurrentMenu.SelectedItem;
806 ShowSubPopup (CurrentMenu, item);
807 SelectItem (item, item.MenuItems [0], false);
810 GrabControl.ActiveTracker = this;
812 ExecFocusedItem (CurrentMenu, CurrentMenu.SelectedItem);
821 ProcessMnemonic (msg, keyData);
829 internal class PopUpWindow : Control
832 private Control form;
834 public PopUpWindow (Control form, Menu menu): base ()
838 SetStyle (ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
839 SetStyle (ControlStyles.ResizeRedraw | ControlStyles.Opaque, true);
843 protected override CreateParams CreateParams
846 CreateParams cp = base.CreateParams;
847 cp.Caption = "Menu PopUp";
848 cp.Style = unchecked ((int)(WindowStyles.WS_POPUP));
849 cp.ExStyle |= (int)(WindowExStyles.WS_EX_TOOLWINDOW | WindowExStyles.WS_EX_TOPMOST);
854 public void ShowWindow ()
856 XplatUI.SetCursor(form.Handle, Cursors.Default.handle);
861 internal override void OnPaintInternal (PaintEventArgs args)
863 ThemeEngine.Current.DrawPopupMenu (args.Graphics, menu, args.ClipRectangle, ClientRectangle);
866 public void HideWindow ()
868 XplatUI.SetCursor (form.Handle, form.Cursor.handle);
869 MenuTracker.HideSubPopups (menu, null);
873 protected override void CreateHandle ()
875 base.CreateHandle ();
879 // Called when the number of items has changed
880 internal void RefreshItems ()
882 Point pt = new Point (Location.X, Location.Y);
884 ThemeEngine.Current.CalcPopupMenuSize (DeviceContext, menu);
886 if ((pt.X + menu.Rect.Width) > SystemInformation.VirtualScreen.Width) {
887 if (((pt.X - menu.Rect.Width) > 0) && !(menu.parent_menu is MainMenu))
888 pt.X = pt.X - menu.Rect.Width;
890 pt.X = SystemInformation.VirtualScreen.Width - menu.Rect.Width;
895 if ((pt.Y + menu.Rect.Height) > SystemInformation.VirtualScreen.Height) {
896 if ((pt.Y - menu.Rect.Height) > 0)
897 pt.Y = pt.Y - menu.Rect.Height;
899 pt.Y = SystemInformation.VirtualScreen.Height - menu.Rect.Height;
906 Width = menu.Rect.Width;
907 Height = menu.Rect.Height;
910 internal override bool ActivateOnShow { get { return false; } }