Skip to content

Add QGIS 4.x (Qt6) support while keeping QGIS 3.22+ compatibility - #99

Merged
samhashemi merged 4 commits into
mainfrom
claude/clever-faraday-l1g3fg
Jun 13, 2026
Merged

Add QGIS 4.x (Qt6) support while keeping QGIS 3.22+ compatibility#99
samhashemi merged 4 commits into
mainfrom
claude/clever-faraday-l1g3fg

Conversation

@samhashemi

@samhashemi samhashemi commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Manual testing plan — ✅ complete on both QGIS 3.x and QGIS 4

Verified on a QGIS 3.4x install (Qt5) and a QGIS 4.0+ install (Qt6), with the f62f2c1 build:

  • QGIS 3.x — all steps verified by @makella (results)
  • QGIS 4.0+ — all steps verified by @makella (results): install, menu/toolbar, sign-in, map creation with categorized/graduated/heatmap vector layers + raster, upload + Open Map, styling fidelity on the Felt side, recent maps, workspaces, log out/re-login, clean Python console.
  • Raster color-ramp question from part 1 confirmed not a regressionmain and this branch produce byte-identical raster FSL for all supported renderer types (details). Pre-existing behavior, to be filed as a separate issue.

Earlier manual testing on QGIS 4 caught two Qt6-only bugs the unit suite can't reach — an endless repaint loop when opening the dialog (Qt.transparent, fixed in 84fea26) and every upload failing (QByteArray.append(str), fixed in f62f2c1). A follow-up audit (static sweep for all known PyQt6 behavior changes + scripted render/click probes of every GUI component inside a QGIS 4.0.3 container) found no further issues, and a regression test now covers the upload request builder.

Original step-by-step test plan (for future releases)
  1. Install: install the built zip via Plugins → Manage and Install Plugins → Install from ZIP. Loads with no Python errors, not flagged incompatible.
  2. Menu/toolbar: Web → Add to Felt menu and the Share Map to Felt toolbar button appear.
  3. Sign in: authorize dialog renders, browser OAuth completes, dialog reflects the signed-in account.
  4. Create a map: from a project with a few vector layers (include categorized, graduated and heatmap styling) plus a raster layer, run Add to Felt. Progress bar advances, upload completes, Open Map opens the map in the browser.
  5. Styling fidelity: on the resulting Felt map, the heatmap gradient and vector colors/line styles match QGIS.
  6. Recent maps: the dialog's recent maps list shows thumbnails, hover/selection works, picking an existing map works.
  7. Workspaces: the workspace dropdown populates and switching updates the recent maps list.
  8. Log out / re-login: Log Out from the settings menu works; signing back in works.
  9. No new warnings/exceptions in the QGIS Python console / message log throughout.

Summary

QGIS 4.0 is Qt6/PyQt6-based and breaks plugins that use unscoped Qt enums or removed Qt5 APIs. This PR updates the plugin so a single codebase runs on everything from QGIS 3.22 through QGIS 4.x, and marks it "QGIS 4 ready" in the plugin repository.

Code changes

  • Fully scoped Qt enums (~190 call sites): Qt.UserRoleQt.ItemDataRole.UserRole, Qt.NoPenQt.PenStyle.NoPen, QDialogButtonBox.OkQDialogButtonBox.StandardButton.Ok, etc. — including the lowercase Qt.GlobalColor members (Qt.transparentQt.GlobalColor.transparent) used in paint paths. The scoped forms work identically on PyQt5 and PyQt6. QGIS API enums (QgsUnitTypes.RenderPoints, QgsWkbTypes.Point, …) are unchanged — verified by introspection in a QGIS 4.0.3 container that the QGIS 4 bindings still support unscoped access for those.
  • Removed Qt5 APIs / behaviors: QFontMetrics.width()horizontalAdvance(), dialog exec_()exec(), QSvgWidget imported from QtSvgWidgets on Qt6 builds (with a fallback for QGIS 4.0.x, which doesn't wrap that module in qgis.PyQt yet), the one direct PyQt5 import now goes through qgis.PyQt, and the multipart upload body is built as bytes instead of QByteArray.append(str), which PyQt6 rejects.
  • metadata.txt: added qgisMaximumVersion=4.99 (what lists the plugin as QGIS 4 compatible — the old supportsQt6 flag is deprecated) and bumped the version to 1.1.0.

Test/CI changes

  • New unit test for create_upload_file_request (the multipart upload body builder) — previously only covered behind a live auth token, which is how the Qt6 upload bug slipped past CI.
  • The heatmap FSL conversion test now has Qt-version-aware expected colors: Qt6 rounds gradient interpolation slightly differently (e.g. #c3c3c3 vs #c4c4c4), and the converter output is correct on both.
  • test_suite.py now creates the QgsApplication before test discovery (on Qt6, widgets created at module import crash without an application instance), passes an explicit top_level_dir (required on newer Python for the test modules' relative imports), and adds a run_tests_and_exit() entry point that reports failures via exit code without running interpreter teardown (exiting a headless QgsApplication can segfault on Qt6, which would mask the result).
  • The test workflow matrix is now QGIS 3.22, 3.34, 3.40 LTR, 3.44 LTR and 4.0. Tests run headless via QT_QPA_PLATFORM=offscreen because the newer official Docker images never execute qgis --code scripts non-interactively (the old qgis_testrunner.sh flow hangs on them).

Verification

Full suite (69 tests) passes with the exact CI command in qgis/qgis:release-3_22 (Qt5), ltr/3.44 (Qt5) and 4.0/4.0.3 (Qt6) Docker images; flake8, pycodestyle and pylint clean/at baseline. CI is green across the whole matrix.

Note: this overlaps with the update-for-qgis4 branch from March — this PR additionally keeps QGIS 3.22 compatibility, so users on older QGIS continue to get updates.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw


Generated by Claude Code

QGIS 4.0 is Qt6/PyQt6-based, and the qgis.PyQt compatibility layer no
longer allows unscoped Qt enum access or removed Qt5 APIs. Update the
plugin so a single codebase runs on QGIS 3.22 through 4.x:

- Use fully scoped Qt enums everywhere (e.g. Qt.ItemDataRole.UserRole,
  Qt.PenStyle.NoPen, QPalette.ColorRole.Base), which work identically
  on PyQt5 and PyQt6. QGIS API enums are unchanged, as the QGIS 4
  bindings still support unscoped access for them.
- Replace Qt5-only APIs: QFontMetrics.width() -> horizontalAdvance(),
  dialog exec_() -> exec(), and import QSvgWidget from QtSvgWidgets on
  Qt6 builds (with fallback for QGIS 4 releases that don't wrap it).
- Replace the one direct PyQt5 import with qgis.PyQt.
- metadata.txt: add qgisMaximumVersion=4.99 so the plugin is marked as
  QGIS 4 ready, and bump version to 1.1.0.
- Make the heatmap FSL conversion test accept Qt6's slightly different
  gradient colour interpolation rounding.
- Test harness fixes for newer QGIS/Python: create the QgsApplication
  before test discovery (widgets created at import time crash on Qt6
  without an application), pass an explicit top level directory to
  unittest discovery, and add a run_tests_and_exit() entry point that
  reports failures through the exit code without running interpreter
  teardown (exiting a headless QgsApplication can crash on Qt6).
- CI: test against QGIS 3.22, 3.34, 3.40 LTR, 3.44 LTR and 4.0 docker
  images, running the suite headless with QT_QPA_PLATFORM=offscreen
  (the qgis_testrunner.sh harness no longer executes scripts on the
  newer images).

Verified: full test suite passes in qgis/qgis:release-3_22, ltr (3.44)
and 4.0 (4.0.3, Qt6) docker images; flake8/pycodestyle/pylint clean.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Plugin ready!

A test version of this PR is available for testing here.

(Built from commit f62f2c1)

claude added 3 commits June 12, 2026 06:55
…ions

The newer qgis/qgis images (3.40/3.44/4.0) exit immediately when run
detached without a TTY, so pass -t to docker run. Also update the lint
workflow off actions/setup-python@v1 with Python 3.9, which is no
longer available on ubuntu-latest runners.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
Qt.GlobalColor members are lowercase so the enum scoping sweep missed
them, and unscoped Qt.transparent raises AttributeError on PyQt6. The
failure only occurs in paint paths (dialog header and recent map
thumbnails), which unit tests don't reach, and repeated paint errors
make the Add to Felt dialog unusable on QGIS 4.

Verified by rendering the dialog and thumbnail compositing in both
QGIS 4.0 (Qt 6.9.2) and QGIS 3.44 (Qt 5.15.17) containers.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
The multipart upload body was built with QByteArray.append(str), which
PyQt6 rejects with TypeError, so every map upload failed on QGIS 4.
Build the body as Python bytes instead and wrap it in a QByteArray at
the end, which behaves identically on PyQt5.

Add a unit test covering create_upload_file_request so this path is
exercised by CI without network access.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
@makella

makella commented Jun 12, 2026

Copy link
Copy Markdown

Test plan part 1: 3.22+ compatibility

@samhashemi went through the first part since i had 3.4... installed already. to finish this test i need to try the raster styling step again with the last released plugin to make sure that it isn't a regression - see step 5 below.

then i'll do part 2 with the newer version of QGIS!

Testing with commit f62f2c1 zip download as the latest.

On QGIS 3.x

Screenshot 2026-06-12 at 1 16 33 PM
  • 1. Install: install the built zip via Plugins → Manage and Install Plugins → Install from ZIP. Loads with no Python errors, not flagged incompatible.
Screenshot 2026-06-12 at 1 18 26 PM
  • 2. Menu/toolbar: Web → Add to Felt menu and the Share Map to Felt toolbar button appear.
Screenshot 2026-06-12 at 1 21 10 PM
  • 3. Sign in: authorize dialog renders, browser OAuth completes, dialog reflects the signed-in account.
Screenshot 2026-06-12 at 1 23 04 PM Screenshot 2026-06-12 at 1 23 29 PM
  • 4. Create a map rom a project with a few vector layers (include categorized, graduated and heatmap styling) plus a raster layer,
Screenshot 2026-06-12 at 1 53 55 PM
  • run Add to Felt. Progress bar advances, upload completes, Open Map opens the map in the browser.
Screenshot 2026-06-12 at 1 54 17 PM
  • 5. Styling fidelity: on the resulting Felt map, the heatmap gradient and vector colors/line styles match QGIS (exercises the FSL conversion paths touched here).

    • resulting map - @samhashemi here i need to check why the raster styling didn't carry through... i will try the previous plugin to see if it does before determining if this is a regression. it is coming in as an image instead of color range. all other styles ARE preserved.
  • 6. Recent maps: the dialog's recent maps list shows thumbnails, hover/selection works, picking an existing map works.

Screenshot 2026-06-12 at 2 16 49 PM
  • 7. Workspaces: the workspace dropdown populates and switching updates the recent maps list.
Screenshot 2026-06-12 at 2 18 04 PM
  • 8. Log out / re-login: Log Out from the settings menu works; signing back in works.
  • 9. No new warnings/exceptions in the QGIS Python console / message log throughout.

Copy link
Copy Markdown
Collaborator Author

@makella thanks for the thorough run-through! On the step 5 raster question — it's not a regression from this PR, so no need to re-test with the previous plugin release unless you want to confirm independently.

To verify, I ran an A/B comparison inside a QGIS 3.44 container: the same DEM raster styled with each supported renderer (singleband pseudocolor interpolated/discrete/exact, singleband gray, paletted), converted to FSL by this branch and by main. The output is byte-for-byte identical, and all five renderer types produce proper FSL color ramps on both versions. This PR's changes to the raster path are pure enum-scoping with no logic change.

What likely happened instead: the plugin only converts single-band renderers to Felt color-ramp styling. If a raster uses anything else (multiband RGB, hillshade, contrast-enhancement-only defaults, etc.), raster_renderer_to_fsl returns nothing and the plugin intentionally falls back to uploading a styled image — behavior that's the same on main. Worth checking which renderer the layer panel shows for that raster (Symbology → Render type); if it's "Singleband pseudocolor" and it still came through as an image, that would be a pre-existing bug worth its own issue rather than part of this PR.

https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw


Generated by Claude Code

@makella

makella commented Jun 13, 2026

Copy link
Copy Markdown

Test plan part 2: QGIS 4.0+

Screenshot 2026-06-12 at 5 25 58 PM
  • 1. Install: install the built zip via Plugins → Manage and Install Plugins → Install from ZIP. Loads with no Python errors, not flagged incompatible.
Screenshot 2026-06-12 at 5 15 15 PM
  • 2. Menu/toolbar: Web → Add to Felt menu and the Share Map to Felt toolbar button appear.

  • 3. Sign in: authorize dialog renders, browser OAuth completes, dialog reflects the signed-in account.

Screenshot 2026-06-12 at 5 16 17 PM Screenshot 2026-06-12 at 5 16 32 PM Screenshot 2026-06-12 at 5 16 54 PM
  • 5. Create a map from a project with a few vector layers (include categorized, graduated and heatmap styling) plus a raster layer
Screenshot 2026-06-12 at 5 26 20 PM
  • run Add to Felt. Progress bar advances,
Screenshot 2026-06-12 at 5 18 50 PM
  • upload completes, Open Map opens the map in the browser.
Screenshot 2026-06-12 at 5 19 07 PM
  • 6. Styling fidelity: on the resulting Felt map, the heatmap gradient and vector colors/line styles match QGIS (exercises the FSL conversion paths touched here).

    • map link
    • @samhashemi yes seems to be that the raster is a different issue that we can file separately
  • 7. Recent maps: the dialog's recent maps list shows thumbnails, hover/selection works, picking an existing map works.

Screenshot 2026-06-12 at 5 23 36 PM
  • 8. Workspaces: the workspace dropdown populates and switching updates the recent maps list.
Screenshot 2026-06-12 at 5 24 18 PM
  • 9. Log out / re-login: Log Out from the settings menu works; signing back in works.
  • 10. No new warnings/exceptions in the QGIS Python console / message log throughout.

@makella
makella self-requested a review June 13, 2026 00:27

@makella makella left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran through test plan on qgis 3.42.1 and 4.0.3 both check out and pass. as noted in the comments, there may be a raster rendering fix and/or bug if symbolized with singleband pseudocolor but that is outside the scope of this PR.

@samhashemi
samhashemi merged commit 94b7cef into main Jun 13, 2026
32 checks passed
@samhashemi
samhashemi deleted the claude/clever-faraday-l1g3fg branch June 13, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants