From 2defb01c253037c50ea69b2ca7608ed974edc3a0 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sun, 17 Sep 2023 21:32:32 +0200 Subject: [PATCH] Add new InfoBar control for displaying notifications to the user --- .../dnSpy.Contracts.DnSpy/App/IAppInfoBar.cs | 33 +++++++ dnSpy/dnSpy.Contracts.DnSpy/App/IAppWindow.cs | 5 + .../App/IInfoBarElement.cs | 40 ++++++++ .../App/IInfoBarInteractionContext.cs | 40 ++++++++ .../dnSpy.Contracts.DnSpy/App/InfoBarIcon.cs | 40 ++++++++ .../App/InfoBarInteraction.cs | 45 +++++++++ .../dnSpy.Contracts.DnSpy/Themes/ColorType.cs | 5 + dnSpy/dnSpy/MainApp/AppInfoBar.cs | 42 ++++++++ dnSpy/dnSpy/MainApp/AppWindow.cs | 7 +- dnSpy/dnSpy/MainApp/InfoBar.xaml | 83 ++++++++++++++++ dnSpy/dnSpy/MainApp/InfoBar.xaml.cs | 27 +++++ dnSpy/dnSpy/MainApp/InfoBarVM.cs | 99 +++++++++++++++++++ dnSpy/dnSpy/Themes/ColorInfos.cs | 20 ++++ dnSpy/dnSpy/Themes/blue.dntheme | 5 + dnSpy/dnSpy/Themes/dark.dntheme | 5 + dnSpy/dnSpy/Themes/light.dntheme | 5 + 16 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/App/IAppInfoBar.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarElement.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarInteractionContext.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarIcon.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarInteraction.cs create mode 100644 dnSpy/dnSpy/MainApp/AppInfoBar.cs create mode 100644 dnSpy/dnSpy/MainApp/InfoBar.xaml create mode 100644 dnSpy/dnSpy/MainApp/InfoBar.xaml.cs create mode 100644 dnSpy/dnSpy/MainApp/InfoBarVM.cs diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/IAppInfoBar.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppInfoBar.cs new file mode 100644 index 0000000000..c43537874b --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppInfoBar.cs @@ -0,0 +1,33 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +namespace dnSpy.Contracts.App { + /// + /// App info bar + /// + public interface IAppInfoBar { + /// + /// Shows a new info bar element of the specific icon with the given message and interactions. + /// + /// The message to display + /// The icon of message + /// Possible interactions on the element + public IInfoBarElement Show(string message, InfoBarIcon icon = InfoBarIcon.Information, params InfoBarInteraction[] interactions); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/IAppWindow.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppWindow.cs index c0dc4caead..eee77a4197 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/App/IAppWindow.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/IAppWindow.cs @@ -53,6 +53,11 @@ public interface IAppWindow { /// IAppStatusBar StatusBar { get; } + /// + /// Gets the instance + /// + IAppInfoBar InfoBar { get; } + /// /// true if the app has been loaded /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarElement.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarElement.cs new file mode 100644 index 0000000000..9344ffba53 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarElement.cs @@ -0,0 +1,40 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +namespace dnSpy.Contracts.App { + /// + /// A single element in the info bar + /// + public interface IInfoBarElement { + /// + /// Message being displayed as part of this info bar element + /// + string Message { get; } + + /// + /// Icon being displayed as part of this info bar element + /// + InfoBarIcon Icon { get; } + + /// + /// Closes the info bar element + /// + void Close(); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarInteractionContext.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarInteractionContext.cs new file mode 100644 index 0000000000..fc75af2eac --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/IInfoBarInteractionContext.cs @@ -0,0 +1,40 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +namespace dnSpy.Contracts.App { + /// + /// Context passed to an interaction + /// + public interface IInfoBarInteractionContext { + /// + /// Interaction text + /// + string InteractionText { get; } + + /// + /// Closes the owning this interaction + /// + void CloseElement(); + + /// + /// Removes the interaction from the + /// + void RemoveInteraction(); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarIcon.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarIcon.cs new file mode 100644 index 0000000000..25c8429f56 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarIcon.cs @@ -0,0 +1,40 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +namespace dnSpy.Contracts.App { + /// + /// Icon displayed in the info bar + /// + public enum InfoBarIcon { + /// + /// Information icon + /// + Information, + + /// + /// Warning icon + /// + Warning, + + /// + /// Error icon + /// + Error + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarInteraction.cs b/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarInteraction.cs new file mode 100644 index 0000000000..340539a887 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/App/InfoBarInteraction.cs @@ -0,0 +1,45 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; + +namespace dnSpy.Contracts.App { + /// + /// Interaction on an + /// + public readonly struct InfoBarInteraction { + /// + /// Interaction text + /// + public string Text { get; } + + /// + /// Action to perform when the interaction is clicked + /// + public Action Action { get; } + + /// + /// Creates a new + /// + public InfoBarInteraction(string text, Action action) { + Text = text; + Action = action; + } + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Themes/ColorType.cs b/dnSpy/dnSpy.Contracts.DnSpy/Themes/ColorType.cs index ab5685e65e..83d291d3c5 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Themes/ColorType.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Themes/ColorType.cs @@ -789,6 +789,11 @@ public enum ColorType : uint { HyperlinkNormal, HyperlinkMouseOver, HyperlinkDisabled, + InfoBarBackground, + InfoBarText, + InfoBarInteractionText, + InfoBarCloseButton, + InfoBarCloseButtonHover, // Add new color types before this comment diff --git a/dnSpy/dnSpy/MainApp/AppInfoBar.cs b/dnSpy/dnSpy/MainApp/AppInfoBar.cs new file mode 100644 index 0000000000..c7caa918e1 --- /dev/null +++ b/dnSpy/dnSpy/MainApp/AppInfoBar.cs @@ -0,0 +1,42 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System.ComponentModel.Composition; +using dnSpy.Contracts.App; +using dnSpy.Controls; + +namespace dnSpy.MainApp { + [Export] + sealed class AppInfoBar : IAppInfoBar, IStackedContentChild { + readonly InfoBar infoBar; + readonly InfoBarVM infoBarVM; + + public object UIObject => infoBar; + + public AppInfoBar() => infoBar = new InfoBar { DataContext = infoBarVM = new InfoBarVM() }; + + public IInfoBarElement Show(string message, InfoBarIcon icon = InfoBarIcon.Information, params InfoBarInteraction[] interactions) { + var notification = new InfoBarElementVM(infoBarVM, message, icon); + foreach (var interaction in interactions) + notification.AddInteraction(interaction.Text, interaction.Action); + infoBarVM.Elements.Insert(0, notification); + return notification; + } + } +} diff --git a/dnSpy/dnSpy/MainApp/AppWindow.cs b/dnSpy/dnSpy/MainApp/AppWindow.cs index b7135002e9..50883b5ef0 100644 --- a/dnSpy/dnSpy/MainApp/AppWindow.cs +++ b/dnSpy/dnSpy/MainApp/AppWindow.cs @@ -42,6 +42,9 @@ sealed class AppWindow : IAppWindow, IDsLoaderContentProvider { public IAppStatusBar StatusBar => statusBar; readonly AppStatusBar statusBar; + public IAppInfoBar InfoBar => infoBar; + readonly AppInfoBar infoBar; + Window IAppWindow.MainWindow => mainWindow!; internal MainWindow MainWindow => mainWindow!; MainWindow? mainWindow; @@ -96,7 +99,7 @@ public void Write() { readonly MainWindowControl mainWindowControl; [ImportingConstructor] - AppWindow(ISettingsService settingsService, IDocumentTabService documentTabService, AppToolBar appToolBar, MainWindowControl mainWindowControl, IWpfCommandService wpfCommandService) { + AppWindow(ISettingsService settingsService, IDocumentTabService documentTabService, AppToolBar appToolBar, AppInfoBar infoBar, MainWindowControl mainWindowControl, IWpfCommandService wpfCommandService) { assemblyInformationalVersion = CalculateAssemblyInformationalVersion(GetType().Assembly); uiSettings = new UISettings(settingsService); uiSettings.Read(); @@ -104,6 +107,7 @@ public void Write() { this.documentTabService = documentTabService; statusBar = new AppStatusBar(); this.appToolBar = appToolBar; + this.infoBar = infoBar; this.mainWindowControl = mainWindowControl; this.wpfCommandService = wpfCommandService; mainWindowCommands = wpfCommandService.GetCommands(ControlConstants.GUID_MAINWINDOW); @@ -124,6 +128,7 @@ static string CalculateAssemblyInformationalVersion(Assembly asm) { public Window InitializeMainWindow() { var sc = new StackedContent(false); sc.AddChild(appToolBar, StackedContentChildInfo.CreateVertical(new GridLength(0, GridUnitType.Auto))); + sc.AddChild(infoBar, StackedContentChildInfo.CreateVertical(new GridLength(0, GridUnitType.Auto))); sc.AddChild(stackedContent, StackedContentChildInfo.CreateVertical(new GridLength(1, GridUnitType.Star))); sc.AddChild(statusBar, StackedContentChildInfo.CreateVertical(new GridLength(0, GridUnitType.Auto))); mainWindow = new MainWindow(sc.UIObject); diff --git a/dnSpy/dnSpy/MainApp/InfoBar.xaml b/dnSpy/dnSpy/MainApp/InfoBar.xaml new file mode 100644 index 0000000000..40d5f653a3 --- /dev/null +++ b/dnSpy/dnSpy/MainApp/InfoBar.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dnSpy/dnSpy/MainApp/InfoBar.xaml.cs b/dnSpy/dnSpy/MainApp/InfoBar.xaml.cs new file mode 100644 index 0000000000..702e51e07f --- /dev/null +++ b/dnSpy/dnSpy/MainApp/InfoBar.xaml.cs @@ -0,0 +1,27 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + + +using System.Windows.Controls; + +namespace dnSpy.MainApp { + public partial class InfoBar : UserControl { + public InfoBar() => InitializeComponent(); + } +} diff --git a/dnSpy/dnSpy/MainApp/InfoBarVM.cs b/dnSpy/dnSpy/MainApp/InfoBarVM.cs new file mode 100644 index 0000000000..efcafcfcec --- /dev/null +++ b/dnSpy/dnSpy/MainApp/InfoBarVM.cs @@ -0,0 +1,99 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; +using System.Collections.ObjectModel; +using System.Windows.Input; +using dnSpy.Contracts.App; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.MVVM; + +namespace dnSpy.MainApp { + sealed class InfoBarVM : ViewModelBase { + public ObservableCollection Elements { get; } = new ObservableCollection(); + + public ICommand RemoveElementCommand { get; } + + public InfoBarVM() => RemoveElementCommand = new RelayCommand(RemoveElement); + + internal void RemoveElement(object? obj) { + if (obj is not InfoBarElementVM notification) + return; + Elements.Remove(notification); + } + } + + sealed class InfoBarElementVM : ViewModelBase, IInfoBarElement { + readonly InfoBarVM parent; + + public string Message { get; } + + public InfoBarIcon Icon { get; } + + public ImageReference Image => Icon switch { + InfoBarIcon.Information => DsImages.StatusInformation, + InfoBarIcon.Warning => DsImages.StatusWarning, + InfoBarIcon.Error => DsImages.StatusError, + _ => DsImages.QuestionMark + }; + + public ObservableCollection Interactions { get; } = new ObservableCollection(); + + public InfoBarElementVM(InfoBarVM parent, string message, InfoBarIcon icon) { + this.parent = parent; + Message = message; + Icon = icon; + } + + public void Close() => parent.RemoveElement(this); + + public void AddInteraction(string text, Action action) => Interactions.Add(new InfoBarInteractionVM(this, text, action)); + + public void RemoveInteraction(InfoBarInteractionVM interaction) => Interactions.Remove(interaction); + } + + sealed class InfoBarInteractionVM : ViewModelBase { + internal readonly InfoBarElementVM parent; + + public string Text { get; } + public ICommand ActionCommand { get; } + + public InfoBarInteractionVM(InfoBarElementVM parent, string text, Action action) { + this.parent = parent; + Text = text; + ActionCommand = new RelayCommand(vm => { + if (vm is not InfoBarInteractionVM interactionVM) + return; + action(new NotificationInteractionContext(interactionVM)); + }); + } + } + + sealed class NotificationInteractionContext : IInfoBarInteractionContext { + readonly InfoBarInteractionVM _interactionVm; + + public string InteractionText => _interactionVm.Text; + + public NotificationInteractionContext(InfoBarInteractionVM interactionVm) => _interactionVm = interactionVm; + + public void CloseElement() => _interactionVm.parent.Close(); + + public void RemoveInteraction() => _interactionVm.parent.RemoveInteraction(_interactionVm); + } +} diff --git a/dnSpy/dnSpy/Themes/ColorInfos.cs b/dnSpy/dnSpy/Themes/ColorInfos.cs index 5182b678d8..e6fce28ad3 100644 --- a/dnSpy/dnSpy/Themes/ColorInfos.cs +++ b/dnSpy/dnSpy/Themes/ColorInfos.cs @@ -1877,6 +1877,26 @@ static class ColorInfos { DefaultBackground = "#FF6D6D6D", BackgroundResourceKey = "HyperlinkDisabled", }, + new BrushColorInfo(ColorType.InfoBarBackground, "") { + DefaultBackground = "#FFFDFBAC", + BackgroundResourceKey = "InfoBarBackground", + }, + new BrushColorInfo(ColorType.InfoBarText, "") { + DefaultBackground = "#FF000000", + BackgroundResourceKey = "InfoBarText", + }, + new BrushColorInfo(ColorType.InfoBarInteractionText, "") { + DefaultBackground = "#FF3399FF", + BackgroundResourceKey = "InfoBarInteractionText", + }, + new BrushColorInfo(ColorType.InfoBarCloseButton, "") { + DefaultBackground = "#FF696969", + BackgroundResourceKey = "InfoBarCloseButton", + }, + new BrushColorInfo(ColorType.InfoBarCloseButtonHover, "") { + DefaultBackground = "#FF000000", + BackgroundResourceKey = "InfoBarCloseButtonHover", + }, new BrushColorInfo(ColorType.LineNumber, "Line number"), new BrushColorInfo(ColorType.ReplLineNumberInput1, "REPL line number #1 (input)"), new BrushColorInfo(ColorType.ReplLineNumberInput2, "REPL line number #2 (input)"), diff --git a/dnSpy/dnSpy/Themes/blue.dntheme b/dnSpy/dnSpy/Themes/blue.dntheme index f3c9f507e4..cfbc2aa701 100644 --- a/dnSpy/dnSpy/Themes/blue.dntheme +++ b/dnSpy/dnSpy/Themes/blue.dntheme @@ -749,5 +749,10 @@ + + + + + diff --git a/dnSpy/dnSpy/Themes/dark.dntheme b/dnSpy/dnSpy/Themes/dark.dntheme index 8a6f724791..8e956e70af 100644 --- a/dnSpy/dnSpy/Themes/dark.dntheme +++ b/dnSpy/dnSpy/Themes/dark.dntheme @@ -749,5 +749,10 @@ + + + + + diff --git a/dnSpy/dnSpy/Themes/light.dntheme b/dnSpy/dnSpy/Themes/light.dntheme index 815bb335d3..929b1dc6ca 100644 --- a/dnSpy/dnSpy/Themes/light.dntheme +++ b/dnSpy/dnSpy/Themes/light.dntheme @@ -317,5 +317,10 @@ + + + + +