chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import AppKit
|
||||
|
||||
enum OverviewScrollStep {
|
||||
case up
|
||||
case down
|
||||
}
|
||||
|
||||
extension StatusItemController {
|
||||
/// Line distance per highlight step for classic scroll wheels.
|
||||
private static let lineScrollStepThreshold: CGFloat = 0.9
|
||||
/// A single fast flick should not race the highlight through the whole list.
|
||||
private static let maxScrollStepsPerEvent = 3
|
||||
|
||||
/// Classic scroll wheels keep row-to-row overview navigation. Precise trackpad scrolling is
|
||||
/// left to AppKit's native menu scroller so the content follows the user's fingers instead
|
||||
/// of waiting for a threshold and jumping the highlighted row.
|
||||
@discardableResult
|
||||
func handleOverviewScrollWheel(_ event: NSEvent, menu: NSMenu) -> Bool {
|
||||
guard self.menuHasOverviewRows(menu) else {
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
return false
|
||||
}
|
||||
// Leave the wheel alone while a row submenu is open (e.g. scrollable charts);
|
||||
// only the root overview list translates scrolling into highlight movement.
|
||||
guard self.openMenus.count <= 1 else {
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
return false
|
||||
}
|
||||
guard !event.hasPreciseScrollingDeltas else {
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
return false
|
||||
}
|
||||
// Precise trackpad/Magic Mouse scrolling already returned above, so this only guards
|
||||
// non-precise devices that still report a momentum phase: swallow that flick tail so the
|
||||
// highlight does not keep stepping after the fingers lift.
|
||||
guard event.momentumPhase.isEmpty else { return true }
|
||||
let delta = event.scrollingDeltaY
|
||||
guard delta != 0 else { return false }
|
||||
|
||||
if self.overviewScrollAccumulatedDelta != 0,
|
||||
(delta > 0) != (self.overviewScrollAccumulatedDelta > 0)
|
||||
{
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
}
|
||||
self.overviewScrollAccumulatedDelta += delta
|
||||
|
||||
let threshold = Self.lineScrollStepThreshold
|
||||
var steps = 0
|
||||
while abs(self.overviewScrollAccumulatedDelta) >= threshold, steps < Self.maxScrollStepsPerEvent {
|
||||
let movingUp = self.overviewScrollAccumulatedDelta > 0
|
||||
self.overviewScrollAccumulatedDelta += movingUp ? -threshold : threshold
|
||||
self.postOverviewScrollNavigation(movingUp ? .up : .down, menu: menu)
|
||||
steps += 1
|
||||
}
|
||||
// Discard the remainder once the cap is hit, otherwise the leftover delta from a
|
||||
// fast flick would keep emitting capped batches on the next small scroll.
|
||||
if steps == Self.maxScrollStepsPerEvent {
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func menuHasOverviewRows(_ menu: NSMenu) -> Bool {
|
||||
menu.items.contains { item in
|
||||
(item.representedObject as? String)?.hasPrefix(Self.overviewRowIdentifierPrefix) == true
|
||||
}
|
||||
}
|
||||
|
||||
func resetOverviewScrollAccumulation() {
|
||||
self.overviewScrollAccumulatedDelta = 0
|
||||
}
|
||||
|
||||
private func postOverviewScrollNavigation(_ step: OverviewScrollStep, menu: NSMenu) {
|
||||
if let handler = self.overviewScrollNavigationHandlerForTesting {
|
||||
handler(step)
|
||||
return
|
||||
}
|
||||
guard let target = self.overviewScrollTargetItem(in: menu, step: step) else { return }
|
||||
let menuID = ObjectIdentifier(menu)
|
||||
guard self.highlightedMenuItems[menuID] !== target else { return }
|
||||
|
||||
// Advance local state immediately so a capped multi-step flick can target successive rows
|
||||
// before AppKit drains the synthetic mouse-move events.
|
||||
self.menu(menu, willHighlight: target)
|
||||
|
||||
guard let view = target.view,
|
||||
let window = view.window
|
||||
else { return }
|
||||
let location = view.convert(
|
||||
NSPoint(x: view.bounds.midX, y: view.bounds.midY),
|
||||
to: nil)
|
||||
guard let event = NSEvent.mouseEvent(
|
||||
with: .mouseMoved,
|
||||
location: location,
|
||||
modifierFlags: [],
|
||||
timestamp: ProcessInfo.processInfo.systemUptime,
|
||||
windowNumber: window.windowNumber,
|
||||
context: nil,
|
||||
eventNumber: 0,
|
||||
clickCount: 0,
|
||||
pressure: 0)
|
||||
else { return }
|
||||
NSApp.postEvent(event, atStart: false)
|
||||
}
|
||||
|
||||
func overviewScrollTargetItem(in menu: NSMenu, step: OverviewScrollStep) -> NSMenuItem? {
|
||||
let rows = menu.items.filter { item in
|
||||
(item.representedObject as? String)?.hasPrefix(Self.overviewRowIdentifierPrefix) == true
|
||||
}
|
||||
guard !rows.isEmpty else { return nil }
|
||||
|
||||
guard let current = self.highlightedMenuItems[ObjectIdentifier(menu)],
|
||||
let currentIndex = rows.firstIndex(where: { $0 === current })
|
||||
else {
|
||||
return step == .down ? rows.first : rows.last
|
||||
}
|
||||
|
||||
let targetIndex: Int = switch step {
|
||||
case .up:
|
||||
max(0, currentIndex - 1)
|
||||
case .down:
|
||||
min(rows.count - 1, currentIndex + 1)
|
||||
}
|
||||
return rows[targetIndex]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user