diff --git a/ExcelUI_Engine/ExcelUI_Engine.csproj b/ExcelUI_Engine/ExcelUI_Engine.csproj
index ad709eeb..e89b6e40 100644
--- a/ExcelUI_Engine/ExcelUI_Engine.csproj
+++ b/ExcelUI_Engine/ExcelUI_Engine.csproj
@@ -10,7 +10,7 @@
BHoM
Copyright � https://github.com/BHoM
9.0.0.0
- 9.1.0.0
+ 9.2.0.0
diff --git a/Excel_UI/Addin/AddIn.cs b/Excel_UI/Addin/AddIn.cs
index b9ab4a65..2cd9df78 100644
--- a/Excel_UI/Addin/AddIn.cs
+++ b/Excel_UI/Addin/AddIn.cs
@@ -28,6 +28,8 @@
using System.Collections.Generic;
using System.Collections;
using System.Linq.Expressions;
+using BH.oM.UI;
+using BH.UI.Excel.Global;
using BH.UI.Excel.Templates;
@@ -41,6 +43,8 @@ public partial class AddIn : IExcelAddIn
public static Dictionary CallerShells { get; private set; } = new Dictionary();
+ public static Dictionary CustomEntryShells { get; private set; } = new Dictionary();
+
public static AddIn Instance { get; private set; } = null;
@@ -57,12 +61,18 @@ public AddIn()
static AddIn()
{
- // Collect the callers from assemblies
+ // Collect the callers from assemblies.
+ // Side effect: constructing each CallerFormula creates a Caller, which triggers
+ // static Caller() in BHoM_UI → Initialisation.Activate() → CustomRibbonEntries populated.
CallerShells = ExcelIntegration.GetExportedAssemblies()
.SelectMany(a => a.GetTypes())
.Where(t => t.Namespace == "BH.UI.Excel.Components" && typeof(CallerFormula).IsAssignableFrom(t))
.Select(t => InstantiateCaller(t))
.ToDictionary(o => o.Caller.GetType().Name);
+
+ // Subscribe to custom ribbon entries and replay those already loaded.
+ // Initialisation.CustomRibbonEntries is fully populated at this point.
+ CustomRibbon.Activate();
}
/*******************************************/
diff --git a/Excel_UI/Excel_UI.csproj b/Excel_UI/Excel_UI.csproj
index 6eacb90a..705444f5 100644
--- a/Excel_UI/Excel_UI.csproj
+++ b/Excel_UI/Excel_UI.csproj
@@ -9,11 +9,10 @@
..\Build\
false
en-US
- true
https://github.com/BHoM/Excel_UI
Copyright © https://github.com/BHoM
9.0.0.0
- 9.1.0.0
+ 9.2.0.0
@@ -84,12 +83,11 @@
False
-
-
+
diff --git a/Excel_UI/Global/CustomRibbon.cs b/Excel_UI/Global/CustomRibbon.cs
new file mode 100644
index 00000000..8ab36d80
--- /dev/null
+++ b/Excel_UI/Global/CustomRibbon.cs
@@ -0,0 +1,90 @@
+/*
+ * This file is part of the Buildings and Habitats object Model (BHoM)
+ * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved.
+ *
+ * Each contributor holds copyright over their respective contributions.
+ * The project versioning (Git) records all such contribution source information.
+ *
+ *
+ * The BHoM is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3.0 of the License, or
+ * (at your option) any later version.
+ *
+ * The BHoM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this code. If not, see .
+ */
+
+using BH.oM.UI;
+using BH.UI.Base;
+using BH.UI.Base.Global;
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace BH.UI.Excel.Global
+{
+ public static class CustomRibbon
+ {
+ /*******************************************/
+ /**** Public Methods ****/
+ /*******************************************/
+
+ public static void Activate()
+ {
+ Initialisation.CustomRibbonEntryLoaded += OnCustomRibbonEntryLoaded;
+
+ foreach (CustomRibbonEntry entry in Initialisation.CustomRibbonEntries)
+ OnCustomRibbonEntryLoaded(null, entry);
+ }
+
+ /*******************************************/
+
+ public static string DeriveId(CustomRibbonEntry entry)
+ {
+ string seed = $"{entry.TabName}|{entry.Category}|{entry.ItemJson}";
+ using (MD5 md5 = MD5.Create())
+ return "c" + BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(seed)))
+ .Replace("-", "").ToLowerInvariant();
+ }
+
+
+ /*******************************************/
+ /**** Private Methods ****/
+ /*******************************************/
+
+ private static void OnCustomRibbonEntryLoaded(object sender, CustomRibbonEntry entry)
+ {
+ try
+ {
+ string id = DeriveId(entry);
+ if (AddIn.CustomEntryShells.ContainsKey(id))
+ return;
+
+ // Validate: confirm CallerType can be instantiated and ItemJson deserialises correctly.
+ Caller temp = Activator.CreateInstance(entry.CallerType) as Caller;
+ if (temp == null)
+ {
+ BH.Engine.Base.Compute.RecordWarning($"Could not instantiate Caller for custom ribbon entry. Tab: {entry.TabName}, Category: {entry.Category}.");
+ return;
+ }
+
+ object item = BH.Engine.Serialiser.Convert.FromJson(entry.ItemJson);
+ temp.SetItem(item);
+
+ AddIn.CustomEntryShells[id] = entry;
+ }
+ catch (Exception e)
+ {
+ BH.Engine.Base.Compute.RecordWarning(e, $"Failed to register custom ribbon entry. Tab: {entry.TabName}, Category: {entry.Category}.");
+ }
+ }
+
+ /*******************************************/
+ }
+}
diff --git a/Excel_UI/Ribbon/Ribbon.cs b/Excel_UI/Ribbon/Ribbon.cs
index 30edd8f6..cc1b98da 100644
--- a/Excel_UI/Ribbon/Ribbon.cs
+++ b/Excel_UI/Ribbon/Ribbon.cs
@@ -20,7 +20,12 @@
* along with this code. If not, see .
*/
+using BH.Engine.Serialiser;
+using BH.oM.UI;
+using BH.UI.Base;
+using BH.UI.Excel.Global;
using BH.UI.Excel.Templates;
+using ExcelDna.Integration;
using ExcelDna.Integration.CustomUI;
using System;
using System.Collections.Generic;
@@ -40,6 +45,13 @@ public partial class Ribbon : ExcelRibbon
public override string GetCustomUI(string RibbonID)
{
+ IEnumerable customTabNames = AddIn.CustomEntryShells.Values
+ .Select(e => e.TabName)
+ .Distinct();
+
+ string customTabsXml = string.Concat(customTabNames.Select(tabName =>
+ $"{GetCustomRibbonXml(tabName)}"));
+
string ribbonxml = $@"
@@ -57,6 +69,7 @@ public override string GetCustomUI(string RibbonID)
+ {customTabsXml}
";
@@ -143,6 +156,10 @@ public Bitmap GetImage(IRibbonControl control)
Templates.CallerFormula caller = GetCaller(control.Id);
if (caller != null)
return caller.Caller.Icon_24x24;
+
+ if (AddIn.CustomEntryShells.TryGetValue(control.Id, out CustomRibbonEntry entry))
+ return entry.Icon;
+
return null;
}
@@ -179,12 +196,131 @@ public void FillFormula(IRibbonControl control)
/*******************************************/
+ public void FillCustomFormula(IRibbonControl control)
+ {
+ if (!AddIn.CustomEntryShells.TryGetValue(control.Tag, out CustomRibbonEntry entry))
+ return;
+
+ try
+ {
+ object item = BH.Engine.Serialiser.Convert.FromJson(entry.ItemJson);
+ CallerFormula formula = AddIn.InstantiateCaller(entry.CallerType.Name, item);
+ if (formula == null)
+ return;
+
+ ExcelAsyncUtil.QueueAsMacro(() => formula.FillFormula(AddIn.CurrentSelection()));
+ }
+ catch (Exception e)
+ {
+ BH.Engine.Base.Compute.RecordWarning(e, $"Failed to fill custom formula. Tab: {entry.TabName}, Category: {entry.Category}.");
+ }
+ }
+
+ /*******************************************/
+
public void OpenLink(IRibbonControl control)
{
System.Diagnostics.Process.Start(control.Tag);
}
/*******************************************/
+
+ public static string GetCustomRibbonXml(string tabName)
+ {
+ Dictionary groups = new Dictionary();
+ Dictionary> boxes = new Dictionary>();
+ XmlDocument doc = new XmlDocument();
+ XmlElement root = doc.CreateElement("root");
+ doc.AppendChild(root);
+
+ foreach (KeyValuePair kvp in AddIn.CustomEntryShells.Where(kvp => kvp.Value.TabName == tabName))
+ {
+ string id = kvp.Key;
+ CustomRibbonEntry entry = kvp.Value;
+
+ // Resolve display name and description via a temporary Caller instance.
+ string label = entry.Category;
+ string supertip = "";
+ try
+ {
+ Caller temp = Activator.CreateInstance(entry.CallerType) as Caller;
+ if (temp != null)
+ {
+ object item = BH.Engine.Serialiser.Convert.FromJson(entry.ItemJson);
+ temp.SetItem(item);
+ label = temp.Name;
+ supertip = temp.Description;
+ }
+ }
+ catch { }
+
+ // Get or create the group for this entry's category.
+ XmlElement group;
+ if (!groups.TryGetValue(entry.Category, out group))
+ {
+ group = (XmlElement)root.AppendChild(doc.CreateElement("group"));
+ group.SetAttribute("id", Sanitise(tabName) + "_" + Sanitise(entry.Category));
+ group.SetAttribute("label", entry.Category);
+ groups[entry.Category] = group;
+ boxes[entry.Category] = new Dictionary();
+ }
+
+ // Get or create the vertical box for this GroupIndex.
+ if (!boxes[entry.Category].ContainsKey(entry.GroupIndex))
+ boxes[entry.Category][entry.GroupIndex] = doc.CreateElement("box");
+
+ XmlElement box = boxes[entry.Category][entry.GroupIndex];
+ box.SetAttribute("id", Sanitise(tabName) + "_" + Sanitise(entry.Category) + "_group" + entry.GroupIndex);
+ box.SetAttribute("boxStyle", "vertical");
+
+ XmlElement btn = doc.CreateElement("button");
+ btn.SetAttribute("id", id);
+ btn.SetAttribute("tag", id);
+ btn.SetAttribute("onAction", "FillCustomFormula");
+ btn.SetAttribute("getImage", "GetImage");
+ btn.SetAttribute("label", label);
+ btn.SetAttribute("screentip", label);
+ if (!string.IsNullOrEmpty(supertip))
+ btn.SetAttribute("supertip", supertip);
+ box.AppendChild(btn);
+ }
+
+ // Append boxes to their groups in GroupIndex order, separated by separators.
+ foreach (KeyValuePair> kvp in boxes)
+ {
+ List ordered = kvp.Value.Keys.ToList();
+ ordered.Sort();
+ foreach (int i in ordered)
+ {
+ groups[kvp.Key].AppendChild(kvp.Value[i]);
+ XmlElement sep = doc.CreateElement("separator");
+ sep.SetAttribute("id", $"sep-custom-{Sanitise(tabName)}-{Sanitise(kvp.Key)}-{i}");
+ groups[kvp.Key].AppendChild(sep);
+ }
+ groups[kvp.Key].RemoveChild(groups[kvp.Key].LastChild);
+ }
+
+ return root.InnerXml;
+ }
+
+
+ /*******************************************/
+ /**** Private Methods ****/
+ /*******************************************/
+
+ private static string Sanitise(string s)
+ {
+ return new string(s.Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray());
+ }
+
+ /*******************************************/
+
+ private static string Escape(string s)
+ {
+ return System.Security.SecurityElement.Escape(s);
+ }
+
+ /*******************************************/
}
}