diff --git a/ModMove/Mover.swift b/ModMove/Mover.swift index ad902da..5d00eaa 100644 --- a/ModMove/Mover.swift +++ b/ModMove/Mover.swift @@ -24,41 +24,67 @@ final class Mover { private var closestCorner: Corner? private var window: AccessibilityElement? private var frame: NSRect? + private var scaleFactor: CGFloat? + + private var prevMousePosition: CGPoint? + private var prevDate: Date = Date() + // Mouse speed is in pixels/second. + private var mouseSpeed: CGFloat = 0 + private let FAST_MOUSE_SPEED_THRESHOLD: CGFloat = 1000 + // Weight given to latest mouse speed for averaging. + private let MOUSE_SPEED_WEIGHT: CGFloat = 0.1 private func mouseMoved(handler: (_ window: AccessibilityElement, _ mouseDelta: CGPoint) -> Void) { - let point = Mouse.currentPosition() + let curMousePos = Mouse.currentPosition() if self.window == nil { - self.window = AccessibilityElement.systemWideElement.element(at: point)?.window() + self.window = AccessibilityElement.systemWideElement.element(at: curMousePos)?.window() } guard let window = self.window else { return } if self.initialMousePosition == nil { - self.initialMousePosition = point + self.prevMousePosition = curMousePos + self.initialMousePosition = curMousePos self.initialWindowPosition = window.position self.initialWindowSize = window.size - self.closestCorner = self.getClosestCorner(window: window, mouse: point) - self.frame = getUsableScreen() + self.closestCorner = self.getClosestCorner(window: window, mouse: curMousePos) + (self.frame, self.scaleFactor) = getUsableScreen() let currentPid = NSRunningApplication.current.processIdentifier if let pid = window.pid(), pid != currentPid { NSRunningApplication(processIdentifier: pid)?.activate(options: .activateIgnoringOtherApps) } window.bringToFront() - } else if let initialMousePosition = self.initialMousePosition { - let mouseDelta = CGPoint(x: point.x - initialMousePosition.x, y: point.y - initialMousePosition.y) + } else if let initMousePos = self.initialMousePosition { + self.trackMouseSpeed(curMousePos: curMousePos) + let mouseDelta = CGPoint(x: curMousePos.x - initMousePos.x, y: curMousePos.y - initMousePos.y) handler(window, mouseDelta) } } - private func getUsableScreen() -> NSRect? { - if var visible = NSScreen.main?.visibleFrame, let full = NSScreen.main?.frame { + private func trackMouseSpeed(curMousePos: CGPoint) { + if let prevMousePos = self.prevMousePosition, let scale = self.scaleFactor { + let mouseDist: CGFloat = sqrt( + pow((curMousePos.x - prevMousePos.x) / scale, 2) + + pow((curMousePos.y - prevMousePos.y) / scale, 2)) + let now = Date() + let timeDiff: CGFloat = CGFloat(now.timeIntervalSince(prevDate)) + let latestMouseSpeed = mouseDist / timeDiff + self.mouseSpeed = latestMouseSpeed * MOUSE_SPEED_WEIGHT + self.mouseSpeed * (1 - MOUSE_SPEED_WEIGHT) + self.prevMousePosition = curMousePos + self.prevDate = now + // NSLog("timeD: %.3f\tmouseD: %.1f\tmouseSpeed: %.1f", timeDiff, mouseDist, scale, self.mouseSpeed) + } + } + + private func getUsableScreen() -> (NSRect, CGFloat) { + if var visible = NSScreen.main?.visibleFrame, let full = NSScreen.main?.frame, let scale = NSScreen.main?.backingScaleFactor { // For some reason, visibleFrame still has minY = 0 even though the menubar is there? visible.origin.y = full.size.height - visible.size.height - return visible + return (visible, scale) } - return NSRect.zero + return (NSRect.zero, 1) } private func getClosestCorner(window: AccessibilityElement, mouse: CGPoint) -> Corner { @@ -81,25 +107,35 @@ final class Mover { private func resizeWindow(window: AccessibilityElement, mouseDelta: CGPoint) { if let initWinSize = self.initialWindowSize, let initWinPos = self.initialWindowPosition, let corner = self.closestCorner, let frame = self.frame { + var mdx = mouseDelta.x + var mdy = mouseDelta.y switch corner { case .TopLeft: - let mdx = max(mouseDelta.x, frame.minX - initWinPos.x) - let mdy = max(mouseDelta.y, frame.minY - initWinPos.y) + if shouldConstrainMouseDelta(window, mouseDelta) { + mdx = max(mouseDelta.x, frame.minX - initWinPos.x) + mdy = max(mouseDelta.y, frame.minY - initWinPos.y) + } window.position = CGPoint(x: initWinPos.x + mdx, y: initWinPos.y + mdy) window.size = CGSize(width: initWinSize.width - mdx, height: initWinSize.height - mdy) case .TopRight: - let mdx = min(mouseDelta.x, frame.maxX - (initWinPos.x + initWinSize.width)) - let mdy = max(mouseDelta.y, frame.minY - initWinPos.y) + if shouldConstrainMouseDelta(window, mouseDelta) { + mdx = min(mouseDelta.x, frame.maxX - (initWinPos.x + initWinSize.width)) + mdy = max(mouseDelta.y, frame.minY - initWinPos.y) + } window.position = CGPoint(x: initWinPos.x, y: initWinPos.y + mdy) window.size = CGSize(width: initWinSize.width + mdx, height: initWinSize.height - mdy ) case .BottomLeft: - let mdx = max(mouseDelta.x, frame.minX - initWinPos.x) - let mdy = min(mouseDelta.y, frame.maxY - (initWinPos.y + initWinSize.height)) + if shouldConstrainMouseDelta(window, mouseDelta) { + mdx = max(mouseDelta.x, frame.minX - initWinPos.x) + mdy = min(mouseDelta.y, frame.maxY - (initWinPos.y + initWinSize.height)) + } window.position = CGPoint(x: initWinPos.x + mdx, y: initWinPos.y) window.size = CGSize(width: initWinSize.width - mdx, height: initWinSize.height + mdy) case .BottomRight: - let mdx = min(mouseDelta.x, frame.maxX - (initWinPos.x + initWinSize.width)) - let mdy = min(mouseDelta.y, frame.maxY - (initWinPos.y + initWinSize.height)) + if shouldConstrainMouseDelta(window, mouseDelta) { + mdx = min(mouseDelta.x, frame.maxX - (initWinPos.x + initWinSize.width)) + mdy = min(mouseDelta.y, frame.maxY - (initWinPos.y + initWinSize.height)) + } window.size = CGSize(width: initWinSize.width + mdx, height: initWinSize.height + mdy) } } @@ -107,14 +143,33 @@ final class Mover { private func moveWindow(window: AccessibilityElement, mouseDelta: CGPoint) { if let initWinPos = self.initialWindowPosition, let initWinSize = self.initialWindowSize, let frame = self.frame { - let mdx = min(max(mouseDelta.x, frame.minX - initWinPos.x), + var mdx = mouseDelta.x + var mdy = mouseDelta.y + if shouldConstrainMouseDelta(window, mouseDelta) { + mdx = min(max(mouseDelta.x, frame.minX - initWinPos.x), frame.maxX - (initWinPos.x + initWinSize.width)) - let mdy = min(max(mouseDelta.y, frame.minY - initWinPos.y), + mdy = min(max(mouseDelta.y, frame.minY - initWinPos.y), frame.maxY - (initWinPos.y + initWinSize.height)) + } window.position = CGPoint(x: initWinPos.x + mdx, y: initWinPos.y + mdy) } } + private func shouldConstrainMouseDelta(_ window: AccessibilityElement, _ mouseDelta: CGPoint) -> Bool { + // Slow moves get constrained. But once a window is out of the frame, we don't constrain it anymore. + if let frame = self.frame { + return self.mouseSpeed < FAST_MOUSE_SPEED_THRESHOLD && windowInsideFrame(window, frame) + } + return false + } + + private func windowInsideFrame(_ window: AccessibilityElement, _ frame: CGRect) -> Bool { + if let pos = window.position, let size = window.size { + return frame.contains(NSMakeRect(pos.x, pos.y, size.width, size.height)) + } + return true + } + private func changed(state: FlagState) { self.removeMonitor() self.resetState()