Skip to content

Commit

Permalink
Merge pull request #49 from pablopunk/resize-quadrants
Browse files Browse the repository at this point in the history
feat: resize quadrants
  • Loading branch information
pablopunk authored Jun 7, 2024
2 parents ed44435 + e2e7338 commit 442e2e7
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 119 deletions.
20 changes: 16 additions & 4 deletions Swift Shift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
172E0BD52B32F36300D5CDA7 /* MouseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172E0BD42B32F36300D5CDA7 /* MouseTracker.swift */; };
173C0E832C123CF700654527 /* DrawCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173C0E822C123CF700654527 /* DrawCircle.swift */; };
174652CE2B33345200241CE6 /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174652CD2B33345200241CE6 /* WindowManager.swift */; };
177FB2412B31F9B900B11BA3 /* PermissionsRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177FB2402B31F9B900B11BA3 /* PermissionsRequestView.swift */; };
177FB2482B31FE7F00B11BA3 /* ShortcutRecorder in Frameworks */ = {isa = PBXBuildFile; productRef = 177FB2472B31FE7F00B11BA3 /* ShortcutRecorder */; };
Expand All @@ -33,6 +34,7 @@

/* Begin PBXFileReference section */
172E0BD42B32F36300D5CDA7 /* MouseTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseTracker.swift; sourceTree = "<group>"; };
173C0E822C123CF700654527 /* DrawCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawCircle.swift; sourceTree = "<group>"; };
174652CD2B33345200241CE6 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
177FB2402B31F9B900B11BA3 /* PermissionsRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsRequestView.swift; sourceTree = "<group>"; };
177FB24B2B31FF1E00B11BA3 /* ShortcutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -69,6 +71,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
173C0E812C123CE700654527 /* utils */ = {
isa = PBXGroup;
children = (
173C0E822C123CF700654527 /* DrawCircle.swift */,
);
path = utils;
sourceTree = "<group>";
};
17550A652B3351000058A94F /* windows */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -118,6 +128,7 @@
17550A6A2B3351A80058A94F /* src */ = {
isa = PBXGroup;
children = (
173C0E812C123CE700654527 /* utils */,
E2CABBA62B447B0A00D66D43 /* updates */,
E236331C2B44668300C0E61F /* preferences */,
17550A692B3351600058A94F /* app */,
Expand Down Expand Up @@ -271,6 +282,7 @@
177FB24C2B31FF1E00B11BA3 /* ShortcutView.swift in Sources */,
172E0BD52B32F36300D5CDA7 /* MouseTracker.swift in Sources */,
1781A5E42B31EE4A00F27910 /* AppView.swift in Sources */,
173C0E832C123CF700654527 /* DrawCircle.swift in Sources */,
E2CABBA52B447AEA00D66D43 /* UpdatesView.swift in Sources */,
177FB2532B321D0500B11BA3 /* ShortcutsManager.swift in Sources */,
E2E6F5EA2B3EF03C005B0D96 /* Constants.swift in Sources */,
Expand Down Expand Up @@ -414,7 +426,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 0.23.0;
CURRENT_PROJECT_VERSION = 0.24.0;
DEVELOPMENT_ASSET_PATHS = "\"Swift Shift/Preview Content\"";
DEVELOPMENT_TEAM = 2TZ4Q825M7;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -430,7 +442,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.24.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.pablopunk.Swift-Shift";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -448,7 +460,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 0.23.0;
CURRENT_PROJECT_VERSION = 0.24.0;
DEVELOPMENT_ASSET_PATHS = "\"Swift Shift/Preview Content\"";
DEVELOPMENT_TEAM = 2TZ4Q825M7;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -464,7 +476,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.24.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.pablopunk.Swift-Shift";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
139 changes: 111 additions & 28 deletions Swift Shift/src/mouse/MouseTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ enum MouseAction {
case none
}

enum Quadrant {
case topLeft, topRight, bottomLeft, bottomRight
}

class MouseTracker {
static let shared = MouseTracker()
private var mouseEventMonitor: Any?
Expand All @@ -18,37 +22,47 @@ class MouseTracker {
private var currentAction: MouseAction = .none
private var trackingTimer: Timer?
private let trackingTimeout: TimeInterval = 4 // in seconds

private var shouldUseQuadrants: Bool = false
private var quadrant: Quadrant?
private var windowSize: CGSize?

private init() {}

func startTracking(for action: MouseAction) {
prepareTracking(for: action)
registerMouseEventMonitor()
startTrackingTimer()
}

func stopTracking(for action: MouseAction) {
guard currentAction == action else { return }
invalidateTrackingTimer()
removeMouseEventMonitor()
resetTrackingVariables()
}



private func prepareTracking(for action: MouseAction) {
guard let currentWindow = WindowManager.getCurrentWindow(),
!shouldIgnore(window: currentWindow) else {
!shouldIgnore(window: currentWindow) else {
trackedWindow = nil
return
}

shouldFocusWindow = PreferencesManager.loadBool(for: .focusOnApp)
shouldUseQuadrants = PreferencesManager.loadBool(for: .useQuadrants)
trackedWindowIsFocused = false
currentAction = action
initialMouseLocation = NSEvent.mouseLocation
trackedWindow = currentWindow
initialWindowLocation = WindowManager.getPosition(window: currentWindow)
windowSize = WindowManager.getSize(window: currentWindow)

if action == .resize && shouldUseQuadrants, let initialMouseLocation = initialMouseLocation, let initialWindowLocation = initialWindowLocation, let windowSize = windowSize {
quadrant = determineQuadrant(mouseLocation: initialMouseLocation, windowSize: windowSize, windowLocation: initialWindowLocation)
}
}

private func shouldIgnore(window: AXUIElement) -> Bool {
guard let app = WindowManager.getNSApplication(from: window),
let bundleIdentifier = app.bundleIdentifier,
Expand All @@ -58,32 +72,53 @@ class MouseTracker {
print("Ignoring", bundleIdentifier)
return true
}


private func determineQuadrant(mouseLocation: NSPoint, windowSize: CGSize, windowLocation: NSPoint) -> Quadrant {
let bounds = WindowManager.getWindowBounds(windowLocation: windowLocation, windowSize: windowSize)

let midX = (bounds.topLeft.x + bounds.topRight.x) / 2
let midY = (bounds.topLeft.y + bounds.bottomLeft.y) / 2

let isLeft = mouseLocation.x < midX
let isTop = mouseLocation.y > midY

switch (isLeft, isTop) {
case (true, true):
return .topLeft
case (false, true):
return .topRight
case (true, false):
return .bottomLeft
case (false, false):
return .bottomRight
}
}

private func registerMouseEventMonitor() {
mouseEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in
self?.handleMouseMoved(event)
}
}

private func startTrackingTimer() {
trackingTimer?.invalidate()
trackingTimer = Timer.scheduledTimer(withTimeInterval: trackingTimeout, repeats: false) { [weak self] _ in
self?.stopTracking(for: self!.currentAction)
}
}

private func handleMouseMoved(_ event: NSEvent) {
guard let _ = initialMouseLocation,
let _ = initialWindowLocation,
let _ = trackedWindow else {
return
}

if shouldFocusWindow && !trackedWindowIsFocused {
WindowManager.focus(window: trackedWindow!)
trackedWindowIsFocused = true
}

switch currentAction {
case .move:
moveWindowBasedOnMouseLocation(event)
Expand All @@ -93,41 +128,89 @@ class MouseTracker {
break
}
}

private func moveWindowBasedOnMouseLocation(_ event: NSEvent) {
let currentMouseLocation = NSEvent.mouseLocation
let deltaX = currentMouseLocation.x - initialMouseLocation!.x
let deltaY = currentMouseLocation.y - initialMouseLocation!.y
let newOrigin = NSPoint(x: initialWindowLocation!.x + deltaX, y: initialWindowLocation!.y - deltaY)

WindowManager.move(window: trackedWindow!, to: newOrigin)
}


private func convertToWindowCoordinates(_ mouseLocation: NSPoint, windowOrigin: NSPoint) -> NSPoint {
return NSPoint(x: mouseLocation.x - windowOrigin.x, y: mouseLocation.y - windowOrigin.y)
}

private func resizeWindowBasedOnMouseLocation(_ event: NSEvent) {
let currentMouseLocation = NSEvent.mouseLocation

// Calculate the change in mouse location since tracking started
let deltaX = currentMouseLocation.x - initialMouseLocation!.x
let deltaY = currentMouseLocation.y - initialMouseLocation!.y

WindowManager.resize(window: trackedWindow!, deltaX: deltaX, deltaY: deltaY)

// Update initial mouse location for the next event
initialMouseLocation = currentMouseLocation

guard let windowSize = windowSize,
let initialMouseLocation = initialMouseLocation,
let initialWindowLocation = initialWindowLocation else {
return
}

var newWidth: CGFloat, newHeight: CGFloat, newOrigin: NSPoint

if (shouldUseQuadrants) {
guard let quadrant = quadrant else { return }

let currentMouseLocation = NSEvent.mouseLocation
let windowRelativeCurrentMouseLocation = convertToWindowCoordinates(currentMouseLocation, windowOrigin: initialWindowLocation)

let deltaX = windowRelativeCurrentMouseLocation.x - (initialMouseLocation.x - initialWindowLocation.x)
let deltaY = windowRelativeCurrentMouseLocation.y - (initialMouseLocation.y - initialWindowLocation.y)

newWidth = windowSize.width
newHeight = windowSize.height
newOrigin = initialWindowLocation

switch quadrant {
case .topLeft:
newWidth -= deltaX
newHeight += deltaY
newOrigin.x += deltaX
newOrigin.y -= deltaY
case .topRight:
newWidth += deltaX
newHeight += deltaY
newOrigin.y -= deltaY
case .bottomLeft:
newWidth -= deltaX
newHeight -= deltaY
newOrigin.x += deltaX
case .bottomRight:
newWidth += deltaX
newHeight -= deltaY
}
} else {
let currentMouseLocation = NSEvent.mouseLocation
let deltaX = currentMouseLocation.x - initialMouseLocation.x
let deltaY = currentMouseLocation.y - initialMouseLocation.y
newWidth = windowSize.width + deltaX
newHeight = windowSize.height - deltaY
newOrigin = initialWindowLocation
}

// Ensure the new width and height are not negative
newWidth = max(newWidth, 1)
newHeight = max(newHeight, 1)
let newSize = CGSize(width: newWidth, height: newHeight)
WindowManager.resize(window: trackedWindow!, to: newSize, from: newOrigin)
}

private func invalidateTrackingTimer() {
trackingTimer?.invalidate()
trackingTimer = nil
}

private func removeMouseEventMonitor() {
if let monitor = mouseEventMonitor {
NSEvent.removeMonitor(monitor)
mouseEventMonitor = nil
}
}

private func resetTrackingVariables() {
trackedWindow = nil
initialMouseLocation = nil
Expand Down
1 change: 1 addition & 0 deletions Swift Shift/src/preferences/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
enum PreferenceKey: String {
case focusOnApp = "focusOnApp"
case showMenuBarIcon = "showMenuBarIcon"
case useQuadrants = "useQuadrants"
}

class PreferencesManager {
Expand Down
6 changes: 6 additions & 0 deletions Swift Shift/src/preferences/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import LaunchAtLogin
struct PreferencesView: View {
@AppStorage(PreferenceKey.showMenuBarIcon.rawValue) var showMenuBarIcon = true
@AppStorage(PreferenceKey.focusOnApp.rawValue) var focusOnApp = true
@AppStorage(PreferenceKey.useQuadrants.rawValue) var useQuadrants = false

var body: some View {
VStack(alignment: .leading) {
Expand All @@ -19,6 +20,11 @@ struct PreferencesView: View {
Text("Focus on window")
Text("The window you're interacting with will gain focus")
}

Toggle(isOn: $useQuadrants) {
Text("Use quadrants")
Text("The resize action will happen from the corner that's closer to your mouse")
}
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions Swift Shift/src/utils/DrawCircle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Cocoa

class CircleView: NSView {
var fillColor: NSColor = .green // Default color is green

init(frame frameRect: NSRect, color: NSColor) {
super.init(frame: frameRect)
self.fillColor = color
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
fillColor.setFill()
let circlePath = NSBezierPath(ovalIn: dirtyRect)
circlePath.fill()
}
}

func drawCircleAt(x: CGFloat, y: CGFloat, diameter: CGFloat, color: NSColor) {
let circleWindow = NSWindow(contentRect: NSRect(x: x, y: y, width: diameter, height: diameter),
styleMask: .borderless,
backing: .buffered,
defer: false)
circleWindow.backgroundColor = .clear
circleWindow.isOpaque = false
circleWindow.hasShadow = false
circleWindow.ignoresMouseEvents = true
circleWindow.level = .floating

let circleView = CircleView(frame: NSRect(x: 0, y: 0, width: diameter, height: diameter), color: color)
circleWindow.contentView = circleView

circleWindow.makeKeyAndOrderFront(nil)
circleWindow.orderFrontRegardless() // Makes the window visible at all times
}
Loading

0 comments on commit 442e2e7

Please sign in to comment.