Show overlay on all Spaces and alongside fullscreen apps
Set canJoinAllSpaces + fullScreenAuxiliary on overlay windows so the border renders regardless of which Space or fullscreen app is active. In window mode, fall back to screen-edge border when Cursor isn't frontmost to avoid a floating rectangle on other Spaces. Made-with: Cursor
This commit is contained in:
@@ -357,12 +357,19 @@ class FlasherDaemon:
|
||||
self._last_flash[workspace] = now
|
||||
|
||||
def _resolve_frames(self, window_frame: tuple) -> list[tuple]:
|
||||
"""Return frame(s) based on flash_mode config."""
|
||||
"""Return frame(s) based on flash_mode config.
|
||||
|
||||
In "window" mode, falls back to screen frame when Cursor is not
|
||||
frontmost (e.g. different Space or behind fullscreen app) to avoid
|
||||
drawing a floating rectangle at stale coordinates.
|
||||
"""
|
||||
mode = self.config.flash_mode
|
||||
if mode == "allscreens":
|
||||
return all_screen_frames()
|
||||
if mode == "screen":
|
||||
return [screen_frame_for_window(window_frame)]
|
||||
if not is_cursor_frontmost():
|
||||
return [screen_frame_for_window(window_frame)]
|
||||
return [window_frame]
|
||||
|
||||
def _cleanup(self) -> None:
|
||||
|
||||
@@ -11,6 +11,8 @@ from Cocoa import (
|
||||
NSView,
|
||||
NSBezierPath,
|
||||
NSTimer,
|
||||
NSWindowCollectionBehaviorCanJoinAllSpaces,
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
)
|
||||
from Foundation import NSInsetRect
|
||||
|
||||
@@ -161,6 +163,10 @@ class OverlayManager:
|
||||
window.setOpaque_(False)
|
||||
window.setBackgroundColor_(NSColor.clearColor())
|
||||
window.setLevel_(2147483631)
|
||||
window.setCollectionBehavior_(
|
||||
NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||
| NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||
)
|
||||
window.setIgnoresMouseEvents_(True)
|
||||
window.setHasShadow_(False)
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ class TestFlasherDaemon:
|
||||
window = {"title": "my-project", "frame": ((0, 0), (800, 600))}
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="light"):
|
||||
daemon._handle_message(
|
||||
@@ -132,6 +133,46 @@ class TestFlasherDaemon:
|
||||
"/path", [((0, 0), (800, 600))], daemon.config.light.completed
|
||||
)
|
||||
|
||||
def test_window_mode_falls_back_to_screen_when_cursor_not_frontmost(self):
|
||||
"""Window mode falls back to screen frame when Cursor isn't frontmost."""
|
||||
daemon = self._make_daemon(approval_delay=0.0, flash_mode="window")
|
||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||
screen = ((0, 0), (1920, 1080))
|
||||
|
||||
daemon._handle_message(
|
||||
json.dumps({"workspace": "/path", "event": "preToolUse", "tool": "Shell"}).encode()
|
||||
)
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.screen_frame_for_window", return_value=screen), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._check_pending()
|
||||
|
||||
daemon.overlay.add_pulse.assert_called_once_with(
|
||||
"/path", [screen], daemon.config.dark.running
|
||||
)
|
||||
|
||||
def test_stop_falls_back_to_screen_when_cursor_not_frontmost(self):
|
||||
"""Stop flash in window mode falls back to screen when Cursor isn't frontmost."""
|
||||
daemon = self._make_daemon(flash_mode="window")
|
||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||
screen = ((0, 0), (1920, 1080))
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.screen_frame_for_window", return_value=screen), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._handle_message(
|
||||
json.dumps({"workspace": "/path", "event": "stop"}).encode()
|
||||
)
|
||||
|
||||
daemon.overlay.add_flash.assert_called_once_with(
|
||||
"/path", [screen], daemon.config.dark.completed
|
||||
)
|
||||
|
||||
def test_allscreens_mode_uses_all_screens(self):
|
||||
daemon = self._make_daemon(flash_mode="allscreens", approval_delay=0.0)
|
||||
window = {"title": "my-project", "frame": ((0, 0), (800, 600))}
|
||||
@@ -380,7 +421,7 @@ class TestFlasherDaemon:
|
||||
)
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._check_pending()
|
||||
@@ -395,6 +436,7 @@ class TestFlasherDaemon:
|
||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._handle_message(
|
||||
@@ -411,6 +453,7 @@ class TestFlasherDaemon:
|
||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._handle_message(
|
||||
@@ -434,7 +477,7 @@ class TestFlasherDaemon:
|
||||
)
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._check_pending()
|
||||
@@ -448,6 +491,7 @@ class TestFlasherDaemon:
|
||||
daemon._active_pulses.clear()
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._handle_message(
|
||||
@@ -475,7 +519,7 @@ class TestFlasherDaemon:
|
||||
)
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="light"):
|
||||
daemon._check_pending()
|
||||
@@ -500,7 +544,7 @@ class TestFlasherDaemon:
|
||||
)
|
||||
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", side_effect=[win_a, win_b]), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._check_pending()
|
||||
@@ -533,6 +577,7 @@ class TestFlasherDaemon:
|
||||
|
||||
window = {"title": "project-a", "frame": ((0, 0), (800, 600))}
|
||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
||||
patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \
|
||||
patch("cursor_flasher.daemon.play_alert"), \
|
||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||
daemon._handle_message(
|
||||
|
||||
Reference in New Issue
Block a user