PrinterInstallerMap: add map wizard page (TBitmapImage hotspots)

Implements the click-to-select site map UI on a custom Inno wizard page,
replacing the blocked Edge .bat-download flow.

- New record fields MapLeft, MapTop on PrinterDataArray; QueryPrinters
  pulls them from the api_printers.asp JSON via the existing
  JsonGetString helper.
- BuildMapPage creates a custom wizard page (CreateCustomPage), places a
  stretched background TBitmapImage with the bundled site map BMP, then
  overlays one TBitmapImage per printer with a non-empty mapleft+maptop.
  Each hotspot is a 14x14 square with its own OnClick.
- MapHotspotClick toggles PrinterSelectionPage.Values[I] for the linked
  printer and swaps the hotspot bitmap (gray<->lime) to reflect the new
  state. Two shared TBitmaps are pre-rendered once via TCanvas.
- PrinterSelectionPage is re-anchored to PrinterMapPage.ID so the
  selections from the map flow into the existing checkbox page where the
  user can review and adjust.
- ShouldSkipPage skips the map page when no printers have map coords,
  preserving legacy behaviour if api_printers.asp is not yet extended or
  if the database has no coordinates set.
- Bundled sitemap2025-dark.bmp (880x680, ~1.7 MB) was scaled down from
  shopdb's sitemap2025-dark.png via PIL (Inno's TBitmap.LoadFromFile is
  BMP-only).

Untested. Needs Inno Setup 6.x compile on Windows + the api_printers.asp
extension to return mapleft, maptop. README documents the privileges
model (admin required, supportuser at UAC) and the Inno class-subset
gotchas verified during design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-30 07:43:54 -04:00
parent ce0abee262
commit 1d044cbbdf
3 changed files with 211 additions and 10 deletions

View File

@@ -53,16 +53,43 @@ Source: "drivers\xerox_x32\*"; DestDir: "{tmp}\xerox_drivers_x32"; Flags: ignore
; Brother MFC Printer Driver (multi-architecture package)
Source: "drivers\brother\*"; DestDir: "{tmp}\brother_drivers"; Flags: ignoreversion recursesubdirs
; Site map for the map-UI wizard page (scaled-down BMP of shopdb's
; sitemap2025-dark.png; original is 3300x2550, this copy is 880x680
; to stay below ~2 MB. Inno's TBitmap.LoadFromFile is BMP-only.)
Source: "sitemap2025-dark.bmp"; DestDir: "{tmp}"; Flags: ignoreversion
[Code]
const
; Source map dimensions in ShopDB pixel-coord space. printers.mapleft
; ranges 0..MAP_SOURCE_W; printers.maptop ranges 0..MAP_SOURCE_H. Both
; are top-left origin in the database (the Leaflet asp page does its
; own Y flip because Leaflet CRS.Simple is Y-up; we are top-down so
; we use them directly).
MAP_SOURCE_W = 3300;
MAP_SOURCE_H = 2550;
HOTSPOT_SIZE = 14;
var
PrinterMapPage: TWizardPage;
PrinterSelectionPage: TInputOptionWizardPage;
PrinterDataArray: array of record
PrinterName: String;
FQDN: String;
Vendor: String;
Model: String;
MapLeft: String;
MapTop: String;
IsInstalled: Boolean;
end;
; Per-hotspot state. Indices are aligned: MapHotspots[i] is the visible
; control, MapHotspotIndex[i] is the corresponding PrinterDataArray index.
; Only printers with non-empty MapLeft+MapTop get a hotspot.
MapHotspots: array of TBitmapImage;
MapHotspotIndex: array of Integer;
; Two pre-rendered dot bitmaps, shared by all hotspots (Bitmap.Assign'd
; into each hotspot's Bitmap on toggle).
DotUnselectedBmp: TBitmap;
DotSelectedBmp: TBitmap;
InstallLog: TStringList;
// ─── Logging ────────────────────────────────────────────────────────────────
@@ -219,6 +246,8 @@ begin
PrinterDataArray[Count].FQDN := Address;
PrinterDataArray[Count].Vendor := Vendor;
PrinterDataArray[Count].Model := Model;
PrinterDataArray[Count].MapLeft := JsonGetString(ObjStr, 'mapleft');
PrinterDataArray[Count].MapTop := JsonGetString(ObjStr, 'maptop');
PrinterDataArray[Count].IsInstalled := False;
Count := Count + 1;
Result := True;
@@ -534,6 +563,141 @@ begin
end;
end;
// ─── Map page: dot bitmaps + hotspot click + page builder ───────────────────
// Pre-render two HOTSPOT_SIZE-square BMPs (gray = unselected, lime = selected)
// shared by every hotspot. On toggle we Bitmap.Assign one of them into the
// hotspot's TBitmapImage.Bitmap and call Invalidate. Drawing a square (not
// a circle) sidesteps the lack of a documented transparent-color path on
// Inno's exposed TBitmap; reads cleanly enough on the dark site map.
procedure BuildDotBitmaps();
var
R: TRect;
begin
R.Left := 0; R.Top := 0; R.Right := HOTSPOT_SIZE; R.Bottom := HOTSPOT_SIZE;
DotUnselectedBmp := TBitmap.Create;
DotUnselectedBmp.Width := HOTSPOT_SIZE;
DotUnselectedBmp.Height := HOTSPOT_SIZE;
DotUnselectedBmp.Canvas.Brush.Color := $00777777; // mid-gray (BGR)
DotUnselectedBmp.Canvas.FillRect(R);
DotUnselectedBmp.Canvas.Pen.Color := clBlack;
DotUnselectedBmp.Canvas.MoveTo(0, 0);
DotUnselectedBmp.Canvas.LineTo(HOTSPOT_SIZE - 1, 0);
DotUnselectedBmp.Canvas.LineTo(HOTSPOT_SIZE - 1, HOTSPOT_SIZE - 1);
DotUnselectedBmp.Canvas.LineTo(0, HOTSPOT_SIZE - 1);
DotUnselectedBmp.Canvas.LineTo(0, 0);
DotSelectedBmp := TBitmap.Create;
DotSelectedBmp.Width := HOTSPOT_SIZE;
DotSelectedBmp.Height := HOTSPOT_SIZE;
DotSelectedBmp.Canvas.Brush.Color := clLime;
DotSelectedBmp.Canvas.FillRect(R);
DotSelectedBmp.Canvas.Pen.Color := clBlack;
DotSelectedBmp.Canvas.MoveTo(0, 0);
DotSelectedBmp.Canvas.LineTo(HOTSPOT_SIZE - 1, 0);
DotSelectedBmp.Canvas.LineTo(HOTSPOT_SIZE - 1, HOTSPOT_SIZE - 1);
DotSelectedBmp.Canvas.LineTo(0, HOTSPOT_SIZE - 1);
DotSelectedBmp.Canvas.LineTo(0, 0);
end;
// Toggle the linked checkbox-page Value, swap the hotspot's bitmap to match
// the new state. Sender is the clicked TBitmapImage; its Tag indexes
// MapHotspotIndex which maps to the underlying PrinterDataArray entry.
procedure MapHotspotClick(Sender: TObject);
var
Hotspot: TBitmapImage;
HotIdx, PrnIdx: Integer;
begin
Hotspot := TBitmapImage(Sender);
HotIdx := Hotspot.Tag;
if (HotIdx < 0) or (HotIdx >= GetArrayLength(MapHotspotIndex)) then Exit;
PrnIdx := MapHotspotIndex[HotIdx];
PrinterSelectionPage.Values[PrnIdx] := not PrinterSelectionPage.Values[PrnIdx];
if PrinterSelectionPage.Values[PrnIdx] then
Hotspot.Bitmap.Assign(DotSelectedBmp)
else
Hotspot.Bitmap.Assign(DotUnselectedBmp);
Hotspot.Invalidate;
end;
// Place the site-map background and one hotspot per printer with non-empty
// mapleft+maptop. Called once from InitializeWizard AFTER PrinterMapPage and
// PrinterSelectionPage exist and Values[] are seeded, so initial hotspot
// colors reflect /PRINTER= pre-selection or already-installed state.
procedure BuildMapPage();
var
I, ML, MT, X, Y, ScaledW, ScaledH, OffsetX: Integer;
ScaleX, ScaleY, Scale: Extended;
SurfW, SurfH: Integer;
MapBg, Hotspot: TBitmapImage;
begin
if PrinterMapPage = nil then Exit;
SurfW := PrinterMapPage.SurfaceWidth;
SurfH := PrinterMapPage.SurfaceHeight;
ScaleX := SurfW / MAP_SOURCE_W;
ScaleY := SurfH / MAP_SOURCE_H;
if ScaleX < ScaleY then Scale := ScaleX else Scale := ScaleY;
ScaledW := Round(MAP_SOURCE_W * Scale);
ScaledH := Round(MAP_SOURCE_H * Scale);
OffsetX := (SurfW - ScaledW) div 2;
MapBg := TBitmapImage.Create(PrinterMapPage);
MapBg.Parent := PrinterMapPage.Surface;
MapBg.Left := OffsetX;
MapBg.Top := 0;
MapBg.Width := ScaledW;
MapBg.Height := ScaledH;
MapBg.Stretch := True;
MapBg.AutoSize := False;
try
MapBg.Bitmap.LoadFromFile(ExpandConstant('{tmp}\sitemap2025-dark.bmp'));
except
AddLog('Map page: failed to load site map BMP from {tmp}');
end;
BuildDotBitmaps();
SetArrayLength(MapHotspots, 0);
SetArrayLength(MapHotspotIndex, 0);
for I := 0 to GetArrayLength(PrinterDataArray) - 1 do
begin
if (PrinterDataArray[I].MapLeft = '') or (PrinterDataArray[I].MapTop = '') then
Continue;
try
ML := StrToInt(PrinterDataArray[I].MapLeft);
MT := StrToInt(PrinterDataArray[I].MapTop);
except
Continue;
end;
X := OffsetX + Round(ML * Scale) - (HOTSPOT_SIZE div 2);
Y := Round(MT * Scale) - (HOTSPOT_SIZE div 2);
Hotspot := TBitmapImage.Create(PrinterMapPage);
Hotspot.Parent := PrinterMapPage.Surface;
Hotspot.Left := X;
Hotspot.Top := Y;
Hotspot.Width := HOTSPOT_SIZE;
Hotspot.Height := HOTSPOT_SIZE;
Hotspot.Stretch := True;
Hotspot.AutoSize := False;
Hotspot.Tag := GetArrayLength(MapHotspots);
Hotspot.OnClick := @MapHotspotClick;
if PrinterSelectionPage.Values[I] then
Hotspot.Bitmap.Assign(DotSelectedBmp)
else
Hotspot.Bitmap.Assign(DotUnselectedBmp);
SetArrayLength(MapHotspots, GetArrayLength(MapHotspots) + 1);
MapHotspots[GetArrayLength(MapHotspots) - 1] := Hotspot;
SetArrayLength(MapHotspotIndex, GetArrayLength(MapHotspotIndex) + 1);
MapHotspotIndex[GetArrayLength(MapHotspotIndex) - 1] := I;
end;
end;
// ─── Wizard initialisation ───────────────────────────────────────────────────
procedure InitializeWizard();
@@ -572,11 +736,16 @@ begin
AutoSelectPrinter := ExpandConstant('{param:PRINTER|}');
FoundMatch := False;
PrinterSelectionPage := CreateInputOptionPage(wpWelcome,
PrinterMapPage := CreateCustomPage(wpWelcome,
'Select Printers on Site Map',
'Click a printer marker to select or deselect it. Green = selected. ' +
'Click Next to review the selection list.');
PrinterSelectionPage := CreateInputOptionPage(PrinterMapPage.ID,
'Manage Network Printers',
'Select printers to install or uncheck to remove',
'Checked printers will be installed. Unchecked printers that are currently installed will be removed. ' +
'Already-installed printers are pre-checked.',
'Already-installed printers are pre-checked. Selections from the site map are reflected here.',
False, False);
for I := 0 to GetArrayLength(PrinterDataArray) - 1 do
@@ -626,6 +795,11 @@ begin
MsgBox('Printer "' + AutoSelectPrinter + '" not found in the database.' + #13#10#13#10 +
'Please select a printer from the list manually.',
mbInformation, MB_OK);
// Build the map page background + hotspots now that PrinterSelectionPage
// exists and Values[] are seeded. Hotspots only spawn for printers with
// non-empty mapleft+maptop; others remain reachable via the checkbox page.
BuildMapPage();
end;
// ─── Page validation ─────────────────────────────────────────────────────────
@@ -1374,4 +1548,10 @@ begin
Result := False;
if PageID = wpSelectDir then
Result := True;
// Skip the map page if no printers have map coords (e.g. all rows have
// null mapleft/maptop, or the API was not yet extended to return them).
// The checkbox page still works in that case, matching legacy behaviour.
if (PrinterMapPage <> nil) and (PageID = PrinterMapPage.ID) and
(GetArrayLength(MapHotspots) = 0) then
Result := True;
end;