diff --git a/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll new file mode 100644 index 000000000..5031a0b0c Binary files /dev/null and b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll differ diff --git a/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll new file mode 100644 index 000000000..e94b9fc2e Binary files /dev/null and b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll differ diff --git a/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.dll b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.dll new file mode 100644 index 000000000..ea8d7a3a1 Binary files /dev/null and b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.dll differ diff --git a/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.resources.dll b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.resources.dll new file mode 100644 index 000000000..692886cdc Binary files /dev/null and b/bin/netcore/engines/IPY342/Microsoft.CodeAnalysis.resources.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll new file mode 100644 index 000000000..83a01f130 Binary files /dev/null and b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll new file mode 100644 index 000000000..80badf258 Binary files /dev/null and b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLoader.dll b/bin/netcore/engines/IPY342/pyRevitLoader.dll index acca7e91c..c3d565063 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLoader.dll and b/bin/netcore/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitRunner.dll b/bin/netcore/engines/IPY342/pyRevitRunner.dll index 780301edc..fcdaa5eb5 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitRunner.dll and b/bin/netcore/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.dll b/bin/netcore/pyRevitLabs.PyRevit.dll index e9c5f4b38..adb240109 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.dll and b/bin/netcore/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netfx/engines/IPY342/Microsoft.Bcl.AsyncInterfaces.dll b/bin/netfx/engines/IPY342/Microsoft.Bcl.AsyncInterfaces.dll new file mode 100644 index 000000000..6031ba1e9 Binary files /dev/null and b/bin/netfx/engines/IPY342/Microsoft.Bcl.AsyncInterfaces.dll differ diff --git a/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll new file mode 100644 index 000000000..87bebb0fe Binary files /dev/null and b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.dll differ diff --git a/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll new file mode 100644 index 000000000..f29b70fb7 Binary files /dev/null and b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.CSharp.resources.dll differ diff --git a/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.dll b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.dll new file mode 100644 index 000000000..69edcc9f8 Binary files /dev/null and b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.dll differ diff --git a/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.resources.dll b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.resources.dll new file mode 100644 index 000000000..bec1e3fb8 Binary files /dev/null and b/bin/netfx/engines/IPY342/Microsoft.CodeAnalysis.resources.dll differ diff --git a/bin/netfx/engines/IPY342/System.Collections.Immutable.dll b/bin/netfx/engines/IPY342/System.Collections.Immutable.dll new file mode 100644 index 000000000..ad944dff1 Binary files /dev/null and b/bin/netfx/engines/IPY342/System.Collections.Immutable.dll differ diff --git a/bin/netfx/engines/IPY342/System.Diagnostics.DiagnosticSource.dll b/bin/netfx/engines/IPY342/System.Diagnostics.DiagnosticSource.dll new file mode 100644 index 000000000..354f5f554 Binary files /dev/null and b/bin/netfx/engines/IPY342/System.Diagnostics.DiagnosticSource.dll differ diff --git a/bin/netfx/engines/IPY342/System.Reflection.Metadata.dll b/bin/netfx/engines/IPY342/System.Reflection.Metadata.dll new file mode 100644 index 000000000..3abdc4448 Binary files /dev/null and b/bin/netfx/engines/IPY342/System.Reflection.Metadata.dll differ diff --git a/bin/netfx/engines/IPY342/System.Runtime.CompilerServices.Unsafe.dll b/bin/netfx/engines/IPY342/System.Runtime.CompilerServices.Unsafe.dll index de9e12447..c5ba4e404 100644 Binary files a/bin/netfx/engines/IPY342/System.Runtime.CompilerServices.Unsafe.dll and b/bin/netfx/engines/IPY342/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/bin/netfx/engines/IPY342/System.Text.Encoding.CodePages.dll b/bin/netfx/engines/IPY342/System.Text.Encoding.CodePages.dll new file mode 100644 index 000000000..ec5e68b1f Binary files /dev/null and b/bin/netfx/engines/IPY342/System.Text.Encoding.CodePages.dll differ diff --git a/bin/netfx/engines/IPY342/System.Threading.Tasks.Extensions.dll b/bin/netfx/engines/IPY342/System.Threading.Tasks.Extensions.dll new file mode 100644 index 000000000..eeec92852 Binary files /dev/null and b/bin/netfx/engines/IPY342/System.Threading.Tasks.Extensions.dll differ diff --git a/bin/netfx/engines/IPY342/System.ValueTuple.dll b/bin/netfx/engines/IPY342/System.ValueTuple.dll new file mode 100644 index 000000000..4ce28fdea Binary files /dev/null and b/bin/netfx/engines/IPY342/System.ValueTuple.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll new file mode 100644 index 000000000..271762aa9 Binary files /dev/null and b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll new file mode 100644 index 000000000..c3bf2cf5d Binary files /dev/null and b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLoader.dll b/bin/netfx/engines/IPY342/pyRevitLoader.dll index 8f5037639..5b2c5756c 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLoader.dll and b/bin/netfx/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitRunner.dll b/bin/netfx/engines/IPY342/pyRevitRunner.dll index da6e3631c..969ea8060 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitRunner.dll and b/bin/netfx/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.dll b/bin/netfx/pyRevitLabs.PyRevit.dll index 3f2521dab..a10cf7428 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.dll and b/bin/netfx/pyRevitLabs.PyRevit.dll differ diff --git a/bin/pyRevitLabs.PyRevit.dll b/bin/pyRevitLabs.PyRevit.dll index e9c5f4b38..d345085b6 100644 Binary files a/bin/pyRevitLabs.PyRevit.dll and b/bin/pyRevitLabs.PyRevit.dll differ diff --git a/dev/Directory.Build.props b/dev/Directory.Build.props index eccbba34a..1bb10fb7a 100644 --- a/dev/Directory.Build.props +++ b/dev/Directory.Build.props @@ -2,8 +2,6 @@ Library - - x64 true diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs index 7a9379536..ff668047f 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs @@ -170,6 +170,12 @@ public static class PyRevitConsts { public const string BundleScriptGrasshopperXPostfix = ".ghx"; public const string BundleScriptRevitFamilyPostfix = ".rfa"; + // loader settings + public const string ConfigsNewLoaderKey = "new_loader"; + public const bool ConfigsNewLoaderDefault = false; + public const string ConfigsUseRoslynKey = "use_roslyn_loader"; + public const bool ConfigsUseRoslynDefault = false; + // theme public static SolidColorBrush PyRevitAccentBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0xf3, 0x9c, 0x12)); public static SolidColorBrush PyRevitBackgroundBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x2c, 0x3e, 0x50)); diff --git a/dev/pyRevitLoader/Directory.Build.props b/dev/pyRevitLoader/Directory.Build.props index 324f3faea..80bd72797 100644 --- a/dev/pyRevitLoader/Directory.Build.props +++ b/dev/pyRevitLoader/Directory.Build.props @@ -1,11 +1,11 @@ - + true true true + true net48;net8.0-windows - false diff --git a/dev/pyRevitLoader/Directory.Build.targets b/dev/pyRevitLoader/Directory.Build.targets index a4820d6e1..85f70d5b4 100644 --- a/dev/pyRevitLoader/Directory.Build.targets +++ b/dev/pyRevitLoader/Directory.Build.targets @@ -6,7 +6,7 @@ - + @@ -17,7 +17,7 @@ - + @@ -26,22 +26,39 @@ - + - - + + + + + + + + + + - + + + + + + + + + + Exclude="$(TargetDir)\**\Xceed.Wpf.AvalonDock.dll" + Condition="'$(IsTestProject)' != 'true'"/> diff --git a/dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs b/dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs index 2e09e0ef6..a639c1781 100644 --- a/dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs +++ b/dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs @@ -3,6 +3,9 @@ using System.Reflection; using Autodesk.Revit.UI; using Autodesk.Revit.Attributes; +using pyRevitAssemblyBuilder.AssemblyMaker; +using pyRevitAssemblyBuilder.SessionManager; +using pyRevitExtensionParser; /* Note: * It is necessary that this code object do not have any references to IronPython. @@ -28,7 +31,21 @@ Result IExternalApplication.OnStartup(UIControlledApplication application) try { - return ExecuteStartupScript(application); + // we need a UIApplication object to assign as `__revit__` in python... + var versionNumber = application.ControlledApplication.VersionNumber; + var fieldName = int.Parse(versionNumber) >= 2017 ? "m_uiapplication" : "m_application"; + var fi = application.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + + var uiApplication = (UIApplication)fi.GetValue(application); + + var executor = new ScriptExecutor(uiApplication); + var result = ExecuteStartupScript(application); + if (result == Result.Failed) + { + TaskDialog.Show("Error Loading pyRevit", executor.Message); + } + + return result; } catch (Exception ex) { @@ -49,11 +66,28 @@ private static void LoadAssembliesInFolder(string folder) } catch { + } } } private static Result ExecuteStartupScript(UIControlledApplication uiControlledApplication) + { + // defy the method of loading the assembly + // based on the config file setup + var config = PyRevitConfig.Load(); + switch (config.NewLoader) + { + case true when config.NewLoaderRoslyn: + return ExecuteStartUpCsharp(uiControlledApplication, AssemblyBuildStrategy.Roslyn); + case true: + return ExecuteStartUpCsharp(uiControlledApplication, AssemblyBuildStrategy.ILPack); + default: + return ExecuteStartUpPython(uiControlledApplication); + } + } + + public static Result ExecuteStartUpPython(UIControlledApplication uiControlledApplication) { // we need a UIApplication object to assign as `__revit__` in python... var versionNumber = uiControlledApplication.ControlledApplication.VersionNumber; @@ -76,18 +110,60 @@ private static Result ExecuteStartupScript(UIControlledApplication uiControlledA return result; } + public static Result ExecuteStartUpCsharp(UIControlledApplication uiControlledApplication, AssemblyBuildStrategy loadingMethod) + { + try + { + var versionNumber = uiControlledApplication.ControlledApplication.VersionNumber; + var fieldName = int.Parse(versionNumber) >= 2017 ? "m_uiapplication" : "m_application"; + var fi = uiControlledApplication.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + var uiApplication = (UIApplication)fi.GetValue(uiControlledApplication); + + // Instantiate all services + var assemblyBuilder = new AssemblyBuilderService(versionNumber, loadingMethod); + var extensionManager = new ExtensionManagerService(); + var hookManager = new HookManager(); + var uiManager = new UIManagerService(uiApplication); + + var sessionManager = new SessionManagerService( + assemblyBuilder, + extensionManager, + hookManager, + uiManager + ); + sessionManager.LoadSession(); + + // execute light version of StartupScript python script + Result result = Result.Succeeded; + var startupScript = GetStartupScriptPath(); + if (startupScript != null) + { + var executor = new ScriptExecutor(uiApplication); // uiControlledApplication); + result = executor.ExecuteScript(startupScript); + if (result == Result.Failed) + { + TaskDialog.Show("Error Loading pyRevit", executor.Message); + } + } + return Result.Succeeded; + } + catch (Exception ex) + { + TaskDialog.Show("Error Starting pyRevit Session", ex.ToString()); + return Result.Failed; + } + } private static string GetStartupScriptPath() { var loaderDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var dllDir = Path.GetDirectoryName(loaderDir); return Path.Combine(dllDir, string.Format("{0}.py", Assembly.GetExecutingAssembly().GetName().Name)); } - Result IExternalApplication.OnShutdown(UIControlledApplication application) { // FIXME: deallocate the python shell... return Result.Succeeded; } } -} +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs new file mode 100644 index 000000000..17286c734 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/AssemblyBuilderService.cs @@ -0,0 +1,196 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +#if !NETFRAMEWORK +using System.Runtime.Loader; +#endif +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Lokad.ILPack; +using System.Text; +using pyRevitExtensionParser; + +namespace pyRevitAssemblyBuilder.AssemblyMaker +{ + public enum AssemblyBuildStrategy + { + Roslyn, + ILPack + } + + public class AssemblyBuilderService + { + private readonly string _revitVersion; + private readonly AssemblyBuildStrategy _buildStrategy; + + public AssemblyBuilderService(string revitVersion, AssemblyBuildStrategy buildStrategy) + { + _revitVersion = revitVersion ?? throw new ArgumentNullException(nameof(revitVersion)); + _buildStrategy = buildStrategy; + +#if !NETFRAMEWORK + if (_buildStrategy == AssemblyBuildStrategy.ILPack) + { + // On .NET Core, hook into AssemblyLoadContext to resolve Lokad.ILPack two folders up + var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var ilPackPath = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "Lokad.ILPack.dll")); + AssemblyLoadContext.Default.Resolving += (context, name) => + { + if (string.Equals(name.Name, "Lokad.ILPack", StringComparison.OrdinalIgnoreCase) + && File.Exists(ilPackPath)) + { + return context.LoadFromAssemblyPath(ilPackPath); + } + return null; + }; + } +#else + if (_buildStrategy == AssemblyBuildStrategy.ILPack) + { + // On .NET Framework, hook into AppDomain to resolve Lokad.ILPack two folders up + var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var ilPackPath = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "Lokad.ILPack.dll")); + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var name = new AssemblyName(args.Name).Name; + if (string.Equals(name, "Lokad.ILPack", StringComparison.OrdinalIgnoreCase) + && File.Exists(ilPackPath)) + { + return Assembly.LoadFrom(ilPackPath); + } + return null; + }; + } +#endif + } + + public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension) + { + if (extension == null) + throw new ArgumentNullException(nameof(extension)); + + string hash = GetStableHash(extension.GetHash() + _revitVersion).Substring(0, 16); + string fileName = $"pyRevit_{_revitVersion}_{hash}_{extension.Name}.dll"; + + string outputDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "pyRevit", + _revitVersion); + Directory.CreateDirectory(outputDir); + + string outputPath = Path.Combine(outputDir, fileName); + + if (_buildStrategy == AssemblyBuildStrategy.Roslyn) + BuildWithRoslyn(extension, outputPath); + else + BuildWithILPack(extension, outputPath); + + return new ExtensionAssemblyInfo(extension.Name, outputPath, isReloading: false); + } + + private void BuildWithRoslyn(ParsedExtension extension, string outputPath) + { + var generator = new RoslynCommandTypeGenerator(); + string code = generator.GenerateExtensionCode(extension); + File.WriteAllText(Path.Combine(Path.GetDirectoryName(outputPath), $"{extension.Name}.cs"), code); + + var tree = CSharpSyntaxTree.ParseText(code); + var compilation = CSharpCompilation.Create( + Path.GetFileNameWithoutExtension(outputPath), + new[] { tree }, + ResolveRoslynReferences(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release)); + + using var fs = new FileStream(outputPath, FileMode.Create); + var result = compilation.Emit(fs); + if (!result.Success) + { + HandleCompilationErrors(result.Diagnostics); + throw new Exception("Roslyn compilation failed."); + } + } + + private void BuildWithILPack(ParsedExtension extension, string outputPath) + { + // Load runtime for dependecy (Probably temparary due to future implementation of env loader in C#) + var loaderDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var twoUp = Path.GetFullPath(Path.Combine(loaderDir, "..", "..")); + var runtimeName = $"PyRevitLabs.PyRevit.Runtime.{_revitVersion}.dll"; + var runtimePath = Directory + .EnumerateFiles(loaderDir, runtimeName, SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + + if (runtimePath != null) + { + Assembly.LoadFrom(runtimePath); + + } + var generator = new ReflectionEmitCommandTypeGenerator(); + var asmName = new AssemblyName(extension.Name) { Version = new Version(1, 0, 0, 0) }; + string moduleName = Path.GetFileNameWithoutExtension(outputPath); + +#if NETFRAMEWORK + var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( + asmName, AssemblyBuilderAccess.RunAndSave, Path.GetDirectoryName(outputPath)); + var moduleBuilder = asmBuilder.DefineDynamicModule(moduleName, Path.GetFileName(outputPath)); +#else + var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run); + var moduleBuilder = asmBuilder.DefineDynamicModule(moduleName); +#endif + + foreach (var cmd in extension.CollectCommandComponents()) + generator.DefineCommandType(extension, cmd, moduleBuilder); + +#if NETFRAMEWORK + asmBuilder.Save(Path.GetFileName(outputPath)); +#else + new AssemblyGenerator().GenerateAssembly(asmBuilder, outputPath); +#endif + } + + private List ResolveRoslynReferences() + { + string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var refs = new List + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), + MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPI.dll")), + MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPIUI.dll")), + MetadataReference.CreateFromFile(Path.Combine(baseDir, $"PyRevitLabs.PyRevit.Runtime.{_revitVersion}.dll")) + }; + string sys = Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll"); + if (File.Exists(sys)) refs.Add(MetadataReference.CreateFromFile(sys)); + return refs; + } + + private static void HandleCompilationErrors(IEnumerable diagnostics) + { + Console.WriteLine("=== Diagnostics ==="); + foreach (var d in diagnostics) + { + Console.WriteLine($"{d.Severity} {d.Id}: {d.GetMessage()}"); + if (d.Location != Location.None) + Console.WriteLine($"Location: {d.Location.GetLineSpan()}"); + } + } + + // TODO: Implement a proper hashing module + private static string GetStableHash(string input) + { + using var sha1 = System.Security.Cryptography.SHA1.Create(); + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input)); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + + public void LoadAssembly(ExtensionAssemblyInfo info) + { + if (!File.Exists(info.Location)) + throw new FileNotFoundException("Assembly not found", info.Location); + Assembly.LoadFrom(info.Location); + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs new file mode 100644 index 000000000..f5b7ab61b --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/CommandTypeGenerator.cs @@ -0,0 +1,253 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Reflection.Emit; +using Autodesk.Revit.Attributes; +using pyRevitExtensionParser; +#if !NETFRAMEWORK +using System.Runtime.Loader; +#endif + + +namespace pyRevitAssemblyBuilder.AssemblyMaker +{ + /// + /// Generates C# code for commands via Roslyn, plus availability classes. + /// + public class RoslynCommandTypeGenerator + { + public string GenerateExtensionCode(ParsedExtension extension) + { + var sb = new StringBuilder(); + sb.AppendLine("#nullable disable"); + sb.AppendLine("using Autodesk.Revit.Attributes;"); + sb.AppendLine("using PyRevitLabs.PyRevit.Runtime;"); + sb.AppendLine(); + + foreach (var cmd in extension.CollectCommandComponents()) + { + string safeClassName = SanitizeClassName(cmd.UniqueId); + string scriptPath = cmd.ScriptPath; + string searchPaths = string.Join(";", new[] + { + Path.GetDirectoryName(cmd.ScriptPath), + Path.Combine(extension.Directory, "lib"), + Path.Combine(extension.Directory, "..", "..", "pyrevitlib"), + Path.Combine(extension.Directory, "..", "..", "site-packages") + }); + string tooltip = cmd.Tooltip ?? string.Empty; + string bundle = Path.GetFileName(Path.GetDirectoryName(cmd.ScriptPath)); + string extName = extension.Name; + string ctrlId = $"CustomCtrl_%{extName}%{bundle}%{cmd.Name}"; + string engineCfgs = "{\"clean\": false, \"persistent\": false, \"full_frame\": false}"; + + // — Command class — + sb.AppendLine("[Regeneration(RegenerationOption.Manual)]"); + sb.AppendLine("[Transaction(TransactionMode.Manual)]"); + sb.AppendLine($"public class {safeClassName} : ScriptCommand"); + sb.AppendLine("{"); + sb.AppendLine($" public {safeClassName}() : base("); + sb.AppendLine($" @\"{EscapeForVerbatim(scriptPath)}\","); + sb.AppendLine($" @\"{EscapeForVerbatim(scriptPath)}\","); + sb.AppendLine($" @\"{EscapeForVerbatim(searchPaths)}\","); + sb.AppendLine($" \"\","); + sb.AppendLine($" \"\","); + sb.AppendLine($" @\"{EscapeForVerbatim(tooltip)}\","); + sb.AppendLine($" \"{Escape(cmd.Name)}\","); + sb.AppendLine($" \"{Escape(bundle)}\","); + sb.AppendLine($" \"{Escape(extName)}\","); + sb.AppendLine($" \"{cmd.UniqueId}\","); + sb.AppendLine($" \"{Escape(ctrlId)}\","); + sb.AppendLine($" \"(zero-doc)\","); + sb.AppendLine($" \"{Escape(engineCfgs)}\""); + sb.AppendLine(" )"); + sb.AppendLine(" {"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + sb.AppendLine(); + + // — Availability class — + sb.AppendLine($"public class {safeClassName}_avail : ScriptCommandExtendedAvail"); + sb.AppendLine("{"); + sb.AppendLine($" public {safeClassName}_avail() : base(\"(zero-doc)\")"); + sb.AppendLine(" {"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + sb.AppendLine(); + } + + return sb.ToString(); + } + + private static string SanitizeClassName(string name) + { + var sb = new StringBuilder(); + foreach (char c in name) + sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + return sb.ToString(); + } + + private static string EscapeForVerbatim(string str) => + (str ?? string.Empty).Replace("\"", "\"\""); + + private static string Escape(string str) => + (str ?? string.Empty) + .Replace("\\", "\\\\") + .Replace("\"", "\\\""); + } + + /// + /// Generates command types via Reflection.Emit and packs via ILPack, + /// plus availability types via the runtime's ScriptCommandExtendedAvail. + /// + public class ReflectionEmitCommandTypeGenerator + { + private const string RuntimeNamePrefix = "PyRevitLabs.PyRevit.Runtime"; + + private static readonly Assembly _runtimeAsm; + private static readonly Type _scriptCommandType; + private static readonly ConstructorInfo _scriptCommandCtor; + private static readonly Type _extendedAvailType; + private static readonly ConstructorInfo _extendedAvailCtor; + + static ReflectionEmitCommandTypeGenerator() + { + // 1) Locate or load the runtime assembly (PyRevitLabs.PyRevit.Runtime.*.dll) + _runtimeAsm = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name.StartsWith(RuntimeNamePrefix, StringComparison.OrdinalIgnoreCase)); + + if (_runtimeAsm == null) + { + var loaderDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var probeDir = Path.GetFullPath(Path.Combine(loaderDir, "..", "..")); + var candidate = Directory.EnumerateFiles(probeDir, $"{RuntimeNamePrefix}.*.dll", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + if (candidate != null) + { +#if NETFRAMEWORK + _runtimeAsm = Assembly.LoadFrom(candidate); +#else + _runtimeAsm = AssemblyLoadContext.Default.LoadFromAssemblyPath(candidate); +#endif + } + } + + if (_runtimeAsm == null) + throw new InvalidOperationException($"Could not load any assembly named {RuntimeNamePrefix}.*.dll"); + + // 2) Resolve ScriptCommand and its 13-string ctor + _scriptCommandType = _runtimeAsm.GetType("PyRevitLabs.PyRevit.Runtime.ScriptCommand") + ?? throw new InvalidOperationException("ScriptCommand type not found."); + var stringParams = Enumerable.Repeat(typeof(string), 13).ToArray(); + _scriptCommandCtor = _scriptCommandType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, stringParams, null) + ?? throw new InvalidOperationException("ScriptCommand constructor not found."); + + // 3) Resolve ScriptCommandExtendedAvail and its single-string ctor + _extendedAvailType = _runtimeAsm.GetType("PyRevitLabs.PyRevit.Runtime.ScriptCommandExtendedAvail") + ?? throw new InvalidOperationException("ScriptCommandExtendedAvail type not found."); + _extendedAvailCtor = _extendedAvailType.GetConstructor(new[] { typeof(string) }) + ?? throw new InvalidOperationException("ScriptCommandExtendedAvail(string) ctor not found."); + } + + /// + /// Defines both the ScriptCommand-derived class and its matching _avail class. + /// + public void DefineCommandType(ParsedExtension extension, ParsedComponent cmd, ModuleBuilder moduleBuilder) + { + // 1) Generate the ScriptCommand type + var typeName = SanitizeClassName(cmd.UniqueId); + var tb = moduleBuilder.DefineType( + typeName, + TypeAttributes.Public | TypeAttributes.Class, + _scriptCommandType); + + // [Regeneration] and [Transaction] attributes + var regenCtor = typeof(RegenerationAttribute) + .GetConstructor(new[] { typeof(RegenerationOption) })!; + tb.SetCustomAttribute(new CustomAttributeBuilder(regenCtor, new object[] { RegenerationOption.Manual })); + + var transCtor = typeof(TransactionAttribute) + .GetConstructor(new[] { typeof(TransactionMode) })!; + tb.SetCustomAttribute(new CustomAttributeBuilder(transCtor, new object[] { TransactionMode.Manual })); + + // Parameterless ctor + var ctor = tb.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + Type.EmptyTypes); + var il = ctor.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + + // Prepare the 13 args + string scriptPath = cmd.ScriptPath ?? string.Empty; + string configPath = cmd.ScriptPath ?? string.Empty; + string searchPaths = string.Join(";", new[] + { + Path.GetDirectoryName(cmd.ScriptPath), + Path.Combine(extension.Directory, "lib"), + Path.Combine(extension.Directory, "..", "..", "pyrevitlib"), + Path.Combine(extension.Directory, "..", "..", "site-packages") + }); + string[] args = { + scriptPath, + configPath, + searchPaths, + "", + "", + cmd.Tooltip ?? string.Empty, + cmd.Name, + Path.GetFileName(Path.GetDirectoryName(cmd.ScriptPath)), + extension.Name, + cmd.UniqueId, + $"CustomCtrl_%{extension.Name}%{Path.GetFileName(Path.GetDirectoryName(cmd.ScriptPath))}%{cmd.Name}", + "(zero-doc)", + "{\"clean\":false,\"persistent\":false,\"full_frame\":false}" + }; + foreach (var a in args) il.Emit(OpCodes.Ldstr, a); + + il.Emit(OpCodes.Call, _scriptCommandCtor); + il.Emit(OpCodes.Ret); + + tb.CreateType(); + + // 2) Generate the matching _avail type + DefineAvailabilityType(moduleBuilder, cmd); + } + + private void DefineAvailabilityType(ModuleBuilder moduleBuilder, ParsedComponent cmd) + { + var availName = SanitizeClassName(cmd.UniqueId) + "_avail"; + var atb = moduleBuilder.DefineType( + availName, + TypeAttributes.Public | TypeAttributes.Class, + _extendedAvailType); + + // Parameterless ctor for ExtendedAvail + var ctor = atb.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + Type.EmptyTypes); + var il = ctor.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, "(zero-doc)"); + il.Emit(OpCodes.Call, _extendedAvailCtor); + il.Emit(OpCodes.Ret); + + atb.CreateType(); + } + + private static string SanitizeClassName(string name) + { + var sb = new StringBuilder(); + foreach (char c in name) + sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + return sb.ToString(); + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/ExtensionAssemblyInfo.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/ExtensionAssemblyInfo.cs new file mode 100644 index 000000000..19258f134 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/AssemblyMaker/ExtensionAssemblyInfo.cs @@ -0,0 +1,21 @@ +namespace pyRevitAssemblyBuilder.AssemblyMaker +{ + public class ExtensionAssemblyInfo + { + public string Name { get; } + public string Location { get; } + public bool IsReloading { get; } + + public ExtensionAssemblyInfo(string name, string location, bool isReloading) + { + Name = name; + Location = location; + IsReloading = isReloading; + } + + public override string ToString() + { + return $"{Name} ({(IsReloading ? "Reloaded" : "Fresh")}) -> {Location}"; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs new file mode 100644 index 000000000..0ea0a6ebc --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using pyRevitExtensionParser; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitAssemblyBuilder.SessionManager +{ + public class ExtensionManagerService + { + public IEnumerable GetInstalledExtensions() + { + var installedExtensions = ExtensionParser.ParseInstalledExtensions(); + //Console.WriteLine(installedExtensions); + return installedExtensions; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/HookManager.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/HookManager.cs new file mode 100644 index 000000000..db4ff893d --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/HookManager.cs @@ -0,0 +1,48 @@ +using pyRevitExtensionParser; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace pyRevitAssemblyBuilder.SessionManager +{ + public class HookManager + { + public void RegisterHooks(ParsedExtension extension) + { + if (extension == null) + return; + + var hooks = GetHookScripts(extension); + var checks = GetCheckScripts(extension); + + foreach (var hook in hooks) + { + Console.WriteLine($"[pyRevit] Found hook script: {hook}"); + } + + foreach (var check in checks) + { + Console.WriteLine($"[pyRevit] Found check script: {check}"); + } + + // Future: implement actual execution logic for scripts if needed + } + + private IEnumerable GetHookScripts(ParsedExtension extension) + { + var hooksPath = Path.Combine(extension.Directory, "hooks"); + return Directory.Exists(hooksPath) + ? Directory.GetFiles(hooksPath) + : Enumerable.Empty(); + } + + private IEnumerable GetCheckScripts(ParsedExtension extension) + { + var checksPath = Path.Combine(extension.Directory, "checks"); + return Directory.Exists(checksPath) + ? Directory.GetFiles(checksPath) + : Enumerable.Empty(); + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/SessionManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/SessionManagerService.cs new file mode 100644 index 000000000..8183e6197 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/SessionManagerService.cs @@ -0,0 +1,37 @@ +using pyRevitAssemblyBuilder.AssemblyMaker; + +namespace pyRevitAssemblyBuilder.SessionManager +{ + public class SessionManagerService + { + private readonly AssemblyBuilderService _assemblyBuilder; + private readonly ExtensionManagerService _extensionManager; + private readonly HookManager _hookManager; + private readonly UIManagerService _uiManager; + + public SessionManagerService( + AssemblyBuilderService assemblyBuilder, + ExtensionManagerService extensionManager, + HookManager hookManager, + UIManagerService uiManager) + { + _assemblyBuilder = assemblyBuilder; + _extensionManager = extensionManager; + _hookManager = hookManager; + _uiManager = uiManager; + } + + public void LoadSession() + { + var extensions = _extensionManager.GetInstalledExtensions(); + + foreach (var ext in extensions) + { + var assmInfo = _assemblyBuilder.BuildExtensionAssembly(ext); + _assemblyBuilder.LoadAssembly(assmInfo); + _uiManager.BuildUI(ext, assmInfo); + _hookManager.RegisterHooks(ext); + } + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/UIManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/UIManagerService.cs new file mode 100644 index 000000000..0095e625f --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/UIManagerService.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Revit.UI; +using pyRevitAssemblyBuilder.AssemblyMaker; +using Autodesk.Windows; +using RibbonPanel = Autodesk.Revit.UI.RibbonPanel; +using RibbonButton = Autodesk.Windows.RibbonButton; +using RibbonItem = Autodesk.Revit.UI.RibbonItem; +using static pyRevitExtensionParser.ExtensionParser; +using pyRevitExtensionParser; + +namespace pyRevitAssemblyBuilder.SessionManager +{ + public class UIManagerService + { + private readonly UIApplication _uiApp; + + public UIManagerService(UIApplication uiApp) + { + _uiApp = uiApp; + } + + public void BuildUI(ParsedExtension extension, ExtensionAssemblyInfo assemblyInfo) + { + if (extension?.Children == null) + return; + + foreach (var component in extension.Children) + RecursivelyBuildUI(component, null, null, extension.Name, assemblyInfo); + } + + private void RecursivelyBuildUI( + ParsedComponent component, + ParsedComponent parentComponent, + RibbonPanel parentPanel, + string tabName, + ExtensionAssemblyInfo assemblyInfo) + { + switch (component.Type) + { + case CommandComponentType.Tab: + try { _uiApp.CreateRibbonTab(component.DisplayName); } catch { } + foreach (var child in component.Children ?? Enumerable.Empty()) + RecursivelyBuildUI(child, component, null, component.DisplayName, assemblyInfo); + break; + + case CommandComponentType.Panel: + var panel = _uiApp.GetRibbonPanels(tabName) + .FirstOrDefault(p => p.Name == component.DisplayName) + ?? _uiApp.CreateRibbonPanel(tabName, component.DisplayName); + foreach (var child in component.Children ?? Enumerable.Empty()) + RecursivelyBuildUI(child, component, panel, tabName, assemblyInfo); + break; + + default: + if (component.HasSlideout) + { + EnsureSlideOutApplied(parentComponent, parentPanel); + } + HandleComponentBuilding(component, parentPanel, tabName, assemblyInfo); + break; + } + } + + private void EnsureSlideOutApplied(ParsedComponent parentComponent,RibbonPanel parentPanel) + { + if (parentPanel != null && parentComponent.Type == CommandComponentType.Panel) + { + try { parentPanel.AddSlideOut(); } catch { } + } + } + + private void HandleComponentBuilding( + ParsedComponent component, + RibbonPanel parentPanel, + string tabName, + ExtensionAssemblyInfo assemblyInfo) + { + switch (component.Type) + { + case CommandComponentType.Stack: + BuildStack(component, parentPanel, assemblyInfo); + break; + case CommandComponentType.PanelButton: + var panelBtnData = CreatePushButton(component, assemblyInfo); + var panelBtn = parentPanel.AddItem(panelBtnData) as PushButton; + if (!string.IsNullOrEmpty(component.Tooltip)) + panelBtn.ToolTip = component.Tooltip; + ModifyToPanelButton(tabName, parentPanel, panelBtn); + break; + case CommandComponentType.PushButton: + case CommandComponentType.SmartButton: + var pbData = CreatePushButton(component, assemblyInfo); + var btn = parentPanel.AddItem(pbData) as PushButton; + if (!string.IsNullOrEmpty(component.Tooltip)) + btn.ToolTip = component.Tooltip; + break; + + case CommandComponentType.PullDown: + CreatePulldown(component, parentPanel, tabName, assemblyInfo, true); + break; + + case CommandComponentType.SplitButton: + case CommandComponentType.SplitPushButton: + var splitData = new SplitButtonData(component.UniqueId, component.DisplayName); + var splitBtn = parentPanel.AddItem(splitData) as SplitButton; + if (splitBtn != null) + { + // Assign tooltip to the split button itself + if (!string.IsNullOrEmpty(component.Tooltip)) + splitBtn.ToolTip = component.Tooltip; + + foreach (var sub in component.Children ?? Enumerable.Empty()) + { + if (sub.Type == CommandComponentType.PushButton) + { + var subBtn = splitBtn.AddPushButton(CreatePushButton(sub, assemblyInfo)); + if (!string.IsNullOrEmpty(sub.Tooltip)) + subBtn.ToolTip = sub.Tooltip; + } + } + } + break; + } + } + + private void ModifyToPanelButton(string tabName, RibbonPanel parentPanel, PushButton panelBtn) + { + try + { + var adwTab = ComponentManager + .Ribbon + .Tabs + .FirstOrDefault(t => t.Id == tabName); + var adwPanel = adwTab + .Panels + .First(p => p.Source.Title == parentPanel.Name); + var adwBtn = adwPanel + .Source + .Items + .First(i => i.AutomationName == panelBtn.ItemText); + adwPanel.Source.Items.Remove(adwBtn); + adwPanel.Source.DialogLauncher = (RibbonButton)adwBtn; + } + catch (Exception ex) + { + Console.WriteLine("Failed modify PushButton to PanelButton"); + Console.WriteLine(ex.Message); + } + } + + private void BuildStack( + ParsedComponent component, + RibbonPanel parentPanel, + ExtensionAssemblyInfo assemblyInfo) + { + var itemDataList = new List(); + var originalItems = new List(); + + foreach (var child in component.Children ?? Enumerable.Empty()) + { + if (child.Type == CommandComponentType.PushButton || + child.Type == CommandComponentType.SmartButton) + { + itemDataList.Add(CreatePushButton(child, assemblyInfo)); + originalItems.Add(child); + } + else if (child.Type == CommandComponentType.PullDown) + { + var pdData = new PulldownButtonData(child.UniqueId, child.DisplayName); + itemDataList.Add(pdData); + originalItems.Add(child); + } + } + + if (itemDataList.Count >= 2) + { + IList stackedItems = null; + if (itemDataList.Count == 2) + stackedItems = parentPanel.AddStackedItems(itemDataList[0], itemDataList[1]); + else + stackedItems = parentPanel.AddStackedItems(itemDataList[0], itemDataList[1], itemDataList[2]); + + if (stackedItems != null) + { + for (int i = 0; i < stackedItems.Count; i++) + { + var ribbonItem = stackedItems[i]; + var origComponent = originalItems[i]; + + // Assign tooltip to push buttons in stack + if (ribbonItem is PushButton pushBtn && !string.IsNullOrEmpty(origComponent.Tooltip)) + { + pushBtn.ToolTip = origComponent.Tooltip; + } + + if (ribbonItem is PulldownButton pdBtn) + { + // Assign tooltip to the pulldown button itself in stack + if (!string.IsNullOrEmpty(origComponent.Tooltip)) + pdBtn.ToolTip = origComponent.Tooltip; + + foreach (var sub in origComponent.Children ?? Enumerable.Empty()) + { + if (sub.Type == CommandComponentType.PushButton) + { + var subBtn = pdBtn.AddPushButton(CreatePushButton(sub, assemblyInfo)); + if (!string.IsNullOrEmpty(sub.Tooltip)) + subBtn.ToolTip = sub.Tooltip; + } + } + } + } + } + } + } + + private PulldownButtonData CreatePulldown( + ParsedComponent component, + RibbonPanel parentPanel, + string tabName, + ExtensionAssemblyInfo assemblyInfo, + bool addToPanel) + { + var pdData = new PulldownButtonData(component.UniqueId, component.DisplayName); + if (!addToPanel) return pdData; + + var pdBtn = parentPanel.AddItem(pdData) as PulldownButton; + if (pdBtn == null) return null; + + // Assign tooltip to the pulldown button itself + if (!string.IsNullOrEmpty(component.Tooltip)) + pdBtn.ToolTip = component.Tooltip; + + foreach (var sub in component.Children ?? Enumerable.Empty()) + { + if (sub.Type == CommandComponentType.PushButton) + { + var subBtn = pdBtn.AddPushButton(CreatePushButton(sub, assemblyInfo)); + if (!string.IsNullOrEmpty(sub.Tooltip)) + subBtn.ToolTip = sub.Tooltip; + } + } + return pdData; + } + + private PushButtonData CreatePushButton(ParsedComponent component, ExtensionAssemblyInfo assemblyInfo) + { + return new PushButtonData( + component.UniqueId, + component.DisplayName, + assemblyInfo.Location, + component.UniqueId); + } + } +} diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj b/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj new file mode 100644 index 000000000..26bd82a1a --- /dev/null +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj @@ -0,0 +1,17 @@ + + + true + true + pyRevitAssemblyBuilder + pyRevitAssemblyBuilder + true + false + true + net48;net8.0-windows + IPY342 + 3.4.2 + true + 10.0 + true + + \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/BundleParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/BundleParser.cs new file mode 100644 index 000000000..89b59d033 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/BundleParser.cs @@ -0,0 +1,249 @@ +using System.Collections.Generic; +using System.IO; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParser +{ + public class BundleParser + { + public class ParsedBundle + { + public List LayoutOrder { get; set; } = new List(); + public Dictionary Titles { get; set; } = new Dictionary(); + public Dictionary Tooltips { get; set; } = new Dictionary(); + public string Author { get; set; } + public string MinRevitVersion { get; set; } + public EngineConfig Engine { get; set; } = new EngineConfig(); + } + + public static class BundleYamlParser + { + public static ParsedBundle Parse(string filePath) + { + var parsed = new ParsedBundle(); + var lines = File.ReadAllLines(filePath); + string currentSection = null; + string currentLanguageKey = null; + bool isInMultilineValue = false; + bool isLiteralMultiline = false; // |- + bool isFoldedMultiline = false; // >- + var multilineContent = new List(); + + for (int i = 0; i < lines.Length; i++) + { + var raw = lines[i]; + var line = raw.Trim(); + + // Skip empty lines (but preserve them in multiline content) + if (string.IsNullOrWhiteSpace(line)) + { + if (isInMultilineValue) + { + multilineContent.Add(""); + } + continue; + } + + // Handle multiline content continuation + if (isInMultilineValue) + { + // Content must be indented more than the language key (more than 2 spaces) + if (raw.StartsWith(" ") || raw.StartsWith("\t\t")) + { + var content = raw.TrimStart(); + multilineContent.Add(content); + continue; + } + else + { + // End of multiline - process accumulated content + FinishMultilineValue(parsed, currentSection, currentLanguageKey, multilineContent, + isLiteralMultiline, isFoldedMultiline); + + // Reset multiline state + isInMultilineValue = false; + isLiteralMultiline = false; + isFoldedMultiline = false; + multilineContent.Clear(); + currentLanguageKey = null; + + // Continue to process the current line that ended the multiline + } + } + + // Top-level sections (no indentation) + if (!raw.StartsWith(" ") && !raw.StartsWith("\t") && line.Contains(":")) + { + var colonIndex = line.IndexOf(':'); + currentSection = line.Substring(0, colonIndex).Trim().ToLowerInvariant(); + var value = line.Substring(colonIndex + 1).Trim(); + + switch (currentSection) + { + case "author": + parsed.Author = value; + break; + case "min_revit_version": + parsed.MinRevitVersion = value; + break; + case "title": + case "tooltip": + case "layout": + case "engine": + // These sections have nested content + break; + } + continue; + } + + // Second-level items (indented with 2 spaces or 1 tab) + if ((raw.StartsWith(" ") && !raw.StartsWith(" ")) || + (raw.StartsWith("\t") && !raw.StartsWith("\t\t"))) + { + if (currentSection == "layout" && line.StartsWith("-")) + { + // Layout list item + var item = line.Substring(1).Trim(); + if ((item.StartsWith("\"") && item.EndsWith("\"")) || + (item.StartsWith("'") && item.EndsWith("'"))) + { + item = item.Substring(1, item.Length - 2); + } + parsed.LayoutOrder.Add(item); + } + else if ((currentSection == "title" || currentSection == "tooltip") && line.Contains(":")) + { + // Language-specific title or tooltip + var colonIndex = line.IndexOf(':'); + currentLanguageKey = line.Substring(0, colonIndex).Trim(); + var value = line.Substring(colonIndex + 1).Trim(); + + if (value == "|-") + { + // Literal multiline (preserve line breaks) + isInMultilineValue = true; + isLiteralMultiline = true; + } + else if (value == ">-") + { + // Folded multiline (join lines) + isInMultilineValue = true; + isFoldedMultiline = true; + } + else if (value == "|" || value == ">") + { + // Legacy multiline + isInMultilineValue = true; + } + else if (!string.IsNullOrEmpty(value)) + { + // Single-line value + if (currentSection == "title") + parsed.Titles[currentLanguageKey] = value; + else if (currentSection == "tooltip") + parsed.Tooltips[currentLanguageKey] = value; + } + else + { + // Empty value after colon - might be implicit multiline + isInMultilineValue = true; + } + } + else if (currentSection == "engine" && line.Contains(":")) + { + // Engine configuration + var colonIndex = line.IndexOf(':'); + var key = line.Substring(0, colonIndex).Trim().ToLowerInvariant(); + var value = line.Substring(colonIndex + 1).Trim().ToLowerInvariant(); + + switch (key) + { + case "clean": + parsed.Engine.Clean = value == "true"; + break; + case "full_frame": + parsed.Engine.FullFrame = value == "true"; + break; + case "persistent": + parsed.Engine.Persistent = value == "true"; + break; + } + } + } + } + + // Handle any remaining multiline content at end of file + if (isInMultilineValue && multilineContent.Count > 0) + { + FinishMultilineValue(parsed, currentSection, currentLanguageKey, multilineContent, + isLiteralMultiline, isFoldedMultiline); + } + + return parsed; + } + + private static void FinishMultilineValue(ParsedBundle parsed, string section, string languageKey, + List content, bool isLiteral, bool isFolded) + { + if (content.Count == 0 || string.IsNullOrEmpty(languageKey) || string.IsNullOrEmpty(section)) + return; + + var processedValue = ProcessMultilineValue(content, isLiteral, isFolded); + + if (section == "title") + parsed.Titles[languageKey] = processedValue; + else if (section == "tooltip") + parsed.Tooltips[languageKey] = processedValue; + } + + private static string ProcessMultilineValue(List lines, bool isLiteral, bool isFolded) + { + if (lines.Count == 0) + return string.Empty; + + if (isLiteral) + { + // Literal style: preserve line breaks + return string.Join("\n", lines).TrimEnd('\n'); + } + else if (isFolded) + { + // Folded style: join lines with spaces, preserve paragraph breaks + var result = new List(); + var currentParagraph = new List(); + + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + { + // Empty line - end current paragraph + if (currentParagraph.Count > 0) + { + result.Add(string.Join(" ", currentParagraph)); + currentParagraph.Clear(); + } + result.Add(""); + } + else + { + currentParagraph.Add(line.Trim()); + } + } + + // Add final paragraph + if (currentParagraph.Count > 0) + { + result.Add(string.Join(" ", currentParagraph)); + } + + return string.Join("\n", result).Trim(); + } + else + { + // Default: join with newlines + return string.Join("\n", lines); + } + } + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs new file mode 100644 index 000000000..9f1e9ed4c --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using static pyRevitExtensionParser.BundleParser; + +namespace pyRevitExtensionParser +{ + public static class ExtensionParser + { + public static IEnumerable ParseInstalledExtensions() + { + PyRevitConfig config = PyRevitConfig.Load(); + List extensionRoots = GetExtensionRoots(); + extensionRoots.AddRange(config.UserExtensionsList); + + // TODO check if they are activated in the config + // ParseExtensionByName + + foreach (var root in extensionRoots) + { + if (!Directory.Exists(root)) + continue; + + foreach (var extDir in Directory.GetDirectories(root, "*.extension")) + { + var extName = Path.GetFileNameWithoutExtension(extDir); + var children = ParseComponents(extDir, extName); + + var bundlePath = Path.Combine(extDir, "bundle.yaml"); + ParsedBundle parsedBundle = File.Exists(bundlePath) + ? BundleYamlParser.Parse(bundlePath) + : null; + + var parsedExtension = new ParsedExtension + { + Name = extName, + Directory = extDir, + Children = children, + LayoutOrder = parsedBundle?.LayoutOrder, + Titles = parsedBundle?.Titles, + Tooltips = parsedBundle?.Tooltips, + MinRevitVersion = parsedBundle?.MinRevitVersion, + Engine = parsedBundle?.Engine + }; + + ReorderByLayout(parsedExtension); + + yield return parsedExtension; + } + } + } + + /// + /// Recursively reorders the given component’s Children in-place + /// according to its own LayoutOrder. If LayoutOrder is null or empty, + /// we skip sorting here but still recurse into children. + /// + private static void ReorderByLayout(ParsedComponent component) + { + if (component.LayoutOrder != null && component.LayoutOrder.Count > 0) + { + + var nameIndexMap = component.LayoutOrder + .Select((name, index) => new { name, index }) + .GroupBy(x => x.name) + .ToDictionary(g => g.Key, g => g.First().index); + + component.Children.Sort((a, b) => + { + int ix = nameIndexMap.TryGetValue(a.DisplayName, out int indexA) ? indexA : int.MaxValue; + int iy = nameIndexMap.TryGetValue(b.DisplayName, out int indexB) ? indexB : int.MaxValue; + return ix.CompareTo(iy); + }); + + var slideoutIndex = component.LayoutOrder.IndexOf(">>>>>"); + + if (slideoutIndex >= 0) + { + var nextelem = component.LayoutOrder[slideoutIndex + 1]; + component.Children.Find(c => c.Name == nextelem).HasSlideout = true; + } + } + + foreach (var child in component.Children) + { + ReorderByLayout(child); + } + } + + private static List GetExtensionRoots() + { + var roots = new List(); + + var current = Path.GetDirectoryName(typeof(ExtensionParser).Assembly.Location); + var defaultPath = Path.GetFullPath(Path.Combine(current, "..", "..", "..", "..", "extensions")); + + // Monkey patch for testing bench + if (!Directory.Exists(defaultPath)) + { + defaultPath = Path.Combine(current, "..", "..", "..", "..", "..", "..", "extensions"); + } + + roots.Add(defaultPath); + + var configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "pyRevit", + "pyRevit_config.ini"); + + if (File.Exists(configPath)) + { + foreach (var line in File.ReadAllLines(configPath)) + { + if (line.StartsWith("userextensions =", StringComparison.OrdinalIgnoreCase)) + { + var parts = line.Substring("userextensions =".Length).Split(';'); + foreach (var part in parts) + { + var path = part.Trim(); + if (!string.IsNullOrWhiteSpace(path)) + roots.Add(path); + } + } + } + } + + return roots; + } + + private static List ParseComponents( + string baseDir, + string extensionName, + string parentPath = null) + { + var components = new List(); + + foreach (var dir in Directory.GetDirectories(baseDir)) + { + var ext = Path.GetExtension(dir); + var componentType = CommandComponentTypeExtensions.FromExtension(ext); + if (componentType == CommandComponentType.Unknown) + continue; + + var namePart = Path.GetFileNameWithoutExtension(dir).Replace(" ", ""); + var displayName = Path.GetFileNameWithoutExtension(dir); + var fullPath = string.IsNullOrEmpty(parentPath) + ? $"{extensionName}_{namePart}" + : $"{parentPath}_{namePart}"; + + string scriptPath = null; + + if (componentType == CommandComponentType.UrlButton) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (File.Exists(yaml)) + scriptPath = yaml; + } + + if (scriptPath == null) + { + scriptPath = Directory + .EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly) + .FirstOrDefault(f => f.EndsWith("script.py", StringComparison.OrdinalIgnoreCase)); + } + + if (scriptPath == null && + (componentType == CommandComponentType.PushButton || + componentType == CommandComponentType.SmartButton || + componentType == CommandComponentType.PullDown || + componentType == CommandComponentType.SplitButton || + componentType == CommandComponentType.SplitPushButton)) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (File.Exists(yaml)) + scriptPath = yaml; + } + + var bundleFile = Path.Combine(dir, "bundle.yaml"); + var children = ParseComponents(dir, extensionName, fullPath); + + // First, get values from Python script + string title = null, author = null, doc = null; + if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + (title, author, doc) = ReadPythonScriptConstants(scriptPath); + } + + // Then parse bundle and override with bundle values if they exist + var bundleInComponent = File.Exists(bundleFile) ? BundleYamlParser.Parse(bundleFile) : null; + + // Override script values with bundle values (bundle takes precedence) + if (bundleInComponent != null) + { + // Use en_us as default locale, fallback to first available, then to script values + var bundleTitle = GetLocalizedValue(bundleInComponent.Titles, "en_us"); + var bundleTooltip = GetLocalizedValue(bundleInComponent.Tooltips, "en_us"); + + if (!string.IsNullOrEmpty(bundleTitle)) + title = bundleTitle; + + if (!string.IsNullOrEmpty(bundleTooltip)) + doc = bundleTooltip; + + if (!string.IsNullOrEmpty(bundleInComponent.Author)) + author = bundleInComponent.Author; + } + + components.Add(new ParsedComponent + { + Name = namePart, + DisplayName = displayName, + ScriptPath = scriptPath, + Tooltip = doc ?? $"Command: {namePart}", // Set Tooltip from bundle -> __doc__ -> fallback + UniqueId = SanitizeClassName(fullPath.ToLowerInvariant()), + Type = componentType, + Children = children, + BundleFile = File.Exists(bundleFile) ? bundleFile : null, + LayoutOrder = bundleInComponent?.LayoutOrder, + Title = title, + Author = author + }); + } + + return components; + } + + /// + /// Gets a localized value from a dictionary, falling back to en_us, then to the first available value + /// + private static string GetLocalizedValue(Dictionary localizedValues, string preferredLocale = "en_us") + { + if (localizedValues == null || localizedValues.Count == 0) + return null; + + // Try preferred locale first + if (localizedValues.TryGetValue(preferredLocale, out string preferredValue)) + return preferredValue; + + // Fallback to en_us if different preferred locale was specified + if (preferredLocale != "en_us" && localizedValues.TryGetValue("en_us", out string enUsValue)) + return enUsValue; + + // Fallback to first available value + return localizedValues.Values.FirstOrDefault(); + } + + private static string SanitizeClassName(string name) + { + var sb = new StringBuilder(); + foreach (char c in name) + sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + return sb.ToString(); + } + + private static (string title, string author, string doc) ReadPythonScriptConstants(string scriptPath) + { + string title = null, author = null, doc = null; + + foreach (var line in File.ReadLines(scriptPath)) + { + if (line.StartsWith("__title__")) + { + title = ExtractPythonConstantValue(line); + } + else if (line.StartsWith("__author__")) + { + author = ExtractPythonConstantValue(line); + } + else if (line.StartsWith("__doc__")) + { + doc = ExtractPythonConstantValue(line); + } + } + + return (title, author, doc); + } + + private static string ExtractPythonConstantValue(string line) + { + var parts = line.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + var value = parts[1].Trim().Trim('\'', '"'); + return value; + } + return null; + } + + public enum CommandComponentType + { + Unknown, + Tab, + Panel, + PushButton, + PullDown, + SplitButton, + SplitPushButton, + Stack, + SmartButton, + PanelButton, + LinkButton, + InvokeButton, + UrlButton, + ContentButton, + NoButton + } + + public static class CommandComponentTypeExtensions + { + public static CommandComponentType FromExtension(string ext) + { + switch (ext.ToLowerInvariant()) + { + case ".tab": return CommandComponentType.Tab; + case ".panel": return CommandComponentType.Panel; + case ".pushbutton": return CommandComponentType.PushButton; + case ".pulldown": return CommandComponentType.PullDown; + case ".splitbutton": return CommandComponentType.SplitButton; + case ".splitpushbutton": return CommandComponentType.SplitPushButton; + case ".stack": return CommandComponentType.Stack; + case ".smartbutton": return CommandComponentType.SmartButton; + case ".panelbutton": return CommandComponentType.PanelButton; + case ".linkbutton": return CommandComponentType.LinkButton; + case ".invokebutton": return CommandComponentType.InvokeButton; + case ".urlbutton": return CommandComponentType.UrlButton; + case ".content": return CommandComponentType.ContentButton; + case ".nobutton": return CommandComponentType.NoButton; + default: return CommandComponentType.Unknown; + } + } + } + public static string ToExtension(this CommandComponentType type) + { + switch (type) + { + case CommandComponentType.Tab: return ".tab"; + case CommandComponentType.Panel: return ".panel"; + case CommandComponentType.PushButton: return ".pushbutton"; + case CommandComponentType.PullDown: return ".pulldown"; + case CommandComponentType.SplitButton: return ".splitbutton"; + case CommandComponentType.SplitPushButton: return ".splitpushbutton"; + case CommandComponentType.Stack: return ".stack"; + case CommandComponentType.SmartButton: return ".smartbutton"; + case CommandComponentType.PanelButton: return ".panelbutton"; + case CommandComponentType.LinkButton: return ".linkbutton"; + case CommandComponentType.InvokeButton: return ".invokebutton"; + case CommandComponentType.UrlButton: return ".urlbutton"; + case CommandComponentType.ContentButton: return ".content"; + case CommandComponentType.NoButton: return ".nobutton"; + default: return string.Empty; + } + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/IniFile.cs b/dev/pyRevitLoader/pyRevitExtensionParser/IniFile.cs new file mode 100644 index 000000000..1061ece2e --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/IniFile.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace pyRevitExtensionParser +{ + internal class IniFile + { + private readonly string _path; + + public IniFile(string iniPath) + { + _path = iniPath; + } + + [DllImport("kernel32")] + private static extern long WritePrivateProfileString(string section, string key, string val, string filePath); + + [DllImport("kernel32")] + private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, int size, string filePath); + + public string IniReadValue(string section, string key) + { + var sb = new StringBuilder(512); + GetPrivateProfileString(section, key, "", sb, sb.Capacity, _path); + return sb.ToString(); + } + + public void IniWriteValue(string section, string key, string value) + { + WritePrivateProfileString(section, key, value, _path); + } + + public IEnumerable GetSections() + { + var sb = new StringBuilder(2048); + GetPrivateProfileString(null, null, null, sb, sb.Capacity, _path); + return sb.ToString().Split(new[] { '\0' }, StringSplitOptions.RemoveEmptyEntries); + } + // Adds a value to the Python-like list in the .ini file + public void AddValueToPythonList(string section, string key, string value) + { + var list = GetPythonList(section, key); + if (!list.Contains(value)) + { + list.Add(value); + SavePythonList(section, key, list); + } + } + + // Removes a value from the Python-like list in the .ini file + public void RemoveValueFromPythonList(string section, string key, string value) + { + var list = GetPythonList(section, key); + if (list.Contains(value)) + { + list.Remove(value); + SavePythonList(section, key, list); + } + } + + // Reads the Python-like list from the .ini file + public List GetPythonList(string section, string key) + { + string pythonListString = IniReadValue(section, key); + return PythonListParser.Parse(pythonListString); + } + + // Saves the Python-like list to the .ini file + public void SavePythonList(string section, string key, List list) + { + string pythonListString = PythonListParser.ToPythonListString(list); + IniWriteValue(section, key, pythonListString); + } + } + public static class PythonListParser + { + // Parses a Python-like list string into a C# List + public static List Parse(string pythonListString) + { + var list = new List(); + var matches = Regex.Matches(pythonListString, @"""([^""]*)"""); + foreach (Match match in matches) + { + list.Add(match.Groups[1].Value.Replace(@"\\", @"\")); + } + return list; + } + + // Converts a C# List back to a Python-like list string + public static string ToPythonListString(List list) + { + return $"[{string.Join(", ", list.ConvertAll(item => $"\"{item.Replace(@"\", @"\\")}\""))}]"; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedComponent.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedComponent.cs new file mode 100644 index 000000000..c83820949 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedComponent.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParser +{ + public class ParsedComponent + { + public string Name { get; set; } + public string DisplayName { get; set; } + public string ScriptPath { get; set; } + public string Tooltip { get; set; } + public string UniqueId { get; set; } + public CommandComponentType Type { get; set; } + public List Children { get; set; } + public string BundleFile { get; set; } + public List LayoutOrder { get; set; } + public bool HasSlideout { get; set; } = false; + public string Title { get; set; } + public string Author { get; set; } + } + +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs new file mode 100644 index 000000000..1e1b35f9c --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParser +{ + public class ParsedExtension : ParsedComponent + { + public string Directory { get; set; } + public Dictionary Titles { get; set; } + public Dictionary Tooltips { get; set; } + public string MinRevitVersion { get; set; } + public EngineConfig Engine { get; set; } + public ExtensionConfig Config { get; set; } + public string GetHash() => Directory.GetHashCode().ToString("X"); + + private static readonly CommandComponentType[] _allowedTypes = new[] { + CommandComponentType.PushButton, + CommandComponentType.PanelButton, + CommandComponentType.SmartButton, + CommandComponentType.UrlButton + }; + + public IEnumerable CollectCommandComponents() + => Collect(this.Children); + + private IEnumerable Collect(IEnumerable list) + { + if (list == null) yield break; + + foreach (var comp in list) + { + if (comp.Children != null) + { + foreach (var child in Collect(comp.Children)) + yield return child; + } + + if (_allowedTypes.Contains(comp.Type)) + yield return comp; + } + } + + } + public class ExtensionConfig + { + public string Name { get; set; } + public bool Disabled { get; set; } + public bool PrivateRepo { get; set; } + public string Username { get; set; } + public string Password { get; set; } + } + + public class EngineConfig + { + public bool Clean { get; set; } + public bool FullFrame { get; set; } + public bool Persistent { get; set; } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs new file mode 100644 index 000000000..96c7b2882 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Windows.Documents; + +namespace pyRevitExtensionParser +{ + public class PyRevitConfig + { + private readonly IniFile _ini; + public string ConfigPath { get; } + + public string UserExtensions + { + get + { + var value = _ini.IniReadValue("core", "userextensions"); + return string.IsNullOrEmpty(value) ? null : value.Trim(); + } + set + { + _ini.IniWriteValue("core", "userextensions", value); + } + } + + public string UserLocale + { + get + { + var value = _ini.IniReadValue("core", "user_locale"); + return string.IsNullOrEmpty(value) ? null : value.Trim(); + } + set + { + _ini.IniWriteValue("core", "user_locale", value); + } + } + + public bool NewLoader + { + get + { + var value = _ini.IniReadValue("core", "new_loader"); + return bool.TryParse(value, out var result) ? result : false; + } + set + { + _ini.IniWriteValue("core", "new_loader", value.ToString().ToLowerInvariant()); + } + } + public bool NewLoaderRoslyn + { + get + { + var value = _ini.IniReadValue("core", "new_loader_roslyn"); + return bool.TryParse(value, out var result) ? result : false; + } + set + { + _ini.IniWriteValue("core", "new_loader_roslyn", value.ToString().ToLowerInvariant()); + } + } + public List UserExtensionsList + { + get + { + var value = _ini.IniReadValue("core", "userextensions"); + return string.IsNullOrEmpty(value) ? new List() : PythonListParser.Parse(value); + } + set + { + _ini.IniWriteValue("core", "userextensions", PythonListParser.ToPythonListString(value)); + } + } + public PyRevitConfig(string configPath) + { + ConfigPath = configPath; + _ini = new IniFile(configPath); + } + + public static PyRevitConfig Load(string customPath = null) + { + string configName = "pyRevit_config.ini"; + string defaultPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "pyRevit", + configName); + + string finalPath = customPath ?? defaultPath; + return new PyRevitConfig(finalPath); + } + + public ExtensionConfig ParseExtensionByName(string extensionName) + { + var sectionPattern = new Regex($"^{extensionName}\\.(extension|lib)$", RegexOptions.IgnoreCase); + + foreach (var section in _ini.GetSections()) + { + if (sectionPattern.IsMatch(section)) + { + return new ExtensionConfig + { + Name = extensionName, + Disabled = bool.TryParse(_ini.IniReadValue(section, "disabled"), out var disabled) && disabled, + PrivateRepo = bool.TryParse(_ini.IniReadValue(section, "private_repo"), out var privateRepo) && privateRepo, + Username = _ini.IniReadValue(section, "username"), + Password = _ini.IniReadValue(section, "password") + }; + } + } + + return null; // Return null if the extension is not found + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj new file mode 100644 index 000000000..2cb0e890f --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj @@ -0,0 +1,26 @@ + + + + true + true + pyRevitExtensionParser + pyRevitExtensionParser + false + false + IPY342 + 3.4.2 + false + false + net48;net8.0-windows + + + + + + + + + + + + diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/DiagnosticTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/DiagnosticTests.cs new file mode 100644 index 000000000..bfb0654f4 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/DiagnosticTests.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Linq; + +namespace pyRevitExtensionParserTest +{ + [TestFixture] + public class DiagnosticTests + { + [Test] + public void DiagnosticPathTest() + { + TestContext.Out.WriteLine("=== Diagnostic Path Information ==="); + TestContext.Out.WriteLine($"Current Directory: {Directory.GetCurrentDirectory()}"); + TestContext.Out.WriteLine($"Test Directory: {TestContext.CurrentContext.TestDirectory}"); + + // Check if Resources directory exists in various locations + var currentDirResources = Path.Combine(Directory.GetCurrentDirectory(), "Resources"); + var testDirResources = Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources"); + + TestContext.Out.WriteLine($"Resources in current dir: {Directory.Exists(currentDirResources)}"); + TestContext.Out.WriteLine($"Resources in test dir: {Directory.Exists(testDirResources)}"); + + if (Directory.Exists(currentDirResources)) + { + TestContext.Out.WriteLine("Files in current dir Resources:"); + var files = Directory.GetFiles(currentDirResources, "*", SearchOption.AllDirectories); + foreach (var file in files.Take(10)) // Limit output + { + TestContext.Out.WriteLine($" {file}"); + } + } + + if (Directory.Exists(testDirResources)) + { + TestContext.Out.WriteLine("Files in test dir Resources:"); + var files = Directory.GetFiles(testDirResources, "*", SearchOption.AllDirectories); + foreach (var file in files.Take(10)) // Limit output + { + TestContext.Out.WriteLine($" {file}"); + } + } + + Assert.Pass("Diagnostic completed"); + } + } +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/ParserBasicTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/ParserBasicTests.cs new file mode 100644 index 000000000..263a4de7c --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/ParserBasicTests.cs @@ -0,0 +1,777 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using pyRevitExtensionParser; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParserTest +{ + [TestFixture] + public class Tests + { + private IEnumerable? _installedExtensions; + + [SetUp] + public void Setup() + { + // Collect all supported by pyRevit extensions + _installedExtensions = ParseInstalledExtensions(); + } + + [Test] + public void ParsingTest() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + TestContext.Out.WriteLine($"Parsed extension: {parsedExtension.Name}"); + } + Assert.Pass("Installed extensions found."); + } + else + { + Assert.Fail("No installed extensions found."); + } + } + + [Test] + public void ParsingExtensionsTest() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintComponentsRecursively(parsedExtension); + } + Assert.Pass("Installed extensions found."); + } + else + { + Assert.Fail("No installed extensions found."); + } + } + + [Test] + public void ParseBundleLayouts() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintLayoutRecursively(parsedExtension); + } + Assert.Pass("..."); + } + else + { + Assert.Fail("..."); + } + } + [Test] + public void HasSlideout() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintSlideoutRecursively(parsedExtension); + } + Assert.Pass("..."); + } + else + { + Assert.Fail("..."); + } + } + + [Test] + public void IsParsingScriptFile() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintTitleRecursively(parsedExtension); + } + Assert.Pass("..."); + } + else + { + Assert.Fail("..."); + } + } + + [Test] + public void ParsingScriptData() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintScriptDataRecursively(parsedExtension); + } + Assert.Pass("Script data parsing completed."); + } + else + { + Assert.Fail("No installed extensions found."); + } + } + + [Test] + public void ParsingBundleFiles() + { + if (_installedExtensions != null) + { + foreach (var parsedExtension in _installedExtensions) + { + PrintBundleDataRecursively(parsedExtension); + } + Assert.Pass("Bundle file parsing completed."); + } + else + { + Assert.Fail("No installed extensions found."); + } + } + + public void PrintLayoutRecursively(ParsedComponent parsedComponent) + { + TestContext.Out.WriteLine($"{parsedComponent.Name}"); + if (parsedComponent.LayoutOrder == null) + { + TestContext.Out.WriteLine($"-- No layout order"); + } + else + { + foreach (var str in parsedComponent.LayoutOrder) + { + TestContext.Out.WriteLine($"-- {str}"); + } + } + + TestContext.Out.WriteLine($"*******************************"); + + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintLayoutRecursively(child); + } + } + } + public void PrintSlideoutRecursively(ParsedComponent parsedComponent) + { + TestContext.Out.WriteLine($"{parsedComponent.Name} -- has slideout {parsedComponent.HasSlideout}"); + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintSlideoutRecursively(child); + } + } + } + public void PrintComponentsRecursively(ParsedComponent parsedComponent, int level = 0) + { + var indent = new string('-', level * 2); + TestContext.Out.WriteLine($"{indent}- ({parsedComponent.Name}) - {parsedComponent.DisplayName}"); + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintComponentsRecursively(child, level + 1); + } + } + } + public void PrintTitleRecursively(ParsedComponent parsedComponent, int level = 0) + { + var indent = new string('-', level * 2); + TestContext.Out.WriteLine($"{indent}- ({parsedComponent.Name}) - {parsedComponent.DisplayName} - {parsedComponent.Title} - {parsedComponent.Tooltip} - {parsedComponent.Author}"); + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintTitleRecursively(child, level + 1); + } + } + } + + public void PrintScriptDataRecursively(ParsedComponent parsedComponent, int level = 0) + { + // Only print components that have script files + if (!string.IsNullOrEmpty(parsedComponent.ScriptPath)) + { + var indent = new string('-', level * 2); + TestContext.Out.WriteLine($"{indent}[SCRIPT] {parsedComponent.Name}"); + TestContext.Out.WriteLine($"{indent} Display Name: {parsedComponent.DisplayName ?? "N/A"}"); + TestContext.Out.WriteLine($"{indent} Script Path: {parsedComponent.ScriptPath}"); + TestContext.Out.WriteLine($"{indent} Title: {parsedComponent.Title ?? "N/A"}"); + TestContext.Out.WriteLine($"{indent} Tooltip: {parsedComponent.Tooltip ?? "N/A"}"); + TestContext.Out.WriteLine($"{indent} Author: {parsedComponent.Author ?? "N/A"}"); + TestContext.Out.WriteLine($"{indent} Component Type: {parsedComponent.Type}"); + + // Special debug for problematic components + if (parsedComponent.Name.Contains("About") || parsedComponent.Name.Contains("Settings") || + parsedComponent.Name.Contains("ManagePackages") || parsedComponent.Name.Contains("Tag")) + { + TestContext.Out.WriteLine($"{indent} [DEBUG] Problematic component detected!"); + TestContext.Out.WriteLine($"{indent} [DEBUG] Bundle File: {parsedComponent.BundleFile ?? "None"}"); + TestContext.Out.WriteLine($"{indent} [DEBUG] Tooltip Length: {parsedComponent.Tooltip?.Length ?? 0}"); + TestContext.Out.WriteLine($"{indent} [DEBUG] Tooltip Contains Newlines: {(parsedComponent.Tooltip?.Contains('\n') ?? false)}"); + TestContext.Out.WriteLine($"{indent} [DEBUG] Tooltip Contains 'en_us': {(parsedComponent.Tooltip?.Contains("en_us") ?? false)}"); + + if (!string.IsNullOrEmpty(parsedComponent.BundleFile)) + { + try + { + var bundleData = BundleParser.BundleYamlParser.Parse(parsedComponent.BundleFile); + TestContext.Out.WriteLine($"{indent} [DEBUG] Bundle Tooltips Count: {bundleData.Tooltips?.Count ?? 0}"); + if (bundleData.Tooltips?.Count > 0) + { + foreach (var kvp in bundleData.Tooltips) + { + TestContext.Out.WriteLine($"{indent} [DEBUG] Bundle Tooltip [{kvp.Key}]: {kvp.Value.Substring(0, Math.Min(50, kvp.Value.Length))}..."); + } + } + } + catch (Exception ex) + { + TestContext.Out.WriteLine($"{indent} [DEBUG] Bundle parse error: {ex.Message}"); + } + } + } + + TestContext.Out.WriteLine($"{indent} --------------------------------"); + } + + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintScriptDataRecursively(child, level + 1); + } + } + } + + public void PrintBundleDataRecursively(ParsedComponent parsedComponent, int level = 0) + { + // Only print components that have bundle files + if (!string.IsNullOrEmpty(parsedComponent.BundleFile)) + { + var indent = new string('-', level * 2); + TestContext.Out.WriteLine($"{indent}[BUNDLE] {parsedComponent.Name}"); + TestContext.Out.WriteLine($"{indent} Display Name: {parsedComponent.DisplayName ?? "N/A"}"); + TestContext.Out.WriteLine($"{indent} Bundle File: {parsedComponent.BundleFile}"); + + try + { + var bundleData = BundleParser.BundleYamlParser.Parse(parsedComponent.BundleFile); + + if (bundleData.Titles?.Count > 0) + { + TestContext.Out.WriteLine($"{indent} Bundle Titles:"); + foreach (var title in bundleData.Titles) + { + TestContext.Out.WriteLine($"{indent} {title.Key}: {title.Value}"); + } + } + + if (bundleData.Tooltips?.Count > 0) + { + TestContext.Out.WriteLine($"{indent} Bundle Tooltips:"); + foreach (var tooltip in bundleData.Tooltips) + { + // Truncate long tooltips for readability + var truncatedTooltip = tooltip.Value.Length > 100 + ? tooltip.Value.Substring(0, 100) + "..." + : tooltip.Value; + TestContext.Out.WriteLine($"{indent} {tooltip.Key}: {truncatedTooltip}"); + } + } + + if (!string.IsNullOrEmpty(bundleData.Author)) + { + TestContext.Out.WriteLine($"{indent} Bundle Author: {bundleData.Author}"); + } + + if (bundleData.LayoutOrder?.Count > 0) + { + TestContext.Out.WriteLine($"{indent} Layout Order: [{string.Join(", ", bundleData.LayoutOrder)}]"); + } + + if (!string.IsNullOrEmpty(bundleData.MinRevitVersion)) + { + TestContext.Out.WriteLine($"{indent} Min Revit Version: {bundleData.MinRevitVersion}"); + } + } + catch (System.Exception ex) + { + TestContext.Out.WriteLine($"{indent} [BUNDLE PARSE ERROR]: {ex.Message}"); + } + + TestContext.Out.WriteLine($"{indent} --------------------------------"); + } + + if (parsedComponent.Children != null) + { + foreach (var child in parsedComponent.Children) + { + PrintBundleDataRecursively(child, level + 1); + } + } + } + + [Test] + public void TestPulldownTooltipParsing() + { + // Try multiple paths to find the test resource + var possiblePaths = new[] + { + @"Resources\TestBundleExtension.extension\TestBundleTab1.tab\TestPanelTwo.panel\TestPulldown.pulldown\bundle.yaml", + Path.Combine("Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestPulldown.pulldown", "bundle.yaml"), + Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestPulldown.pulldown", "bundle.yaml"), + Path.Combine(Directory.GetCurrentDirectory(), "Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestPulldown.pulldown", "bundle.yaml") + }; + + string testBundlePath = null; + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + testBundlePath = path; + break; + } + } + + if (testBundlePath != null) + { + try + { + var bundleData = BundleParser.BundleYamlParser.Parse(testBundlePath); + + TestContext.Out.WriteLine("=== Test Pulldown Bundle Parsing Results ==="); + TestContext.Out.WriteLine($"Bundle file: {testBundlePath}"); + + if (bundleData.Titles?.Count > 0) + { + TestContext.Out.WriteLine("Titles:"); + foreach (var title in bundleData.Titles) + { + TestContext.Out.WriteLine($" {title.Key}: {title.Value}"); + } + } + + if (bundleData.Tooltips?.Count > 0) + { + TestContext.Out.WriteLine("Tooltips:"); + foreach (var tooltip in bundleData.Tooltips) + { + TestContext.Out.WriteLine($" {tooltip.Key}: {tooltip.Value}"); + } + } + + if (!string.IsNullOrEmpty(bundleData.Author)) + { + TestContext.Out.WriteLine($"Author: {bundleData.Author}"); + } + + // Verify that en_us tooltip was parsed correctly + Assert.IsTrue(bundleData.Tooltips.ContainsKey("en_us"), "Should contain en_us tooltip"); + + var enTooltip = bundleData.Tooltips["en_us"]; + TestContext.Out.WriteLine($"English tooltip: '{enTooltip}'"); + + // Should not contain YAML syntax + Assert.IsFalse(enTooltip.Contains("en_us:"), "Tooltip should not contain YAML syntax"); + Assert.IsFalse(enTooltip.Contains(">-"), "Tooltip should not contain YAML folding indicators"); + + // Should contain the actual content + Assert.IsTrue(enTooltip.Contains("This is a test tooltip for the pulldown button"), "Should contain the pulldown tooltip content"); + + Assert.Pass("Pulldown tooltip parsing test completed successfully."); + } + catch (Exception ex) + { + Assert.Fail($"Failed to parse test pulldown bundle: {ex.Message}"); + } + } + else + { + // Show what paths were tried for debugging + TestContext.Out.WriteLine("Attempted paths:"); + foreach (var path in possiblePaths) + { + TestContext.Out.WriteLine($" {path} - Exists: {File.Exists(path)}"); + } + TestContext.Out.WriteLine($"Current Directory: {Directory.GetCurrentDirectory()}"); + TestContext.Out.WriteLine($"Test Directory: {TestContext.CurrentContext.TestDirectory}"); + + Assert.Fail("Test pulldown bundle file not found in any expected location"); + } + } + + [Test] + public void TestMultilineTooltipParsing() + { + // Try multiple paths to find the test resource + var possiblePaths = new[] + { + @"Resources\TestBundleExtension.extension\TestBundleTab1.tab\TestPanelTwo.panel\TestTooltip.pushbutton\bundle.yaml", + Path.Combine("Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestTooltip.pushbutton", "bundle.yaml"), + Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestTooltip.pushbutton", "bundle.yaml"), + Path.Combine(Directory.GetCurrentDirectory(), "Resources", "TestBundleExtension.extension", "TestBundleTab1.tab", "TestPanelTwo.panel", "TestTooltip.pushbutton", "bundle.yaml") + }; + + string testBundlePath = null; + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + testBundlePath = path; + break; + } + } + + if (testBundlePath != null) + { + try + { + var bundleData = BundleParser.BundleYamlParser.Parse(testBundlePath); + + TestContext.Out.WriteLine("=== Test Bundle Parsing Results ==="); + TestContext.Out.WriteLine($"Bundle file: {testBundlePath}"); + + if (bundleData.Titles?.Count > 0) + { + TestContext.Out.WriteLine("Titles:"); + foreach (var title in bundleData.Titles) + { + TestContext.Out.WriteLine($" {title.Key}: {title.Value}"); + } + } + + if (bundleData.Tooltips?.Count > 0) + { + TestContext.Out.WriteLine("Tooltips:"); + foreach (var tooltip in bundleData.Tooltips) + { + TestContext.Out.WriteLine($" {tooltip.Key}: {tooltip.Value}"); + } + } + + if (!string.IsNullOrEmpty(bundleData.Author)) + { + TestContext.Out.WriteLine($"Author: {bundleData.Author}"); + } + + // Verify that en_us tooltip was parsed correctly + Assert.IsTrue(bundleData.Tooltips.ContainsKey("en_us"), "Should contain en_us tooltip"); + + var enTooltip = bundleData.Tooltips["en_us"]; + TestContext.Out.WriteLine($"English tooltip: '{enTooltip}'"); + + // Should not contain YAML syntax + Assert.IsFalse(enTooltip.Contains("en_us:"), "Tooltip should not contain YAML syntax"); + Assert.IsFalse(enTooltip.Contains(">-"), "Tooltip should not contain YAML folding indicators"); + + // Should contain the actual content + Assert.IsTrue(enTooltip.Contains("This is a test tooltip in English"), "Should contain the English content"); + + Assert.Pass("Multiline tooltip parsing test completed successfully."); + } + catch (Exception ex) + { + Assert.Fail($"Failed to parse test bundle: {ex.Message}"); + } + } + else + { + // Show what paths were tried for debugging + TestContext.Out.WriteLine("Attempted paths:"); + foreach (var path in possiblePaths) + { + TestContext.Out.WriteLine($" {path} - Exists: {File.Exists(path)}"); + } + TestContext.Out.WriteLine($"Current Directory: {Directory.GetCurrentDirectory()}"); + TestContext.Out.WriteLine($"Test Directory: {TestContext.CurrentContext.TestDirectory}"); + + Assert.Fail("Test bundle file not found in any expected location"); + } + } + + [Test] + public void TestPulldownComponentParsing() + { + // Find the test extension directory in Resources + var possibleExtensionPaths = new[] + { + @"Resources\TestBundleExtension.extension", + Path.Combine("Resources", "TestBundleExtension.extension"), + Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "TestBundleExtension.extension"), + Path.Combine(Directory.GetCurrentDirectory(), "Resources", "TestBundleExtension.extension") + }; + + string testExtensionPath = null; + foreach (var path in possibleExtensionPaths) + { + if (Directory.Exists(path)) + { + testExtensionPath = path; + break; + } + } + + if (testExtensionPath != null) + { + try + { + // Parse the test extension manually since there's no direct ParseExtension method + var extName = Path.GetFileNameWithoutExtension(testExtensionPath); + var children = ParseTestComponents(testExtensionPath, extName); + + var bundlePath = Path.Combine(testExtensionPath, "bundle.yaml"); + var parsedBundle = File.Exists(bundlePath) + ? BundleParser.BundleYamlParser.Parse(bundlePath) + : null; + + var parsedExtension = new ParsedExtension + { + Name = extName, + Directory = testExtensionPath, + Children = children, + LayoutOrder = parsedBundle?.LayoutOrder, + Titles = parsedBundle?.Titles, + Tooltips = parsedBundle?.Tooltips, + MinRevitVersion = parsedBundle?.MinRevitVersion, + Engine = parsedBundle?.Engine + }; + + TestContext.Out.WriteLine($"=== Testing Pulldown Component in {parsedExtension.Name} ==="); + TestContext.Out.WriteLine($"Extension Path: {testExtensionPath}"); + + var pulldownComponent = FindComponentRecursively(parsedExtension, "TestPulldown"); + if (pulldownComponent != null) + { + TestContext.Out.WriteLine($"Found pulldown component: {pulldownComponent.Name}"); + TestContext.Out.WriteLine($"Display Name: {pulldownComponent.DisplayName}"); + TestContext.Out.WriteLine($"Type: {pulldownComponent.Type}"); + TestContext.Out.WriteLine($"Tooltip: '{pulldownComponent.Tooltip ?? "NULL"}'"); + TestContext.Out.WriteLine($"Bundle File: {pulldownComponent.BundleFile ?? "NULL"}"); + TestContext.Out.WriteLine($"Title: {pulldownComponent.Title ?? "NULL"}"); + + // Verify the component was parsed correctly + Assert.AreEqual(CommandComponentType.PullDown, pulldownComponent.Type, "Component should be PullDown type"); + Assert.IsNotNull(pulldownComponent.Tooltip, "Tooltip should not be null"); + Assert.IsTrue(pulldownComponent.Tooltip.Contains("test tooltip for the pulldown button"), + $"Tooltip should contain expected text, but was: '{pulldownComponent.Tooltip}'"); + + // Check child components + if (pulldownComponent.Children != null && pulldownComponent.Children.Count > 0) + { + TestContext.Out.WriteLine($"Child components count: {pulldownComponent.Children.Count}"); + foreach (var child in pulldownComponent.Children) + { + TestContext.Out.WriteLine($" Child: {child.Name} - {child.DisplayName} - Tooltip: '{child.Tooltip ?? "NULL"}'"); + } + } + + Assert.Pass("Pulldown component parsing test completed successfully."); + } + else + { + Assert.Fail("TestPulldown component not found in parsed extension"); + } + } + catch (Exception ex) + { + Assert.Fail($"Failed to parse test extension: {ex.Message}"); + } + } + else + { + // Show what paths were tried for debugging + TestContext.Out.WriteLine("Attempted extension paths:"); + foreach (var path in possibleExtensionPaths) + { + TestContext.Out.WriteLine($" {path} - Exists: {Directory.Exists(path)}"); + } + TestContext.Out.WriteLine($"Current Directory: {Directory.GetCurrentDirectory()}"); + TestContext.Out.WriteLine($"Test Directory: {TestContext.CurrentContext.TestDirectory}"); + + Assert.Fail("TestBundleExtension directory not found in any expected location"); + } + } + + // Helper method to parse components similar to the ExtensionParser's ParseComponents method + private List ParseTestComponents(string baseDir, string extensionName, string parentPath = null) + { + var components = new List(); + + foreach (var dir in Directory.GetDirectories(baseDir)) + { + var ext = Path.GetExtension(dir); + var componentType = CommandComponentTypeExtensions.FromExtension(ext); + if (componentType == CommandComponentType.Unknown) + continue; + + var namePart = Path.GetFileNameWithoutExtension(dir).Replace(" ", ""); + var displayName = Path.GetFileNameWithoutExtension(dir); + var fullPath = string.IsNullOrEmpty(parentPath) + ? $"{extensionName}_{namePart}" + : $"{parentPath}_{namePart}"; + + string scriptPath = null; + + if (componentType == CommandComponentType.UrlButton) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (File.Exists(yaml)) + scriptPath = yaml; + } + + if (scriptPath == null) + { + scriptPath = Directory + .EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly) + .FirstOrDefault(f => f.EndsWith("script.py", StringComparison.OrdinalIgnoreCase)); + } + + if (scriptPath == null && + (componentType == CommandComponentType.PushButton || + componentType == CommandComponentType.SmartButton || + componentType == CommandComponentType.PullDown || + componentType == CommandComponentType.SplitButton || + componentType == CommandComponentType.SplitPushButton)) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (File.Exists(yaml)) + scriptPath = yaml; + } + + var bundleFile = Path.Combine(dir, "bundle.yaml"); + var children = ParseTestComponents(dir, extensionName, fullPath); + + // First, get values from Python script + string title = null, author = null, doc = null; + if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + (title, author, doc) = ReadPythonScriptConstants(scriptPath); + } + + // Then parse bundle and override with bundle values if they exist + var bundleInComponent = File.Exists(bundleFile) ? BundleParser.BundleYamlParser.Parse(bundleFile) : null; + + // Override script values with bundle values (bundle takes precedence) + if (bundleInComponent != null) + { + // Use en_us as default locale, fallback to first available, then to script values + var bundleTitle = GetLocalizedValue(bundleInComponent.Titles, "en_us"); + var bundleTooltip = GetLocalizedValue(bundleInComponent.Tooltips, "en_us"); + + if (!string.IsNullOrEmpty(bundleTitle)) + title = bundleTitle; + + if (!string.IsNullOrEmpty(bundleTooltip)) + doc = bundleTooltip; + + if (!string.IsNullOrEmpty(bundleInComponent.Author)) + author = bundleInComponent.Author; + } + + components.Add(new ParsedComponent + { + Name = namePart, + DisplayName = displayName, + ScriptPath = scriptPath, + Tooltip = doc ?? $"Command: {namePart}", // Set Tooltip from bundle -> __doc__ -> fallback + UniqueId = SanitizeClassName(fullPath.ToLowerInvariant()), + Type = componentType, + Children = children, + BundleFile = File.Exists(bundleFile) ? bundleFile : null, + LayoutOrder = bundleInComponent?.LayoutOrder, + Title = title, + Author = author + }); + } + + return components; + } + + // Helper methods copied from ExtensionParser + private static string GetLocalizedValue(Dictionary localizedValues, string preferredLocale = "en_us") + { + if (localizedValues == null || localizedValues.Count == 0) + return null; + + if (localizedValues.TryGetValue(preferredLocale, out string preferredValue)) + return preferredValue; + + if (preferredLocale != "en_us" && localizedValues.TryGetValue("en_us", out string enUsValue)) + return enUsValue; + + return localizedValues.Values.FirstOrDefault(); + } + + private static string SanitizeClassName(string name) + { + var sb = new System.Text.StringBuilder(); + foreach (char c in name) + sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + return sb.ToString(); + } + + private static (string title, string author, string doc) ReadPythonScriptConstants(string scriptPath) + { + string title = null, author = null, doc = null; + + foreach (var line in File.ReadLines(scriptPath)) + { + if (line.StartsWith("__title__")) + { + title = ExtractPythonConstantValue(line); + } + else if (line.StartsWith("__author__")) + { + author = ExtractPythonConstantValue(line); + } + else if (line.StartsWith("__doc__")) + { + doc = ExtractPythonConstantValue(line); + } + } + + return (title, author, doc); + } + + private static string ExtractPythonConstantValue(string line) + { + var parts = line.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + var value = parts[1].Trim().Trim('\'', '"'); + return value; + } + return null; + } + + private ParsedComponent FindComponentRecursively(ParsedComponent parent, string componentName) + { + if (parent.Name == componentName) + return parent; + + if (parent.Children != null) + { + foreach (var child in parent.Children) + { + var found = FindComponentRecursively(child, componentName); + if (found != null) + return found; + } + } + + return null; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelOne.panel/PanelOneButton1.pushbutton/Test1-script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelOne.panel/PanelOneButton1.pushbutton/Test1-script.py new file mode 100644 index 000000000..e69de29bb diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelOne.panel/PanelOneButton2.pushbutton/Test2-script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelOne.panel/PanelOneButton2.pushbutton/Test2-script.py new file mode 100644 index 000000000..e69de29bb diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelThree.panel/PanelThreeButton1.pushbutton/Test1-script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelThree.panel/PanelThreeButton1.pushbutton/Test1-script.py new file mode 100644 index 000000000..e69de29bb diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelThree.panel/PanelThreeButton2.pushbutton/Test2-script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelThree.panel/PanelThreeButton2.pushbutton/Test2-script.py new file mode 100644 index 000000000..e69de29bb diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/TestAboutScript.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/TestAboutScript.py new file mode 100644 index 000000000..0c10a8b8e --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/TestAboutScript.py @@ -0,0 +1,5 @@ +__title__ = "About" +__author__ = "test_doc" +__doc__ = "This should be overridden by bundle" + +print("About script") \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/bundle.yaml new file mode 100644 index 000000000..297ac347a --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestAbout.pushbutton/bundle.yaml @@ -0,0 +1,19 @@ +title: + en_us: About pyRevit + ru: ? ????????? + +tooltip: + ru: >- + ? pyRevit. ????????? ???? ????? pyRevit. ?? ?????? ????? ???????????????? ?????????? ??? pyRevit ????????, ?????????? ? ????? ???????????? ? ?????????? ? ????? ?????? ??????????. + ????? ????????: + ?????? ????? (dosymep) + ?????: dosymep@gmail.com + github: https://github.com/dosymep + en_us: >- + About pyRevit. Opens the pyRevit blog website. You can find detailed information on how pyRevit works, updates about the new tools and changes, and a lot of other information there. + fr_fr: >- + propos de pyRevit. Ouvre le site web du blog pyRevit. Vous pouvez y trouver des informations dtailles sur le fonctionnement de pyRevit, des mises jour sur les nouveaux outils et les changements, et beaucoup d'autres informations. + es_es: >- + Sobre pyRevit. Abre el blog online de pyRevit. Puedes encontrar informacin detallada del funcionamiento de pyRevit, actualizaciones y cambios de las herramientas, y mucha otra informacin. + +author: test_doc \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/bundle.yaml new file mode 100644 index 000000000..15c94e929 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/bundle.yaml @@ -0,0 +1,7 @@ +title: + en_us: Sub Button One + +tooltip: + en_us: This is the first sub-button in the pulldown. + +author: Test Author \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/script.py new file mode 100644 index 000000000..b36a834bf --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton1.pushbutton/script.py @@ -0,0 +1,2 @@ +# Test script for sub-button 1 +print("Sub Button 1 executed") \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/bundle.yaml new file mode 100644 index 000000000..f06a4dddf --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/bundle.yaml @@ -0,0 +1,7 @@ +title: + en_us: Sub Button Two + +tooltip: + en_us: This is the second sub-button in the pulldown. + +author: Test Author \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/script.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/script.py new file mode 100644 index 000000000..8bbb319fd --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/SubButton2.pushbutton/script.py @@ -0,0 +1,2 @@ +# Test script for sub-button 2 +print("Sub Button 2 executed") \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/bundle.yaml new file mode 100644 index 000000000..87cf28f73 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestPulldown.pulldown/bundle.yaml @@ -0,0 +1,9 @@ +title: + en_us: Test Pulldown Button + +tooltip: + en_us: >- + This is a test tooltip for the pulldown button. It should appear when + hovering over the main pulldown button, not the default command tooltip. + +author: Test Author \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/TestTooltipScript.py b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/TestTooltipScript.py new file mode 100644 index 000000000..3e556b9dc --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/TestTooltipScript.py @@ -0,0 +1,5 @@ +__title__ = "Test Tooltip Script" +__author__ = "Test Author from Script" +__doc__ = "This tooltip should be overridden by the bundle" + +print("Test script") \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/bundle.yaml new file mode 100644 index 000000000..1ecf0d9c2 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/TestPanelTwo.panel/TestTooltip.pushbutton/bundle.yaml @@ -0,0 +1,13 @@ +title: + en_us: Test Tooltip Button + ru: ???????? ?????? + +tooltip: + en_us: >- + This is a test tooltip in English. It should show only this English text + and not include any other language versions or raw YAML content. + ru: >- + ??? ???????? ????????? ?? ??????? ?????. ??? ?????? ?????????? ?????? + ??????? ?????, ? ?? ???????? ?????? ???????? ??????. + +author: Test Author \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/bundle.yaml new file mode 100644 index 000000000..599e62d0c --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/TestBundleTab1.tab/bundle.yaml @@ -0,0 +1,4 @@ +layout: + - TestPanelThree + - TestPanelTwo + - TestPanelOne \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/bundle.yaml b/dev/pyRevitLoader/pyRevitExtensionParserTester/Resources/TestBundleExtension.extension/bundle.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj new file mode 100644 index 000000000..ba242c8d5 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj @@ -0,0 +1,43 @@ + + + + enable + enable + true + true + IPY342 + 3.4.2 + false + true + false + false + false + false + true + latest + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/dev/pyRevitLoader/pyRevitLoader.2712PR/pyRevitLoader.2712PR.csproj b/dev/pyRevitLoader/pyRevitLoader.2712PR/pyRevitLoader.2712PR.csproj index c3eb3cbcc..da354c1d9 100644 --- a/dev/pyRevitLoader/pyRevitLoader.2712PR/pyRevitLoader.2712PR.csproj +++ b/dev/pyRevitLoader/pyRevitLoader.2712PR/pyRevitLoader.2712PR.csproj @@ -6,6 +6,7 @@ $(DefineConstants);PYREVITLABS_ENGINE IPY2712PR python_2712pr_lib.zip + true 2.7.12 \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitLoader.342/pyRevitLoader.342.csproj b/dev/pyRevitLoader/pyRevitLoader.342/pyRevitLoader.342.csproj index 7b0612c95..16027009e 100644 --- a/dev/pyRevitLoader/pyRevitLoader.342/pyRevitLoader.342.csproj +++ b/dev/pyRevitLoader/pyRevitLoader.342/pyRevitLoader.342.csproj @@ -6,5 +6,7 @@ IPY342 python_342_lib.zip 3.4.2 + true + true diff --git a/dev/pyRevitLoader/pyRevitLoader.sln b/dev/pyRevitLoader/pyRevitLoader.sln index 60dd779a5..bb71a699b 100644 --- a/dev/pyRevitLoader/pyRevitLoader.sln +++ b/dev/pyRevitLoader/pyRevitLoader.sln @@ -15,6 +15,26 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitLoader.342", "pyRevi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitRunner.342", "pyRevitRunner.342\pyRevitRunner.342.csproj", "{C21AE8F2-9A0E-436F-9CBD-B7ED170F85A2}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scaffolding", "Scaffolding", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitAssemblyBuilder", "pyRevitAssemblyBuilder\pyRevitAssemblyBuilder.csproj", "{37607FDE-9E96-28B6-AAB7-975357B22391}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitExtensionParser", "pyRevitExtensionParser\pyRevitExtensionParser.csproj", "{E28B1811-FBD5-F701-8ECA-553894F780EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props and Targets - Solution", "Props and Targets - Solution", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props and Targets - Master", "Props and Targets - Master", "{3744BF2E-E027-4A7E-8E3C-21B56DDB4971}" + ProjectSection(SolutionItems) = preProject + ..\Directory.Build.props = ..\Directory.Build.props + ..\Directory.Build.targets = ..\Directory.Build.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pyRevitExtensionParserTest", "pyRevitExtensionParserTester\pyRevitExtensionParserTest.csproj", "{7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -37,6 +57,18 @@ Global {C21AE8F2-9A0E-436F-9CBD-B7ED170F85A2}.Debug|x64.Build.0 = Debug|Any CPU {C21AE8F2-9A0E-436F-9CBD-B7ED170F85A2}.Release|x64.ActiveCfg = Release|Any CPU {C21AE8F2-9A0E-436F-9CBD-B7ED170F85A2}.Release|x64.Build.0 = Release|Any CPU + {37607FDE-9E96-28B6-AAB7-975357B22391}.Debug|x64.ActiveCfg = Debug|Any CPU + {37607FDE-9E96-28B6-AAB7-975357B22391}.Debug|x64.Build.0 = Debug|Any CPU + {37607FDE-9E96-28B6-AAB7-975357B22391}.Release|x64.ActiveCfg = Release|Any CPU + {37607FDE-9E96-28B6-AAB7-975357B22391}.Release|x64.Build.0 = Release|Any CPU + {E28B1811-FBD5-F701-8ECA-553894F780EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {E28B1811-FBD5-F701-8ECA-553894F780EC}.Debug|x64.Build.0 = Debug|Any CPU + {E28B1811-FBD5-F701-8ECA-553894F780EC}.Release|x64.ActiveCfg = Release|Any CPU + {E28B1811-FBD5-F701-8ECA-553894F780EC}.Release|x64.Build.0 = Release|Any CPU + {7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8}.Debug|x64.Build.0 = Debug|Any CPU + {7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8}.Release|x64.ActiveCfg = Release|Any CPU + {7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +78,9 @@ Global {9DB5A32B-7C8A-463B-8DB5-DFD992ADED8A} = {74C50D6F-68E5-411B-A781-800BC60C9ECF} {B1E9CE55-3A8E-4961-B441-33EEB933E237} = {5D24CE7B-67FD-4685-AC89-578FF15552A2} {C21AE8F2-9A0E-436F-9CBD-B7ED170F85A2} = {74C50D6F-68E5-411B-A781-800BC60C9ECF} + {37607FDE-9E96-28B6-AAB7-975357B22391} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E28B1811-FBD5-F701-8ECA-553894F780EC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {7396BE4A-8DA6-CCCC-C7EE-12A2A8F787D8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8CC1064C-1D16-446B-B480-989CE124102A} diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py index d0524d036..769fbaa29 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py @@ -9,7 +9,9 @@ from pyrevit import script from pyrevit.userconfig import user_config - +__title__= "test_title" +__author__= "test_author" +__doc__ = "test_doc" logger = script.get_logger() diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.en_us.xaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.en_us.xaml index 295cf6646..4f6604d39 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.en_us.xaml +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.en_us.xaml @@ -57,6 +57,9 @@ Misc options for pyRevit development Load Beta Tools (Scripts with __beta__ = True, Reload is required) + Enables new, faster version of pyRevit loader (beta) + Use RoslynLoader + Caching Reset Caching to default 200 diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.fr_fr.xaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.fr_fr.xaml index 338cf6e85..af0307b4a 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.fr_fr.xaml +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.fr_fr.xaml @@ -57,6 +57,9 @@ Options diverses pour le développement de pyRevit Charger les outils bêta (Scripts avec __beta__ = True, le rechargement est requis) + Active une nouvelle version plus rapide du chargeur pyRevit (bêta) + Utiliser le chargeur RoslynLoader + Mise en cache Reset la mise en cache par défaut 200 diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.ru.xaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.ru.xaml index 6080cde86..6d917e8c9 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.ru.xaml +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.ru.xaml @@ -58,6 +58,9 @@ Разные настройки разработки pyRevit Загружать бета-инструменты (Скрипты с __beta__ = True, требуется перезапуск) + Использовать новую более быструю версию загрузчика pyRevit (бета) + Использовать загрузчик RoslynLoader + Кеширование Сбросить настройки кеширования 220 diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.xaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.xaml index d208f7d4c..342e721bb 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.xaml +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.xaml @@ -207,6 +207,19 @@ + + + + + + + + + + + + + diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py index 553d553ea..d4cebd409 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py @@ -156,6 +156,9 @@ def _setup_core_options(self): self.minhostdrivefreespace_tb.Text = str(user_config.min_host_drivefreespace) self.loadbetatools_cb.IsChecked = user_config.load_beta + + self.new_loader.IsChecked = user_config.new_loader + self.use_roslyn_loader.IsChecked = user_config.use_roslyn_loader def _setup_engines(self): """Sets up the list of available engines.""" @@ -845,6 +848,7 @@ def _save_core_options(self): user_config.min_host_drivefreespace = 0 user_config.load_beta = self.loadbetatools_cb.IsChecked + user_config.new_loader = self.new_loader.IsChecked def _save_engines(self): # set active cpython engine diff --git a/pyrevitlib/pyrevit/coreutils/logger.py b/pyrevitlib/pyrevit/coreutils/logger.py index 17e0af284..af6dfd35b 100644 --- a/pyrevitlib/pyrevit/coreutils/logger.py +++ b/pyrevitlib/pyrevit/coreutils/logger.py @@ -347,7 +347,6 @@ def get_logger(logger_name): logger.addHandler(get_stdout_hndlr()) logger.propagate = False logger.addHandler(get_file_hndlr()) - loggers.update({logger_name: logger}) return logger diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index 8d9673a89..726a5404c 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -377,7 +377,7 @@ def _calculate_extension_dir_hash(self): pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' pat += '|(\\' + exts.CONTENT_BUTTON_POSTFIX + ')' - # tnteresting directories + # interesting directories pat += '|(\\' + exts.COMP_LIBRARY_DIR_NAME + ')' pat += '|(\\' + exts.COMP_HOOKS_DIR_NAME + ')' # search for scripts, setting files (future support), and layout files diff --git a/pyrevitlib/pyrevit/loader/sessionmgr.py b/pyrevitlib/pyrevit/loader/sessionmgr.py index 654abaf78..c7c1dcaac 100644 --- a/pyrevitlib/pyrevit/loader/sessionmgr.py +++ b/pyrevitlib/pyrevit/loader/sessionmgr.py @@ -255,6 +255,10 @@ def load_session(): a dll assembly needs to be created. This function handles these tasks through interactions with .extensions, .loader.asmmaker, and .loader.uimaker. + Load session now takes a light parameter that will skip the assembly creation + and UI creation. This is for the case when pyRevitAssemblyMaker.dll is + used to create the assembly and UI + Examples: ```python from pyrevit.loader.sessionmgr import load_session @@ -282,7 +286,9 @@ def load_session(): _perform_onsessionloadstart_ops() # create a new session - _new_session() + if not user_config.new_loader: + _new_session() + # other cases are carried out by the pyRevitAssemblyMaker.dll # perform post-load tasks _perform_onsessionloadcomplete_ops() diff --git a/pyrevitlib/pyrevit/userconfig.py b/pyrevitlib/pyrevit/userconfig.py index 7af641dfa..d4ed773a1 100644 --- a/pyrevitlib/pyrevit/userconfig.py +++ b/pyrevitlib/pyrevit/userconfig.py @@ -301,7 +301,33 @@ def load_beta(self, state): CONSTS.ConfigsLoadBetaKey, value=state ) - + @property + def new_loader(self): + """Whether to use new csharp loader.""" + return self.core.get_option( + CONSTS.ConfigsNewLoaderKey, + default_value=CONSTS.ConfigsNewLoaderDefault, + ) + + @new_loader.setter + def new_loader(self, state): + self.core.set_option( + CONSTS.ConfigsNewLoaderKey, + value=state + ) + @property + def use_roslyn_loader(self): + """Whether to use a Roslyn loader.""" + return self.core.get_option( + CONSTS.ConfigsUseRoslynKey, + default_value=CONSTS.ConfigsUseRoslynDefault, + ) + @use_roslyn_loader.setter + def use_roslyn_loader(self, state): + self.core.set_option( + CONSTS.ConfigsUseRoslynKey, + value=state + ) @property def cpython_engine_version(self): """CPython engine version to use.""" @@ -858,6 +884,7 @@ def verify_configs(config_file_path=None): # this pushes reading settings at first import of this module. try: verify_configs(CONFIG_FILE) + print('Using config file: %s', CONFIG_FILE) user_config = PyRevitConfig(cfg_file_path=CONFIG_FILE, config_type=CONFIG_TYPE) upgrade.upgrade_user_config(user_config)