Skip to content

Commit

Permalink
Resize window, teleport mouse, and more.
Browse files Browse the repository at this point in the history
- Added a menu button to reset the window size to match the configured
resolution.
- Added a menu button to enable/disable the ability to teleport the mouse
cursor to the virtual display when clicking over the window.
- Added a menu button to enable/disable the ability to resize the window.
- Increased the maximum possible virtual display resolution to 2K monitors
(3840x2160).
- Added more standard resolutions.
- Removed the previous way to detect whether the mouse is in the virtual
display based on periodic polling.
- Added events to capture the mouse movement and when the window is clicked.
- Enhanced the MouseLocationState to include mouse-related characteristics as
the mouse location, the event generted by the mouse, as well as the screen
where the mouse is positioned, and flags to know the current mouse status
and position, which can be used to later enhance this application.
  • Loading branch information
Earl Ramirez Sanchez committed Nov 2, 2023
1 parent 2267a19 commit 38682a5
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 21 deletions.
81 changes: 80 additions & 1 deletion DeskPad/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,107 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window = NSWindow(contentViewController: viewController)
window.title = "DeskPad"
window.makeKeyAndOrderFront(nil)
window?.titlebarAppearsTransparent = true
window.titlebarAppearsTransparent = true
window.isMovableByWindowBackground = true
window.titleVisibility = .hidden
window.backgroundColor = .white
window.acceptsMouseMovedEvents = true
window.contentMinSize = CGSize(width: 400, height: 300)
window.contentMaxSize = CGSize(width: 3840, height: 2160)

let mainMenu = NSMenu()
let mainMenuItem = NSMenuItem()
let subMenu = NSMenu(title: "MainMenu")

// Menu item for isTeleportEnabled
let enableMouseTeleportMenuItem = NSMenuItem(
title: "Enable mouse teleport to virtual display on click",
action: #selector(toggleMouseTeleportEnable(_:)),
keyEquivalent: ""
)

if UserDefaults.standard.object(forKey: "isTeleportEnabled") == nil {
UserDefaults.standard.set(true, forKey: "isTeleportEnabled")
}

let isTeleportEnabled = UserDefaults.standard.bool(forKey: "isTeleportEnabled")
enableMouseTeleportMenuItem.state = isTeleportEnabled ? .on : .off

// Menu item for isWindowResizeAllowed
let enableWindowResizeMenuItem = NSMenuItem(
title: "Enable ability to resize the window",
action: #selector(toggleWindowResizeEnable(_:)),
keyEquivalent: ""
)

if UserDefaults.standard.object(forKey: "isWindowResizeAllowed") == nil {
UserDefaults.standard.set(true, forKey: "isWindowResizeAllowed")
}

let isWindowResizeAllowed = UserDefaults.standard.bool(forKey: "isWindowResizeAllowed")
enableWindowResizeMenuItem.state = isWindowResizeAllowed ? .on : .off

let resetWindowSizeMenuItem = NSMenuItem(
title: "Reset window size to match the original resolution",
action: #selector(resetWindowSize(_:)),
keyEquivalent: ""
)

// Menu item for quit application
let quitMenuItem = NSMenuItem(
title: "Quit",
action: #selector(NSApp.terminate),
keyEquivalent: "q"
)
subMenu.addItem(enableMouseTeleportMenuItem)
subMenu.addItem(enableWindowResizeMenuItem)
subMenu.addItem(resetWindowSizeMenuItem)
subMenu.addItem(quitMenuItem)
mainMenuItem.submenu = subMenu
mainMenu.items = [mainMenuItem]
NSApplication.shared.mainMenu = mainMenu

NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp]) {
store.dispatch(MouseLocationEvent.localMouseClicked(mouseLocation: NSEvent.mouseLocation, event: $0))
return $0
}

NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
store.dispatch(MouseLocationEvent.localMouseMoved(mouseLocation: NSEvent.mouseLocation, event: $0))
return $0
}

NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) {
store.dispatch(MouseLocationEvent.globalMouseMoved(mouseLocation: NSEvent.mouseLocation, event: $0))
}

store.dispatch(MouseLocationAction.setWindow(window: window))
store.dispatch(MouseLocationSettings.enableTeleport(isTeleportEnabled: isTeleportEnabled))
store.dispatch(MouseLocationSettings.enableWindowResize(isWindowResizeAllowed: isWindowResizeAllowed))
store.dispatch(AppDelegateAction.didFinishLaunching)
}

func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
return true
}

func applicationSupportsSecureRestorableState(_: NSApplication) -> Bool {
return true
}

@objc func toggleMouseTeleportEnable(_ sender: NSMenuItem) {
sender.state = (sender.state == .on) ? .off : .on
UserDefaults.standard.set(sender.state == .on, forKey: "isTeleportEnabled")
store.dispatch(MouseLocationSettings.enableTeleport(isTeleportEnabled: sender.state == .on))
}

@objc func toggleWindowResizeEnable(_ sender: NSMenuItem) {
sender.state = (sender.state == .on) ? .off : .on
UserDefaults.standard.set(sender.state == .on, forKey: "isWindowResizeAllowed")
store.dispatch(MouseLocationSettings.enableWindowResize(isWindowResizeAllowed: sender.state == .on))
}

@objc func resetWindowSize(_: NSMenuItem) {
store.dispatch(MouseLocationSettings.resetWindowSize)
}
}
98 changes: 87 additions & 11 deletions DeskPad/Backend/MouseLocation/MouseLocationSideEffect.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,98 @@
import Foundation
import ReSwift

private var timer: Timer?

enum MouseLocationAction: Action {
case located(isWithinScreen: Bool)
case located(isWithinVirtualDisplay: Bool)
case setWindow(window: NSWindow)
case update(screenContainingMouse: NSScreen?, screenContainingWindow: NSScreen?, screenVirtualDisplay: NSScreen?, isWithinVirtualDisplay: Bool, isWithinScreenContainingWindow: Bool, isWithinWindow: Bool, isWithinWindowContent: Bool, isWithinVirtualDisplayBorder: Bool)
}

enum MouseLocationSettings: Action {
case enableTeleport(isTeleportEnabled: Bool)
case enableWindowResize(isWindowResizeAllowed: Bool)
case resetWindowSize
}

enum MouseLocationEvent: Action {
case localMouseClicked(mouseLocation: NSPoint, event: NSEvent)
case localMouseMoved(mouseLocation: NSPoint, event: NSEvent)
case globalMouseMoved(mouseLocation: NSPoint, event: NSEvent)
}

func mouseLocationSideEffect() -> SideEffect {
return { _, dispatch, getState in
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { _ in
let mouseLocation = NSEvent.mouseLocation
let screens = NSScreen.screens
let screenContainingMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) })
let isWithinScreen = screenContainingMouse?.displayID == getState()?.screenConfigurationState.displayID
dispatch(MouseLocationAction.located(isWithinScreen: isWithinScreen))
return { action, dispatch, getState in
guard let appState = getState() else { return }
UserDefaults.standard.set(appState.mouseLocationState.isTeleportEnabled, forKey: "isTeleportEnabled")
UserDefaults.standard.set(appState.mouseLocationState.isWindowResizeAllowed, forKey: "isWindowResizeAllowed")
let event = appState.mouseLocationState.event
if event != nil, appState.screenConfigurationState.resolution != .zero {
let mouseLocation = appState.mouseLocationState.mouseLocation
let screens = NSScreen.screens
let window = appState.mouseLocationState.window
let screenContainingMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) })
let screenContainingWindow = (screens.first { NSPointInRect((window?.frame.origin)!, $0.frame) })
let screenVirtualDisplay = (screens.first { $0.displayID == appState.screenConfigurationState.displayID })
let isWithinVirtualDisplay = screenContainingMouse?.displayID == screenVirtualDisplay?.displayID
let isWithinScreenContainingWindow = screenContainingMouse?.displayID == screenContainingWindow?.displayID
let isWithinWindow = NSMouseInRect(mouseLocation, (window?.frame)!, false)
let isWithinWindowContent = NSMouseInRect((window?.mouseLocationOutsideOfEventStream)!, (window?.contentView?.frame)!, false)
// let isTeleportEnabled = appState.mouseLocationState.isTeleportEnabled

let borderWidth: CGFloat = 5 // Border width
let innerRect = NSInsetRect((screenVirtualDisplay?.frame)!, borderWidth, borderWidth)
let isWithinInnerRectangle = NSMouseInRect(mouseLocation, innerRect, false)
let isWithinVirtualDisplayBorder = isWithinVirtualDisplay && !isWithinInnerRectangle ? true : false

/* print("")
print("Mouse event")
print("event:", event)
print("windowNumber:", window?.windowNumber as Any)
print("screenContainingMouse:", screenContainingMouse?.localizedName as Any)
print("screenContainingWindow:", screenContainingWindow?.localizedName as Any)
print("screenVirtualDisplay:", screenVirtualDisplay?.localizedName as Any)
print("isWithinVirtualDisplay:", isWithinVirtualDisplay)
print("isWithinScreenContainingWindow:", isWithinScreenContainingWindow)
print("isWithinWindow:", isWithinWindow)
print("isWithinWindowContent:", isWithinWindowContent)
print("isWithinVirtualDisplayBorder:", isWithinVirtualDisplayBorder)
print("isTeleportEnabled:", isTeleportEnabled) */

dispatch(MouseLocationAction.update(screenContainingMouse: screenContainingMouse, screenContainingWindow: screenContainingWindow, screenVirtualDisplay: screenVirtualDisplay, isWithinVirtualDisplay: isWithinVirtualDisplay, isWithinScreenContainingWindow: isWithinScreenContainingWindow, isWithinWindow: isWithinWindow, isWithinWindowContent: isWithinWindowContent, isWithinVirtualDisplayBorder: isWithinVirtualDisplayBorder))
}

switch action {
case MouseLocationEvent.localMouseClicked:
// print("localClick:", mouseLocation)
if appState.mouseLocationState.isTeleportEnabled, appState.mouseLocationState.isWithinWindowContent {
teleportMouseToVirtualDisplay(state: appState.mouseLocationState)
}
case MouseLocationEvent.localMouseMoved:
// print("localMove:", mouseLocation)
do {}
case MouseLocationEvent.globalMouseMoved:
// print("globalMove:", mouseLocation)
do {}
case MouseLocationSettings.resetWindowSize:
appState.mouseLocationState.window?.contentAspectRatio = appState.screenConfigurationState.resolution
appState.mouseLocationState.window?.setContentSize(appState.screenConfigurationState.resolution)
appState.mouseLocationState.window?.center()
case MouseLocationSettings.enableWindowResize:
if appState.mouseLocationState.isWindowResizeAllowed {
appState.mouseLocationState.window?.styleMask = [.titled, .closable, .resizable, .miniaturizable]
} else {
appState.mouseLocationState.window?.styleMask = [.titled, .closable, .miniaturizable]
}
default:
return
}
}
}

func teleportMouseToVirtualDisplay(state: MouseLocationState) {
// Move the mouse to the virtual display
let newX = CGFloat(state.window!.mouseLocationOutsideOfEventStream.x * (state.screenVirtualDisplay!.frame.width / state.window!.contentView!.frame.size.width))
let newY = CGFloat(state.screenVirtualDisplay!.frame.height - state.window!.mouseLocationOutsideOfEventStream.y * (state.screenVirtualDisplay!.frame.height / state.window!.contentView!.frame.size.height))
let newPoint = NSPoint(x: newX, y: newY)
// print("newPoint:", newPoint)
CGDisplayMoveCursorToPoint(state.screenVirtualDisplay!.displayID, newPoint)
}
48 changes: 44 additions & 4 deletions DeskPad/Backend/MouseLocation/MouseLocationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,57 @@ import Foundation
import ReSwift

struct MouseLocationState: Equatable {
let isWithinScreen: Bool
let mouseLocation: NSPoint
let event: NSEvent?
let window: NSWindow?
let screenContainingMouse: NSScreen?
let screenContainingWindow: NSScreen?
let screenVirtualDisplay: NSScreen?
let isTeleportEnabled: Bool
let isWindowResizeAllowed: Bool
let isWithinVirtualDisplay: Bool
let isWithinScreenContainingWindow: Bool
let isWithinWindow: Bool
let isWithinWindowContent: Bool
let isWithinVirtualDisplayBorder: Bool

static var initialState: MouseLocationState {
return MouseLocationState(isWithinScreen: false)
return MouseLocationState(
mouseLocation: NSPoint.zero,
event: nil,
window: nil,
screenContainingMouse: nil,
screenContainingWindow: nil,
screenVirtualDisplay: nil,
isTeleportEnabled: UserDefaults.standard.bool(forKey: "isTeleportEnabled"),
isWindowResizeAllowed: UserDefaults.standard.bool(forKey: "isWindowResizeAllowed"),
isWithinVirtualDisplay: false,
isWithinScreenContainingWindow: false,
isWithinWindow: false,
isWithinWindowContent: false,
isWithinVirtualDisplayBorder: false
)
}
}

func mouseLocationReducer(action: Action, state: MouseLocationState) -> MouseLocationState {
switch action {
case let MouseLocationAction.located(isWithinScreen):
return MouseLocationState(isWithinScreen: isWithinScreen)
case let MouseLocationAction.update(screenContainingMouse: screenContainingMouse, screenContainingWindow: screenContainingWindow, screenVirtualDisplay: screenVirtualDisplay, isWithinVirtualDisplay: isWithinVirtualDisplay, isWithinScreenContainingWindow: isWithinScreenContainingWindow, isWithinWindow: isWithinWindow, isWithinWindowContent: isWithinWindowContent, isWithinVirtualDisplayBorder: isWithinVirtualDisplayBorder):
return MouseLocationState(mouseLocation: state.mouseLocation, event: nil, window: state.window, screenContainingMouse: screenContainingMouse, screenContainingWindow: screenContainingWindow, screenVirtualDisplay: screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: isWithinVirtualDisplay, isWithinScreenContainingWindow: isWithinScreenContainingWindow, isWithinWindow: isWithinWindow, isWithinWindowContent: isWithinWindowContent, isWithinVirtualDisplayBorder: isWithinVirtualDisplayBorder)
case let MouseLocationAction.located(isWithinVirtualDisplay):
return MouseLocationState(mouseLocation: state.mouseLocation, event: nil, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationAction.setWindow(window):
return MouseLocationState(mouseLocation: state.mouseLocation, event: nil, window: window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationSettings.enableTeleport(isTeleportEnabled):
return MouseLocationState(mouseLocation: state.mouseLocation, event: nil, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationSettings.enableWindowResize(isWindowResizeAllowed):
return MouseLocationState(mouseLocation: state.mouseLocation, event: nil, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationEvent.localMouseClicked(mouseLocation, event):
return MouseLocationState(mouseLocation: mouseLocation, event: event, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationEvent.localMouseMoved(mouseLocation, event):
return MouseLocationState(mouseLocation: mouseLocation, event: event, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)
case let MouseLocationEvent.globalMouseMoved(mouseLocation, event):
return MouseLocationState(mouseLocation: mouseLocation, event: event, window: state.window, screenContainingMouse: state.screenContainingMouse, screenContainingWindow: state.screenContainingWindow, screenVirtualDisplay: state.screenVirtualDisplay, isTeleportEnabled: state.isTeleportEnabled, isWindowResizeAllowed: state.isWindowResizeAllowed, isWithinVirtualDisplay: state.isWithinVirtualDisplay, isWithinScreenContainingWindow: state.isWithinScreenContainingWindow, isWithinWindow: state.isWithinWindow, isWithinWindowContent: state.isWithinWindowContent, isWithinVirtualDisplayBorder: state.isWithinVirtualDisplayBorder)

default:
return state
Expand Down
Loading

0 comments on commit 38682a5

Please sign in to comment.