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>? {
|
private static func handleMouseMoved(_ cgEvent: CGEvent) -> Unmanaged<CGEvent>? {
|
||||||
updateDeadzoneSituation(cgEvent)
|
if isAllowedToReactToPointerMovement(cgEvent.location) {
|
||||||
if isAllowedToMouseHover {
|
|
||||||
TilesView.thumbnailOverView.updateHover()
|
TilesView.thumbnailOverView.updateHover()
|
||||||
}
|
}
|
||||||
return Unmanaged.passUnretained(cgEvent)
|
return Unmanaged.passUnretained(cgEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func isAllowedToReactToPointerMovement(_ location: CGPoint) -> Bool {
|
||||||
|
updateDeadzoneSituation(location)
|
||||||
|
return isAllowedToMouseHover
|
||||||
|
}
|
||||||
|
|
||||||
private static func pointerLocationInWindow() -> NSPoint {
|
private static func pointerLocationInWindow() -> NSPoint {
|
||||||
TilesPanel.shared.mouseLocationOutsideOfEventStream
|
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
|
/// 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
|
/// we ignore those, as they are not intended. Intended movements will be larger and not ignored
|
||||||
private static func updateDeadzoneSituation(_ cgEvent: CGEvent) {
|
private static func updateDeadzoneSituation(_ location: CGPoint) {
|
||||||
let location = cgEvent.location
|
|
||||||
guard let deadZoneInitialPosition else {
|
guard let deadZoneInitialPosition else {
|
||||||
deadZoneInitialPosition = location
|
deadZoneInitialPosition = location
|
||||||
isAllowedToMouseHover = false
|
isAllowedToMouseHover = false
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ class TileView: FlippedView {
|
|||||||
|
|
||||||
var mouseUpCallback: (() -> Void)!
|
var mouseUpCallback: (() -> Void)!
|
||||||
var mouseMovedCallback: (() -> Void)!
|
var mouseMovedCallback: (() -> Void)!
|
||||||
var dragAndDropTimer: Timer?
|
|
||||||
var indexInRecycledViews: Int!
|
var indexInRecycledViews: Int!
|
||||||
private var lastDragLocation: NSPoint?
|
|
||||||
|
|
||||||
var isFirstInRow = false
|
var isFirstInRow = false
|
||||||
var isLastInRow = false
|
var isLastInRow = false
|
||||||
@@ -32,57 +30,6 @@ class TileView: FlippedView {
|
|||||||
|
|
||||||
override func isAccessibilityElement() -> Bool { true }
|
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() {
|
func mouseMoved() {
|
||||||
updateLabelTooltipIfNeeded()
|
updateLabelTooltipIfNeeded()
|
||||||
mouseMovedCallback()
|
mouseMovedCallback()
|
||||||
@@ -96,7 +43,6 @@ class TileView: FlippedView {
|
|||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(frame: .zero)
|
self.init(frame: .zero)
|
||||||
setupView()
|
setupView()
|
||||||
observeDragAndDrop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The frame used by TileUnderLayer to position the highlight rectangle.
|
/// The frame used by TileUnderLayer to position the highlight rectangle.
|
||||||
@@ -569,11 +515,6 @@ class TileView: FlippedView {
|
|||||||
return "Red badge"
|
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? {
|
static func makeShadow(_ color: NSColor?) -> NSShadow? {
|
||||||
let shadow = NSShadow()
|
let shadow = NSShadow()
|
||||||
shadow.shadowColor = color
|
shadow.shadowColor = color
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ class ScrollView: NSScrollView {
|
|||||||
|
|
||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(frame: .zero)
|
self.init(frame: .zero)
|
||||||
documentView = FlippedView(frame: .zero)
|
documentView = TilesDocumentView(frame: .zero)
|
||||||
documentView!.wantsLayer = true
|
documentView!.wantsLayer = true
|
||||||
drawsBackground = false
|
drawsBackground = false
|
||||||
hasVerticalScroller = true
|
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 {
|
class FlippedView: NSView {
|
||||||
override var isFlipped: Bool { true }
|
override var isFlipped: Bool { true }
|
||||||
// Prevent AppKit from recursively marking all NSControl subviews as needsDisplay during makeKeyAndOrderFront
|
// Prevent AppKit from recursively marking all NSControl subviews as needsDisplay during makeKeyAndOrderFront
|
||||||
|
|||||||
Reference in new issue
Block a user