diff --git a/CSharpCodeAnalyst/App.xaml.cs b/CSharpCodeAnalyst/App.xaml.cs index 8d60603..eccbc8a 100644 --- a/CSharpCodeAnalyst/App.xaml.cs +++ b/CSharpCodeAnalyst/App.xaml.cs @@ -6,6 +6,7 @@ using CSharpCodeAnalyst.CycleArea; using CSharpCodeAnalyst.Exploration; using CSharpCodeAnalyst.GraphArea; +using CSharpCodeAnalyst.InfoPanel; using CSharpCodeAnalyst.TreeArea; using CSharpCodeAnalyst.SearchArea; using Microsoft.Extensions.Configuration; @@ -38,6 +39,10 @@ protected override void OnStartup(StartupEventArgs e) IConfiguration configuration = builder.Build(); var settings = configuration.GetSection("ApplicationSettings").Get(); + if (settings is null) + { + settings = new ApplicationSettings(); + } var messaging = new MessageBus(); var explorer = new CodeGraphExplorer(); @@ -51,6 +56,9 @@ protected override void OnStartup(StartupEventArgs e) var treeViewModel = new TreeViewModel(messaging); var searchViewModel = new SearchViewModel(messaging); var cycleViewModel = new CycleSummaryViewModel(); + var infoPanelViewModel = new InfoPanelViewModel(settings); + + viewModel.InfoPanelViewModel = infoPanelViewModel; viewModel.GraphViewModel = graphViewModel; viewModel.TreeViewModel = treeViewModel; viewModel.SearchViewModel = searchViewModel; @@ -65,8 +73,8 @@ protected override void OnStartup(StartupEventArgs e) // Adding a node triggered in tree view, handled in graph view messaging.Subscribe(graphViewModel.HandleAddNodeToGraphRequest); - // Context-sensitive help triggered in the graph, handled in the main view model - messaging.Subscribe(viewModel.HandleUpdateQuickInfo); + // Context-sensitive help triggered in the graph, handled in the info panel + messaging.Subscribe(infoPanelViewModel.HandleUpdateQuickInfo); messaging.Subscribe(cycleViewModel.HandleCycleCalculationComplete); diff --git a/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj b/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj index f88b3a4..0d43b7f 100644 --- a/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj +++ b/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj @@ -9,6 +9,10 @@ lamp.ico + + + + PreserveNewest @@ -18,6 +22,7 @@ + diff --git a/CSharpCodeAnalyst/Constants.cs b/CSharpCodeAnalyst/Constants.cs index b5d30bc..262b4f8 100644 --- a/CSharpCodeAnalyst/Constants.cs +++ b/CSharpCodeAnalyst/Constants.cs @@ -1,7 +1,16 @@ -namespace CSharpCodeAnalyst; +using Microsoft.Msagl.Drawing; + +namespace CSharpCodeAnalyst; public class Constants { public const double TreeMinWidthCollapsed = 24; - public const double TreeMinWidthExpanded = 200; + public const double TreeMinWidthExpanded = 400; + + public static Color FlagColor = Color.Red; + public static int FlagLineWidth = 3; + public static double DefaultLineWidth = 1; + public static Color DefaultLineColor = Color.Black; + + public static int DoubleClickMilliseconds = 350; } \ No newline at end of file diff --git a/CSharpCodeAnalyst/GraphArea/CodeElementContextCommand.cs b/CSharpCodeAnalyst/GraphArea/CodeElementContextCommand.cs index 817420a..5cef72a 100644 --- a/CSharpCodeAnalyst/GraphArea/CodeElementContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/CodeElementContextCommand.cs @@ -1,4 +1,5 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; @@ -8,26 +9,31 @@ public class CodeElementContextCommand : ICodeElementContextCommand private readonly Func? _canExecute; private readonly CodeElementType? _type; - public CodeElementContextCommand(string label, CodeElementType type, Action action) + public CodeElementContextCommand(string label, CodeElementType type, Action action, ImageSource? icon = null) { _type = type; _action = action; Label = label; + Icon = icon; } /// /// Generic for all code elements /// public CodeElementContextCommand(string label, Action action, - Func? canExecute = null) + Func? canExecute = null, ImageSource? icon = null) { _type = null; _action = action; _canExecute = canExecute; Label = label; + Icon = icon; } + public bool IsVisible { get; set; } = true; public string Label { get; } + public ImageSource? Icon { get; } + public bool IsDoubleClickable { get; set; } public bool CanHandle(CodeElement element) { diff --git a/CSharpCodeAnalyst/GraphArea/GlobalContextCommand.cs b/CSharpCodeAnalyst/GraphArea/GlobalContextCommand.cs index c1412a4..3162efb 100644 --- a/CSharpCodeAnalyst/GraphArea/GlobalContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/GlobalContextCommand.cs @@ -1,4 +1,5 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; @@ -10,24 +11,27 @@ public class GlobalContextCommand : IGlobalContextCommand private readonly Action> _action; private readonly Func, bool>? _canExecute; - public GlobalContextCommand(string label, Action> action) + public GlobalContextCommand(string label, Action> action, ImageSource? icon = null) { _action = action; Label = label; + Icon = icon; } /// /// Generic for all code elements /// public GlobalContextCommand(string label, Action> action, - Func, bool>? canExecute = null) + Func, bool>? canExecute, ImageSource? icon = null) { _action = action; _canExecute = canExecute; Label = label; + Icon = icon; } public string Label { get; } + public ImageSource? Icon { get; } public void Invoke(List selectedElements) diff --git a/CSharpCodeAnalyst/GraphArea/GraphViewModel.cs b/CSharpCodeAnalyst/GraphArea/GraphViewModel.cs index 2ee6941..cbc037e 100644 --- a/CSharpCodeAnalyst/GraphArea/GraphViewModel.cs +++ b/CSharpCodeAnalyst/GraphArea/GraphViewModel.cs @@ -3,6 +3,8 @@ using System.IO; using System.Windows; using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; using CodeParser.Extensions; using Contracts.Graph; using CSharpCodeAnalyst.Common; @@ -19,18 +21,18 @@ internal class GraphViewModel : INotifyPropertyChanged { private readonly ICodeGraphExplorer _explorer; private readonly IPublisher _publisher; - private readonly ApplicationSettings? _settings; + private readonly ApplicationSettings _settings; private readonly LinkedList _undoStack = new(); private readonly int _undoStackSize = 10; private readonly IGraphViewer _viewer; private HighlightOption _selectedHighlightOption; private RenderOption _selectedRenderOption; - private bool _showFlatGraph; private bool _showDataFlow; + private bool _showFlatGraph; internal GraphViewModel(IGraphViewer viewer, ICodeGraphExplorer explorer, IPublisher publisher, - ApplicationSettings? settings) + ApplicationSettings settings) { _viewer = viewer; _explorer = explorer; @@ -69,18 +71,29 @@ internal GraphViewModel(IGraphViewer viewer, ICodeGraphExplorer explorer, IPubli DeleteSelectedWithChildren, CanHandleIfSelectedElements)); _viewer.AddGlobalContextMenuCommand(new GlobalContextCommand(Strings.SelectedAddParent, AddParents, CanHandleIfSelectedElements)); + _viewer.AddGlobalContextMenuCommand(new GlobalContextCommand(Strings.ClearAllFlags, ClearAllFlags)); // Static commands - _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.Expand, Expand, CanExpand)); - _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.Collapse, Collapse, CanCollapse)); + _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.Expand, Expand, CanExpand) + { + IsDoubleClickable = true, + IsVisible = false + }); + _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.Collapse, Collapse, CanCollapse) + { + IsDoubleClickable = true, + IsVisible = false + }); + _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.ToggleFlag, ToggleFlag, icon: LoadIcon("Resources/flag.png"))); _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.Delete, DeleteWithoutChildren)); _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.DeleteWithChildren, DeleteWithChildren)); _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.FindInTree, FindInTreeRequest)); _viewer.AddContextMenuCommand(new CodeElementContextCommand(Strings.AddParent, AddParent)); _viewer.AddContextMenuCommand(new SeparatorCommand()); + // Methods and properties HashSet elementTypes = [CodeElementType.Method, CodeElementType.Property]; foreach (var elementType in elementTypes) @@ -323,6 +336,36 @@ private void AddParents(List codeElements) AddToGraph(result.Elements, []); } + private void ToggleFlag(CodeElement codeElement) + { + _viewer.ToggleFlag(codeElement.Id); + } + + private void ClearAllFlags(List selectedElements) + { + _viewer.ClearAllFlags(); + } + + private static ImageSource? LoadIcon(string iconPath) + { + try + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri($"pack://application:,,,/{iconPath}"); + bitmap.DecodePixelWidth = 16; + bitmap.DecodePixelHeight = 16; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + return bitmap; + } + catch + { + return null; + } + } + private void FindInTreeRequest(CodeElement codeElement) { _publisher.Publish(new LocateInTreeRequest(codeElement.Id)); @@ -406,19 +449,19 @@ private void AddToGraph(IEnumerable originalCodeElements, IEnumerab bool addCollapsed = false) { PushUndo(); - + var elementsToAdd = originalCodeElements.ToList(); var relationshipsToAdd = relationships.ToList(); - + // Apply "Automatically add containing type" setting - if (_settings?.AutomaticallyAddContainingType == true) + if (_settings.AutomaticallyAddContainingType) { var elementIds = elementsToAdd.Select(e => e.Id).ToHashSet(); var result = _explorer.CompleteToContainingTypes(elementIds); elementsToAdd.AddRange(result.Elements); relationshipsToAdd.AddRange(result.Relationships); } - + _viewer.AddToGraph(elementsToAdd, relationshipsToAdd, addCollapsed); } @@ -546,11 +589,6 @@ public void ShowGlobalContextMenu() private bool ProceedWithLargeGraph(int numberOfElements) { - if (_settings is null) - { - return true; - } - // Meanwhile we collapse the graph. if (numberOfElements > _settings.WarningCodeElementLimit) { diff --git a/CSharpCodeAnalyst/GraphArea/GraphViewer.cs b/CSharpCodeAnalyst/GraphArea/GraphViewer.cs index 11f2ca3..4ee4490 100644 --- a/CSharpCodeAnalyst/GraphArea/GraphViewer.cs +++ b/CSharpCodeAnalyst/GraphArea/GraphViewer.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Windows.Controls; @@ -22,6 +23,7 @@ namespace CSharpCodeAnalyst.GraphArea; /// internal class GraphViewer : IGraphViewer, IGraphBinding, INotifyPropertyChanged { + private readonly Stopwatch _clickStopwatch = Stopwatch.StartNew(); private readonly List _edgeCommands = []; private readonly List _globalCommands = []; private readonly MsaglBuilder _msaglBuilder; @@ -37,11 +39,11 @@ internal class GraphViewer : IGraphViewer, IGraphBinding, INotifyPropertyChanged private CodeGraph _clonedCodeGraph = new(); private IQuickInfoFactory? _factory; + private bool _flow; private Microsoft.Msagl.WpfGraphControl.GraphViewer? _msaglViewer; private PresentationState _presentationState = new(); private RenderOption _renderOption = new DefaultRenderOptions(); private bool _showFlatGraph; - private bool _flow; /// /// Note: @@ -196,6 +198,19 @@ public void ShowGlobalContextMenu() } var menuItem = new MenuItem { Header = command.Label }; + + // Add icon if provided + if (command.Icon != null) + { + var iconImage = new Image + { + Width = 16, + Height = 16, + Source = command.Icon + }; + menuItem.Icon = iconImage; + } + menuItem.Click += (_, _) => command.Invoke(selectedElements); globalContextMenu.Items.Add(menuItem); } @@ -273,6 +288,26 @@ public bool IsCollapsed(string id) return _presentationState.IsCollapsed(id); } + public bool IsFlagged(string id) + { + return _presentationState.IsFlagged(id); + } + + public void ToggleFlag(string id) + { + var currentState = _presentationState.IsFlagged(id); + var newState = !currentState; + _presentationState.SetFlaggedState(id, newState); + RefreshFlagsWithoutLayout([id], newState); + } + + public void ClearAllFlags() + { + var ids = _presentationState.NodeIdToFlagged.Keys.ToList(); + _presentationState.ClearAllFlags(); + RefreshFlagsWithoutLayout(ids, false); + } + public void LoadSession(List codeElements, List relationships, PresentationState state) { if (_msaglViewer is null) @@ -301,6 +336,31 @@ public void LoadSession(CodeGraph newGraph, PresentationState? presentationState public event PropertyChangedEventHandler? PropertyChanged; + private void RefreshFlagsWithoutLayout(List ids, bool isFlagged) + { + foreach (var id in ids) + { + var node = _msaglViewer?.Graph.FindNode(id); + if (node is null) + { + // Unexpected. + RefreshGraph(); + break; + } + + if (isFlagged) + { + node.Attr.Color = Constants.FlagColor; + node.Attr.LineWidth = Constants.FlagLineWidth; + } + else + { + node.Attr.Color = Constants.DefaultLineColor; + node.Attr.LineWidth = Constants.DefaultLineWidth; + } + } + } + private bool IsBoundToPanel() { return _msaglViewer is not null; @@ -365,6 +425,8 @@ private void RefreshGraph() } } + + private void ObjectUnderMouseCursorChanged(object? sender, ObjectUnderMouseCursorChangedEventArgs e) { if (_clickedObject is null) @@ -404,6 +466,11 @@ bool IsCtrlPressed() return Keyboard.IsKeyDown(Key.LeftCtrl); } + if (TryEmulateDoubleClick(e)) + { + return; + } + if (e.LeftButtonIsPressed) { var obj = _msaglViewer?.ObjectUnderMouseCursor; @@ -463,6 +530,45 @@ bool IsCtrlPressed() } } + private bool TryEmulateDoubleClick(MsaglMouseEventArgs e) + { + if (e.LeftButtonIsPressed) + { + var elapsed = _clickStopwatch.ElapsedMilliseconds; + if (elapsed > 0 && elapsed < Constants.DoubleClickMilliseconds) + { + OnDoubleClick(e); + _clickStopwatch.Restart(); + return true; + } + + _clickStopwatch.Restart(); + } + + return false; + } + + private void OnDoubleClick(MsaglMouseEventArgs args) + { + // Execute double click action if any registered + if (_msaglViewer?.ObjectUnderMouseCursor is IViewerNode clickedObject) + { + var node = clickedObject.Node; + var element = GetCodeElementFromUserData(node); + if (element is not null) + { + foreach (var cmd in _nodeCommands) + { + if (cmd.IsDoubleClickable && cmd.CanHandle(element)) + { + cmd.Invoke(element); + return; + } + } + } + } + } + private void AddContextMenuEntries(List relationships, ContextMenu contextMenu) { if (relationships.Count == 0) @@ -472,9 +578,22 @@ private void AddContextMenuEntries(List relationships, ContextMenu foreach (var cmd in _edgeCommands) { - var menuItem = new MenuItem { Header = cmd.Label }; if (cmd.CanHandle(relationships)) { + var menuItem = new MenuItem { Header = cmd.Label }; + + // Add icon if provided + if (cmd.Icon != null) + { + var iconImage = new Image + { + Width = 16, + Height = 16, + Source = cmd.Icon + }; + menuItem.Icon = iconImage; + } + menuItem.Click += (_, _) => cmd.Invoke(relationships); contextMenu.Items.Add(menuItem); } @@ -528,12 +647,25 @@ private void AddToContextMenuEntries(CodeElement? element, ContextMenu contextMe continue; } - if (!cmd.CanHandle(element)) + if (!cmd.IsVisible || !cmd.CanHandle(element)) { continue; } var menuItem = new MenuItem { Header = cmd.Label }; + + // Add icon if provided + if (cmd.Icon != null) + { + var iconImage = new Image + { + Width = 16, + Height = 16, + Source = cmd.Icon + }; + menuItem.Icon = iconImage; + } + menuItem.Click += (_, _) => cmd.Invoke(element); contextMenu.Items.Add(menuItem); lastItemIsSeparator = false; diff --git a/CSharpCodeAnalyst/GraphArea/ICodeElementContextCommand.cs b/CSharpCodeAnalyst/GraphArea/ICodeElementContextCommand.cs index dd9f082..60a0b8f 100644 --- a/CSharpCodeAnalyst/GraphArea/ICodeElementContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/ICodeElementContextCommand.cs @@ -1,11 +1,14 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; public interface ICodeElementContextCommand { + bool IsVisible { get; set; } string Label { get; } - + ImageSource? Icon { get; } + bool IsDoubleClickable { get; set; } bool CanHandle(CodeElement item); void Invoke(CodeElement item); } \ No newline at end of file diff --git a/CSharpCodeAnalyst/GraphArea/IGlobalContextCommand.cs b/CSharpCodeAnalyst/GraphArea/IGlobalContextCommand.cs index 8dcca3b..0993f29 100644 --- a/CSharpCodeAnalyst/GraphArea/IGlobalContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/IGlobalContextCommand.cs @@ -1,10 +1,12 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; public interface IGlobalContextCommand { string Label { get; } + ImageSource? Icon { get; } bool CanHandle(List selectedElements); void Invoke(List selectedElements); diff --git a/CSharpCodeAnalyst/GraphArea/IGraphViewer.cs b/CSharpCodeAnalyst/GraphArea/IGraphViewer.cs index 85c573f..fb47745 100644 --- a/CSharpCodeAnalyst/GraphArea/IGraphViewer.cs +++ b/CSharpCodeAnalyst/GraphArea/IGraphViewer.cs @@ -61,5 +61,8 @@ internal interface IGraphViewer void LoadSession(CodeGraph newGraph, PresentationState? presentationState); void AddContextMenuCommand(IRelationshipContextCommand command); - + + bool IsFlagged(string id); + void ToggleFlag(string id); + void ClearAllFlags(); } \ No newline at end of file diff --git a/CSharpCodeAnalyst/GraphArea/IRelationshipContextCommand.cs b/CSharpCodeAnalyst/GraphArea/IRelationshipContextCommand.cs index bd84236..edde3ea 100644 --- a/CSharpCodeAnalyst/GraphArea/IRelationshipContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/IRelationshipContextCommand.cs @@ -1,10 +1,12 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; public interface IRelationshipContextCommand { string Label { get; } + ImageSource? Icon { get; } bool CanHandle(List relationships); void Invoke(List relationships); diff --git a/CSharpCodeAnalyst/GraphArea/MsaglBuilder.cs b/CSharpCodeAnalyst/GraphArea/MsaglBuilder.cs index 9a0e99f..5ef13a4 100644 --- a/CSharpCodeAnalyst/GraphArea/MsaglBuilder.cs +++ b/CSharpCodeAnalyst/GraphArea/MsaglBuilder.cs @@ -14,13 +14,13 @@ public Graph CreateGraph(CodeGraph codeGraph, PresentationState presentationStat { if (showFlatGraph) { - return CreateFlatGraph(codeGraph, showInformationFlow); + return CreateFlatGraph(codeGraph, presentationState, showInformationFlow); } return CreateHierarchicalGraph(codeGraph, presentationState, showInformationFlow); } - private Graph CreateFlatGraph(CodeGraph codeGraph, bool showInformationFlow) + private Graph CreateFlatGraph(CodeGraph codeGraph, PresentationState presentationState, bool showInformationFlow) { // Since we start with a fresh graph we don't need to check for existing nodes and edges. @@ -29,7 +29,7 @@ private Graph CreateFlatGraph(CodeGraph codeGraph, bool showInformationFlow) // Add nodes foreach (var codeElement in codeGraph.Nodes.Values) { - CreateNode(graph, codeElement); + CreateNode(graph, codeElement, presentationState); } // Add edges and hierarchy @@ -57,9 +57,9 @@ private Graph CreateHierarchicalGraph(CodeGraph codeGraph, PresentationState pre { var visibleGraph = GetVisibleGraph(codeGraph, presentationState); var graph = new Graph("graph"); - var subGraphs = CreateSubGraphs(codeGraph, visibleGraph); + var subGraphs = CreateSubGraphs(codeGraph, visibleGraph, presentationState); - AddNodesToHierarchicalGraph(graph, visibleGraph, codeGraph, subGraphs); + AddNodesToHierarchicalGraph(graph, visibleGraph, codeGraph, subGraphs, presentationState); AddEdgesToHierarchicalGraph(graph, codeGraph, visibleGraph, showInformationFlow); return graph; @@ -96,7 +96,7 @@ private void CollectVisibleNodes(CodeElement root, PresentationState state, Code private void AddNodesToHierarchicalGraph(Graph graph, CodeGraph visibleGraph, CodeGraph codeGraph, - Dictionary subGraphs) + Dictionary subGraphs, PresentationState presentationState) { // Add nodes and sub graphs. Each node that has children becomes a subgraph. foreach (var visibleNode in visibleGraph.Nodes.Values) @@ -112,7 +112,7 @@ private void AddNodesToHierarchicalGraph(Graph graph, CodeGraph visibleGraph, Co // We need to assign the node without visibility restrictions. // The collapse/expand context menu handler needs the children. - AddNodeToParent(graph, codeGraph.Nodes[visibleNode.Id], subGraphs); + AddNodeToParent(graph, codeGraph.Nodes[visibleNode.Id], subGraphs, presentationState); } } } @@ -130,9 +130,9 @@ private void AddSubgraphToParent(Graph graph, CodeElement visibleNode, Subgraph } } - private void AddNodeToParent(Graph graph, CodeElement node, Dictionary subGraphs) + private void AddNodeToParent(Graph graph, CodeElement node, Dictionary subGraphs, PresentationState presentationState) { - var newNode = CreateNode(graph, node); + var newNode = CreateNode(graph, node, presentationState); if (node.Parent != null) { subGraphs[node.Parent.Id].AddNode(newNode); @@ -184,16 +184,37 @@ private void AddEdgesToHierarchicalGraph(Graph graph, CodeGraph codeGraph, CodeG /// /// Pre-creates all sub-graphs /// - private Dictionary CreateSubGraphs(CodeGraph codeGraph, CodeGraph visibleGraph) + private Dictionary CreateSubGraphs(CodeGraph codeGraph, CodeGraph visibleGraph, PresentationState state) { return visibleGraph.Nodes.Values .Where(n => visibleGraph.Nodes[n.Id].Children.Any()) .ToDictionary(n => n.Id, n => new Subgraph(n.Id) { + LabelText = n.Name, UserData = codeGraph.Nodes[n.Id], - Attr = { FillColor = GetColor(n) } + Attr = CreateNodeAttr(n) + + }); + + + NodeAttr CreateNodeAttr(CodeElement element) + { + var attr = new NodeAttr + { + Id = element.Id, + FillColor = GetColor(element) + }; + + if (state.IsFlagged(element.Id)) + { + attr.LineWidth = Constants.FlagLineWidth; + attr.Color = Constants.FlagColor; + } + + return attr; + } } private string GetHighestVisibleParentOrSelf(string id, CodeGraph codeGraph, CodeGraph visibleGraph) @@ -216,7 +237,6 @@ private string GetHighestVisibleParentOrSelf(string id, CodeGraph codeGraph, Cod private void CreateEdgeForHierarchicalStructure(Graph graph, KeyValuePair<(string source, string target), List> mappedRelationships) { - // MSAGL does not allow two same edges with different labels to the same subgraph. // So I collapse them to a single one that carries all the user data. @@ -316,13 +336,20 @@ private static void CreateContainmentEdge(Graph graph, Relationship relationship edge.UserData = relationship; } - private static Node CreateNode(Graph graph, CodeElement codeElement) + private static Node CreateNode(Graph graph, CodeElement codeElement, PresentationState presentationState) { var node = graph.AddNode(codeElement.Id); node.Attr.FillColor = GetColor(codeElement); node.LabelText = codeElement.Name; node.UserData = codeElement; + // Apply flagged styling if the element is flagged + if (presentationState.IsFlagged(codeElement.Id)) + { + node.Attr.LineWidth = Constants.FlagLineWidth; + node.Attr.Color = Constants.FlagColor; + } + return node; } diff --git a/CSharpCodeAnalyst/GraphArea/PresentationState.cs b/CSharpCodeAnalyst/GraphArea/PresentationState.cs index 653c772..8e1e07c 100644 --- a/CSharpCodeAnalyst/GraphArea/PresentationState.cs +++ b/CSharpCodeAnalyst/GraphArea/PresentationState.cs @@ -1,14 +1,18 @@ -namespace CSharpCodeAnalyst.GraphArea; +using System.Text.Json.Serialization; + +namespace CSharpCodeAnalyst.GraphArea; public class PresentationState { - private readonly Dictionary _defaultState; - private readonly Dictionary _nodeIdToCollapsed; + private Dictionary _defaultState; + private Dictionary _nodeIdToCollapsed; + private Dictionary _nodeIdToFlagged; public PresentationState(Dictionary defaultState) { - _defaultState = defaultState.ToDictionary(p => p.Key, propa => propa.Value); + _defaultState = defaultState?.ToDictionary(p => p.Key, propa => propa.Value) ?? new Dictionary(); _nodeIdToCollapsed = _defaultState.ToDictionary(p => p.Key, p => p.Value); + _nodeIdToFlagged = new Dictionary(); } public PresentationState() @@ -16,6 +20,27 @@ public PresentationState() // Nothing is collapsed _defaultState = new Dictionary(); _nodeIdToCollapsed = new Dictionary(); + _nodeIdToFlagged = new Dictionary(); + } + + // Public properties for JSON serialization + [JsonPropertyName("defaultState")] public Dictionary DefaultState + { + get => _defaultState; + set => _defaultState = value ?? new Dictionary(); + } + + [JsonPropertyName("nodeIdToCollapsed")] + public Dictionary NodeIdToCollapsed + { + get => _nodeIdToCollapsed; + set => _nodeIdToCollapsed = value ?? new Dictionary(); + } + + [JsonPropertyName("nodeIdToFlagged")] public Dictionary NodeIdToFlagged + { + get => _nodeIdToFlagged; + set => _nodeIdToFlagged = value ?? new Dictionary(); } public PresentationState Clone() @@ -26,6 +51,11 @@ public PresentationState Clone() clone.SetCollapsedState(pair.Key, pair.Value); } + foreach (var pair in _nodeIdToFlagged) + { + clone.SetFlaggedState(pair.Key, pair.Value); + } + return clone; } @@ -41,11 +71,28 @@ public void SetCollapsedState(string id, bool isCollapsed) _nodeIdToCollapsed[id] = isCollapsed; } + public bool IsFlagged(string id) + { + _nodeIdToFlagged.TryGetValue(id, out var isFlagged); + return isFlagged; + } + + public void SetFlaggedState(string id, bool isFlagged) + { + _nodeIdToFlagged[id] = isFlagged; + } + + public void ClearAllFlags() + { + _nodeIdToFlagged.Clear(); + } + internal void RemoveStates(HashSet ids) { foreach (var id in ids) { _nodeIdToCollapsed.Remove(id); + _nodeIdToFlagged.Remove(id); _defaultState.Remove(id); } } diff --git a/CSharpCodeAnalyst/GraphArea/RelationshipContextCommand.cs b/CSharpCodeAnalyst/GraphArea/RelationshipContextCommand.cs index 39c7964..04617d1 100644 --- a/CSharpCodeAnalyst/GraphArea/RelationshipContextCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/RelationshipContextCommand.cs @@ -1,4 +1,5 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; @@ -8,26 +9,29 @@ public class RelationshipContextCommand : IRelationshipContextCommand private readonly Func, bool>? _canExecute; private readonly RelationshipType? _type; - public RelationshipContextCommand(string label, RelationshipType type, Action> action) + public RelationshipContextCommand(string label, RelationshipType type, Action> action, ImageSource? icon = null) { _type = type; _action = action; Label = label; + Icon = icon; } /// /// Generic for all code elements /// public RelationshipContextCommand(string label, Action> action, - Func, bool>? canExecute = null) + Func, bool>? canExecute = null, ImageSource? icon = null) { _type = null; _action = action; _canExecute = canExecute; Label = label; + Icon = icon; } public string Label { get; } + public ImageSource? Icon { get; } public bool CanHandle(List relationships) { diff --git a/CSharpCodeAnalyst/GraphArea/SeparatorCommand.cs b/CSharpCodeAnalyst/GraphArea/SeparatorCommand.cs index a617691..a50c873 100644 --- a/CSharpCodeAnalyst/GraphArea/SeparatorCommand.cs +++ b/CSharpCodeAnalyst/GraphArea/SeparatorCommand.cs @@ -1,14 +1,24 @@ -using Contracts.Graph; +using System.Windows.Media; +using Contracts.Graph; namespace CSharpCodeAnalyst.GraphArea; public class SeparatorCommand : ICodeElementContextCommand { + public bool IsVisible { get; set; } = true; + public string Label { get => string.Empty; } + public ImageSource? Icon + { + get => null; + } + + public bool IsDoubleClickable { get; set; } + public bool CanHandle(CodeElement item) { return true; diff --git a/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml b/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml new file mode 100644 index 0000000..e9e0413 --- /dev/null +++ b/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml.cs b/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml.cs new file mode 100644 index 0000000..1f0c08e --- /dev/null +++ b/CSharpCodeAnalyst/InfoPanel/InfoPanel.xaml.cs @@ -0,0 +1,34 @@ +using System.Windows; +using System.Windows.Controls; + +namespace CSharpCodeAnalyst.InfoPanel +{ + /// + /// Interaction logic for InfoPanel.xaml + /// + public partial class InfoPanel : UserControl + { + public static readonly DependencyProperty IsVisibleProperty = + DependencyProperty.Register("IsVisible", typeof(bool), typeof(InfoPanel), + new PropertyMetadata(true, OnIsVisibleChanged)); + + public bool IsVisible + { + get => (bool)GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + public InfoPanel() + { + InitializeComponent(); + } + + private static void OnIsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is InfoPanel panel) + { + panel.Visibility = (bool)e.NewValue ? Visibility.Visible : Visibility.Collapsed; + } + } + } +} \ No newline at end of file diff --git a/CSharpCodeAnalyst/InfoPanel/InfoPanelViewModel.cs b/CSharpCodeAnalyst/InfoPanel/InfoPanelViewModel.cs new file mode 100644 index 0000000..fb444dc --- /dev/null +++ b/CSharpCodeAnalyst/InfoPanel/InfoPanelViewModel.cs @@ -0,0 +1,131 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Input; +using Contracts.Graph; +using CSharpCodeAnalyst.Common; +using CSharpCodeAnalyst.Configuration; +using CSharpCodeAnalyst.Help; +using CSharpCodeAnalyst.Resources; +using Prism.Commands; + +namespace CSharpCodeAnalyst.InfoPanel; + +internal class InfoPanelViewModel : INotifyPropertyChanged +{ + private bool _hide; + private bool _isInfoPanelVisible; + + private List _quickInfo = QuickInfoFactory.NoInfoProviderRegistered; + + public InfoPanelViewModel(ApplicationSettings settings) + { + _isInfoPanelVisible = settings.DefaultShowQuickHelp; + OpenSourceLocationCommand = new DelegateCommand(OpenSourceLocation); + } + + public ICommand OpenSourceLocationCommand { get; } + + public bool IsInfoPanelVisible + { + get => _isInfoPanelVisible; + set + { + if (_isInfoPanelVisible == value) + { + return; + } + + _isInfoPanelVisible = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsInfoPanelVisibleEffective)); + } + } + + public bool IsInfoPanelVisibleEffective + { + get => IsInfoPanelVisible && !_hide; + } + + + public List QuickInfo + { + get => _quickInfo; + set + { + if (Equals(value, _quickInfo)) + { + return; + } + + _quickInfo = value; + OnPropertyChanged(); + } + } + + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OpenSourceLocation(SourceLocation? location) + { + if (location is null) + { + return; + } + + var process = new Process(); + var startInfo = new ProcessStartInfo + { + FileName = "\"C:\\Program Files\\Notepad++\\notepad++.exe\"", + Arguments = $"-n{location.Line} -c{location.Column} \"{location.File}\"", + UseShellExecute = false, + RedirectStandardOutput = false, + CreateNoWindow = true + }; + + try + { + process.StartInfo = startInfo; + process.Start(); + } + catch (Exception ex) + { + var message = string.Format(Strings.OperationFailed_Message, ex.Message); + MessageBox.Show(message, Strings.Error_Title, MessageBoxButton.OK, + MessageBoxImage.Error); + } + } + + public void HandleUpdateQuickInfo(QuickInfoUpdate quickInfoUpdate) + { + // May come from any view + if (IsInfoPanelVisible is false) + { + // This can be very slow if updated even the help is not visible. + return; + } + + QuickInfo = quickInfoUpdate.QuickInfo; + } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void Clear() + { + _quickInfo = QuickInfoFactory.NoInfoProviderRegistered; + OnPropertyChanged(nameof(IsInfoPanelVisibleEffective)); + } + + /// + /// Hide the info panel temporarily for example when the wong page is shown.. + /// + public void Hide(bool hide) + { + _hide = hide; + OnPropertyChanged(nameof(IsInfoPanelVisibleEffective)); + } +} \ No newline at end of file diff --git a/CSharpCodeAnalyst/MainViewModel.cs b/CSharpCodeAnalyst/MainViewModel.cs index 289a0f9..fd38d80 100644 --- a/CSharpCodeAnalyst/MainViewModel.cs +++ b/CSharpCodeAnalyst/MainViewModel.cs @@ -24,12 +24,13 @@ using CSharpCodeAnalyst.Gallery; using CSharpCodeAnalyst.GraphArea; using CSharpCodeAnalyst.Help; +using CSharpCodeAnalyst.Import; +using CSharpCodeAnalyst.InfoPanel; using CSharpCodeAnalyst.MetricArea; using CSharpCodeAnalyst.Project; using CSharpCodeAnalyst.Resources; -using CSharpCodeAnalyst.TreeArea; using CSharpCodeAnalyst.SearchArea; -using CSharpCodeAnalyst.Import; +using CSharpCodeAnalyst.TreeArea; using Microsoft.Win32; using Prism.Commands; @@ -50,7 +51,7 @@ internal class MainViewModel : INotifyPropertyChanged private bool _isCanvasHintsVisible = true; - private bool _isInfoPanelVisible; + private bool _isLeftPanelExpanded = true; @@ -62,22 +63,22 @@ internal class MainViewModel : INotifyPropertyChanged private ObservableCollection _metrics = []; private LegendDialog? _openedLegendDialog; + private SearchViewModel? _searchViewModel; + - private List _quickInfo = QuickInfoFactory.NoInfoProviderRegistered; private int _selectedTabIndex; private TreeViewModel? _treeViewModel; - private SearchViewModel? _searchViewModel; - internal MainViewModel(MessageBus messaging, ApplicationSettings? settings) + internal MainViewModel(MessageBus messaging, ApplicationSettings settings) { // Initialize settings - _applicationSettings = settings ?? new ApplicationSettings(); - + _applicationSettings = settings; + // Apply settings _projectExclusionFilters = new ProjectExclusionRegExCollection(); _maxDegreeOfParallelism = _applicationSettings.MaxDegreeOfParallelism; - _isInfoPanelVisible = _applicationSettings.DefaultShowQuickHelp; + _projectExclusionFilters.Initialize(_applicationSettings.DefaultProjectExcludeFilter, ";"); _messaging = messaging; @@ -98,7 +99,7 @@ internal MainViewModel(MessageBus messaging, ApplicationSettings? settings) OpenFilterDialogCommand = new DelegateCommand(OpenFilterDialog); OpenSettingsDialogCommand = new DelegateCommand(OpenSettingsDialog); ExportToPngCommand = new DelegateCommand(ExportToPng); - OpenSourceLocationCommand = new DelegateCommand(OpenSourceLocation); + CopyToExplorerGraphCommand = new DelegateCommand(CopyToExplorerGraph); _loadMessage = string.Empty; @@ -122,20 +123,6 @@ public CycleSummaryViewModel? CycleSummaryViewModel } } - public List QuickInfo - { - get => _quickInfo; - set - { - if (Equals(value, _quickInfo)) - { - return; - } - - _quickInfo = value; - OnPropertyChanged(nameof(QuickInfo)); - } - } public GraphViewModel? GraphViewModel { @@ -147,21 +134,6 @@ public GraphViewModel? GraphViewModel } } - public bool IsInfoPanelVisible - { - get => _isInfoPanelVisible && _selectedTabIndex == 0; - set - { - if (_isInfoPanelVisible == value) - { - return; - } - - _isInfoPanelVisible = value; - OnPropertyChanged(nameof(IsInfoPanelVisible)); - } - } - public bool IsLeftPanelExpanded { get => _isLeftPanelExpanded; @@ -208,7 +180,7 @@ public string LoadMessage public ICommand CopyToExplorerGraphCommand { get; set; } public ICommand FindCyclesCommand { get; } public ICommand ExportToDsiCommand { get; } - public ICommand OpenSourceLocationCommand { get; } + public ICommand SearchCommand { get; } public ICommand ExportToPngCommand { get; } public ICommand ShowLegendCommand { get; } @@ -247,7 +219,7 @@ public int SelectedTabIndex _selectedTabIndex = value; OnPropertyChanged(nameof(SelectedTabIndex)); - OnPropertyChanged(nameof(IsInfoPanelVisible)); + InfoPanelViewModel.Hide(_selectedTabIndex != 0); } } @@ -279,6 +251,8 @@ public ObservableCollection Metrics get => _metrics; } + public InfoPanelViewModel InfoPanelViewModel { get; set; } + public event PropertyChangedEventHandler? PropertyChanged; @@ -369,7 +343,7 @@ private void OpenSettingsDialog() private void ApplySettings() { // Settings must be reloaded - + // Save settings to configuration file SaveSettings(); } @@ -384,7 +358,7 @@ private void SaveSettings() catch (Exception ex) { // Log error or show message to user - MessageBox.Show($"{Strings.Settings_Save_Error} {ex.Message}", Strings.Error_Title, + MessageBox.Show($"{Strings.Settings_Save_Error} {ex.Message}", Strings.Error_Title, MessageBoxButton.OK, MessageBoxImage.Warning); } } @@ -411,50 +385,6 @@ private void Search() } } - - private void OpenSourceLocation(SourceLocation? location) - { - if (location is null) - { - return; - } - - var process = new Process(); - var startInfo = new ProcessStartInfo - { - FileName = "\"C:\\Program Files\\Notepad++\\notepad++.exe\"", - Arguments = $"-n{location.Line} -c{location.Column} \"{location.File}\"", - UseShellExecute = false, - RedirectStandardOutput = false, - CreateNoWindow = true - }; - - try - { - process.StartInfo = startInfo; - process.Start(); - } - catch (Exception ex) - { - var message = string.Format(Strings.OperationFailed_Message, ex.Message); - MessageBox.Show(message, Strings.Error_Title, MessageBoxButton.OK, - MessageBoxImage.Error); - } - } - - public void HandleUpdateQuickInfo(QuickInfoUpdate quickInfoUpdate) - { - // May come from any view - if (_applicationSettings.DefaultShowQuickHelp is false) - { - // This can be very slow if updated even the help is not visible. - return; - } - - QuickInfo = quickInfoUpdate.QuickInfo; - } - - /// /// Exports the whole project to dsi. /// @@ -612,6 +542,7 @@ private void LoadCodeGraph(CodeGraph codeGraph) SearchViewModel?.LoadCodeGraph(_codeGraph); GraphViewModel?.LoadCodeGraph(_codeGraph); CycleSummaryViewModel?.Clear(); + InfoPanelViewModel?.Clear(); // Default output: summary of graph var numberOfRelationships = codeGraph.GetAllRelationships().Count(); @@ -787,18 +718,19 @@ private void LoadProject() // Load settings - if (projectData.Settings.TryGetValue(nameof(IsInfoPanelVisible), out var isInfoPanelVisibleString)) + if (projectData.Settings.TryGetValue(nameof(InfoPanelViewModel.IsInfoPanelVisible), out var isInfoPanelVisibleString)) { - IsInfoPanelVisible = bool.Parse(isInfoPanelVisibleString); + InfoPanelViewModel.IsInfoPanelVisible = bool.Parse(isInfoPanelVisibleString); } if (GraphViewModel != null) - + { if (projectData.Settings.TryGetValue(nameof(GraphViewModel.ShowFlatGraph), out var showFlatGraph)) { GraphViewModel.ShowFlatGraph = bool.Parse(showFlatGraph); } + if (projectData.Settings.TryGetValue(nameof(GraphViewModel.ShowDataFlow), out var showFlow)) { GraphViewModel.ShowDataFlow = bool.Parse(showFlow); @@ -847,7 +779,7 @@ private void SaveProject() var projectData = new ProjectData(); projectData.SetCodeGraph(_codeGraph); projectData.SetGallery(_gallery ?? new Gallery.Gallery()); - projectData.Settings[nameof(IsInfoPanelVisible)] = IsInfoPanelVisible.ToString(); + projectData.Settings[nameof(InfoPanelViewModel.IsInfoPanelVisible)] = InfoPanelViewModel.IsInfoPanelVisible.ToString(); projectData.Settings[nameof(GraphViewModel.ShowFlatGraph)] = _graphViewModel.ShowFlatGraph.ToString(); projectData.Settings[nameof(GraphViewModel.ShowDataFlow)] = _graphViewModel.ShowDataFlow.ToString(); projectData.Settings[nameof(ProjectExclusionRegExCollection)] = _projectExclusionFilters.ToString(); diff --git a/CSharpCodeAnalyst/MainWindow.xaml b/CSharpCodeAnalyst/MainWindow.xaml index 5e0052b..6b6f951 100644 --- a/CSharpCodeAnalyst/MainWindow.xaml +++ b/CSharpCodeAnalyst/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="clr-namespace:CSharpCodeAnalyst.Converters" xmlns:metricArea="clr-namespace:CSharpCodeAnalyst.MetricArea" + xmlns:infoPanel="clr-namespace:CSharpCodeAnalyst.InfoPanel" mc:Ignorable="d" xmlns:resources="clr-namespace:CSharpCodeAnalyst.Resources" x:Name="RootWindow" @@ -19,7 +20,7 @@ - + @@ -180,7 +181,7 @@ + IsChecked="{Binding InfoPanelViewModel.IsInfoPanelVisible, Mode=TwoWay}"> @@ -278,7 +279,7 @@ - @@ -400,6 +401,16 @@ GridLinesVisibility="Horizontal" SelectionMode="Extended" Margin="5,0,5,0"> + + + + + + - @@ -577,84 +588,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/CSharpCodeAnalyst/MainWindow.xaml.cs b/CSharpCodeAnalyst/MainWindow.xaml.cs index e9e3850..0e15364 100644 --- a/CSharpCodeAnalyst/MainWindow.xaml.cs +++ b/CSharpCodeAnalyst/MainWindow.xaml.cs @@ -144,6 +144,8 @@ public void HandleLocateInTreeRequest(LocateInTreeRequest request) return; } + CodeStructureTab.SelectedIndex = 0; + // Causes TreeViewItem_Loaded treeViewModel.ExpandParents(request.Id); diff --git a/CSharpCodeAnalyst/Resources/Strings.Designer.cs b/CSharpCodeAnalyst/Resources/Strings.Designer.cs index 88d72fb..03aa0b8 100644 --- a/CSharpCodeAnalyst/Resources/Strings.Designer.cs +++ b/CSharpCodeAnalyst/Resources/Strings.Designer.cs @@ -78,6 +78,15 @@ public static string AddSelectedToGraph_Label { } } + /// + /// Looks up a localized string similar to Add selected to graph (collapsed). + /// + public static string AddSelectedToGraphCollapsed_Label { + get { + return ResourceManager.GetString("AddSelectedToGraphCollapsed_Label", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add to graph. /// @@ -141,6 +150,15 @@ public static string Clear_Tooltip { } } + /// + /// Looks up a localized string similar to Clear All Flags. + /// + public static string ClearAllFlags { + get { + return ResourceManager.GetString("ClearAllFlags", resourceCulture); + } + } + /// /// Looks up a localized string similar to Clear search. /// @@ -1349,6 +1367,15 @@ public static string Settings_Settings_AddContainingType_Tooltip { } } + /// + /// Looks up a localized string similar to Toggle Flag. + /// + public static string ToggleFlag { + get { + return ResourceManager.GetString("ToggleFlag", resourceCulture); + } + } + /// /// Looks up a localized string similar to Tools. /// diff --git a/CSharpCodeAnalyst/Resources/Strings.resx b/CSharpCodeAnalyst/Resources/Strings.resx index 590ce3d..c74b39d 100644 --- a/CSharpCodeAnalyst/Resources/Strings.resx +++ b/CSharpCodeAnalyst/Resources/Strings.resx @@ -333,6 +333,12 @@ Complete to containing types + + Toggle Flag + + + Clear All Flags + Deleting model elements will clear the code graph. Do you want to proceed? @@ -554,6 +560,9 @@ Add selected to graph + + Add selected to graph (collapsed) + Name diff --git a/CSharpCodeAnalyst/Resources/flag.png b/CSharpCodeAnalyst/Resources/flag.png new file mode 100644 index 0000000..0e338f2 Binary files /dev/null and b/CSharpCodeAnalyst/Resources/flag.png differ