diff --git a/.gitignore b/.gitignore index f8d9017804..bc32f10128 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ extensions/* **/packages **/dev/**/bin **/dev/pyRevitLabs/TestResults/ +**/*_wpftmp.csproj # ignore sphinx build files docs/_* diff --git a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index a63f4ef91d..7d2bb71018 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll index 380522e934..2ec3dcfd11 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll index 5bbaefc461..2f77a41764 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll index 599c5450bf..47c51008f0 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll index 1b6205a7aa..fe9bb9345d 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll index 5889ebde5e..b65d925b6b 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll index a63f4ef91d..7d2bb71018 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll 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 index 380522e934..2ec3dcfd11 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll index 45496c6c1b..ad20617fd5 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll index b133ceaf87..12fd00f7a1 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLoader.dll b/bin/netcore/engines/IPY342/pyRevitLoader.dll index 05764e8dc5..51ad09e027 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 3732780f48..3d348b5819 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.Common.dll b/bin/netcore/pyRevitLabs.Common.dll index 63d5fe1380..3081db1038 100644 Binary files a/bin/netcore/pyRevitLabs.Common.dll and b/bin/netcore/pyRevitLabs.Common.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonCLI.dll b/bin/netcore/pyRevitLabs.CommonCLI.dll index 3d12b90fb1..401a39a02d 100644 Binary files a/bin/netcore/pyRevitLabs.CommonCLI.dll and b/bin/netcore/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonWPF.dll b/bin/netcore/pyRevitLabs.CommonWPF.dll index 574b10ec44..e8b757e038 100644 Binary files a/bin/netcore/pyRevitLabs.CommonWPF.dll and b/bin/netcore/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netcore/pyRevitLabs.DeffrelDB.dll b/bin/netcore/pyRevitLabs.DeffrelDB.dll index a462c1cd82..6e2bd86349 100644 Binary files a/bin/netcore/pyRevitLabs.DeffrelDB.dll and b/bin/netcore/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netcore/pyRevitLabs.Emojis.dll b/bin/netcore/pyRevitLabs.Emojis.dll index 76f1240c7a..dadc2e2ee2 100644 Binary files a/bin/netcore/pyRevitLabs.Emojis.dll and b/bin/netcore/pyRevitLabs.Emojis.dll differ diff --git a/bin/netcore/pyRevitLabs.Language.dll b/bin/netcore/pyRevitLabs.Language.dll index b8225fc45d..b137c6fd36 100644 Binary files a/bin/netcore/pyRevitLabs.Language.dll and b/bin/netcore/pyRevitLabs.Language.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll index 25c3aa1c4c..1dd3190dc2 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.dll b/bin/netcore/pyRevitLabs.PyRevit.dll index 9415681299..962ebd48be 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.dll and b/bin/netcore/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll index d6c86b6b0f..dc91711210 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll index 74f44d9d4c..63c1f5537c 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll index 6c98579568..a956dfeebe 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll and b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netcore/pyRevitLabs.UnitTests.dll b/bin/netcore/pyRevitLabs.UnitTests.dll index ed1baf14b2..8ef8984095 100644 Binary files a/bin/netcore/pyRevitLabs.UnitTests.dll and b/bin/netcore/pyRevitLabs.UnitTests.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index 5096d1bd0a..b76fcffaac 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll index 551bc16118..820a30638f 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll index 7bf53d19d0..a299266e53 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll index 1c9e3e8cb7..1dcf4d988a 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll index 36666c526d..a01641fc9d 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll index cc94131ef0..d89b66af0b 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll index 302004a748..9c2650f0be 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll index 4001713875..6f4c8928aa 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll index 235ac56038..e42cbf9b50 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll index 59847f8e8c..4ee4844f63 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll index 6ab678ed68..1cc80b0a4f 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll index b6a72d4f97..9d8963e3ff 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll index 5096d1bd0a..b76fcffaac 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll 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 index 551bc16118..820a30638f 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll index 75080c68c5..9fe15f0322 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll index 91326d4526..71f95c3ec8 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll index 8955bed5bd..1c32248253 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll index 21552d7d92..3413ac905c 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll index bf90f50f9f..24073b9e34 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll index 7354b8b121..da1a7e5d48 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll index 1c930004cf..d6e70b40fe 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll index 389da8965e..cdb39a46a0 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLoader.dll b/bin/netfx/engines/IPY342/pyRevitLoader.dll index 69a6979041..150a287ec1 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 fe685dfdd0..5f046805ab 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.Common.dll b/bin/netfx/pyRevitLabs.Common.dll index a9d97060de..7f85179401 100644 Binary files a/bin/netfx/pyRevitLabs.Common.dll and b/bin/netfx/pyRevitLabs.Common.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonCLI.dll b/bin/netfx/pyRevitLabs.CommonCLI.dll index 3205e3d00e..f8229ca893 100644 Binary files a/bin/netfx/pyRevitLabs.CommonCLI.dll and b/bin/netfx/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonWPF.dll b/bin/netfx/pyRevitLabs.CommonWPF.dll index d6e731aa4e..46812d93c9 100644 Binary files a/bin/netfx/pyRevitLabs.CommonWPF.dll and b/bin/netfx/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netfx/pyRevitLabs.DeffrelDB.dll b/bin/netfx/pyRevitLabs.DeffrelDB.dll index 63b0549cc2..012848b0bc 100644 Binary files a/bin/netfx/pyRevitLabs.DeffrelDB.dll and b/bin/netfx/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netfx/pyRevitLabs.Emojis.dll b/bin/netfx/pyRevitLabs.Emojis.dll index 4a2db3b907..c2afa627e5 100644 Binary files a/bin/netfx/pyRevitLabs.Emojis.dll and b/bin/netfx/pyRevitLabs.Emojis.dll differ diff --git a/bin/netfx/pyRevitLabs.Language.dll b/bin/netfx/pyRevitLabs.Language.dll index 99b80d7147..c7c99f754c 100644 Binary files a/bin/netfx/pyRevitLabs.Language.dll and b/bin/netfx/pyRevitLabs.Language.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll index 708ab92a84..3507215588 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.dll b/bin/netfx/pyRevitLabs.PyRevit.dll index c89ea47e91..b540c7da95 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.dll and b/bin/netfx/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll index c25fcfb512..5ee8889989 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll index 0af792746a..b77f9ac8cf 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll index 87b7dd7c91..2141e0782d 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll and b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netfx/pyRevitLabs.UnitTests.dll b/bin/netfx/pyRevitLabs.UnitTests.dll index 33d20aea4b..f4cd301430 100644 Binary files a/bin/netfx/pyRevitLabs.UnitTests.dll and b/bin/netfx/pyRevitLabs.UnitTests.dll differ diff --git a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs index 3eba2784fd..7f60900e45 100644 --- a/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs +++ b/dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs @@ -183,6 +183,10 @@ public static class PyRevitConsts { public const string ConfigsNewLoaderKey = "new_loader"; public const bool ConfigsNewLoaderDefault = true; + // layout settings + public const string ConfigsDisableCustomLayoutsKey = "disable_custom_layouts"; + public const bool ConfigsDisableCustomLayoutsDefault = 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/pyRevitExtensionParser/ExtensionParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs index fd307873f0..ef95548194 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs @@ -386,6 +386,11 @@ public static IEnumerable ParseInstalledExtensions(IEnumerable< // single cache without needing their own copy. Fix for #3268. private static PyRevitConfig GetConfig() => PyRevitConfig.Load(); + /// + /// Internal accessor for GetConfig, used by LayoutParser for custom layout path resolution. + /// + internal static PyRevitConfig GetConfigInternal() => GetConfig(); + /// /// Parses a single extension from the given extension directory path /// @@ -508,14 +513,30 @@ private static ParsedExtension ParseExtension(string extDir, int revitYear = 0) rocketModeCompatible = true; } - // FIXED — pass revitYear through: - var children = ParseComponents(extDir, extName, null, - extensionTemplates.Count > 0 ? extensionTemplates : null, - revitYear); + // Check for layout-based parsing (extension_layout.yaml) + var config = GetConfig(); + List children; + List assemblyOnlyComponents = null; + if (LayoutParser.HasLayoutFile(extDir)) + { + LogDebug($"Using layout-based parsing for extension: {extName}"); + var layoutResult = LayoutParser.ParseLayout( + extDir, extName, + extensionTemplates.Count > 0 ? extensionTemplates : null, + revitYear); + children = layoutResult.Tabs; + assemblyOnlyComponents = layoutResult.UnreferencedTools; + } + else + { + // Legacy directory-walking parser + children = ParseComponents(extDir, extName, null, + extensionTemplates.Count > 0 ? extensionTemplates : null, + revitYear); + } // Read extension config from pyRevit config file (cached). // Config is keyed by folder name (e.g. [extension_test.extension]) so it matches install and Python. - var config = GetConfig(); var extConfig = config.ParseExtensionByName(extName); var parsedExtension = new ParsedExtension @@ -534,6 +555,7 @@ private static ParsedExtension ParseExtension(string extDir, int revitYear = 0) Engine = parsedBundle?.Engine, Config = extConfig, RocketModeCompatible = rocketModeCompatible, + AssemblyOnlyComponents = assemblyOnlyComponents, AuthorizedUsers = authUsers, AuthorizedGroups = authGroups }; @@ -914,466 +936,421 @@ private static List ParseComponents( continue; var namePart = Path.GetFileNameWithoutExtension(dir).Replace(" ", ""); - var displayName = Path.GetFileNameWithoutExtension(dir); var fullPath = string.IsNullOrEmpty(parentPath) ? $"{extensionName}_{namePart}" : $"{parentPath}_{namePart}"; - string scriptPath = null; + var component = ParseSingleBundle(dir, componentType, extensionName, fullPath, inheritedTemplates, revitYear); + if (component != null) + components.Add(component); + } - if (componentType == CommandComponentType.UrlButton) + return components; + } + + /// + /// Parses a single bundle directory into a ParsedComponent. Shared by + /// the directory-walking ParseComponents path and the layout-driven + /// LayoutParser path, so script detection, icon parsing, version gating, + /// etc. stay in one place. + /// + /// Path to the component bundle directory + /// The component type (from folder extension) + /// Name of the parent extension + /// The path used to generate UniqueId (e.g. "extname_toolname") + /// Templates inherited from parent + /// Running Revit version year (0 to skip filtering) + /// A ParsedComponent, or null if version-incompatible + internal static ParsedComponent ParseSingleBundle( + string dir, + CommandComponentType componentType, + string extensionName, + string uniquePath, + Dictionary inheritedTemplates, + int revitYear) + { + var namePart = Path.GetFileNameWithoutExtension(dir).Replace(" ", ""); + var displayName = Path.GetFileNameWithoutExtension(dir); + + string scriptPath = null; + + if (componentType == CommandComponentType.UrlButton) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (FileExists(yaml)) + scriptPath = yaml; + } + + if (scriptPath == null) + { + var dirFiles = GetFilesInDirectory(dir, "*script.*", SearchOption.TopDirectoryOnly); + var validEndings = new[] { "script", "_script", "-script", ".script" }; + dirFiles = dirFiles.Where(f => + validEndings.Any(end => Path.GetFileNameWithoutExtension(f).EndsWith(end, StringComparison.OrdinalIgnoreCase)) + ).ToArray(); + + var scriptExtensions = new[] { ".py", ".cs", ".vb", ".rb", ".dyn", ".gh", ".ghx", ".rfa" }; + foreach (var scriptExt in scriptExtensions) { - var yaml = Path.Combine(dir, "bundle.yaml"); - if (FileExists(yaml)) - scriptPath = yaml; + var scriptFile = $"script{scriptExt}"; + scriptPath = dirFiles.FirstOrDefault(f => + f.EndsWith(scriptFile, StringComparison.OrdinalIgnoreCase)); + if (scriptPath != null) + break; } if (scriptPath == null) { - // Look for script files in order of preference: .py, .cs, .vb, .rb, .dyn, .gh, .ghx, .rfa - // Use cached file listing instead of EnumerateFiles - var dirFiles = GetFilesInDirectory(dir, "*script.*", SearchOption.TopDirectoryOnly); - var validEndings = new[] { "script", "_script", "-script", ".script" }; - dirFiles = dirFiles.Where(f => - validEndings.Any(end => Path.GetFileNameWithoutExtension(f).EndsWith(end, StringComparison.OrdinalIgnoreCase)) - ).ToArray(); - - // Check for scripts in priority order - var scriptExtensions = new[] { ".py", ".cs", ".vb", ".rb", ".dyn", ".gh", ".ghx", ".rfa" }; + var allFiles = GetFilesInDirectory(dir, "*", SearchOption.TopDirectoryOnly); foreach (var scriptExt in scriptExtensions) { - var scriptFile = $"script{scriptExt}"; - scriptPath = dirFiles.FirstOrDefault(f => - f.EndsWith(scriptFile, StringComparison.OrdinalIgnoreCase)); + scriptPath = allFiles.FirstOrDefault(f => + (f.EndsWith($"_script{scriptExt}", StringComparison.OrdinalIgnoreCase) || + (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && + !f.EndsWith($"_config{scriptExt}", StringComparison.OrdinalIgnoreCase)))); if (scriptPath != null) break; } - - // If no script.* file found, look for any file with the target extensions - // This handles cases like BIM1_ArrowHeadSwitcher_script.dyn - if (scriptPath == null) - { - var allFiles = GetFilesInDirectory(dir, "*", SearchOption.TopDirectoryOnly); - foreach (var scriptExt in scriptExtensions) - { - // Look for any file ending with _script{ext} or just {ext} - scriptPath = allFiles.FirstOrDefault(f => - (f.EndsWith($"_script{scriptExt}", StringComparison.OrdinalIgnoreCase) || - (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && - !f.EndsWith($"_config{scriptExt}", StringComparison.OrdinalIgnoreCase)))); - if (scriptPath != null) - break; - } - } } + } - if (scriptPath == null && - (componentType == CommandComponentType.PushButton || - componentType == CommandComponentType.SmartButton || - componentType == CommandComponentType.PullDown || - componentType == CommandComponentType.SplitButton || - componentType == CommandComponentType.SplitPushButton || - componentType == CommandComponentType.InvokeButton)) - { - var yaml = Path.Combine(dir, "bundle.yaml"); - if (FileExists(yaml)) - scriptPath = yaml; - } + if (scriptPath == null && + (componentType == CommandComponentType.PushButton || + componentType == CommandComponentType.SmartButton || + componentType == CommandComponentType.PullDown || + componentType == CommandComponentType.SplitButton || + componentType == CommandComponentType.SplitPushButton || + componentType == CommandComponentType.InvokeButton)) + { + var yaml = Path.Combine(dir, "bundle.yaml"); + if (FileExists(yaml)) + scriptPath = yaml; + } - // Look for config script (*config.py, *config.cs, etc.) - // e.g. both "config.py" and "name_config.py" will match - string configScriptPath = null; - var configExtensions = new[] { ".py", ".cs", ".vb", ".rb", ".dyn", ".gh", ".ghx" }; - var allDirFiles = GetFilesInDirectory(dir, "*", SearchOption.TopDirectoryOnly); - // Prefer exact "config{ext}" match, then fall back to postfix "*config{ext}" + // Config script detection + string configScriptPath = null; + var configExtensions = new[] { ".py", ".cs", ".vb", ".rb", ".dyn", ".gh", ".ghx" }; + var allDirFiles = GetFilesInDirectory(dir, "*", SearchOption.TopDirectoryOnly); + foreach (var configExt in configExtensions) + { + var configFile = $"config{configExt}"; + configScriptPath = allDirFiles.FirstOrDefault(f => + Path.GetFileName(f).Equals(configFile, StringComparison.OrdinalIgnoreCase)); + if (configScriptPath != null) + break; + } + if (configScriptPath == null) + { foreach (var configExt in configExtensions) { - var configFile = $"config{configExt}"; + var configPostfix = $"config{configExt}"; configScriptPath = allDirFiles.FirstOrDefault(f => - Path.GetFileName(f).Equals(configFile, StringComparison.OrdinalIgnoreCase)); + Path.GetFileName(f).EndsWith(configPostfix, StringComparison.OrdinalIgnoreCase)); if (configScriptPath != null) break; } - if (configScriptPath == null) - { - foreach (var configExt in configExtensions) - { - var configPostfix = $"config{configExt}"; - configScriptPath = allDirFiles.FirstOrDefault(f => - Path.GetFileName(f).EndsWith(configPostfix, StringComparison.OrdinalIgnoreCase)); - if (configScriptPath != null) - break; - } - } - // If no separate config script found, use the main script path - if (configScriptPath == null) - configScriptPath = scriptPath; - - // Handle .content bundles - special logic for Revit family (.rfa) files - // Content bundles load RFA files, with scriptPath being the primary content - // and configScriptPath being the alternative content (CTRL+Click) - if (componentType == CommandComponentType.ContentButton) + } + if (configScriptPath == null) + configScriptPath = scriptPath; + + // Handle .content bundles + if (componentType == CommandComponentType.ContentButton) + { + var bundleYaml = Path.Combine(dir, "bundle.yaml"); + ParsedBundle tempBundle = null; + if (FileExists(bundleYaml)) { - var bundleYaml = Path.Combine(dir, "bundle.yaml"); - ParsedBundle tempBundle = null; - if (FileExists(bundleYaml)) + try { - try - { - tempBundle = BundleParser.BundleYamlParser.Parse(bundleYaml); - } - catch (Exception ex) - { - LogParseException(bundleYaml, ex); - } + tempBundle = BundleParser.BundleYamlParser.Parse(bundleYaml); } - - // Try to get content from bundle.yaml metadata first - if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.Content)) + catch (Exception ex) { - scriptPath = ResolveContentPath(dir, tempBundle.Content); + LogParseException(bundleYaml, ex); } + } - // If no content in metadata, use naming convention - if (scriptPath == null) - { - // Look for version-specific content first: content_{version}.rfa - var versionedContent = GetFilesInDirectory(dir, "content_*.rfa", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - if (versionedContent != null) - { - scriptPath = versionedContent; - } - else - { - // Look for default content.rfa - var defaultContent = Path.Combine(dir, "content.rfa"); - if (FileExists(defaultContent)) - { - scriptPath = defaultContent; - } - else - { - // Look for any .rfa file in the directory - var anyRfa = GetFilesInDirectory(dir, "*.rfa", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - if (anyRfa != null) - { - scriptPath = anyRfa; - } - } - } - } + if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.Content)) + { + scriptPath = ResolveContentPath(dir, tempBundle.Content); + } - // Handle alternative content (CTRL+Click) - if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.ContentAlt)) - { - configScriptPath = ResolveContentPath(dir, tempBundle.ContentAlt); - } + if (scriptPath == null) + { + var versionedContent = GetFilesInDirectory(dir, "content_*.rfa", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + if (versionedContent != null) + scriptPath = versionedContent; else { - // Look for version-specific alternative content: other_{version}.rfa - var versionedAltContent = GetFilesInDirectory(dir, "other_*.rfa", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - if (versionedAltContent != null) - { - configScriptPath = versionedAltContent; - } + var defaultContent = Path.Combine(dir, "content.rfa"); + if (FileExists(defaultContent)) + scriptPath = defaultContent; else { - // Look for default other.rfa - var defaultAltContent = Path.Combine(dir, "other.rfa"); - if (FileExists(defaultAltContent)) - { - configScriptPath = defaultAltContent; - } - else - { - // Fall back to main content path - configScriptPath = scriptPath; - } + var anyRfa = GetFilesInDirectory(dir, "*.rfa", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + if (anyRfa != null) + scriptPath = anyRfa; } } } - // Look for on/off icons for smartbuttons and toggle buttons - string onIconPath = null, onIconDarkPath = null, offIconPath = null, offIconDarkPath = null; - if (componentType == CommandComponentType.SmartButton || - componentType == CommandComponentType.PushButton) + if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.ContentAlt)) { - // Parse on/off icons with theme support - (onIconPath, onIconDarkPath, offIconPath, offIconDarkPath) = ParseToggleIcons(dir); + configScriptPath = ResolveContentPath(dir, tempBundle.ContentAlt); } - - // Look for tooltip media file (tooltip.mp4, tooltip.swf, tooltip.png) - var mediaFile = FindMediaFile(dir); - - // Look for help file (help.* pattern) for file-based help - var helpFile = FindHelpFile(dir); - - var bundleFile = Path.Combine(dir, "bundle.yaml"); - - // Then parse bundle and override with bundle values if they exist - ParsedBundle bundleInComponent = null; - if (FileExists(bundleFile)) + else { - try - { - bundleInComponent = BundleParser.BundleYamlParser.Parse(bundleFile); - } - catch (Exception ex) + var versionedAltContent = GetFilesInDirectory(dir, "other_*.rfa", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + if (versionedAltContent != null) + configScriptPath = versionedAltContent; + else { - LogParseException(bundleFile, ex); + var defaultAltContent = Path.Combine(dir, "other.rfa"); + if (FileExists(defaultAltContent)) + configScriptPath = defaultAltContent; + else + configScriptPath = scriptPath; } } + } - // Merge templates: inherited templates + current bundle templates - // Current bundle templates override inherited ones - var mergedTemplates = new Dictionary(); - if (inheritedTemplates != null) - { - foreach (var kvp in inheritedTemplates) - { - mergedTemplates[kvp.Key] = kvp.Value; - } - } - if (bundleInComponent?.Templates != null) + // Toggle icons + string onIconPath = null, onIconDarkPath = null, offIconPath = null, offIconDarkPath = null; + if (componentType == CommandComponentType.SmartButton || + componentType == CommandComponentType.PushButton) + { + (onIconPath, onIconDarkPath, offIconPath, offIconDarkPath) = ParseToggleIcons(dir); + } + + var mediaFile = FindMediaFile(dir); + var helpFile = FindHelpFile(dir); + var bundleFile = Path.Combine(dir, "bundle.yaml"); + + ParsedBundle bundleInComponent = null; + if (FileExists(bundleFile)) + { + try { - foreach (var kvp in bundleInComponent.Templates) - { - mergedTemplates[kvp.Key] = kvp.Value; - } + bundleInComponent = BundleParser.BundleYamlParser.Parse(bundleFile); } - - // Get author from bundle to add to templates for child components - // This allows children to use {{author}} to inherit from parent - string bundleAuthor = bundleInComponent?.Author; - if (!string.IsNullOrEmpty(bundleAuthor) && !bundleAuthor.Contains("{{")) + catch (Exception ex) { - // Only add if it's a concrete value, not a template reference itself - mergedTemplates["author"] = bundleAuthor; + LogParseException(bundleFile, ex); } + } - // Pass merged templates to child components - var children = ParseComponents(dir, extensionName, fullPath, mergedTemplates, revitYear); + // Merge templates + var mergedTemplates = new Dictionary(); + if (inheritedTemplates != null) + { + foreach (var kvp in inheritedTemplates) + mergedTemplates[kvp.Key] = kvp.Value; + } + if (bundleInComponent?.Templates != null) + { + foreach (var kvp in bundleInComponent.Templates) + mergedTemplates[kvp.Key] = kvp.Value; + } - // First, get values from Python script - string title = null, author = null, doc = null; - string scriptContext = null, scriptHelpUrl = null, scriptHighlight = null; - string scriptMinRevitVersion = null, scriptMaxRevitVersion = null; - bool scriptIsBeta = false, scriptCleanEngine = false, scriptFullFrameEngine = false, scriptPersistentEngine = false; - Dictionary scriptLocalizedTitles = null; - Dictionary scriptLocalizedTooltips = null; - Dictionary scriptLocalizedHelpUrls = null; + string bundleAuthor = bundleInComponent?.Author; + if (!string.IsNullOrEmpty(bundleAuthor) && !bundleAuthor.Contains("{{")) + mergedTemplates["author"] = bundleAuthor; + + // Parse children (for containers like pulldown, splitbutton) + var children = ParseComponents(dir, extensionName, uniquePath, mergedTemplates, revitYear); + + // Script constants + string title = null, author = null, doc = null; + string scriptContext = null, scriptHelpUrl = null, scriptHighlight = null; + string scriptMinRevitVersion = null, scriptMaxRevitVersion = null; + bool scriptIsBeta = false, scriptCleanEngine = false, scriptFullFrameEngine = false, scriptPersistentEngine = false; + Dictionary scriptLocalizedTitles = null; + Dictionary scriptLocalizedTooltips = null; + Dictionary scriptLocalizedHelpUrls = null; + + if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + var scriptConstants = ReadPythonScriptConstants(scriptPath); + title = scriptConstants.Title; + scriptLocalizedTitles = scriptConstants.LocalizedTitles; + author = scriptConstants.Author; + doc = scriptConstants.Doc; + scriptLocalizedTooltips = scriptConstants.LocalizedTooltips; + scriptContext = scriptConstants.Context; + scriptHelpUrl = scriptConstants.HelpUrl; + scriptLocalizedHelpUrls = scriptConstants.LocalizedHelpUrls; + scriptHighlight = scriptConstants.Highlight; + scriptMinRevitVersion = scriptConstants.MinRevitVersion; + scriptMaxRevitVersion = scriptConstants.MaxRevitVersion; + scriptIsBeta = scriptConstants.IsBeta; + scriptCleanEngine = scriptConstants.CleanEngine; + scriptFullFrameEngine = scriptConstants.FullFrameEngine; + scriptPersistentEngine = scriptConstants.PersistentEngine; + } - if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + // Bundle overrides + if (bundleInComponent != null) + { + var bundleTitle = GetLocalizedValue(bundleInComponent.Titles); + if (string.IsNullOrEmpty(bundleTitle) && + bundleInComponent.Titles != null && + bundleInComponent.Titles.TryGetValue("en_us", out var bundleTitleEnUs)) { - var scriptConstants = ReadPythonScriptConstants(scriptPath); - title = scriptConstants.Title; - scriptLocalizedTitles = scriptConstants.LocalizedTitles; - author = scriptConstants.Author; - doc = scriptConstants.Doc; - scriptLocalizedTooltips = scriptConstants.LocalizedTooltips; - scriptContext = scriptConstants.Context; - scriptHelpUrl = scriptConstants.HelpUrl; - scriptLocalizedHelpUrls = scriptConstants.LocalizedHelpUrls; - scriptHighlight = scriptConstants.Highlight; - scriptMinRevitVersion = scriptConstants.MinRevitVersion; - scriptMaxRevitVersion = scriptConstants.MaxRevitVersion; - scriptIsBeta = scriptConstants.IsBeta; - scriptCleanEngine = scriptConstants.CleanEngine; - scriptFullFrameEngine = scriptConstants.FullFrameEngine; - scriptPersistentEngine = scriptConstants.PersistentEngine; + bundleTitle = bundleTitleEnUs; } - // Override script values with bundle values (bundle takes precedence) - if (bundleInComponent != null) + var bundleTooltip = GetLocalizedValue(bundleInComponent.Tooltips); + if (string.IsNullOrEmpty(bundleTooltip) && + bundleInComponent.Tooltips != null && + bundleInComponent.Tooltips.TryGetValue("en_us", out var bundleTooltipEnUs)) { - // Use default locale for initial title/tooltip assignment - var bundleTitle = GetLocalizedValue(bundleInComponent.Titles); - if (string.IsNullOrEmpty(bundleTitle) && - bundleInComponent.Titles != null && - bundleInComponent.Titles.TryGetValue("en_us", out var bundleTitleEnUs)) - { - bundleTitle = bundleTitleEnUs; - } + bundleTooltip = bundleTooltipEnUs; + } - var bundleTooltip = GetLocalizedValue(bundleInComponent.Tooltips); - if (string.IsNullOrEmpty(bundleTooltip) && - bundleInComponent.Tooltips != null && - bundleInComponent.Tooltips.TryGetValue("en_us", out var bundleTooltipEnUs)) - { - bundleTooltip = bundleTooltipEnUs; - } + if (!string.IsNullOrEmpty(bundleTitle)) + title = bundleTitle; + if (!string.IsNullOrEmpty(bundleTooltip)) + doc = bundleTooltip; + if (!string.IsNullOrEmpty(bundleInComponent.Author)) + author = bundleInComponent.Author; + } - if (!string.IsNullOrEmpty(bundleTitle)) - title = bundleTitle; + // Merge localized values + var finalLocalizedTitles = scriptLocalizedTitles ?? new Dictionary(); + var finalLocalizedTooltips = scriptLocalizedTooltips ?? new Dictionary(); + var finalLocalizedHelpUrls = scriptLocalizedHelpUrls ?? new Dictionary(); + + if (bundleInComponent?.Titles != null) + { + foreach (var kvp in bundleInComponent.Titles) + finalLocalizedTitles[kvp.Key] = kvp.Value; + } + if (bundleInComponent?.Tooltips != null) + { + foreach (var kvp in bundleInComponent.Tooltips) + finalLocalizedTooltips[kvp.Key] = kvp.Value; + } + if (bundleInComponent?.HelpUrls != null) + { + foreach (var kvp in bundleInComponent.HelpUrls) + finalLocalizedHelpUrls[kvp.Key] = kvp.Value; + } - if (!string.IsNullOrEmpty(bundleTooltip)) - doc = bundleTooltip; + // Template substitution + title = SubstituteTemplates(title, mergedTemplates); + doc = SubstituteTemplates(doc, mergedTemplates); + author = SubstituteTemplates(author, mergedTemplates); + var hyperlink = SubstituteTemplates(bundleInComponent?.Hyperlink, mergedTemplates); + var bundleHelpUrl = SubstituteTemplates(bundleInComponent?.HelpUrl, mergedTemplates); + scriptHelpUrl = SubstituteTemplates(scriptHelpUrl, mergedTemplates); + + finalLocalizedTitles = SubstituteTemplatesInDict(finalLocalizedTitles, mergedTemplates); + finalLocalizedTooltips = SubstituteTemplatesInDict(finalLocalizedTooltips, mergedTemplates); + finalLocalizedHelpUrls = SubstituteTemplatesInDict(finalLocalizedHelpUrls, mergedTemplates); + + // Context + string finalContext; + var bundleContext = bundleInComponent?.GetFormattedContext(); + if (bundleInComponent != null && + (bundleInComponent.ContextItems?.Count > 0 || + bundleInComponent.ContextRules?.Count > 0 || + !string.IsNullOrEmpty(bundleInComponent.Context))) + { + finalContext = bundleContext; + } + else if (!string.IsNullOrEmpty(scriptContext)) + { + finalContext = scriptContext; + } + else + { + finalContext = null; + } - if (!string.IsNullOrEmpty(bundleInComponent.Author)) - author = bundleInComponent.Author; - } + string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) + ? bundleInComponent.Highlight + : scriptHighlight; - // Merge localized values: bundle takes precedence over script - var finalLocalizedTitles = scriptLocalizedTitles ?? new Dictionary(); - var finalLocalizedTooltips = scriptLocalizedTooltips ?? new Dictionary(); - var finalLocalizedHelpUrls = scriptLocalizedHelpUrls ?? new Dictionary(); + string finalHelpUrl = !string.IsNullOrEmpty(bundleHelpUrl) + ? bundleHelpUrl + : scriptHelpUrl; - // If bundle has localized values, they override script values - if (bundleInComponent?.Titles != null) - { - foreach (var kvp in bundleInComponent.Titles) - { - finalLocalizedTitles[kvp.Key] = kvp.Value; - } - } + string finalHyperlink = !string.IsNullOrEmpty(hyperlink) ? hyperlink : scriptHelpUrl; - if (bundleInComponent?.Tooltips != null) - { - foreach (var kvp in bundleInComponent.Tooltips) - { - finalLocalizedTooltips[kvp.Key] = kvp.Value; - } - } + string finalMinRevitVersion = !string.IsNullOrEmpty(bundleInComponent?.MinRevitVersion) + ? bundleInComponent.MinRevitVersion + : scriptMinRevitVersion; - if (bundleInComponent?.HelpUrls != null) - { - foreach (var kvp in bundleInComponent.HelpUrls) - { - finalLocalizedHelpUrls[kvp.Key] = kvp.Value; - } - } + string finalMaxRevitVersion = !string.IsNullOrEmpty(bundleInComponent?.MaxRevitVersion) + ? bundleInComponent.MaxRevitVersion + : scriptMaxRevitVersion; - // Apply template substitution to string values - title = SubstituteTemplates(title, mergedTemplates); - doc = SubstituteTemplates(doc, mergedTemplates); - author = SubstituteTemplates(author, mergedTemplates); - var hyperlink = SubstituteTemplates(bundleInComponent?.Hyperlink, mergedTemplates); - var bundleHelpUrl = SubstituteTemplates(bundleInComponent?.HelpUrl, mergedTemplates); - scriptHelpUrl = SubstituteTemplates(scriptHelpUrl, mergedTemplates); - - // Apply template substitution to localized values - finalLocalizedTitles = SubstituteTemplatesInDict(finalLocalizedTitles, mergedTemplates); - finalLocalizedTooltips = SubstituteTemplatesInDict(finalLocalizedTooltips, mergedTemplates); - finalLocalizedHelpUrls = SubstituteTemplatesInDict(finalLocalizedHelpUrls, mergedTemplates); - - // Determine final context: bundle takes precedence over script - // bundleInComponent?.GetFormattedContext() returns "(zero-doc)" when no context in bundle - // so we need to check if there's actually a context defined in the bundle - string finalContext; - var bundleContext = bundleInComponent?.GetFormattedContext(); - if (bundleInComponent != null && - (bundleInComponent.ContextItems?.Count > 0 || - bundleInComponent.ContextRules?.Count > 0 || - !string.IsNullOrEmpty(bundleInComponent.Context))) - { - // Bundle has explicit context defined - finalContext = bundleContext; - } - else if (!string.IsNullOrEmpty(scriptContext)) - { - // Use script context - finalContext = scriptContext; - } - else - { - // No context defined - button will always be available (no availability class) - finalContext = null; - } + bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta + ? bundleInComponent.IsBeta + : scriptIsBeta; - // Determine final highlight: bundle takes precedence over script - string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) - ? bundleInComponent.Highlight - : scriptHighlight; - - // Determine final help URL: bundle helpurl takes precedence over script helpurl - string finalHelpUrl = !string.IsNullOrEmpty(bundleHelpUrl) - ? bundleHelpUrl - : scriptHelpUrl; - - // Determine final help URL: bundle hyperlink takes precedence over script helpurl - string finalHyperlink = !string.IsNullOrEmpty(hyperlink) ? hyperlink : scriptHelpUrl; - - // Determine final min Revit version: bundle takes precedence over script - string finalMinRevitVersion = !string.IsNullOrEmpty(bundleInComponent?.MinRevitVersion) - ? bundleInComponent.MinRevitVersion - : scriptMinRevitVersion; - - // Determine final max Revit version: bundle takes precedence over script - string finalMaxRevitVersion = !string.IsNullOrEmpty(bundleInComponent?.MaxRevitVersion) - ? bundleInComponent.MaxRevitVersion - : scriptMaxRevitVersion; - - // Determine final beta status: bundle takes precedence over script - bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta - ? bundleInComponent.IsBeta - : scriptIsBeta; - - // Determine final engine config: bundle takes precedence, but script can add flags - var finalEngine = bundleInComponent?.Engine ?? new EngineConfig(); - if (scriptCleanEngine) finalEngine.Clean = true; - if (scriptFullFrameEngine) finalEngine.FullFrame = true; - if (scriptPersistentEngine) finalEngine.Persistent = true; - - // Component-level version gate: skip this component (and its children) if it declares - // a version range that doesn't include the running Revit year. - if (!IsRevitVersionCompatible(finalMinRevitVersion, finalMaxRevitVersion, revitYear, displayName)) - continue; + var finalEngine = bundleInComponent?.Engine ?? new EngineConfig(); + if (scriptCleanEngine) finalEngine.Clean = true; + if (scriptFullFrameEngine) finalEngine.FullFrame = true; + if (scriptPersistentEngine) finalEngine.Persistent = true; - components.Add(new ParsedComponent - { - Name = namePart, - DisplayName = displayName, - ScriptPath = scriptPath, - ConfigScriptPath = configScriptPath, - Tooltip = doc ?? "", - UniqueId = SanitizeClassName(fullPath.ToLowerInvariant()), - Type = componentType, - Children = children, - BundleFile = FileExists(bundleFile) ? bundleFile : null, - LayoutOrder = bundleInComponent?.LayoutOrder, - LayoutItemTitles = bundleInComponent?.LayoutItemTitles, - LayoutDirectives = bundleInComponent?.LayoutDirectives, - Title = title, - Author = author, - Context = finalContext, - Hyperlink = finalHyperlink, - HelpUrl = finalHelpUrl, - Highlight = finalHighlight, - MinRevitVersion = finalMinRevitVersion, - MaxRevitVersion = finalMaxRevitVersion, - IsBeta = finalIsBeta, - Collapsed = bundleInComponent?.Collapsed ?? false, - InheritIcon = bundleInComponent?.InheritIcon ?? true, - LargeIcon = bundleInComponent?.LargeIcon ?? false, - PanelBackground = bundleInComponent?.PanelBackground, - TitleBackground = bundleInComponent?.TitleBackground, - SlideoutBackground = bundleInComponent?.SlideoutBackground, - Icons = ParseIconsForComponent(dir), - TargetAssembly = bundleInComponent?.Assembly, - CommandClass = bundleInComponent?.CommandClass, - AvailabilityClass = bundleInComponent?.AvailabilityClass, - Modules = bundleInComponent?.Modules ?? new List(), - LocalizedTitles = finalLocalizedTitles.Count > 0 ? finalLocalizedTitles : null, - LocalizedTooltips = finalLocalizedTooltips.Count > 0 ? finalLocalizedTooltips : null, - LocalizedHelpUrls = finalLocalizedHelpUrls.Count > 0 ? finalLocalizedHelpUrls : null, - Directory = dir, - Engine = finalEngine, - Members = bundleInComponent?.Members ?? new List(), - OnIconPath = onIconPath, - OnIconDarkPath = onIconDarkPath, - OffIconPath = offIconPath, - OffIconDarkPath = offIconDarkPath, - MediaFile = mediaFile, - HelpFile = helpFile - }); - } + // Version gate + if (!IsRevitVersionCompatible(finalMinRevitVersion, finalMaxRevitVersion, revitYear, displayName)) + return null; - return components; + return new ParsedComponent + { + Name = namePart, + DisplayName = displayName, + ScriptPath = scriptPath, + ConfigScriptPath = configScriptPath, + Tooltip = doc ?? "", + UniqueId = SanitizeClassName(uniquePath.ToLowerInvariant()), + Type = componentType, + Children = children, + BundleFile = FileExists(bundleFile) ? bundleFile : null, + LayoutOrder = bundleInComponent?.LayoutOrder, + LayoutItemTitles = bundleInComponent?.LayoutItemTitles, + LayoutDirectives = bundleInComponent?.LayoutDirectives, + Title = title, + Author = author, + Context = finalContext, + Hyperlink = finalHyperlink, + HelpUrl = finalHelpUrl, + Highlight = finalHighlight, + MinRevitVersion = finalMinRevitVersion, + MaxRevitVersion = finalMaxRevitVersion, + IsBeta = finalIsBeta, + Collapsed = bundleInComponent?.Collapsed ?? false, + InheritIcon = bundleInComponent?.InheritIcon ?? true, + LargeIcon = bundleInComponent?.LargeIcon ?? false, + PanelBackground = bundleInComponent?.PanelBackground, + TitleBackground = bundleInComponent?.TitleBackground, + SlideoutBackground = bundleInComponent?.SlideoutBackground, + Icons = ParseIconsForComponent(dir), + TargetAssembly = bundleInComponent?.Assembly, + CommandClass = bundleInComponent?.CommandClass, + AvailabilityClass = bundleInComponent?.AvailabilityClass, + Modules = bundleInComponent?.Modules ?? new List(), + LocalizedTitles = finalLocalizedTitles.Count > 0 ? finalLocalizedTitles : null, + LocalizedTooltips = finalLocalizedTooltips.Count > 0 ? finalLocalizedTooltips : null, + LocalizedHelpUrls = finalLocalizedHelpUrls.Count > 0 ? finalLocalizedHelpUrls : null, + Directory = dir, + Engine = finalEngine, + Members = bundleInComponent?.Members ?? new List(), + OnIconPath = onIconPath, + OnIconDarkPath = onIconDarkPath, + OffIconPath = offIconPath, + OffIconDarkPath = offIconDarkPath, + MediaFile = mediaFile, + HelpFile = helpFile + }; } + /// /// Gets a localized value from a dictionary, falling back to the default locale, then to the first available value. /// This is the public API for getting localized values. diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/LayoutParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/LayoutParser.cs new file mode 100644 index 0000000000..e36a48516a --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParser/LayoutParser.cs @@ -0,0 +1,715 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.RepresentationModel; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParser +{ + /// + /// Result of parsing an extension layout. + /// + public class LayoutParseResult + { + /// Tabs that form the ribbon UI hierarchy. + public List Tabs { get; set; } = new List(); + + /// + /// Tools from the tool index that are NOT referenced in the layout. + /// These should be compiled into the assembly but not shown in the ribbon, + /// so that layout changes don't require a new assembly build. + /// + public List UnreferencedTools { get; set; } = new List(); + } + + /// + /// Parses extension_layout.yaml files to build the component tree. + /// When an extension has an extension_layout.yaml, tools are discovered + /// from a flat tools/ directory and arranged into tabs/panels based on + /// the YAML layout declarations. + /// + public static class LayoutParser + { + private const string LayoutFileName = "extension_layout.yaml"; + private const string ToolsDirName = "tools"; + + // YAML keys + private const string TabsKey = "tabs"; + private const string PanelsKey = "panels"; + private const string NameKey = "name"; + private const string TitleKey = "title"; + private const string LayoutKey = "layout"; + private const string LayoutFileKey = "layout_file"; + private const string StackKey = "stack"; + + /// + /// Checks whether an extension directory has an extension_layout.yaml file, + /// or a custom layout path configured. + /// + public static bool HasLayoutFile(string extensionDir) + { + if (string.IsNullOrEmpty(extensionDir)) + return false; + + // Check custom layout path in config first + var extName = Path.GetFileNameWithoutExtension(extensionDir); + var config = ExtensionParser.GetConfigInternal(); + var customPath = config?.GetCustomLayoutPath(extName); + if (!string.IsNullOrEmpty(customPath)) + return true; + + // Check bundled layout file + var layoutPath = Path.Combine(extensionDir, LayoutFileName); + return File.Exists(layoutPath); + } + + /// + /// Gets the path to the layout file for the given extension directory. + /// Resolution order: + /// 1. Custom layout path from user config (per extension) + /// 2. Bundled extension_layout.yaml in extension root + /// 3. null (legacy mode) + /// + public static string GetLayoutFilePath(string extensionDir) + { + if (string.IsNullOrEmpty(extensionDir)) + return null; + + // Check custom layout path in config first + var extName = Path.GetFileNameWithoutExtension(extensionDir); + var config = ExtensionParser.GetConfigInternal(); + var customPath = config?.GetCustomLayoutPath(extName); + if (!string.IsNullOrEmpty(customPath)) + return customPath; + + // Bundled layout file + var layoutPath = Path.Combine(extensionDir, LayoutFileName); + return File.Exists(layoutPath) ? layoutPath : null; + } + + /// + /// Parses the extension using layout-based discovery. + /// Scans tools/ for recognized bundles, then arranges them into + /// the tab/panel structure declared in extension_layout.yaml. + /// + /// Path to the .extension directory + /// Name of the extension (without .extension suffix) + /// Templates inherited from extension bundle.yaml + /// Running Revit version year (0 to skip filtering) + /// List of ParsedComponent (tabs) representing the layout tree + public static LayoutParseResult ParseLayout( + string extensionDir, + string extensionName, + Dictionary inheritedTemplates, + int revitYear) + { + // Resolve layout file (custom path > bundled) + var layoutPath = GetLayoutFilePath(extensionDir) + ?? Path.Combine(extensionDir, LayoutFileName); + var toolsDir = Path.Combine(extensionDir, ToolsDirName); + + // Build tool index from tools/ directory + var toolIndex = BuildToolIndex(toolsDir, extensionName, inheritedTemplates, revitYear); + + // Also scan legacy folder structure (.tab/.panel/) for tool bundles + ScanLegacyDirectoryForTools(extensionDir, extensionName, inheritedTemplates, revitYear, toolIndex); + + // Parse the layout YAML + var layoutYaml = LoadYaml(layoutPath); + if (layoutYaml == null) + return new LayoutParseResult(); + + // Track which tools are referenced by the layout + var referencedTools = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Build the component tree from layout + var layoutDir = Path.GetDirectoryName(layoutPath); + var tabs = BuildComponentTree(layoutYaml, extensionDir, layoutDir, extensionName, toolIndex, referencedTools); + + // Collect tools that exist on disk but aren't in the layout. + // These will be compiled into the assembly but not shown in the ribbon. + var unreferenced = toolIndex + .Where(kvp => !referencedTools.Contains(kvp.Key)) + .Select(kvp => kvp.Value) + .ToList(); + + return new LayoutParseResult { Tabs = tabs, UnreferencedTools = unreferenced }; + } + + #region Tool Index + + /// + /// Scans the tools/ directory recursively for recognized bundles. + /// Returns a dictionary mapping tool name to its ParsedComponent. + /// + private static Dictionary BuildToolIndex( + string toolsDir, + string extensionName, + Dictionary inheritedTemplates, + int revitYear) + { + var index = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(toolsDir) || !Directory.Exists(toolsDir)) + return index; + + ScanToolDirectory(toolsDir, extensionName, inheritedTemplates, revitYear, index); + + ExtensionParser.LogDebug( + $"Layout: Built tool index with {index.Count} entries from: {toolsDir}"); + + return index; + } + + /// + /// Recursively scans a directory for tool bundles. + /// Recognized bundles (directories with known postfixes) are indexed. + /// Plain directories (no recognized postfix) are recursed into as + /// organizational folders. + /// + private static void ScanToolDirectory( + string searchDir, + string extensionName, + Dictionary inheritedTemplates, + int revitYear, + Dictionary index) + { + string[] dirs; + try + { + dirs = Directory.GetDirectories(searchDir); + } + catch (Exception ex) + { + ExtensionParser.LogError($"Layout: Cannot list directory {searchDir}: {ex.Message}"); + return; + } + + foreach (var dir in dirs) + { + var dirName = Path.GetFileName(dir); + + // Skip hidden/private entries + if (dirName.StartsWith(".") || dirName.StartsWith("_")) + continue; + + var ext = Path.GetExtension(dir); + var componentType = CommandComponentTypeExtensions.FromExtension(ext); + + if (componentType != CommandComponentType.Unknown) + { + // This is a recognized tool bundle - index it + IndexTool(dir, componentType, extensionName, inheritedTemplates, revitYear, index); + } + else + { + // Plain organizational folder - recurse into it + ScanToolDirectory(dir, extensionName, inheritedTemplates, revitYear, index); + } + } + } + + /// + /// Scans the legacy folder structure (.tab/.panel/ hierarchy) for tool bundles + /// and adds them to the existing tool index. This allows layout YAML files to + /// reference tools regardless of whether they live in tools/ or the legacy structure. + /// Containers (.tab, .panel) are recursed into but not indexed themselves. + /// Duplicates are skipped (tools/ wins since it's indexed first). + /// + private static void ScanLegacyDirectoryForTools( + string extensionDir, + string extensionName, + Dictionary inheritedTemplates, + int revitYear, + Dictionary index) + { + if (string.IsNullOrEmpty(extensionDir) || !Directory.Exists(extensionDir)) + return; + + var toolsDir = Path.Combine(extensionDir, ToolsDirName); + ScanLegacySubdirectory(extensionDir, toolsDir, extensionName, inheritedTemplates, revitYear, index); + + ExtensionParser.LogDebug( + $"Layout: After legacy scan, tool index has {index.Count} total entries"); + } + + /// + /// Recursively scans a directory in the legacy structure. + /// .tab and .panel directories are recursed into (containers). + /// Tool bundles (.pushbutton, .pulldown, etc.) are indexed. + /// Plain directories without recognized postfixes are skipped. + /// + private static void ScanLegacySubdirectory( + string searchDir, + string toolsDirToSkip, + string extensionName, + Dictionary inheritedTemplates, + int revitYear, + Dictionary index) + { + string[] dirs; + try + { + dirs = Directory.GetDirectories(searchDir); + } + catch (Exception ex) + { + ExtensionParser.LogError($"Layout: Cannot list directory {searchDir}: {ex.Message}"); + return; + } + + foreach (var dir in dirs) + { + var dirName = Path.GetFileName(dir); + + // Skip hidden/private entries + if (dirName.StartsWith(".") || dirName.StartsWith("_")) + continue; + + // Skip the tools/ directory (already indexed) + if (!string.IsNullOrEmpty(toolsDirToSkip) && + string.Equals(Path.GetFullPath(dir), Path.GetFullPath(toolsDirToSkip), + StringComparison.OrdinalIgnoreCase)) + continue; + + var ext = Path.GetExtension(dir); + var componentType = CommandComponentTypeExtensions.FromExtension(ext); + + if (componentType == CommandComponentType.Tab || + componentType == CommandComponentType.Panel || + componentType == CommandComponentType.Stack) + { + // Structural container - recurse into it + ScanLegacySubdirectory(dir, null, extensionName, inheritedTemplates, revitYear, index); + } + else if (componentType != CommandComponentType.Unknown) + { + // Tool bundle - index it (IndexTool skips duplicates) + IndexTool(dir, componentType, extensionName, inheritedTemplates, revitYear, index); + } + // Plain directories (Unknown) are skipped in legacy mode + } + } + + /// + /// Creates a ParsedComponent from a tool bundle directory and adds it to the index. + /// For container tools (pulldown, splitbutton), also parses their children. + /// + private static void IndexTool( + string toolPath, + CommandComponentType componentType, + string extensionName, + Dictionary inheritedTemplates, + int revitYear, + Dictionary index) + { + var toolName = Path.GetFileNameWithoutExtension(toolPath); + var displayName = toolName; + + // Check for duplicate names + if (index.ContainsKey(toolName)) + { + ExtensionParser.LogWarning( + $"Layout: Duplicate tool name \"{toolName}\" found at {toolPath}. " + + $"Skipping (first occurrence wins)."); + return; + } + + // Use the standard ParseComponents logic to fully parse this single bundle. + // We parse the tool directory as if it were the only entry in a parent directory. + // This reuses all existing script detection, bundle.yaml parsing, icon handling, etc. + var parsed = ParseSingleBundle(toolPath, componentType, extensionName, toolName, + inheritedTemplates, revitYear); + + if (parsed == null) + return; + + index[toolName] = parsed; + ExtensionParser.LogDebug( + $"Layout: Indexed tool \"{toolName}\" ({componentType}) from {toolPath}"); + } + + /// + /// Parses a single tool bundle directory into a ParsedComponent. + /// This delegates to ExtensionParser.ParseComponentSingle to reuse + /// all existing bundle.yaml, script detection, and icon parsing logic. + /// + private static ParsedComponent ParseSingleBundle( + string bundleDir, + CommandComponentType componentType, + string extensionName, + string toolName, + Dictionary inheritedTemplates, + int revitYear) + { + // Build the unique ID path in layout format: extensionname_toolname + var uniquePath = $"{extensionName}_{toolName}"; + + // Use the shared single-bundle parser + return ExtensionParser.ParseSingleBundle( + bundleDir, componentType, extensionName, uniquePath, + inheritedTemplates, revitYear); + } + + #endregion + + #region YAML Parsing + + /// + /// Loads and parses a YAML file into a YamlMappingNode. + /// + private static YamlMappingNode LoadYaml(string yamlPath) + { + if (!File.Exists(yamlPath)) + return null; + + try + { + var yamlText = File.ReadAllText(yamlPath); + var stream = new YamlStream(); + using (var reader = new StringReader(yamlText)) + { + stream.Load(reader); + } + + if (stream.Documents.Count < 1) + return null; + + return stream.Documents[0].RootNode as YamlMappingNode; + } + catch (Exception ex) + { + ExtensionParser.LogError( + $"Layout: Failed to parse YAML file {yamlPath}: {ex.Message}"); + return null; + } + } + + /// + /// Gets a scalar string value from a YAML node. + /// + private static string GetScalar(YamlNode node) + { + return (node as YamlScalarNode)?.Value; + } + + /// + /// Gets a child node from a mapping node by key. + /// + private static YamlNode GetMappingValue(YamlMappingNode mapping, string key) + { + foreach (var entry in mapping.Children) + { + if (string.Equals(GetScalar(entry.Key), key, StringComparison.OrdinalIgnoreCase)) + return entry.Value; + } + return null; + } + + #endregion + + #region Component Tree Building + + /// + /// Builds the full component tree (tabs > panels > tools) from the layout YAML. + /// + private static List BuildComponentTree( + YamlMappingNode layoutRoot, + string extensionDir, + string layoutDir, + string extensionName, + Dictionary toolIndex, + HashSet referencedTools) + { + var tabs = new List(); + + var tabsNode = GetMappingValue(layoutRoot, TabsKey) as YamlSequenceNode; + if (tabsNode == null) + { + ExtensionParser.LogWarning( + $"Layout: No 'tabs' key found in layout file for {extensionName}"); + return tabs; + } + + foreach (var tabNode in tabsNode.Children) + { + var tabMapping = tabNode as YamlMappingNode; + if (tabMapping == null) + continue; + + var tab = CreateTab(tabMapping, extensionDir, layoutDir, extensionName, toolIndex, referencedTools); + if (tab != null) + tabs.Add(tab); + } + + return tabs; + } + + /// + /// Creates a Tab component from a YAML tab entry. + /// + private static ParsedComponent CreateTab( + YamlMappingNode tabMapping, + string extensionDir, + string layoutDir, + string extensionName, + Dictionary toolIndex, + HashSet referencedTools) + { + var name = GetScalar(GetMappingValue(tabMapping, NameKey)); + if (string.IsNullOrEmpty(name)) + { + ExtensionParser.LogWarning("Layout: Tab entry missing 'name' field"); + return null; + } + + var title = GetScalar(GetMappingValue(tabMapping, TitleKey)) ?? name; + var namePart = name.Replace(" ", ""); + var uniqueId = ExtensionParser.SanitizeClassName( + $"{extensionName}_{namePart}".ToLowerInvariant()); + + var tab = new ParsedComponent + { + Name = namePart, + DisplayName = name, + Title = title, + Type = CommandComponentType.Tab, + UniqueId = uniqueId, + Directory = extensionDir, + Children = new List() + }; + + // Parse panels + var panelsNode = GetMappingValue(tabMapping, PanelsKey) as YamlSequenceNode; + if (panelsNode == null) + return tab; + + foreach (var panelNode in panelsNode.Children) + { + var panelMapping = panelNode as YamlMappingNode; + if (panelMapping == null) + continue; + + var panel = CreatePanel(panelMapping, extensionDir, layoutDir, extensionName, toolIndex, referencedTools); + if (panel != null) + tab.Children.Add(panel); + } + + return tab; + } + + /// + /// Creates a Panel component from a YAML panel entry. + /// Supports both inline layout and external .panel.yaml files. + /// + private static ParsedComponent CreatePanel( + YamlMappingNode panelMapping, + string extensionDir, + string layoutDir, + string extensionName, + Dictionary toolIndex, + HashSet referencedTools) + { + var name = GetScalar(GetMappingValue(panelMapping, NameKey)); + if (string.IsNullOrEmpty(name)) + { + ExtensionParser.LogWarning("Layout: Panel entry missing 'name' field"); + return null; + } + + var title = GetScalar(GetMappingValue(panelMapping, TitleKey)) ?? name; + var namePart = name.Replace(" ", ""); + var uniqueId = ExtensionParser.SanitizeClassName( + $"{extensionName}_{namePart}".ToLowerInvariant()); + + var panel = new ParsedComponent + { + Name = namePart, + DisplayName = name, + Title = title, + Type = CommandComponentType.Panel, + UniqueId = uniqueId, + Directory = extensionDir, + Children = new List() + }; + + // Determine layout source: external file or inline + YamlSequenceNode layoutList = null; + + var layoutFileName = GetScalar(GetMappingValue(panelMapping, LayoutFileKey)); + if (!string.IsNullOrEmpty(layoutFileName)) + { + // Try layout directory first (for custom cached layouts), fall back to extension dir + var panelLayoutPath = Path.Combine(layoutDir, layoutFileName); + if (!File.Exists(panelLayoutPath)) + panelLayoutPath = Path.Combine(extensionDir, layoutFileName); + layoutList = LoadPanelLayoutFile(panelLayoutPath); + } + + if (layoutList == null) + { + // Try inline layout + layoutList = GetMappingValue(panelMapping, LayoutKey) as YamlSequenceNode; + } + + if (layoutList != null) + { + PopulatePanel(panel, layoutList, extensionName, toolIndex, referencedTools); + } + + return panel; + } + + /// + /// Loads a .panel.yaml file and returns the layout sequence node. + /// + private static YamlSequenceNode LoadPanelLayoutFile(string panelLayoutPath) + { + var panelYaml = LoadYaml(panelLayoutPath); + if (panelYaml == null) + { + ExtensionParser.LogWarning( + $"Layout: Panel layout file not found or invalid: {panelLayoutPath}"); + return null; + } + + return GetMappingValue(panelYaml, LayoutKey) as YamlSequenceNode; + } + + /// + /// Populates a panel's children from a layout sequence. + /// Each entry in the layout can be: + /// - A string (tool name reference, separator "---", or slideout ">>>") + /// - A mapping with a "stack:" key (creates a stack group) + /// + private static void PopulatePanel( + ParsedComponent panel, + YamlSequenceNode layoutList, + string extensionName, + Dictionary toolIndex, + HashSet referencedTools) + { + int stackCounter = 0; + foreach (var item in layoutList.Children) + { + if (item is YamlScalarNode scalarNode) + { + var value = scalarNode.Value; + if (string.IsNullOrEmpty(value)) + continue; + + var trimmed = value.Trim(); + if (trimmed == "---") + { + // Separator + panel.Children.Add(new ParsedComponent + { + Name = "---", + DisplayName = "---", + Type = CommandComponentType.Separator, + Directory = panel.Directory + }); + } + else if (trimmed == ">>>") + { + // Slideout marker + panel.Children.Add(new ParsedComponent + { + Name = ">>>", + DisplayName = ">>>", + Type = CommandComponentType.Separator, + HasSlideout = true, + Directory = panel.Directory + }); + } + else + { + // Tool name reference - look up in index + if (toolIndex.TryGetValue(trimmed, out var tool)) + { + panel.Children.Add(tool); + referencedTools.Add(trimmed); + } + else + { + ExtensionParser.LogWarning( + $"Layout: Tool \"{trimmed}\" referenced in panel " + + $"\"{panel.DisplayName}\" not found in tools/ directory"); + } + } + } + else if (item is YamlMappingNode mappingNode) + { + // Check for stack: key + var stackNode = GetMappingValue(mappingNode, StackKey) as YamlSequenceNode; + if (stackNode != null) + { + var stack = CreateStack(stackNode, panel, extensionName, toolIndex, referencedTools, stackCounter); + if (stack != null) + { + panel.Children.Add(stack); + stackCounter++; + } + } + } + } + } + + /// + /// Creates a Stack component from a YAML stack entry. + /// A stack groups 2-3 buttons vertically. + /// + private static ParsedComponent CreateStack( + YamlSequenceNode stackItems, + ParsedComponent parentPanel, + string extensionName, + Dictionary toolIndex, + HashSet referencedTools, + int stackIndex) + { + var stackChildren = new List(); + + foreach (var stackItem in stackItems.Children) + { + var toolName = GetScalar(stackItem); + if (string.IsNullOrEmpty(toolName)) + continue; + + if (toolIndex.TryGetValue(toolName, out var tool)) + { + stackChildren.Add(tool); + referencedTools.Add(toolName); + } + else + { + ExtensionParser.LogWarning( + $"Layout: Tool \"{toolName}\" referenced in stack " + + $"(panel \"{parentPanel.DisplayName}\") not found in tools/ directory"); + } + } + + if (stackChildren.Count == 0) + return null; + + // Use a stable per-panel counter so two stacks can't collide + // when concatenated child names happen to match (e.g. ["A","BC"] + // and ["AB","C"] would both yield "ABC"). + var stackName = $"_stack_{stackIndex}"; + var uniqueId = ExtensionParser.SanitizeClassName( + $"{extensionName}_{parentPanel.Name}_{stackName}".ToLowerInvariant()); + + return new ParsedComponent + { + Name = stackName, + DisplayName = stackName, + Type = CommandComponentType.Stack, + UniqueId = uniqueId, + Directory = parentPanel.Directory, + Children = stackChildren + }; + } + + #endregion + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs index 1c837ebd4d..876f55cf1f 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ParsedExtension.cs @@ -30,6 +30,14 @@ public class ParsedExtension : ParsedComponent /// public List ExternalLayoutDirectives { get; set; } = new List(); + /// + /// Tools from the tool index that are NOT referenced in the layout YAML. + /// These are included in assembly generation but NOT shown in the ribbon UI. + /// This ensures all tools have compiled command types available, so adding + /// a tool to the layout later doesn't require a new assembly build. + /// + public List AssemblyOnlyComponents { get; set; } + /// /// Gets or sets the list of users authorized to access this extension. /// When populated, only users in this list can use the extension. @@ -76,6 +84,7 @@ private bool DirExists(string path) private static readonly HashSet _hashDirSuffixes = new HashSet(StringComparer.OrdinalIgnoreCase) { + ".extension", ".tab", ".panel", ".pulldown", @@ -93,7 +102,8 @@ private bool DirExists(string path) private static readonly HashSet _hashDirNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "lib", - "hooks" + "hooks", + "tools" }; /// @@ -265,6 +275,8 @@ public string StartupScript /// /// Collects all command components from this extension (cached after first call). /// Builds control IDs for each component based on hierarchy. + /// Also includes assembly-only components (tools not in the layout but still + /// compiled into the DLL so they're available if the layout changes). /// public IEnumerable CollectCommandComponents() { @@ -273,6 +285,14 @@ public IEnumerable CollectCommandComponents() _cachedCommandComponents = new List(); // Start with no parent control ID - tabs will be the first level CollectInto(_cachedCommandComponents, this.Children, null); + + // Include tools that exist on disk but aren't referenced in the layout. + // They won't appear in the ribbon, but their command types will be + // compiled into the assembly so layout changes don't require a rebuild. + if (AssemblyOnlyComponents != null) + { + CollectInto(_cachedCommandComponents, AssemblyOnlyComponents, null); + } } return _cachedCommandComponents; } diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs index 8c5080c695..9f257e08d1 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/PyRevitConfig.cs @@ -84,6 +84,23 @@ public string UserLocale } } + /// + /// Gets whether custom layouts are globally disabled. + /// When true, GetCustomLayoutPath always returns null (bundled layouts still apply). + /// + public bool DisableCustomLayouts + { + get + { + var value = _ini.IniReadValue("core", "disable_custom_layouts"); + return TryParseConfigBool(value, out var result) && result; + } + set + { + _ini.IniWriteValue("core", "disable_custom_layouts", value ? TrueString : FalseString); + } + } + /// /// Gets or sets whether the new loader architecture is enabled. /// @@ -607,6 +624,53 @@ public ExtensionConfig ParseExtensionByName(string extensionName) return null; // Return null if the extension is not found } + /// + /// Gets the custom layout file path for an extension, if configured. + /// + /// Extension name (without .extension suffix) + /// Path to custom layout file, or null if not configured or file doesn't exist + public string GetCustomLayoutPath(string extensionName) + { + if (string.IsNullOrEmpty(extensionName)) + return null; + + // Global toggle: skip custom layouts when disabled + if (DisableCustomLayouts) + return null; + + var section = $"{extensionName}.extension"; + var value = _ini.IniReadValue(section, "custom_layout_path"); + + if (string.IsNullOrEmpty(value)) + return null; + + value = value.Trim(); + // Strip quotes if present + if (value.Length >= 2 && + ((value[0] == '"' && value[value.Length - 1] == '"') || + (value[0] == '\'' && value[value.Length - 1] == '\''))) + { + value = value.Substring(1, value.Length - 2).Trim(); + } + + // Only return if file exists + return File.Exists(value) ? value : null; + } + + /// + /// Sets or clears the custom layout file path for an extension. + /// + /// Extension name (without .extension suffix) + /// Path to custom layout file, or null/empty to clear + public void SetCustomLayoutPath(string extensionName, string layoutPath) + { + if (string.IsNullOrEmpty(extensionName)) + return; + + var section = $"{extensionName}.extension"; + _ini.IniWriteValue(section, "custom_layout_path", layoutPath ?? string.Empty); + } + /// /// Parses booleans from INI values (matches Python/json-style and common variants). /// diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj index e484d26063..cfc8885adc 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj +++ b/dev/pyRevitLoader/pyRevitExtensionParser/pyRevitExtensionParser.csproj @@ -35,6 +35,7 @@ + diff --git a/extensions/pyRevitCore.extension/extension_layout.yaml b/extensions/pyRevitCore.extension/extension_layout.yaml new file mode 100644 index 0000000000..d0877725eb --- /dev/null +++ b/extensions/pyRevitCore.extension/extension_layout.yaml @@ -0,0 +1,27 @@ +tabs: + - name: pyRevit + panels: + - name: pyRevit + layout: + - stack: + - Search + - Spy + - Reload + - '>>>' + - About + - Update + - Extensions + - stack: + - Wiki + - Blog + - Repo + - stack: + - apidocs + - emojis + - rpw Docs + - stack: + - Icons8 + - RegEx Tool + - Report Bugs + - Settings + - Layout Builder diff --git a/extensions/pyRevitCore.extension/layouts/simple.layout.yaml b/extensions/pyRevitCore.extension/layouts/simple.layout.yaml new file mode 100644 index 0000000000..1f1a63e608 --- /dev/null +++ b/extensions/pyRevitCore.extension/layouts/simple.layout.yaml @@ -0,0 +1,14 @@ +tabs: +- name: pyRevit + panels: + - name: pyRevit + layout: + - Search + - '>>>' + - stack: + - About + - Update + - Extensions + - Settings + - Layout Builder + diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/bundle.yaml b/extensions/pyRevitCore.extension/pyRevit.tab/bundle.yaml deleted file mode 100644 index 0858eece69..0000000000 --- a/extensions/pyRevitCore.extension/pyRevit.tab/bundle.yaml +++ /dev/null @@ -1,2 +0,0 @@ -layout: - - pyRevit[beforeall:] \ No newline at end of file diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/bundle.yaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/bundle.yaml deleted file mode 100644 index a2b76ce356..0000000000 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/bundle.yaml +++ /dev/null @@ -1,18 +0,0 @@ -title: - en_us: pyRevit - fr_fr: pyRevit - ru: pyRevit - chinese_s: pyRevit - es_es: pyRevit - de_de: pyRevit - pt_br: pyRevit -layout: - - tools - - ">>>>>" - - About - - Update - - Extensions - - prlinks - - rpwlinks - - links - - Settings \ No newline at end of file diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/prlinks.stack/bundle.yaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/prlinks.stack/bundle.yaml deleted file mode 100644 index f208f2d704..0000000000 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/prlinks.stack/bundle.yaml +++ /dev/null @@ -1,4 +0,0 @@ -layout: - - Wiki - - Blog - - Repo \ No newline at end of file diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/tools.stack/bundle.yaml b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/tools.stack/bundle.yaml deleted file mode 100644 index 2d17f8f5c2..0000000000 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/tools.stack/bundle.yaml +++ /dev/null @@ -1,4 +0,0 @@ -layout: - - Search - - Spy - - Reload \ No newline at end of file diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.chinese_s.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.chinese_s.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.chinese_s.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.chinese_s.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.de_de.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.de_de.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.de_de.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.de_de.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.en_us.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.en_us.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.en_us.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.en_us.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.es_es.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.es_es.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.es_es.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.es_es.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.fr_fr.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.fr_fr.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.fr_fr.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.fr_fr.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.pt_br.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.pt_br.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.pt_br.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.pt_br.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.ru.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.ru.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.ResourceDictionary.ru.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.ResourceDictionary.ru.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.xaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/AboutWindow.xaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/AboutWindow.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py b/extensions/pyRevitCore.extension/tools/About.pushbutton/aboutscript.py similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/aboutscript.py rename to extensions/pyRevitCore.extension/tools/About.pushbutton/aboutscript.py diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/bundle.yaml b/extensions/pyRevitCore.extension/tools/About.pushbutton/bundle.yaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/bundle.yaml rename to extensions/pyRevitCore.extension/tools/About.pushbutton/bundle.yaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/credits.psd b/extensions/pyRevitCore.extension/tools/About.pushbutton/credits.psd similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/credits.psd rename to extensions/pyRevitCore.extension/tools/About.pushbutton/credits.psd diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/icon.png b/extensions/pyRevitCore.extension/tools/About.pushbutton/icon.png similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/About.pushbutton/icon.png rename to extensions/pyRevitCore.extension/tools/About.pushbutton/icon.png diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.chinese_s.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.chinese_s.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.chinese_s.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.chinese_s.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.de_de.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.de_de.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.de_de.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.de_de.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.en_us.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.en_us.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.en_us.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.en_us.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.es_es.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.es_es.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.es_es.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.es_es.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.fr_fr.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.fr_fr.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.fr_fr.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.fr_fr.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.pt_br.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.pt_br.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.pt_br.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.pt_br.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.ru.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.ru.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.ru.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.ResourceDictionary.ru.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.xaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.xaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/ExtensionsWindow.xaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/ExtensionsWindow.xaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/bundle.yaml b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/bundle.yaml similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/bundle.yaml rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/bundle.yaml diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/icon.dark.png b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/icon.dark.png similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/icon.dark.png rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/icon.dark.png diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/icon.png b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/icon.png similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/icon.png rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/icon.png diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py b/extensions/pyRevitCore.extension/tools/Extensions.smartbutton/script.py similarity index 100% rename from extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py rename to extensions/pyRevitCore.extension/tools/Extensions.smartbutton/script.py diff --git a/extensions/pyRevitCore.extension/tools/Layout Builder.pushbutton/LayoutBuilderWindow.xaml b/extensions/pyRevitCore.extension/tools/Layout Builder.pushbutton/LayoutBuilderWindow.xaml new file mode 100644 index 0000000000..8cf28364de --- /dev/null +++ b/extensions/pyRevitCore.extension/tools/Layout Builder.pushbutton/LayoutBuilderWindow.xaml @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +