Skip to content
5 changes: 5 additions & 0 deletions SolidDna/CADBooster.SolidDna/Errors/SolidDnaErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ public enum SolidDnaErrorCode
/// </summary>
SolidWorksCommandItemPositionError = 12010,

/// <summary>
/// There was an error while trying to activate a Context Menu Item that was already activated
/// </summary>
SolidWorksCommandContextMenuItemReActivateError = 12011,

#endregion

#region Export Data (13,000)
Expand Down
9 changes: 9 additions & 0 deletions SolidDna/CADBooster.SolidDna/Logging/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public static void AddFileLogger<TAddIn>(string filePath, FileLoggerConfiguratio
AddLogger<TAddIn>(new FileLogger("SolidDna", filePath, configuration));
}

/// <summary>
/// Add a message box logger for all SolidDna log messages.
/// Is cleaned up when your add-in unloads.
/// </summary>
/// <typeparam name="TAddIn"></typeparam>
/// <param name="logLevel"></param>
public static void AddMessageBoxLogger<TAddIn>(LogLevel logLevel = LogLevel.Critical) where TAddIn : SolidAddIn
=> AddLogger<TAddIn>(new MessageBoxLogger() { LogLevel = logLevel });

/// <summary>
/// Add a logger for all SolidDna log messages.
/// Is cleaned up when your add-in unloads.
Expand Down
167 changes: 167 additions & 0 deletions SolidDna/CADBooster.SolidDna/Logging/MessageBoxLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CADBooster.SolidDna
{
/// <summary>
/// A logger implementation that displays messages in SOLIDWORKS message boxes
/// </summary>
/// <remarks>
/// Messages are shown either immediately or grouped by scope when using <see cref="BeginScope{TState}"/>.
/// </remarks>
public class MessageBoxLogger : ILogger
{
/// <summary>
/// Represents a scope for grouping log messages
/// </summary>
/// <remarks>
/// Messages are buffered until the scope is disposed, then shown in a single message box
/// </remarks>
private class MessageLogScope : IDisposable
{
private readonly StringBuilder _string = new StringBuilder();
private readonly Stack<MessageLogScope> _scopes;
private readonly LogLevel _logLevel;
private readonly HashSet<LogLevel> _levels = new HashSet<LogLevel>();

/// <summary>
/// Creates a new log scope and pushes it onto the scope stack
/// </summary>
/// <param name="scopes">The parent scope stack</param>
/// <param name="logLevel">The minimum log level for this scope</param>
public MessageLogScope(Stack<MessageLogScope> scopes, LogLevel logLevel)
{
_scopes = scopes;
_logLevel = logLevel;
_scopes.Push(this);
}

/// <summary>
/// Logs a message to this scope
/// </summary>
/// <param name="logLevel">The severity level of the message</param>
/// <param name="message">The message text</param>
public void Log(LogLevel logLevel, string message)
{
if (logLevel < _logLevel)
return;

_ = _levels.Add(logLevel);
if (string.IsNullOrWhiteSpace(message) == false)
_ = _string.AppendLine(message);
}

/// <summary>
/// Disposes the scope and shows all buffered messages
/// </summary>
/// <exception cref="SolidDnaException">
/// Thrown if the dispose sequence is incorrect
/// </exception>
public void Dispose()
{
var scope = _scopes.Peek();
if (scope != this)
throw new SolidDnaException(new SolidDnaError(), new InvalidOperationException("Incorrect dispose sequence"));

_ = _scopes.Pop();

var message = scope._string.ToString();
var maxLevel = _levels.Count == 0 ? LogLevel.Trace : _levels.Max();

if (maxLevel < _logLevel)
return;

_ = SolidWorksEnvironment.Application.ShowMessageBox(
message,
ConvertLogLevelToIcon(maxLevel),
SolidWorksMessageBoxButtons.Ok
);
}
}

/// <summary>
/// Gets or sets the minimum log level that will be displayed
/// </summary>
/// <remarks>
/// Defaults to <see cref="LogLevel.Critical"/> to only show critical errors by default
/// </remarks>
public LogLevel LogLevel { get; set; } = LogLevel.Critical;

private readonly Stack<MessageLogScope> _scopes = new Stack<MessageLogScope>();

/// <summary>
/// Begins a logical operation scope
/// </summary>
/// <typeparam name="TState">Not used</typeparam>
/// <param name="state">The identifier for the scope</param>
/// <returns>An <see cref="IDisposable"/> that ends the scope when disposed</returns>
/// <remarks>
/// Messages logged within the scope will be grouped together and shown when the scope is disposed
/// </remarks>
public IDisposable BeginScope<TState>(TState state)
=> new MessageLogScope(_scopes, LogLevel);

/// <summary>
/// Checks if the given log level is enabled
/// </summary>
/// <param name="logLevel">Level to be checked</param>
/// <returns>True if enabled</returns>
public bool IsEnabled(LogLevel logLevel) => LogLevel <= logLevel;

/// <summary>
/// Logs a message to a SOLIDWORKS message box
/// </summary>
/// <typeparam name="TState">The type of the state object</typeparam>
/// <param name="logLevel">The severity level of the message</param>
/// <param name="eventId">The event ID</param>
/// <param name="state">The message state</param>
/// <param name="exception">The exception to log</param>
/// <param name="formatter">Function to format the message</param>
/// <remarks>
/// Shows messages immediately when not in a scope, or buffers them when in a scope
/// </remarks>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel < LogLevel)
return;

if (_scopes.Count == 0)
_ = SolidWorksEnvironment.Application.ShowMessageBox(
formatter.Invoke(state, exception),
ConvertLogLevelToIcon(logLevel),
SolidWorksMessageBoxButtons.Ok
);
else
_scopes.Peek().Log(logLevel, formatter.Invoke(state, exception));
}

/// <summary>
/// Converts a <see cref="LogLevel"/> to a SOLIDWORKS message box icon
/// </summary>
/// <param name="logLevel">The log level to convert</param>
/// <returns>The appropriate message box icon</returns>
/// <exception cref="ArgumentException">Thrown for unsupported log levels</exception>
private static SolidWorksMessageBoxIcon ConvertLogLevelToIcon(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace:
return SolidWorksMessageBoxIcon.Information;
case LogLevel.Debug:
return SolidWorksMessageBoxIcon.Information;
case LogLevel.Information:
return SolidWorksMessageBoxIcon.Information;
case LogLevel.Warning:
return SolidWorksMessageBoxIcon.Warning;
case LogLevel.Error:
return SolidWorksMessageBoxIcon.Stop;
case LogLevel.Critical:
return SolidWorksMessageBoxIcon.Stop;
default:
throw new ArgumentException("Unsupported enum value", nameof(logLevel));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class CommandManager : SolidDnaObject<ICommandManager>
/// </summary>
private readonly List<CommandManagerFlyout> mCommandFlyouts = new List<CommandManagerFlyout>();

/// <summary>
/// A list of all created command context menu items
/// </summary>
private readonly List<ICommandCreated> mCommandContextItems = new List<ICommandCreated>();

/// <summary>
/// Unique ID for flyouts (just increment every time we add one)
/// </summary>
Expand Down Expand Up @@ -76,6 +81,16 @@ public CommandManagerGroup CreateCommandTab(string title, int id, List<ICommandM
#pragma warning restore CS0618
}

/// <summary>
/// Creates context menu items from the provided collection of <see cref="ICommandCreatable"/> objects
/// </summary>
/// <param name="commandItems">The collection of command items to create</param>
public void CreateContextMenuItems(IEnumerable<ICommandCreatable> commandItems)
{
foreach (var item in commandItems)
mCommandContextItems.AddRange(item.Create());
}

/// <summary>
/// Create a command group from a list of <see cref="CommandManagerItem"/> items. Uses a single list of items, separators and flyouts.
/// NOTE: If you set <paramref name="ignorePreviousVersion"/> to false, you should pick a new ID every time you change your tab.
Expand Down Expand Up @@ -119,7 +134,7 @@ public CommandManagerGroup CreateCommandGroupAndTabs(string title, int id, List<

// Track all flyouts for all add-ins that use SolidDNA
mCommandFlyouts.AddRange(commandManagerItems.OfType<CommandManagerFlyout>());

// Create the group
group.Create(this, title);

Expand Down Expand Up @@ -251,7 +266,10 @@ private CommandManagerGroup CreateCommandGroup(string title, int id, List<IComma
private void RemoveCommandFlyout(CommandManagerFlyout flyout)
{
lock (mCommandFlyouts)
{
BaseObject.RemoveFlyoutGroup(flyout.UserId);
mCommandFlyouts.Remove(flyout);
}
}

/// <summary>
Expand All @@ -262,7 +280,10 @@ private void RemoveCommandFlyout(CommandManagerFlyout flyout)
private void RemoveCommandGroup(CommandManagerGroup group, bool runtimeOnly = false)
{
lock (mCommandGroups)
{
BaseObject.RemoveCommandGroup2(group.UserId, runtimeOnly);
mCommandGroups.Remove(group);
}
}

/// <summary>
Expand Down Expand Up @@ -318,6 +339,10 @@ public override void Dispose()
// Remove all command flyouts
mCommandFlyouts?.ForEach(RemoveCommandFlyout);

// Dispose all command context menu items
mCommandContextItems?.ForEach(x => x.Dispose());
mCommandContextItems.Clear();

base.Dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CADBooster.SolidDna
{
/// <summary>
/// Provides extension methods for converting <see cref="CommandManagerItem"/> objects to <see cref="ICommandCreatable"/>
/// </summary>
public static class CommandManagerItemExtensions
{
/// <summary>
/// Converts a collection of <see cref="CommandManagerItem"/> objects to <see cref="ICommandCreatable"/> objects
/// </summary>
/// <param name="items">The collection of <see cref="CommandManagerItem"/> objects to convert</param>
/// <param name="selectTypeSelector">An optional function to determine the selection type for each item</param>
/// <returns>A collection of <see cref="ICommandCreatable"/> objects</returns>
public static IEnumerable<ICommandCreatable> AsCommandCreatable(this IEnumerable<CommandManagerItem> items,
Func<CommandManagerItem, SelectionType> selectTypeSelector = null)
=> items.Select(x =>
x.AsCommandCreatable(
selectTypeSelector is null
? SelectionType.Everything
: selectTypeSelector.Invoke(x)));

/// <summary>
/// Converts a single <see cref="CommandManagerItem"/> object to an <see cref="ICommandCreatable"/> object
/// </summary>
/// <param name="item">The <see cref="CommandManagerItem"/> object to convert</param>
/// <returns>An <see cref="ICommandCreatable"/> object</returns>
public static ICommandCreatable AsCommandCreatable(this CommandManagerItem item)
=> item.AsCommandCreatable(SelectionType.Everything);

/// <summary>
/// Converts a single <see cref="CommandManagerItem"/> object to an <see cref="ICommandCreatable"/> object
/// </summary>
/// <param name="item">The <see cref="CommandManagerItem"/> object to convert</param>
/// <param name="selectType">The selection type for the item (defaults to <see cref="SelectionType.Everything"/>)</param>
/// <returns>An <see cref="ICommandCreatable"/> object</returns>
public static ICommandCreatable AsCommandCreatable(this CommandManagerItem item, SelectionType selectType)
=> new CommandContextItem()
{
Name = item.Name,
Hint = item.Hint,
OnClick = item.OnClick,
OnStateCheck = item.OnStateCheck,
SelectionType = selectType,
VisibleForAssemblies = item.VisibleForAssemblies,
VisibleForDrawings = item.VisibleForDrawings,
VisibleForParts = item.VisibleForParts
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CADBooster.SolidDna
{
/// <summary>
/// A command context menu item base class
/// </summary>
public abstract class CommandContextBase
{

private bool _isCreated;

#region Public Properties

/// <summary>
/// The help text for this item.
/// </summary>
public string Hint { get; set; }

/// <summary>
/// True to show this item in the context menu when an assembly is open.
/// </summary>
public bool VisibleForAssemblies { get; set; } = true;

/// <summary>
/// True to show this item in the context menu when a drawing is open.
/// </summary>
public bool VisibleForDrawings { get; set; } = true;

/// <summary>
/// True to show this item in the context menu when a part is open.
/// </summary>
public bool VisibleForParts { get; set; } = true;

/// <summary>
/// The action to call when the item is clicked
/// </summary>
public Action OnClick { get; set; }

/// <summary>
/// The action to call when the item state requested
/// </summary>
public Action<CommandManagerItemStateCheckArgs> OnStateCheck { get; set; }

/// <summary>
/// The selection type that determines where the context menu will be shown
/// </summary>
public SelectionType SelectionType { get; set; } = SelectionType.Everything;

#endregion

/// <summary>
/// Creates the command context item for the specified document types
/// </summary>
/// <param name="path">The path to use for hierarchical naming. If empty, the item's name is used</param>
/// <returns>A list of created command context items</returns>
/// <exception cref="SolidDnaException">Thrown if the item has already been created</exception>
public virtual IEnumerable<ICommandCreated> Create(string path = "")
{
if (_isCreated)
throw new SolidDnaException(
SolidDnaErrors.CreateError(SolidDnaErrorTypeCode.SolidWorksCommandManager,
SolidDnaErrorCode.SolidWorksCommandContextMenuItemReActivateError));

_isCreated = true;

return Enumerable.Empty<ICommandCreated>();
}
}
}
Loading