feat: improve drag-and-drop over alttab (closes #5350)
This commit is contained in:
1 parent
ea7284b387
commit
a809ffad61
3 files changed
+109
-64
No files matched your search
@@ -115,13 +115,17 @@ class CursorEvents {
|
||||
}
|
||||
|
||||
private static func handleMouseMoved(_ cgEvent: CGEvent) -> Unmanaged<CGEvent>? {
|
||||
updateDeadzoneSituation(cgEvent)
|
||||
if isAllowedToMouseHover {
|
||||
if isAllowedToReactToPointerMovement(cgEvent.location) {
|
||||
TilesView.thumbnailOverView.updateHover()
|
||||
}
|
||||
return Unmanaged.passUnretained(cgEvent)
|
||||
}
|
||||
|
||||
static func isAllowedToReactToPointerMovement(_ location: CGPoint) -> Bool {
|
||||
updateDeadzoneSituation(location)
|
||||
return isAllowedToMouseHover
|
||||
}
|
||||
|
||||
private static func pointerLocationInWindow() -> NSPoint {
|
||||
TilesPanel.shared.mouseLocationOutsideOfEventStream
|
||||
}
|
||||
@@ -158,8 +162,7 @@ class CursorEvents {
|
||||
|
||||
/// when using the trackpad, the user may swipe with a slight mistake. This will create a small cursor movement
|
||||
/// we ignore those, as they are not intended. Intended movements will be larger and not ignored
|
||||
private static func updateDeadzoneSituation(_ cgEvent: CGEvent) {
|
||||
let location = cgEvent.location
|
||||
private static func updateDeadzoneSituation(_ location: CGPoint) {
|
||||
guard let deadZoneInitialPosition else {
|
||||
deadZoneInitialPosition = location
|
||||
isAllowedToMouseHover = false
|
||||
|
||||
@@ -17,9 +17,7 @@ class TileView: FlippedView {
|
||||
|
||||
var mouseUpCallback: (() -> Void)!
|
||||
var mouseMovedCallback: (() -> Void)!
|
||||
var dragAndDropTimer: Timer?
|
||||
var indexInRecycledViews: Int!
|
||||
private var lastDragLocation: NSPoint?
|
||||
|
||||
var isFirstInRow = false
|
||||
var isLastInRow = false
|
||||
@@ -32,57 +30,6 @@ class TileView: FlippedView {
|
||||
|
||||
override func isAccessibilityElement() -> Bool { true }
|
||||
|
||||
override func wantsPeriodicDraggingUpdates() -> Bool { false }
|
||||
|
||||
override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
let location = sender.draggingLocation
|
||||
if location != lastDragLocation {
|
||||
lastDragLocation = location
|
||||
mouseMovedCallback()
|
||||
}
|
||||
return .link
|
||||
}
|
||||
|
||||
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
lastDragLocation = sender.draggingLocation
|
||||
mouseMovedCallback()
|
||||
setDraggingTimer()
|
||||
return .link
|
||||
}
|
||||
|
||||
private func setDraggingTimer() {
|
||||
dragAndDropTimer?.invalidate()
|
||||
dragAndDropTimer = Timer(timeInterval: 2, repeats: false, block: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
// the user can tab to focus the next thumbnail, while still dragging. We don't want to perform drag then
|
||||
if Windows.selectedWindowIndex == Windows.hoveredWindowIndex {
|
||||
self.mouseUpCallback()
|
||||
}
|
||||
})
|
||||
dragAndDropTimer?.tolerance = 0.2
|
||||
if let dragAndDropTimer {
|
||||
// we need to target .common since dragging is running in .eventTracking on trackpad
|
||||
RunLoop.main.add(dragAndDropTimer, forMode: .common)
|
||||
}
|
||||
}
|
||||
|
||||
override func draggingExited(_ sender: NSDraggingInfo?) {
|
||||
lastDragLocation = nil
|
||||
dragAndDropTimer?.invalidate()
|
||||
dragAndDropTimer = nil
|
||||
TilesView.thumbnailOverView.resetHoveredWindow()
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
dragAndDropTimer?.invalidate()
|
||||
dragAndDropTimer = nil
|
||||
let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self]) as! [URL]
|
||||
let appUrl = window_!.application.bundleURL!
|
||||
let open = try? NSWorkspace.shared.open(urls, withApplicationAt: appUrl, options: [], configuration: [:])
|
||||
App.hideUi()
|
||||
return open != nil
|
||||
}
|
||||
|
||||
func mouseMoved() {
|
||||
updateLabelTooltipIfNeeded()
|
||||
mouseMovedCallback()
|
||||
@@ -96,7 +43,6 @@ class TileView: FlippedView {
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
setupView()
|
||||
observeDragAndDrop()
|
||||
}
|
||||
|
||||
/// The frame used by TileUnderLayer to position the highlight rectangle.
|
||||
@@ -569,11 +515,6 @@ class TileView: FlippedView {
|
||||
return "Red badge"
|
||||
}
|
||||
|
||||
private func observeDragAndDrop() {
|
||||
// we only handle URLs (i.e. not text, image, or other draggable things)
|
||||
registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
|
||||
}
|
||||
|
||||
static func makeShadow(_ color: NSColor?) -> NSShadow? {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = color
|
||||
|
||||
@@ -644,7 +644,7 @@ class ScrollView: NSScrollView {
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
documentView = FlippedView(frame: .zero)
|
||||
documentView = TilesDocumentView(frame: .zero)
|
||||
documentView!.wantsLayer = true
|
||||
drawsBackground = false
|
||||
hasVerticalScroller = true
|
||||
@@ -678,6 +678,107 @@ class ScrollView: NSScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
class TilesDocumentView: FlippedView {
|
||||
private static let dragAndDropTimerDelay = TimeInterval(2)
|
||||
private static let dragAndDropTimerResetDistance = CGFloat(5)
|
||||
|
||||
private weak var dragTarget: TileView?
|
||||
private var lastDragLocation: NSPoint?
|
||||
private var timerResetLocation: NSPoint?
|
||||
private var dragAndDropTimer: Timer?
|
||||
|
||||
override func wantsPeriodicDraggingUpdates() -> Bool { false }
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
// we only handle URLs (i.e. not text, image, or other draggable things)
|
||||
registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
let location = sender.draggingLocation
|
||||
lastDragLocation = location
|
||||
return dragOperation(location)
|
||||
}
|
||||
|
||||
override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
let location = sender.draggingLocation
|
||||
if location != lastDragLocation { lastDragLocation = location }
|
||||
return dragOperation(location)
|
||||
}
|
||||
|
||||
override func draggingExited(_ sender: NSDraggingInfo?) {
|
||||
resetDraggingState(true)
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
defer { resetDraggingState(false) }
|
||||
guard let target = targetView(sender.draggingLocation) ?? dragTarget,
|
||||
let window = target.window_,
|
||||
let appUrl = window.application.bundleURL else { return false }
|
||||
guard let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
!urls.isEmpty else { return false }
|
||||
let open = try? NSWorkspace.shared.open(urls, withApplicationAt: appUrl, options: [], configuration: [:])
|
||||
if open != nil { App.hideUi() }
|
||||
return open != nil
|
||||
}
|
||||
|
||||
override func concludeDragOperation(_ sender: NSDraggingInfo?) {
|
||||
resetDraggingState(false)
|
||||
}
|
||||
|
||||
private func dragOperation(_ location: NSPoint) -> NSDragOperation {
|
||||
guard let target = targetView(location) else {
|
||||
resetDraggingState(true)
|
||||
return []
|
||||
}
|
||||
if dragTarget !== target {
|
||||
dragTarget = target
|
||||
restartDraggingTimer(location)
|
||||
} else if shouldRestartDraggingTimer(location) {
|
||||
restartDraggingTimer(location)
|
||||
}
|
||||
if let window, CursorEvents.isAllowedToReactToPointerMovement(window.convertPoint(toScreen: location)) {
|
||||
target.mouseMovedCallback()
|
||||
}
|
||||
return .link
|
||||
}
|
||||
|
||||
private func shouldRestartDraggingTimer(_ location: NSPoint) -> Bool {
|
||||
guard let timerResetLocation else { return true }
|
||||
return hypot(location.x - timerResetLocation.x, location.y - timerResetLocation.y) >= Self.dragAndDropTimerResetDistance
|
||||
}
|
||||
|
||||
private func restartDraggingTimer(_ location: NSPoint) {
|
||||
timerResetLocation = location
|
||||
dragAndDropTimer?.invalidate()
|
||||
dragAndDropTimer = Timer(timeInterval: Self.dragAndDropTimerDelay, repeats: false, block: { [weak self] _ in
|
||||
guard let self, let dragTarget = self.dragTarget else { return }
|
||||
dragTarget.mouseUpCallback()
|
||||
})
|
||||
dragAndDropTimer?.tolerance = 0.2
|
||||
if let dragAndDropTimer { RunLoop.main.add(dragAndDropTimer, forMode: .common) }
|
||||
}
|
||||
|
||||
private func resetDraggingState(_ resetHoveredWindow: Bool) {
|
||||
lastDragLocation = nil
|
||||
dragTarget = nil
|
||||
timerResetLocation = nil
|
||||
dragAndDropTimer?.invalidate()
|
||||
dragAndDropTimer = nil
|
||||
if resetHoveredWindow { TilesView.thumbnailOverView.resetHoveredWindow() }
|
||||
}
|
||||
|
||||
private func targetView(_ location: NSPoint) -> TileView? {
|
||||
let overlay = TilesView.thumbnailOverView
|
||||
return overlay.findTarget(overlay.convert(location, from: nil))
|
||||
}
|
||||
}
|
||||
|
||||
class FlippedView: NSView {
|
||||
override var isFlipped: Bool { true }
|
||||
// Prevent AppKit from recursively marking all NSControl subviews as needsDisplay during makeKeyAndOrderFront
|
||||
|
||||
Reference in new issue
Block a user