Skip to content

Commit

Permalink
Refactor window menu composition: simplify by using WPF patterns.
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-englert committed Sep 3, 2024
1 parent d7b6ba4 commit c6697c5
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 207 deletions.
21 changes: 13 additions & 8 deletions ILSpy/Docking/PaneCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
Expand All @@ -25,16 +26,14 @@

namespace ICSharpCode.ILSpy.Docking
{
public class PaneCollection<T> : INotifyCollectionChanged, ICollection<T>
public class PaneCollection<T> : INotifyCollectionChanged, IList<T>
where T : PaneModel, new()
{
private ObservableCollection<T> observableCollection = new ObservableCollection<T>();
private readonly ObservableCollection<T> observableCollection = [];

public event NotifyCollectionChangedEventHandler CollectionChanged;

public PaneCollection()
{
observableCollection.CollectionChanged += (sender, e) => CollectionChanged?.Invoke(this, e);
public event NotifyCollectionChangedEventHandler CollectionChanged {
add => observableCollection.CollectionChanged += value;
remove => observableCollection.CollectionChanged -= value;
}

public void Add(T item = null)
Expand All @@ -46,7 +45,6 @@ public void Add(T item = null)
item.IsVisible = true;
item.IsActive = true;
}

public int Count => observableCollection.Count;
public bool IsReadOnly => false;
public void Clear() => observableCollection.Clear();
Expand All @@ -55,5 +53,12 @@ public void Add(T item = null)
public bool Remove(T item) => observableCollection.Remove(item);
public IEnumerator<T> GetEnumerator() => observableCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => observableCollection.GetEnumerator();
int IList<T>.IndexOf(T item) => observableCollection.IndexOf(item);

Check warning on line 56 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.IndexOf' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 56 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.IndexOf' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 56 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.IndexOf' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 56 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.IndexOf' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
void IList<T>.Insert(int index, T item) => throw new NotImplementedException("Only Add is supported");
void IList<T>.RemoveAt(int index) => observableCollection.RemoveAt(index);

Check warning on line 58 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.RemoveAt' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 58 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.RemoveAt' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 58 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.RemoveAt' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 58 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.RemoveAt' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
T IList<T>.this[int index] {
get => observableCollection[index];

Check warning on line 60 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.get_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 60 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.get_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 60 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.get_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 60 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.get_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
set => observableCollection[index] = value;

Check warning on line 61 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.set_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 61 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.set_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 61 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.set_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 61 in ILSpy/Docking/PaneCollection.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'PaneCollection' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'System.Collections.Generic.IList<T>.set_Item' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
}
}
}
275 changes: 76 additions & 199 deletions ILSpy/Util/MenuService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
Expand All @@ -14,6 +12,8 @@
using ICSharpCode.ILSpy.ViewModels;

using TomsToolbox.Composition;
using TomsToolbox.ObservableCollections;
using TomsToolbox.Wpf.Converters;

namespace ICSharpCode.ILSpy.Util
{
Expand Down Expand Up @@ -111,193 +111,18 @@ public void InitWindowMenu(Menu mainMenu, InputBindingCollection inputBindings)
{
var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));

var separatorBeforeTools = new Separator();
var separatorBeforeDocuments = new Separator();
var defaultItems = windowMenuItem.Items.Cast<object>().ToArray();

windowMenuItem.Items.Add(separatorBeforeTools);
windowMenuItem.Items.Add(separatorBeforeDocuments);
windowMenuItem.Items.Clear();

var dock = DockWorkspace.Instance;

dock.ToolPanes.CollectionChanged += ToolsChanged;
dock.TabPages.CollectionChanged += TabsChanged;
MessageBus<DockWorkspaceActiveTabPageChangedEventArgs>.Subscribers += ActiveTabPageChanged;
var toolItems = dock.ToolPanes.ObservableSelect(toolPane => CreateMenuItem(toolPane, inputBindings));
var tabItems = dock.TabPages.ObservableSelect(tabPage => CreateMenuItem(tabPage, dock));

ToolsChanged(dock.ToolPanes, new(NotifyCollectionChangedAction.Reset));
TabsChanged(dock.TabPages, new(NotifyCollectionChangedAction.Reset));
var allItems = new ObservableCompositeCollection<object>(defaultItems, [new Separator()], toolItems, [new Separator()], tabItems);

void ToolsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
int endIndex = windowMenuItem.Items.IndexOf(separatorBeforeDocuments);
int startIndex = windowMenuItem.Items.IndexOf(separatorBeforeTools) + 1;
int insertionIndex;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
insertionIndex = Math.Min(endIndex, startIndex + e.NewStartingIndex);
foreach (ToolPaneModel pane in e.NewItems)
{
MenuItem menuItem = CreateMenuItem(pane);
windowMenuItem.Items.Insert(insertionIndex, menuItem);
insertionIndex++;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (ToolPaneModel pane in e.OldItems)
{
for (int i = endIndex - 1; i >= startIndex; i--)
{
MenuItem item = (MenuItem)windowMenuItem.Items[i];
if (pane == item.Tag)
{
windowMenuItem.Items.RemoveAt(i);
item.Tag = null;
endIndex--;
break;
}
}
}
break;
case NotifyCollectionChangedAction.Replace:
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
for (int i = endIndex - 1; i >= startIndex; i--)
{
MenuItem item = (MenuItem)windowMenuItem.Items[0];
item.Tag = null;
windowMenuItem.Items.RemoveAt(i);
endIndex--;
}
insertionIndex = endIndex;
foreach (ToolPaneModel pane in dock.ToolPanes)
{
MenuItem menuItem = CreateMenuItem(pane);
windowMenuItem.Items.Insert(insertionIndex, menuItem);
insertionIndex++;
}
break;
}

MenuItem CreateMenuItem(ToolPaneModel pane)
{
MenuItem menuItem = new();
menuItem.Command = pane.AssociatedCommand ?? new ToolPaneCommand(pane.ContentId);
menuItem.Header = pane.Title;
menuItem.Tag = pane;
var shortcutKey = pane.ShortcutKey;
if (shortcutKey != null)
{
inputBindings.Add(new(menuItem.Command, shortcutKey));
menuItem.InputGestureText = shortcutKey.GetDisplayStringForCulture(CultureInfo.CurrentUICulture);
}
if (!string.IsNullOrEmpty(pane.Icon))
{
menuItem.Icon = new Image {
Width = 16,
Height = 16,
Source = Images.Load(pane, pane.Icon)
};
}

return menuItem;
}
}

void TabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
int endIndex = windowMenuItem.Items.Count;
int startIndex = windowMenuItem.Items.IndexOf(separatorBeforeDocuments) + 1;
int insertionIndex;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
insertionIndex = Math.Min(endIndex, startIndex + e.NewStartingIndex);
foreach (TabPageModel pane in e.NewItems)
{
MenuItem menuItem = CreateMenuItem(pane);
pane.PropertyChanged += TabPageChanged;
windowMenuItem.Items.Insert(insertionIndex, menuItem);
insertionIndex++;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (TabPageModel pane in e.OldItems)
{
for (int i = endIndex - 1; i >= startIndex; i--)
{
MenuItem item = (MenuItem)windowMenuItem.Items[i];
if (pane == item.Tag)
{
windowMenuItem.Items.RemoveAt(i);
pane.PropertyChanged -= TabPageChanged;
item.Tag = null;
endIndex--;
break;
}
}
}
break;
case NotifyCollectionChangedAction.Replace:
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
for (int i = endIndex - 1; i >= startIndex; i--)
{
MenuItem item = (MenuItem)windowMenuItem.Items[i];
windowMenuItem.Items.RemoveAt(i);
((TabPageModel)item.Tag).PropertyChanged -= TabPageChanged;
endIndex--;
}
insertionIndex = endIndex;
foreach (TabPageModel pane in dock.TabPages)
{
MenuItem menuItem = CreateMenuItem(pane);
pane.PropertyChanged += TabPageChanged;
windowMenuItem.Items.Insert(insertionIndex, menuItem);
insertionIndex++;
}
break;
}

MenuItem CreateMenuItem(TabPageModel pane)
{
MenuItem menuItem = new();
menuItem.Command = new TabPageCommand(pane);
menuItem.Header = pane.Title.Length > 20 ? pane.Title.Substring(20) + "..." : pane.Title;
menuItem.Tag = pane;
menuItem.IsCheckable = true;
menuItem.IsChecked = menuItem.Tag == dock.ActiveTabPage;

return menuItem;
}
}

void TabPageChanged(object sender, PropertyChangedEventArgs e)
{
var windowMenu = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
foreach (MenuItem menuItem in windowMenu.Items.OfType<MenuItem>())
{
if (menuItem.IsCheckable && menuItem.Tag == sender)
{
string title = ((TabPageModel)sender).Title;
menuItem.Header = title.Length > 20 ? title.Substring(0, 20) + "..." : title;
}
}
}

void ActiveTabPageChanged(object sender, EventArgs e)
{
foreach (MenuItem menuItem in windowMenuItem.Items.OfType<MenuItem>())
{
if (menuItem.IsCheckable && menuItem.Tag is TabPageModel)
{
menuItem.IsChecked = menuItem.Tag == dock.ActiveTabPage;
}
}
}
windowMenuItem.ItemsSource = allItems;
}

public void InitToolbar(ToolBar toolBar)
Expand All @@ -311,49 +136,101 @@ public void InitToolbar(ToolBar toolBar)
{
foreach (var command in commandGroup)
{
toolBar.Items.Insert(navigationPos++, MakeToolbarItem(command));
toolBar.Items.Insert(navigationPos++, CreateToolbarItem(command));
openPos++;
}
}
else if (commandGroup.Key == Properties.Resources.ResourceManager.GetString("Open"))
{
foreach (var command in commandGroup)
{
toolBar.Items.Insert(openPos++, MakeToolbarItem(command));
toolBar.Items.Insert(openPos++, CreateToolbarItem(command));
}
}
else
{
toolBar.Items.Add(new Separator());
foreach (var command in commandGroup)
{
toolBar.Items.Add(MakeToolbarItem(command));
toolBar.Items.Add(CreateToolbarItem(command));
}
}
}

}

Button MakeToolbarItem(IExport<ICommand, IToolbarCommandMetadata> command)
public void Init(Menu mainMenu, ToolBar toolBar, InputBindingCollection inputBindings)
{
InitMainMenu(mainMenu);
InitWindowMenu(mainMenu, inputBindings);
InitToolbar(toolBar);
}

static object CreateMenuItem(TabPageModel pane, DockWorkspace dock)
{
var header = new TextBlock {
MaxWidth = 200,
TextTrimming = TextTrimming.CharacterEllipsis
};

header.SetBinding(TextBlock.TextProperty, new Binding(nameof(pane.Title)) {
Source = pane
});

MenuItem menuItem = new() {
Command = new TabPageCommand(pane),
Header = header,
Tag = pane,
IsCheckable = true
};

menuItem.SetBinding(MenuItem.IsCheckedProperty, new Binding(nameof(dock.ActiveTabPage)) {
Source = dock,
ConverterParameter = pane,
Converter = BinaryOperationConverter.Equality
});

return menuItem;
}

static object CreateMenuItem(ToolPaneModel pane, InputBindingCollection inputBindings)
{
MenuItem menuItem = new() {
Command = pane.AssociatedCommand ?? new ToolPaneCommand(pane.ContentId),
Header = pane.Title,
Tag = pane
};
var shortcutKey = pane.ShortcutKey;
if (shortcutKey != null)
{
inputBindings.Add(new(menuItem.Command, shortcutKey));
menuItem.InputGestureText = shortcutKey.GetDisplayStringForCulture(CultureInfo.CurrentUICulture);
}
if (!string.IsNullOrEmpty(pane.Icon))
{
menuItem.Icon = new Image {
Width = 16,
Height = 16,
Source = Images.Load(pane, pane.Icon)
};
}

return menuItem;
}

static Button CreateToolbarItem(IExport<ICommand, IToolbarCommandMetadata> command)
{
return new() {
Style = ThemeManager.Current.CreateToolBarButtonStyle(),
Command = CommandWrapper.Unwrap(command.Value),
ToolTip = Properties.Resources.ResourceManager.GetString(command.Metadata.ToolTip),
Tag = command.Metadata.Tag,
ToolTip = Properties.Resources.ResourceManager.GetString(command.Metadata?.ToolTip),
Tag = command.Metadata?.Tag,
Content = new Image {
Width = 16,
Height = 16,
Source = Images.Load(command.Value, command.Metadata.ToolbarIcon)
Source = Images.Load(command.Value, command.Metadata?.ToolbarIcon)
}
};
}

public void Init(Menu mainMenu, ToolBar toolBar, InputBindingCollection inputBindings)
{
InitMainMenu(mainMenu);
InitWindowMenu(mainMenu, inputBindings);
InitToolbar(toolBar);
}
}
}

0 comments on commit c6697c5

Please sign in to comment.