From 128a5a827e8ed5f79ed4fd308a730009d0845610 Mon Sep 17 00:00:00 2001
From: Fernando
Date: Tue, 9 Jun 2026 08:07:26 -0400
Subject: [PATCH 1/3] feat(file-assoc): OS file handlers and launch-with-file
(#664)
Register QtMeshEditor as a 3D model handler on macOS, Windows, and Linux,
and route double-click / argv opens through AppLaunchHandler with
single-instance focus-and-load. Includes packaging, WinGet FileExtensions,
docs, and verification script.
Co-authored-by: Cursor
---
.github/workflows/deploy.yml | 47 ++++
README.md | 2 +
packaging/linux/DEBIAN-postinst | 8 +
packaging/linux/qtmesheditor-mimetypes.xml | 28 +++
packaging/linux/qtmesheditor.desktop | 11 +
packaging/macos/homebrew-cask-postflight.rb | 7 +
packaging/windows/QtMeshEditor.iss | 94 ++++++++
.../register-windows-file-associations.ps1 | 44 ++++
scripts/update-winget.sh | 11 +
scripts/verify-file-associations.sh | 45 ++++
snap/gui/qtmesheditor.desktop | 10 +-
src/AppLaunchHandler.cpp | 202 ++++++++++++++++++
src/AppLaunchHandler.h | 49 +++++
src/AppLaunchHandler_test.cpp | 80 +++++++
src/CMakeLists.txt | 2 +
src/Info.plist.in | 181 ++++++++++++++++
src/main.cpp | 58 ++---
src/mainwindow.cpp | 16 ++
src/mainwindow.h | 2 +
website/src/DocsApp.jsx | 32 +++
20 files changed, 886 insertions(+), 43 deletions(-)
create mode 100644 packaging/linux/DEBIAN-postinst
create mode 100644 packaging/linux/qtmesheditor-mimetypes.xml
create mode 100644 packaging/linux/qtmesheditor.desktop
create mode 100644 packaging/macos/homebrew-cask-postflight.rb
create mode 100644 packaging/windows/QtMeshEditor.iss
create mode 100644 scripts/register-windows-file-associations.ps1
create mode 100755 scripts/verify-file-associations.sh
create mode 100644 src/AppLaunchHandler.cpp
create mode 100644 src/AppLaunchHandler.h
create mode 100644 src/AppLaunchHandler_test.cpp
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 6f38b111b..ced87dbaf 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -43,6 +43,8 @@ jobs:
- uses: actions/checkout@v4
- name: Pinned GitHub Action refs match CMakeLists.txt
run: ./scripts/sync-doc-versions-from-cmake.sh --check
+ - name: File association packaging files present
+ run: chmod +x ./scripts/verify-file-associations.sh && ./scripts/verify-file-associations.sh
####################################################################
# Asset Scan (runs first, before all builds)
@@ -485,11 +487,28 @@ jobs:
name: QtMeshEditor-${{github.ref_name}}-bin-Windows
path: ${{github.workspace}}/bin
+ - name: Bundle Windows file-association helper
+ run: |
+ New-Item -ItemType Directory -Force -Path "${{github.workspace}}/bin/scripts" | Out-Null
+ Copy-Item "${{github.workspace}}/scripts/register-windows-file-associations.ps1" "${{github.workspace}}/bin/scripts/"
+ shell: powershell
+
- name: Compress File
if: github.event_name == 'release' && github.event.action == 'published'
run: Compress-Archive ${{github.workspace}}/bin QtMeshEditor-${{github.ref_name}}-bin-Windows.zip
shell: powershell
+ - name: Build Windows installer (Inno Setup)
+ if: github.event_name == 'release' && github.event.action == 'published'
+ shell: powershell
+ run: |
+ choco install innosetup -y --no-progress
+ $version = "${{ github.ref_name }}".TrimStart("v")
+ $iss = Get-Content "${{github.workspace}}/packaging/windows/QtMeshEditor.iss" -Raw
+ $iss = $iss.Replace("@PROJECT_VERSION@", $version)
+ Set-Content -Path "${{github.workspace}}/packaging/windows/QtMeshEditor.build.iss" -Value $iss
+ & "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" "${{github.workspace}}/packaging/windows/QtMeshEditor.build.iss"
+
- name: Smoke-test CLI from zip
if: github.event_name == 'release' && github.event.action == 'published'
shell: powershell
@@ -528,6 +547,17 @@ jobs:
overwrite: false
verbose: true
+ - name: Upload Windows installer to release
+ if: github.event_name == 'release' && github.event.action == 'published'
+ uses: xresloader/upload-to-github-release@main
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ file: QtMeshEditor-${{github.ref_name}}-setup-Windows.exe
+ update_latest_release: true
+ overwrite: false
+ verbose: true
+
####################################################################
# Linux Deploy
####################################################################
@@ -771,8 +801,14 @@ jobs:
mkdir -p ./pack-deb/usr/share/qtmesheditor/platforms/
mkdir -p ./pack-deb/usr/lib/qtmesheditor/
mkdir -p ./pack-deb/usr/share/doc/qtmesheditor/
+ mkdir -p ./pack-deb/usr/share/applications/
+ mkdir -p ./pack-deb/usr/share/mime/packages/
mkdir ./pack-deb/DEBIAN/
cp ./bin/DEBIAN-control ./pack-deb/DEBIAN/control
+ cp ./packaging/linux/qtmesheditor.desktop ./pack-deb/usr/share/applications/
+ cp ./packaging/linux/qtmesheditor-mimetypes.xml ./pack-deb/usr/share/mime/packages/
+ cp ./packaging/linux/DEBIAN-postinst ./pack-deb/DEBIAN/postinst
+ chmod 755 ./pack-deb/DEBIAN/postinst
cp ./bin/QtMeshEditor ./pack-deb/usr/share/qtmesheditor/qtmesheditor
# Create proper launcher script
@@ -2055,6 +2091,17 @@ jobs:
sed -i "s/version '.*'/version '$VERSION'/" "$CASK_FILE"
sed -i "s/sha256 '.*'/sha256 '$SHA256'/" "$CASK_FILE"
+ # Ensure Finder picks up CFBundleDocumentTypes after install (#664).
+ if ! grep -q 'lsregister' "$CASK_FILE"; then
+ cat >> "$CASK_FILE" <<'RUBY'
+
+ postflight do
+ system_command "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
+ args: ["-f", "-R", "#{appdir}/QtMeshEditor.app"]
+ end
+RUBY
+ fi
+
echo "Updated cask file:"
cat "$CASK_FILE"
diff --git a/README.md b/README.md
index 2a4f7e87e..767c8055b 100755
--- a/README.md
+++ b/README.md
@@ -3,6 +3,8 @@
Automate your 3D asset pipeline — scan, validate, convert, fix, and merge 3D assets with GUI + CLI + CI/CD support.
+**Open 3D files from your OS** — after install, double-click `.fbx`, `.glb`, `.gltf`, `.obj`, `.dae`, `.stl`, `.ply`, `.mesh`, and PS1 `.rsd`/`.tmd` in Finder, Explorer, or your Linux file manager (or use **Open With**). QtMeshEditor loads the model in the running window; a second launch focuses the existing instance instead of spawning another process. Portable Windows ZIP users can run `bin/scripts/register-windows-file-associations.ps1` to register handlers without admin rights.
+
[](https://GitHub.com/fernandotonon/QtMeshEditor/stargazers) Star if you like it!
[]()
diff --git a/packaging/linux/DEBIAN-postinst b/packaging/linux/DEBIAN-postinst
new file mode 100644
index 000000000..dc8465b9b
--- /dev/null
+++ b/packaging/linux/DEBIAN-postinst
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+if command -v update-mime-database >/dev/null 2>&1; then
+ update-mime-database /usr/share/mime >/dev/null 2>&1 || true
+fi
+if command -v update-desktop-database >/dev/null 2>&1; then
+ update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
+fi
diff --git a/packaging/linux/qtmesheditor-mimetypes.xml b/packaging/linux/qtmesheditor-mimetypes.xml
new file mode 100644
index 000000000..7530c5c35
--- /dev/null
+++ b/packaging/linux/qtmesheditor-mimetypes.xml
@@ -0,0 +1,28 @@
+
+
+
+ Ogre mesh
+
+
+
+ PlayStation RSD mesh
+
+
+
+ PlayStation TMD mesh
+
+
+
+ QtMeshEditor scene
+
+
+
+
+ Autodesk FBX model
+
+
+
+ PLY model
+
+
+
diff --git a/packaging/linux/qtmesheditor.desktop b/packaging/linux/qtmesheditor.desktop
new file mode 100644
index 000000000..89a9a6dc9
--- /dev/null
+++ b/packaging/linux/qtmesheditor.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Name=QtMeshEditor
+Comment=Open and inspect 3D models (FBX, glTF, OBJ, and more)
+GenericName=3D Model Viewer
+Keywords=3D;mesh;FBX;glTF;model;animation;viewer;
+Exec=qtmesheditor %F
+Icon=qtmesheditor
+Terminal=false
+Type=Application
+Categories=Graphics;Viewer;
+MimeType=model/gltf+json;model/gltf-binary;model/obj;model/stl;application/vnd.collada+xml;application/vnd.ms-fbx;application/x-ogre-mesh;application/x-ps1-rsd;application/x-ps1-tmd;application/x-qtmesheditor-scene;model/vnd.ply;
diff --git a/packaging/macos/homebrew-cask-postflight.rb b/packaging/macos/homebrew-cask-postflight.rb
new file mode 100644
index 000000000..ab6bf92e5
--- /dev/null
+++ b/packaging/macos/homebrew-cask-postflight.rb
@@ -0,0 +1,7 @@
+# Append to homebrew-qtmesheditor/Casks/qtmesheditor.rb so Finder picks up
+# CFBundleDocumentTypes without a reboot:
+#
+# postflight do
+# system_command "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
+# args: ["-f", "-R", "#{appdir}/QtMeshEditor.app"]
+# end
diff --git a/packaging/windows/QtMeshEditor.iss b/packaging/windows/QtMeshEditor.iss
new file mode 100644
index 000000000..69396ec28
--- /dev/null
+++ b/packaging/windows/QtMeshEditor.iss
@@ -0,0 +1,94 @@
+; Inno Setup script — per-user install with HKCU file associations (#664)
+#define MyAppName "QtMeshEditor"
+#define MyAppVersion "@PROJECT_VERSION@"
+#define MyAppPublisher "Fernando Tonon"
+#define MyAppURL "https://github.com/fernandotonon/QtMeshEditor"
+#define MyAppExeName "QtMeshEditor.exe"
+
+[Setup]
+AppId={{A4B8D6F2-3C1E-4F9A-9B2D-664E8A3F9B2D}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}/issues
+AppUpdatesURL={#MyAppURL}/releases
+DefaultDirName={localappdata}\Programs\{#MyAppName}
+DefaultGroupName={#MyAppName}
+DisableProgramGroupPage=yes
+PrivilegesRequired=lowest
+OutputBaseFilename=QtMeshEditor-{#MyAppVersion}-setup-Windows
+Compression=lzma2
+SolidCompression=yes
+WizardStyle=modern
+ArchitecturesAllowed=x64
+ArchitecturesInstallIn64BitMode=x64
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "fileassoc"; Description: "Register QtMeshEditor as an ""Open with"" handler for 3D model files"; GroupDescription: "File associations:"; Flags: checkedonce
+
+[Files]
+Source: "..\..\bin\*"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+Name: "{group}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"
+
+[Registry]
+; Animatable / common interchange formats (Alternate handler)
+Root: HKCU; Subkey: "Software\Classes\.fbx"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.fbx"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx"; ValueType: string; ValueName: ""; ValueData: "FBX 3D Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.glb"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.glb"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb"; ValueType: string; ValueName: ""; ValueData: "glTF Binary Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.gltf"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.gltf"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf"; ValueType: string; ValueName: ""; ValueData: "glTF Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.obj"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.obj"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj"; ValueType: string; ValueName: ""; ValueData: "Wavefront OBJ Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.dae"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.dae"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae"; ValueType: string; ValueName: ""; ValueData: "Collada Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.stl"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.stl"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl"; ValueType: string; ValueName: ""; ValueData: "STL Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.ply"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.ply"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply"; ValueType: string; ValueName: ""; ValueData: "PLY Model"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+; Native / PS1 formats (Owner)
+Root: HKCU; Subkey: "Software\Classes\.mesh"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.mesh"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh"; ValueType: string; ValueName: ""; ValueData: "Ogre Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.rsd"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.rsd"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd"; ValueType: string; ValueName: ""; ValueData: "PlayStation RSD Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+Root: HKCU; Subkey: "Software\Classes\.tmd"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.tmd"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd"; ValueType: string; ValueName: ""; ValueData: "PlayStation TMD Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
+Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
+
+[Run]
+Filename: "{app}\bin\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent
diff --git a/scripts/register-windows-file-associations.ps1 b/scripts/register-windows-file-associations.ps1
new file mode 100644
index 000000000..35a3b5252
--- /dev/null
+++ b/scripts/register-windows-file-associations.ps1
@@ -0,0 +1,44 @@
+# Register QtMeshEditor as a per-user "Open with" handler for common 3D formats.
+# Usage (from extracted zip or installed bin folder):
+# pwsh -File scripts/register-windows-file-associations.ps1
+# pwsh -File scripts/register-windows-file-associations.ps1 -BinDir "C:\path\to\bin"
+
+param(
+ [string]$BinDir = (Join-Path $PSScriptRoot "..\bin")
+)
+
+$ErrorActionPreference = "Stop"
+$exe = Join-Path $BinDir "QtMeshEditor.exe"
+if (-not (Test-Path $exe)) {
+ Write-Error "QtMeshEditor.exe not found at $exe"
+}
+
+$extensions = @{
+ ".fbx" = "FBX 3D Model"
+ ".glb" = "glTF Binary Model"
+ ".gltf" = "glTF Model"
+ ".obj" = "Wavefront OBJ Model"
+ ".dae" = "Collada Model"
+ ".stl" = "STL Model"
+ ".ply" = "PLY Model"
+ ".mesh" = "Ogre Mesh"
+ ".rsd" = "PlayStation RSD Mesh"
+ ".tmd" = "PlayStation TMD Mesh"
+}
+
+foreach ($entry in $extensions.GetEnumerator()) {
+ $ext = $entry.Key
+ $label = $entry.Value
+ $progId = "QtMeshEditor.Model$($ext.Replace('.', ''))"
+ New-Item -Path "HKCU:\Software\Classes\$ext" -Force | Out-Null
+ Set-ItemProperty -Path "HKCU:\Software\Classes\$ext" -Name "(default)" -Value $progId
+ New-Item -Path "HKCU:\Software\Classes\$progId" -Force | Out-Null
+ Set-ItemProperty -Path "HKCU:\Software\Classes\$progId" -Name "(default)" -Value $label
+ New-Item -Path "HKCU:\Software\Classes\$progId\DefaultIcon" -Force | Out-Null
+ Set-ItemProperty -Path "HKCU:\Software\Classes\$progId\DefaultIcon" -Name "(default)" -Value "$exe,0"
+ New-Item -Path "HKCU:\Software\Classes\$progId\shell\open\command" -Force | Out-Null
+ Set-ItemProperty -Path "HKCU:\Software\Classes\$progId\shell\open\command" -Name "(default)" -Value "`"$exe`" `"%1`""
+ Write-Host "Registered $ext -> $progId"
+}
+
+Write-Host "Done. QtMeshEditor should appear in Explorer 'Open with' for registered extensions."
diff --git a/scripts/update-winget.sh b/scripts/update-winget.sh
index 809edfdd0..bdc30b48f 100755
--- a/scripts/update-winget.sh
+++ b/scripts/update-winget.sh
@@ -90,6 +90,17 @@ NestedInstallerFiles:
PortableCommandAlias: qtmesheditor
- RelativeFilePath: bin\\qtmesh.exe
PortableCommandAlias: qtmesh
+FileExtensions:
+ - fbx
+ - glb
+ - gltf
+ - obj
+ - dae
+ - stl
+ - ply
+ - mesh
+ - rsd
+ - tmd
Installers:
- Architecture: x64
InstallerUrl: ${ZIP_URL}
diff --git a/scripts/verify-file-associations.sh b/scripts/verify-file-associations.sh
new file mode 100755
index 000000000..d1cab6d66
--- /dev/null
+++ b/scripts/verify-file-associations.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Smoke-check that file-association packaging files exist and list expected extensions.
+# Full OS verification still requires manual VM tests (issue #669).
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+FAIL=0
+
+check_contains() {
+ local file="$1"
+ local needle="$2"
+ if ! grep -q "$needle" "$file"; then
+ echo "verify-file-associations: missing '$needle' in $file" >&2
+ FAIL=1
+ fi
+}
+
+echo "=== macOS Info.plist ==="
+PLIST="$ROOT/src/Info.plist.in"
+for ext in fbx glb gltf obj dae stl ply mesh rsd tmd; do
+ check_contains "$PLIST" "${ext}"
+done
+check_contains "$PLIST" "CFBundleDocumentTypes"
+check_contains "$PLIST" "UTExportedTypeDeclarations"
+
+echo "=== Linux desktop + MIME ==="
+DESKTOP="$ROOT/packaging/linux/qtmesheditor.desktop"
+MIME="$ROOT/packaging/linux/qtmesheditor-mimetypes.xml"
+check_contains "$DESKTOP" "Exec=qtmesheditor %F"
+check_contains "$DESKTOP" "MimeType="
+check_contains "$MIME" "application/x-ogre-mesh"
+check_contains "$MIME" "application/vnd.ms-fbx"
+
+echo "=== Windows packaging ==="
+check_contains "$ROOT/packaging/windows/QtMeshEditor.iss" "QtMeshEditor.Model.fbx"
+check_contains "$ROOT/scripts/register-windows-file-associations.ps1" ".fbx"
+
+echo "=== Qt launch handler ==="
+check_contains "$ROOT/src/AppLaunchHandler.h" "kServerName"
+check_contains "$ROOT/src/main.cpp" "AppLaunchHandler"
+
+if [[ "$FAIL" -ne 0 ]]; then
+ exit 1
+fi
+echo "verify-file-associations: OK"
diff --git a/snap/gui/qtmesheditor.desktop b/snap/gui/qtmesheditor.desktop
index f4d452211..bf29809f7 100644
--- a/snap/gui/qtmesheditor.desktop
+++ b/snap/gui/qtmesheditor.desktop
@@ -1,9 +1,11 @@
[Desktop Entry]
Name=QtMeshEditor
-Comment=Free 3D asset tool — FBX/glTF/materials/MCP CLI
-Keywords=3D;mesh;FBX;glTF;model;material;animation;Ogre;editor;development;
-Exec=qtmesheditor %U
+Comment=Open and inspect 3D models (FBX, glTF, OBJ, and more)
+GenericName=3D Model Viewer
+Keywords=3D;mesh;FBX;glTF;model;material;animation;viewer;
+Exec=qtmesheditor %F
Icon=${SNAP}/meta/gui/icon.png
Terminal=false
Type=Application
-Categories=Graphics;
+Categories=Graphics;Viewer;
+MimeType=model/gltf+json;model/gltf-binary;model/obj;model/stl;application/vnd.collada+xml;application/vnd.ms-fbx;application/x-ogre-mesh;application/x-ps1-rsd;application/x-ps1-tmd;application/x-qtmesheditor-scene;model/vnd.ply;
diff --git a/src/AppLaunchHandler.cpp b/src/AppLaunchHandler.cpp
new file mode 100644
index 000000000..39818b3f5
--- /dev/null
+++ b/src/AppLaunchHandler.cpp
@@ -0,0 +1,202 @@
+#include "AppLaunchHandler.h"
+
+#include "Manager.h"
+#include "SentryReporter.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+bool isCliSubcommand(const QString& arg)
+{
+ static const QStringList kSubcommands = {
+ QStringLiteral("info"), QStringLiteral("fix"), QStringLiteral("convert"),
+ QStringLiteral("anim"), QStringLiteral("validate"), QStringLiteral("lod"),
+ QStringLiteral("pose"), QStringLiteral("turntable"), QStringLiteral("scan"),
+ QStringLiteral("material"), QStringLiteral("pack-textures"),
+ QStringLiteral("normal-from-height"), QStringLiteral("memory"),
+ QStringLiteral("analyze"), QStringLiteral("vertex-cache"),
+ QStringLiteral("decimate"), QStringLiteral("atlas"), QStringLiteral("atlas-apply"),
+ QStringLiteral("optimize"), QStringLiteral("bake-vertex-colors"),
+ QStringLiteral("vat"), QStringLiteral("uv"), QStringLiteral("retopo"),
+ QStringLiteral("skin"), QStringLiteral("morph"), QStringLiteral("nodeanim"),
+ };
+ return kSubcommands.contains(arg);
+}
+
+bool isGuiModeFlag(const QString& arg)
+{
+ return arg == QStringLiteral("--mcp") || arg == QStringLiteral("-mcp")
+ || arg == QStringLiteral("--with-mcp") || arg == QStringLiteral("--http-port");
+}
+
+} // namespace
+
+AppLaunchHandler::AppLaunchHandler(QObject* parent)
+ : QObject(parent)
+{
+ if (qobject_cast(QCoreApplication::instance()))
+ QCoreApplication::instance()->installEventFilter(this);
+}
+
+AppLaunchHandler::~AppLaunchHandler()
+{
+ if (m_server) {
+ m_server->close();
+ QLocalServer::removeServer(QLatin1String(kServerName));
+ }
+}
+
+bool AppLaunchHandler::isCliInvocation(int argc, char* argv[])
+{
+ const QString execName = QFileInfo(QString::fromLocal8Bit(argv[0])).fileName().toLower();
+ if (execName.startsWith(QStringLiteral("qtmesh")) && !execName.contains(QStringLiteral("editor")))
+ return true;
+
+ for (int i = 1; i < argc; ++i) {
+ const QString arg = QString::fromLocal8Bit(argv[i]);
+ if (arg == QStringLiteral("--cli") || arg == QStringLiteral("--help")
+ || arg == QStringLiteral("-h") || arg == QStringLiteral("--version")
+ || arg == QStringLiteral("-v")) {
+ return true;
+ }
+ }
+
+ for (int i = 1; i < argc; ++i) {
+ const QString arg = QString::fromLocal8Bit(argv[i]);
+ if (arg.startsWith(QLatin1Char('-')))
+ continue;
+ if (isCliSubcommand(arg))
+ return true;
+ break;
+ }
+ return false;
+}
+
+bool AppLaunchHandler::isImportableMeshPath(const QString& path)
+{
+ const QString lower = path.toLower();
+ if (lower.endsWith(QStringLiteral(".scene.glb"))
+ || lower.endsWith(QStringLiteral(".scene.gltf"))) {
+ return true;
+ }
+
+ const QStringList extensions =
+ Manager::defaultImportExtensions().split(QLatin1Char(' '), Qt::SkipEmptyParts);
+ for (const QString& ext : extensions) {
+ if (lower.endsWith(ext, Qt::CaseInsensitive))
+ return true;
+ }
+ return false;
+}
+
+QStringList AppLaunchHandler::collectGuiLaunchPaths(const QStringList& arguments)
+{
+ QStringList paths;
+ for (int i = 1; i < arguments.size(); ++i) {
+ const QString& arg = arguments.at(i);
+ if (arg.startsWith(QLatin1Char('-')))
+ continue;
+ if (isGuiModeFlag(arg)) {
+ if (arg == QStringLiteral("--http-port") && i + 1 < arguments.size())
+ ++i;
+ continue;
+ }
+ if (isCliSubcommand(arg))
+ break;
+
+ const QFileInfo info(arg);
+ if (!isImportableMeshPath(arg))
+ continue;
+ if (!info.exists() || !info.isFile() || !info.isReadable())
+ continue;
+ paths.append(info.absoluteFilePath());
+ }
+ return paths;
+}
+
+bool AppLaunchHandler::tryForwardToRunningInstance(const QStringList& paths)
+{
+ if (paths.isEmpty())
+ return false;
+
+ QLocalSocket socket;
+ socket.connectToServer(QLatin1String(kServerName));
+ if (!socket.waitForConnected(750))
+ return false;
+
+ QByteArray payload;
+ {
+ QDataStream out(&payload, QIODevice::WriteOnly);
+ out.setVersion(QDataStream::Qt_6_0);
+ out << paths;
+ }
+ socket.write(payload);
+ socket.flush();
+ socket.waitForBytesWritten(1500);
+ socket.disconnectFromServer();
+ return true;
+}
+
+bool AppLaunchHandler::startSingleInstanceServer()
+{
+ if (m_server)
+ return true;
+
+ QLocalServer::removeServer(QLatin1String(kServerName));
+ m_server = new QLocalServer(this);
+ if (!m_server->listen(QLatin1String(kServerName)))
+ return false;
+
+ connect(m_server, &QLocalServer::newConnection, this, [this]() {
+ while (m_server->hasPendingConnections()) {
+ QLocalSocket* socket = m_server->nextPendingConnection();
+ connect(socket, &QLocalSocket::readyRead, this, [this, socket]() {
+ const QByteArray payload = socket->readAll();
+ if (payload.isEmpty())
+ return;
+ QDataStream in(payload);
+ in.setVersion(QDataStream::Qt_6_0);
+ QStringList paths;
+ in >> paths;
+ handleIncomingPaths(paths);
+ socket->disconnectFromServer();
+ socket->deleteLater();
+ });
+ }
+ });
+ return true;
+}
+
+void AppLaunchHandler::handleIncomingPaths(const QStringList& paths)
+{
+ QStringList accepted;
+ for (const QString& path : paths) {
+ if (!isImportableMeshPath(path))
+ continue;
+ const QFileInfo info(path);
+ if (!info.exists() || !info.isFile())
+ continue;
+ accepted.append(info.absoluteFilePath());
+ }
+ if (!accepted.isEmpty())
+ emit filesRequested(accepted);
+}
+
+bool AppLaunchHandler::eventFilter(QObject* watched, QEvent* event)
+{
+ if (event->type() == QEvent::FileOpen) {
+ auto* openEvent = static_cast(event);
+ const QString path = openEvent->file();
+ if (!path.isEmpty() && isImportableMeshPath(path)) {
+ handleIncomingPaths({QFileInfo(path).absoluteFilePath()});
+ return true;
+ }
+ }
+ return QObject::eventFilter(watched, event);
+}
diff --git a/src/AppLaunchHandler.h b/src/AppLaunchHandler.h
new file mode 100644
index 000000000..885a8e5c9
--- /dev/null
+++ b/src/AppLaunchHandler.h
@@ -0,0 +1,49 @@
+#ifndef APPLAUNCHHANDLER_H
+#define APPLAUNCHHANDLER_H
+
+#include
+#include
+
+class QLocalServer;
+class QEvent;
+
+/// Routes OS file-open requests (argv, Finder QFileOpenEvent, second-instance
+/// socket) into the running GUI. CLI activation rules in main.cpp take precedence.
+class AppLaunchHandler : public QObject
+{
+ Q_OBJECT
+
+public:
+ static constexpr const char* kServerName = "QtMeshEditorSingleInstance-v1";
+
+ explicit AppLaunchHandler(QObject* parent = nullptr);
+ ~AppLaunchHandler() override;
+
+ /// Mirrors main.cpp CLI detection: true when CLIPipeline should run.
+ static bool isCliInvocation(int argc, char* argv[]);
+
+ /// Positional mesh/scene paths from QApplication::arguments() (flags skipped).
+ static QStringList collectGuiLaunchPaths(const QStringList& arguments);
+
+ /// Extension check against Manager::defaultImportExtensions() (+ scene.glb).
+ static bool isImportableMeshPath(const QString& path);
+
+ /// If another GUI instance is running, forward paths and return true (caller exits).
+ bool tryForwardToRunningInstance(const QStringList& paths);
+
+ /// Listen for subsequent launches. Emits filesRequested when paths arrive.
+ bool startSingleInstanceServer();
+
+signals:
+ void filesRequested(const QStringList& paths);
+
+protected:
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ void handleIncomingPaths(const QStringList& paths);
+
+ QLocalServer* m_server = nullptr;
+};
+
+#endif // APPLAUNCHHANDLER_H
diff --git a/src/AppLaunchHandler_test.cpp b/src/AppLaunchHandler_test.cpp
new file mode 100644
index 000000000..06d59c71a
--- /dev/null
+++ b/src/AppLaunchHandler_test.cpp
@@ -0,0 +1,80 @@
+#include "AppLaunchHandler.h"
+#include "Manager.h"
+
+#include
+#include
+#include
+
+namespace {
+
+char arg0[] = "QtMeshEditor";
+char argInfo[] = "info";
+char argModel[] = "hero.fbx";
+char argHelp[] = "--help";
+
+TEST(AppLaunchHandlerTest, IsCliInvocation_Subcommand)
+{
+ char* argv[] = {arg0, argInfo, argModel, nullptr};
+ EXPECT_TRUE(AppLaunchHandler::isCliInvocation(3, argv));
+}
+
+TEST(AppLaunchHandlerTest, IsCliInvocation_HelpFlag)
+{
+ char* argv[] = {arg0, argHelp, nullptr};
+ EXPECT_TRUE(AppLaunchHandler::isCliInvocation(2, argv));
+}
+
+TEST(AppLaunchHandlerTest, IsCliInvocation_GuiModelPath)
+{
+ char* argv[] = {arg0, argModel, nullptr};
+ EXPECT_FALSE(AppLaunchHandler::isCliInvocation(2, argv));
+}
+
+TEST(AppLaunchHandlerTest, IsImportableMeshPath_KnownExtensions)
+{
+ EXPECT_TRUE(AppLaunchHandler::isImportableMeshPath(QStringLiteral("/tmp/hero.fbx")));
+ EXPECT_TRUE(AppLaunchHandler::isImportableMeshPath(QStringLiteral("/tmp/level.mesh")));
+ EXPECT_TRUE(AppLaunchHandler::isImportableMeshPath(QStringLiteral("/tmp/scene.scene.glb")));
+ EXPECT_FALSE(AppLaunchHandler::isImportableMeshPath(QStringLiteral("/tmp/readme.pdf")));
+}
+
+TEST(AppLaunchHandlerTest, CollectGuiLaunchPaths_SkipsFlagsAndSubcommands)
+{
+ const QStringList args = {
+ QStringLiteral("QtMeshEditor"),
+ QStringLiteral("--with-mcp"),
+ QStringLiteral("scan"),
+ QStringLiteral("./assets"),
+ };
+ EXPECT_TRUE(AppLaunchHandler::collectGuiLaunchPaths(args).isEmpty());
+}
+
+TEST(AppLaunchHandlerTest, CollectGuiLaunchPaths_ReadsExistingMeshFile)
+{
+ QTemporaryDir dir;
+ ASSERT_TRUE(dir.isValid());
+ const QString meshPath = dir.filePath(QStringLiteral("cube.obj"));
+ QFile obj(meshPath);
+ if (!obj.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ GTEST_SKIP() << "Could not create temp OBJ";
+ }
+ obj.write("o cube\nv 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n");
+ obj.close();
+
+ const QStringList args = {
+ QStringLiteral("QtMeshEditor"),
+ QStringLiteral("--verbose"),
+ meshPath,
+ };
+ const QStringList paths = AppLaunchHandler::collectGuiLaunchPaths(args);
+ ASSERT_EQ(paths.size(), 1);
+ EXPECT_EQ(paths.front(), QFileInfo(meshPath).absoluteFilePath());
+}
+
+TEST(AppLaunchHandlerTest, DefaultImportExtensions_AlignsWithManager)
+{
+ EXPECT_FALSE(Manager::defaultImportExtensions().isEmpty());
+ EXPECT_TRUE(AppLaunchHandler::isImportableMeshPath(QStringLiteral("x.glb")));
+}
+
+} // namespace
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f893c7904..168e415ee 100755
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,6 +5,7 @@
set(SRC_FILES
about.cpp
AppConsoleLog.cpp
+AppLaunchHandler.cpp
AnimationBlender.cpp
AnimationControlController.cpp
CurveEditModel.cpp
@@ -146,6 +147,7 @@ GlobalDefinitions.h
Euler.h
about.h
AppConsoleLog.h
+AppLaunchHandler.h
AppSettingsKeys.h
mainwindow.h
Manager.h
diff --git a/src/Info.plist.in b/src/Info.plist.in
index 511c75948..6920a7943 100644
--- a/src/Info.plist.in
+++ b/src/Info.plist.in
@@ -42,5 +42,186 @@
NSSupportsAutomaticGraphicsSwitching
+
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ FBX 3D Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ fbx
+
+
+ CFBundleTypeName
+ glTF 2.0 Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+
+ gltf
+ glb
+ gltf2
+ glb2
+
+
+
+ CFBundleTypeName
+ Wavefront OBJ Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ obj
+
+
+ CFBundleTypeName
+ Collada Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ dae
+
+
+ CFBundleTypeName
+ STL Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ stl
+
+
+ CFBundleTypeName
+ PLY Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ ply
+
+
+ CFBundleTypeName
+ VRM Avatar
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ vrm
+
+
+ CFBundleTypeName
+ 3D Studio Model
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+ 3ds
+
+
+ CFBundleTypeName
+ QtMesh Scene
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ CFBundleTypeExtensions
+
+ scene.glb
+ scene.gltf
+
+
+
+ CFBundleTypeName
+ Ogre Mesh
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Owner
+ LSItemContentTypes
+ com.qtmesheditor.mesh
+ CFBundleTypeExtensions
+ mesh
+
+
+ CFBundleTypeName
+ PlayStation RSD Mesh
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Owner
+ LSItemContentTypes
+ com.qtmesheditor.ps1-rsd
+ CFBundleTypeExtensions
+ rsd
+
+
+ CFBundleTypeName
+ PlayStation TMD Mesh
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Owner
+ LSItemContentTypes
+ com.qtmesheditor.ps1-tmd
+ CFBundleTypeExtensions
+ tmd
+
+
+
+ UTExportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ com.qtmesheditor.mesh
+ UTTypeDescription
+ Ogre Mesh
+ UTTypeConformsTo
+ public.3d-content
+ UTTypeTagSpecification
+
+ public.filename-extension
+ mesh
+
+
+
+ UTTypeIdentifier
+ com.qtmesheditor.ps1-rsd
+ UTTypeDescription
+ PlayStation RSD Mesh
+ UTTypeConformsTo
+ public.3d-content
+ UTTypeTagSpecification
+
+ public.filename-extension
+ rsd
+
+
+
+ UTTypeIdentifier
+ com.qtmesheditor.ps1-tmd
+ UTTypeDescription
+ PlayStation TMD Mesh
+ UTTypeConformsTo
+ public.3d-content
+ UTTypeTagSpecification
+
+ public.filename-extension
+ tmd
+
+
+
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 74e2482b2..52e5dd57b 100755
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -29,6 +29,7 @@
#include "SentryReporter.h"
#include "CLIPipeline.h"
#include "AppConsoleLog.h"
+#include "AppLaunchHandler.h"
#ifndef Q_OS_WIN
#include
@@ -66,39 +67,7 @@ int main(int argc, char *argv[])
#endif
// CLI pipeline mode detection — check before creating QApplication
- {
- bool cliMode = false;
- QString execName = QFileInfo(QString(argv[0])).fileName().toLower();
- if (execName.startsWith("qtmesh") && !execName.contains("editor")) {
- cliMode = true;
- }
- for (int i = 1; i < argc; ++i) {
- QString arg(argv[i]);
- if (arg == "--cli" || arg == "--help" || arg == "-h" ||
- arg == "--version" || arg == "-v") {
- cliMode = true;
- break;
- }
- }
- if (!cliMode) {
- for (int i = 1; i < argc; ++i) {
- QString arg(argv[i]);
- if (arg.startsWith("-"))
- continue; // skip flags like --verbose
- if (arg == "info" || arg == "fix" || arg == "convert" || arg == "anim"
- || arg == "validate" || arg == "lod" || arg == "pose" || arg == "turntable"
- || arg == "scan" || arg == "material" || arg == "pack-textures"
- || arg == "normal-from-height" || arg == "memory"
- || arg == "analyze" || arg == "vertex-cache"
- || arg == "decimate" || arg == "atlas" || arg == "atlas-apply"
- || arg == "optimize" || arg == "bake-vertex-colors"
- || arg == "vat" || arg == "uv" || arg == "retopo"
- || arg == "skin" || arg == "morph" || arg == "nodeanim")
- cliMode = true;
- break; // first non-flag arg determines mode
- }
- }
- if (cliMode) {
+ if (AppLaunchHandler::isCliInvocation(argc, argv)) {
#ifdef Q_OS_WIN
// QtMeshEditor.exe is a WIN32 GUI subsystem executable — it has no
// console by default. Reattach to the parent console (PowerShell/cmd)
@@ -111,8 +80,7 @@ int main(int argc, char *argv[])
freopen("CONOUT$", "w", stderr);
}
#endif
- return CLIPipeline::run(argc, argv);
- }
+ return CLIPipeline::run(argc, argv);
}
// Check for MCP server mode before creating QApplication
@@ -259,10 +227,16 @@ int main(int argc, char *argv[])
return ThemeManager::qmlInstance(engine, scriptEngine);
});
- // Show welcome dialog before creating MainWindow
+ const QStringList launchPaths = AppLaunchHandler::collectGuiLaunchPaths(a.arguments());
+ AppLaunchHandler launchHandler;
+ if (!launchPaths.isEmpty() && launchHandler.tryForwardToRunningInstance(launchPaths))
+ return 0;
+ launchHandler.startSingleInstanceServer();
+
+ // Show welcome dialog before creating MainWindow (skip when OS opened a file)
QString welcomeOpenFile;
bool welcomeNewScene = false;
- if (WelcomeDialog::shouldShow()) {
+ if (launchPaths.isEmpty() && WelcomeDialog::shouldShow()) {
WelcomeDialog welcome;
welcome.exec();
if (welcome.userAction() == WelcomeDialog::OpenFile ||
@@ -280,8 +254,14 @@ int main(int argc, char *argv[])
MainWindow w;
w.show();
- // Act on welcome dialog choice
- if (!welcomeOpenFile.isEmpty()) {
+ QObject::connect(&launchHandler, &AppLaunchHandler::filesRequested, &w,
+ &MainWindow::openLaunchFiles);
+
+ if (!launchPaths.isEmpty()) {
+ QTimer::singleShot(0, &w, [&w, launchPaths]() {
+ w.openLaunchFiles(launchPaths);
+ });
+ } else if (!welcomeOpenFile.isEmpty()) {
QTimer::singleShot(0, &w, [&w, welcomeOpenFile]() {
w.loadFile(welcomeOpenFile);
});
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 1429fa420..ffd7f948b 100755
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -3631,6 +3631,22 @@ void MainWindow::loadFile(const QString& filePath)
mUriList.append(filePath);
}
+void MainWindow::openLaunchFiles(const QStringList& paths)
+{
+ if (paths.isEmpty())
+ return;
+
+ show();
+ raise();
+ activateWindow();
+
+ for (const QString& path : paths) {
+ SentryReporter::addBreadcrumb(QStringLiteral("app.launch.file_open"),
+ QFileInfo(path).fileName());
+ loadFile(path);
+ }
+}
+
void MainWindow::importMeshs(const QStringList &_uriList)
{
auto txn = SentryReporter::startTransaction("ui.import", "file.import");
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 634fc1b9a..44184a501 100755
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -63,6 +63,8 @@ class MainWindow : public QMainWindow, public Ogre::FrameListener
virtual ~MainWindow();
void importMeshs(const QStringList &_uriList);
void loadFile(const QString& filePath);
+ /// Focus the window and queue one or more OS launch paths for import.
+ void openLaunchFiles(const QStringList& paths);
void setMCPServer(MCPServer* server);
/// Recreate Ogre render windows (e.g. after MSAA samples change in Preferences).
diff --git a/website/src/DocsApp.jsx b/website/src/DocsApp.jsx
index c6f65ea37..a0f5b4f96 100644
--- a/website/src/DocsApp.jsx
+++ b/website/src/DocsApp.jsx
@@ -5,6 +5,7 @@ import useQtmeshActionRef from './hooks/useQtmeshActionRef';
const NAV = [
{ section: 'Getting Started', items: [
{ id: 'installation', label: 'Installation' },
+ { id: 'file-associations', label: 'Open 3D Files' },
{ id: 'quick-start', label: 'Quick Start' },
{ id: 'playstation-rsd-ply', label: 'PlayStation RSD / Psy-Q PLY' },
]},
@@ -205,6 +206,37 @@ cmake --build build --target QtMeshEditor -j4
# The 'qtmesh' symlink is created automatically`}
+
+ Open 3D Files from the OS
+
+ QtMeshEditor registers as a document handler on macOS, Windows, and Linux. After install,
+ double-click supported models or use your file manager's Open With menu.
+ Launching with a file path loads it in the GUI; if the app is already running, the existing
+ window is focused and the new file is imported (single-instance).
+
+ Supported extensions
+
+ .fbx, .glb, .gltf, .obj, .dae,
+ .stl, .ply, .vrm, .3ds, QtMesh-native
+ .mesh, PS1 .rsd/.tmd, and saved scenes
+ .scene.glb/.scene.gltf.
+
+ Per platform
+
+ - macOS —
Info.plist declares CFBundleDocumentTypes;
+ Homebrew cask runs lsregister after install so Finder sees handlers immediately.
+ - Windows — Inno Setup installer (release) registers per-user handlers under
+
HKCU. Portable ZIP includes
+ bin/scripts/register-windows-file-associations.ps1 for manual registration.
+ - Linux —
.desktop + XDG MIME package in the .deb and Snap;
+ postinstall runs update-mime-database and update-desktop-database.
+
+
+ CLI mode is unchanged: qtmesh info model.fbx still runs headless. Only
+ QtMeshEditor model.fbx (or OS open-with) enters GUI-with-file mode.
+
+
+
Quick Start
{`# Inspect a model
From 65030040b45beec7dc43fb1adb3ae49d331e23fa Mon Sep 17 00:00:00 2001
From: Fernando
Date: Tue, 9 Jun 2026 16:46:58 -0400
Subject: [PATCH 2/3] fix(file-assoc): address PR #716 review feedback
Queue launch paths during the welcome dialog, register Windows handlers
via OpenWithProgids instead of replacing defaults, add Sentry breadcrumbs,
tighten docs to per-platform registration scope, and harden CI/scripts.
Co-authored-by: Cursor
---
.github/workflows/deploy.yml | 7 ++++-
packaging/windows/QtMeshEditor.iss | 23 +++++++-------
.../register-windows-file-associations.ps1 | 8 +++--
scripts/update-winget.sh | 2 +-
scripts/verify-file-associations.sh | 5 +--
src/AppLaunchHandler.cpp | 7 ++++-
src/main.cpp | 14 +++++++--
website/src/DocsApp.jsx | 31 +++++++++++--------
8 files changed, 61 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ced87dbaf..a248d31ef 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -503,11 +503,16 @@ jobs:
shell: powershell
run: |
choco install innosetup -y --no-progress
+ $isccPath = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
+ if (-not (Test-Path $isccPath)) {
+ Write-Error "Inno Setup installation failed or ISCC.exe not found at $isccPath"
+ exit 1
+ }
$version = "${{ github.ref_name }}".TrimStart("v")
$iss = Get-Content "${{github.workspace}}/packaging/windows/QtMeshEditor.iss" -Raw
$iss = $iss.Replace("@PROJECT_VERSION@", $version)
Set-Content -Path "${{github.workspace}}/packaging/windows/QtMeshEditor.build.iss" -Value $iss
- & "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" "${{github.workspace}}/packaging/windows/QtMeshEditor.build.iss"
+ & $isccPath "${{github.workspace}}/packaging/windows/QtMeshEditor.build.iss"
- name: Smoke-test CLI from zip
if: github.event_name == 'release' && github.event.action == 'published'
diff --git a/packaging/windows/QtMeshEditor.iss b/packaging/windows/QtMeshEditor.iss
index 69396ec28..eebb95e9d 100644
--- a/packaging/windows/QtMeshEditor.iss
+++ b/packaging/windows/QtMeshEditor.iss
@@ -38,54 +38,53 @@ Name: "{group}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"
[Registry]
-; Animatable / common interchange formats (Alternate handler)
-Root: HKCU; Subkey: "Software\Classes\.fbx"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.fbx"; Tasks: fileassoc; Flags: uninsdeletevalue
+; Open With only — do not replace the user's default ProgID for each extension.
+Root: HKCU; Subkey: "Software\Classes\.fbx\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.fbx"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx"; ValueType: string; ValueName: ""; ValueData: "FBX 3D Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.fbx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.glb"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.glb"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.glb\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.glb"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb"; ValueType: string; ValueName: ""; ValueData: "glTF Binary Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.glb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.gltf"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.gltf"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.gltf\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.gltf"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf"; ValueType: string; ValueName: ""; ValueData: "glTF Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.gltf\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.obj"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.obj"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.obj\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.obj"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj"; ValueType: string; ValueName: ""; ValueData: "Wavefront OBJ Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.obj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.dae"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.dae"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.dae\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.dae"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae"; ValueType: string; ValueName: ""; ValueData: "Collada Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.dae\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.stl"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.stl"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.stl\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.stl"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl"; ValueType: string; ValueName: ""; ValueData: "STL Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.stl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.ply"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.ply"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.ply\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.ply"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply"; ValueType: string; ValueName: ""; ValueData: "PLY Model"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.ply\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-; Native / PS1 formats (Owner)
-Root: HKCU; Subkey: "Software\Classes\.mesh"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.mesh"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.mesh\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.mesh"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh"; ValueType: string; ValueName: ""; ValueData: "Ogre Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.mesh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.rsd"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.rsd"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.rsd\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.rsd"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd"; ValueType: string; ValueName: ""; ValueData: "PlayStation RSD Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.rsd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
-Root: HKCU; Subkey: "Software\Classes\.tmd"; ValueType: string; ValueName: ""; ValueData: "QtMeshEditor.Model.tmd"; Tasks: fileassoc; Flags: uninsdeletevalue
+Root: HKCU; Subkey: "Software\Classes\.tmd\OpenWithProgids"; ValueType: string; ValueName: "QtMeshEditor.Model.tmd"; ValueData: ""; Tasks: fileassoc; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd"; ValueType: string; ValueName: ""; ValueData: "PlayStation TMD Mesh"; Tasks: fileassoc; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\bin\{#MyAppExeName},0"; Tasks: fileassoc
Root: HKCU; Subkey: "Software\Classes\QtMeshEditor.Model.tmd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\bin\{#MyAppExeName}"" ""%1"""; Tasks: fileassoc
diff --git a/scripts/register-windows-file-associations.ps1 b/scripts/register-windows-file-associations.ps1
index 35a3b5252..84c7a3a80 100644
--- a/scripts/register-windows-file-associations.ps1
+++ b/scripts/register-windows-file-associations.ps1
@@ -1,4 +1,5 @@
# Register QtMeshEditor as a per-user "Open with" handler for common 3D formats.
+# Does not change the user's default app for each extension.
# Usage (from extracted zip or installed bin folder):
# pwsh -File scripts/register-windows-file-associations.ps1
# pwsh -File scripts/register-windows-file-associations.ps1 -BinDir "C:\path\to\bin"
@@ -30,15 +31,16 @@ foreach ($entry in $extensions.GetEnumerator()) {
$ext = $entry.Key
$label = $entry.Value
$progId = "QtMeshEditor.Model$($ext.Replace('.', ''))"
- New-Item -Path "HKCU:\Software\Classes\$ext" -Force | Out-Null
- Set-ItemProperty -Path "HKCU:\Software\Classes\$ext" -Name "(default)" -Value $progId
+ $openWithKey = "HKCU:\Software\Classes\$ext\OpenWithProgids"
+ New-Item -Path $openWithKey -Force | Out-Null
+ New-ItemProperty -Path $openWithKey -Name $progId -PropertyType String -Value "" -Force | Out-Null
New-Item -Path "HKCU:\Software\Classes\$progId" -Force | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Classes\$progId" -Name "(default)" -Value $label
New-Item -Path "HKCU:\Software\Classes\$progId\DefaultIcon" -Force | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Classes\$progId\DefaultIcon" -Name "(default)" -Value "$exe,0"
New-Item -Path "HKCU:\Software\Classes\$progId\shell\open\command" -Force | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Classes\$progId\shell\open\command" -Name "(default)" -Value "`"$exe`" `"%1`""
- Write-Host "Registered $ext -> $progId"
+ Write-Host "Registered $ext in OpenWithProgids -> $progId"
}
Write-Host "Done. QtMeshEditor should appear in Explorer 'Open with' for registered extensions."
diff --git a/scripts/update-winget.sh b/scripts/update-winget.sh
index bdc30b48f..d53644b42 100755
--- a/scripts/update-winget.sh
+++ b/scripts/update-winget.sh
@@ -24,7 +24,7 @@ echo "=== Updating WinGet manifest for ${PKG_ID} v${VERSION} ==="
# Compute SHA256
echo "Downloading and computing SHA256..."
-SHA256=$(curl -sL "${ZIP_URL}" | shasum -a 256 | cut -d' ' -f1 | tr 'a-f' 'A-F')
+SHA256=$(curl -fsSL "${ZIP_URL}" | shasum -a 256 | cut -d' ' -f1 | tr 'a-f' 'A-F')
if [ -z "${SHA256}" ]; then
echo "ERROR: Failed to compute SHA256. Is the release published?"
exit 1
diff --git a/scripts/verify-file-associations.sh b/scripts/verify-file-associations.sh
index d1cab6d66..cf97a1ecb 100755
--- a/scripts/verify-file-associations.sh
+++ b/scripts/verify-file-associations.sh
@@ -9,7 +9,7 @@ FAIL=0
check_contains() {
local file="$1"
local needle="$2"
- if ! grep -q "$needle" "$file"; then
+ if ! grep -Fq -- "$needle" "$file"; then
echo "verify-file-associations: missing '$needle' in $file" >&2
FAIL=1
fi
@@ -32,8 +32,9 @@ check_contains "$MIME" "application/x-ogre-mesh"
check_contains "$MIME" "application/vnd.ms-fbx"
echo "=== Windows packaging ==="
+check_contains "$ROOT/packaging/windows/QtMeshEditor.iss" "OpenWithProgids"
check_contains "$ROOT/packaging/windows/QtMeshEditor.iss" "QtMeshEditor.Model.fbx"
-check_contains "$ROOT/scripts/register-windows-file-associations.ps1" ".fbx"
+check_contains "$ROOT/scripts/register-windows-file-associations.ps1" "OpenWithProgids"
echo "=== Qt launch handler ==="
check_contains "$ROOT/src/AppLaunchHandler.h" "kServerName"
diff --git a/src/AppLaunchHandler.cpp b/src/AppLaunchHandler.cpp
index 39818b3f5..e4d185925 100644
--- a/src/AppLaunchHandler.cpp
+++ b/src/AppLaunchHandler.cpp
@@ -184,8 +184,11 @@ void AppLaunchHandler::handleIncomingPaths(const QStringList& paths)
continue;
accepted.append(info.absoluteFilePath());
}
- if (!accepted.isEmpty())
+ if (!accepted.isEmpty()) {
+ SentryReporter::addBreadcrumb(QStringLiteral("app.launch.file_open"),
+ QStringLiteral("Received %1 file(s) via launch handler").arg(accepted.size()));
emit filesRequested(accepted);
+ }
}
bool AppLaunchHandler::eventFilter(QObject* watched, QEvent* event)
@@ -194,6 +197,8 @@ bool AppLaunchHandler::eventFilter(QObject* watched, QEvent* event)
auto* openEvent = static_cast(event);
const QString path = openEvent->file();
if (!path.isEmpty() && isImportableMeshPath(path)) {
+ SentryReporter::addBreadcrumb(QStringLiteral("app.launch.file_open"),
+ QStringLiteral("macOS FileOpen: %1").arg(QFileInfo(path).fileName()));
handleIncomingPaths({QFileInfo(path).absoluteFilePath()});
return true;
}
diff --git a/src/main.cpp b/src/main.cpp
index 52e5dd57b..fceedaa40 100755
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -233,6 +233,13 @@ int main(int argc, char *argv[])
return 0;
launchHandler.startSingleInstanceServer();
+ // Buffer OS file requests until MainWindow exists (welcome dialog is modal).
+ QStringList queuedLaunchPaths = launchPaths;
+ QObject::connect(&launchHandler, &AppLaunchHandler::filesRequested, &a,
+ [&queuedLaunchPaths](const QStringList& paths) {
+ queuedLaunchPaths.append(paths);
+ });
+
// Show welcome dialog before creating MainWindow (skip when OS opened a file)
QString welcomeOpenFile;
bool welcomeNewScene = false;
@@ -254,12 +261,13 @@ int main(int argc, char *argv[])
MainWindow w;
w.show();
+ QObject::disconnect(&launchHandler, &AppLaunchHandler::filesRequested, nullptr, nullptr);
QObject::connect(&launchHandler, &AppLaunchHandler::filesRequested, &w,
&MainWindow::openLaunchFiles);
- if (!launchPaths.isEmpty()) {
- QTimer::singleShot(0, &w, [&w, launchPaths]() {
- w.openLaunchFiles(launchPaths);
+ if (!queuedLaunchPaths.isEmpty()) {
+ QTimer::singleShot(0, &w, [&w, queuedLaunchPaths]() {
+ w.openLaunchFiles(queuedLaunchPaths);
});
} else if (!welcomeOpenFile.isEmpty()) {
QTimer::singleShot(0, &w, [&w, welcomeOpenFile]() {
diff --git a/website/src/DocsApp.jsx b/website/src/DocsApp.jsx
index a0f5b4f96..a8acee21c 100644
--- a/website/src/DocsApp.jsx
+++ b/website/src/DocsApp.jsx
@@ -214,22 +214,27 @@ cmake --build build --target QtMeshEditor -j4
Launching with a file path loads it in the GUI; if the app is already running, the existing
window is focused and the new file is imported (single-instance).
- Supported extensions
+ App import support
- .fbx, .glb, .gltf, .obj, .dae,
- .stl, .ply, .vrm, .3ds, QtMesh-native
- .mesh, PS1 .rsd/.tmd, and saved scenes
- .scene.glb/.scene.gltf.
+ The editor can import many formats via drag-and-drop or QtMeshEditor path/to/model.ext,
+ including .vrm, .3ds, and additional Assimp types beyond the OS registration
+ sets below.
- Per platform
+ Registered Open With handlers
- - macOS —
Info.plist declares CFBundleDocumentTypes;
- Homebrew cask runs lsregister after install so Finder sees handlers immediately.
- - Windows — Inno Setup installer (release) registers per-user handlers under
-
HKCU. Portable ZIP includes
- bin/scripts/register-windows-file-associations.ps1 for manual registration.
- - Linux —
.desktop + XDG MIME package in the .deb and Snap;
- postinstall runs update-mime-database and update-desktop-database.
+ - macOS —
.fbx, .glb/.gltf, .obj,
+ .dae, .stl, .ply, .vrm, .3ds,
+ .scene.glb/.scene.gltf, .mesh, .rsd, .tmd
+ via CFBundleDocumentTypes; Homebrew cask runs lsregister after install.
+ - Windows —
.fbx, .glb, .gltf, .obj,
+ .dae, .stl, .ply, .mesh, .rsd, .tmd
+ added to OpenWithProgids (not default-app takeover). Inno Setup installer on release;
+ portable ZIP includes bin/scripts/register-windows-file-associations.ps1.
+ - Linux —
.fbx, .glb/.gltf, .obj,
+ .dae, .stl, .ply, .mesh, .rsd, .tmd,
+ .scene.glb/.scene.gltf via .desktop + XDG MIME in
+ .deb and Snap; postinstall runs update-mime-database and
+ update-desktop-database.
CLI mode is unchanged: qtmesh info model.fbx still runs headless. Only
From 3e0d2a7787692031a348bf51dd7359dfa52bcb94 Mon Sep 17 00:00:00 2001
From: Fernando
Date: Tue, 9 Jun 2026 18:47:18 -0400
Subject: [PATCH 3/3] fix(file-assoc): repair deploy.yml YAML and Windows
script review items
Replace the heredoc in the Homebrew cask update step with echo lines so YAML parses correctly, and align the portable registration script with the Inno Setup ProgID format and bin layout.
Co-authored-by: Cursor
---
.github/workflows/deploy.yml | 14 ++++++-------
.../register-windows-file-associations.ps1 | 21 +++++++++++++++++--
scripts/verify-file-associations.sh | 1 +
3 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a248d31ef..073a9c5cf 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2098,13 +2098,13 @@ jobs:
# Ensure Finder picks up CFBundleDocumentTypes after install (#664).
if ! grep -q 'lsregister' "$CASK_FILE"; then
- cat >> "$CASK_FILE" <<'RUBY'
-
- postflight do
- system_command "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
- args: ["-f", "-R", "#{appdir}/QtMeshEditor.app"]
- end
-RUBY
+ {
+ echo ''
+ echo ' postflight do'
+ echo ' system_command "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",'
+ echo ' args: ["-f", "-R", "#{appdir}/QtMeshEditor.app"]'
+ echo ' end'
+ } >> "$CASK_FILE"
fi
echo "Updated cask file:"
diff --git a/scripts/register-windows-file-associations.ps1 b/scripts/register-windows-file-associations.ps1
index 84c7a3a80..95b2387f2 100644
--- a/scripts/register-windows-file-associations.ps1
+++ b/scripts/register-windows-file-associations.ps1
@@ -5,10 +5,27 @@
# pwsh -File scripts/register-windows-file-associations.ps1 -BinDir "C:\path\to\bin"
param(
- [string]$BinDir = (Join-Path $PSScriptRoot "..\bin")
+ [string]$BinDir
)
$ErrorActionPreference = "Stop"
+
+function Resolve-QtMeshEditorBinDir {
+ param([string]$Requested)
+ if ($Requested) { return $Requested }
+
+ $portableBin = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..") -ErrorAction SilentlyContinue).Path
+ $repoBin = Join-Path $PSScriptRoot "..\bin"
+ if ($portableBin -and (Test-Path (Join-Path $portableBin "QtMeshEditor.exe"))) {
+ return $portableBin
+ }
+ if (Test-Path (Join-Path $repoBin "QtMeshEditor.exe")) {
+ return (Resolve-Path -LiteralPath $repoBin).Path
+ }
+ return $repoBin
+}
+
+$BinDir = Resolve-QtMeshEditorBinDir -Requested $BinDir
$exe = Join-Path $BinDir "QtMeshEditor.exe"
if (-not (Test-Path $exe)) {
Write-Error "QtMeshEditor.exe not found at $exe"
@@ -30,7 +47,7 @@ $extensions = @{
foreach ($entry in $extensions.GetEnumerator()) {
$ext = $entry.Key
$label = $entry.Value
- $progId = "QtMeshEditor.Model$($ext.Replace('.', ''))"
+ $progId = "QtMeshEditor.Model.$($ext.TrimStart('.'))"
$openWithKey = "HKCU:\Software\Classes\$ext\OpenWithProgids"
New-Item -Path $openWithKey -Force | Out-Null
New-ItemProperty -Path $openWithKey -Name $progId -PropertyType String -Value "" -Force | Out-Null
diff --git a/scripts/verify-file-associations.sh b/scripts/verify-file-associations.sh
index cf97a1ecb..800fb4fb4 100755
--- a/scripts/verify-file-associations.sh
+++ b/scripts/verify-file-associations.sh
@@ -35,6 +35,7 @@ echo "=== Windows packaging ==="
check_contains "$ROOT/packaging/windows/QtMeshEditor.iss" "OpenWithProgids"
check_contains "$ROOT/packaging/windows/QtMeshEditor.iss" "QtMeshEditor.Model.fbx"
check_contains "$ROOT/scripts/register-windows-file-associations.ps1" "OpenWithProgids"
+check_contains "$ROOT/scripts/register-windows-file-associations.ps1" "QtMeshEditor.Model."
echo "=== Qt launch handler ==="
check_contains "$ROOT/src/AppLaunchHandler.h" "kServerName"