From 0da14244772a744c358ced1c20b2ad813ea1f2ff Mon Sep 17 00:00:00 2001 From: kozhilya Date: Mon, 28 Apr 2025 19:23:40 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B4=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20?= =?UTF-8?q?=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...signer.cs => 20250428065903_1.Designer.cs} | 2 +- ...0250426125550_1.cs => 20250428065903_1.cs} | 0 .../EntityTreeViewerControl.Designer.cs | 17 +- .../EntityTreeViewerControl.cs | 281 ++++++------- .../EntityTreeViewerRow.Designer.cs | 130 ++++++ .../EntityTreeViewer/EntityTreeViewerRow.cs | 158 +++++++ .../EntityTreeViewer/EntityTreeViewerRow.resx | 120 ++++++ .../EntityTreeViewer/StructureBuilder.cs | 395 ++++++++++++++++++ Pf2eViewer/Pf2eViewer.csproj | 1 + Pf2eViewer/TestForm.Designer.cs | 24 +- Pf2eViewer/TestForm.cs | 34 +- 11 files changed, 977 insertions(+), 185 deletions(-) rename Pf2eModel/Migrations/{20250426125550_1.Designer.cs => 20250428065903_1.Designer.cs} (99%) rename Pf2eModel/Migrations/{20250426125550_1.cs => 20250428065903_1.cs} (100%) create mode 100644 Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.Designer.cs create mode 100644 Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.cs create mode 100644 Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.resx create mode 100644 Pf2eViewer/EntityTreeViewer/StructureBuilder.cs diff --git a/Pf2eModel/Migrations/20250426125550_1.Designer.cs b/Pf2eModel/Migrations/20250428065903_1.Designer.cs similarity index 99% rename from Pf2eModel/Migrations/20250426125550_1.Designer.cs rename to Pf2eModel/Migrations/20250428065903_1.Designer.cs index 7e50928..21536e1 100644 --- a/Pf2eModel/Migrations/20250426125550_1.Designer.cs +++ b/Pf2eModel/Migrations/20250428065903_1.Designer.cs @@ -13,7 +13,7 @@ using Pf2eModel.Model.Enum; namespace Pf2eModel.Migrations { [DbContext(typeof(Pf2eDbContext))] - [Migration("20250426125550_1")] + [Migration("20250428065903_1")] partial class _1 { /// diff --git a/Pf2eModel/Migrations/20250426125550_1.cs b/Pf2eModel/Migrations/20250428065903_1.cs similarity index 100% rename from Pf2eModel/Migrations/20250426125550_1.cs rename to Pf2eModel/Migrations/20250428065903_1.cs diff --git a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.Designer.cs b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.Designer.cs index 8c79c98..11a9dba 100644 --- a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.Designer.cs +++ b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.Designer.cs @@ -28,39 +28,28 @@ partial class EntityTreeViewerControl /// private void InitializeComponent() { - VerticalScrollBar = new VScrollBar(); ContentPanel = new Panel(); SuspendLayout(); // - // VerticalScrollBar - // - VerticalScrollBar.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; - VerticalScrollBar.Location = new Point(400, 0); - VerticalScrollBar.Name = "VerticalScrollBar"; - VerticalScrollBar.Size = new Size(20, 520); - VerticalScrollBar.TabIndex = 0; - // // ContentPanel // ContentPanel.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; ContentPanel.Location = new Point(0, 0); ContentPanel.Name = "ContentPanel"; - ContentPanel.Size = new Size(400, 520); + ContentPanel.Size = new Size(420, 520); ContentPanel.TabIndex = 2; + ContentPanel.Resize += ContentPanel_Resize; // // EntityTreeViewerControl // AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; Controls.Add(ContentPanel); - Controls.Add(VerticalScrollBar); Name = "EntityTreeViewerControl"; Size = new Size(420, 520); ResumeLayout(false); } #endregion - - private VScrollBar VerticalScrollBar; - private Panel ContentPanel; + public Panel ContentPanel; } diff --git a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.cs b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.cs index db1f4d5..27b2af8 100644 --- a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.cs +++ b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerControl.cs @@ -1,16 +1,5 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; -using System.Windows.Forms.VisualStyles; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using System.Data; +using Pf2eViewer.EntityTreeViewer; namespace Pf2eViewer; @@ -20,6 +9,8 @@ public partial class EntityTreeViewerControl : UserControl private EntityView? _entityView = null; + private int _depthSize = 20; + public int ItemHeight { get => _itemHeight; @@ -36,16 +27,62 @@ public partial class EntityTreeViewerControl : UserControl set { _entityView = value; + UpdateData(); + } + } + + public int DepthSize + { + get => _depthSize; + set + { + _depthSize = value; UpdateView(); } } + public int NameWidth { get; private set; } = 200; + + private readonly List Rows = []; + + private List ViewerRows = []; + + public DarkModeCS? DarkMode; + public EntityTreeViewerControl() { InitializeComponent(); + DoubleBuffered = true; + + ContentPanel.AutoScroll = false; + ContentPanel.VerticalScroll.Enabled = true; + ContentPanel.VerticalScroll.Visible = true; + + ContentPanel.MouseWheel += ContentPanel_MouseWheel; } - public void UpdateView() + private int _visibleRows; + + public int ScrollPosition + { + get => ContentPanel.VerticalScroll.Value / ItemHeight; + set + { + VScrollProperties scrollBar = ContentPanel.VerticalScroll; + int oldValue = scrollBar.Value / ItemHeight; + int visibleItems = scrollBar.LargeChange / ItemHeight; + + int scrollValue = Math.Min(Math.Max(value, 0), _visibleRows - visibleItems); + + if (oldValue != scrollValue) + { + ContentPanel.VerticalScroll.Value = scrollValue * ItemHeight; + UpdateScroll(); + } + } + } + + public void UpdateData() { ContentPanel.Controls.Clear(); @@ -56,174 +93,96 @@ public partial class EntityTreeViewerControl : UserControl Rows.Clear(); - Rows.Add(new HeaderRow("Entity")); - Rows.Add(new ObjectComplex(null, EntityView.Entity)); - Rows.Add(new HeaderRow(EntityView.SubEntity?.GetType().ToString() ?? "SubEntity")); - Rows.Add(new ObjectComplex(null, EntityView.SubEntity)); + StructureBuilder builder = new(); - List displayRows = Rows.SelectMany(r => r.GetTextRows()).ToList(); + Rows.Add(builder.Create("Entity", EntityView.Entity)); - for (int i = 0; i < displayRows.Count; i++) + if (EntityView.SubEntity is not null) { - DisplayRow dr = displayRows[i]; + string typeName = EntityView.SubEntity.GetType().Name; - TextBox tb = new() - { - Top = i * ItemHeight, - Left = 0, - Width = ContentPanel.Width, - Height = ItemHeight, - Font = Font, - Text = string.Join( - "", - [new(' ', dr.Depth * 2), dr.Name, dr.Value != "" ? " : " : "", dr.Value] - ), - Margin = new(0), - }; - - ContentPanel.Controls.Add(tb); + Rows.Add(builder.Create(typeName, EntityView.SubEntity)); } + + UpdateRows(); + UpdateView(); } - private Panel GetObjectPanel(object obj) + public void UpdateRows() { - Panel result = new(); + ViewerRows = Rows.SelectMany(r => r.GetTextRows(this)).ToList(); - Type type = obj.GetType(); + ContentPanel.Controls.Clear(); - PropertyInfo[] properties = type.GetProperties(); + ContentPanel.Controls.AddRange(ViewerRows.ToArray()); + + UpdateView(); + } + + public void UpdateView() + { + foreach (EntityTreeViewerRow row in ViewerRows) + { + row.Visible = row.Item.Visible; + } + + List rows = ViewerRows.Where(r => r.Item.Visible).ToList(); + + int maxNameWidth = -1; + int viewRow = 0; + + foreach (EntityTreeViewerRow row in rows) + { + row.ViewIndex = viewRow++; + int nameWidth = row.GetNameWidth() + row.Item.Depth * DepthSize; + maxNameWidth = Math.Max(maxNameWidth, nameWidth); + } + + NameWidth = maxNameWidth; + + foreach (EntityTreeViewerRow row in rows) + { + row.Refresh(); + } + + UpdateScroll(); + } + + public void UpdateScroll() + { + List rows = ViewerRows.Where(r => r.Item.Visible).ToList(); + _visibleRows = rows.Count; + + ContentPanel.VerticalScroll.Enabled = true; + ContentPanel.VerticalScroll.Visible = true; + + ContentPanel.VerticalScroll.SmallChange = ItemHeight; + ContentPanel.VerticalScroll.LargeChange = ContentPanel.Height; + ContentPanel.VerticalScroll.Minimum = 0; + ContentPanel.VerticalScroll.Maximum = _visibleRows * ItemHeight; + + // ContentPanel.VerticalScroll.Value = VerticalScrollBar.Value * ItemHeight; /* - FieldInfo[] fields = type.GetFields( - BindingFlags.Instance - | BindingFlags.Static - | BindingFlags.NonPublic - | BindingFlags.Public - ); + ContentPanel.Top = -VerticalScrollBar.Value * ItemHeight; + ContentPanel.Invalidate(); */ - for (int i = 0; i < properties.Length; i++) - { - PropertyInfo property = properties[i]; - - TextBox tb = new() - { - Top = i * ItemHeight, - Left = 0, - Width = ContentPanel.Width, - Height = ItemHeight, - Font = Font, - Text = property.Name, - Margin = new(0), - }; - - result.Controls.Add(tb); - } - - result.Width = ContentPanel.Width; - result.Height = properties.Length * ItemHeight * 2; - - return result; } - private List Rows = []; + // Events - private class DisplayRow + private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) { - public string Name = ""; - - public string Value = ""; - - public int Depth = 0; + UpdateScroll(); } - private abstract class Row + private void ContentPanel_MouseWheel(object? sender, MouseEventArgs e) { - public abstract ICollection GetTextRows(); - - public int Depth = 0; + Focus(); + // ScrollPosition -= e.Delta / 120; } - private class HeaderRow(string name) : Row + private void ContentPanel_Resize(object sender, EventArgs e) { - public readonly string Name = name; - - public override ICollection GetTextRows() - { - return [new() { Name = Name, Depth = Depth }]; - } - } - - private abstract class ObjectField(PropertyInfo property, object? parent) : Row - { - public readonly PropertyInfo? Property = property; - - public readonly string Name = property.Name; - - public readonly Type Type = property.PropertyType; - - public abstract int Rows { get; } - - public static ObjectField Resolve(PropertyInfo property, object? parent) - { - return property.PropertyType.IsPrimitive - ? new ObjectPrimitive(property, parent) - : new ObjectComplex(property, parent); - } - } - - private class ObjectPrimitive : ObjectField - { - public readonly object? Value; - - public override int Rows => 1; - - public ObjectPrimitive(PropertyInfo? property, object? parent) - : base(property, parent) - { - Value = Property.GetValue(parent, null); - } - - public override ICollection GetTextRows() - { - return - [ - new() - { - Name = Name, - Value = Value?.ToString() ?? "null", - Depth = Depth, - }, - ]; - } - } - - private class ObjectComplex : ObjectField - { - public readonly List Fields; - - public bool Collapsed = false; - - public override int Rows => 1 + Fields.Select(f => f.Rows).Sum(); - - public ObjectComplex(PropertyInfo? property, object? parent) - : base(property, parent) - { - PropertyInfo[] properties = Type.GetProperties(); - - object? value = Property?.GetValue(parent, null) ?? parent; - - Fields = properties.Select(f => Resolve(f, value)).ToList(); - } - - public override ICollection GetTextRows() - { - List rows = [new() { Name = Name, Depth = Depth }]; - - if (!Collapsed) - { - rows.AddRange(Fields.SelectMany(f => f.GetTextRows()).ToList()); - } - - return rows; - } + UpdateView(); } } diff --git a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.Designer.cs b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.Designer.cs new file mode 100644 index 0000000..1bc1be8 --- /dev/null +++ b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.Designer.cs @@ -0,0 +1,130 @@ +namespace Pf2eViewer.EntityTreeViewer; + +partial class EntityTreeViewerRow +{ + /// + /// Обязательная переменная конструктора. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Освободить все используемые ресурсы. + /// + /// истинно, если управляемый ресурс должен быть удален; иначе ложно. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Код, автоматически созданный конструктором компонентов + + /// + /// Требуемый метод для поддержки конструктора — не изменяйте + /// содержимое этого метода с помощью редактора кода. + /// + private void InitializeComponent() + { + NameLabel = new Label(); + IconPictureBox = new PictureBox(); + NamePanel = new Panel(); + ValuePanel = new Panel(); + TypeLabel = new Label(); + ValueLabel = new Label(); + ((System.ComponentModel.ISupportInitialize) IconPictureBox).BeginInit(); + NamePanel.SuspendLayout(); + ValuePanel.SuspendLayout(); + SuspendLayout(); + // + // NameLabel + // + NameLabel.Dock = DockStyle.Fill; + NameLabel.Location = new Point(0, 0); + NameLabel.Margin = new Padding(0); + NameLabel.Name = "NameLabel"; + NameLabel.Size = new Size(200, 38); + NameLabel.TabIndex = 0; + NameLabel.Text = "Name"; + NameLabel.TextAlign = ContentAlignment.MiddleLeft; + // + // IconPictureBox + // + IconPictureBox.Dock = DockStyle.Left; + IconPictureBox.Location = new Point(0, 0); + IconPictureBox.Margin = new Padding(0); + IconPictureBox.Name = "IconPictureBox"; + IconPictureBox.Size = new Size(20, 38); + IconPictureBox.TabIndex = 1; + IconPictureBox.TabStop = false; + IconPictureBox.Click += IconPictureBox_Click; + // + // NamePanel + // + NamePanel.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; + NamePanel.Controls.Add(NameLabel); + NamePanel.Controls.Add(IconPictureBox); + NamePanel.Location = new Point(0, 0); + NamePanel.Name = "NamePanel"; + NamePanel.Size = new Size(200, 38); + NamePanel.TabIndex = 2; + // + // ValuePanel + // + ValuePanel.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + ValuePanel.BackColor = SystemColors.Control; + ValuePanel.Controls.Add(ValueLabel); + ValuePanel.Controls.Add(TypeLabel); + ValuePanel.Location = new Point(200, 0); + ValuePanel.Margin = new Padding(0); + ValuePanel.Name = "ValuePanel"; + ValuePanel.Size = new Size(288, 38); + ValuePanel.TabIndex = 4; + // + // TypeLabel + // + TypeLabel.Dock = DockStyle.Left; + TypeLabel.Location = new Point(0, 0); + TypeLabel.Margin = new Padding(0); + TypeLabel.Name = "TypeLabel"; + TypeLabel.Size = new Size(57, 38); + TypeLabel.TabIndex = 3; + TypeLabel.Text = "Type"; + TypeLabel.TextAlign = ContentAlignment.MiddleLeft; + // + // ValueLabel + // + ValueLabel.AutoEllipsis = true; + ValueLabel.Dock = DockStyle.Fill; + ValueLabel.Location = new Point(0, 0); + ValueLabel.Name = "ValueLabel"; + ValueLabel.Size = new Size(288, 38); + ValueLabel.TabIndex = 4; + ValueLabel.Text = "Value"; + ValueLabel.TextAlign = ContentAlignment.MiddleLeft; + // + // EntityTreeViewerRow + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + Controls.Add(NamePanel); + Controls.Add(ValuePanel); + Name = "EntityTreeViewerRow"; + Size = new Size(488, 38); + ((System.ComponentModel.ISupportInitialize) IconPictureBox).EndInit(); + NamePanel.ResumeLayout(false); + ValuePanel.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private Label NameLabel; + private PictureBox IconPictureBox; + private Panel NamePanel; + private Panel ValuePanel; + private Label TypeLabel; + private Label ValueLabel; +} diff --git a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.cs b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.cs new file mode 100644 index 0000000..89cba62 --- /dev/null +++ b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.cs @@ -0,0 +1,158 @@ +using FontAwesome.Sharp; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Header; + +namespace Pf2eViewer.EntityTreeViewer; + +internal partial class EntityTreeViewerRow : UserControl +{ + public readonly EntityTreeViewerControl ParentControl; + + public readonly StructureField Item; + + public int ViewIndex = 0; + + public int Depth => Item.Depth; + + public EntityTreeViewerRow(EntityTreeViewerControl parent, StructureField item) + { + ParentControl = parent; + Font = ParentControl.Font; + + Item = item; + + InitializeComponent(); + ApplyItem(); + Refresh(); + } + + private void ApplyItem() + { + TypeLabel.Font = new Font(Font.FontFamily, Font.Size - 2, FontStyle.Italic); + + NameLabel.Text = Item.Name; + TypeLabel.Text = Item.GetTypeString(); + if (TypeLabel.Text != "") + { + TypeLabel.Text = $"({TypeLabel.Text})"; + } + ValueLabel.Text = Item.GetValueString(); + + switch (Item) + { + case StructureFieldPrimitive primitive: + switch (Type.GetTypeCode(primitive.Type)) + { + case TypeCode.Boolean: + ValuePanel.ForeColor = Color.Red; + break; + case TypeCode.Int32: + ValuePanel.ForeColor = Color.Blue; + break; + case TypeCode.String: + ValuePanel.ForeColor = Color.OrangeRed; + break; + } + + ValueLabel.ForeColor = ValuePanel.ForeColor; + TypeLabel.ForeColor = ValuePanel.ForeColor; + break; + + case StructureFieldComplex complex: + ValueLabel.Font = new(Font, FontStyle.Italic); + break; + + case StructureFieldNull @null: + break; + + case StructureFieldReference reference: + break; + } + } + + public int GetNameWidth() + { + Size textSize = TextRenderer.MeasureText(NameLabel.Text, Font); + return textSize.Width + ParentControl.DepthSize; + } + + public new void Refresh() + { + base.Refresh(); + + Left = 0; + Top = ViewIndex * ParentControl.ItemHeight; + Width = ParentControl.ContentPanel.Width - Left - SystemInformation.VerticalScrollBarWidth; + Height = ParentControl.ItemHeight; + + // Name Panel + NamePanel.Left = Depth * ParentControl.DepthSize; + NamePanel.Width = ParentControl.NameWidth - NamePanel.Left; + + IconPictureBox.Width = ParentControl.ItemHeight; + + // Value Panel + ValuePanel.Left = NamePanel.Left + NamePanel.Width; + ValuePanel.Width = Width - ValuePanel.Left; + + Size typeSize = TextRenderer.MeasureText(TypeLabel.Text, TypeLabel.Font); + TypeLabel.Width = typeSize.Width + 3; + + TypeLabel.ForeColor = ValueLabel.ForeColor = GetValueColor(); + + if (Item.GetType() == typeof(StructureFieldComplex)) + { + StructureFieldComplex complex = (StructureFieldComplex)Item; + + IconPictureBox.Image = complex.Collapsed + ? IconChar.PlusSquare.ToBitmap(IconFont.Auto, ParentControl.DepthSize, ForeColor) + : IconChar.MinusSquare.ToBitmap(IconFont.Auto, ParentControl.DepthSize, ForeColor); + } + } + + public Color GetValueColor() + { + Func blend = (Color color) => _colorBlend([ForeColor, color]); + + switch (Item) + { + case StructureFieldPrimitive primitive: + return Type.GetTypeCode(primitive.Type) switch + { + TypeCode.Boolean => blend(Color.DarkGoldenrod), + TypeCode.Int32 => blend(Color.Blue), + TypeCode.Double => blend(Color.Aqua), + TypeCode.String => blend(Color.Green), + }; + + case StructureFieldEnum @enum: + return blend(Color.Red); + } + + return ForeColor; + } + + private Color _colorBlend(List clrArr) + { + int r = 0; + int g = 0; + int b = 0; + foreach (Color color in clrArr) + { + r += color.R; + g += color.G; + b += color.B; + } + return Color.FromArgb(r / clrArr.Count, g / clrArr.Count, b / clrArr.Count); + } + + private void IconPictureBox_Click(object sender, EventArgs e) + { + if (Item.GetType() == typeof(StructureFieldComplex)) + { + StructureFieldComplex complex = (StructureFieldComplex)Item; + + complex.Collapsed = !complex.Collapsed; + ParentControl.UpdateView(); + } + } +} diff --git a/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.resx b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/Pf2eViewer/EntityTreeViewer/EntityTreeViewerRow.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Pf2eViewer/EntityTreeViewer/StructureBuilder.cs b/Pf2eViewer/EntityTreeViewer/StructureBuilder.cs new file mode 100644 index 0000000..a8464b3 --- /dev/null +++ b/Pf2eViewer/EntityTreeViewer/StructureBuilder.cs @@ -0,0 +1,395 @@ +using System.Collections; +using System.Reflection; +using LinqToDB.Extensions; +using Pf2eModel.Model.Common.I18n; + +namespace Pf2eViewer.EntityTreeViewer; + +internal class StructureBuilder +{ + private readonly List _processedObjects = []; + + public StructureFieldComplex Create(string name, object value, int depth = 0) + { + _ = RegisterProcessed(value); + + StructureFieldComplex field = new(this, value, name, depth); + + CompleteProcessed(value, field); + + return field; + } + + public bool RegisterProcessed(object value) + { + if (_processedObjects.Any(p => p.Value == value)) + { + return false; + } + + StructureProcessedObject processed = new(value); + + _processedObjects.Add(processed); + + return true; + } + + public void CompleteProcessed(object value, StructureFieldComplex field) + { + var q = _processedObjects.FirstOrDefault(p => p.Value == value); + + if (q == null) + { + return; + } + + q.Field = field; + } + + public StructureField Resolve(string name, object? value, int depth) + { + switch (GetFieldType(value)) + { + case StructureFieldType.Null: + return new StructureFieldNull(this, name, depth); + + case StructureFieldType.Primitive: + return new StructureFieldPrimitive(this, value!, name, depth); + + case StructureFieldType.Enum: + return new StructureFieldEnum(this, value!, name, depth); + + case StructureFieldType.Complex: + default: + + StructureProcessedObject? processed = _processedObjects.FirstOrDefault(p => + p.Value == value + ); + + if (processed is not null) + { + var reference = new StructureFieldReference(this, name, null, depth); + processed.AddReference(reference); + return reference; + } + + return Create(name, value!, depth); + } + } + + public static StructureFieldType GetFieldType(object? value) + { + if (value is null) + { + return StructureFieldType.Null; + } + + Type type = value.GetType(); + + if (type.IsEnum) + { + return StructureFieldType.Enum; + } + + if (type.IsPrimitive || type == typeof(string)) + { + return StructureFieldType.Primitive; + } + + return StructureFieldType.Complex; + } +} + +internal class StructureProcessedObject(object value) +{ + public readonly object Value = value; + + private StructureFieldComplex? _field = null; + + public StructureFieldComplex? Field + { + get => _field; + set + { + _field = value; + foreach (var reference in _references) + { + reference.Reference = Field; + } + } + } + + private readonly List _references = []; + + public void AddReference(StructureFieldReference reference) + { + _references.Add(reference); + + if (_field is not null) + { + reference.Reference = Field; + } + } +} + +internal enum StructureFieldType +{ + Null, + Primitive, + Enum, + Complex, + Reference, +} + +internal abstract class StructureField(StructureBuilder builder, string name, int depth) +{ + public readonly string Name = name; + + public abstract StructureFieldType FieldType { get; } + + public readonly StructureBuilder Builder = builder; + + public virtual bool Visible { get; set; } = true; + + public int Depth = depth; + + public abstract string GetTypeString(); + + public abstract string GetValueString(); + + public virtual ICollection GetTextRows(EntityTreeViewerControl control) + { + return [new EntityTreeViewerRow(control, this)]; + } +} + +internal class StructureFieldNull(StructureBuilder builder, string name, int depth) + : StructureField(builder, name, depth) +{ + public override StructureFieldType FieldType => StructureFieldType.Null; + + public override string GetTypeString() => "null"; + + public override string GetValueString() => ""; +} + +internal class StructureFieldPrimitive( + StructureBuilder builder, + object value, + string name, + int depth +) : StructureField(builder, name, depth) +{ + public override StructureFieldType FieldType => StructureFieldType.Primitive; + + public object Value = value; + + public readonly Type Type = value.GetType(); + + public override string GetTypeString() + { + return Type.GetTypeCode(Type) switch + { + TypeCode.Boolean => "bool", + TypeCode.Int32 => "int", + TypeCode.String => "string", + TypeCode.Double => "double", + _ => Type.Name, + }; + } + + public override string GetValueString() + { + if (Type == typeof(string)) + { + return "\"" + ((string)Value).Replace("\"", "\\\"") + "\""; + } + + return Value.ToString() ?? string.Empty; + } +} + +internal class StructureFieldEnum(StructureBuilder builder, object value, string name, int depth) + : StructureField(builder, name, depth) +{ + public override StructureFieldType FieldType => StructureFieldType.Enum; + + public object Value = value; + + public readonly Type Type = value.GetType(); + + public override string GetTypeString() => "enum"; + + public override string GetValueString() => $"{Type.Name}.{Value}"; +} + +internal class StructureFieldComplex : StructureField +{ + public override StructureFieldType FieldType => StructureFieldType.Complex; + + public readonly List Fields = []; + + public readonly Type Type; + + private bool _collapsed = false; + + public bool Collapsed + { + get => _collapsed; + set + { + _collapsed = value; + UpdateFieldVisibility(); + } + } + + private bool _visible = true; + + public StructureFieldComplex(StructureBuilder builder, object value, string name, int depth) + : base(builder, name, depth) + { + Type = value.GetType(); + + SetFields((dynamic)value); + } + + public override bool Visible + { + get => _visible; + set + { + _visible = value; + UpdateFieldVisibility(); + } + } + + private void UpdateFieldVisibility() + { + foreach (var field in Fields) + { + field.Visible = Visible && !Collapsed; + } + } + + private void SetFields(LocaleString localeString) + { + List records = []; + + foreach (var field in localeString) + { + records.Add(new FieldRecord(field.Locale.ToString(), field.Value)); + } + + SetFieldsFromRecords(records); + } + + private void SetFields(IEnumerable enumrable) + { + int i = 0; + List records = []; + + foreach (var item in enumrable) + { + records.Add(new FieldRecord($"[{i++}]", item)); + } + + SetFieldsFromRecords(records); + } + + private void SetFields(object value) + { + Type type = value.GetType(); + PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + SetFieldsFromRecords( + properties + .Where(property => + property.GetAttribute() is null + ) + .Select(property => new FieldRecord(property.Name, property.GetValue(value, null))) + .ToList() + ); + } + + private void SetFieldsFromRecords(List records) + { + foreach (FieldRecord record in records) + { + if (record.FieldType == StructureFieldType.Complex) + { + record.Allowed = Builder.RegisterProcessed(record.Value!); + } + } + + foreach (var record in records) + { + if (record.FieldType == StructureFieldType.Complex && record.Allowed) + { + Fields.Add(Builder.Create(record.Name, record.Value!, Depth + 1)); + } + else + { + Fields.Add(Builder.Resolve(record.Name, record.Value!, Depth + 1)); + } + } + } + + public override ICollection GetTextRows(EntityTreeViewerControl control) + { + List rows = base.GetTextRows(control).ToList(); + + rows.AddRange(Fields.SelectMany(f => f.GetTextRows(control)).ToList()); + + return rows; + } + + private class FieldRecord(string name, object? value) + { + public string Name = name; + + public object? Value = value; + + public StructureFieldType FieldType => StructureBuilder.GetFieldType(Value); + + public bool Allowed = true; + } + + public override string GetTypeString() + { + if (Type.IsGenericEnumerableType()) + { + Type elementType = Type.GetGenericArguments().Single(); + + return $"{elementType.Name}[]"; + } + else + { + return Type.Name; + } + } + + public override string GetValueString() + { + if (Type.IsGenericEnumerableType()) + { + return Fields.Count.ToString() + (Fields.Count == 1 ? " item" : " items"); + } + return ""; + } +} + +internal class StructureFieldReference( + StructureBuilder builder, + string name, + StructureFieldComplex? reference, + int depth +) : StructureField(builder, name, depth) +{ + public StructureFieldComplex? Reference = reference; + + public override StructureFieldType FieldType => StructureFieldType.Reference; + + public override string GetTypeString() => Reference?.GetTypeString() ?? "???"; + + public override string GetValueString() => ""; +} diff --git a/Pf2eViewer/Pf2eViewer.csproj b/Pf2eViewer/Pf2eViewer.csproj index 59761fe..ab9617d 100644 --- a/Pf2eViewer/Pf2eViewer.csproj +++ b/Pf2eViewer/Pf2eViewer.csproj @@ -7,6 +7,7 @@ enable + diff --git a/Pf2eViewer/TestForm.Designer.cs b/Pf2eViewer/TestForm.Designer.cs index 519a836..bd44c60 100644 --- a/Pf2eViewer/TestForm.Designer.cs +++ b/Pf2eViewer/TestForm.Designer.cs @@ -28,17 +28,32 @@ partial class TestForm /// private void InitializeComponent() { + RandomButton = new Button(); entityTreeViewerControl1 = new EntityTreeViewerControl(); SuspendLayout(); // + // RandomButton + // + RandomButton.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + RandomButton.Location = new Point(12, 12); + RandomButton.Name = "RandomButton"; + RandomButton.Size = new Size(426, 23); + RandomButton.TabIndex = 0; + RandomButton.Text = "Random"; + RandomButton.UseVisualStyleBackColor = true; + RandomButton.Click += RandomButton_Click; + // // entityTreeViewerControl1 // entityTreeViewerControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + entityTreeViewerControl1.DepthSize = 20; entityTreeViewerControl1.EntityView = null; - entityTreeViewerControl1.Location = new Point(12, 12); + entityTreeViewerControl1.Font = new Font("Consolas", 9F); + entityTreeViewerControl1.ItemHeight = 20; + entityTreeViewerControl1.Location = new Point(12, 38); entityTreeViewerControl1.Name = "entityTreeViewerControl1"; - entityTreeViewerControl1.Size = new Size(426, 632); - entityTreeViewerControl1.TabIndex = 0; + entityTreeViewerControl1.Size = new Size(426, 606); + entityTreeViewerControl1.TabIndex = 1; // // TestForm // @@ -46,12 +61,15 @@ partial class TestForm AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(450, 656); Controls.Add(entityTreeViewerControl1); + Controls.Add(RandomButton); Name = "TestForm"; Text = "TestForm"; + Load += TestForm_Load; ResumeLayout(false); } #endregion + private Button RandomButton; private EntityTreeViewerControl entityTreeViewerControl1; } \ No newline at end of file diff --git a/Pf2eViewer/TestForm.cs b/Pf2eViewer/TestForm.cs index c4ae87b..b356267 100644 --- a/Pf2eViewer/TestForm.cs +++ b/Pf2eViewer/TestForm.cs @@ -14,19 +14,41 @@ namespace Pf2eViewer; public partial class TestForm : Form { + public DarkModeCS DarkMode; + + public Pf2eDbContext Context = new(); + + public int Count = 0; + public TestForm() { InitializeComponent(); - _ = new DarkModeCS(this) { ColorMode = DarkModeCS.DisplayMode.SystemDefault }; + DarkMode = new DarkModeCS(this) { ColorMode = DarkModeCS.DisplayMode.SystemDefault }; - Pf2eDbContext context = new(); - int count = context.Entities.Count(); - int id = Random.Shared.Next(1, count + 1); + entityTreeViewerControl1.DarkMode = DarkMode; + } - Entity entity = context.Entities.FirstOrDefault(e => e.Id == id)!; + public void RandomEntity() + { + int id = Random.Shared.Next(1, Count + 1); + + Entity entity = Context.Entities.FirstOrDefault(e => e.Id == id)!; + + EntityView view = new(Context, entity.Type, id); - EntityView view = new(context, entity.Type, id); entityTreeViewerControl1.EntityView = view; } + + private void TestForm_Load(object sender, EventArgs e) + { + Count = Context.Entities.Count(); + + RandomEntity(); + } + + private void RandomButton_Click(object sender, EventArgs e) + { + RandomEntity(); + } }