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)