From c74e92fb2e3b0ab3d3d8a0f603643a5a9b3910e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Koral?= <45078678+oguzhankoral@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:14:10 +0300 Subject: [PATCH] Fix(tekla): Read write model cards to/from appdata + fixes double UI (#395) * Fix multiple problems * Connect windows each other * Add note --- .../Bindings/TeklaBasicConnectorBinding.cs | 9 +- .../HostApp/TeklaDocumentModelStore.cs | 95 +++++++++++++++++-- .../SpeckleTeklaPanelHost.cs | 64 ++++++++++--- 3 files changed, 147 insertions(+), 21 deletions(-) diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs index 23fba0b59..e20d5e886 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/Bindings/TeklaBasicConnectorBinding.cs @@ -12,6 +12,7 @@ namespace Speckle.Connector.Tekla2024.Bindings; public class TeklaBasicConnectorBinding : IBasicConnectorBinding { + public BasicConnectorBindingCommands Commands { get; } private readonly ISpeckleApplication _speckleApplication; private readonly DocumentModelStore _store; public string Name => "baseBinding"; @@ -32,6 +33,12 @@ TSM.Model model Parent = parent; _logger = logger; _model = model; + Commands = new BasicConnectorBindingCommands(parent); + _store.DocumentChanged += (_, _) => + parent.TopLevelExceptionHandler.FireAndForget(async () => + { + await Commands.NotifyDocumentChanged().ConfigureAwait(false); + }); } public string GetSourceApplicationName() => _speckleApplication.Slug; @@ -141,6 +148,4 @@ await Task.Run(() => _logger.LogError(ex, "Failed to highlight objects"); } } - - public BasicConnectorBindingCommands Commands { get; } } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs index 891d0972b..c995af7ae 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/HostApp/TeklaDocumentModelStore.cs @@ -1,17 +1,100 @@ -using Speckle.Connectors.DUI.Models; +using System.IO; +using Microsoft.Extensions.Logging; +using Speckle.Connectors.DUI.Models; using Speckle.Newtonsoft.Json; +using Speckle.Sdk; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Logging; namespace Speckle.Connector.Tekla2024.HostApp; public class TeklaDocumentModelStore : DocumentModelStore { + private readonly ISpeckleApplication _speckleApplication; + private readonly ILogger _logger; + private readonly TSM.Model _model; + private readonly TSM.Events _events; + private string HostAppUserDataPath { get; set; } + private string DocumentStateFile { get; set; } + private string ModelPathHash { get; set; } + public TeklaDocumentModelStore( - JsonSerializerSettings jsonSerializerSettings - // ITopLevelExceptionHandler topLevelExceptionHandler + JsonSerializerSettings jsonSerializerSettings, + ISpeckleApplication speckleApplication, + ILogger logger ) - : base(jsonSerializerSettings, true) { } + : base(jsonSerializerSettings, true) + { + _speckleApplication = speckleApplication; + _logger = logger; + _model = new TSM.Model(); + SetPaths(); + _events = new TSM.Events(); + _events.ModelLoad += () => + { + SetPaths(); + ReadFromFile(); + OnDocumentChanged(); + }; + _events.Register(); + if (SpeckleTeklaPanelHost.IsInitialized) + { + ReadFromFile(); + OnDocumentChanged(); + } + } + + private void SetPaths() + { + ModelPathHash = Crypt.Md5(_model.GetInfo().ModelPath, length: 32); + HostAppUserDataPath = Path.Combine( + SpecklePathProvider.UserSpeckleFolderPath, + "Connectors", + _speckleApplication.Slug + ); + DocumentStateFile = Path.Combine(HostAppUserDataPath, $"{ModelPathHash}.json"); + } + + public override void WriteToFile() + { + string serializedState = Serialize(); + try + { + if (!Directory.Exists(HostAppUserDataPath)) + { + Directory.CreateDirectory(HostAppUserDataPath); + } + File.WriteAllText(DocumentStateFile, serializedState); + } + catch (Exception ex) when (!ex.IsFatal()) + { + _logger.LogError(ex.Message); + } + } + + public override void ReadFromFile() + { + try + { + if (!Directory.Exists(HostAppUserDataPath)) + { + Models = new(); + return; + } - public override void WriteToFile() { } + if (!File.Exists(DocumentStateFile)) + { + Models = new(); + return; + } - public override void ReadFromFile() { } + string serializedState = File.ReadAllText(DocumentStateFile); + Models = Deserialize(serializedState) ?? new(); + } + catch (Exception ex) when (!ex.IsFatal()) + { + Models = new(); + _logger.LogError(ex.Message); + } + } } diff --git a/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs b/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs index d051c6bf2..b535cd82b 100644 --- a/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs +++ b/Connectors/Tekla/Speckle.Connector.TeklaShared/SpeckleTeklaPanelHost.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.Runtime.InteropServices; using System.Windows.Forms; using System.Windows.Forms.Integration; using Microsoft.Extensions.DependencyInjection; @@ -14,13 +16,57 @@ namespace Speckle.Connector.Tekla2024; public class SpeckleTeklaPanelHost : PluginFormBase { - private ElementHost Host { get; } + private static SpeckleTeklaPanelHost? s_instance; + private ElementHost Host { get; set; } public Model Model { get; private set; } public static new ServiceProvider? Container { get; private set; } - private static readonly List s_instances = new(); + + // NOTE: Somehow tekla triggers this class twice at the beginning and on first dialog our webview appears + // with small size of render in Host even if we set it as Dock.Fill. But on second trigger dialog initializes as expected. + // So, we do not init our plugin at first attempt, we just close it at first. + // On second, we init plugin and mark plugin as 'Initialized' to handle later init attempts nicely. + // We make 'IsInitialized' as 'false' only whenever our main dialog is closed explicitly by user. + private static bool IsFirst { get; set; } = true; + public static bool IsInitialized { get; private set; } + + //window owner call + [DllImport("user32.dll", SetLastError = true)] + [SuppressMessage("Security", "CA5392:Use DefaultDllImportSearchPaths attribute for P/Invokes")] + private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr value); + + private const int GWL_HWNDPARENT = -8; public SpeckleTeklaPanelHost() { + if (IsFirst) + { + IsFirst = false; + Close(); + } + else + { + if (IsInitialized) + { + s_instance?.BringToFront(); + Close(); + return; + } + IsInitialized = true; + InitializeInstance(); + s_instance?.BringToFront(); + } + } + + protected override void OnClosed(EventArgs e) + { + s_instance?.Dispose(); + IsInitialized = false; + } + + private void InitializeInstance() + { + s_instance = this; // Assign the current instance to the static field + this.Text = "Speckle (Beta)"; this.Name = "Speckle (Beta)"; @@ -37,17 +83,6 @@ public SpeckleTeklaPanelHost() this.Icon = Icon.FromHandle(bmp.GetHicon()); } - // adds instances to tracking list - s_instances.Add(this); - - if (s_instances.Count > 1) - { - var firstInstance = s_instances[0]; - s_instances.RemoveAt(0); - // hides the first instance if there is more than one - firstInstance.Hide(); - } - var services = new ServiceCollection(); services.Initialize(HostApplications.TeklaStructures, GetVersion()); services.AddTekla(); @@ -66,10 +101,13 @@ public SpeckleTeklaPanelHost() ); } var webview = Container.GetRequiredService(); + webview.RenderSize = new System.Windows.Size(800, 600); Host = new() { Child = webview, Dock = DockStyle.Fill }; Controls.Add(Host); Operation.DisplayPrompt("Speckle connector initialized."); + this.TopLevel = true; + SetWindowLongPtr(Handle, GWL_HWNDPARENT, MainWindow.Frame.Handle); Show(); Activate(); Focus();