M14: "Edit with PdfCraft" on the File Explorer menu of PDF files (#682) (#1180)

* M14: "Edit with PdfCraft" on the File Explorer menu of PDF files (#682)

The Windows MSI adds a classic shell verb under
HKLM\Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit that
runs "pdfcraft.exe" "%1" with the app's icon. It appears whichever app is
the default for .pdf and never changes that default. On Windows 11 it is
under "Show more options": a first-level entry needs an IExplorerCommand
handler and package identity, which an MSI doesn't have.

MultiSelectModel is Document: several selected PDFs (up to 15) start one
launch each, and the existing single-instance hand-off opens them as tabs
of one window.

The verb has its own component, conditioned on a new secure public
property INSTALLPDFCONTEXTMENU (default 1, a welcome-dialog checkbox,
INSTALLPDFCONTEXTMENU=0 for unattended installs), following
INSTALLDESKTOPSHORTCUT. Its menu value is the key path, so uninstall
removes every value. The portable zip still registers nothing.

test-msi.ps1 checks the compiled Registry, Component, Property and
CheckBox rows, and that the .pdf default is never written. Docs: README,
docs/releasing.md, parity notes, ROADMAP log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Leave ROADMAP.md to main (its log moved on); the change is described in the PR

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 18:35:02 -04:00
1 parent 0ef75d294c
commit f61a1e5465
6 files changed
+79 -14

No files matched your search

+7 -3
View File
@@ -448,14 +448,18 @@ The portable zip runs from any folder, a USB stick included. Its `portable.txt`
logs and crash recovery in a `PdfCraftData` folder next to `pdfcraft.exe`, so nothing is written to
`%APPDATA%`; delete that file to use the normal per-user folders.
The MSI installs for all users and requires administrator privileges. For unattended deployment
without a desktop shortcut, run from an elevated terminal:
The MSI installs for all users and requires administrator privileges. It adds "Edit with PdfCraft"
to the right-click menu of PDF files in File Explorer (on Windows 11, under "Show more options"),
without changing which app opens PDFs by default. For unattended deployment without a desktop
shortcut, run from an elevated terminal:
```powershell
msiexec /i "pdfcraft-<ver>-windows-x64.msi" /qn /norestart INSTALLDESKTOPSHORTCUT=0
```
Use the MSI for your architecture. Per-user installation overrides are not supported.
Add `INSTALLPDFCONTEXTMENU=0` to leave out the right-click menu entry. Pass both on upgrades too,
since each install applies its own choices. Use the MSI for your architecture. Per-user
installation overrides are not supported.
### macOS
+12 -4
View File
@@ -119,10 +119,18 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable.
this.
- `installer-ui.wxs` supplies native welcome, maintenance, progress, files-in-use and outcome
dialogs. Full UI confirms success with Finish; failures and cancellations have distinct messages.
The welcome dialog has a "Create a desktop shortcut" checkbox, ticked by default. `/qn` and `/qb`
stay unattended and create the desktop shortcut unless `INSTALLDESKTOPSHORTCUT=0` is passed.
- `test-msi.ps1 <file.msi>` checks the compiled shortcut and dialog tables. Packaging runs it in a
child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install
The welcome dialog has a "Create a desktop shortcut" checkbox and an "Edit with PdfCraft"
right-click menu checkbox, both ticked by default. `/qn` and `/qb` stay unattended and create
both unless `INSTALLDESKTOPSHORTCUT=0` or `INSTALLPDFCONTEXTMENU=0` is passed.
- The PDF right-click entry (#682) is a classic verb under
`HKLM\Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit`, so it shows whichever app
is the default for `.pdf` and never changes that default. Several selected PDFs (up to 15, the
limit for a `Document` verb) start one `pdfcraft.exe "<file>"` each, and the single-instance
hand-off opens them as tabs. On Windows 11 it is under "Show more options"; a first-level entry
needs an `IExplorerCommand` handler and package identity (a sparse MSIX package), which the MSI
doesn't have.
- `test-msi.ps1 <file.msi>` checks the compiled shortcut, context-menu and dialog tables. Packaging
runs it in a child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install
smoke test checks that both all-users shortcuts point at the installed `pdfcraft.exe` and are
removed on uninstall, and that `INSTALLDESKTOPSHORTCUT=0` skips the desktop one.
- The portable zip holds both executables, the README, the licences, the OFL licence of each
+3 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Original, text-only native MSI dialogs. No third-party UI artwork or custom-action DLLs.
Full UI gets welcome (with the desktop-shortcut checkbox)/progress/outcome screens; /qn and /qb stay unattended. -->
Full UI gets welcome (with the desktop-shortcut and PDF context-menu checkboxes)/progress/outcome screens; /qn and /qb stay unattended. -->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<UI Id="PdfcraftInstallerUI">
@@ -14,6 +14,8 @@
Text="Setup will install [ProductName] for all users and add it to the Start menu. Click Install to continue." />
<Control Id="DesktopShortcut" Type="CheckBox" X="20" Y="105" Width="330" Height="17"
Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="Create a &amp;desktop shortcut" />
<Control Id="PdfContextMenu" Type="CheckBox" X="20" Y="123" Width="330" Height="17"
Property="INSTALLPDFCONTEXTMENU" CheckBoxValue="1" Text="Add &quot;&amp;Edit with PdfCraft&quot; to the right-click menu of PDF files" />
<Control Id="Install" Type="PushButton" X="195" Y="145" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&amp;Install">
<Publish Event="EndDialog" Value="Return" />
</Control>
+22 -3
View File
@@ -5,9 +5,10 @@
wix build pdfcraft.wxs installer-ui.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d ModelsDir=... -d IconPath=... -o out.msi
Per-machine install into Program Files, a Start Menu shortcut, a desktop shortcut (on by default,
optional), an App Paths entry, and PdfCraft registered as a PDF handler (offered under "Open with"
and in Default apps, never forced as the default). Files opened with PdfCraft arrive on its
command line.
optional), an App Paths entry, PdfCraft registered as a PDF handler (offered under "Open with"
and in Default apps, never forced as the default), and "Edit with PdfCraft" on PDF files'
right-click menu (on by default, optional). Files opened with PdfCraft arrive on its command
line.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
@@ -40,6 +41,9 @@
INSTALLDESKTOPSHORTCUT=0 on the msiexec command line for unattended installs. Secure, so the
choice made in the (unelevated) UI reaches the elevated install. -->
<Property Id="INSTALLDESKTOPSHORTCUT" Value="1" Secure="yes" />
<!-- "Edit with PdfCraft" on the right-click menu of PDF files in File Explorer (#682): on by
default, the same way, and INSTALLPDFCONTEXTMENU=0 for unattended installs without it. -->
<Property Id="INSTALLPDFCONTEXTMENU" Value="1" Secure="yes" />
<UIRef Id="PdfcraftInstallerUI" />
<StandardDirectory Id="ProgramFiles6432Folder">
@@ -136,6 +140,21 @@
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Shortcuts" Name="Desktop" Type="integer" Value="1" KeyPath="yes" />
</Component>
<!-- PDF context menu (#682): "Edit with PdfCraft" opens the file in PdfCraft whichever app is
the default for .pdf, which it leaves alone (SystemFileAssociations, like the image
menu). Several PDFs selected (up to 15, Windows' limit for this kind of entry) start one
launch each, and the single-instance hand-off opens them as tabs of one window. On
Windows 11 the entry is under "Show more options": the first-level menu only takes
packaged apps' (IExplorerCommand) entries. Its own component with an HKLM key path, so
INSTALLPDFCONTEXTMENU=0 leaves it out and uninstall removes it. -->
<Component Id="PdfcraftPdfContextMenu" Guid="439F084F-657D-4AC2-A4CC-F099C518D71F"
Condition="INSTALLPDFCONTEXTMENU = 1">
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Value="Edit with PdfCraft" Type="string" KeyPath="yes" />
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="Icon" Value="&quot;[#PdfcraftExe]&quot;,0" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="MultiSelectModel" Value="Document" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit\command" Value="&quot;[#PdfcraftExe]&quot; &quot;%1&quot;" Type="string" />
</Component>
<Component Id="PdfcraftCli">
<File Id="PdfcraftCliExe" Source="$(var.BinDir)\pdfcraft-cli.exe" KeyPath="yes" />
</Component>
+33 -1
View File
@@ -117,6 +117,38 @@ foreach ($ext in @('png', 'jpg', 'jpeg', 'tif', 'tiff', 'gif', 'bmp', 'jp2', 'j2
Assert-Equal $selection[0] 'Single' "$ext context menu selection"
}
# PDF context menu (#682): "Edit with PdfCraft" opens the file in PdfCraft. Its own component, gated
# by INSTALLPDFCONTEXTMENU (default 1, secure, welcome checkbox), with its menu value as key path so
# uninstall removes it. Several selected PDFs each start pdfcraft.exe "%1" (Document), which the
# single-instance hand-off opens as tabs. The .pdf default is never set, so the user's default app
# stays.
$pdfKey = 'Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit'
$pdfMenu = Read-Row ('SELECT `Registry`, `Value`, `Component_`, `Root` FROM `Registry` WHERE `Key` = ''' + $pdfKey + ''' AND `Name` IS NULL') 4
Assert-Equal $pdfMenu[1] 'Edit with PdfCraft' 'PDF context menu label'
Assert-Equal $pdfMenu[2] 'PdfcraftPdfContextMenu' 'PDF context menu component'
Assert-Equal $pdfMenu[3] '2' 'PDF context menu HKLM root'
foreach ($value in @(@('Icon', '"[#PdfcraftExe]",0'), @('MultiSelectModel', 'Document'))) {
$row = Read-Row ('SELECT `Value`, `Component_` FROM `Registry` WHERE `Key` = ''' + $pdfKey + ''' AND `Name` = ''' + $value[0] + '''') 2
Assert-Equal $row[0] $value[1] "PDF context menu $($value[0])"
Assert-Equal $row[1] 'PdfcraftPdfContextMenu' "PDF context menu $($value[0]) component"
}
$pdfCommand = Read-Row ('SELECT `Value`, `Component_` FROM `Registry` WHERE `Key` = ''' + $pdfKey + '\command''') 2
Assert-Equal $pdfCommand[0] '"[#PdfcraftExe]" "%1"' 'PDF context menu command'
Assert-Equal $pdfCommand[1] 'PdfcraftPdfContextMenu' 'PDF context menu command component'
$pdfComponent = Read-Row 'SELECT `Condition`, `KeyPath` FROM `Component` WHERE `Component` = ''PdfcraftPdfContextMenu''' 2
Assert-Equal $pdfComponent[0] 'INSTALLPDFCONTEXTMENU = 1' 'PDF context menu condition'
Assert-Equal $pdfComponent[1] $pdfMenu[0] 'PDF context menu key path'
$pdfDefault = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''INSTALLPDFCONTEXTMENU''' 1
Assert-Equal $pdfDefault[0] '1' 'PDF context menu default'
if (($secure[0] -split ';') -notcontains 'INSTALLPDFCONTEXTMENU') { throw "INSTALLPDFCONTEXTMENU is not secure: '$($secure[0])'" }
$pdfCheckbox = Read-Row 'SELECT `Type`, `Property`, `Text` FROM `Control` WHERE `Dialog_` = ''InstallWelcome'' AND `Control` = ''PdfContextMenu''' 3
Assert-Equal $pdfCheckbox[0] 'CheckBox' 'Welcome PDF context-menu control'
Assert-Equal $pdfCheckbox[1] 'INSTALLPDFCONTEXTMENU' 'Welcome PDF context-menu property'
if ($pdfCheckbox[2] -notmatch 'Edit with PdfCraft') { throw "Welcome PDF context-menu label: '$($pdfCheckbox[2])'" }
$pdfChecked = Read-Row 'SELECT `Value` FROM `CheckBox` WHERE `Property` = ''INSTALLPDFCONTEXTMENU''' 1
Assert-Equal $pdfChecked[0] '1' 'Welcome PDF context-menu value'
Assert-NoRow 'SELECT `Registry` FROM `Registry` WHERE `Key` = ''Software\Classes\.pdf'' AND `Name` IS NULL' '.pdf default app'
# Negative sequences are Windows Installer's success/user-exit/failure paths. Only full UI
# shows these dialogs: an unattended /qn or /qb install must never wait for a Finish click.
foreach ($exit in @(@('InstallComplete', '-1'), @('InstallCancelled', '-2'), @('InstallFailed', '-3'))) {
@@ -147,4 +179,4 @@ $rm = Read-Row 'SELECT `Dialog` FROM `Dialog` WHERE `Dialog` = ''MsiRMFilesInUse
Assert-Equal $rm[0] 'MsiRMFilesInUse' 'Files-in-use dialog'
[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Database)
[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Installer)
Write-Output 'ok MSI: per-machine scope guard, publisher, persistent progress text, Start Menu shortcut, optional desktop shortcut (default on, checkbox), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog, OCR models'
Write-Output 'ok MSI: per-machine scope guard, publisher, persistent progress text, Start Menu shortcut, optional desktop shortcut (default on, checkbox), optional PDF context menu (default on, checkbox), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog, OCR models'
+2 -2
View File
@@ -23,7 +23,7 @@ status = "shipped"
commands = ["file.open"]
tools = ["doc_open"]
evidence = ["crates/automation/tests/automation.rs::open_inspect_render_and_find", "crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/ui.rs::files_and_quit_from_the_operating_system"]
notes = "File ▸ Open, the command line (Linux and Windows file associations), and on macOS Finder, Open With and the Dock (Apple events; the bundle declares PDF as a document type)"
notes = "File ▸ Open, the command line (Linux and Windows file associations), the Windows MSI's \"Edit with PdfCraft\" entry on PDF files' File Explorer right-click menu (#682; several selected PDFs open as tabs of one window), and on macOS Finder, Open With and the Dock (Apple events; the bundle declares PDF as a document type). Not yet: a first-level Windows 11 menu entry (needs package identity) or Combine from the file manager (#585)"
[[feature]]
id = "core.repair-broken-files"
@@ -10035,7 +10035,7 @@ status = "planned"
commands = []
tools = []
evidence = []
notes = "Windows MSI creates a Start menu shortcut and an optional (default on) desktop shortcut and confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. PDF files use a dedicated embedded document icon when PdfCraft is chosen as the default reader (confirmed in Windows Explorer); first-page thumbnails remain unsupported. Signed installers across platforms remain planned."
notes = "Windows MSI creates a Start menu shortcut, an optional (default on) desktop shortcut and an optional (default on, INSTALLPDFCONTEXTMENU) \"Edit with PdfCraft\" File Explorer menu entry for PDF files (#682), and confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. PDF files use a dedicated embedded document icon when PdfCraft is chosen as the default reader (confirmed in Windows Explorer); first-page thumbnails remain unsupported. Signed installers across platforms remain planned."
[[feature]]
id = "misc.web-deploy"