Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Excel_UI/Addin/AddIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand All @@ -41,6 +43,8 @@ public partial class AddIn : IExcelAddIn

public static Dictionary<string, CallerFormula> CallerShells { get; private set; } = new Dictionary<string, CallerFormula>();

public static Dictionary<string, CustomRibbonEntry> CustomEntryShells { get; private set; } = new Dictionary<string, CustomRibbonEntry>();

public static AddIn Instance { get; private set; } = null;


Expand All @@ -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();
}

/*******************************************/
Expand Down
2 changes: 0 additions & 2 deletions Excel_UI/Excel_UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<OutputPath>..\Build\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<FallbackCulture>en-US</FallbackCulture>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<Description>https://github.com/BHoM/Excel_UI</Description>
<Copyright>Copyright © https://github.com/BHoM</Copyright>
<AssemblyVersion>9.0.0.0</AssemblyVersion>
Expand Down Expand Up @@ -84,7 +83,6 @@
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<PackageReference Include="ExcelDna.AddIn" Version="1.9.0" />
<PackageReference Include="System.Resources.Extensions" Version="9.0.0" />
<PackageReference Include="ExcelDna.Integration" Version="1.9.0" />
<PackageReference Include="ExcelDna.IntelliSense" Version="1.1.0" />
<PackageReference Include="ExcelDna.Registration" Version="1.9.0" />
Expand Down
90 changes: 90 additions & 0 deletions Excel_UI/Global/CustomRibbon.cs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
*/

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}.");
}
}

/*******************************************/
}
}
136 changes: 136 additions & 0 deletions Excel_UI/Ribbon/Ribbon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
* along with this code. If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
*/

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;
Expand All @@ -40,6 +45,13 @@ public partial class Ribbon : ExcelRibbon

public override string GetCustomUI(string RibbonID)
{
IEnumerable<string> customTabNames = AddIn.CustomEntryShells.Values
.Select(e => e.TabName)
.Distinct();

string customTabsXml = string.Concat(customTabNames.Select(tabName =>
$"<tab id='custom_{Sanitise(tabName)}' label='{Escape(tabName)}'>{GetCustomRibbonXml(tabName)}</tab>"));

string ribbonxml = $@"
<customUI xmlns='http://schemas.microsoft.com/office/2006/01/customui' loadImage='LoadImage'>
<ribbon>
Expand All @@ -57,6 +69,7 @@ public override string GetCustomUI(string RibbonID)
<button id='bhomxyz' onAction='OpenLink' imageMso='GetExternalDataFromWeb' label='bhom.xyz' tag='{Engine.Base.Query.BHoMWebsiteURL()}' supertip='Visit the BHoM website.' />
</group>
</tab>
{customTabsXml}
</tabs>
</ribbon>
</customUI>";
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<string, XmlElement> groups = new Dictionary<string, XmlElement>();
Dictionary<string, Dictionary<int, XmlElement>> boxes = new Dictionary<string, Dictionary<int, XmlElement>>();
XmlDocument doc = new XmlDocument();
XmlElement root = doc.CreateElement("root");
doc.AppendChild(root);

foreach (KeyValuePair<string, CustomRibbonEntry> 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<int, XmlElement>();
}

// 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<string, Dictionary<int, XmlElement>> kvp in boxes)
{
List<int> 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);
}

/*******************************************/
}
}

Expand Down