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; }
}
}
