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
|
self._last_flash[workspace] = now
|
||||||
|
|
||||||
def _resolve_frames(self, window_frame: tuple) -> list[tuple]:
|
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
|
mode = self.config.flash_mode
|
||||||
if mode == "allscreens":
|
if mode == "allscreens":
|
||||||
return all_screen_frames()
|
return all_screen_frames()
|
||||||
if mode == "screen":
|
if mode == "screen":
|
||||||
return [screen_frame_for_window(window_frame)]
|
return [screen_frame_for_window(window_frame)]
|
||||||
|
if not is_cursor_frontmost():
|
||||||
|
return [screen_frame_for_window(window_frame)]
|
||||||
return [window_frame]
|
return [window_frame]
|
||||||
|
|
||||||
def _cleanup(self) -> None:
|
def _cleanup(self) -> None:
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from Cocoa import (
|
|||||||
NSView,
|
NSView,
|
||||||
NSBezierPath,
|
NSBezierPath,
|
||||||
NSTimer,
|
NSTimer,
|
||||||
|
NSWindowCollectionBehaviorCanJoinAllSpaces,
|
||||||
|
NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||||
)
|
)
|
||||||
from Foundation import NSInsetRect
|
from Foundation import NSInsetRect
|
||||||
|
|
||||||
@@ -161,6 +163,10 @@ class OverlayManager:
|
|||||||
window.setOpaque_(False)
|
window.setOpaque_(False)
|
||||||
window.setBackgroundColor_(NSColor.clearColor())
|
window.setBackgroundColor_(NSColor.clearColor())
|
||||||
window.setLevel_(2147483631)
|
window.setLevel_(2147483631)
|
||||||
|
window.setCollectionBehavior_(
|
||||||
|
NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||||
|
| NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||||
|
)
|
||||||
window.setIgnoresMouseEvents_(True)
|
window.setIgnoresMouseEvents_(True)
|
||||||
window.setHasShadow_(False)
|
window.setHasShadow_(False)
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class TestFlasherDaemon:
|
|||||||
window = {"title": "my-project", "frame": ((0, 0), (800, 600))}
|
window = {"title": "my-project", "frame": ((0, 0), (800, 600))}
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="light"):
|
patch(PATCH_APPEARANCE, return_value="light"):
|
||||||
daemon._handle_message(
|
daemon._handle_message(
|
||||||
@@ -132,6 +133,46 @@ class TestFlasherDaemon:
|
|||||||
"/path", [((0, 0), (800, 600))], daemon.config.light.completed
|
"/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):
|
def test_allscreens_mode_uses_all_screens(self):
|
||||||
daemon = self._make_daemon(flash_mode="allscreens", approval_delay=0.0)
|
daemon = self._make_daemon(flash_mode="allscreens", approval_delay=0.0)
|
||||||
window = {"title": "my-project", "frame": ((0, 0), (800, 600))}
|
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), \
|
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("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._check_pending()
|
daemon._check_pending()
|
||||||
@@ -395,6 +436,7 @@ class TestFlasherDaemon:
|
|||||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._handle_message(
|
daemon._handle_message(
|
||||||
@@ -411,6 +453,7 @@ class TestFlasherDaemon:
|
|||||||
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
window = {"title": "proj", "frame": ((0, 0), (800, 600))}
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert") as mock_alert, \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._handle_message(
|
daemon._handle_message(
|
||||||
@@ -434,7 +477,7 @@ class TestFlasherDaemon:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._check_pending()
|
daemon._check_pending()
|
||||||
@@ -448,6 +491,7 @@ class TestFlasherDaemon:
|
|||||||
daemon._active_pulses.clear()
|
daemon._active_pulses.clear()
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._handle_message(
|
daemon._handle_message(
|
||||||
@@ -475,7 +519,7 @@ class TestFlasherDaemon:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="light"):
|
patch(PATCH_APPEARANCE, return_value="light"):
|
||||||
daemon._check_pending()
|
daemon._check_pending()
|
||||||
@@ -500,7 +544,7 @@ class TestFlasherDaemon:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", side_effect=[win_a, win_b]), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._check_pending()
|
daemon._check_pending()
|
||||||
@@ -533,6 +577,7 @@ class TestFlasherDaemon:
|
|||||||
|
|
||||||
window = {"title": "project-a", "frame": ((0, 0), (800, 600))}
|
window = {"title": "project-a", "frame": ((0, 0), (800, 600))}
|
||||||
with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \
|
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("cursor_flasher.daemon.play_alert"), \
|
||||||
patch(PATCH_APPEARANCE, return_value="dark"):
|
patch(PATCH_APPEARANCE, return_value="dark"):
|
||||||
daemon._handle_message(
|
daemon._handle_message(
|
||||||
|
|||||||
Reference in New Issue
Block a user