diff --git a/Rubberduck.Core/UI/Converters/GroupingToBooleanConverter.cs b/Rubberduck.Core/UI/Converters/GroupingToBooleanConverter.cs new file mode 100644 index 0000000000..de01f0da04 --- /dev/null +++ b/Rubberduck.Core/UI/Converters/GroupingToBooleanConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Rubberduck.UI.ToDoItems; + +namespace Rubberduck.UI.Converters +{ + public class ToDoItemGroupingToBooleanConverter : GroupingToBooleanConverter { } + + /// + /// Provides a mutually exclusive binding between an ToDoItemGrouping and a boolean. + /// Note: This is a stateful converter, so each bound control requires its own converter instance. + /// + public class GroupingToBooleanConverter : IValueConverter where T : IConvertible, IComparable + { + private T _state; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(parameter is T governing) || + !(value is T bound)) + { + return false; + } + + _state = bound; + return _state.Equals(governing); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(parameter is T governing) || + !(value is bool isSet)) + { + return _state; + } + + _state = isSet ? governing : _state; + return _state; + } + } +} diff --git a/Rubberduck.Core/UI/ToDoItems/ToDoExplorerControl.xaml b/Rubberduck.Core/UI/ToDoItems/ToDoExplorerControl.xaml index 103794e67e..76afea7032 100644 --- a/Rubberduck.Core/UI/ToDoItems/ToDoExplorerControl.xaml +++ b/Rubberduck.Core/UI/ToDoItems/ToDoExplorerControl.xaml @@ -5,22 +5,24 @@ xmlns:toDoItems="clr-namespace:Rubberduck.UI.ToDoItems" xmlns:themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" xmlns:controls="clr-namespace:Rubberduck.UI.Controls" - xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns:converters="clr-namespace:Rubberduck.UI.Converters" + xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" x:Class="Rubberduck.UI.ToDoItems.ToDoExplorerControl" Language="{UICulture}" mc:Ignorable="d" - d:DesignHeight="300" d:DesignWidth="300" + d:DesignHeight="300" d:DesignWidth="400" d:DataContext="{d:DesignInstance {x:Type toDoItems:ToDoExplorerViewModel}, IsDesignTimeCreatable=False}"> - - + + + - + + @@ -32,245 +34,15 @@ - - - + + + + + - - + - - + + + - - - - - - - - - - - - - - - - - - + @@ -480,73 +244,86 @@ - + - - - - - - - - - - + + + - - - - - - - - - - + SelectionUnit="FullRow" + InitialExpandedState="True"> + + + + diff --git a/Rubberduck.Core/UI/ToDoItems/ToDoExplorerViewModel.cs b/Rubberduck.Core/UI/ToDoItems/ToDoExplorerViewModel.cs index 5dc538ab51..53bb0a1a36 100644 --- a/Rubberduck.Core/UI/ToDoItems/ToDoExplorerViewModel.cs +++ b/Rubberduck.Core/UI/ToDoItems/ToDoExplorerViewModel.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Windows; using System.Text.RegularExpressions; +using System.Windows.Data; using NLog; using Rubberduck.Parsing.VBA; using Rubberduck.Settings; @@ -15,104 +17,98 @@ using Rubberduck.Parsing.Symbols; using Rubberduck.Resources.ToDoExplorer; using Rubberduck.Interaction.Navigation; +using Rubberduck.Parsing.UIContext; using Rubberduck.VBEditor.Utility; namespace Rubberduck.UI.ToDoItems { + public enum ToDoItemGrouping + { + None, + Marker, + Location + }; + public sealed class ToDoExplorerViewModel : ViewModelBase, INavigateSelection, IDisposable { private readonly RubberduckParserState _state; private readonly IGeneralConfigService _configService; private readonly ISettingsFormFactory _settingsFormFactory; - - public ToDoExplorerViewModel(RubberduckParserState state, IGeneralConfigService configService, ISettingsFormFactory settingsFormFactory, ISelectionService selectionService) + private readonly IUiDispatcher _uiDispatcher; + + public ToDoExplorerViewModel( + RubberduckParserState state, + IGeneralConfigService configService, + ISettingsFormFactory settingsFormFactory, + ISelectionService selectionService, + IUiDispatcher uiDispatcher) { _state = state; _configService = configService; _settingsFormFactory = settingsFormFactory; + _uiDispatcher = uiDispatcher; _state.StateChanged += HandleStateChanged; - _navigateCommand = new Lazy(() => new NavigateCommand(selectionService)); - - SetMarkerGroupingCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), param => - { - GroupByMarker = (bool)param; - GroupByLocation = !(bool)param; - }); + NavigateCommand = new NavigateCommand(selectionService); + RemoveCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteRemoveCommand, CanExecuteRemoveCommand); + CollapseAllCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteCollapseAll); + ExpandAllCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteExpandAll); + CopyResultsCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteCopyResultsCommand, CanExecuteCopyResultsCommand); + OpenTodoSettingsCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), ExecuteOpenTodoSettingsCommand); - SetLocationGroupingCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), param => - { - GroupByLocation = (bool)param; - GroupByMarker = !(bool)param; - }); + Items = CollectionViewSource.GetDefaultView(_items); + OnPropertyChanged(nameof(Items)); + Grouping = ToDoItemGrouping.Marker; } - private ObservableCollection _items = new ObservableCollection(); - public ObservableCollection Items - { - get => _items; - set - { - if (_items == value) - { - return; - } + private readonly ObservableCollection _items = new ObservableCollection(); - _items = value; - OnPropertyChanged(); - } - } + public ICollectionView Items { get; } - private bool _groupByMarker = true; - public bool GroupByMarker + private static readonly Dictionary GroupDescriptions = new Dictionary { - get => _groupByMarker; + { ToDoItemGrouping.Marker, new PropertyGroupDescription("Type") }, + { ToDoItemGrouping.Location, new PropertyGroupDescription("Selection.QualifiedName.Name") } + }; + + private ToDoItemGrouping _grouping; + public ToDoItemGrouping Grouping + { + get => _grouping; set { - if (_groupByMarker == value) + if (value == _grouping) { return; } - _groupByMarker = value; + _grouping = value; + Items.GroupDescriptions.Clear(); + Items.GroupDescriptions.Add(GroupDescriptions[_grouping]); + Items.Refresh(); OnPropertyChanged(); } } - private bool _groupByLocation; - public bool GroupByLocation + private bool _expanded; + public bool ExpandedState { - get => _groupByLocation; + get => _expanded; set { - if (_groupByLocation == value) - { - return; - } - - _groupByLocation = value; + _expanded = value; OnPropertyChanged(); } } - public CommandBase SetMarkerGroupingCommand { get; } - - public CommandBase SetLocationGroupingCommand { get; } - - private CommandBase _refreshCommand; - public CommandBase RefreshCommand + private ToDoItem _selectedItem; + public INavigateSource SelectedItem { - get + get => _selectedItem; + set { - if (_refreshCommand != null) - { - return _refreshCommand; - } - return _refreshCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), _ => - { - _state.OnParseRequested(this); - }, - _ => _state.IsDirty()); + _selectedItem = value as ToDoItem; + OnPropertyChanged(); } } @@ -123,122 +119,109 @@ private void HandleStateChanged(object sender, EventArgs e) return; } - Items = new ObservableCollection(GetItems()); + _uiDispatcher.Invoke(() => + { + _items.Clear(); + foreach (var item in _state.AllComments.SelectMany(GetToDoMarkers)) + { + _items.Add(item); + } + }); } - private ToDoItem _selectedItem; - public INavigateSource SelectedItem + public INavigateCommand NavigateCommand { get; } + + public ReparseCommand RefreshCommand { get; set; } + + public CommandBase RemoveCommand { get; } + + public CommandBase CollapseAllCommand { get; } + + public CommandBase ExpandAllCommand { get; } + + public CommandBase CopyResultsCommand { get; } + + public CommandBase OpenTodoSettingsCommand { get; } + + private void ExecuteCollapseAll(object parameter) { - get => _selectedItem; - set - { - _selectedItem = value as ToDoItem; - OnPropertyChanged(); - } + ExpandedState = false; } - private CommandBase _removeCommand; - public CommandBase RemoveCommand + private void ExecuteExpandAll(object parameter) { - get - { - if (_removeCommand != null) - { - return _removeCommand; - } - return _removeCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), _ => - { - if (_selectedItem == null) - { - return; - } + ExpandedState = true; + } - var component = _state.ProjectsProvider.Component(_selectedItem.Selection.QualifiedName); - using (var module = component.CodeModule) - { - var oldContent = module.GetLines(_selectedItem.Selection.Selection.StartLine, 1); - var newContent = oldContent.Remove(_selectedItem.Selection.Selection.StartColumn - 1); + private bool CanExecuteRemoveCommand(object obj) => SelectedItem != null && RefreshCommand.CanExecute(obj); - module.ReplaceLine(_selectedItem.Selection.Selection.StartLine, newContent); - } + private void ExecuteRemoveCommand(object obj) + { + if (!CanExecuteRemoveCommand(obj)) + { + return; + } - RefreshCommand.Execute(null); - } - ); + var component = _state.ProjectsProvider.Component(_selectedItem.Selection.QualifiedName); + using (var module = component.CodeModule) + { + var oldContent = module.GetLines(_selectedItem.Selection.Selection.StartLine, 1); + var newContent = oldContent.Remove(_selectedItem.Selection.Selection.StartColumn - 1); + + module.ReplaceLine(_selectedItem.Selection.Selection.StartLine, newContent); } + + RefreshCommand.Execute(null); } - private CommandBase _copyResultsCommand; - public CommandBase CopyResultsCommand + private bool CanExecuteCopyResultsCommand(object obj) => _items.Any(); + + public void ExecuteCopyResultsCommand(object obj) { - get + const string xmlSpreadsheetDataFormat = "XML Spreadsheet"; + if (!CanExecuteCopyResultsCommand(obj)) { - if (_copyResultsCommand != null) - { - return _copyResultsCommand; - } - return _copyResultsCommand = new DelegateCommand(LogManager.GetCurrentClassLogger(), _ => - { - const string xmlSpreadsheetDataFormat = "XML Spreadsheet"; - if (_items == null) - { - return; - } - ColumnInfo[] columnInfos = { new ColumnInfo("Type"), new ColumnInfo("Description"), new ColumnInfo("Project"), new ColumnInfo("Component"), new ColumnInfo("Line", hAlignment.Right), new ColumnInfo("Column", hAlignment.Right) }; + return; + } - var resultArray = _items.OfType().Select(result => result.ToArray()).ToArray(); + ColumnInfo[] columnInfos = { new ColumnInfo("Type"), new ColumnInfo("Description"), new ColumnInfo("Project"), new ColumnInfo("Component"), new ColumnInfo("Line", hAlignment.Right), new ColumnInfo("Column", hAlignment.Right) }; - var resource = _items.Count == 1 - ? ToDoExplorerUI.ToDoExplorer_NumberOfIssuesFound_Singular - : ToDoExplorerUI.ToDoExplorer_NumberOfIssuesFound_Plural; + var resultArray = _items.OfType().Select(result => result.ToArray()).ToArray(); - var title = string.Format(resource, DateTime.Now.ToString(CultureInfo.InvariantCulture), _items.Count); + var resource = _items.Count == 1 + ? ToDoExplorerUI.ToDoExplorer_NumberOfIssuesFound_Singular + : ToDoExplorerUI.ToDoExplorer_NumberOfIssuesFound_Plural; - var textResults = title + Environment.NewLine + string.Join("", _items.OfType().Select(result => result.ToClipboardString() + Environment.NewLine).ToArray()); - var csvResults = ExportFormatter.Csv(resultArray, title, columnInfos); - var htmlResults = ExportFormatter.HtmlClipboardFragment(resultArray, title, columnInfos); - var rtfResults = ExportFormatter.RTF(resultArray, title); + var title = string.Format(resource, DateTime.Now.ToString(CultureInfo.InvariantCulture), _items.Count); - // todo: verify that this disposing this stream breaks the xmlSpreadsheetDataFormat - var stream = ExportFormatter.XmlSpreadsheetNew(resultArray, title, columnInfos); + var textResults = title + Environment.NewLine + string.Join("", _items.OfType().Select(result => result.ToClipboardString() + Environment.NewLine).ToArray()); + var csvResults = ExportFormatter.Csv(resultArray, title, columnInfos); + var htmlResults = ExportFormatter.HtmlClipboardFragment(resultArray, title, columnInfos); + var rtfResults = ExportFormatter.RTF(resultArray, title); - IClipboardWriter _clipboard = new ClipboardWriter(); - //Add the formats from richest formatting to least formatting - _clipboard.AppendStream(DataFormats.GetDataFormat(xmlSpreadsheetDataFormat).Name, stream); - _clipboard.AppendString(DataFormats.Rtf, rtfResults); - _clipboard.AppendString(DataFormats.Html, htmlResults); - _clipboard.AppendString(DataFormats.CommaSeparatedValue, csvResults); - _clipboard.AppendString(DataFormats.UnicodeText, textResults); + // todo: verify that this disposing this stream breaks the xmlSpreadsheetDataFormat + var stream = ExportFormatter.XmlSpreadsheetNew(resultArray, title, columnInfos); - _clipboard.Flush(); + IClipboardWriter _clipboard = new ClipboardWriter(); + //Add the formats from richest formatting to least formatting + _clipboard.AppendStream(DataFormats.GetDataFormat(xmlSpreadsheetDataFormat).Name, stream); + _clipboard.AppendString(DataFormats.Rtf, rtfResults); + _clipboard.AppendString(DataFormats.Html, htmlResults); + _clipboard.AppendString(DataFormats.CommaSeparatedValue, csvResults); + _clipboard.AppendString(DataFormats.UnicodeText, textResults); - }); - } + _clipboard.Flush(); } - private CommandBase _openTodoSettings; - public CommandBase OpenTodoSettings + public void ExecuteOpenTodoSettingsCommand(object obj) { - get + using (var window = _settingsFormFactory.Create(SettingsViews.TodoSettings)) { - if (_openTodoSettings != null) - { - return _openTodoSettings; - } - return _openTodoSettings = new DelegateCommand(LogManager.GetCurrentClassLogger(), _ => - { - using (var window = _settingsFormFactory.Create(SettingsViews.TodoSettings)) - { - window.ShowDialog(); - _settingsFormFactory.Release(window); - } - }); + window.ShowDialog(); + _settingsFormFactory.Release(window); } } - private readonly Lazy _navigateCommand; - public INavigateCommand NavigateCommand => _navigateCommand.Value; - private IEnumerable GetToDoMarkers(CommentNode comment) { var markers = _configService.LoadConfiguration().UserSettings.ToDoListSettings.ToDoMarkers; @@ -247,11 +230,6 @@ private IEnumerable GetToDoMarkers(CommentNode comment) .Select(marker => new ToDoItem(marker.Text, comment)); } - private IEnumerable GetItems() - { - return _state.AllComments.SelectMany(GetToDoMarkers); - } - public void Dispose() { if (_state != null) diff --git a/Rubberduck.Resources/Icons/Fugue/sticky-note-pin.png b/Rubberduck.Resources/Icons/Fugue/sticky-note-pin.png new file mode 100644 index 0000000000..adaf4f0b54 Binary files /dev/null and b/Rubberduck.Resources/Icons/Fugue/sticky-note-pin.png differ diff --git a/Rubberduck.Resources/Icons/Fugue/todo-completed.png b/Rubberduck.Resources/Icons/Fugue/todo-completed.png new file mode 100644 index 0000000000..97d7ab9d54 Binary files /dev/null and b/Rubberduck.Resources/Icons/Fugue/todo-completed.png differ diff --git a/RubberduckTests/TodoExplorer/TodoExplorerTests.cs b/RubberduckTests/TodoExplorer/TodoExplorerTests.cs index 48d13f694e..60018af2c9 100644 --- a/RubberduckTests/TodoExplorer/TodoExplorerTests.cs +++ b/RubberduckTests/TodoExplorer/TodoExplorerTests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using Moq; using System.Linq; @@ -7,6 +8,10 @@ using Rubberduck.UI.ToDoItems; using RubberduckTests.Mocks; using Rubberduck.Common; +using Rubberduck.Parsing.UIContext; +using Rubberduck.SettingsProvider; +using Rubberduck.ToDoItems; +using Rubberduck.UI.Command; using Rubberduck.VBEditor.SafeComWrappers; using Rubberduck.VBEditor.Utility; @@ -37,7 +42,7 @@ public void PicksUpComments() using (var state = parser.State) { var cs = GetConfigService(new[] { "TODO", "NOTE", "BUG" }); - var vm = new ToDoExplorerViewModel(state, cs, null, selectionService); + var vm = new ToDoExplorerViewModel(state, cs, null, selectionService, GetMockedUiDispatcher()); parser.Parse(new CancellationTokenSource()); if (state.Status >= ParserState.Error) @@ -45,7 +50,7 @@ public void PicksUpComments() Assert.Inconclusive("Parser Error"); } - var comments = vm.Items.Select(s => s.Type); + var comments = vm.Items.OfType().Select(s => s.Type); Assert.IsTrue(comments.SequenceEqual(new[] { "TODO", "NOTE", "BUG" })); } @@ -74,7 +79,7 @@ public void PicksUpComments_StrangeCasing() using (var state = parser.State) { var cs = GetConfigService(new[] { "TODO", "NOTE", "BUG" }); - var vm = new ToDoExplorerViewModel(state, cs, null, selectionService); + var vm = new ToDoExplorerViewModel(state, cs, null, selectionService, GetMockedUiDispatcher()); parser.Parse(new CancellationTokenSource()); if (state.Status >= ParserState.Error) @@ -82,7 +87,7 @@ public void PicksUpComments_StrangeCasing() Assert.Inconclusive("Parser Error"); } - var comments = vm.Items.Select(s => s.Type); + var comments = vm.Items.OfType().Select(s => s.Type); Assert.IsTrue(comments.SequenceEqual(new[] { "TODO", "NOTE", "BUG", "BUG" })); } @@ -111,7 +116,7 @@ public void PicksUpComments_SpecialCharacters() using (var state = parser.State) { var cs = GetConfigService(new[] { "TO-DO", "N@TE", "BUG " }); - var vm = new ToDoExplorerViewModel(state, cs, null, selectionService); + var vm = new ToDoExplorerViewModel(state, cs, null, selectionService, GetMockedUiDispatcher()); parser.Parse(new CancellationTokenSource()); if (state.Status >= ParserState.Error) @@ -119,7 +124,7 @@ public void PicksUpComments_SpecialCharacters() Assert.Inconclusive("Parser Error"); } - var comments = vm.Items.Select(s => s.Type); + var comments = vm.Items.OfType().Select(s => s.Type); Assert.IsTrue(comments.SequenceEqual(new[] { "TO-DO", "N@TE", "BUG " })); } @@ -147,7 +152,7 @@ public void AvoidsFalsePositiveComments() using (var state = parser.State) { var cs = GetConfigService(new[] { "TODO", "NOTE", "BUG" }); - var vm = new ToDoExplorerViewModel(state, cs, null, selectionService); + var vm = new ToDoExplorerViewModel(state, cs, null, selectionService, GetMockedUiDispatcher()); parser.Parse(new CancellationTokenSource()); if (state.Status >= ParserState.Error) @@ -155,7 +160,7 @@ public void AvoidsFalsePositiveComments() Assert.Inconclusive("Parser Error"); } - var comments = vm.Items.Select(s => s.Type); + var comments = vm.Items.OfType().Select(s => s.Type); Assert.IsTrue(comments.Count() == 0); } @@ -184,7 +189,10 @@ public void RemoveRemovesComment() using (var state = parser.State) { var cs = GetConfigService(new[] { "TODO", "NOTE", "BUG" }); - var vm = new ToDoExplorerViewModel(state, cs, null, selectionService); + var vm = new ToDoExplorerViewModel(state, cs, null, selectionService, GetMockedUiDispatcher()) + { + RefreshCommand = new ReparseCommand(vbe.Object, new Mock>().Object, state, null, null, null) + }; parser.Parse(new CancellationTokenSource()); if (state.Status >= ParserState.Error) @@ -192,12 +200,11 @@ public void RemoveRemovesComment() Assert.Inconclusive("Parser Error"); } - vm.SelectedItem = vm.Items.Single(); + vm.SelectedItem = vm.Items.OfType().Single(); vm.RemoveCommand.Execute(null); var module = project.Object.VBComponents[0].CodeModule; Assert.AreEqual(expected, module.Content()); - Assert.IsFalse(vm.Items.Any()); } } @@ -219,5 +226,12 @@ private Configuration GetTodoConfig(string[] markers) var userSettings = new UserSettings(null, null, null, todoSettings, null, null, null, null); return new Configuration(userSettings); } + + private IUiDispatcher GetMockedUiDispatcher() + { + var dispatcher = new Mock(); + dispatcher.Setup(m => m.Invoke(It.IsAny())).Callback((Action argument) => argument.Invoke()); + return dispatcher.Object; + } } }