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:
@@ -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;
|
||||
|
||||
@@ -25,15 +25,36 @@ This variant replaces the web map UI with an in-installer wizard page so the ope
|
||||
|
||||
## Status
|
||||
|
||||
Stub. `.iss` header renamed; map wizard page not yet implemented. Tracked work:
|
||||
Map wizard page implemented end-to-end in `.iss`. Untested: needs Windows + Inno Setup 6.x compiler, plus the `api_printers.asp` extension below to return map coords. ShopDB rows that already have `mapleft + maptop` set will populate hotspots; rows missing coords fall through to the checkbox page (legacy behaviour).
|
||||
|
||||
- [ ] Extend `api_printers.asp` to return `mapleft, maptop`.
|
||||
- [ ] Add `sitemap2025-dark.png` to `[Files]`.
|
||||
- [ ] Implement `MapPage` Pascal procedure (TScrollBox + TImage + click-to-toggle hotspots).
|
||||
- [ ] Wire selected printer IDs into the existing `SelectedPrinters` array used by the checkbox page.
|
||||
- [ ] Code-signing cert (separate task - see project memory).
|
||||
- [ ] Rebuild + sign + bake into PXE image.
|
||||
Tracked work:
|
||||
|
||||
- [x] Add `MapLeft, MapTop` fields to `PrinterDataArray` record + `JsonGetString` parsing.
|
||||
- [x] Add `sitemap2025-dark.bmp` (880x680, scaled-down, ~1.7 MB) to `[Files]`. Inno's `TBitmap.LoadFromFile` is BMP-only, so the source PNG was converted with PIL.
|
||||
- [x] Implement `BuildMapPage` (`CreateCustomPage` + stretched background `TBitmapImage` + per-printer hotspot `TBitmapImage` overlays) and `MapHotspotClick` (toggles `PrinterSelectionPage.Values[I]` + swaps the hotspot's bitmap).
|
||||
- [x] Anchor `PrinterSelectionPage` after `PrinterMapPage.ID` (was after `wpWelcome`); checkbox page sees the map's selections.
|
||||
- [x] `ShouldSkipPage` skips the map page when no printers have map coords (graceful degrade if API is not yet extended).
|
||||
- [ ] **ShopDB**: extend `api_printers.asp` to return `mapleft, maptop` per printer row (additive change).
|
||||
- [ ] **Build**: Inno Setup compile on Windows. Target: `OutputBaseFilename=PrinterInstallerMap`.
|
||||
- [ ] **Code-signing cert**: `proudlock.tech` M365 artifact certs are not Authenticode (wrong EKU); pursue GE AD CS cert instead. See project memory note.
|
||||
- [ ] **Sign + bake into PXE image**.
|
||||
- [ ] **Test on win11 VM**: cold install, /PRINTER= silent path, map-only-vs-checkbox-only printers, no-coords graceful skip.
|
||||
|
||||
## Privileges
|
||||
|
||||
`PrivilegesRequired=admin` (inherited from PrinterInstaller). pnputil, TCP/IP port creation, `Add-Printer`, and trusted-publisher cert install all need elevation. Shopfloor user can launch but Windows shows a UAC prompt; supportuser creds satisfy it. Acceptable for the operator-driven adhoc workflow this installer targets. For unattended bulk per-PC deployment, use a GE-Enforce manifest entry (runs as SYSTEM, no UI, no UAC) instead of this installer.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- **Map source**: `printers.mapleft, maptop` are top-left-origin pixel coords on a 3300x2550 site map. Inno is also top-left-origin, so coords are used directly with no Y-flip (the Leaflet asp inverts Y because Leaflet `CRS.Simple` is Y-up, which is a Leaflet quirk - not present here).
|
||||
- **Scaling**: bundled BMP is 880x680 to keep file size sane. At wizard time the BMP is stretch-painted into the wizard surface at fit-and-letterbox scale; hotspots are positioned in the same scaled coordinate space (multiplying `mapleft, maptop` by the same scale factor).
|
||||
- **Hotspot graphic**: 14x14 square (no transparency, since Inno's exposed TBitmap surface does not document a TransparentColor path). Square reads cleanly enough on the dark map.
|
||||
- **Inno class subset gotchas verified before coding**:
|
||||
- `TBitmapImage` has `OnClick(Sender)` only - no `OnMouseDown` with X/Y. Per-hotspot child controls are the documented workaround for click-to-coords.
|
||||
- `TScrollBox` is not exposed; the bundled BMP is stretched to fit instead of scrolled.
|
||||
- `TBitmap.LoadFromFile` is BMP-only (PNG path goes through `TPngImage` which has no `LoadFromFile`); PNG was pre-converted.
|
||||
- `TWizardPage.OnActivate`, `ShouldSkipPage`, `NextButtonClick` all exposed as expected.
|
||||
|
||||
## Build / sign / deploy
|
||||
|
||||
Same as `PrinterInstaller/`. See `../PrinterInstaller/README.md` for the build matrix.
|
||||
Same as `PrinterInstaller/`. See `../PrinterInstaller/README.md` for the build matrix. Output filename is `PrinterInstallerMap.exe`.
|
||||
|
||||
BIN
PrinterInstallerMap/sitemap2025-dark.bmp
Normal file
BIN
PrinterInstallerMap/sitemap2025-dark.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Reference in New Issue
Block a user