Skip to content

Improve crop#100

Merged
AlanRockefeller merged 4 commits into
mainfrom
test
Jun 19, 2026
Merged

Improve crop#100
AlanRockefeller merged 4 commits into
mainfrom
test

Conversation

@AlanRockefeller

@AlanRockefeller AlanRockefeller commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced persistence and tracking of stack end markers across all stack operations
  • Bug Fixes

    • Improved crop and rotation tool stability with better pivot alignment
    • Fixed "Jump to Last Uploaded" menu behavior to properly close menus
    • Enhanced version comparison logic for software update detection
  • Documentation

    • Clarified keyboard shortcut help text for stack marking functions

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AlanRockefeller, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b5cdca1-7785-4aeb-bf76-f1d4674b7e7c

📥 Commits

Reviewing files that changed from the base of the PR and between 0daad9f and daf253f.

📒 Files selected for processing (2)
  • faststack/app.py
  • faststack/qml/Components.qml

Walkthrough

This PR adds full stack_end_index tracking across AppController operations (init, define, duplicate, delete, undo, rollback, sort/dir reset, and UI labeling), refactors the QML crop overlay from image-local to viewport-space coordinates with new mapping helpers, and includes minor fixes for menu deferral and version-string normalization.

Changes

Stack End Index Tracking

Layer / File(s) Summary
Data contract and state initialization
faststack/deletion_types.py, faststack/app.py
UIStateRestoration gains optional saved_stack_end_index; AppController initializes stack_end_index at construction and clears it in clear_all_stacks and _switch_to_directory.
Stack definition helper and UI labeling
faststack/app.py, faststack/qml/Main.qml
New _define_pending_stack() helper centralizes finalization (validate, persist, emit signals, post status, clear markers); _get_stack_info returns "Stack End Marked" label; key-binding text updated accordingly.
Delete / undo stack_end_index lifecycle
faststack/app.py
_delete_indices captures pre_stack_end_snapshot, shifts or clears stack_end_index after deletions, and stores it in UIStateRestoration; undo_delete and _rollback_ui_items restore it on undo/rollback.
Duplicate shift and migration skip
faststack/app.py
duplicate_current_image shifts stack_end_index when an insertion lands at or before it; jump_to_last_uploaded passes migrate=False to skip per-entry legacy sidecar migration during the scan.

Crop Overlay Viewport-Space Refactor

Layer / File(s) Summary
Viewport-space cropOverlay component
faststack/qml/Components.qml
New cropOverlay lives under the root viewport and provides cropViewRectForBox(), cropBoxFromViewRect(), dimension-swap detection, clamped dimmer rectangles, and crop frame/handles; old image-local overlay is removed.
Rotation pivot helpers and geometry freezing
faststack/qml/Components.qml
mainImage uses an explicit Rotation transform with cropRotationOriginX/Y helpers; updateRotatorGeometry freezes AABB to base dimensions during crop mode and skips recomputeFitScale while cropping.
Crop state and reset helpers
faststack/qml/Components.qml
mainMouseArea gains viewport-crop start properties and cropRotationPivotX/Y; resetCropRotation() centralizes rotation reset; all crop-exit paths call it. setCropBoxStartFromBox and beginNewCrop seed viewport-space start positions.
Mouse hit-testing and drag rewrite
faststack/qml/Components.qml
onPressed rotate-knob detection uses cropOverlay.cropViewRectForBox with DPI-scaled tolerance; edge/corner detection uses viewport viewPoint; move/resize seeds both normalized and viewport-space start; rotation angle center uses cropOverlay mapping.
updateCropBox viewport-space rewrite
faststack/qml/Components.qml
updateCropBox maps points through cropOverlay.mapFromItem, enforces min extents in view space, and writes currentCropBox via cropBoxFromViewRect for both constrained and freeform branches.

Minor Standalone Fixes

Layer / File(s) Summary
Menu defer fix and version normalization
faststack/qml/Main.qml, faststack/updater.py
"Jump to Last Uploaded" closes submenus and defers via Qt.callLater; updater.py adds BUILD_SUFFIX_RE and strips SemVer + metadata before build-suffix removal in normalize_version.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AlanRockefeller/faststack#47: Modifies deletion_types.py and app.py delete/undo restoration paths to persist stack_start_index; this PR extends the same pattern to stack_end_index.
  • AlanRockefeller/faststack#71: Substantially modifies Components.qml crop/rotate interaction flow, including crop overlay and image-source handling, overlapping directly with the viewport-space crop refactor here.
  • AlanRockefeller/faststack#92: Modifies Components.qml crop/rotate cancel and ESC handling and crop-mode state/rendering, which intersect with the resetCropRotation and crop-exit path changes in this PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% 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 'Improve crop' is vague and does not adequately describe the substantial changes across multiple files and systems. Provide a more specific title that captures the main scope of changes, such as 'Refactor crop overlay and stack end index tracking' or 'Fix crop mode coordinate mapping and stack persistence'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 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.

@AlanRockefeller

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0daad9f919

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1121 to +1122
cropRotationPivotX = ((box[0] + box[2]) / 2000)
cropRotationPivotY = ((box[1] + box[3]) / 2000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep straighten preview aligned with saved crop

When the crop box is off-center and the user straightens, moving the QML rotation origin to the crop center makes the image rotate around a different point than the saved render: execute_crop ultimately uses the editor mapping that rotates content around the source center (_rotated_content_point subtracts src_w / 2 and src_h / 2). The fixed viewport crop frame will therefore preview different pixels than the crop that gets committed, so off-center straighten+crop can save the wrong region.

Useful? React with 👍 / 👎.

Comment thread faststack/app.py
Comment on lines +4446 to +4450
meta = self.sidecar.get_metadata(
img.path,
create=False,
migrate=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve metadata migration for jump action

For folders with older sidecars that still store uploaded flags under legacy keys, this lookup now returns None instead of letting get_metadata migrate/find the entry, so “Jump to Last Uploaded” can report no upload or jump to an earlier image even though an uploaded image exists. The sidecar API reserves migrate=False for bulk grid-style reads, while this is a user action that depends on accurate flags.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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.

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 (3)
faststack/app.py (1)

7805-7806: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Insert partial rollbacks at shifted positions.

_rollback_ui_items() can receive only failed items while earlier deleted items remain removed. In that case inserting at the original idx places restored items too far right, while the stack/batch restoration below shifts indices as if the list were compressed.

🐛 Proposed fix
-        for idx, img in sorted(items, key=lambda x: x[0]):
-            self.image_files.insert(min(idx, len(self.image_files)), img)
+        restored_original_indices = {idx for idx, _ in items}
+        still_deleted = sorted(
+            {idx for idx, _ in job.removed_items} - restored_original_indices
+        )
+        for idx, img in sorted(items, key=lambda x: x[0]):
+            insert_idx = idx - sum(1 for d in still_deleted if d < idx)
+            self.image_files.insert(min(insert_idx, len(self.image_files)), img)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@faststack/app.py` around lines 7805 - 7806, In the _rollback_ui_items()
method around the loop that processes sorted items, the issue is that inserting
at the original idx position doesn't account for items that may have already
been deleted from earlier rollbacks. Instead of inserting directly at min(idx,
len(self.image_files)), calculate the effective insertion position by counting
how many items with indices less than idx are still missing from
self.image_files, then subtract that count from idx to get the correct shifted
position where the item should be reinserted into the current compressed list
state.
faststack/qml/Components.qml (2)

906-914: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop stale rotation timer updates when resetting crop state.

resetCropRotation() is used from index/source/crop-exit paths, but a running rotationThrottleTimer can still call set_straighten_angle(pendingRotation, ...) after the reset. Stop the queued update in the reset helper so a stale angle cannot be applied to the next crop/current image.

Proposed fix
 function resetCropRotation() {
+    clearPendingRotation(0)
     cropRotation = 0
     cropRotationPivotX = 0.5
     cropRotationPivotY = 0.5
 }

Also applies to: 927-931

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@faststack/qml/Components.qml` around lines 906 - 914, The
rotationThrottleTimer can continue executing after resetCropRotation() is
called, causing stale rotation updates to be applied. In the resetCropRotation()
function, explicitly stop the rotationThrottleTimer by calling its stop() method
to prevent the onTriggered handler from executing any pending
set_straighten_angle() calls after the crop state has been reset. This ensures
stale angle values cannot be applied to the next crop or current image.

1109-1147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t skip crop hit-testing after a non-knob click in rotate mode.

The else if (isFullImage) binds to the rotate-knob if. When rotate mode is active but the click misses the knob, no crop branch sets cropDragMode, then beginCropInteraction() starts a drag with "none" or stale state. Make the normal crop hit-test run after the optional knob check unless the knob path returned.

Proposed fix
-                // If crop box is full image, always start a new crop
-                else if (isFullImage) {
+                // If crop box is full image, always start a new crop
+                if (isFullImage) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@faststack/qml/Components.qml` around lines 1109 - 1147, The normal crop
hit-testing logic starting with the `else if (isFullImage)` check is incorrectly
nested as an `else` branch of the rotate-knob check in the `if
(mainMouseArea.isRotating && cropOverlay.visible && rotateKnob.visible)` block.
This causes the normal crop logic to be skipped entirely when rotate mode is
active but the knob is not clicked. Move the `else if (isFullImage)` block and
the subsequent `else if (inside)` checks outside of the `else` relationship so
they execute independently after the rotate-knob check completes, unless the
rotate-knob path has already returned. This ensures crop hit-testing always runs
when a knob click is missed in rotate mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@faststack/qml/Components.qml`:
- Around line 619-623: The aspect ratio constraint in
applyAspectRatioConstraint() is not being adjusted when dimensions are swapped
by _dimensionsAreSwapped(). When rectW and rectH are swapped in the
cropViewRectForBox() method (and also in the location referenced at lines
1456-1460), you need to also swap the aspect ratio constraint values (such as
the pixelAspectRatio or related aspect ratio variables) to ensure the fixed
aspect ratio is maintained after the dimension swap. This prevents a 16:9 aspect
ratio from inverting to 9:16 when _dimensionsAreSwapped() becomes true. Apply
this fix wherever dimensions are being swapped and aspect ratio constraints are
involved.

---

Outside diff comments:
In `@faststack/app.py`:
- Around line 7805-7806: In the _rollback_ui_items() method around the loop that
processes sorted items, the issue is that inserting at the original idx position
doesn't account for items that may have already been deleted from earlier
rollbacks. Instead of inserting directly at min(idx, len(self.image_files)),
calculate the effective insertion position by counting how many items with
indices less than idx are still missing from self.image_files, then subtract
that count from idx to get the correct shifted position where the item should be
reinserted into the current compressed list state.

In `@faststack/qml/Components.qml`:
- Around line 906-914: The rotationThrottleTimer can continue executing after
resetCropRotation() is called, causing stale rotation updates to be applied. In
the resetCropRotation() function, explicitly stop the rotationThrottleTimer by
calling its stop() method to prevent the onTriggered handler from executing any
pending set_straighten_angle() calls after the crop state has been reset. This
ensures stale angle values cannot be applied to the next crop or current image.
- Around line 1109-1147: The normal crop hit-testing logic starting with the
`else if (isFullImage)` check is incorrectly nested as an `else` branch of the
rotate-knob check in the `if (mainMouseArea.isRotating && cropOverlay.visible &&
rotateKnob.visible)` block. This causes the normal crop logic to be skipped
entirely when rotate mode is active but the knob is not clicked. Move the `else
if (isFullImage)` block and the subsequent `else if (inside)` checks outside of
the `else` relationship so they execute independently after the rotate-knob
check completes, unless the rotate-knob path has already returned. This ensures
crop hit-testing always runs when a knob click is missed in rotate mode.
🪄 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: 857659ed-8b0a-4709-bf72-ef8034cc0c8a

📥 Commits

Reviewing files that changed from the base of the PR and between 0a82c3c and 0daad9f.

📒 Files selected for processing (5)
  • faststack/app.py
  • faststack/deletion_types.py
  • faststack/qml/Components.qml
  • faststack/qml/Main.qml
  • faststack/updater.py

Comment thread faststack/qml/Components.qml
@AlanRockefeller AlanRockefeller merged commit ee24f7b into main Jun 19, 2026
5 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