Skip to content

Clean up QML#75

Merged
AlanRockefeller merged 2 commits intomainfrom
test
Apr 13, 2026
Merged

Clean up QML#75
AlanRockefeller merged 2 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Apr 12, 2026

Ran qmllint on all of the QML and cleaned up some of the warnings.

Summary by CodeRabbit

  • Bug Fixes

    • Safer dialog/window state handling and guarded UI bindings to reduce errors when optional state is missing
    • More reliable thumbnail/folder stats display by ensuring stable folder-stats data is available
  • New Features

    • Standardized menu action row component for improved menu visuals and interaction consistency
  • Tests

    • Added contract tests to validate thumbnail delegate vs. model compatibility

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Walkthrough

Refactors many QML components to use bound, component-scoped references (uiStateRef, controllerRef) and local ids, adds a new menu delegate, converts ThumbnailTile to required model-driven properties, ensures folderStats never returns null, and adds a contract test validating delegate↔model role compatibility.

Changes

Cohort / File(s) Summary
Dialog components (controllerRef)
faststack/qml/DeleteBatchDialog.qml, faststack/qml/ExifDialog.qml, faststack/qml/FilterDialog.qml, faststack/qml/JumpToImageDialog.qml
Added property var controllerRef: typeof controller !== "undefined" ? controller : null; routed dialog lifecycle and button actions through *.controllerRef with null guards; introduced local button ids for styling bindings.
Progress / Histogram / Tool windows (uiStateRef/controllerRef)
faststack/qml/BatchProgressDialog.qml, faststack/qml/HistogramWindow.qml, faststack/qml/DarkenToolPanel.qml
Added uiStateRef/controllerRef properties and switched bindings, Connections targets, visibility and action calls to use these refs with fallbacks and null checks; small UI id/pressed→down hover binding tweaks.
Main app and major dialogs (bound behavior & refs)
faststack/qml/Main.qml, faststack/qml/Components.qml, faststack/qml/ImageEditorDialog.qml, faststack/qml/SettingsDialog.qml
Enabled pragma ComponentBehavior: Bound, introduced component-scoped uiStateRef/controllerRef, rewired numerous bindings/handlers to use guarded refs, added helper functions (e.g., grid prefetch, loader helpers), standardized button ids and slider refactoring (slider metadata moved into model rows).
Grid & tiles (model-driven delegates)
faststack/qml/ThumbnailGridView.qml, faststack/qml/ThumbnailTile.qml
Added uiStateRef and thumbnailModelRef; converted ThumbnailTile.qml to declare many required property fields sourced from the model, retained mirror aliases, updated click/keyboard logic to use guarded uiStateRef, and removed redundant delegate role passthroughs.
New UI component
faststack/qml/MenuActionItem.qml
Introduced MenuActionItem (ItemDelegate) for standardized menu row styling and hover/selected/text color properties; used by Main.qml to replace custom delegates.
Model change
faststack/thumbnail_view/model.py
Added _empty_folder_stats_payload() and return that dict for missing folderStats role instead of None to provide stable QML payload.
Tests
faststack/tests/thumbnail_view/test_qml_delegate_contract.py
New pytest contract tests verifying ThumbnailTile.qml required properties exist as model roles and that ThumbnailModel returns non-null values (and a default folderStats payload) for synthetic rows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Clean up QML' is vague and generic, using non-descriptive language that doesn't convey the specific changes made (introducing bound component behavior, adding uiStateRef/controllerRef properties, refactoring bindings across multiple QML files). Use a more specific title that captures the main refactoring work, such as 'Refactor QML components to use bound properties for uiState and controller' or 'Add uiStateRef/controllerRef properties and fix qmllint warnings across QML files'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
faststack/qml/Components.qml (1)

514-521: ⚠️ Potential issue | 🟠 Major

Guard onDisplaySizeChanged() until uiStateRef is initialized.

This function is invoked from mainImage.Component.onCompleted, but uiStateRef is assigned later in loupeView.Component.onCompleted. In QML, child component completion handlers can fire before parent handlers, creating a startup race condition where loupeView.uiStateRef is still null when reportDisplaySize() executes.

Minimal fix
 function reportDisplaySize() {
-    if (imageViewport.width > 0 && imageViewport.height > 0) {
+    if (loupeView.uiStateRef && imageViewport.width > 0 && imageViewport.height > 0) {
         var dpr = Screen.devicePixelRatio
         loupeView.uiStateRef.onDisplaySizeChanged(
             Math.round(imageViewport.width * dpr),
             Math.round(imageViewport.height * dpr)
         )
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/qml/Components.qml` around lines 514 - 521, reportDisplaySize() can
run before loupeView.uiStateRef is initialized (called from
mainImage.Component.onCompleted while loupeView.Component.onCompleted sets
uiStateRef later), causing a null dereference; guard the call by checking that
loupeView.uiStateRef is non-null before invoking onDisplaySizeChanged (i.e., in
reportDisplaySize() verify loupeView.uiStateRef !== null/undefined and return or
defer if not initialized) and keep the Math.round(...) DPI math unchanged so the
update only runs when the uiStateRef is ready.
🧹 Nitpick comments (3)
faststack/qml/ExifDialog.qml (1)

21-21: This pattern is systemic—consider addressing across all affected QML files, not just ExifDialog.

Line 21 uses global symbol probing with typeof controller !== "undefined" ? controller : null. While this makes the fallback explicit, the same pattern appears in 10+ other QML files (ThumbnailTile, JumpToImageDialog, FilterDialog, HistogramWindow, DeleteBatchDialog, DarkenToolPanel, BatchProgressDialog, ThumbnailGridView). If refactoring to explicit parent injection, apply consistently across the codebase for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/qml/ExifDialog.qml` at line 21, The code uses global symbol probing
(property var controllerRef: typeof controller !== "undefined" ? controller :
null) which is fragile; replace this pattern with explicit parent injection: add
a required property on the root QML component (e.g., property var controllerRef)
and have parents pass controller when instantiating the component (or use
property alias to an id on the parent), update ExifDialog.qml (replace the
typeof check with a plain property var controllerRef) and apply the same change
to the other affected components (ThumbnailTile, JumpToImageDialog,
FilterDialog, HistogramWindow, DeleteBatchDialog, DarkenToolPanel,
BatchProgressDialog, ThumbnailGridView) to ensure consistent, maintainable
wiring of controller references across the codebase.
faststack/tests/thumbnail_view/test_qml_delegate_contract.py (1)

25-30: Make the QML contract scan less brittle.

This regex treats every required property anywhere in ThumbnailTile.qml as part of the model/delegate contract and only matches lines indented with exactly four spaces. A formatting-only cleanup or a nested helper delegate with its own required property will fail this test even when the actual ThumbnailTile contract is unchanged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/tests/thumbnail_view/test_qml_delegate_contract.py` around lines 25
- 30, The test _thumbnail_tile_required_properties currently uses a brittle
regex that only matches lines with exactly four spaces; update it to first
extract the ThumbnailTile component block from ThumbnailTile.qml (locate the
component start like "ThumbnailTile" or the root QML component and its matching
braces) and then run a looser regex such as r"^\s*required property \w+ (\w+)"
with MULTILINE on that extracted block so you only capture top-level required
properties for the tile delegate while accepting any indentation changes.
faststack/qml/ThumbnailTile.qml (1)

31-50: Optional follow-up: collapse the mirror layer.

This works for the lint cleanup, but the tile* proxy properties now duplicate the entire delegate contract. A later pass could bind the UI directly to the required roles to remove one maintenance layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/qml/ThumbnailTile.qml` around lines 31 - 50, ThumbnailTile.qml
currently duplicates the entire delegate contract via the tile* proxy properties
(e.g., tileIndex, tileFilePath, tileIsFolder, tileThumbnailSource, etc.);
collapse this mirror layer by removing the redundant tile* properties and rewire
the UI bindings to the original delegate properties (index, filePath, fileName,
isFolder, isStacked, isUploaded, isEdited, isRestacked, isFavorite, isTodo,
isInBatch, isCurrent, thumbnailSource, folderStats, isSelected, isParentFolder,
hasBackups, hasDeveloped), or keep only the small subset of tile* proxies
actually used and update consumers to bind directly to the original names so the
component no longer maintains a duplicated contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/qml/SettingsDialog.qml`:
- Around line 671-679: The SpinBox value is only initialized inside
prefetchRadiusLoader.onLoaded, so reopening the dialog can show a stale control
value; update the loader's "value" whenever the dialog is opened or when
settingsDialog.prefetchRadius changes. Modify code that opens the dialog (or add
an onVisible/onOpened handler on settingsDialog) to call
settingsDialog.setLoaderProperty(prefetchRadiusLoader, "value",
settingsDialog.prefetchRadius) unconditionally, and/or add a binding or observer
from settingsDialog.prefetchRadius to call setLoaderProperty so the
styledSpinBox (via prefetchRadiusLoader) is always synced; keep the existing
connectLoaderSignal("valueChanged") handler to write back into
settingsDialog.prefetchRadius.

---

Outside diff comments:
In `@faststack/qml/Components.qml`:
- Around line 514-521: reportDisplaySize() can run before loupeView.uiStateRef
is initialized (called from mainImage.Component.onCompleted while
loupeView.Component.onCompleted sets uiStateRef later), causing a null
dereference; guard the call by checking that loupeView.uiStateRef is non-null
before invoking onDisplaySizeChanged (i.e., in reportDisplaySize() verify
loupeView.uiStateRef !== null/undefined and return or defer if not initialized)
and keep the Math.round(...) DPI math unchanged so the update only runs when the
uiStateRef is ready.

---

Nitpick comments:
In `@faststack/qml/ExifDialog.qml`:
- Line 21: The code uses global symbol probing (property var controllerRef:
typeof controller !== "undefined" ? controller : null) which is fragile; replace
this pattern with explicit parent injection: add a required property on the root
QML component (e.g., property var controllerRef) and have parents pass
controller when instantiating the component (or use property alias to an id on
the parent), update ExifDialog.qml (replace the typeof check with a plain
property var controllerRef) and apply the same change to the other affected
components (ThumbnailTile, JumpToImageDialog, FilterDialog, HistogramWindow,
DeleteBatchDialog, DarkenToolPanel, BatchProgressDialog, ThumbnailGridView) to
ensure consistent, maintainable wiring of controller references across the
codebase.

In `@faststack/qml/ThumbnailTile.qml`:
- Around line 31-50: ThumbnailTile.qml currently duplicates the entire delegate
contract via the tile* proxy properties (e.g., tileIndex, tileFilePath,
tileIsFolder, tileThumbnailSource, etc.); collapse this mirror layer by removing
the redundant tile* properties and rewire the UI bindings to the original
delegate properties (index, filePath, fileName, isFolder, isStacked, isUploaded,
isEdited, isRestacked, isFavorite, isTodo, isInBatch, isCurrent,
thumbnailSource, folderStats, isSelected, isParentFolder, hasBackups,
hasDeveloped), or keep only the small subset of tile* proxies actually used and
update consumers to bind directly to the original names so the component no
longer maintains a duplicated contract.

In `@faststack/tests/thumbnail_view/test_qml_delegate_contract.py`:
- Around line 25-30: The test _thumbnail_tile_required_properties currently uses
a brittle regex that only matches lines with exactly four spaces; update it to
first extract the ThumbnailTile component block from ThumbnailTile.qml (locate
the component start like "ThumbnailTile" or the root QML component and its
matching braces) and then run a looser regex such as r"^\s*required property \w+
(\w+)" with MULTILINE on that extracted block so you only capture top-level
required properties for the tile delegate while accepting any indentation
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2f0bdec-08e7-4412-b4b6-a9911270a138

📥 Commits

Reviewing files that changed from the base of the PR and between 04164a8 and b13432d.

📒 Files selected for processing (16)
  • faststack/qml/BatchProgressDialog.qml
  • faststack/qml/Components.qml
  • faststack/qml/DarkenToolPanel.qml
  • faststack/qml/DeleteBatchDialog.qml
  • faststack/qml/ExifDialog.qml
  • faststack/qml/FilterDialog.qml
  • faststack/qml/HistogramWindow.qml
  • faststack/qml/ImageEditorDialog.qml
  • faststack/qml/JumpToImageDialog.qml
  • faststack/qml/Main.qml
  • faststack/qml/MenuActionItem.qml
  • faststack/qml/SettingsDialog.qml
  • faststack/qml/ThumbnailGridView.qml
  • faststack/qml/ThumbnailTile.qml
  • faststack/tests/thumbnail_view/test_qml_delegate_contract.py
  • faststack/thumbnail_view/model.py

Comment thread faststack/qml/SettingsDialog.qml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
faststack/tests/thumbnail_view/test_qml_delegate_contract.py (1)

34-36: Add strict=True to zip() call to catch parser divergence.

The two sequences (root_body.splitlines() and sanitized_root_body.splitlines()) should always have equal length and line count, since _sanitize_qml() preserves character count and newlines. Adding strict=True makes this assumption explicit and will catch bugs if the sanitization or extraction logic ever breaks.

Proposed patch
-    for raw_line, sanitized_line in zip(
-        root_body.splitlines(), sanitized_root_body.splitlines()
-    ):
+    for raw_line, sanitized_line in zip(
+        root_body.splitlines(), sanitized_root_body.splitlines(), strict=True
+    ):

The project requires Python ≥3.11, which fully supports strict=True (available since 3.10).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/tests/thumbnail_view/test_qml_delegate_contract.py` around lines 34
- 36, The zip over root_body.splitlines() and sanitized_root_body.splitlines()
should use strict=True to fail fast on any length mismatch; update the zip(...)
call in the loop comparing root_body.splitlines() and
sanitized_root_body.splitlines() (in the test that validates _sanitize_qml
behavior) to zip(..., strict=True) so any parser/sanitizer divergence raises
immediately.
faststack/qml/SettingsDialog.qml (1)

986-998: Consider adding the same focus guard used by other SpinBox bindings.

awbTintBiasLoader keeps its value binding active while focused, unlike nearby controls. Aligning behavior can avoid edit-time churn and keep interaction consistent.

♻️ Suggested small consistency patch
 Binding {
     target: awbTintBiasLoader.item
     property: "value"
     value: settingsDialog.awbTintBias
-    when: awbTintBiasLoader.item
+    when: awbTintBiasLoader.item
+          && !settingsDialog.loaderProperty(awbTintBiasLoader, "activeFocus", false)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/qml/SettingsDialog.qml` around lines 986 - 998, The Binding on
awbTintBiasLoader.item.value keeps updating while the SpinBox is focused; mirror
other SpinBox bindings by adding a focus guard so the Binding is only active
when the control exists and is not focused. Update the Binding's when expression
to require awbTintBiasLoader.item && !awbTintBiasLoader.item.focus (or
activeFocus if your codebase uses that), and ensure any connectLoaderSignal
handler (the "valueChanged" callback that sets settingsDialog.awbTintBias)
continues to sync user edits while focused but does not re-enable the
programmatic binding until focus is lost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/qml/SettingsDialog.qml`:
- Around line 65-68: Remove the Component.onCompleted block that assigns uiState
and controller into settingsDialog (the lines where settingsDialog.uiStateRef =
uiState and settingsDialog.controllerRef = controller) and instead declare
defensive properties on settingsDialog using the typeof pattern so uiStateRef
and controllerRef default to null when context properties are absent (i.e., set
settingsDialog.uiStateRef and settingsDialog.controllerRef via typeof uiState
!== "undefined" ? uiState : null and typeof controller !== "undefined" ?
controller : null); this matches the pattern used by
JumpToImageDialog/HistogramWindow/FilterDialog/ExifDialog/DeleteBatchDialog and
ensures safe fallback without runtime assignment in Component.onCompleted.

---

Nitpick comments:
In `@faststack/qml/SettingsDialog.qml`:
- Around line 986-998: The Binding on awbTintBiasLoader.item.value keeps
updating while the SpinBox is focused; mirror other SpinBox bindings by adding a
focus guard so the Binding is only active when the control exists and is not
focused. Update the Binding's when expression to require awbTintBiasLoader.item
&& !awbTintBiasLoader.item.focus (or activeFocus if your codebase uses that),
and ensure any connectLoaderSignal handler (the "valueChanged" callback that
sets settingsDialog.awbTintBias) continues to sync user edits while focused but
does not re-enable the programmatic binding until focus is lost.

In `@faststack/tests/thumbnail_view/test_qml_delegate_contract.py`:
- Around line 34-36: The zip over root_body.splitlines() and
sanitized_root_body.splitlines() should use strict=True to fail fast on any
length mismatch; update the zip(...) call in the loop comparing
root_body.splitlines() and sanitized_root_body.splitlines() (in the test that
validates _sanitize_qml behavior) to zip(..., strict=True) so any
parser/sanitizer divergence raises immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47484926-651e-49a3-8953-3fbd5b604ba2

📥 Commits

Reviewing files that changed from the base of the PR and between b13432d and f977056.

📒 Files selected for processing (3)
  • faststack/qml/Components.qml
  • faststack/qml/SettingsDialog.qml
  • faststack/tests/thumbnail_view/test_qml_delegate_contract.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • faststack/qml/Components.qml

Comment on lines +65 to +68
Component.onCompleted: {
settingsDialog.uiStateRef = uiState
settingsDialog.controllerRef = controller
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where SettingsDialog is instantiated and opened
rg -n -C4 'SettingsDialog|id:\s*settingsDialog|settingsDialog\.open\(' faststack/qml

# 2) Find where uiState/controller are introduced and propagated
rg -n -C4 '\buiState\b|\bcontroller\b|uiStateRef|controllerRef' faststack/qml

Repository: AlanRockefeller/faststack

Length of output: 50382


🏁 Script executed:

head -80 faststack/qml/SettingsDialog.qml | tail -40

Repository: AlanRockefeller/faststack

Length of output: 1493


🏁 Script executed:

sed -n '120,180p' faststack/qml/SettingsDialog.qml

Repository: AlanRockefeller/faststack

Length of output: 3017


Use the safer typeof pattern for context property assignment like other dialogs.

Component.onCompleted assigns unqualified uiState and controller into settingsDialog.uiStateRef / settingsDialog.controllerRef (lines 65-68). While the code guards these refs at usage sites (lines 128, 161, 175), the assignment itself lacks the defensive typeof check used consistently by JumpToImageDialog, HistogramWindow, FilterDialog, ExifDialog, and DeleteBatchDialog. Replace with:

property var uiStateRef: typeof uiState !== "undefined" ? uiState : null
property var controllerRef: typeof controller !== "undefined" ? controller : null

Remove the Component.onCompleted block (lines 65-68) since context properties are available at declaration time. This matches the codebase pattern and ensures graceful fallback if context is unavailable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/qml/SettingsDialog.qml` around lines 65 - 68, Remove the
Component.onCompleted block that assigns uiState and controller into
settingsDialog (the lines where settingsDialog.uiStateRef = uiState and
settingsDialog.controllerRef = controller) and instead declare defensive
properties on settingsDialog using the typeof pattern so uiStateRef and
controllerRef default to null when context properties are absent (i.e., set
settingsDialog.uiStateRef and settingsDialog.controllerRef via typeof uiState
!== "undefined" ? uiState : null and typeof controller !== "undefined" ?
controller : null); this matches the pattern used by
JumpToImageDialog/HistogramWindow/FilterDialog/ExifDialog/DeleteBatchDialog and
ensures safe fallback without runtime assignment in Component.onCompleted.

@AlanRockefeller AlanRockefeller merged commit 7e9b51d into main Apr 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant