feat: make drag-and-drop trigger area configurable & fix shelf disable behavior#1056
feat: make drag-and-drop trigger area configurable & fix shelf disable behavior#1056tornado-bunk wants to merge 4 commits intoTheBoredTeam:devfrom
Conversation
Introduce a user-facing setting to choose between an expanded and notch-only drag detection region and use the selected value when computing the shelf drag hotspot. Also fix a pre-existing bug in drag content validation so drag detection continues to work reliably across multiple drags. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit resolves a bug where dragging files onto the notch (or the expanded detection area) would still trigger the Shelf to open even when the "Enable Shelf" setting was disabled. The issue was caused by two main factors: 1. The global drag detectors in `boringNotchApp` (which handle the physical notch area and expanded zone) were only checking the `expandedDragDetection` setting, ignoring the global `boringShelf` toggle. 2. The `onDrop` modifier in `ContentView` (which handles drops directly onto the closed notch UI) was unconditionally active, meaning any drag event over the window would trigger the shelf logic regardless of user preferences. This fix implements a robust solution by: - **Updating Drag Detectors**: `setupDragDetectors()` now strictly checks that both `boringShelf` and `expandedDragDetection` are enabled before activating any monitors. - **Introducing New Observers**: Added a `boringShelfChanged` notification that fires when the Shelf is toggled in settings, ensuring drag detectors are immediately started or stopped without requiring an app restart. - **Conditional Drop Targets**: Modified the `dragDetector` view and the general `onDrop` delegate in `ContentView` to respect the `boringShelf` and `expandedDragDetection` states. The general drop delegate now actively prevents the drop target state from becoming true if the feature is disabled.
There was a problem hiding this comment.
Pull request overview
This PR enhances the Shelf's drag-and-drop functionality by making the detection area configurable and fixing a bug where drag detection persisted when the shelf was disabled.
Changes:
- Replaced binary "Expanded drag detection" toggle with a more flexible system: a toggle for "Enable drag detection" and a picker for "Detection area" (Expanded area vs. Notch only)
- Fixed bug where drag detectors were active even when "Enable shelf" was disabled by adding
boringShelfcheck tosetupDragDetectors()and ContentView drop handlers - Added
boringShelfChangednotification to reactively update drag detector state
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| boringNotch/observers/DragDetector.swift | Modified drag content validation logic and removed trailing whitespace |
| boringNotch/models/Constants.swift | Added DragDetectionArea enum, boringShelfChanged notification, and dragDetectionArea default setting |
| boringNotch/components/Settings/Views/ShelfSettingsView.swift | Added detection area picker, renamed toggle, and added notification handlers for shelf enable/disable |
| boringNotch/boringNotchApp.swift | Updated setupDragDetectors() to respect both boringShelf and expandedDragDetection settings, added support for configurable detection area sizes, and added boringShelfChanged notification observer |
| boringNotch/Localizable.xcstrings | Added localization entries for new UI strings and marked old string as stale |
| boringNotch/ContentView.swift | Added conditional logic to disable drop delegates when shelf or drag detection is disabled |
| ] | ||
| let isValid = dragPasteboard.pasteboardItems?.allSatisfy { item in | ||
| item.types.allSatisfy { validTypes.contains($0) } | ||
| item.types.contains { validTypes.contains($0) } |
There was a problem hiding this comment.
The change from allSatisfy to contains changes the validation logic in a way that weakens content validation. Previously, the code verified that ALL types in each pasteboard item were valid types. Now it only checks if ANY type in each item is valid. This means items with a mix of valid and invalid types would now pass validation when they would have been rejected before. Consider whether this relaxed validation is intentional. If the goal is to accept items that have at least one valid type (regardless of other types), this is correct. Otherwise, revert to allSatisfy.
| item.types.contains { validTypes.contains($0) } | |
| item.types.allSatisfy { validTypes.contains($0) } |
There was a problem hiding this comment.
Yes, changing from allSatisfy to contains is intentional.
Pasteboard items (e.g. from Finder) usually include many extra types (dyn., com.apple.finder., etc.) in addition to .fileURL / URL / string.
With allSatisfy, a single non-whitelisted type caused us to reject otherwise valid drags, which is why drag detection stopped working reliably after the first drag during my tests.
| Picker("Detection area", selection: $dragDetectionArea) { | ||
| ForEach(DragDetectionArea.allCases) { area in | ||
| Text(area.rawValue).tag(area) | ||
| } | ||
| } | ||
| .disabled(!expandedDragDetection) | ||
| .onChange(of: dragDetectionArea) { | ||
| NotificationCenter.default.post( | ||
| name: Notification.Name.expandedDragDetectionChanged, | ||
| object: nil | ||
| ) | ||
| } |
There was a problem hiding this comment.
The UI presents "Enable drag detection" toggle and a "Detection area" picker, but the implementation creates a confusing relationship. The picker is disabled when drag detection is off, but the underlying dragDetectionArea setting still exists and is used even when expandedDragDetection is false (though setupDragDetectors early-returns). This creates semantic confusion: what does it mean to have a "detection area" setting when detection is disabled? Consider one of these approaches: (1) Hide the picker entirely when detection is disabled instead of just disabling it, or (2) Rename the toggle to better reflect that it enables/disables the global monitoring system, while the picker controls the active area when enabled.
| Picker("Detection area", selection: $dragDetectionArea) { | |
| ForEach(DragDetectionArea.allCases) { area in | |
| Text(area.rawValue).tag(area) | |
| } | |
| } | |
| .disabled(!expandedDragDetection) | |
| .onChange(of: dragDetectionArea) { | |
| NotificationCenter.default.post( | |
| name: Notification.Name.expandedDragDetectionChanged, | |
| object: nil | |
| ) | |
| } | |
| if expandedDragDetection { | |
| Picker("Detection area", selection: $dragDetectionArea) { | |
| ForEach(DragDetectionArea.allCases) { area in | |
| Text(area.rawValue).tag(area) | |
| } | |
| } | |
| .onChange(of: dragDetectionArea) { | |
| NotificationCenter.default.post( | |
| name: Notification.Name.expandedDragDetectionChanged, | |
| object: nil | |
| ) | |
| } | |
| } |
There was a problem hiding this comment.
When expandedDragDetection is false we early‑return from setupDragDetectors(), so dragDetectionArea isn’t used at runtime; the disabled picker only shows (and preserves) which area will be used once detection is turned back on.
| @@ -205,19 +205,29 @@ class AppDelegate: NSObject, NSApplicationDelegate { | |||
|
|
|||
| private func setupDragDetectorForScreen(_ screen: NSScreen) { | |||
There was a problem hiding this comment.
Doesn't the existing drop target already cover the closed notch? In this case a global monitor shouldn't be necessary. In general i would like to avoid the global monitor, since it false positives with Safari tabs and is probably less performant than a drop target.
| } | ||
| Defaults.Toggle(key: .expandedDragDetection) { | ||
| Text("Expanded drag detection area") | ||
| Text("Enable drag detection") |
There was a problem hiding this comment.
Creating a new setting while still using an older variable is generally bad practice. In the current version, this value only disabled drag detection outside of the closed notch window, but now it is being used to disable all drag detection. If this code gets deployed, it will apply the old setting value to the new unrelated setting, causing unexpected behaviour for users by disabling dragging entirely.
| } | ||
| .disabled(!expandedDragDetection) | ||
| .onChange(of: dragDetectionArea) { | ||
| NotificationCenter.default.post( |
There was a problem hiding this comment.
While we eventually need to move away from using notifications for these settings, I’m not requesting a change here since this pattern is still consistent.
This PR addresses some issues regarding the Shelf's drag-and-drop functionality.
First, it replaces the binary "Expanded drag detection" toggle with a more flexible
DragDetectionAreaconfiguration, allowing users to choose between the expanded area (full open notch size) or restricting detection strictly to the notch only.Second, it fixes a bug where the Shelf would continue to detect drag events even when the "Enable shelf" setting was disabled. Previously, the global drag detectors and the
onDropmodified inContentViewdid not respect the disabled state of the shelf.Changes
DragDetectionAreaenum and picker in Settings -> Shelf. Options: "Expanded area" (default) and "Notch only".setupDragDetectors()inboringNotchApp.swiftto checkDefaults[.boringShelf]before starting monitors.ContentView.swift'sdragDetectorand generalonDropdelegate to be conditional based on bothboringShelfandexpandedDragDetectionsettings.boringShelfChangednotification to reactively update drag detector state without app restart.Test Plan
Configuration Test:
Disable Shelf Test:
Disable Drag Detection Test:
Screenshots
Screen Recordings
Registrazione.schermo.2026-02-25.alle.20.45.57.mp4