Short description
π§© LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.
ποΈ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.
π‘οΈ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.

Command:
Main window: LISPLOADEXTRA
Last session report: LISPLOADREPORT
Loader diagnostics: LISPLOADDIAG
Description:
π§© LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.
ποΈ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.
π‘οΈ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.
Helper function: (if any)β
Functionalities:
β
Multi format scanning – discovers LSP VLX FAS and DLL files in the structured source tree.
β
Modeless loader window – stays usable while AutoCAD remains active.
β
Category and name filters – narrows the displayed tool collection.
β
Text search – filters visible loader rows by search text.
β
Checkbox selection – supports selective loading and Select All or None actions.
β
Load and reload actions – process checked files or force reload the chosen set.
β
Load Selected – loads highlighted grid rows and is also available through F3.
β
Persistent checked state – stores checked file paths in ss checked.json between sessions.
β
Automatic disk rescan – uses FileSystemWatcher with debounce when supported files change.
β
Right click tools – provides load reload open folder open editor copy path and checkbox actions.
β
Editor integration – opens source files in Notepad++ with a Notepad fallback.
β
Live log panel – shows real time color coded loading messages with Store Log Clear and Auto Scroll.
β
Status change flash – briefly highlights rows whose load status changed.
β
Diagnostics command – searches loaded AutoCAD assemblies for LISP related invocation methods.
Result:
π― LispLoaderExtra v1.5.000 provides the described AutoCAD or Civil 3D workflow.
π The generated SEO description is based on the supplied source code or project files.
Images, animations etc.

XAML code:
ο»Ώ<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Autodesk.Windows;assembly=AdWindows">
<src:RibbonToolTip x:Key="LispLoaderExtra">
<src:RibbonToolTip.ExpandedContent>
<StackPanel>
<TextBlock Background="AntiqueWhite" TextAlignment="Left">
<Bold>Function Syntax: LISPLOADEXTRA / LISPLOADREPORT / LISPLOADDIAG</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 25.06.2026 8:46:25AM</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 11.08.2026 3:38:21PM</Bold>
<LineBreak/>
<Hyperlink>N/A</Hyperlink>
<Bold><Run Foreground="DodgerBlue">Description</Run></Bold><LineBreak/>
<Run Foreground="White">π§© LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.</Run><LineBreak/>
<Run Foreground="White">ποΈ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.</Run><LineBreak/>
<Run Foreground="White">π‘οΈ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.</Run><LineBreak/>
<Bold><Run Foreground="DodgerBlue">Function Syntax</Run></Bold><LineBreak/>
<Run Foreground="White">Main window: </Run><Bold><Run Foreground="Orange">LISPLOADEXTRA</Run></Bold><LineBreak/>
<Run Foreground="White">Last session report: </Run><Bold><Run Foreground="Orange">LISPLOADREPORT</Run></Bold><LineBreak/>
<Run Foreground="White">Loader diagnostics: </Run><Bold><Run Foreground="Orange">LISPLOADDIAG</Run></Bold><LineBreak/>
<Bold><Run Foreground="LimeGreen">Primary Workflow</Run></Bold><LineBreak/>
<Run Foreground="White">1. NETLOAD the compiled LispLoaderExtra DLL into AutoCAD.</Run><LineBreak/>
<Run Foreground="White">2. Run LISPLOADEXTRA to open the modeless loader window.</Run><LineBreak/>
<Run Foreground="White">3. The application scans the configured Help_File tree for supported program files.</Run><LineBreak/>
<Run Foreground="White">4. Filter by AutoCAD or Civil 3D category name or free text and check the files that should be processed.</Run><LineBreak/>
<Run Foreground="White">5. Use Load Reload or Load Selected to process the required files.</Run><LineBreak/>
<Run Foreground="White">6. Review per row status version information and the live color coded log.</Run><LineBreak/>
<Run Foreground="White">7. Use LISPLOADREPORT to review last session results or LISPLOADDIAG when loader API diagnostics are required.</Run><LineBreak/>
<Bold><Run Foreground="LimeGreen">Functionalities</Run></Bold><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Multi format scanning</Run></Bold><Run Foreground="White"> - discovers LSP VLX FAS and DLL files in the structured source tree.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Modeless loader window</Run></Bold><Run Foreground="White"> - stays usable while AutoCAD remains active.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Category and name filters</Run></Bold><Run Foreground="White"> - narrows the displayed tool collection.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Text search</Run></Bold><Run Foreground="White"> - filters visible loader rows by search text.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Checkbox selection</Run></Bold><Run Foreground="White"> - supports selective loading and Select All or None actions.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Load and reload actions</Run></Bold><Run Foreground="White"> - process checked files or force reload the chosen set.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Load Selected</Run></Bold><Run Foreground="White"> - loads highlighted grid rows and is also available through F3.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Persistent checked state</Run></Bold><Run Foreground="White"> - stores checked file paths in ss checked.json between sessions.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Automatic disk rescan</Run></Bold><Run Foreground="White"> - uses FileSystemWatcher with debounce when supported files change.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Right click tools</Run></Bold><Run Foreground="White"> - provides load reload open folder open editor copy path and checkbox actions.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Editor integration</Run></Bold><Run Foreground="White"> - opens source files in Notepad++ with a Notepad fallback.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Live log panel</Run></Bold><Run Foreground="White"> - shows real time color coded loading messages with Store Log Clear and Auto Scroll.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Status change flash</Run></Bold><Run Foreground="White"> - briefly highlights rows whose load status changed.</Run><LineBreak/>
<Run Foreground="White">β
</Run><Bold><Run Foreground="LimeGreen">Diagnostics command</Run></Bold><Run Foreground="White"> - searches loaded AutoCAD assemblies for LISP related invocation methods.</Run><LineBreak/>
<Bold><Run Foreground="DarkRed">Important Notes</Run></Bold><LineBreak/>
<Run Foreground="White">β οΈ </Run><Bold><Run Foreground="DarkRed">Project version is 1.5.000.</Run></Bold><LineBreak/>
<Run Foreground="White">β οΈ </Run><Bold><Run Foreground="DarkRed">LISP VLX and FAS loading uses SendStringToExecute first then acedInvoke and reflection fallback strategies.</Run></Bold><LineBreak/>
<Run Foreground="White">β οΈ </Run><Bold><Run Foreground="DarkRed">A 30 second per file timeout is used by the current FileLoader implementation.</Run></Bold><LineBreak/>
<Run Foreground="White">β οΈ </Run><Bold><Run Foreground="DarkRed">DLL loading uses Assembly.LoadFrom and AutoCAD ExtensionLoader.</Run></Bold><LineBreak/>
<Run Foreground="White">β οΈ </Run><Bold><Run Foreground="DarkRed">Escape is swallowed by the modeless window to avoid passing the key into the AutoCAD viewport.</Run></Bold><LineBreak/>
<Bold><Run Foreground="Yellow">SEO Keywords</Run></Bold><LineBreak/>
<Run Foreground="Yellow">π·οΈ AutoCAD π·οΈ Civil 3D π·οΈ LispLoaderExtra π·οΈ AutoLISP loader π·οΈ VLX loader π·οΈ FAS loader π·οΈ DLL loader π·οΈ NETLOAD π·οΈ WinForms π·οΈ FileSystemWatcher π·οΈ LISPLOADREPORT π·οΈ LISPLOADDIAG π·οΈ CAD automation π·οΈ Plugin manager</Run><LineBreak/>
</TextBlock>
<Grid>
<Image Source="Image_000.png" Stretch="Uniform"/>
</Grid>
</StackPanel>
</src:RibbonToolTip.ExpandedContent>
</src:RibbonToolTip>
</ResourceDictionary>
C# code:
// ===== FILE: DiskScanner.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace LispLoaderExtra
{
/// <summary>
/// Scans the Help_File folder tree and returns ScannedFile entries.
///
/// Structure:
/// AutoCAD: Help_File\{MenuFolder}\{SubFolder}\*.{lsp,vlx,fas,dll}
/// MenuFolder must contain "_menu" in its name (case-insensitive)
/// SubFolder is one level deep inside the _menu folder
/// Files directly inside SubFolder are collected (no deeper recursion)
///
/// Civil 3D: Help_File\C3D\{C3D_*_menu}\{SubFolder}\*.{lsp,vlx,fas,dll}
/// Display name strips the "C3D_" prefix: C3D_Alignment_menu β Alignment_menu
///
/// Exclusions (applied to every path segment below the base):
/// - Exact names (case-insensitive): ss, vlx_dll, vlx_fas, lispsloader
/// - Names starting with those tokens + "_" or "-" (e.g. ss_01, vlx_fas_2)
/// - Names starting with "@"
///
/// Improvements over previous version:
/// - All string comparisons use net48-compatible overloads (no StringComparison on Contains)
/// - Split uses char[] instead of two-arg char overload (net48 compat)
/// - Results sorted: AutoCAD first, then Civil 3D; alphabetically within each group
/// - SafeGetDirectories / SafeGetFiles log errors instead of swallowing silently
/// </summary>
internal static class DiskScanner
{
internal static readonly string BaseFolder =
@"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File";
private static readonly string C3DFolder =
Path.Combine(BaseFolder, "C3D");
private static readonly HashSet<string> ExcludedNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ "ss", "vlx_dll", "vlx_fas", "lispsloader", "lispploaderextra" };
private static readonly HashSet<string> SupportedExtensions =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ ".lsp", ".vlx", ".fas", ".dll" };
private static readonly char[] PathSeps = new char[] { '\\', '/' };
// ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal static List<ScannedFile> Scan()
{
var results = new List<ScannedFile>();
if (!Directory.Exists(BaseFolder))
{
LogManager.Warn("DiskScanner", $"Base folder not found: {BaseFolder}");
return results;
}
ScanAutoCAD(results);
ScanC3D(results);
// Sort: category first, then menu name, then file name
results.Sort((a, b) =>
{
int c = string.Compare(a.Category, b.Category, StringComparison.OrdinalIgnoreCase);
if (c != 0) return c;
c = string.Compare(a.MenuName, b.MenuName, StringComparison.OrdinalIgnoreCase);
if (c != 0) return c;
return string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase);
});
// Assign sequential row numbers after sort
for (int i = 0; i < results.Count; i++)
results[i].RowIndex = i + 1;
LogManager.Info("DiskScanner",
$"Scan complete: {results.Count} files " +
$"(AutoCAD: {results.Count(f => f.Category == "AutoCAD")}, " +
$"Civil 3D: {results.Count(f => f.Category == "Civil 3D")})");
return results;
}
/// <summary>Returns distinct sorted menu names for the given category.</summary>
internal static List<string> GetMenuNames(string category, List<ScannedFile> files)
{
return files
.Where(f => category == "All" ||
string.Equals(f.Category, category, StringComparison.OrdinalIgnoreCase))
.Select(f => f.MenuName)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
}
// ββ AutoCAD scan ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Pattern: Help_File \ {*_menu folder} \ {sub-folder} \ {files}
private static void ScanAutoCAD(List<ScannedFile> results)
{
foreach (string menuDir in SafeGetDirectories(BaseFolder))
{
string menuName = Path.GetFileName(menuDir);
// Must contain "_menu" β net48: use IndexOf, not Contains(str, comparison)
if (menuName.IndexOf("_menu", StringComparison.OrdinalIgnoreCase) < 0) continue;
if (string.Equals(menuName, "C3D", StringComparison.OrdinalIgnoreCase)) continue;
if (IsExcluded(menuName)) continue;
foreach (string subDir in SafeGetDirectories(menuDir))
{
if (IsExcluded(Path.GetFileName(subDir))) continue;
foreach (string file in SafeGetFiles(subDir))
{
string ext = Path.GetExtension(file);
if (!SupportedExtensions.Contains(ext)) continue;
if (IsExcludedPath(file)) continue;
results.Add(new ScannedFile(
category: "AutoCAD",
menuName: menuName,
fileName: Path.GetFileName(file),
extension: ext,
fullPath: file));
}
}
}
}
// ββ Civil 3D scan βββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Pattern: Help_File \ C3D \ {C3D_*_menu folder} \ {sub-folder} \ {files}
private static void ScanC3D(List<ScannedFile> results)
{
if (!Directory.Exists(C3DFolder)) return;
foreach (string menuDir in SafeGetDirectories(C3DFolder))
{
string menuName = Path.GetFileName(menuDir);
if (menuName.IndexOf("_menu", StringComparison.OrdinalIgnoreCase) < 0) continue;
if (IsExcluded(menuName)) continue;
// Strip "C3D_" prefix for display: C3D_Alignment_menu β Alignment_menu
string displayName = menuName.StartsWith("C3D_", StringComparison.OrdinalIgnoreCase)
? menuName.Substring(4)
: menuName;
foreach (string subDir in SafeGetDirectories(menuDir))
{
if (IsExcluded(Path.GetFileName(subDir))) continue;
foreach (string file in SafeGetFiles(subDir))
{
string ext = Path.GetExtension(file);
if (!SupportedExtensions.Contains(ext)) continue;
if (IsExcludedPath(file)) continue;
results.Add(new ScannedFile(
category: "Civil 3D",
menuName: displayName,
fileName: Path.GetFileName(file),
extension: ext,
fullPath: file));
}
}
}
}
// ββ Exclusion helpers βββββββββββββββββββββββββββββββββββββββββββββββββ
private static bool IsExcluded(string folderName)
{
if (string.IsNullOrEmpty(folderName)) return false;
if (folderName.StartsWith("@")) return true;
string low = folderName.ToLowerInvariant();
foreach (string ex in ExcludedNames)
{
string exl = ex.ToLowerInvariant();
if (low == exl ||
low.StartsWith(exl + "_", StringComparison.Ordinal) ||
low.StartsWith(exl + "-", StringComparison.Ordinal))
return true;
}
return false;
}
private static bool IsExcludedPath(string fullPath)
{
if (fullPath.Length <= BaseFolder.Length) return false;
string rel = fullPath.Substring(BaseFolder.Length).TrimStart(PathSeps);
// net48: Split(char[]) β not Split(char, char)
foreach (string part in rel.Split(PathSeps))
{
if (IsExcluded(part)) return true;
}
return false;
}
// ββ Safe I/O βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static string[] SafeGetDirectories(string path)
{
try { return Directory.GetDirectories(path); }
catch (System.Exception ex)
{
LogManager.Warn("DiskScanner", $"Cannot read directories: {path} β {ex.Message}");
return new string[0];
}
}
private static string[] SafeGetFiles(string path)
{
try { return Directory.GetFiles(path); }
catch (System.Exception ex)
{
LogManager.Warn("DiskScanner", $"Cannot read files: {path} β {ex.Message}");
return new string[0];
}
}
}
}
// ===== FILE: LoadSession.cs =====
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LispLoaderExtra
{
/// <summary>
/// Collects all LoadResult entries for one plugin-load session.
/// Provides summary statistics and formatted report text.
/// Singleton β one session per AutoCAD load.
/// </summary>
internal static class LoadSession
{
private static readonly List<LoadResult> _results = new List<LoadResult>();
private static DateTime _startTime;
internal static IReadOnlyList<LoadResult> Results => _results.AsReadOnly();
internal static void Begin()
{
_results.Clear();
_startTime = DateTime.Now;
}
internal static void Add(LoadResult r)
{
_results.Add(r);
// Mirror to log
switch (r.Status)
{
case LoadStatus.OK:
if (r.Extension == ".dll" && !string.IsNullOrEmpty(r.ErrorMessage))
{
// DLL OK β ErrorMessage carries the version stamp
LogManager.Info("FileLoader",
$"[OK-DLL] {r.FileName} v{r.ErrorMessage} ({r.Duration.TotalMilliseconds:0}ms)");
LogManager.Info("FileLoader", $" {r.Path}");
}
else
{
LogManager.Info("FileLoader",
$"[OK-{r.TypeLabel,-3}] {r.FileName} ({r.Duration.TotalMilliseconds:0}ms)");
LogManager.Info("FileLoader", $" {r.Path}");
}
break;
case LoadStatus.Failed:
LogManager.Error("FileLoader", $"[FAIL-{r.TypeLabel,-3}] {r.FileName}");
LogManager.Error("FileLoader", $" {r.ErrorMessage}");
LogManager.Error("FileLoader", $" {r.Path}");
break;
case LoadStatus.NotFound:
LogManager.Warn("FileLoader", $"[MISSING] {r.Path}");
break;
case LoadStatus.Skipped:
LogManager.Warn("FileLoader", $"[SKIP] {r.Path} (unsupported type)");
break;
}
}
// ββ Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal static int TotalCount => _results.Count;
internal static int OkCount => _results.Count(r => r.Status == LoadStatus.OK);
internal static int FailCount => _results.Count(r => r.Status == LoadStatus.Failed);
internal static int NotFoundCount => _results.Count(r => r.Status == LoadStatus.NotFound);
internal static int SkipCount => _results.Count(r => r.Status == LoadStatus.Skipped);
internal static int CountByType(string ext) =>
_results.Count(r => r.Extension == ext && r.Status == LoadStatus.OK);
internal static TimeSpan TotalDuration => DateTime.Now - _startTime;
// ββ Summary line (for command line) βββββββββββββββββββββββββββββββββββ
internal static string SummaryLine =>
$"Load complete β " +
$"OK: {OkCount} " +
$"FAILED: {FailCount} " +
$"NOT FOUND: {NotFoundCount} " +
$"SKIPPED: {SkipCount} " +
$"| {TotalDuration.TotalSeconds:0.0}s total";
// ββ Full text report (for clipboard / log) ββββββββββββββββββββββββββββ
internal static string BuildTextReport()
{
var sb = new StringBuilder();
string line = new string('β', 72);
sb.AppendLine(line);
sb.AppendLine($" LispLoaderExtra β Load Report");
sb.AppendLine($" {DateTime.Now:dd.MM.yyyy HH:mm:ss}");
sb.AppendLine(line);
sb.AppendLine($" Total files: {TotalCount}");
sb.AppendLine($" OK: {OkCount} " +
$"(LSP:{CountByType(".lsp")} " +
$"VLX:{CountByType(".vlx")} " +
$"FAS:{CountByType(".fas")} " +
$"DLL:{CountByType(".dll")})");
sb.AppendLine($" Failed: {FailCount}");
sb.AppendLine($" Not found: {NotFoundCount}");
sb.AppendLine($" Skipped: {SkipCount}");
sb.AppendLine($" Total time: {TotalDuration.TotalSeconds:0.00}s");
sb.AppendLine(line);
// Errors first
var errors = _results.Where(r => r.Status == LoadStatus.Failed).ToList();
if (errors.Count > 0)
{
sb.AppendLine(" ERRORS:");
foreach (var r in errors)
{
sb.AppendLine($" [{r.TypeLabel}] {r.FileName}");
sb.AppendLine($" {r.Path}");
sb.AppendLine($" {r.ErrorMessage}");
}
sb.AppendLine(line);
}
var missing = _results.Where(r => r.Status == LoadStatus.NotFound).ToList();
if (missing.Count > 0)
{
sb.AppendLine(" NOT FOUND:");
foreach (var r in missing)
sb.AppendLine($" {r.Path}");
sb.AppendLine(line);
}
sb.AppendLine(" ALL FILES:");
foreach (var r in _results)
{
string status = r.Status switch {
LoadStatus.OK => "OK ",
LoadStatus.Failed => "FAIL ",
LoadStatus.NotFound => "MISSING",
LoadStatus.Skipped => "SKIP ",
_ => "???????",
};
string extra = (r.Extension == ".dll" && r.Status == LoadStatus.OK && !string.IsNullOrEmpty(r.ErrorMessage))
? $" v{r.ErrorMessage}"
: "";
sb.AppendLine($" {status} [{r.TypeLabel,-3}] {r.FileName} ({r.Duration.TotalMilliseconds:0}ms){extra}");
}
sb.AppendLine(line);
return sb.ToString();
}
}
}
// ===== FILE: ConfigReader.cs =====
using System;
using System.Collections.Generic;
using System.IO;
namespace LispLoaderExtra
{
internal static class ConfigReader
{
private const string Source = "ConfigReader";
// Base folder for resolving relative paths listed in the config
internal static readonly string BaseFolder =
@"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File";
// Config file lives next to the log in the ss\ folder
internal static readonly string ConfigFile =
Path.Combine(LogManager.LogFolder, "LoaderConfig.txt");
internal static IReadOnlyList<string> ReadPaths()
{
LogManager.Info(Source, $"Looking for config: {ConfigFile}");
if (!File.Exists(ConfigFile))
{
LogManager.Error(Source, $"Config not found: {ConfigFile}");
return Array.Empty<string>();
}
var paths = new List<string>();
try
{
foreach (string raw in File.ReadAllLines(ConfigFile))
{
string line = raw.Trim();
if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("#")) continue;
if (line.StartsWith(";")) continue;
string resolved = Path.IsPathRooted(line)
? line
: Path.Combine(BaseFolder, line);
paths.Add(resolved);
}
LogManager.Info(Source, $"Config loaded β {paths.Count} entries");
}
catch (System.Exception ex)
{
LogManager.Error(Source, $"Failed to read config: {ex.Message}");
}
return paths;
}
}
}
// ===== FILE: LoadResult.cs =====
using System;
namespace LispLoaderExtra
{
/// <summary>
/// The outcome of attempting to load a single file.
/// </summary>
internal enum LoadStatus
{
OK, // loaded successfully
Failed, // file exists but load threw an error
NotFound, // file path does not exist on disk
Skipped, // unsupported extension
}
/// <summary>
/// Immutable record of one file-load attempt.
/// </summary>
internal sealed class LoadResult
{
internal string Path { get; }
internal string Extension { get; } // .lsp / .vlx / .fas / .dll
internal LoadStatus Status { get; }
internal string ErrorMessage{ get; } // empty string when OK
internal TimeSpan Duration { get; }
internal DateTime Timestamp { get; }
internal LoadResult(
string path,
string extension,
LoadStatus status,
string errorMessage,
TimeSpan duration)
{
Path = path;
Extension = extension.ToLowerInvariant();
Status = status;
ErrorMessage = errorMessage ?? string.Empty;
Duration = duration;
Timestamp = DateTime.Now;
}
// Convenience helpers
internal string FileName => System.IO.Path.GetFileName(Path);
internal string TypeLabel => Extension.TrimStart('.').ToUpperInvariant(); // LSP / VLX / FAS / DLL
internal bool IsOk => Status == LoadStatus.OK;
internal bool HasError => Status == LoadStatus.Failed;
}
}
// ===== FILE: FileLoader.cs =====
// Alias System.Exception explicitly to avoid ambiguity with
// Autodesk.AutoCAD.Runtime.Exception which lives in the same namespace.
using SysEx = System.Exception;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
namespace LispLoaderExtra
{
/// <summary>
/// Loads .lsp / .vlx / .fas / .dll files safely from ANY thread.
///
/// THREAD-SAFETY β ROOT CAUSE OF v1.3 ACCESS VIOLATION
/// βββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// acedInvoke (P/Invoke into accore.dll) and ExtensionLoader.Load
/// MUST run on AutoCAD's document-context thread. In v1.3 the form is
/// modeless (Show, not ShowDialog), so Load() was called from the WinForms
/// UI thread β Access Violation at 96AFEB62h.
///
/// FIX β ExecuteInApplicationContext
/// ββββββββββββββββββββββββββββββββββ
/// Every load is posted to AutoCAD's own thread via:
/// Application.DocumentManager.ExecuteInApplicationContext(cb, null)
/// A ManualResetEventSlim lets the UI thread block until the result is
/// ready (30 s timeout) while the form remains repaint-responsive.
///
/// LISP LOAD STRATEGIES (tried in order on the ACAD thread):
/// A. Document.SendStringToExecute("(load \"/path\")\n", false, false, false)
/// Safest: injects LISP into AutoCAD's own command evaluator.
/// Signature: (string commandString, bool activate, bool wrapUpInactiveDoc,
/// bool echoCommand) β no 'wrap' named param.
/// B. acedInvoke P/Invoke from accore.dll
/// C. Reflection scan for acedInvoke in acmgd / accoremgd
///
/// DLL LOAD:
/// Assembly.LoadFrom + ExtensionLoader.Load on ACAD thread.
/// Version stamp: "major.minor.build_[dd.mm.yyyy_hh.mm.ss]" (file creation time).
/// </summary>
internal static class FileLoader
{
// ββ resbuf type codes βββββββββββββββββββββββββββββββββββββββββββββββββ
private const short RTSTR = 5005;
private const short RTLB = 5016;
private const short RTLE = 5017;
// ββ P/Invoke β only safe on ACAD application-context thread ββββββββββ
[DllImport("accore.dll",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "acedInvoke")]
private static extern int AcedInvoke(IntPtr args, out IntPtr result);
private static bool _pinvokeAvailable = true;
// ββ Reflection cache ββββββββββββββββββββββββββββββββββββββββββββββββββ
private static MethodInfo? _reflectMethod;
private static bool _reflectSearched;
private static readonly object _reflectLock = new object();
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// PUBLIC ENTRY β safe to call from any thread (UI, background, etc.)
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal static LoadResult Load(string path)
{
if (string.IsNullOrWhiteSpace(path))
return Make(path ?? "", "", LoadStatus.Skipped, "Empty path");
path = path.Trim();
string ext = Path.GetExtension(path).ToLowerInvariant();
if (!File.Exists(path))
return Make(path, ext, LoadStatus.NotFound, "File not found: " + path);
// Post work to AutoCAD's application-context thread and block
// until done. ManualResetEventSlim avoids spinning.
LoadResult? result = null;
using var done = new ManualResetEventSlim(false);
Application.DocumentManager.ExecuteInApplicationContext(
delegate
{
try
{
switch (ext)
{
case ".lsp":
case ".vlx":
case ".fas":
result = LoadLisp(path, ext);
break;
case ".dll":
result = LoadDll(path);
break;
default:
result = Make(path, ext, LoadStatus.Skipped,
"Unsupported extension: " + ext);
break;
}
}
catch (SysEx ex)
{
result = Make(path, ext, LoadStatus.Failed,
"Unhandled error in application context: " + ex.Message);
}
finally
{
done.Set();
}
},
null);
// 30 s timeout β graceful degradation if ACAD is waiting for modal input
if (!done.Wait(TimeSpan.FromSeconds(30)))
{
LogManager.Warn("FileLoader",
$"Load timed out (30 s): {Path.GetFileName(path)}");
return Make(path, ext, LoadStatus.Failed,
"Load timed out β AutoCAD may be waiting for user input.");
}
return result ?? Make(path, ext, LoadStatus.Failed, "No result returned.");
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// LISP / VLX / FAS β runs on ACAD application-context thread
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static LoadResult LoadLisp(string path, string ext)
{
var sw = Stopwatch.StartNew();
try
{
Document? doc = Application.DocumentManager?.MdiActiveDocument;
if (doc == null)
return Make(path, ext, LoadStatus.Failed,
"No active document β open a drawing first.", sw.Elapsed);
// ββ Strategy A: SendStringToExecute βββββββββββββββββββββββββββ
// Signature (AutoCAD 2010+):
// void SendStringToExecute(string commandString,
// bool activate,
// bool wrapUpInactiveDoc,
// bool echoCommand)
// NOTE: named param is 'wrapUpInactiveDoc', NOT 'wrap'.
// Use positional args to stay compatible across ACAD versions.
try
{
// LISP requires forward slashes in paths
string lispPath = path.Replace('\\', '/');
string lispExpr = "(load \"" + lispPath + "\")\n";
using (doc.LockDocument())
{
// positional: (commandString, activate, wrapUpInactiveDoc, echoCommand)
doc.SendStringToExecute(lispExpr, false, false, false);
}
sw.Stop();
LogManager.Info("FileLoader",
"[SendString] Loaded: " + Path.GetFileName(path));
return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
}
catch (SysEx ssEx)
{
LogManager.Warn("FileLoader",
"SendStringToExecute failed for " + Path.GetFileName(path) +
": " + ssEx.Message + " β trying acedInvoke.");
}
// ββ Strategy B: P/Invoke acedInvoke ββββββββββββββββββββββββββ
if (_pinvokeAvailable)
{
try
{
using (doc.LockDocument())
using (var rb = new ResultBuffer(
new TypedValue(RTLB),
new TypedValue(RTSTR, "load"),
new TypedValue(RTSTR, path),
new TypedValue(RTLE)))
{
AcedInvoke(rb.UnmanagedObject, out IntPtr _);
}
sw.Stop();
return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
}
catch (EntryPointNotFoundException epnf)
{
_pinvokeAvailable = false;
LogManager.Warn("FileLoader",
"acedInvoke entry not found: " + epnf.Message);
}
catch (DllNotFoundException dnf)
{
_pinvokeAvailable = false;
LogManager.Warn("FileLoader",
"accore.dll not found: " + dnf.Message);
}
catch (SysEx ex)
{
LogManager.Warn("FileLoader",
"acedInvoke threw: " + ex.Message + " β trying reflection.");
}
}
// ββ Strategy C: Reflection ββββββββββββββββββββββββββββββββββββ
MethodInfo? method = GetReflectMethod();
if (method != null)
{
try
{
using (doc.LockDocument())
using (var rb = new ResultBuffer(
new TypedValue(RTLB),
new TypedValue(RTSTR, "load"),
new TypedValue(RTSTR, path),
new TypedValue(RTLE)))
{
method.Invoke(null, new object?[] { rb, null });
}
sw.Stop();
return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
}
catch (SysEx ex)
{
sw.Stop();
return Make(path, ext, LoadStatus.Failed,
ex.InnerException?.Message ?? ex.Message, sw.Elapsed);
}
}
sw.Stop();
return Make(path, ext, LoadStatus.Failed,
"All load strategies failed β run LISPLOADDIAG for details.", sw.Elapsed);
}
catch (SysEx ex)
{
sw.Stop();
return Make(path, ext, LoadStatus.Failed, ex.Message, sw.Elapsed);
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// DLL β runs on ACAD application-context thread
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static LoadResult LoadDll(string path)
{
var sw = Stopwatch.StartNew();
try
{
System.Reflection.Assembly asm =
System.Reflection.Assembly.LoadFrom(path);
ExtensionLoader.Load(path);
sw.Stop();
Version? verObj = asm.GetName().Version;
string verNum = verObj != null
? string.Format("{0}.{1}.{2:D3}", verObj.Major, verObj.Minor, verObj.Build)
: "?.?.???";
string creationStamp;
try { creationStamp = File.GetCreationTime(path).ToString("dd.MM.yyyy_HH.mm.ss"); }
catch { creationStamp = "??.??.????_??.??.??"; }
// version stamp stored in ErrorMessage (re-used as info field for OK results)
return Make(path, ".dll", LoadStatus.OK,
verNum + "_[" + creationStamp + "]", sw.Elapsed);
}
catch (SysEx ex)
{
sw.Stop();
return Make(path, ".dll", LoadStatus.Failed, ex.Message, sw.Elapsed);
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// REFLECTION SCAN β cached after first successful search
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static MethodInfo? GetReflectMethod()
{
if (_reflectSearched) return _reflectMethod;
lock (_reflectLock)
{
if (_reflectSearched) return _reflectMethod;
_reflectSearched = true;
string[] targets = { "acmgd", "accoremgd", "acdbmgd", "acmgdui", "acapp" };
foreach (System.Reflection.Assembly asm in
AppDomain.CurrentDomain.GetAssemblies())
{
string asmName = asm.GetName().Name ?? "";
bool isTarget = false;
foreach (string t in targets)
if (asmName.Equals(t, StringComparison.OrdinalIgnoreCase))
{ isTarget = true; break; }
if (!isTarget) continue;
Type[] types;
try { types = asm.GetTypes(); } catch { continue; }
foreach (Type type in types)
{
MethodInfo[] methods;
try
{
methods = type.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static);
}
catch { continue; }
foreach (MethodInfo m in methods)
{
if (!m.Name.Equals("acedInvoke",
StringComparison.OrdinalIgnoreCase)) continue;
ParameterInfo[] parms = m.GetParameters();
if (parms.Length < 1) continue;
if (parms[0].ParameterType != typeof(ResultBuffer) &&
parms[0].ParameterType != typeof(IntPtr)) continue;
LogManager.Info("FileLoader",
"acedInvoke found via reflection: " +
type.FullName + "." + m.Name);
_reflectMethod = m;
return m;
}
}
}
LogManager.Warn("FileLoader",
"acedInvoke not found via reflection β run LISPLOADDIAG.");
return null;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// FACTORY
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static LoadResult Make(
string path, string ext, LoadStatus status,
string error, TimeSpan duration = default) =>
new LoadResult(
path,
string.IsNullOrEmpty(ext) ? ".?" : ext,
status, error, duration);
}
}
// ===== FILE: VersionStamp.cs =====
using System;
using System.Linq;
using System.Reflection;
namespace LispLoaderExtra
{
/// <summary>
/// Prints the version banner whenever the plugin loads.
///
/// Format: v1.0.000 | built 12.06.2026 10:30:00
///
/// Version β AssemblyVersion set in <Version> in the .csproj
/// Timestamp β AssemblyMetadata("BuildTimestamp", "dd.MM.yyyy_HH.mm.ss")
/// injected at compile time by the MSBuild SetBuildTimestamp target.
/// No generated .cs file required.
/// </summary>
internal static class VersionStamp
{
private const string Source = "LispLoader";
internal static void Print()
{
Assembly asm = Assembly.GetExecutingAssembly();
// ββ Version βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Version.ToString(3) drops leading zeros from the build number:
// 1.2.000 β "1.2.0" (wrong). Format manually to get "1.2.000".
var v = asm.GetName().Version;
string version = v != null
? v.Major + "." + v.Minor + "." + v.Build.ToString("D3")
: "?.?.???";
// ββ Timestamp from AssemblyMetadata βββββββββββββββββββββββββββββββ
// The .csproj target adds:
// [assembly: AssemblyMetadata("BuildTimestamp", "12.06.2026_10.30.00")]
string raw = asm
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == "BuildTimestamp")
?.Value
?? "??.??.????_??.??.??";
// raw = "12.06.2026_10.30.00"
string[] halves = raw.Split('_');
string date = halves.Length > 0 ? halves[0] : "??"; // 12.06.2026
string time = halves.Length > 1 ? halves[1].Replace('.', ':') : "??"; // 10:30:00
string banner = $"v{version} | built {date} {time}";
LogManager.Separator();
LogManager.Info(Source, banner);
LogManager.Separator();
}
}
}
// ===== FILE: LoaderWindow.cs =====
// No Autodesk.AutoCAD.Runtime using here β avoids Exception ambiguity.
// All ACAD calls go through FileLoader / LogManager / DiskScanner instead.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LispLoaderExtra
{
/// <summary>
/// Main loader window β type LISPLOADEXTRA in AutoCAD.
///
/// v1.5 β compile-error fixes + new improvements
/// ββββββββββββββββββββββββββββββββββββββββββββββββ
/// Fixes (from compiler errors reported on v1.4):
/// β’ 'Exception' ambiguity: no Autodesk.AutoCAD.Runtime using in this file;
/// all catches use System.Exception via the global alias.
/// β’ SendStringToExecute named-param 'wrap' β positional args in FileLoader.
///
/// New in v1.5:
/// β’ "Load Selected" toolbar button + F3 shortcut
/// β’ Right-click context menu (Load / Reload / Open Folder / Copy Path /
/// Copy All Paths / Check / Uncheck)
/// β’ Progress bar in the status strip
/// β’ Column header tooltips
/// β’ DLL File cell shows version stamp in italic after load
/// β’ _loading guard prevents double-triggers
/// β’ FileSystemWatcher: auto-rescan when files change on disk
/// β’ Persist checked state to ss\checked.json between sessions
/// β’ "Open in Notepad++" right-click item (falls back to Notepad)
/// β’ Error-diff highlight: rows that changed FAILβOK or OKβFAIL glow briefly
///
/// Form behaviour:
/// FormBorderStyle.SizableToolWindow β small title bar, no taskbar entry
/// ProcessCmdKey swallows Escape (viewport-safe)
/// Modeless β stays open while AutoCAD is active
/// Live log panel (bottom pane) β real-time, colour-coded
/// Store Log / Clear / Auto-scroll
/// </summary>
internal sealed class LoaderWindow : Form
{
// ββ Palette βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static readonly Color BgDark = Color.FromArgb(26, 26, 34);
private static readonly Color BgPanel = Color.FromArgb(36, 36, 48);
private static readonly Color BgGrid = Color.FromArgb(20, 20, 28);
private static readonly Color BgSearch = Color.FromArgb(44, 44, 58);
private static readonly Color BgLog = Color.FromArgb(16, 16, 22);
private static readonly Color ColOk = Color.FromArgb(80, 200, 100);
private static readonly Color ColFail = Color.FromArgb(220, 70, 70);
private static readonly Color ColMiss = Color.FromArgb(220, 155, 40);
private static readonly Color ColText = Color.FromArgb(210, 210, 222);
private static readonly Color ColSub = Color.FromArgb(120, 120, 138);
private static readonly Color ColAccent = Color.FromArgb(80, 145, 215);
private static readonly Color ColWarn = Color.FromArgb(230, 160, 40);
private static readonly Color ColError = Color.FromArgb(220, 70, 70);
private static readonly Color ColInfo = Color.FromArgb(100, 200, 240);
private static readonly Color ColLsp = Color.FromArgb(100, 210, 120);
private static readonly Color ColVlx = Color.FromArgb(180, 130, 240);
private static readonly Color ColFas = Color.FromArgb(240, 185, 80);
private static readonly Color ColDll = Color.FromArgb(240, 205, 70);
private static readonly Color ColC3D = Color.FromArgb(110, 185, 240);
private static readonly Color ColAcad = Color.FromArgb(160, 230, 155);
private static readonly Color ColFlash = Color.FromArgb(255, 255, 100); // diff-flash
// ββ Column indices ββββββββββββββββββββββββββββββββββββββββββββββββββββ
private const int CI_Chk = 0;
private const int CI_Status = 1;
private const int CI_Num = 2;
private const int CI_Cat = 3;
private const int CI_Menu = 4;
private const int CI_File = 5;
private const int CI_Ext = 6;
private const int CI_Ms = 7;
private const int CI_Path = 8;
// ββ Checked-state persistence file ββββββββββββββββββββββββββββββββββββ
private static readonly string CheckedFile =
System.IO.Path.Combine(LogManager.LogFolder, "checked.json");
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private List<ScannedFile> _allFiles = new List<ScannedFile>();
private List<ScannedFile> _visible = new List<ScannedFile>();
private bool _scanning = false;
private bool _loading = false;
// Tracks which files changed status in the last load (for flash)
private readonly System.Collections.Generic.HashSet<string> _flashPaths
= new System.Collections.Generic.HashSet<string>(
StringComparer.OrdinalIgnoreCase);
// FileSystemWatcher for auto-rescan
private FileSystemWatcher? _watcher;
private System.Windows.Forms.Timer? _rescanTimer; // debounce
// ββ Controls β toolbar ββββββββββββββββββββββββββββββββββββββββββββββββ
private ComboBox _cbCat = null!;
private ComboBox _cbName = null!;
private TextBox _txtSearch = null!;
private Button _btnScan = null!;
private Button _btnLoad = null!;
private Button _btnReload = null!;
private Button _btnLoadSel = null!;
private Button _btnAll = null!;
private Button _btnNone = null!;
private Button _btnReport = null!;
// ββ Controls β main βββββββββββββββββββββββββββββββββββββββββββββββββββ
private DataGridView _grid = null!;
private Label _lblTitle = null!;
private Label _lblStatus = null!;
private ProgressBar _progress = null!;
private ToolTip _tooltip = null!;
private ContextMenuStrip _ctxMenu = null!;
// ββ Log panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private RichTextBox _logBox = null!;
private Button _btnStoreLog = null!;
private Button _btnClearLog = null!;
private CheckBox _chkAutoScroll = null!;
private SplitContainer _split = null!;
// ββ Constructor βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal LoaderWindow()
{
BuildLayout();
LogManager.Subscribe(OnLogLine);
// Seed log panel with prior session lines from ring-buffer
foreach (string line in LogManager.GetRingSnapshot())
AppendLogLine(line);
LoadCheckedState(); // restore persisted checkbox ticks
SetupWatcher(); // FileSystemWatcher for auto-rescan
DoScan();
}
// ββ Escape swallow ββββββββββββββββββββββββββββββββββββββββββββββββββββ
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape) return true; // consumed β safe in viewport
return base.ProcessCmdKey(ref msg, keyData);
}
// ββ Cleanup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
protected override void OnFormClosed(FormClosedEventArgs e)
{
LogManager.Unsubscribe(OnLogLine);
SaveCheckedState();
TearDownWatcher();
base.OnFormClosed(e);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// LAYOUT
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildLayout()
{
Text = "LispLoaderExtra";
Size = new Size(1300, 840);
MinimumSize = new Size(920, 600);
StartPosition = FormStartPosition.CenterScreen;
BackColor = BgDark;
ForeColor = ColText;
Font = new Font("Segoe UI", 9f);
FormBorderStyle = FormBorderStyle.SizableToolWindow;
ShowInTaskbar = false;
KeyPreview = true;
KeyDown += OnKeyDown;
_tooltip = new ToolTip {
AutoPopDelay = 10000,
InitialDelay = 400,
ReshowDelay = 200,
ShowAlways = true,
};
BuildContextMenu();
BuildTitleBar();
BuildToolbar();
BuildStatusBar(); // Dock.Bottom β must be added before Fill panel
BuildSplitArea();
}
// ββ Title bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildTitleBar()
{
var panel = new Panel { Dock = DockStyle.Top, Height = 42, BackColor = BgPanel };
_lblTitle = new Label {
Text = "LispLoaderExtra",
Font = new Font("Segoe UI", 13f, FontStyle.Bold),
ForeColor = ColAccent,
BackColor = Color.Transparent,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(12, 0, 0, 0),
};
panel.Controls.Add(_lblTitle);
Controls.Add(panel);
}
// ββ Toolbar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildToolbar()
{
var tb = new Panel { Dock = DockStyle.Top, Height = 42, BackColor = BgPanel };
int x = 10;
tb.Controls.Add(Lbl("Cat:", x, 12, 28)); x += 30;
_cbCat = Combo(x, 7, 100);
_cbCat.Items.AddRange(new object[] { "All", "AutoCAD", "Civil 3D" });
_cbCat.SelectedIndex = 0;
_cbCat.SelectedIndexChanged += OnCatChanged;
tb.Controls.Add(_cbCat); x += 108;
tb.Controls.Add(Lbl("Name:", x, 12, 40)); x += 42;
_cbName = Combo(x, 7, 190);
_cbName.SelectedIndexChanged += OnNameChanged;
tb.Controls.Add(_cbName); x += 198;
tb.Controls.Add(Lbl("π", x, 11, 20)); x += 22;
_txtSearch = new TextBox {
Location = new Point(x, 8),
Size = new Size(150, 26),
BackColor = BgSearch,
ForeColor = ColText,
Font = new Font("Segoe UI", 9f),
BorderStyle = BorderStyle.FixedSingle,
};
_txtSearch.TextChanged += (s, e) => ApplyFilters();
tb.Controls.Add(_txtSearch); x += 158;
x += 8;
_btnScan = Btn("π Scan", ColAccent, 80, x, tb); x += 88;
_btnLoad = Btn("βΆ Load", Color.FromArgb(35, 120, 55), 78, x, tb); x += 86;
_btnReload = Btn("π Reload", Color.FromArgb(35, 95, 125), 82, x, tb); x += 90;
_btnLoadSel = Btn("βΆ Load Sel.", Color.FromArgb(80, 100, 40), 90, x, tb); x += 98;
x += 6;
_btnAll = Btn("β All", Color.FromArgb(55, 55, 70), 60, x, tb); x += 68;
_btnNone = Btn("β None", Color.FromArgb(55, 55, 70), 60, x, tb);
_btnReport = Btn("π Report", Color.FromArgb(75, 55, 110), 96, 0, tb);
_btnReport.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_btnReport.Location = new Point(tb.Width - 108, 6);
tb.Resize += (s, e) => _btnReport.Location = new Point(tb.Width - 108, 6);
tb.Controls.Add(_btnReport);
_btnScan.Click += (s, e) => DoScan();
_btnLoad.Click += (s, e) => DoLoad(LoadScope.Checked, reload: false);
_btnReload.Click += (s, e) => DoLoad(LoadScope.Checked, reload: true);
_btnLoadSel.Click += (s, e) => DoLoad(LoadScope.Selected, reload: false);
_btnAll.Click += (s, e) => SetAllChecked(true);
_btnNone.Click += (s, e) => SetAllChecked(false);
_btnReport.Click += (s, e) => OpenReport();
_tooltip.SetToolTip(_btnScan, "Re-scan disk (F5)");
_tooltip.SetToolTip(_btnLoad, "Load checked files β skip already-OK (F2)");
_tooltip.SetToolTip(_btnReload, "Force-reload ALL checked files");
_tooltip.SetToolTip(_btnLoadSel, "Load only highlighted grid row(s) (F3)");
_tooltip.SetToolTip(_btnAll, "Check all visible rows (Ctrl+A)");
_tooltip.SetToolTip(_btnNone, "Uncheck all visible rows");
_tooltip.SetToolTip(_btnReport, "Open full load-session report");
Controls.Add(tb);
}
// ββ Right-click context menu ββββββββββββββββββββββββββββββββββββββββββ
private void BuildContextMenu()
{
_ctxMenu = new ContextMenuStrip {
BackColor = Color.FromArgb(36, 36, 48),
ForeColor = ColText,
Font = new Font("Segoe UI", 9f),
RenderMode = ToolStripRenderMode.System,
};
CtxItem("βΆ Load Selected", OnCtxLoad, "Load selected rows on ACAD thread");
CtxItem("π Reload Selected", OnCtxReload, "Force-reload selected rows");
_ctxMenu.Items.Add(new ToolStripSeparator());
CtxItem("π Open Folder", OnCtxOpenFolder, "Open file's folder in Explorer");
CtxItem("π Open in Editor", OnCtxOpenEditor, "Open in Notepad++ or Notepad");
CtxItem("π Copy Path", OnCtxCopyPath, "Copy full path to clipboard");
CtxItem("π Copy All Paths", OnCtxCopyAllPaths,"Copy paths of all selected rows");
_ctxMenu.Items.Add(new ToolStripSeparator());
CtxItem("β Check Selected", OnCtxCheck, "Tick selected rows");
CtxItem("β Uncheck Selected", OnCtxUncheck, "Untick selected rows");
}
private void CtxItem(string text, EventHandler h, string tip)
{
var item = new ToolStripMenuItem(text) {
BackColor = Color.FromArgb(36, 36, 48),
ForeColor = ColText,
};
item.Click += h;
_ctxMenu.Items.Add(item);
}
// ββ Status bar + progress βββββββββββββββββββββββββββββββββββββββββββββ
private void BuildStatusBar()
{
var bar = new Panel {
Dock = DockStyle.Bottom,
Height = 28,
BackColor = Color.FromArgb(18, 18, 26),
};
_lblStatus = new Label {
Dock = DockStyle.Fill,
ForeColor = ColSub,
Font = new Font("Consolas", 8f),
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(10, 0, 0, 0),
};
_progress = new ProgressBar {
Dock = DockStyle.Right,
Width = 180,
Minimum = 0,
Maximum = 100,
Value = 0,
Style = ProgressBarStyle.Continuous,
Visible = false,
};
bar.Controls.Add(_lblStatus);
bar.Controls.Add(_progress);
Controls.Add(bar);
}
// ββ Split area: grid (top) / log (bottom) βββββββββββββββββββββββββββββ
private void BuildSplitArea()
{
_split = new SplitContainer {
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
Panel1MinSize = 180,
Panel2MinSize = 60,
SplitterDistance = 510,
BackColor = BgDark,
SplitterWidth = 5,
};
BuildGrid();
BuildLogPanel();
Controls.Add(_split);
}
// ββ Grid ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildGrid()
{
_grid = new DataGridView {
Dock = DockStyle.Fill,
BackgroundColor = BgGrid,
BorderStyle = BorderStyle.None,
RowHeadersVisible = false,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
ReadOnly = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
MultiSelect = true,
AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None,
ColumnHeadersHeightSizeMode =
DataGridViewColumnHeadersHeightSizeMode.DisableResizing,
ColumnHeadersHeight = 28,
RowTemplate = { Height = 22 },
GridColor = Color.FromArgb(40, 40, 54),
DefaultCellStyle = {
BackColor = BgGrid,
ForeColor = ColText,
SelectionBackColor = Color.FromArgb(48, 88, 138),
SelectionForeColor = Color.White,
Font = new Font("Consolas", 8.5f),
Padding = new Padding(3, 0, 3, 0),
},
ColumnHeadersDefaultCellStyle = {
BackColor = Color.FromArgb(42, 42, 56),
ForeColor = ColSub,
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
SelectionBackColor = Color.FromArgb(42, 42, 56),
},
EnableHeadersVisualStyles = false,
ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText,
ContextMenuStrip = _ctxMenu,
};
// Checkbox column
_grid.Columns.Add(new DataGridViewCheckBoxColumn {
HeaderText = "β",
Width = 30,
ReadOnly = false,
Resizable = DataGridViewTriState.False,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
AddCol("Status", 58, false, "center", "Load result: OK / FAIL / MISSING / SKIP");
AddCol("#", 46, false, "center", "Row number after sort");
AddCol("Cat", 68, false, "center", "AutoCAD or Civil 3D");
AddCol("Menu", 155, false, "left", "Parent _menu folder name");
AddCol("File", 230, true, "left", "File name β DLLs show version stamp after load");
AddCol("Ext", 42, false, "center", "File extension");
AddCol("ms", 50, false, "center", "Load time in milliseconds");
AddCol("Full path", 380, true, "left", "Absolute path on disk");
_grid.CellFormatting += GridCellFormatting;
_grid.CellValueChanged += GridCellValueChanged;
_grid.CellDoubleClick += GridCellDoubleClick;
_grid.CellMouseMove += GridCellMouseMove;
// Commit checkbox immediately
_grid.CurrentCellDirtyStateChanged += (s, e) => {
if (_grid.IsCurrentCellDirty)
_grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
};
// Header checkbox col β toggle all
_grid.ColumnHeaderMouseClick += (s, e) => {
if (e.ColumnIndex == CI_Chk)
SetAllChecked(_visible.Any(f => !f.Checked));
};
// Right-click β ensure clicked row is selected first
_grid.CellMouseDown += (s, e) => {
if (e.Button == MouseButtons.Right && e.RowIndex >= 0)
if (!_grid.Rows[e.RowIndex].Selected)
{
_grid.ClearSelection();
_grid.Rows[e.RowIndex].Selected = true;
}
};
_split.Panel1.Controls.Add(_grid);
}
// ββ Log panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildLogPanel()
{
var logBar = new Panel { Dock = DockStyle.Top, Height = 32, BackColor = BgPanel };
logBar.Controls.Add(new Label {
Text = "π Live Log",
Location = new Point(8, 7),
Size = new Size(82, 20),
ForeColor = ColSub,
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
BackColor = Color.Transparent,
});
_btnStoreLog = LogBtn("πΎ Store Log", Color.FromArgb(30, 60, 40),
Color.FromArgb(55, 120, 75), 94);
_btnStoreLog.Click += OnStoreLog;
_tooltip.SetToolTip(_btnStoreLog,
"Save live log to a timestamped file in the ss\\ folder");
_btnClearLog = LogBtn("π Clear", Color.FromArgb(50, 30, 30),
Color.FromArgb(110, 55, 55), 192);
_btnClearLog.Size = new Size(68, 24);
_btnClearLog.Click += (s, e) => _logBox.Clear();
_chkAutoScroll = new CheckBox {
Text = "Auto-scroll",
Location = new Point(268, 7),
Size = new Size(92, 20),
Checked = true,
ForeColor = ColSub,
Font = new Font("Segoe UI", 8f),
BackColor = Color.Transparent,
};
logBar.Controls.AddRange(new Control[] {
_btnStoreLog, _btnClearLog, _chkAutoScroll });
_logBox = new RichTextBox {
Dock = DockStyle.Fill,
BackColor = BgLog,
ForeColor = ColText,
Font = new Font("Consolas", 8f),
ReadOnly = true,
ScrollBars = RichTextBoxScrollBars.Both,
WordWrap = false,
BorderStyle = BorderStyle.None,
};
_split.Panel2.Controls.Add(_logBox);
_split.Panel2.Controls.Add(logBar);
}
private Button LogBtn(string text, Color bg, Color border, int x)
{
var b = new Button {
Text = text,
Location = new Point(x, 4),
Size = new Size(90, 24),
FlatStyle = FlatStyle.Flat,
BackColor = bg,
ForeColor = ColText,
Font = new Font("Segoe UI", 8f),
Cursor = Cursors.Hand,
FlatAppearance = { BorderColor = border, BorderSize = 1 },
};
return b;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// FILE SYSTEM WATCHER β auto-rescan when disk changes
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void SetupWatcher()
{
try
{
if (!Directory.Exists(DiskScanner.BaseFolder)) return;
_watcher = new FileSystemWatcher(DiskScanner.BaseFolder) {
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite |
NotifyFilters.DirectoryName,
Filter = "*.*",
EnableRaisingEvents = true,
};
// Debounce: wait 3 s of quiet after the last event before rescanning
_rescanTimer = new System.Windows.Forms.Timer { Interval = 3000 };
_rescanTimer.Tick += (s, e) => {
_rescanTimer.Stop();
if (IsHandleCreated && !IsDisposed)
Invoke((MethodInvoker)(() => {
LogManager.Info("LoaderWindow",
"FileSystemWatcher: disk change detected β rescanning.");
DoScan();
}));
};
FileSystemEventHandler kick = (s, e) => {
string ext = System.IO.Path.GetExtension(e.Name ?? "")
.ToLowerInvariant();
if (ext == ".lsp" || ext == ".vlx" || ext == ".fas" || ext == ".dll")
if (IsHandleCreated && !IsDisposed)
Invoke((MethodInvoker)(() => _rescanTimer.Start()));
};
_watcher.Created += kick;
_watcher.Deleted += kick;
_watcher.Renamed += (s, e) => kick(s,
new FileSystemEventArgs(WatcherChangeTypes.Changed, e.FullPath, e.Name));
LogManager.Info("LoaderWindow",
"FileSystemWatcher active on: " + DiskScanner.BaseFolder);
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow",
"FileSystemWatcher setup failed: " + ex.Message);
}
}
private void TearDownWatcher()
{
try
{
_rescanTimer?.Stop();
_rescanTimer?.Dispose();
_watcher?.Dispose();
}
catch { }
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CHECKED-STATE PERSISTENCE
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Simple line-delimited file: one full path per line = file is checked.
// (Keeps net48 compat without pulling in System.Text.Json / Newtonsoft.)
private void SaveCheckedState()
{
try
{
Directory.CreateDirectory(LogManager.LogFolder);
var lines = _allFiles
.Where(f => f.Checked)
.Select(f => f.FullPath);
File.WriteAllLines(CheckedFile, lines);
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow",
"Could not save checked state: " + ex.Message);
}
}
private void LoadCheckedState()
{
try
{
if (!File.Exists(CheckedFile)) return;
var checked_ = new System.Collections.Generic.HashSet<string>(
File.ReadAllLines(CheckedFile),
StringComparer.OrdinalIgnoreCase);
foreach (var f in _allFiles)
f.Checked = checked_.Contains(f.FullPath);
// Refresh grid checkboxes
for (int i = 0; i < _visible.Count && i < _grid.Rows.Count; i++)
_grid.Rows[i].Cells[CI_Chk].Value = _visible[i].Checked;
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow",
"Could not restore checked state: " + ex.Message);
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// LIVE LOG
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void OnLogLine(string line)
{
if (IsDisposed) return;
if (InvokeRequired)
{
try { Invoke(new Action<string>(OnLogLine), line); }
catch { }
return;
}
AppendLogLine(line);
}
private void AppendLogLine(string line)
{
if (_logBox == null || _logBox.IsDisposed) return;
Color c = ColText;
if (line.IndexOf("[ERROR]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColError;
else if (line.IndexOf("[WARN ]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColWarn;
else if (line.IndexOf("[INFO ]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColInfo;
else if (line.StartsWith("βββ") || line.StartsWith("---")) c = ColSub;
int start = _logBox.TextLength;
_logBox.AppendText(line + "\n");
_logBox.Select(start, line.Length);
_logBox.SelectionColor = c;
_logBox.SelectionLength = 0;
if (_chkAutoScroll.Checked)
_logBox.ScrollToCaret();
}
private void OnStoreLog(object? sender, EventArgs e)
{
try
{
string stamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
string outFile = System.IO.Path.Combine(LogManager.LogFolder,
"LispLoaderExtra_stored_" + stamp + ".log");
Directory.CreateDirectory(LogManager.LogFolder);
File.WriteAllText(outFile, _logBox.Text);
LogManager.Info("LoaderWindow", "Log stored β " + outFile);
MessageBox.Show("Log saved to:\n" + outFile, "LispLoaderExtra",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (System.Exception ex)
{
MessageBox.Show("Failed to store log:\n" + ex.Message, "LispLoaderExtra",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CONTEXT MENU HANDLERS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void OnCtxLoad(object? s, EventArgs e) => DoLoad(LoadScope.Selected, false);
private void OnCtxReload(object? s, EventArgs e) => DoLoad(LoadScope.Selected, true);
private void OnCtxCheck(object? s, EventArgs e) => SetSelectedChecked(true);
private void OnCtxUncheck(object? s, EventArgs e) => SetSelectedChecked(false);
private void OnCtxOpenFolder(object? s, EventArgs e)
{
var f = GetFirstSelectedFile();
if (f == null) return;
try
{
if (File.Exists(f.FullPath))
Process.Start("explorer.exe", "/select,\"" + f.FullPath + "\"");
else
{
string? dir = System.IO.Path.GetDirectoryName(f.FullPath);
if (dir != null && Directory.Exists(dir))
Process.Start("explorer.exe", dir);
}
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow", "Explorer open failed: " + ex.Message);
}
}
private void OnCtxOpenEditor(object? s, EventArgs e)
{
var f = GetFirstSelectedFile();
if (f == null) return;
if (!File.Exists(f.FullPath))
{
SetStatus("File not found: " + f.FullPath);
return;
}
try
{
// Try Notepad++ first (most common on dev machines)
string npp = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Notepad++", "notepad++.exe");
if (!File.Exists(npp))
npp = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
"Notepad++", "notepad++.exe");
string editor = File.Exists(npp) ? npp : "notepad.exe";
Process.Start(editor, "\"" + f.FullPath + "\"");
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow", "Open in editor failed: " + ex.Message);
}
}
private void OnCtxCopyPath(object? s, EventArgs e)
{
var f = GetFirstSelectedFile();
if (f == null) return;
try { Clipboard.SetText(f.FullPath); }
catch { }
}
private void OnCtxCopyAllPaths(object? s, EventArgs e)
{
var paths = GetSelectedFiles().Select(f => f.FullPath);
try { Clipboard.SetText(string.Join(Environment.NewLine, paths)); }
catch { }
}
// ββ Selection helpers βββββββββββββββββββββββββββββββββββββββββββββββββ
private ScannedFile? GetFirstSelectedFile()
{
foreach (DataGridViewRow row in _grid.SelectedRows)
if (row.Tag is ScannedFile f) return f;
return null;
}
private List<ScannedFile> GetSelectedFiles()
{
var list = new List<ScannedFile>();
var rows = _grid.SelectedRows.Cast<DataGridViewRow>()
.OrderBy(r => r.Index);
foreach (var row in rows)
if (row.Tag is ScannedFile f) list.Add(f);
return list;
}
private void SetSelectedChecked(bool value)
{
foreach (DataGridViewRow row in _grid.SelectedRows)
{
row.Cells[CI_Chk].Value = value;
if (row.Tag is ScannedFile f) f.Checked = value;
}
_grid.Refresh();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// SCAN
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void DoScan()
{
if (_scanning) return;
_scanning = true;
_btnScan.Text = "Scanningβ¦";
_btnScan.Enabled = false;
SetStatus("Scanning diskβ¦");
Application.DoEvents();
_grid.Rows.Clear();
_allFiles.Clear();
try
{
_allFiles = DiskScanner.Scan();
LoadCheckedState(); // restore ticks after rescan
RefreshNameDropdown();
UpdateTitleAndStatus();
}
catch (System.Exception ex)
{
LogManager.Error("LoaderWindow", "Scan failed: " + ex.Message);
SetStatus("Scan failed: " + ex.Message);
}
finally
{
_scanning = false;
_btnScan.Text = "π Scan";
_btnScan.Enabled = true;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// DROPDOWNS / FILTER
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void OnCatChanged(object sender, EventArgs e) => RefreshNameDropdown();
private void OnNameChanged(object sender, EventArgs e) => ApplyFilters();
private void RefreshNameDropdown()
{
string cat = _cbCat.SelectedItem?.ToString() ?? "All";
_cbName.SelectedIndexChanged -= OnNameChanged;
_cbName.Items.Clear();
_cbName.Items.Add("All");
foreach (var n in DiskScanner.GetMenuNames(cat, _allFiles))
_cbName.Items.Add(n);
_cbName.SelectedIndex = 0;
_cbName.SelectedIndexChanged += OnNameChanged;
ApplyFilters();
}
private void ApplyFilters()
{
string cat = _cbCat.SelectedItem?.ToString() ?? "All";
string name = _cbName.SelectedItem?.ToString() ?? "All";
string search = _txtSearch.Text.Trim();
_visible = _allFiles
.Where(f => cat == "All" ||
string.Equals(f.Category, cat, StringComparison.OrdinalIgnoreCase))
.Where(f => name == "All" ||
string.Equals(f.MenuName, name, StringComparison.OrdinalIgnoreCase))
.Where(f => search == "" ||
f.FileName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 ||
f.MenuName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
PopulateGrid();
UpdateTitleAndStatus();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// GRID POPULATION
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void PopulateGrid()
{
_grid.SuspendLayout();
_grid.Rows.Clear();
foreach (var f in _visible)
{
int i = _grid.Rows.Add();
var row = _grid.Rows[i];
row.Tag = f;
row.Cells[CI_Chk ].Value = f.Checked;
row.Cells[CI_Status].Value = StatusLabel(f);
row.Cells[CI_Num ].Value = f.RowIndex.ToString();
row.Cells[CI_Cat ].Value = f.Category;
row.Cells[CI_Menu ].Value = f.MenuName;
row.Cells[CI_File ].Value = FileDisplayName(f);
row.Cells[CI_Ext ].Value = f.TypeLabel;
row.Cells[CI_Ms ].Value = f.LastResult != null
? f.LastResult.Duration.TotalMilliseconds.ToString("0") : "";
row.Cells[CI_Path ].Value = f.FullPath;
}
_grid.ResumeLayout();
}
private void RefreshRow(int gridRow, ScannedFile f)
{
if (gridRow < 0 || gridRow >= _grid.Rows.Count) return;
var row = _grid.Rows[gridRow];
row.Cells[CI_Status].Value = StatusLabel(f);
row.Cells[CI_File ].Value = FileDisplayName(f);
row.Cells[CI_Ms ].Value = f.LastResult != null
? f.LastResult.Duration.TotalMilliseconds.ToString("0") : "";
}
private static string FileDisplayName(ScannedFile f)
{
if (f.Extension == ".dll" &&
f.LastResult?.Status == LoadStatus.OK &&
!string.IsNullOrEmpty(f.LastResult.ErrorMessage))
return f.FileName + " Β· " + f.LastResult.ErrorMessage;
return f.FileName;
}
private static string StatusLabel(ScannedFile f) =>
f.LastResult == null ? "" :
f.LastResult.Status == LoadStatus.OK ? "OK" :
f.LastResult.Status == LoadStatus.Failed ? "FAIL" :
f.LastResult.Status == LoadStatus.NotFound ? "MISSING" :
f.LastResult.Status == LoadStatus.Skipped ? "SKIP" : "";
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// LOAD / RELOAD
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private enum LoadScope { Checked, Selected }
private void DoLoad(LoadScope scope, bool reload)
{
if (_loading) { SetStatus("Load already in progress."); return; }
// ββ Build work list βββββββββββββββββββββββββββββββββββββββββββββββ
var toLoad = new List<(ScannedFile file, int row)>();
if (scope == LoadScope.Selected)
{
var rows = _grid.SelectedRows.Cast<DataGridViewRow>()
.OrderBy(r => r.Index);
foreach (var row in rows)
{
if (!(row.Tag is ScannedFile f)) continue;
int vi = _visible.IndexOf(f);
if (vi >= 0) toLoad.Add((f, vi));
}
}
else
{
for (int i = 0; i < _visible.Count; i++)
{
var f = _visible[i];
bool chk = _grid.Rows[i].Cells[CI_Chk].Value as bool? ?? false;
if (!chk) continue;
if (!reload && f.LastResult?.Status == LoadStatus.OK) continue;
toLoad.Add((f, i));
}
}
if (toLoad.Count == 0)
{
SetStatus(scope == LoadScope.Selected
? "No rows selected."
: reload ? "Nothing checked to reload."
: "All checked files already OK β use π Reload to force.");
return;
}
// ββ Lock UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_loading = true;
_btnLoad.Enabled = false;
_btnReload.Enabled = false;
_btnLoadSel.Enabled = false;
_btnScan.Enabled = false;
_progress.Maximum = toLoad.Count;
_progress.Value = 0;
_progress.Visible = true;
_flashPaths.Clear();
try
{
LoadSession.Begin();
int ok = 0, fail = 0, miss = 0;
for (int idx = 0; idx < toLoad.Count; idx++)
{
var (file, gridRow) = toLoad[idx];
LoadStatus prevStatus = file.LastResult?.Status ?? LoadStatus.Skipped;
SetStatus("Loading " + (idx + 1) + " / " + toLoad.Count +
": " + file.FileName);
_progress.Value = idx + 1;
Application.DoEvents();
// FileLoader.Load marshals to ACAD thread internally
LoadResult result = FileLoader.Load(file.FullPath);
file.LastResult = result;
LoadSession.Add(result);
RefreshRow(gridRow, file);
_grid.InvalidateRow(gridRow);
Application.DoEvents();
// Track status changes for flash highlighting
if (result.Status != prevStatus)
_flashPaths.Add(file.FullPath);
switch (result.Status)
{
case LoadStatus.OK: ok++; break;
case LoadStatus.Failed: fail++; break;
case LoadStatus.NotFound: miss++; break;
}
}
_grid.Refresh();
FlashChangedRows(); // brief highlight for rows that changed status
string summary = "Done β β OK: " + ok;
if (fail > 0) summary += " β FAILED: " + fail;
if (miss > 0) summary += " β NOT FOUND: " + miss;
summary += " (" + LoadSession.TotalDuration.TotalSeconds.ToString("0.0") + "s)";
if (fail > 0 || miss > 0) summary += " β π Report for details";
SetStatus(summary);
UpdateTitleAndStatus();
LogManager.Separator();
LogManager.Info("LoaderWindow", LoadSession.SummaryLine);
LogManager.Separator();
}
finally
{
_loading = false;
_btnLoad.Enabled = true;
_btnReload.Enabled = true;
_btnLoadSel.Enabled = true;
_btnScan.Enabled = true;
_progress.Visible = false;
}
}
// ββ Status-change flash βββββββββββββββββββββββββββββββββββββββββββββββ
// Briefly highlights rows whose status changed so the user can spot
// what actually changed without scanning the whole grid.
private void FlashChangedRows()
{
if (_flashPaths.Count == 0) return;
// Paint flash colour for 600 ms then restore normal formatting
var flashTimer = new System.Windows.Forms.Timer { Interval = 600 };
flashTimer.Tick += (s, e) => {
flashTimer.Stop();
flashTimer.Dispose();
_flashPaths.Clear();
_grid.Refresh();
};
// The CellFormatting handler already checks _flashPaths
_grid.Refresh();
flashTimer.Start();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// GRID EVENTS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void GridCellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex != CI_Chk || e.RowIndex < 0) return;
if (_grid.Rows[e.RowIndex].Tag is ScannedFile f)
f.Checked = _grid.Rows[e.RowIndex].Cells[CI_Chk].Value as bool? ?? false;
}
private void GridCellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex == CI_Chk) return;
if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;
try
{
if (File.Exists(f.FullPath))
Process.Start("explorer.exe", "/select,\"" + f.FullPath + "\"");
else
{
string? dir = System.IO.Path.GetDirectoryName(f.FullPath);
if (dir != null && Directory.Exists(dir))
Process.Start("explorer.exe", dir);
}
}
catch (System.Exception ex)
{
LogManager.Warn("LoaderWindow", "Explorer open failed: " + ex.Message);
}
}
private string _lastTooltipPath = "";
private void GridCellMouseMove(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.RowIndex < 0) return;
if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;
if (f.FullPath == _lastTooltipPath) return;
_lastTooltipPath = f.FullPath;
string tip = f.FullPath;
if (f.LastResult?.Status == LoadStatus.Failed)
tip += "\n\nError: " + f.LastResult.ErrorMessage;
else if (f.LastResult?.Status == LoadStatus.OK && f.Extension == ".dll")
tip += "\n\nLoaded OK (" +
f.LastResult.Duration.TotalMilliseconds.ToString("0") + " ms)" +
"\nVersion: " + f.LastResult.ErrorMessage;
else if (f.LastResult?.Status == LoadStatus.OK)
tip += "\n\nLoaded OK (" +
f.LastResult.Duration.TotalMilliseconds.ToString("0") + " ms)";
tip += "\n\nRight-click for options β’ Double-click to open folder";
_tooltip.SetToolTip(_grid, tip);
}
private void GridCellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
if (e.RowIndex < 0 || e.RowIndex >= _grid.Rows.Count) return;
if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;
// Flash rows that changed status in the last load
if (_flashPaths.Contains(f.FullPath))
{
e.CellStyle.BackColor = Color.FromArgb(60, 55, 20);
e.CellStyle.ForeColor = ColFlash;
return;
}
// Status column
if (e.ColumnIndex == CI_Status && f.LastResult != null)
{
e.CellStyle.ForeColor = f.LastResult.Status == LoadStatus.OK ? ColOk :
f.LastResult.Status == LoadStatus.Failed ? ColFail :
f.LastResult.Status == LoadStatus.NotFound ? ColMiss :
ColSub;
e.CellStyle.Font = new Font("Consolas", 8f, FontStyle.Bold);
}
// Extension / file colour
if (e.ColumnIndex == CI_Ext || e.ColumnIndex == CI_File)
{
e.CellStyle.ForeColor = f.Extension == ".lsp" ? ColLsp :
f.Extension == ".vlx" ? ColVlx :
f.Extension == ".fas" ? ColFas :
f.Extension == ".dll" ? ColDll : ColText;
// DLL with version: italic
if (e.ColumnIndex == CI_File && f.Extension == ".dll" &&
f.LastResult?.Status == LoadStatus.OK)
e.CellStyle.Font = new Font("Consolas", 8.5f, FontStyle.Italic);
}
// Category colour
if (e.ColumnIndex == CI_Cat)
e.CellStyle.ForeColor = f.Category == "Civil 3D" ? ColC3D : ColAcad;
// Dim missing rows
if (f.LastResult?.Status == LoadStatus.NotFound)
e.CellStyle.ForeColor = Color.FromArgb(
(int)(e.CellStyle.ForeColor.R * 0.5),
(int)(e.CellStyle.ForeColor.G * 0.5),
(int)(e.CellStyle.ForeColor.B * 0.5));
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// KEYBOARD SHORTCUTS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void OnKeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.F5:
DoScan();
e.Handled = true;
break;
case Keys.F2:
DoLoad(LoadScope.Checked, false);
e.Handled = true;
break;
case Keys.F3:
DoLoad(LoadScope.Selected, false);
e.Handled = true;
break;
case Keys.A when e.Control:
SetAllChecked(true);
e.Handled = true;
break;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// HELPERS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void SetAllChecked(bool value)
{
_grid.SuspendLayout();
foreach (DataGridViewRow row in _grid.Rows)
{
row.Cells[CI_Chk].Value = value;
if (row.Tag is ScannedFile f) f.Checked = value;
}
_grid.ResumeLayout();
_grid.Refresh();
}
private void OpenReport()
{
using var win = new ReportWindow();
win.ShowDialog(this);
}
private void SetStatus(string text) => _lblStatus.Text = text;
private void UpdateTitleAndStatus()
{
int total = _visible.Count;
int lsp = _visible.Count(f => f.Extension == ".lsp");
int vlx = _visible.Count(f => f.Extension == ".vlx");
int fas = _visible.Count(f => f.Extension == ".fas");
int dll = _visible.Count(f => f.Extension == ".dll");
int loaded = _visible.Count(f => f.LastResult?.Status == LoadStatus.OK);
int failed = _visible.Count(f => f.LastResult?.Status == LoadStatus.Failed);
_lblTitle.Text =
"LispLoaderExtra " + _allFiles.Count + " files discovered" +
(_allFiles.Count != total ? " β " + total + " showing" : "");
string s = "Showing " + total +
" β LSP: " + lsp + " VLX: " + vlx +
" FAS: " + fas + " DLL: " + dll;
if (loaded > 0 || failed > 0)
s += " β β " + loaded + " β " + failed;
SetStatus(s);
}
// ββ Widget factories ββββββββββββββββββββββββββββββββββββββββββββββββββ
private void AddCol(string hdr, int w, bool fill, string align, string tip = "")
{
_grid.Columns.Add(new DataGridViewTextBoxColumn {
HeaderText = hdr,
Width = w,
ReadOnly = true,
AutoSizeMode = fill
? DataGridViewAutoSizeColumnMode.Fill
: DataGridViewAutoSizeColumnMode.None,
SortMode = DataGridViewColumnSortMode.Automatic,
Resizable = DataGridViewTriState.True,
ToolTipText = tip,
DefaultCellStyle = {
Alignment = align == "center"
? DataGridViewContentAlignment.MiddleCenter
: DataGridViewContentAlignment.MiddleLeft,
},
});
}
private static Label Lbl(string text, int x, int y, int w) =>
new Label {
Text = text,
Location = new Point(x, y),
Size = new Size(w, 20),
ForeColor = ColSub,
BackColor = Color.Transparent,
Font = new Font("Segoe UI", 9f),
TextAlign = ContentAlignment.MiddleLeft,
};
private static ComboBox Combo(int x, int y, int w) =>
new ComboBox {
Location = new Point(x, y),
Size = new Size(w, 26),
DropDownStyle = ComboBoxStyle.DropDownList,
BackColor = Color.FromArgb(48, 48, 62),
ForeColor = ColText,
Font = new Font("Segoe UI", 9f),
FlatStyle = FlatStyle.Flat,
};
private static Button Btn(string text, Color border, int w, int x, Panel tb)
{
var b = new Button {
Text = text,
Location = new Point(x, 6),
Size = new Size(w, 30),
FlatStyle = FlatStyle.Flat,
BackColor = Color.FromArgb(
Math.Max(0, border.R / 3),
Math.Max(0, border.G / 3),
Math.Max(0, border.B / 3)),
ForeColor = Color.FromArgb(220, 220, 232),
Font = new Font("Segoe UI", 8.5f),
Cursor = Cursors.Hand,
FlatAppearance = { BorderColor = border, BorderSize = 1 },
};
if (x > 0) tb.Controls.Add(b);
return b;
}
}
}
// ===== FILE: ReportWindow.cs =====
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LispLoaderExtra
{
/// <summary>
/// WinForms report window β shows the full load session results.
/// Open anytime by typing LISPLOADREPORT in AutoCAD.
/// </summary>
internal sealed class ReportWindow : Form
{
// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static readonly Color BgDark = Color.FromArgb(30, 30, 30);
private static readonly Color BgPanel = Color.FromArgb(40, 40, 40);
private static readonly Color BgGrid = Color.FromArgb(25, 25, 25);
private static readonly Color ColOk = Color.FromArgb(60, 180, 90);
private static readonly Color ColFail = Color.FromArgb(220, 60, 60);
private static readonly Color ColMiss = Color.FromArgb(220, 150, 40);
private static readonly Color ColSkip = Color.FromArgb(120, 120, 120);
private static readonly Color ColText = Color.FromArgb(220, 220, 220);
private static readonly Color ColSub = Color.FromArgb(150, 150, 150);
private static readonly Color ColAccent = Color.FromArgb(80, 140, 210);
// ββ Controls ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private DataGridView _grid = null!;
private Label _lblSummary = null!;
private Label _lblStats = null!;
private TextBox _txtError = null!;
private Button _btnAll = null!;
private Button _btnErrors = null!;
private Button _btnOk = null!;
private Button _btnCopy = null!;
private Button _btnLog = null!;
private Button _btnReload = null!;
private Label _lblVersion = null!;
private List<LoadResult> _current = new List<LoadResult>();
// ββ Constructor βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal ReportWindow()
{
BuildLayout();
Refresh_All();
}
// ββ Layout ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void BuildLayout()
{
Text = "LispLoaderExtra β Load Report";
Size = new Size(1050, 680);
MinimumSize = new Size(800, 500);
StartPosition = FormStartPosition.CenterScreen;
BackColor = BgDark;
ForeColor = ColText;
Font = new Font("Segoe UI", 9f);
FormBorderStyle = FormBorderStyle.Sizable;
// ββ Top bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var top = new Panel {
Dock = DockStyle.Top, Height = 90, BackColor = BgPanel,
Padding = new Padding(12, 8, 12, 8)
};
_lblSummary = MakeLabel("", 13f, bold: true);
_lblSummary.Location = new Point(12, 10);
_lblSummary.Size = new Size(700, 24);
_lblSummary.ForeColor = ColAccent;
_lblStats = MakeLabel("", 9f);
_lblStats.Location = new Point(12, 36);
_lblStats.Size = new Size(900, 18);
_lblStats.ForeColor = ColSub;
_lblVersion = MakeLabel("", 8.5f);
_lblVersion.Location = new Point(12, 56);
_lblVersion.Size = new Size(900, 16);
_lblVersion.ForeColor = ColSub;
top.Controls.AddRange(new Control[] { _lblSummary, _lblStats, _lblVersion });
// ββ Toolbar βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var toolbar = new Panel {
Dock = DockStyle.Top, Height = 38, BackColor = BgPanel,
Padding = new Padding(8, 4, 8, 4)
};
_btnAll = MakeFilterBtn("All", 0);
_btnErrors = MakeFilterBtn("Errors", 1);
_btnOk = MakeFilterBtn("OK only", 2);
_btnCopy = MakeActionBtn("π Copy Report", ColAccent);
_btnLog = MakeActionBtn("π Open Log", Color.FromArgb(70,70,90));
_btnReload = MakeActionBtn("π Reload", Color.FromArgb(60,90,60));
_btnCopy.Click += (s,e) => CopyReport();
_btnLog.Click += (s,e) => OpenLog();
_btnReload.Click += (s,e) => Refresh_All();
_btnAll.Click += (s,e) => ApplyFilter(null);
_btnErrors.Click += (s,e) => ApplyFilter(LoadStatus.Failed);
_btnOk.Click += (s,e) => ApplyFilter(LoadStatus.OK);
// Layout buttons left-to-right
int x = 8;
foreach (var b in new[] { _btnAll, _btnErrors, _btnOk })
{ b.Location = new Point(x, 5); x += b.Width + 4; }
x += 20;
foreach (var b in new[] { _btnCopy, _btnLog, _btnReload })
{ b.Location = new Point(x, 5); x += b.Width + 4; }
toolbar.Controls.AddRange(new Control[] {
_btnAll, _btnErrors, _btnOk, _btnCopy, _btnLog, _btnReload });
// ββ Grid ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_grid = new DataGridView {
Dock = DockStyle.Fill,
BackgroundColor = BgGrid,
BorderStyle = BorderStyle.None,
RowHeadersVisible = false,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
ReadOnly = true,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
MultiSelect = false,
AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None,
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing,
ColumnHeadersHeight = 28,
RowTemplate = { Height = 22 },
GridColor = Color.FromArgb(50, 50, 50),
DefaultCellStyle = {
BackColor = BgGrid,
ForeColor = ColText,
SelectionBackColor = Color.FromArgb(55, 95, 145),
SelectionForeColor = Color.White,
Font = new Font("Consolas", 8.5f),
Padding = new Padding(3, 0, 3, 0),
},
ColumnHeadersDefaultCellStyle = {
BackColor = Color.FromArgb(50, 50, 60),
ForeColor = ColSub,
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
SelectionBackColor = Color.FromArgb(50,50,60),
},
EnableHeadersVisualStyles = false,
};
AddCol("Status", 60, false);
AddCol("Type", 42, false);
AddCol("File name", 220, true);
AddCol("ms", 48, false);
AddCol("Full path", 480, true);
_grid.SelectionChanged += GridSelectionChanged;
_grid.CellFormatting += GridCellFormatting;
// ββ Error detail panel ββββββββββββββββββββββββββββββββββββββββββββ
var errPanel = new Panel {
Dock = DockStyle.Bottom,
Height = 90,
BackColor = Color.FromArgb(35, 20, 20),
Padding = new Padding(8, 4, 8, 4),
};
var errLabel = MakeLabel("Error detail:", 8.5f, bold: true);
errLabel.ForeColor = ColFail;
errLabel.Location = new Point(8, 4);
errLabel.Size = new Size(100, 16);
_txtError = new TextBox {
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
BackColor = Color.FromArgb(35, 20, 20),
ForeColor = Color.FromArgb(240, 100, 100),
Font = new Font("Consolas", 8.5f),
BorderStyle = BorderStyle.None,
Location = new Point(8, 22),
Size = new Size(1020, 60),
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom,
Text = "(select a row to see error detail)",
};
errPanel.Controls.AddRange(new Control[] { errLabel, _txtError });
// ββ Splitter + content area ββββββββββββββββββββββββββββββββββββββββ
var split = new SplitContainer {
Dock = DockStyle.Fill,
Orientation = Orientation.Horizontal,
Panel1MinSize = 200,
Panel2MinSize = 60,
SplitterDistance = 430,
BackColor = BgDark,
};
split.Panel1.Controls.Add(_grid);
split.Panel2.Controls.Add(_txtError);
split.Panel2.Controls.Add(errLabel);
Controls.Add(split);
Controls.Add(toolbar);
Controls.Add(top);
}
// ββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void Refresh_All()
{
_current = LoadSession.Results.ToList();
PopulateGrid(_current);
UpdateSummary();
}
private void ApplyFilter(LoadStatus? status)
{
_current = status.HasValue
? LoadSession.Results.Where(r => r.Status == status.Value).ToList()
: LoadSession.Results.ToList();
PopulateGrid(_current);
// Highlight active filter button
foreach (var b in new[] { _btnAll, _btnErrors, _btnOk })
b.FlatAppearance.BorderColor = BgPanel;
var active = status switch {
LoadStatus.Failed => _btnErrors,
LoadStatus.OK => _btnOk,
_ => _btnAll,
};
active.FlatAppearance.BorderColor = ColAccent;
}
private void PopulateGrid(List<LoadResult> items)
{
_grid.Rows.Clear();
foreach (var r in items)
{
int i = _grid.Rows.Add(
StatusLabel(r.Status),
r.TypeLabel,
r.FileName,
r.Duration.TotalMilliseconds.ToString("0"),
r.Path);
_grid.Rows[i].Tag = r;
}
}
private void UpdateSummary()
{
int total = LoadSession.TotalCount;
int ok = LoadSession.OkCount;
int fail = LoadSession.FailCount;
int miss = LoadSession.NotFoundCount;
_lblSummary.Text =
$"Loaded {ok} / {total} files " +
(fail > 0 ? $"β {fail} error(s) " : "") +
(miss > 0 ? $"β {miss} not found " : "");
_lblStats.Text =
$"LSP: {LoadSession.CountByType(".lsp")} " +
$"VLX: {LoadSession.CountByType(".vlx")} " +
$"FAS: {LoadSession.CountByType(".fas")} " +
$"DLL: {LoadSession.CountByType(".dll")} " +
$"β Total time: {LoadSession.TotalDuration.TotalSeconds:0.00}s " +
$"β Log: {LogManager.LogFolder}";
_lblVersion.Text =
$"Config: {ConfigReader.ConfigFile}";
}
// ββ Grid events βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void GridSelectionChanged(object? sender, EventArgs e)
{
if (_grid.SelectedRows.Count == 0) { _txtError.Text = ""; return; }
var r = _grid.SelectedRows[0].Tag as LoadResult;
if (r == null) { _txtError.Text = ""; return; }
_txtError.Text = r.Status switch {
LoadStatus.OK => $"OK β {r.Path}\r\nLoaded in {r.Duration.TotalMilliseconds:0} ms",
LoadStatus.Failed => $"FAILED β {r.Path}\r\n\r\n{r.ErrorMessage}",
LoadStatus.NotFound => $"NOT FOUND β {r.Path}\r\n\r\nCheck that the path in LoaderConfig.txt is correct.",
LoadStatus.Skipped => $"SKIPPED β {r.Path}\r\n\r\n{r.ErrorMessage}",
_ => "",
};
}
private void GridCellFormatting(object? sender, DataGridViewCellFormattingEventArgs e)
{
if (e.RowIndex < 0 || e.RowIndex >= _grid.Rows.Count) return;
var r = _grid.Rows[e.RowIndex].Tag as LoadResult;
if (r == null) return;
Color fg = r.Status switch {
LoadStatus.OK => ColOk,
LoadStatus.Failed => ColFail,
LoadStatus.NotFound => ColMiss,
LoadStatus.Skipped => ColSkip,
_ => ColText,
};
// Colour the Status column strongly, dim the rest slightly
if (e.ColumnIndex == 0)
e.CellStyle.ForeColor = fg;
else if (r.Status != LoadStatus.OK)
e.CellStyle.ForeColor = Color.FromArgb(
(fg.R + ColText.R) / 2,
(fg.G + ColText.G) / 2,
(fg.B + ColText.B) / 2);
}
// ββ Actions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void CopyReport()
{
try
{
Clipboard.SetText(LoadSession.BuildTextReport());
MessageBox.Show("Report copied to clipboard.", "LispLoaderExtra",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch { }
}
private void OpenLog()
{
string log = Path.Combine(LogManager.LogFolder, "LispLoaderExtra.log");
if (File.Exists(log))
Process.Start("notepad.exe", log);
else
MessageBox.Show($"Log file not found:\n{log}", "LispLoaderExtra",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void AddCol(string header, int width, bool fill)
{
var c = new DataGridViewTextBoxColumn {
HeaderText = header,
Width = width,
AutoSizeMode = fill
? DataGridViewAutoSizeColumnMode.Fill
: DataGridViewAutoSizeColumnMode.None,
SortMode = DataGridViewColumnSortMode.Automatic,
Resizable = DataGridViewTriState.True,
};
_grid.Columns.Add(c);
}
private static Label MakeLabel(string text, float size, bool bold = false) =>
new Label {
Text = text,
Font = new Font("Segoe UI", size,
bold ? FontStyle.Bold : FontStyle.Regular),
ForeColor = ColText,
BackColor = Color.Transparent,
AutoSize = false,
};
private static Button MakeFilterBtn(string text, int _) =>
new Button {
Text = text,
Size = new Size(72, 28),
FlatStyle = FlatStyle.Flat,
BackColor = Color.FromArgb(50, 50, 60),
ForeColor = ColText,
Font = new Font("Segoe UI", 8.5f),
Cursor = Cursors.Hand,
FlatAppearance = { BorderColor = Color.FromArgb(70,70,80), BorderSize = 1 },
};
private static Button MakeActionBtn(string text, Color bg) =>
new Button {
Text = text,
Size = new Size(110, 28),
FlatStyle = FlatStyle.Flat,
BackColor = Color.FromArgb(bg.R / 2, bg.G / 2, bg.B / 2),
ForeColor = ColText,
Font = new Font("Segoe UI", 8.5f),
Cursor = Cursors.Hand,
FlatAppearance = { BorderColor = bg, BorderSize = 1 },
};
private static string StatusLabel(LoadStatus s) => s switch {
LoadStatus.OK => "OK",
LoadStatus.Failed => "FAIL",
LoadStatus.NotFound => "MISSING",
LoadStatus.Skipped => "SKIP",
_ => "?",
};
}
}
// ===== FILE: ScannedFile.cs =====
using System;
namespace LispLoaderExtra
{
/// <summary>
/// One file discovered by the disk scanner.
/// This is the pre-load data model (before any load attempt).
/// </summary>
internal sealed class ScannedFile
{
internal int RowIndex { get; set; } // display #
internal string Category { get; } // "AutoCAD" | "Civil 3D"
internal string MenuName { get; } // e.g. "3D_Menu", "Alignment_menu"
internal string FileName { get; } // e.g. "QuickMirror.lsp"
internal string Extension { get; } // ".lsp" | ".vlx" | ".fas" | ".dll"
internal string FullPath { get; }
internal bool Checked { get; set; } = true;
// Set after a load attempt
internal LoadResult? LastResult { get; set; }
internal string TypeLabel =>
Extension.TrimStart('.').ToUpperInvariant();
internal ScannedFile(
string category, string menuName,
string fileName, string extension, string fullPath)
{
Category = category;
MenuName = menuName;
FileName = fileName;
Extension = extension.ToLowerInvariant();
FullPath = fullPath;
}
}
}
// ===== FILE: LispLoaderExtra.cs =====
using System;
using System.IO;
using Autodesk.AutoCAD.Runtime;
[assembly: ExtensionApplication(typeof(LispLoaderExtra.LispLoaderExtraApp))]
namespace LispLoaderExtra
{
/// <summary>
/// Plugin entry point β loaded once via _.netload.
///
/// Initialize() does nothing except:
/// 1. Print the version banner to the AutoCAD command line + log
/// 2. Ensure the log folder exists
/// 3. Tell the user which commands are available
///
/// NO files are auto-loaded. The user opens the window and decides.
///
/// Commands registered by ReportCommand.cs:
/// LISPLOADEXTRA β opens the loader window (scan + checkbox + load)
/// LISPLOADREPORT β opens the report window (last session results)
/// </summary>
public class LispLoaderExtraApp : IExtensionApplication
{
private const string Source = "LispLoaderExtra";
public void Initialize()
{
try
{
// Ensure log folder exists so LogManager can write immediately
Directory.CreateDirectory(LogManager.LogFolder);
}
catch { }
// Version banner
VersionStamp.Print();
// Commands hint
LogManager.Info(Source, "Ready. Type:");
LogManager.Info(Source, " LISPLOADEXTRA β open loader window");
LogManager.Info(Source, " LISPLOADREPORT β open last-session report");
LogManager.Separator();
}
public void Terminate()
{
LogManager.Info(Source, "LispLoaderExtra unloaded.");
}
}
}
// ===== FILE: LogManager.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
namespace LispLoaderExtra
{
internal enum LogLevel { INFO, WARN, ERROR }
/// <summary>
/// Writes timestamped log lines to:
/// 1. The AutoCAD command line
/// 2. The persistent .log file on disk
/// 3. An in-memory ring-buffer (last 2000 lines) for the live log panel
///
/// Format: HH:mm:ss [LEVEL] [Source] message
/// Example: 05:29:43 [INFO ] [PvSplitApi] SplitDataLines.Add(β¦)
///
/// Subscribers (e.g. LoaderWindow log panel) register via LogManager.Subscribe().
/// Log file rotates to .bak.log at 5 MB.
/// </summary>
internal static class LogManager
{
// ββ Paths βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal static readonly string LogFolder =
@"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File\Lisp_menu\LispLoaderExtra\ss";
private static readonly string LogFile =
Path.Combine(LogFolder, "LispLoaderExtra.log");
private const long MaxLogBytes = 5 * 1024 * 1024; // 5 MB
// ββ In-memory ring-buffer (for live log panel) βββββββββββββββββββββ
private const int RingSize = 2000;
private static readonly Queue<string> _ring = new Queue<string>(RingSize);
private static readonly object _ringLock = new object();
// ββ Subscriber callbacks βββββββββββββββββββββββββββββββββββββββββββ
// The log panel in LoaderWindow subscribes here.
private static event Action<string>? _lineAdded;
internal static void Subscribe(Action<string> handler)
{
lock (_ringLock)
_lineAdded += handler;
}
internal static void Unsubscribe(Action<string> handler)
{
lock (_ringLock)
_lineAdded -= handler;
}
/// <summary>Returns a snapshot of the in-memory log (oldest first).</summary>
internal static string[] GetRingSnapshot()
{
lock (_ringLock)
return _ring.ToArray();
}
// ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
internal static void Info (string source, string message) => Write(LogLevel.INFO, source, message);
internal static void Warn (string source, string message) => Write(LogLevel.WARN, source, message);
internal static void Error(string source, string message) => Write(LogLevel.ERROR, source, message);
internal static void Separator() => Write(null, null, new string('β', 72));
// ββ Core ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private static void Write(LogLevel? level, string? source, string message)
{
string line = Format(level, source, message);
// 1. AutoCAD command line
try
{
Document? doc = Application.DocumentManager?.MdiActiveDocument;
doc?.Editor?.WriteMessage("\n" + line);
}
catch { }
// 2. Disk β ensure folder exists first
try
{
Directory.CreateDirectory(LogFolder);
RotateIfNeeded();
File.AppendAllText(LogFile, line + Environment.NewLine);
}
catch { }
// 3. In-memory ring + subscriber notification
Action<string>? notify = null;
lock (_ringLock)
{
if (_ring.Count >= RingSize) _ring.Dequeue();
_ring.Enqueue(line);
notify = _lineAdded;
}
// Fire subscribers outside the lock so they can call back into LogManager
try { notify?.Invoke(line); }
catch { }
}
internal static string Format(LogLevel? level, string? source, string message)
{
string ts = DateTime.Now.ToString("HH:mm:ss");
string lvl = level.HasValue ? $"[{level.Value,-5}]" : " ";
string src = source != null ? $"[{source}]" : string.Empty;
return $"{ts} {lvl} {src,-22} {message}";
}
private static void RotateIfNeeded()
{
if (!File.Exists(LogFile)) return;
if (new FileInfo(LogFile).Length < MaxLogBytes) return;
string bak = Path.ChangeExtension(LogFile, ".bak.log");
if (File.Exists(bak)) File.Delete(bak);
File.Move(LogFile, bak);
}
}
}
// ===== FILE: ReportCommand.cs =====
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using Autodesk.AutoCAD.Runtime;
namespace LispLoaderExtra
{
/// <summary>
/// AutoCAD commands:
/// LISPLOADEXTRA β open the loader window (modeless: stays open while you work)
/// LISPLOADREPORT β open the report window (last session)
/// LISPLOADDIAG β dump acmgd/accoremgd type names to log (debug)
/// </summary>
public static class ReportCommand
{
// Keep a single modeless instance alive so AutoCAD can continue working
// while the form is open. ShowDialog() blocks the ACAD command thread;
// Show() does not β but it requires a message pump, which the Session
// CommandFlags flag provides.
private static LoaderWindow? _loaderInstance;
[CommandMethod("LISPLOADEXTRA", CommandFlags.Session)]
public static void ShowLoader()
{
System.Windows.Forms.Application.EnableVisualStyles();
// If already open, bring it to front instead of opening a second copy
if (_loaderInstance != null && !_loaderInstance.IsDisposed)
{
_loaderInstance.BringToFront();
_loaderInstance.Activate();
return;
}
_loaderInstance = new LoaderWindow();
// Show() is modeless β AutoCAD keeps its message pump running,
// so the form stays responsive and files load immediately.
// We pass IntPtr.Zero so no parent window is set; the form floats
// above AutoCAD via TopMost=true on its own.
_loaderInstance.Show(new Win32Window(
Autodesk.AutoCAD.ApplicationServices.Application.MainWindow.Handle));
}
[CommandMethod("LISPLOADREPORT", CommandFlags.Session)]
public static void ShowReport()
{
System.Windows.Forms.Application.EnableVisualStyles();
using var win = new ReportWindow();
win.ShowDialog();
}
/// <summary>
/// Diagnostic β type LISPLOADDIAG once after installing on a new
/// AutoCAD version.
/// </summary>
[CommandMethod("LISPLOADDIAG", CommandFlags.Session)]
public static void Diagnose()
{
const string Src = "LISPLOADDIAG";
LogManager.Separator();
LogManager.Info(Src, "Scanning ALL loaded assemblies for LISP-related methods...");
int found = 0;
string[] keywords = { "acedInvoke", "acedEval", "RunLisp", "LispInvoke", "EvalLisp" };
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
string asmName = asm.GetName().Name ?? "";
Type[] types;
try { types = asm.GetTypes(); }
catch { continue; }
foreach (Type t in types)
{
MethodInfo[] methods;
try
{
methods = t.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance);
}
catch { continue; }
foreach (var m in methods)
{
bool match = false;
foreach (string kw in keywords)
if (m.Name.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0)
{ match = true; break; }
if (!match) continue;
string sig = string.Join(", ",
System.Linq.Enumerable.Select(
m.GetParameters(),
p => p.ParameterType.Name + " " + p.Name));
LogManager.Info(Src,
$"[{asmName}] {t.FullName}.{m.Name}({sig})");
found++;
}
}
}
try
{
using var rb = new Autodesk.AutoCAD.DatabaseServices.ResultBuffer(
new Autodesk.AutoCAD.DatabaseServices.TypedValue(5016),
new Autodesk.AutoCAD.DatabaseServices.TypedValue(5017));
IntPtr ptr = rb.UnmanagedObject;
LogManager.Info(Src, $"ResultBuffer.UnmanagedObject OK: 0x{ptr:X}");
}
catch (System.Exception ex)
{
LogManager.Warn(Src, "ResultBuffer.UnmanagedObject failed: " + ex.Message);
}
LogManager.Separator();
LogManager.Info(Src, $"Total LISP methods found: {found}");
LogManager.Separator();
MessageBox.Show(
$"Found {found} LISP-related method(s).\n" +
$"Check log:\n{System.IO.Path.Combine(LogManager.LogFolder, "LispLoaderExtra.log")}",
"LISPLOADDIAG",
MessageBoxButtons.OK,
found > 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
}
}
/// <summary>
/// Wraps an HWND so it can be passed to Form.Show(IWin32Window).
/// This makes the WinForms form a child of the AutoCAD main window
/// in the Windows Z-order, so it stays above AutoCAD without being
/// on top of every other application.
/// </summary>
internal sealed class Win32Window : IWin32Window
{
public Win32Window(IntPtr handle) { Handle = handle; }
public IntPtr Handle { get; }
}
}
