Skip to content

Conversation

@MrJarnould
Copy link

Summary

This PR addresses several issues preventing reliable Dock screenshots on macOS, specifically on Retina displays and systems with non-standard Dock configurations.

1. Hardcoded Dock Configuration

Problem: The previous implementation assumed the Dock was always on the bottom and set to autohide=True. This caused failures for users with the Dock on the left/right or always visible.

Solution:

  • Added get_dock_orientation(): Executes defaults read com.apple.dock orientation to detect if the Dock is left, right, or bottom.
  • Added get_dock_autohide(): Executes defaults read com.apple.dock autohide.
  • The DockCapture class now defaults to these system settings instead of hardcoded values.

2. Unreliable Dock Reveal

Problem: The mouse movement logic used a small offset (e.g., screen_width // 2, screen_height - 2), which often failed to trigger the Dock reveal animation reliably.

Solution:

  • Updated cursor logic to target the exact screen edge (e.g., pixel 0 or screen_height - 1).
  • Added a try/finally block to ensure the mouse cursor is restored to its original position even if capture fails.

3. Low-Resolution & Misaligned Screenshots

Problem:

  • capture_full_screen used a bbox argument which inadvertently caused ImageGrab to return a downscaled 1x image on Retina displays (logical points instead of physical pixels).
  • Hardcoded 2x scaling logic in crop_screenshot caused "double-cropping" (capturing empty space) when applied to these 1x images.

Solution:

  • Capture: Removed the bbox argument to enable native Retina (2x) resolution (e.g., 3024x1964).
  • Scaling: Refactored crop_screenshot and segment_image to dynamically calculate the scale factor (by comparing image pixels to screen points). This ensures correct cropping and bounding box alignment regardless of whether the source image is 1x or 2x.

Copy link
Contributor

@mshamrai mshamrai left a comment

Choose a reason for hiding this comment

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

Thank you for the PR! We really appreciate the collaboration.
Can you please elaborate on the change in scaling factor calculation?



def crop_screenshot(image_path, window_coords, output_path):
backing_scale_factor = AppKit.NSScreen.mainScreen().backingScaleFactor()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't you like this function? It should return the scaling factor, there is no need to compute it manually

Copy link
Author

Choose a reason for hiding this comment

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

I avoided backingScaleFactor because it introduces a critical bug in multi-monitor setups. NSScreen.mainScreen() returns the properties of the primary display (e.g., the MacBook), regardless of where the window is actually located.

I verified this with a physical test case using a Retina MacBook (Main, 2.0x) and an LG External Monitor (Extended, 1.0x).

  1. I moved the Calculator app to the external monitor (1.0x).
  2. mainScreen().backingScaleFactor() still returned 2.0 because the Mac is the primary display.
  3. The script applied 2x math to the 1.0x image, resulting in a broken/blank screenshot because the crop area was doubled and shifted out of bounds.

By calculating the scale manually, we ensure the math matches the actual monitor the window is on.

if scale_factor is None:
# Heuristic: Compare image width vs window element width (points) to detect 1x vs 2x.
# If explicit detection fails, fallback to the screen's backing scale factor.
current_scaling_factor = _screen_scaling_factor
Copy link
Contributor

Choose a reason for hiding this comment

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

So why don't you like the backing scale factor? Did you have problems with it?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, the problem is that backingScaleFactor is environment-dependent, whereas measuring the image is data-dependent (and therefore reliable).

Since macOS creates screenshots based on the target display's resolution, the image file itself is the source of truth. My implementation divides the Image Width (px) by the Window Width (pts) to derive the exact multiplier used for that specific capture.

  • Window on Retina: 460px / 230pts = 2.0
  • Window on External: 230px / 230pts = 1.0

This makes the logic display-agnostic. It works correctly even in mixed-DPI setups, Sidecar, or potentially headless CI environments where mainScreen might report default values that don't match the virtual framebuffer.

Copy link
Contributor

@mshamrai mshamrai left a comment

Choose a reason for hiding this comment

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

I am not sure, that your implementation will return needed value in the case you mention. Unless you find the screen where the window is.

And can you please create a function for the scale_factor calculation in order to reduce code duplicates? You may put it in window_tools.py file.

Much appreciated!



# Get main screen width (Points) to determine ratio
screen = AppKit.NSScreen.mainScreen()
Copy link
Contributor

Choose a reason for hiding this comment

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

But here you also use AppKit.NSScreen.mainScreen(), which returns the main screen. It looks like in this case your implementation would return the same value of the scale_factor

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the feedback. You're right. Comparing the image width to mainScreen in crop_screenshot was incorrect because it re-introduced the same fragility as backingScaleFactor (comparing a window object to a screen reference).

I pushed a refactor that addresses both points:

  1. Shared Helper: moved the scale calculation logic to a reusable function get_image_scale_factor in window_tools.py to remove the code duplication
  2. Logic Fix: In screenshot_app_window.py, updated the logic to pass the window's logical width as the reference target instead of the screen width
# New logic
scale_factor = get_image_scale_factor(img_width, window_width)

Ensures the ratio is calculated strictly as Window Pixels / Window Points which guarantees the correct scale (1.0 vs 2.0) regardless of which monitor the window is on.

Copy link
Contributor

@mshamrai mshamrai left a comment

Choose a reason for hiding this comment

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

Good! I think we almost done here. Only some refactoring remains

frame = screen.frame()
screen_width_points = frame.size.width
# Local import to avoid circular dependency
from macapptree.window_tools import get_image_scale_factor
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the local import is a good practice. Can you please refactor it to avoid local imports and circular dependencies? Maybe move it to a new file?

Copy link
Author

Choose a reason for hiding this comment

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

Done. Created scale_utils.py as a standalone module with no dependencies on window_tools or screenshot_app_window. Both files now import from it at the top level.

# But get_image_scale_factor handles logic.
# Let's check get_image_scale_factor implementation again.
# I changed it to return None instead of global _screen_scaling_factor because _screen_scaling_factor is not available here.
# So I need to import AppKit here to get fallback.
Copy link
Contributor

Choose a reason for hiding this comment

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

These comments look totally unnecessary

Copy link
Author

Choose a reason for hiding this comment

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

removed

# Let's check get_image_scale_factor implementation again.
# I changed it to return None instead of global _screen_scaling_factor because _screen_scaling_factor is not available here.
# So I need to import AppKit here to get fallback.
scale_factor = AppKit.NSScreen.mainScreen().backingScaleFactor()
Copy link
Contributor

Choose a reason for hiding this comment

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

And this logic is already incapsulated in the function, so I think you should not have it here

Copy link
Author

Choose a reason for hiding this comment

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

removed. The fallback is now handled inside get_image_scale_factor() so callers don't need to check for None.

"""
if target_width_points > 0:
ratio = image_width_px / target_width_points
if abs(ratio - 1.0) < 0.1:
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please introduce new eps input variable with the default 0.1 instead of using constant as is?

Copy link
Author

Choose a reason for hiding this comment

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

done. Added eps: float = 0.1 as a parameter.

elif abs(ratio - 2.0) < 0.1:
current_scaling_factor = 2.0
target_width = bx2 - bx
except Exception:
Copy link
Contributor

Choose a reason for hiding this comment

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

What exceptions are you expecting here? It looks like it is unnecessary

Copy link
Author

Choose a reason for hiding this comment

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

Removed. Replaced with a simple if hasattr() check.

Copy link

@crawlmacos crawlmacos left a comment

Choose a reason for hiding this comment

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

Looks good enough. Thank you

@MrJarnould MrJarnould force-pushed the fix/dock-orientation branch from d635d20 to 1b3c966 Compare January 21, 2026 13:00
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.

3 participants