/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections.Generic; using System.Drawing; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.Containers; using System.Collections; using System.Windows.Forms; using JetBrains.DataStructures; namespace JetBrains.Omea.GUIControls { public class MenuHostWrapper { private readonly object _menuParent; public MenuHostWrapper( ToolStripDropDown menuStrip, MethodInvoker action ) { _menuParent = menuStrip; menuStrip.Opening += delegate { action.Invoke(); }; } public MenuHostWrapper( ToolStripDropDownItem menuItem, MethodInvoker action ) { _menuParent = menuItem; menuItem.DropDownOpening += delegate{ action.Invoke(); }; } public ToolStripItemCollection Items { get { return (_menuParent is ContextMenuStrip) ? ((ContextMenuStrip)_menuParent).Items : ((ToolStripMenuItem)_menuParent).DropDownItems; } } } public abstract class MenuActionManager { internal class MenuAction { private readonly string _resourceType; private readonly IAction _action; private readonly IActionStateFilter[] _filters; private readonly Image _actionIcon; public MenuAction( string resourceType, string name, Image icon, IAction action, IActionStateFilter[] filters ) { _resourceType = resourceType; Name = name; _action = action; _filters = filters; _actionIcon = icon; } internal string ResourceType { get { return _resourceType; } } internal string Name { get; set; } internal IAction Action { get { return _action; } } internal Image MenuIcon { get { return _actionIcon; } } internal IActionStateFilter[] Filters { get { return _filters; } } } internal class MenuActionGroup { private readonly string _name; private readonly string _submenuName; private readonly AnchoredList _actions = new AnchoredList(); public MenuActionGroup( string name, string submenuName ) { _actions.AllowDuplicates = true; _name = name; _submenuName = submenuName; } internal void Add( MenuAction action, ListAnchor anchor ) { _actions.Add( action.Action.ToString(), action, anchor ); } internal string Name { get { return _name; } } internal string SubmenuName { get { return _submenuName; } } public IEnumerable Actions { get { return _actions; } } public bool RemoveAction( IAction action ) { foreach( MenuAction menuAction in _actions ) { if ( menuAction.Action == action ) { _actions.Remove( menuAction ); return true; } } return false; } } protected bool _persistentMnemonics = true; protected bool _allowVisibleResTypeMismatched; private bool _mnemonicsAssigned; private readonly MenuHostWrapper _menu; private readonly AnchoredList _actionGroups = new AnchoredList(); // private readonly HashSet _usedMnemonics = new HashSet(); private readonly HashSet _suppressedSeparators = new HashSet(); // MenuItem -> MenuAction private readonly Dictionary _itemToActionMap = new Dictionary(); protected abstract IActionContext CurrentContext { get; } #region Ctor and Properties protected MenuActionManager( ToolStripDropDownItem menu ) { _menu = new MenuHostWrapper( menu, UpdateMenuActions ); } protected MenuActionManager( ToolStripDropDown menu ) { _menu = new MenuHostWrapper( menu, UpdateMenuActions ); } #endregion Ctor and Properties #region Register/Unregister Group and Action public void RegisterGroup( string name, string submenuName, ListAnchor anchor ) { if ( _actionGroups.FindByKey( name ) == null ) { _actionGroups.Add( name, new MenuActionGroup( name, submenuName ), anchor ); } } public bool ContainsGroup( string groupName ) { return _actionGroups.FindByKey( groupName ) != null; } /// /// Registers an action in an action group of the menu. /// public void RegisterAction( IAction action, string groupId, ListAnchor anchor, string text, Image iconRes, string resourceType, IActionStateFilter[] filters ) { MenuActionGroup group = (MenuActionGroup) _actionGroups.FindByKey( groupId ); if ( group == null ) throw new ArgumentException( "ContextMenuManager -- Invalid action group name " + groupId, "groupId" ); _mnemonicsAssigned = false; group.Add( new MenuAction( resourceType, text, iconRes, action, filters ), anchor ); } /// If the specified action is present in one of the action groups of the manager, /// removes it from there. public bool UnregisterAction( IAction action ) { foreach( MenuActionGroup group in _actionGroups ) { if ( group.RemoveAction( action ) ) { return true; } } return false; } public void SuppressGroupSeparator( string groupId1, string groupId2 ) { _suppressedSeparators.Add( groupId1 + ":" + groupId2 ); _suppressedSeparators.Add( groupId2 + ":" + groupId1 ); } #endregion Register/Unregister Group and Action #region Menonics Assignment private void AssignMnemonics() { ResetUsedMnemonics(); foreach( MenuActionGroup group in _actionGroups ) foreach( MenuAction action in group.Actions ) { action.Name = AssignMnemonic( action.Name ); } } private void ResetUsedMnemonics() { _usedMnemonics.Clear(); foreach( MenuActionGroup group in _actionGroups ) foreach( MenuAction action in group.Actions ) { CheckMnemonicAssigned( action.Name ); } } /// /// If an '&' char is already present in the menu item text, store it /// in order next mnemonics are not clashed with them. /// private bool CheckMnemonicAssigned( string text ) { int i = text.IndexOf( '&', 0, Math.Max( text.Length - 1, 0 ) ); bool found = (i != -1) && (text[ i + 1 ] != '&'); if( found ) _usedMnemonics.Add( Char.ToLower( text[ i + 1 ] ) ); return found; } /// /// Automatically assigns a mnemonic for a menu action. /// private string AssignMnemonic( string text ) { if ( CheckMnemonicAssigned( text ) ) { return text; } // try to assign mnemonics on word start characters for( int i = 0; i < text.Length - 1; i++ ) { if ( Char.IsWhiteSpace( text, i ) && !Char.IsWhiteSpace( text, i + 1 ) ) { string res = CheckAndAssign( text, i + 1 ); if( res != null ) return res; } } // then, try any character for( int i = 0; i < text.Length; i++ ) { if ( !Char.IsWhiteSpace( text, i ) ) { string res = CheckAndAssign( text, i ); if( res != null ) return res; } } return text; } private string CheckAndAssign( string text, int i ) { char candidate = Char.ToLower( text [ i ] ); if ( !_usedMnemonics.Contains( candidate ) ) { _usedMnemonics.Add( candidate ); return text.Substring( 0, i ) + '&' + text.Substring( i ); } return null; } #endregion Menonics Assignment #region FillMenu /// /// Fills the menu with actions, depending on the specified context. /// public void FillMenuIfNecessary( IActionContext context ) { if( _menu.Items.Count == 0 ) { if ( _persistentMnemonics ) { if ( !_mnemonicsAssigned ) { AssignMnemonics(); _mnemonicsAssigned = true; } } else { ResetUsedMnemonics(); } FillMenuImpl( context ); } } private void FillMenuImpl( IActionContext context ) { bool haveDefaultAction = false; HashSet usedShortcuts = new HashSet(); MenuActionGroup lastGroup = null; IResourceList selection = context.SelectedResources; ToolStripItemCollection items = _menu.Items; foreach( MenuActionGroup group in _actionGroups ) { if( !IsSeparatorSuppressedBetweenGroups( lastGroup, group ) && IsParentContainerKeeped( items, lastGroup, group ) ) items.Add( new ToolStripSeparator() ); if( !IsParentContainerKeeped( items, lastGroup, group ) ) items = _menu.Items; if( group.SubmenuName != null ) { if ( lastGroup == null || group.SubmenuName != lastGroup.SubmenuName ) { ToolStripMenuItem subMenu = new ToolStripMenuItem( group.SubmenuName ); items.Add( subMenu ); items = subMenu.DropDownItems; } } lastGroup = group; foreach( MenuAction action in group.Actions ) { ToolStripMenuItem item = AddActionToMenu( action, items, context, usedShortcuts ); if( !haveDefaultAction && selection.Count == 1 && Core.ActionManager.GetDoubleClickAction( selection[ 0 ] ) == action.Action ) { item.Font = new Font( item.Font, FontStyle.Bold ); haveDefaultAction = true; } } } } private bool IsSeparatorSuppressedBetweenGroups( MenuActionGroup lastGroup, MenuActionGroup group ) { return (lastGroup == null) || IsSeparatorSuppressed( group, lastGroup ); } private bool IsParentContainerKeeped( ToolStripItemCollection items, MenuActionGroup lastGroup, MenuActionGroup group ) { return (items == _menu.Items) || (group.SubmenuName != null) && (lastGroup != null) && (group.SubmenuName.Equals( lastGroup.SubmenuName )); } private ToolStripMenuItem AddActionToMenu( MenuAction menuAction, ToolStripItemCollection curMenuItems, IActionContext context, HashSet usedShortcuts ) { ActionPresentation presentation = new ActionPresentation(); presentation.Reset(); ToolStripMenuItem item = new ToolStripMenuItem(); item.Text = menuAction.Name; Keys? keyShortcut = Core.ActionManager.GetKeyboardShortcutEx( menuAction.Action, context ); if ( keyShortcut != null && !usedShortcuts.Contains( keyShortcut ) ) { item.ShortcutKeys = (Keys)keyShortcut; usedShortcuts.Add( keyShortcut ); } if( menuAction.MenuIcon != null ) { item.Image = menuAction.MenuIcon; } item.Click += ExecuteMenuAction; curMenuItems.Add( item ); _itemToActionMap[ item ] = menuAction; return item; } #endregion FillMenu #region Update Menu /// /// Updates the visible and enabled state of actions in the menu without rebuilding /// the menu entirely. /// If there is no menu item in the menu, then we need to fill it for a first time /// from the registered groups and actions. /// public void UpdateMenuActions() { IActionContext context = CurrentContext; string[] resTypes = context.SelectedResources.GetAllTypes(); FillMenuIfNecessary( context ); UpdateMenuActions( context, resTypes, _menu.Items ); } private bool UpdateMenuActions( IActionContext context, string[] resTypes, ToolStripItemCollection items ) { bool anyVisible = false; bool anyItemSinceSeparator = false; ToolStripSeparator lastSeparator = null; ActionPresentation presentation = new ActionPresentation(); if( !_persistentMnemonics ) ResetUsedMnemonics(); foreach( ToolStripItem item in items ) { presentation.Reset(); if( item is ToolStripMenuItem ) { if( _itemToActionMap.ContainsKey( (ToolStripMenuItem)item ) ) { MenuAction menuAction = _itemToActionMap[ (ToolStripMenuItem)item ]; UpdateAction( menuAction, context, resTypes, ref presentation ); } else if( HaveGroupBySubName( item.Text ) ) { presentation.Visible = UpdateMenuActions( context, resTypes, ((ToolStripMenuItem)item).DropDownItems ); } anyItemSinceSeparator = (anyItemSinceSeparator || presentation.Visible); anyVisible = (anyVisible || presentation.Visible); SetActionFlags( item, presentation ); } else // ToolStripSeparator { item.Visible = anyItemSinceSeparator; lastSeparator = anyItemSinceSeparator ? (ToolStripSeparator)item : null; anyItemSinceSeparator = false; } } // Remove a separator if it is the last item in the list. if( !anyItemSinceSeparator && (lastSeparator != null) ) lastSeparator.Visible = false; return anyVisible; } private void UpdateAction( MenuAction action, IActionContext context, string[] resTypes, ref ActionPresentation presentation ) { presentation.Text = action.Name; try { if( action.ResourceType != null && !IsActionTypeMatches( resTypes, action ) ) { presentation.Visible = _allowVisibleResTypeMismatched; presentation.Enabled = false; return; } if ( action.Filters != null ) { foreach( IActionStateFilter filter in action.Filters ) { filter.Update( context, ref presentation ); if ( !presentation.Visible ) break; } } if ( presentation.Visible ) { string text = presentation.Text; action.Action.Update( context, ref presentation ); if( !_persistentMnemonics && presentation.Visible ) presentation.Text = AssignMnemonic( presentation.Text ); if( text != presentation.Text ) presentation.TextChanged = true; } } catch( Exception ex ) { Core.ReportException( ex, false ); presentation.Visible = false; } } private static bool IsActionTypeMatches( string[] resTypes, MenuAction action ) { return resTypes.Length == 1 && resTypes [ 0 ] == action.ResourceType; } private bool HaveGroupBySubName( string name ) { foreach( MenuActionGroup gr in _actionGroups ) { if( gr.SubmenuName == name ) return true; } return false; } /// Sets the properties of a menu item from an ActionPresentation instance. private static void SetActionFlags( ToolStripItem item, ActionPresentation presentation ) { item.Enabled = presentation.Enabled; item.Visible = presentation.Visible; if( item is ToolStripMenuItem ) { ((ToolStripMenuItem)item).Checked = presentation.Checked; } if( presentation.TextChanged ) item.Text = presentation.Text; } private bool IsSeparatorSuppressed( MenuActionGroup group1, MenuActionGroup group2 ) { return _suppressedSeparators.Contains( group1.Name + ":" + group2.Name ); } #endregion Update Menu #region Execute private void ExecuteMenuAction( object sender, EventArgs e ) { MenuAction menuAction = _itemToActionMap[ (ToolStripMenuItem) sender ]; if ( menuAction != null ) { try { menuAction.Action.Execute( CurrentContext ); } catch( Exception ex ) { Core.ReportException( ex, false ); } } } #endregion Execute } public class MainMenuActionManager : MenuActionManager { public MainMenuActionManager( ToolStripDropDownItem menu ) : base( menu ) { _persistentMnemonics = _allowVisibleResTypeMismatched = true; } protected override IActionContext CurrentContext { get { return Core.ActionManager.GetMainMenuActionContext(); } } } public class ContextMenuActionManager : MenuActionManager { protected IActionContext _lastContext; public ContextMenuActionManager( ToolStripDropDown menu ) : base( menu ) { _persistentMnemonics = _allowVisibleResTypeMismatched = false; } protected override IActionContext CurrentContext { get { #region Preconditions if( _lastContext == null ) throw new InvalidOperationException( "ContextMenuActionManager -- ActionContext must be set before its usage." ); #endregion Preconditions return _lastContext; } } public IActionContext ActionContext { set { _lastContext = value; } } } }