From e631c6c0cbf6a86f52e35508eefe503ee8a97a7e Mon Sep 17 00:00:00 2001 From: Grzegorz Glogowski Date: Tue, 23 Jul 2019 16:02:41 +0200 Subject: [PATCH] Add Filtering based on obsolete metadata. Code refactoring --- Src/SwqlStudio/MainForm.Designer.cs | 12 +- Src/SwqlStudio/MainForm.cs | 8 ++ Src/SwqlStudio/Metadata/Entity.cs | 4 +- Src/SwqlStudio/Metadata/IObsoleteMetadata.cs | 8 ++ Src/SwqlStudio/Metadata/Property.cs | 5 +- .../Metadata/SwisMetaDataProvider.cs | 49 ++++--- Src/SwqlStudio/Metadata/Verb.cs | 5 +- .../ObjectExplorer/DocumentationBuilder.cs | 21 ++- .../Filtering/INodeFilterStrategy.cs | 10 ++ .../ObjectExplorer/Filtering/NodeFilter.cs | 77 +++++++++++ .../Filtering/ObsoleteNodeFilter.cs | 63 +++++++++ .../Filtering/SearchNodeFilter.cs | 130 ++++++++++++++++++ .../Filtering/VisibilityStatus.cs | 29 ++++ .../ObjectExplorer/ObjectExplorer.cs | 115 ++-------------- .../ObjectExplorer/StringBuilderExtensions.cs | 8 ++ .../ObjectExplorer/TreeNodeUtils.cs | 72 +--------- .../ObjectExplorer/TreeNodesBuilder.cs | 26 +++- .../Properties/Settings.Designer.cs | 12 ++ Src/SwqlStudio/Properties/Settings.settings | 3 + Src/SwqlStudio/QueriesDockPanel.cs | 5 + Src/SwqlStudio/SwqlStudio.csproj | 6 + Src/SwqlStudio/app.config | 9 +- 22 files changed, 470 insertions(+), 207 deletions(-) create mode 100644 Src/SwqlStudio/Metadata/IObsoleteMetadata.cs create mode 100644 Src/SwqlStudio/ObjectExplorer/Filtering/INodeFilterStrategy.cs create mode 100644 Src/SwqlStudio/ObjectExplorer/Filtering/NodeFilter.cs create mode 100644 Src/SwqlStudio/ObjectExplorer/Filtering/ObsoleteNodeFilter.cs create mode 100644 Src/SwqlStudio/ObjectExplorer/Filtering/SearchNodeFilter.cs create mode 100644 Src/SwqlStudio/ObjectExplorer/Filtering/VisibilityStatus.cs diff --git a/Src/SwqlStudio/MainForm.Designer.cs b/Src/SwqlStudio/MainForm.Designer.cs index d11de49a4..f8da1a67d 100644 --- a/Src/SwqlStudio/MainForm.Designer.cs +++ b/Src/SwqlStudio/MainForm.Designer.cs @@ -93,6 +93,7 @@ private void InitializeComponent() this.newFileToolButton = new System.Windows.Forms.ToolStripButton(); this.openFileButton = new System.Windows.Forms.ToolStripButton(); this.saveToolButton = new System.Windows.Forms.ToolStripButton(); + this.showObsoleteToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.menu.SuspendLayout(); this.mainToolbar.SuspendLayout(); @@ -398,7 +399,8 @@ private void InitializeComponent() this.groupEntityTreeToolStripMenuItem, this.enableAutocompleteToolStripMenuItem, this.discoverQueryParametersMenuItem, - this.promptToSaveOnCloseToolStripMenuItem}); + this.promptToSaveOnCloseToolStripMenuItem, + this.showObsoleteToolStripMenuItem}); this.preferencesToolStripMenuItem.Name = "preferencesToolStripMenuItem"; this.preferencesToolStripMenuItem.Size = new System.Drawing.Size(80, 20); this.preferencesToolStripMenuItem.Text = "&Preferences"; @@ -640,6 +642,13 @@ private void InitializeComponent() this.saveToolButton.ToolTipText = "Save (Ctrl+S)"; this.saveToolButton.Click += new System.EventHandler(this.menuFileSave_Click); // + // showObsoleteToolStripMenuItem + // + this.showObsoleteToolStripMenuItem.Name = "showObsoleteToolStripMenuItem"; + this.showObsoleteToolStripMenuItem.Size = new System.Drawing.Size(214, 22); + this.showObsoleteToolStripMenuItem.Text = "Show Obsolete"; + this.showObsoleteToolStripMenuItem.Click += new System.EventHandler(this.showObsoleteToolStripMenuItem_Click); + // // MainForm // this.AllowDrop = true; @@ -730,6 +739,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem findToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; private System.Windows.Forms.ToolStripMenuItem replaceToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem showObsoleteToolStripMenuItem; } } diff --git a/Src/SwqlStudio/MainForm.cs b/Src/SwqlStudio/MainForm.cs index 2fcdc3a41..1247463b5 100644 --- a/Src/SwqlStudio/MainForm.cs +++ b/Src/SwqlStudio/MainForm.cs @@ -418,6 +418,7 @@ private void preferencesToolStripMenuItem_DropDownOpening(object sender, EventAr { enableAutocompleteToolStripMenuItem.Checked = Settings.Default.AutocompleteEnabled; promptToSaveOnCloseToolStripMenuItem.Checked = Settings.Default.PromptToSaveOnClose; + showObsoleteToolStripMenuItem.Checked = Settings.Default.ShowObsolete; } private void searchInTreeHotKeyToolStripMenuItem_Click(object sender, EventArgs e) @@ -506,5 +507,12 @@ private void replaceToolStripMenuItem_Click(object sender, EventArgs e) activeTab.ReplaceDialog(); } + + private void showObsoleteToolStripMenuItem_Click(object sender, EventArgs e) + { + Settings.Default.ShowObsolete = !Settings.Default.ShowObsolete; + Settings.Default.Save(); + filesDock.RefreshObjectExplorerFilters(); + } } } diff --git a/Src/SwqlStudio/Metadata/Entity.cs b/Src/SwqlStudio/Metadata/Entity.cs index f6040a718..4d1c753bc 100644 --- a/Src/SwqlStudio/Metadata/Entity.cs +++ b/Src/SwqlStudio/Metadata/Entity.cs @@ -2,7 +2,7 @@ namespace SwqlStudio.Metadata { - public class Entity + public class Entity : IObsoleteMetadata { private readonly List properties = new List(); private readonly List verbs = new List(); @@ -12,6 +12,8 @@ public class Entity public string BaseType { get; set; } public bool IsIndication { get; set; } public bool IsAbstract { get; set; } + public bool IsObsolete { get; set; } + public string ObsolescenceReason { get; set; } public bool CanCreate { get; set; } public bool CanDelete { get; set; } diff --git a/Src/SwqlStudio/Metadata/IObsoleteMetadata.cs b/Src/SwqlStudio/Metadata/IObsoleteMetadata.cs new file mode 100644 index 000000000..36dc6ae3e --- /dev/null +++ b/Src/SwqlStudio/Metadata/IObsoleteMetadata.cs @@ -0,0 +1,8 @@ +namespace SwqlStudio.Metadata +{ + public interface IObsoleteMetadata + { + bool IsObsolete { get; set; } + string ObsolescenceReason { get; set; } + } +} diff --git a/Src/SwqlStudio/Metadata/Property.cs b/Src/SwqlStudio/Metadata/Property.cs index 2c9d7d7eb..b70f0494e 100644 --- a/Src/SwqlStudio/Metadata/Property.cs +++ b/Src/SwqlStudio/Metadata/Property.cs @@ -1,7 +1,7 @@  namespace SwqlStudio.Metadata { - public class Property : ITypedMetadata + public class Property : ITypedMetadata, IObsoleteMetadata { public string Name { get; set; } public string Type { get; set; } @@ -9,5 +9,8 @@ public class Property : ITypedMetadata public bool IsInherited { get; set; } public bool IsKey { get; set; } public string Summary { get; set; } + + public bool IsObsolete { get; set; } + public string ObsolescenceReason { get; set; } } } diff --git a/Src/SwqlStudio/Metadata/SwisMetaDataProvider.cs b/Src/SwqlStudio/Metadata/SwisMetaDataProvider.cs index c3eae838f..fbb41321b 100644 --- a/Src/SwqlStudio/Metadata/SwisMetaDataProvider.cs +++ b/Src/SwqlStudio/Metadata/SwisMetaDataProvider.cs @@ -10,7 +10,7 @@ class SwisMetaDataProvider : IMetadataProvider private readonly ConnectionInfo info; private Dictionary entities = new Dictionary(); - + public SwisMetaDataProvider(ConnectionInfo info) { this.info = info; @@ -19,22 +19,36 @@ public SwisMetaDataProvider(ConnectionInfo info) } private readonly Lazy _capabilities; + private Capability Capabilities => _capabilities.Value; - public void Refresh() + private readonly IEnumerable _metadataDefaultAttributes = new[] { + "Entity.FullName", "Entity.Namespace", "Entity.BaseType", + "(Entity.Type ISA 'System.Indication') AS IsIndication", + "Entity.Properties.Name", "Entity.Properties.Type", "Entity.Properties.IsNavigable", + "Entity.Properties.IsInherited", "Entity.Properties.IsKey", "Entity.Verbs.EntityName", "Entity.Verbs.Name", + "Entity.IsAbstract" + }; + + private IEnumerable AccessControlMetadataAttributes => Capabilities.HasFlag(Capability.AccessControl) + ? new[] {"Entity.CanCreate", "Entity.CanDelete", "Entity.CanInvoke", "Entity.CanRead", "Entity.CanUpdate"} + : new string[0]; + + private IEnumerable DocumentationMetadataAttributes => Capabilities.HasFlag(Capability.Documentation) + ? new[] { "Entity.Summary", "Entity.Properties.Summary", "Entity.Verbs.Summary" } + : new string[0]; - const string queryTemplate = - @"SELECT Entity.FullName, Entity.Namespace, Entity.BaseType, (Entity.Type ISA 'System.Indication') AS IsIndication, - Entity.Properties.Name, Entity.Properties.Type, Entity.Properties.IsNavigable, Entity.Properties.IsInherited, Entity.Properties.IsKey, - Entity.Verbs.EntityName, Entity.Verbs.Name, Entity.IsAbstract{0}{1} -FROM Metadata.Entity"; - const string crudFragment = - ", Entity.CanCreate, Entity.CanDelete, Entity.CanInvoke, Entity.CanRead, Entity.CanUpdate"; - const string docFragment = ", Entity.Summary, Entity.Properties.Summary, Entity.Verbs.Summary"; + private IEnumerable ObsoleteMetadataAttributes => Capabilities.HasFlag(Capability.Obsolete) + ? new[] { "Entity.IsObsolete", "Entity.ObsolescenceReason", "Entity.Properties.IsObsolete", "Entity.Properties.ObsolescenceReason", + "Entity.Verbs.IsObsolete", "Entity.Verbs.ObsolescenceReason" } + : new string[0]; - var capabilities = _capabilities.Value; - string query = string.Format(queryTemplate, capabilities.HasFlag(Capability.AccessControl) ? crudFragment : string.Empty, - capabilities.HasFlag(Capability.Documentation) ? docFragment : string.Empty); + private IEnumerable MetadataAttributes => _metadataDefaultAttributes + .Concat(AccessControlMetadataAttributes).Concat(DocumentationMetadataAttributes).Concat(ObsoleteMetadataAttributes); + + public void Refresh() + { + string query = $"SELECT {string.Join(",", MetadataAttributes)} FROM Metadata.Entity"; entities = info.Query(query).ToDictionary(entity => entity.FullName); @@ -53,22 +67,25 @@ public enum Capability None = 0, AccessControl = 1, Documentation = 2, + Obsolete = 4, } public Capability GetCapabilities() { const string query = @"SELECT Name FROM Metadata.Property -WHERE EntityName='Metadata.Entity' AND Name IN ('CanCreate', 'Summary')"; +WHERE EntityName='Metadata.Entity' AND Name IN ('CanCreate', 'Summary', 'IsObsolete')"; Capability cap = Capability.None; DataTable dt = info.Query(query); foreach (DataRow row in dt.Rows) { - if ((string)row["Name"] == "CanCreate") + if ((string) row["Name"] == "CanCreate") cap |= Capability.AccessControl; - else if ((string)row["Name"] == "Summary") + else if ((string) row["Name"] == "Summary") cap |= Capability.Documentation; + else if ((string) row["Name"] == "IsObsolete") + cap |= Capability.Obsolete; } return cap; diff --git a/Src/SwqlStudio/Metadata/Verb.cs b/Src/SwqlStudio/Metadata/Verb.cs index 71f487f32..8be6483f0 100644 --- a/Src/SwqlStudio/Metadata/Verb.cs +++ b/Src/SwqlStudio/Metadata/Verb.cs @@ -2,7 +2,7 @@ namespace SwqlStudio.Metadata { - public class Verb + public class Verb : IObsoleteMetadata { private readonly List _arguments = new List(); @@ -10,6 +10,9 @@ public class Verb public string EntityName { get; set; } public string Summary { get; set; } + public bool IsObsolete { get; set; } + public string ObsolescenceReason { get; set; } + public List Arguments { get { return _arguments;} diff --git a/Src/SwqlStudio/ObjectExplorer/DocumentationBuilder.cs b/Src/SwqlStudio/ObjectExplorer/DocumentationBuilder.cs index b45efaa8c..5ccd42f15 100644 --- a/Src/SwqlStudio/ObjectExplorer/DocumentationBuilder.cs +++ b/Src/SwqlStudio/ObjectExplorer/DocumentationBuilder.cs @@ -47,6 +47,7 @@ private static Documentation VerbDocumentation(Verb verb) { var builder = new StringBuilder(); builder.AppendName(verb.Name); + builder.AppendObsoleteSection(verb); builder.AppendSummaryParagraph(verb.Summary); var docs = builder.ToString(); return new Documentation("Verb", docs); @@ -58,6 +59,7 @@ private static Documentation EntityDocumentation(IMetadataProvider provider, Ent builder.AppendName(entity.FullName); builder.Append($"Base type: {entity.BaseType}\r\n"); builder.AppendAccessControl(provider.ConnectionInfo, entity); + builder.AppendObsoleteSection(entity); builder.AppendSummaryParagraph(entity.Summary); var docs = builder.ToString(); return new Documentation("Entity", docs); @@ -75,8 +77,10 @@ private static Documentation NamespaceDocumentation(string nameSpace, int childr private static Documentation PropertyDocumentation(Property property) { - var docs = MetadataDocumentation(property); - return new Documentation("Property", docs); + var builder = new StringBuilder(); + builder.Append(MetadataDocumentation(property)); + builder.AppendObsoleteSection(property); + return new Documentation("Property", builder.ToString()); } private static Documentation ProviderDocumentation(IMetadataProvider provider) @@ -100,11 +104,21 @@ public static string ToToolTip(ITypedMetadata metadata) builder.AppendSummary(metadata.Summary); return builder.ToString(); } - + + public static string ToToolTip(ITypedMetadata metadata, IObsoleteMetadata obsoleteMetadata) + { + var builder = new StringBuilder(); + builder.AppendSummary(metadata.Summary); + builder.AppendObsoleteSection(obsoleteMetadata); + return builder.ToString(); + } + + public static string ToToolTip(ConnectionInfo connection, Entity entity) { var builder = new StringBuilder(); builder.AppendSummary(entity.Summary); + builder.AppendObsoleteSection(entity); return builder.ToString(); } @@ -112,6 +126,7 @@ public static string ToToolTip(Verb verb) { var builder = new StringBuilder(); builder.AppendSummary(verb.Summary); + builder.AppendObsoleteSection(verb); return builder.ToString(); } diff --git a/Src/SwqlStudio/ObjectExplorer/Filtering/INodeFilterStrategy.cs b/Src/SwqlStudio/ObjectExplorer/Filtering/INodeFilterStrategy.cs new file mode 100644 index 000000000..7e89c8156 --- /dev/null +++ b/Src/SwqlStudio/ObjectExplorer/Filtering/INodeFilterStrategy.cs @@ -0,0 +1,10 @@ +using System.Windows.Forms; + +namespace SwqlStudio.Filtering +{ + internal interface INodeFilterStrategy + { + void Initialize(TreeNode node); + VisibilityStatus GetVisibility(TreeNode node); + } +} diff --git a/Src/SwqlStudio/ObjectExplorer/Filtering/NodeFilter.cs b/Src/SwqlStudio/ObjectExplorer/Filtering/NodeFilter.cs new file mode 100644 index 000000000..017a1cc56 --- /dev/null +++ b/Src/SwqlStudio/ObjectExplorer/Filtering/NodeFilter.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using SwqlStudio.Metadata; +using SwqlStudio.Properties; + +namespace SwqlStudio.Filtering +{ + internal class NodeFilter + { + private readonly List _filters = new List(); + private const int _limitOfExpandedNodes = 5000; + private int _visibleNodesCount; + + public NodeFilter(TreeView treeData, string filter) + { + InitializeFilters(treeData, filter); + } + + private void InitializeFilters(TreeView treeData, string filter) + { + if (!Settings.Default.ShowObsolete) + { + _filters.Add(new ObsoleteNodeFilter()); + } + if (filter != null) + { + _filters.Add(new SearchNodeFilter(filter)); + } + + TreeNodeUtils.IterateNodes(treeData, node => _filters.ForEach(strategy => strategy.Initialize(node))); + + TreeNodeUtils.IterateNodes(treeData, node => + { + if (FilterAction(node)) _visibleNodesCount++; + }); + } + + private VisibilityStatus NodeVisibility(TreeNode node) => _filters.Aggregate(VisibilityStatus.None, (current, filter) => current | filter.GetVisibility(node)); + + internal bool FilterAction(TreeNode node) => !NodeVisibility(node).HasFlag(VisibilityStatus.NotVisible); + + internal void UpdateAction(TreeNode data, TreeNode display) + { + var nodeVisibility = NodeVisibility(data); + + if (nodeVisibility.HasFlag(VisibilityStatus.ChildVisible)) + // this one is not directly visible, gray it out + display.ForeColor = Color.LightBlue; + + else if (nodeVisibility.HasFlag(VisibilityStatus.ParentVisible)) + // this one is not directly visible, gray it out + display.ForeColor = Color.DarkGray; + + // this one is visible directly, ensure it is visible (parents are expanded). + // it's children are visible as well, but may be not expanded + else if (nodeVisibility.HasFlag(VisibilityStatus.Visible)) + { + //expand parents of visible node + if (_visibleNodesCount < _limitOfExpandedNodes) + { + display.EnsureVisible(); + } + //If too many objects are visible, treeView performance drops dramatically. + //In case of many object only entity level is expanded + else + { + if (data.Tag is Entity) + { + display.EnsureVisible(); + } + } + } + } + } +} diff --git a/Src/SwqlStudio/ObjectExplorer/Filtering/ObsoleteNodeFilter.cs b/Src/SwqlStudio/ObjectExplorer/Filtering/ObsoleteNodeFilter.cs new file mode 100644 index 000000000..efc168704 --- /dev/null +++ b/Src/SwqlStudio/ObjectExplorer/Filtering/ObsoleteNodeFilter.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using SwqlStudio.Metadata; + +namespace SwqlStudio.Filtering +{ + /// + /// Filtering strategy marks as not visible (VisibilityStatus.None) nodes that does not have visible children nodes. + /// All other nodes are marked with no special visibility status (VisibilityStatus.None). + /// + internal class ObsoleteNodeFilter : INodeFilterStrategy + { + private readonly Dictionary _visibility = new Dictionary(TreeNodeUtils.ReferenceEqualityComparer.Default); + + public void Initialize(TreeNode node) + { + InitializeObsoleteVisibility(node); + } + + public VisibilityStatus GetVisibility(TreeNode node) => _visibility.ContainsKey(node) ? _visibility[node] : VisibilityStatus.None; + + private void InitializeObsoleteVisibility(TreeNode node) + { + if (!_visibility.ContainsKey(node)) + { + _visibility[node] = IsObsolete(node) ? VisibilityStatus.NotVisible : VisibilityStatus.None; + } + } + + public bool IsObsolete(TreeNode node) + { + var tag = node?.Tag as IObsoleteMetadata; + + return tag != null && tag.IsObsolete && !HasVisibleChildEntity(node); + } + + private bool HasVisibleChildEntity(TreeNode node) + { + if (node.Nodes.Count == 0) + { + return false; + } + + if (_visibility.ContainsKey(node)) + { + return _visibility[node] == VisibilityStatus.None; + } + + foreach (TreeNode child in node.Nodes) + { + var tag = child?.Tag as Entity; + if (tag != null && (!tag.IsObsolete || HasVisibleChildEntity(child))) + { + _visibility.Add(node, VisibilityStatus.None); + return true; + } + } + + _visibility.Add(node, VisibilityStatus.NotVisible); + return false; + } + } +} diff --git a/Src/SwqlStudio/ObjectExplorer/Filtering/SearchNodeFilter.cs b/Src/SwqlStudio/ObjectExplorer/Filtering/SearchNodeFilter.cs new file mode 100644 index 000000000..7f9852e65 --- /dev/null +++ b/Src/SwqlStudio/ObjectExplorer/Filtering/SearchNodeFilter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; + +namespace SwqlStudio.Filtering +{ + /// + /// Used in filtering of the nodes in Object explorer. It filters based on provided keyword (filter) + /// Everything that match pattern is marked as Visible and all parents of visible nodes are marked as ChildVisible. + /// Everything that does not match is considered NotVisible or ParentVisible if there is explicitly visible parent. + /// + internal class SearchNodeFilter : INodeFilterStrategy + { + private readonly string _filter; + + private readonly Dictionary _visibility = new Dictionary(TreeNodeUtils.ReferenceEqualityComparer.Default); + + public SearchNodeFilter(string filter) + { + _filter = filter; + } + + public void Initialize(TreeNode node) + { + if (PassesFilter(node)) + SetVisible(node); + } + + private void SetVisible(TreeNode node) + { + _visibility[node] = VisibilityStatus.Visible; + for (var parent = node.Parent; parent != null; parent = parent.Parent) + { + _visibility.TryGetValue(parent, out var parentVisibility); + if (parentVisibility != VisibilityStatus.Visible)//for deprecated we care for entity only + parentVisibility = VisibilityStatus.ChildVisible; + + _visibility[parent] = parentVisibility; + } + } + + public VisibilityStatus GetVisibility(TreeNode node) + { + if (!_visibility.TryGetValue(node, out var rv)) + { + rv = VisibilityStatus.NotVisible; + // check if some of the parents is not directly visible (visible, not childvisible) + for (var parent = node.Parent; parent != null; parent = parent.Parent) + { + if (_visibility.TryGetValue(parent, out var parentVisibility)) + { + if (parentVisibility == VisibilityStatus.Visible) + rv = VisibilityStatus.ParentVisible; + else + break; + } + } + } + + return rv; + } + + private bool PassesFilter(TreeNode node) + { + var nodeText = node.Text.Split('(')[0]; // trim everything after first (, we do not want to use it in search + + if (_filter == null || nodeText.IndexOf(_filter, StringComparison.OrdinalIgnoreCase) >= 0) + return true; + + else // behave in 'special' way for dots, and capitals, the same way as resharper does + // let's Met.Ent match also METadata...ENTity + // also Met.EntNa should match Metadata.EntityName + { + var filter = SplitTextByCapsAndDot(_filter); + var text = SplitTextByCapsAndDot(nodeText); + + int textPivot = 0; + + for (int filterPivot = 0; filterPivot < filter.Count; filterPivot++) + { + for (; textPivot <= text.Count; textPivot++) + { + if (textPivot == text.Count) + return false; + + if (text[textPivot].StartsWith(filter[filterPivot], StringComparison.OrdinalIgnoreCase)) + break; + } + textPivot++; + } + + return true; + } + } + + private static List SplitTextByCapsAndDot(string text) + { + var rv = new List(); + + var buf = new StringBuilder(); + + void FlushBuffer() + { + if (buf.Length > 0) + rv.Add(buf.ToString()); + + buf.Clear(); + } + + foreach (var t in text) + { + if (t == '.') + { + FlushBuffer(); + } + else + { + if (char.IsUpper(t)) + FlushBuffer(); + + buf.Append(t); + } + } + + FlushBuffer(); + return rv; + } + } +} \ No newline at end of file diff --git a/Src/SwqlStudio/ObjectExplorer/Filtering/VisibilityStatus.cs b/Src/SwqlStudio/ObjectExplorer/Filtering/VisibilityStatus.cs new file mode 100644 index 000000000..7e6220093 --- /dev/null +++ b/Src/SwqlStudio/ObjectExplorer/Filtering/VisibilityStatus.cs @@ -0,0 +1,29 @@ +using System; + +namespace SwqlStudio.Filtering +{ + [Flags] + internal enum VisibilityStatus + { + /// + /// Visibility Status is not set for this node + /// + None = 0, + /// + /// This node is not visible + /// + NotVisible = 1, + /// + /// Some children of this node passed filter + /// + ChildVisible = 2, + /// + /// This node passed filter + /// + Visible = 4, + /// + /// This node's parent passed filter + /// + ParentVisible = 8 + } +} diff --git a/Src/SwqlStudio/ObjectExplorer/ObjectExplorer.cs b/Src/SwqlStudio/ObjectExplorer/ObjectExplorer.cs index 9b66e815e..488c60aec 100644 --- a/Src/SwqlStudio/ObjectExplorer/ObjectExplorer.cs +++ b/Src/SwqlStudio/ObjectExplorer/ObjectExplorer.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Windows.Forms; using SolarWinds.Logging; +using SwqlStudio.Filtering; using SwqlStudio.Metadata; using SwqlStudio.Utils; using TextBox = System.Windows.Forms.TextBox; @@ -37,9 +38,8 @@ internal class ObjectExplorer : Control private ImageList objectExplorerImageList; private System.ComponentModel.IContainer components; private TreeNode _dragNode; - private readonly TreeNodesBuilder treeNodesBuilder = new TreeNodesBuilder(); + private readonly TreeNodesBuilder treeNodesBuilder = new TreeNodesBuilder(DefaultFont); public event TreeViewEventHandler SelectionChanged; - public ITabsFactory TabsFactory { get; set; } public ObjectExplorer() @@ -134,7 +134,7 @@ private void SetFilter(string filter) UpdateDrawnNodes(); } } - + // since we have data in _treeData, but are displaying _tree, // we need to copy data from treedata to trees (according to applied filter) private void UpdateDrawnNodes() @@ -143,45 +143,13 @@ private void UpdateDrawnNodes() _treeIsUnderUpdate = true; _tree.BeginUpdate(); - _tree.Nodes.Clear(); try { - // quick path - if (_filter == null) - { - _treeBindings = TreeNodeUtils.CopyTree(_treeData, _tree, null, null); - } - else - { - var visibility = new TreeNodeUtils.NodeVisibility(); - TreeNodeUtils.IterateNodes(_treeData, node => - { - if (PassesFilter(node)) - visibility.SetVisible(node); - }); + var filters = new NodeFilter(_treeData, _filter); - _treeBindings = TreeNodeUtils.CopyTree(_treeData, _tree, - node => visibility.GetVisibility(node) != - TreeNodeUtils.NodeVisibility.VisibilityStatus.NotVisible, (data, display) => - { - var nodeVisibility = visibility.GetVisibility(data); - if (nodeVisibility == TreeNodeUtils.NodeVisibility.VisibilityStatus.ChildVisible) - // this one is not directly visible, gray it out - display.ForeColor = Color.LightBlue; - - if (nodeVisibility == TreeNodeUtils.NodeVisibility.VisibilityStatus.ParentVisible) - // this one is not directly visible, gray it out - display.ForeColor = Color.DarkGray; - - // this one is visible directly, ensure it is visible (parents are expanded). - // it's children are visible as well, but may be not expanded - if (nodeVisibility == TreeNodeUtils.NodeVisibility.VisibilityStatus.Visible) - display.EnsureVisible(); - }); + _treeBindings = TreeNodeUtils.CopyTree(_treeData, _tree, filters.FilterAction, filters.UpdateAction); - - _tree.SetHorizontalScroll(0); - } + _tree.SetHorizontalScroll(0); UpdateDrawnNodesSelection(); } @@ -192,73 +160,6 @@ private void UpdateDrawnNodes() } } - private bool PassesFilter(TreeNode node) - { - var nodeText = node.Text.Split('(')[0]; // trim everything after first (, we do not want to use it in search - - if (nodeText.IndexOf(_filter, StringComparison.OrdinalIgnoreCase) >= 0) - return true; - - else // behave in 'special' way for dots, and capitals, the same way as resharper does - // let's Met.Ent match also METadata...ENTity - // also Met.EntNa should match Metadata.EntityName - { - var filter = SplitTextByCapsAndDot(_filter); - var text = SplitTextByCapsAndDot(nodeText); - - int textPivot = 0; - - for (int filterPivot = 0; filterPivot < filter.Count; filterPivot++) - { - for (; textPivot <= text.Count; textPivot++) - { - if (textPivot == text.Count) - return false; - - if (text[textPivot].StartsWith(filter[filterPivot], StringComparison.OrdinalIgnoreCase)) - break; - } - textPivot++; - } - - return true; - } - } - - private static List SplitTextByCapsAndDot(string text) - { - var rv = new List(); - - var buf = new StringBuilder(); - - void FlushBuffer() - { - if (buf.Length > 0) - rv.Add(buf.ToString()); - - buf.Clear(); - } - - foreach (var t in text) - { - if (t == '.') - { - FlushBuffer(); - } - else - { - if (char.IsUpper(t)) - FlushBuffer(); - - buf.Append(t); - } - } - - FlushBuffer(); - return rv; - } - - private void UpdateDrawnNodesSelection() { _tree.SelectedNode = _treeBindings.FindDisplayNode(_treeData.SelectedNode); @@ -271,7 +172,7 @@ private bool AllowExpandCollapse Point p = _tree.PointToClient(MousePosition); var hitTest = _tree.HitTest(p); - return hitTest != null && hitTest.Location != TreeViewHitTestLocations.Label; + return hitTest != null && hitTest.Location != TreeViewHitTestLocations.Label || _treeIsUnderUpdate; } } @@ -355,6 +256,8 @@ public void RefreshAllServers() UpdateDrawnNodes(); } + public void RefreshFilters() => UpdateDrawnNodes(); + internal void RefreshServer(ConnectionInfo connection) { TreeNodeWithConnectionInfo serverNode = FindServerNodeByConnection(connection); diff --git a/Src/SwqlStudio/ObjectExplorer/StringBuilderExtensions.cs b/Src/SwqlStudio/ObjectExplorer/StringBuilderExtensions.cs index aa23abfe8..e0ed8990a 100644 --- a/Src/SwqlStudio/ObjectExplorer/StringBuilderExtensions.cs +++ b/Src/SwqlStudio/ObjectExplorer/StringBuilderExtensions.cs @@ -45,5 +45,13 @@ public static void AppendAccessControl(this StringBuilder builder, ConnectionInf builder.Append($"Can Delete: {entity.CanDelete}\r\n"); } } + + public static void AppendObsoleteSection(this StringBuilder builder, IObsoleteMetadata entity) + { + if (entity != null && entity.IsObsolete) + { + builder.Append($"Obsolete: {entity.ObsolescenceReason}{Environment.NewLine}"); + } + } } } \ No newline at end of file diff --git a/Src/SwqlStudio/ObjectExplorer/TreeNodeUtils.cs b/Src/SwqlStudio/ObjectExplorer/TreeNodeUtils.cs index 122752ef2..842be6f41 100644 --- a/Src/SwqlStudio/ObjectExplorer/TreeNodeUtils.cs +++ b/Src/SwqlStudio/ObjectExplorer/TreeNodeUtils.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using System.Text; using System.Windows.Forms; +using SwqlStudio.Filtering; +using SwqlStudio.Metadata; namespace SwqlStudio { @@ -42,9 +44,7 @@ private static void CopyTree(TreeNodeCollection source, TreeNodeCollection targe { newNode = CloneShallow(node); } - - target.Add(newNode); bindings.BindNodes(node, newNode); CopyTree(node.Nodes, newNode.Nodes, bindings, filter, updateAction); @@ -80,71 +80,7 @@ internal static void CopyContent(TreeNode source, TreeNode target) target.ContextMenu = source.ContextMenu; target.ContextMenuStrip = source.ContextMenuStrip; target.Checked = source.Checked; - } - - /// - /// Used in filtering of the nodes. - /// - /// Keeps track of visibility of each single one - /// - public class NodeVisibility - { - private readonly Dictionary _visibility = new Dictionary(ReferenceEqualityComparer.Default); - - - public void SetVisible(TreeNode node) - { - _visibility[node] = VisibilityStatus.Visible; - for (var parent = node.Parent; parent != null; parent = parent.Parent) - { - _visibility.TryGetValue(parent, out var parentVisibility); - if (parentVisibility != VisibilityStatus.Visible) - parentVisibility = VisibilityStatus.ChildVisible; - - _visibility[parent] = parentVisibility; - } - } - - public VisibilityStatus GetVisibility(TreeNode node) - { - if (!_visibility.TryGetValue(node, out var rv)) - { - rv = VisibilityStatus.NotVisible; - // check if some of the parents is not directly visible (visible, not childvisible) - for (var parent = node.Parent; parent != null; parent = parent.Parent) - { - if (_visibility.TryGetValue(parent, out var parentVisibility)) - { - if (parentVisibility == VisibilityStatus.Visible) - rv = VisibilityStatus.ParentVisible; - else - break; - } - } - } - - return rv; - } - - public enum VisibilityStatus - { - /// - /// This node is not visible - /// - NotVisible, - /// - /// Some children of this node passed filter - /// - ChildVisible, - /// - /// This node passed filter - /// - Visible, - /// - /// This node's parent passed filter - /// - ParentVisible - } + target.NodeFont = source.NodeFont; } /// @@ -184,7 +120,7 @@ public TreeNode FindDataNode(TreeNode displayTreeNode) return rv; } } - private sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer + internal sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer { public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer(); diff --git a/Src/SwqlStudio/ObjectExplorer/TreeNodesBuilder.cs b/Src/SwqlStudio/ObjectExplorer/TreeNodesBuilder.cs index 02ddde71c..4193bdfca 100644 --- a/Src/SwqlStudio/ObjectExplorer/TreeNodesBuilder.cs +++ b/Src/SwqlStudio/ObjectExplorer/TreeNodesBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Windows.Forms; using SwqlStudio.Metadata; @@ -10,7 +11,15 @@ internal class TreeNodesBuilder { public EntityGroupingMode EntityGroupingMode { get; set; } - private static TreeNodeWithConnectionInfo MakeEntityTreeNode(IMetadataProvider provider, Entity entity) + private readonly Font _defaultFont; + private Font NodeFont(IObsoleteMetadata entity) => entity.IsObsolete ? new Font(_defaultFont, FontStyle.Strikeout) : null; + + public TreeNodesBuilder(Font defaultFont) + { + _defaultFont = defaultFont; + } + + private TreeNodeWithConnectionInfo MakeEntityTreeNode(IMetadataProvider provider, Entity entity) { var entityNode = CreateEntityNode(provider, entity); @@ -30,7 +39,7 @@ private static TreeNodeWithConnectionInfo MakeEntityTreeNode(IMetadataProvider p return entityNode; } - private static void AddVerbsToNode(TreeNode entityNode, Entity entity, IMetadataProvider provider) + private void AddVerbsToNode(TreeNode entityNode, Entity entity, IMetadataProvider provider) { foreach (var verb in entity.Verbs.OrderBy(v => v.Name)) { @@ -39,19 +48,21 @@ private static void AddVerbsToNode(TreeNode entityNode, Entity entity, IMetadata var argumentsPlaceholder = new ArgumentsPlaceholderTreeNode(verb, provider); verbNode.Nodes.Add(argumentsPlaceholder); + verbNode.NodeFont = NodeFont(verb); entityNode.Nodes.Add(verbNode); } } - private static void AddPropertiesToNode(IMetadataProvider provider, TreeNode entityNode, IEnumerable properties) + private void AddPropertiesToNode(IMetadataProvider provider, TreeNode entityNode, IEnumerable properties) { foreach (Property property in properties.OrderBy(c => c.Name)) { string name = DocumentationBuilder.ToNodeText(property); var imageKey = ImageKeys.GetImageKey(property); TreeNode node = CreateNode(provider, name, imageKey, property); - node.ToolTipText = DocumentationBuilder.ToToolTip(property); + node.ToolTipText = DocumentationBuilder.ToToolTip(property, property); + node.NodeFont = NodeFont(property); entityNode.Nodes.Add(node); } } @@ -68,7 +79,7 @@ public static void RebuildVerbArguments(TreeNode verbNode, IMetadataProvider pro } } - private static TreeNodeWithConnectionInfo[] MakeEntityTreeNodes(IMetadataProvider provider, IEnumerable entities) + private TreeNodeWithConnectionInfo[] MakeEntityTreeNodes(IMetadataProvider provider, IEnumerable entities) { return entities.Select(e => MakeEntityTreeNode(provider, e)).ToArray(); } @@ -108,7 +119,7 @@ public void RebuildDatabaseNode(TreeNode databaseNode, IMetadataProvider provide } } - private static void GroupByHierarchy(IMetadataProvider provider, TreeNode baseNode) + private void GroupByHierarchy(IMetadataProvider provider, TreeNode baseNode) { Entity baseEntity = baseNode != null ? baseNode.Tag as Entity : null; @@ -169,11 +180,12 @@ private static TreeNodeWithConnectionInfo CreateEntityNode(IMetadataProvider pro return entityNode; } - private static TreeNodeWithConnectionInfo CreateEntityNode(IMetadataProvider provider, Entity entity) + private TreeNodeWithConnectionInfo CreateEntityNode(IMetadataProvider provider, Entity entity) { var imageKey = ImageKeys.GetImageKey(entity); var node = CreateNode(provider, entity.FullName, imageKey, entity); node.ToolTipText = DocumentationBuilder.ToToolTip(provider.ConnectionInfo, entity); + node.NodeFont = NodeFont(entity); return node; } diff --git a/Src/SwqlStudio/Properties/Settings.Designer.cs b/Src/SwqlStudio/Properties/Settings.Designer.cs index a1948c04b..cd6c2cb1a 100644 --- a/Src/SwqlStudio/Properties/Settings.Designer.cs +++ b/Src/SwqlStudio/Properties/Settings.Designer.cs @@ -331,5 +331,17 @@ internal sealed partial class Settings : global::System.Configuration.Applicatio this["PromptToSaveOnClose"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool ShowObsolete { + get { + return ((bool)(this["ShowObsolete"])); + } + set { + this["ShowObsolete"] = value; + } + } } } diff --git a/Src/SwqlStudio/Properties/Settings.settings b/Src/SwqlStudio/Properties/Settings.settings index d3b05ffe9..7ecb29c1b 100644 --- a/Src/SwqlStudio/Properties/Settings.settings +++ b/Src/SwqlStudio/Properties/Settings.settings @@ -98,5 +98,8 @@ True + + True + \ No newline at end of file diff --git a/Src/SwqlStudio/QueriesDockPanel.cs b/Src/SwqlStudio/QueriesDockPanel.cs index 342531d47..37440673b 100644 --- a/Src/SwqlStudio/QueriesDockPanel.cs +++ b/Src/SwqlStudio/QueriesDockPanel.cs @@ -184,6 +184,11 @@ internal void SetEntityGroupingMode(EntityGroupingMode mode) objectExplorer.RefreshAllServers(); } + internal void RefreshObjectExplorerFilters() + { + objectExplorer.RefreshFilters(); + } + internal void FocusSearch() { objectExplorer.FocusSearch(); diff --git a/Src/SwqlStudio/SwqlStudio.csproj b/Src/SwqlStudio/SwqlStudio.csproj index 505d4ee82..5234b5d3d 100644 --- a/Src/SwqlStudio/SwqlStudio.csproj +++ b/Src/SwqlStudio/SwqlStudio.csproj @@ -212,6 +212,7 @@ + @@ -226,6 +227,11 @@ + + + + + diff --git a/Src/SwqlStudio/app.config b/Src/SwqlStudio/app.config index c3668c4a3..a68c0e605 100644 --- a/Src/SwqlStudio/app.config +++ b/Src/SwqlStudio/app.config @@ -12,13 +12,13 @@ - + - + - + True @@ -32,6 +32,9 @@ True + + True +